UNPKG

@insforge/sdk

Version:

TypeScript SDK for InsForge Backend-as-a-Service platform

1,107 lines (1,098 loc) 31.8 kB
// src/types.ts var InsForgeError = class _InsForgeError extends Error { constructor(message, statusCode, error, nextActions) { super(message); this.name = "InsForgeError"; this.statusCode = statusCode; this.error = error; this.nextActions = nextActions; } static fromApiError(apiError) { return new _InsForgeError( apiError.message, apiError.statusCode, apiError.error, apiError.nextActions ); } }; // src/lib/http-client.ts var HttpClient = class { constructor(config) { this.userToken = null; this.baseUrl = config.baseUrl || "http://localhost:7130"; this.fetch = config.fetch || (globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0); this.anonKey = config.anonKey; this.defaultHeaders = { ...config.headers }; if (!this.fetch) { throw new Error( "Fetch is not available. Please provide a fetch implementation in the config." ); } } buildUrl(path, params) { const url = new URL(path, this.baseUrl); if (params) { Object.entries(params).forEach(([key, value]) => { if (key === "select") { let normalizedValue = value.replace(/\s+/g, " ").trim(); normalizedValue = normalizedValue.replace(/\s*\(\s*/g, "(").replace(/\s*\)\s*/g, ")").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s+(?=[^()]*\))/g, ","); url.searchParams.append(key, normalizedValue); } else { url.searchParams.append(key, value); } }); } return url.toString(); } async request(method, path, options = {}) { const { params, headers = {}, body, ...fetchOptions } = options; const url = this.buildUrl(path, params); const requestHeaders = { ...this.defaultHeaders }; const authToken = this.userToken || this.anonKey; if (authToken) { requestHeaders["Authorization"] = `Bearer ${authToken}`; } let processedBody; if (body !== void 0) { if (typeof FormData !== "undefined" && body instanceof FormData) { processedBody = body; } else { if (method !== "GET") { requestHeaders["Content-Type"] = "application/json;charset=UTF-8"; } processedBody = JSON.stringify(body); } } Object.assign(requestHeaders, headers); const response = await this.fetch(url, { method, headers: requestHeaders, body: processedBody, ...fetchOptions }); if (response.status === 204) { return void 0; } let data; const contentType = response.headers.get("content-type"); if (contentType?.includes("json")) { data = await response.json(); } else { data = await response.text(); } if (!response.ok) { if (data && typeof data === "object" && "error" in data) { if (!data.statusCode && !data.status) { data.statusCode = response.status; } const error = InsForgeError.fromApiError(data); Object.keys(data).forEach((key) => { if (key !== "error" && key !== "message" && key !== "statusCode") { error[key] = data[key]; } }); throw error; } throw new InsForgeError( `Request failed: ${response.statusText}`, response.status, "REQUEST_FAILED" ); } return data; } get(path, options) { return this.request("GET", path, options); } post(path, body, options) { return this.request("POST", path, { ...options, body }); } put(path, body, options) { return this.request("PUT", path, { ...options, body }); } patch(path, body, options) { return this.request("PATCH", path, { ...options, body }); } delete(path, options) { return this.request("DELETE", path, options); } setAuthToken(token) { this.userToken = token; } getHeaders() { const headers = { ...this.defaultHeaders }; const authToken = this.userToken || this.anonKey; if (authToken) { headers["Authorization"] = `Bearer ${authToken}`; } return headers; } }; // src/lib/token-manager.ts var TOKEN_KEY = "insforge-auth-token"; var USER_KEY = "insforge-auth-user"; var TokenManager = class { constructor(storage) { if (storage) { this.storage = storage; } else if (typeof window !== "undefined" && window.localStorage) { this.storage = window.localStorage; } else { const store = /* @__PURE__ */ new Map(); this.storage = { getItem: (key) => store.get(key) || null, setItem: (key, value) => { store.set(key, value); }, removeItem: (key) => { store.delete(key); } }; } } saveSession(session) { this.storage.setItem(TOKEN_KEY, session.accessToken); this.storage.setItem(USER_KEY, JSON.stringify(session.user)); } getSession() { const token = this.storage.getItem(TOKEN_KEY); const userStr = this.storage.getItem(USER_KEY); if (!token || !userStr) { return null; } try { const user = JSON.parse(userStr); return { accessToken: token, user }; } catch { this.clearSession(); return null; } } getAccessToken() { const token = this.storage.getItem(TOKEN_KEY); return typeof token === "string" ? token : null; } clearSession() { this.storage.removeItem(TOKEN_KEY); this.storage.removeItem(USER_KEY); } }; // src/modules/database-postgrest.ts import { PostgrestClient } from "@supabase/postgrest-js"; function createInsForgePostgrestFetch(httpClient, tokenManager) { return async (input, init) => { const url = typeof input === "string" ? input : input.toString(); const urlObj = new URL(url); const tableName = urlObj.pathname.slice(1); const insforgeUrl = `${httpClient.baseUrl}/api/database/records/${tableName}${urlObj.search}`; const token = tokenManager.getAccessToken(); const httpHeaders = httpClient.getHeaders(); const authToken = token || httpHeaders["Authorization"]?.replace("Bearer ", ""); const headers = new Headers(init?.headers); if (authToken && !headers.has("Authorization")) { headers.set("Authorization", `Bearer ${authToken}`); } const response = await fetch(insforgeUrl, { ...init, headers }); return response; }; } var Database = class { constructor(httpClient, tokenManager) { this.postgrest = new PostgrestClient("http://dummy", { fetch: createInsForgePostgrestFetch(httpClient, tokenManager), headers: {} }); } /** * Create a query builder for a table * * @example * // Basic query * const { data, error } = await client.database * .from('posts') * .select('*') * .eq('user_id', userId); * * // With count (Supabase style!) * const { data, error, count } = await client.database * .from('posts') * .select('*', { count: 'exact' }) * .range(0, 9); * * // Just get count, no data * const { count } = await client.database * .from('posts') * .select('*', { count: 'exact', head: true }); * * // Complex queries with OR * const { data } = await client.database * .from('posts') * .select('*, users!inner(*)') * .or('status.eq.active,status.eq.pending'); * * // All features work: * - Nested selects * - Foreign key expansion * - OR/AND/NOT conditions * - Count with head * - Range pagination * - Upserts */ from(table) { return this.postgrest.from(table); } }; // src/modules/auth.ts var Auth = class { constructor(http, tokenManager) { this.http = http; this.tokenManager = tokenManager; this.database = new Database(http, tokenManager); this.detectOAuthCallback(); } /** * Automatically detect and handle OAuth callback parameters in the URL * This runs on initialization to seamlessly complete the OAuth flow * Matches the backend's OAuth callback response (backend/src/api/routes/auth.ts:540-544) */ detectOAuthCallback() { if (typeof window === "undefined") return; try { const params = new URLSearchParams(window.location.search); const accessToken = params.get("access_token"); const userId = params.get("user_id"); const email = params.get("email"); const name = params.get("name"); if (accessToken && userId && email) { const session = { accessToken, user: { id: userId, email, name: name || "", // These fields are not provided by backend OAuth callback // They'll be populated when calling getCurrentUser() emailVerified: false, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() } }; this.tokenManager.saveSession(session); this.http.setAuthToken(accessToken); const url = new URL(window.location.href); url.searchParams.delete("access_token"); url.searchParams.delete("user_id"); url.searchParams.delete("email"); url.searchParams.delete("name"); if (params.has("error")) { url.searchParams.delete("error"); } window.history.replaceState({}, document.title, url.toString()); } } catch (error) { console.debug("OAuth callback detection skipped:", error); } } /** * Sign up a new user */ async signUp(request) { try { const response = await this.http.post("/api/auth/users", request); const session = { accessToken: response.accessToken, user: response.user }; this.tokenManager.saveSession(session); this.http.setAuthToken(response.accessToken); return { data: response, error: null }; } catch (error) { if (error instanceof InsForgeError) { return { data: null, error }; } return { data: null, error: new InsForgeError( error instanceof Error ? error.message : "An unexpected error occurred during sign up", 500, "UNEXPECTED_ERROR" ) }; } } /** * Sign in with email and password */ async signInWithPassword(request) { try { const response = await this.http.post("/api/auth/sessions", request); const session = { accessToken: response.accessToken, user: response.user }; this.tokenManager.saveSession(session); this.http.setAuthToken(response.accessToken); return { data: response, error: null }; } catch (error) { if (error instanceof InsForgeError) { return { data: null, error }; } return { data: null, error: new InsForgeError( "An unexpected error occurred during sign in", 500, "UNEXPECTED_ERROR" ) }; } } /** * Sign in with OAuth provider */ async signInWithOAuth(options) { try { const { provider, redirectTo, skipBrowserRedirect } = options; const params = redirectTo ? { redirect_uri: redirectTo } : void 0; const endpoint = `/api/auth/oauth/${provider}`; const response = await this.http.get(endpoint, { params }); if (typeof window !== "undefined" && !skipBrowserRedirect) { window.location.href = response.authUrl; return { data: {}, error: null }; } return { data: { url: response.authUrl, provider }, error: null }; } catch (error) { if (error instanceof InsForgeError) { return { data: {}, error }; } return { data: {}, error: new InsForgeError( "An unexpected error occurred during OAuth initialization", 500, "UNEXPECTED_ERROR" ) }; } } /** * Sign out the current user */ async signOut() { try { this.tokenManager.clearSession(); this.http.setAuthToken(null); return { error: null }; } catch (error) { return { error: new InsForgeError( "Failed to sign out", 500, "SIGNOUT_ERROR" ) }; } } /** * Get the current user with full profile information * Returns both auth info (id, email, role) and profile data (nickname, avatar_url, bio, etc.) */ async getCurrentUser() { try { const session = this.tokenManager.getSession(); if (!session?.accessToken) { return { data: null, error: null }; } this.http.setAuthToken(session.accessToken); const authResponse = await this.http.get("/api/auth/sessions/current"); const { data: profile, error: profileError } = await this.database.from("users").select("*").eq("id", authResponse.user.id).single(); if (profileError && profileError.code !== "PGRST116") { return { data: null, error: profileError }; } return { data: { user: authResponse.user, profile }, error: null }; } catch (error) { if (error instanceof InsForgeError && error.statusCode === 401) { await this.signOut(); return { data: null, error: null }; } if (error instanceof InsForgeError) { return { data: null, error }; } return { data: null, error: new InsForgeError( "An unexpected error occurred while fetching user", 500, "UNEXPECTED_ERROR" ) }; } } /** * Get any user's profile by ID * Returns profile information from the users table (nickname, avatar_url, bio, etc.) */ async getProfile(userId) { const { data, error } = await this.database.from("users").select("*").eq("id", userId).single(); if (error && error.code === "PGRST116") { return { data: null, error: null }; } return { data, error }; } /** * Get the current session (only session data, no API call) * Returns the stored JWT token and basic user info from local storage */ getCurrentSession() { try { const session = this.tokenManager.getSession(); if (session?.accessToken) { this.http.setAuthToken(session.accessToken); return { data: { session }, error: null }; } return { data: { session: null }, error: null }; } catch (error) { if (error instanceof InsForgeError) { return { data: { session: null }, error }; } return { data: { session: null }, error: new InsForgeError( "An unexpected error occurred while getting session", 500, "UNEXPECTED_ERROR" ) }; } } /** * Set/Update the current user's profile * Updates profile information in the users table (nickname, avatar_url, bio, etc.) */ async setProfile(profile) { const session = this.tokenManager.getSession(); if (!session?.user?.id) { return { data: null, error: new InsForgeError( "No authenticated user found", 401, "UNAUTHENTICATED" ) }; } const { data, error } = await this.database.from("users").update(profile).eq("id", session.user.id).select().single(); return { data, error }; } }; // src/modules/storage.ts var StorageBucket = class { constructor(bucketName, http) { this.bucketName = bucketName; this.http = http; } /** * Upload a file with a specific key * Uses the upload strategy from backend (direct or presigned) * @param path - The object key/path * @param file - File or Blob to upload */ async upload(path, file) { try { const strategyResponse = await this.http.post( `/api/storage/buckets/${this.bucketName}/upload-strategy`, { filename: path, contentType: file.type || "application/octet-stream", size: file.size } ); if (strategyResponse.method === "presigned") { return await this.uploadWithPresignedUrl(strategyResponse, file); } if (strategyResponse.method === "direct") { const formData = new FormData(); formData.append("file", file); const response = await this.http.request( "PUT", `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`, { body: formData, headers: { // Don't set Content-Type, let browser set multipart boundary } } ); return { data: response, error: null }; } throw new InsForgeError( `Unsupported upload method: ${strategyResponse.method}`, 500, "STORAGE_ERROR" ); } catch (error) { return { data: null, error: error instanceof InsForgeError ? error : new InsForgeError( "Upload failed", 500, "STORAGE_ERROR" ) }; } } /** * Upload a file with auto-generated key * Uses the upload strategy from backend (direct or presigned) * @param file - File or Blob to upload */ async uploadAuto(file) { try { const filename = file instanceof File ? file.name : "file"; const strategyResponse = await this.http.post( `/api/storage/buckets/${this.bucketName}/upload-strategy`, { filename, contentType: file.type || "application/octet-stream", size: file.size } ); if (strategyResponse.method === "presigned") { return await this.uploadWithPresignedUrl(strategyResponse, file); } if (strategyResponse.method === "direct") { const formData = new FormData(); formData.append("file", file); const response = await this.http.request( "POST", `/api/storage/buckets/${this.bucketName}/objects`, { body: formData, headers: { // Don't set Content-Type, let browser set multipart boundary } } ); return { data: response, error: null }; } throw new InsForgeError( `Unsupported upload method: ${strategyResponse.method}`, 500, "STORAGE_ERROR" ); } catch (error) { return { data: null, error: error instanceof InsForgeError ? error : new InsForgeError( "Upload failed", 500, "STORAGE_ERROR" ) }; } } /** * Internal method to handle presigned URL uploads */ async uploadWithPresignedUrl(strategy, file) { try { const formData = new FormData(); if (strategy.fields) { Object.entries(strategy.fields).forEach(([key, value]) => { formData.append(key, value); }); } formData.append("file", file); const uploadResponse = await fetch(strategy.uploadUrl, { method: "POST", body: formData }); if (!uploadResponse.ok) { throw new InsForgeError( `Upload to storage failed: ${uploadResponse.statusText}`, uploadResponse.status, "STORAGE_ERROR" ); } if (strategy.confirmRequired && strategy.confirmUrl) { const confirmResponse = await this.http.post( strategy.confirmUrl, { size: file.size, contentType: file.type || "application/octet-stream" } ); return { data: confirmResponse, error: null }; } return { data: { key: strategy.key, bucket: this.bucketName, size: file.size, mimeType: file.type || "application/octet-stream", uploadedAt: (/* @__PURE__ */ new Date()).toISOString(), url: this.getPublicUrl(strategy.key) }, error: null }; } catch (error) { throw error instanceof InsForgeError ? error : new InsForgeError( "Presigned upload failed", 500, "STORAGE_ERROR" ); } } /** * Download a file * Uses the download strategy from backend (direct or presigned) * @param path - The object key/path * Returns the file as a Blob */ async download(path) { try { const strategyResponse = await this.http.post( `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}/download-strategy`, { expiresIn: 3600 } ); const downloadUrl = strategyResponse.url; const headers = {}; if (strategyResponse.method === "direct") { Object.assign(headers, this.http.getHeaders()); } const response = await fetch(downloadUrl, { method: "GET", headers }); if (!response.ok) { try { const error = await response.json(); throw InsForgeError.fromApiError(error); } catch { throw new InsForgeError( `Download failed: ${response.statusText}`, response.status, "STORAGE_ERROR" ); } } const blob = await response.blob(); return { data: blob, error: null }; } catch (error) { return { data: null, error: error instanceof InsForgeError ? error : new InsForgeError( "Download failed", 500, "STORAGE_ERROR" ) }; } } /** * Get public URL for a file * @param path - The object key/path */ getPublicUrl(path) { return `${this.http.baseUrl}/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}`; } /** * List objects in the bucket * @param prefix - Filter by key prefix * @param search - Search in file names * @param limit - Maximum number of results (default: 100, max: 1000) * @param offset - Number of results to skip */ async list(options) { try { const params = {}; if (options?.prefix) params.prefix = options.prefix; if (options?.search) params.search = options.search; if (options?.limit) params.limit = options.limit.toString(); if (options?.offset) params.offset = options.offset.toString(); const response = await this.http.get( `/api/storage/buckets/${this.bucketName}/objects`, { params } ); return { data: response, error: null }; } catch (error) { return { data: null, error: error instanceof InsForgeError ? error : new InsForgeError( "List failed", 500, "STORAGE_ERROR" ) }; } } /** * Delete a file * @param path - The object key/path */ async remove(path) { try { const response = await this.http.delete( `/api/storage/buckets/${this.bucketName}/objects/${encodeURIComponent(path)}` ); return { data: response, error: null }; } catch (error) { return { data: null, error: error instanceof InsForgeError ? error : new InsForgeError( "Delete failed", 500, "STORAGE_ERROR" ) }; } } }; var Storage = class { constructor(http) { this.http = http; } /** * Get a bucket instance for operations * @param bucketName - Name of the bucket */ from(bucketName) { return new StorageBucket(bucketName, this.http); } }; // src/modules/ai.ts var AI = class { constructor(http) { this.http = http; this.chat = new Chat(http); this.images = new Images(http); } }; var Chat = class { constructor(http) { this.completions = new ChatCompletions(http); } }; var ChatCompletions = class { constructor(http) { this.http = http; } /** * Create a chat completion - OpenAI-like response format * * @example * ```typescript * // Non-streaming * const completion = await client.ai.chat.completions.create({ * model: 'gpt-4', * messages: [{ role: 'user', content: 'Hello!' }] * }); * console.log(completion.choices[0].message.content); * * // With images * const response = await client.ai.chat.completions.create({ * model: 'gpt-4-vision', * messages: [{ * role: 'user', * content: 'What is in this image?', * images: [{ url: 'https://example.com/image.jpg' }] * }] * }); * * // Streaming - returns async iterable * const stream = await client.ai.chat.completions.create({ * model: 'gpt-4', * messages: [{ role: 'user', content: 'Tell me a story' }], * stream: true * }); * * for await (const chunk of stream) { * if (chunk.choices[0]?.delta?.content) { * process.stdout.write(chunk.choices[0].delta.content); * } * } * ``` */ async create(params) { const backendParams = { model: params.model, messages: params.messages, temperature: params.temperature, maxTokens: params.maxTokens, topP: params.topP, stream: params.stream }; if (params.stream) { const headers = this.http.getHeaders(); headers["Content-Type"] = "application/json"; const response2 = await this.http.fetch( `${this.http.baseUrl}/api/ai/chat/completion`, { method: "POST", headers, body: JSON.stringify(backendParams) } ); if (!response2.ok) { const error = await response2.json(); throw new Error(error.error || "Stream request failed"); } return this.parseSSEStream(response2, params.model); } const response = await this.http.post( "/api/ai/chat/completion", backendParams ); const content = response.text || ""; return { id: `chatcmpl-${Date.now()}`, object: "chat.completion", created: Math.floor(Date.now() / 1e3), model: response.metadata?.model, choices: [ { index: 0, message: { role: "assistant", content }, finish_reason: "stop" } ], usage: response.metadata?.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }; } /** * Parse SSE stream into async iterable of OpenAI-like chunks */ async *parseSSEStream(response, model) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { const dataStr = line.slice(6).trim(); if (dataStr) { try { const data = JSON.parse(dataStr); if (data.chunk || data.content) { yield { id: `chatcmpl-${Date.now()}`, object: "chat.completion.chunk", created: Math.floor(Date.now() / 1e3), model, choices: [ { index: 0, delta: { content: data.chunk || data.content }, finish_reason: data.done ? "stop" : null } ] }; } if (data.done) { reader.releaseLock(); return; } } catch (e) { console.warn("Failed to parse SSE data:", dataStr); } } } } } } finally { reader.releaseLock(); } } }; var Images = class { constructor(http) { this.http = http; } /** * Generate images - OpenAI-like response format * * @example * ```typescript * // Text-to-image * const response = await client.ai.images.generate({ * model: 'dall-e-3', * prompt: 'A sunset over mountains', * }); * console.log(response.images[0].url); * * // Image-to-image (with input images) * const response = await client.ai.images.generate({ * model: 'stable-diffusion-xl', * prompt: 'Transform this into a watercolor painting', * images: [ * { url: 'https://example.com/input.jpg' }, * // or base64-encoded Data URI: * { url: 'data:image/jpeg;base64,/9j/4AAQ...' } * ] * }); * ``` */ async generate(params) { const response = await this.http.post( "/api/ai/image/generation", params ); let data = []; if (response.images && response.images.length > 0) { data = response.images.map((img) => ({ b64_json: img.imageUrl.replace(/^data:image\/\w+;base64,/, ""), content: response.text })); } else if (response.text) { data = [{ content: response.text }]; } return { created: Math.floor(Date.now() / 1e3), data, ...response.metadata?.usage && { usage: { total_tokens: response.metadata.usage.totalTokens || 0, input_tokens: response.metadata.usage.promptTokens || 0, output_tokens: response.metadata.usage.completionTokens || 0 } } }; } }; // src/modules/functions.ts var Functions = class { constructor(http) { this.http = http; } /** * Invokes an Edge Function * @param slug The function slug to invoke * @param options Request options */ async invoke(slug, options = {}) { try { const { method = "POST", body, headers = {} } = options; const path = `/functions/${slug}`; const data = await this.http.request( method, path, { body, headers } ); return { data, error: null }; } catch (error) { return { data: null, error // Pass through the full error object with all properties }; } } }; // src/client.ts var InsForgeClient = class { constructor(config = {}) { this.http = new HttpClient(config); this.tokenManager = new TokenManager(config.storage); if (config.edgeFunctionToken) { this.http.setAuthToken(config.edgeFunctionToken); this.tokenManager.saveSession({ accessToken: config.edgeFunctionToken, user: {} // Will be populated by getCurrentUser() }); } const existingSession = this.tokenManager.getSession(); if (existingSession?.accessToken) { this.http.setAuthToken(existingSession.accessToken); } this.auth = new Auth( this.http, this.tokenManager ); this.database = new Database(this.http, this.tokenManager); this.storage = new Storage(this.http); this.ai = new AI(this.http); this.functions = new Functions(this.http); } /** * Get the underlying HTTP client for custom requests * * @example * ```typescript * const httpClient = client.getHttpClient(); * const customData = await httpClient.get('/api/custom-endpoint'); * ``` */ getHttpClient() { return this.http; } /** * Future modules will be added here: * - database: Database operations * - storage: File storage operations * - functions: Serverless functions * - tables: Table management * - metadata: Backend metadata */ }; // src/index.ts function createClient(config) { return new InsForgeClient(config); } var index_default = InsForgeClient; export { AI, Auth, Database, Functions, HttpClient, InsForgeClient, InsForgeError, Storage, StorageBucket, TokenManager, createClient, index_default as default }; //# sourceMappingURL=index.mjs.map