UNPKG

@fine-dev/fine-js

Version:

Javascript client for Fine BaaS

286 lines (216 loc) 10.6 kB
# Fine SDK The Fine SDK is a powerful toolkit designed to simplify building full-stack web applications. It provides a unified interface for common application needs like authentication, database operations, file storage, and AI capabilities. ## Getting Started To use Fine SDK, you need to initialize it with the URL to your Fine backend server: ```javascript import { FineClient } from "@fine-dev/fine-js" const fine = new FineClient() ``` The Fine backend server needs to be set up separately. Follow the vibe-backend documentation (`@fine-dev/vibe-backend`) to deploy your own instance. ## Authentication Fine SDK provides authentication capabilities out of the box, available as `.auth` on `FineClient` instances. All parts of the SDK work with the authentication client to handle authorization. See the [BetterAuth docs](https://www.better-auth.com/docs/introduction) for more details about the client. ### Getting the session To get the user session, you may use the `.auth.useSession` React hook. `useSession` can ONLY be called inside of the body of a function component, just like any other react hook, and returns the following object: ```typescript ReturnType<FineClient["auth"]["useSession"]> = { data: { user: { id: string, name: string, email: string, createdAt: Date, updatedAt: Date, image: string | null }, session: { id: string, createdAt: Date, updatedAt: Date, userId: string, expiresAt: Date, token: string } } | null, isPending: boolean } ``` `isPending` indicates whether the user has been loaded. `isPending === false && data === null` indicates that the user is not logged in. ## Database and Persistence Fine SDK provides a simple interface for database operations. Using the database does not necessarily require authentication, however note that anonymous users only have read permissions. This means that if you implement data mutations, you will need to make sure that only authenticated users can access them. Example usage of the Fine SDK: ```javascript // Select tasks in a workspace with the given ids const tasks = await fine.table("tasks").select("id, description").eq("workspace", workspaceId).like("title", "Cook%") // Insert new tasks and fetch them const newTasks = await fine.table("tasks").insert(newTasks).select() // Update an existing task const updatedTasks, error = await fine.table("tasks").update(updates).eq("id", taskId).select() // Delete a task await fine.table("tasks").delete().eq("id", taskId) ``` Data pulled from the database will have JSON rows as strings, as this is how they are stored in the database. You will need to parse these before using them to actually access the JSON structure. Checking the user's authentication status before doing database actions is not required - the SDK will take care of this for you. ### D1RestClient Interface This is the underlying interface that powers the database functionality: ``` type Fetch = typeof fetch; export type GenericSchema = Record<string, Record<string, any>>; export default class D1RestClient<Tables extends GenericSchema = GenericSchema> { private baseUrl; private headers; fetch: Fetch; constructor({ baseUrl, headers, fetch: customFetch }: { baseUrl: string; headers?: Record<string, string>; fetch?: Fetch; }); table<TableName extends keyof Tables>(tableName: TableName): D1QueryBuilder<Tables, TableName>; } declare class D1QueryBuilder<Tables extends Record<string, any>, TableName extends keyof Tables> { url: URL; headers: Record<string, string>; fetch: Fetch; constructor(url: URL, { headers, fetch }: { headers?: Record<string, string>; fetch: Fetch; }); select(columns?: string): Omit<D1FilterBuilder<Tables[TableName][]>, "select">; insert(values: Tables[TableName] | Tables[TableName][]): D1FilterBuilder<Tables[TableName][]>; update(values: Partial<Tables[TableName]>): D1FilterBuilder<Tables[TableName][]>; delete(): D1FilterBuilder<Tables[TableName][]>; } declare class D1FilterBuilder<ResultType> { url: URL; headers: Record<string, string>; fetch: Fetch; method: "GET" | "POST" | "PATCH" | "DELETE"; body?: any; constructor({ url, headers, fetch, method, body }: { url: URL; headers: Record<string, string>; fetch: Fetch; method: "GET" | "POST" | "PATCH" | "DELETE"; body?: any; }); eq(column: string, value: any): this; neq(column: string, value: any): this; gt(column: string, value: any): this; lt(column: string, value: any): this; like(column: string, pattern: string): this; in(column: string, values: any[]): this; order(column: string, { ascending }?: { ascending?: boolean | undefined; }): this; limit(count: number): this; offset(count: number): this; select(columns?: string): this; then(resolve: (value: ResultType | null) => void, reject?: (reason?: any) => void): Promise<void>; } ``` ## File Storage Use Fine's storage client (`fine.storage`) whenever your application needs file storage capabilities, e.g. for user profile pictures, E-commerce product images, document management, etc. The storage client follows an entity-based approach to file storage: - Each file is associated with a specific entity (table row) in your database. - Files are referenced by an `EntityReference` which consists of: - `table`: The database table name - `id`: The unique ID of the record - `field`: The field/column name in that table that stores the filename - When a file is uploaded, the file name is updated automatically on the relevant entity to maintain referential integrity. Do not touch the related column, as it might break the application behavior. ### Usage ```javascript // An `EntityReference` is used to indicate which row and column in the database the file is connected to const entityRef = { table: "recipes", id: "recipe-123", field: "imageName" } // Upload a file. This will also add the file name to the relevant column in your data model. const fileInput = document.getElementById("fileInput").files[0] const metadata = { alt: "Chocolate cake recipe image", createdBy: "user-456" } // Metadata is optional await fine.storage.upload(entityRef, file, metadata, true) // Set isPublic (the 4th parameter) to `true` to make the file publicly accessible. // Get a URL for a file using the entity reference and filename const imageUrl = fine.storage.getDownloadUrl(entityRef, recipe.imageName) // You can use this URL in an image tag - it will work if the user has permission to fetch the row, or if the image is public <img src={imageUrl} /> // Trigger a file download await fine.storage.download(entityRef, recipe.imageName) // Delete a file await fine.storage.delete(entityRef, recipe.imageName) ``` ## AI Assistants Fine SDK provides AI assistant capabilities through `.ai` on `FineClient` instances. This allows you to define different system prompts for different purposes. ### Creating an assistant To create a new assistant, add a row to the `_ai_assistants` table in your Fine backend: - `id` - A slug-like ID that will be used by the SDK to determine which assistant to call. - `name` - The assistant's name. - `systemPrompt` - A system prompt that provides the agent with instructions on what it should and should not do. ### Using AI in your code `.ai` allows you to interact with the Fine AI backend with minimal boilerplate. Users need to be authenticated for the AI SDK to work. **Sending or streaming a message is the primary way to create threads and messages**. You will rarely need to call the methods that create threads or messages directly. #### 💬 Sending a Message (Streaming) This is the **main way** to interact with the system. When you stream a message, both the **message** and its **thread** (if needed) are created automatically. ```ts await client.message(assistantId, "Hello, world!").stream((event) => { switch (event.type) { case "runStarted": console.log(`Run started:`, event) break case "contentChunk": process.stdout.write(event.chunk) break case "runCompleted": console.log(`\nResponse complete:`, event.fullResponse) break case "runError": console.error(`Stream error:`, event.error) break } }) ``` You can also chain `.setMetadata()` to attach arbitrary metadata: ```ts await client .message(assistantId, "Hey there!") .setMetadata({ source: "homepage" }) .stream((event) => { if (event.type === "contentChunk") process.stdout.write(event.chunk) }) ``` If you don't need streaming, use `.send()` instead: ```ts const result = await client.message(assistantId, "Quick reply").setMetadata({ test: true }).send() if ("status" in result && result.status === "completed") { console.log("Response:", result.content) } else { console.error("Failed:", result) } ``` #### Image Uploads Fine's AI SDK makes it simple to attach images to your messages: 1. Obtain a list of `File` objects (or a single `File`). This is usually done with an input of type `file`, or with a drag event's `dataTransfer` property. 2. Chain an `.attach()` call to your message, passing it the files you obtained in step 1. The SDK will upload the files for you, and pass them on to the assistant - no need to do anything else! ```ts await client.message(assistantId, "What is this dish?").attach(files).send() ``` #### 🧵 Working with Threads You can fetch a user's threads easily: ```ts const threads = await client.threads for (const thread of threads) { console.log("Thread ID:", thread.id) } ``` This will only return threads belonging to the logged-in user, which is useful for chat-like interfaces. Reference an existing thread like so: ```ts const thread = client.thread("thread_123") ``` Stream a message into an existing thread: ```typescript await thread.message(assistantId, "Continue the story...").stream((event) => { if (event.type === "contentChunk") process.stdout.write(event.chunk) }) ``` #### Thread utilities - Fetch thread metadata: ```typescript const data = await thread.data ``` - Fetch all messages in a thread: ```typescript const messages = await thread.messages messages.forEach((msg) => { console.log(`[${msg.role}]`, msg.content) }) ``` - Update thread metadata: ```typescript await thread.update({ topic: "Customer Support" }) ``` - Delete a thread: ```typescript await thread.delete() ```