0.0.24 • Published 6 months ago

@fine-dev/fine-js v0.0.24

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

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:

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 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:

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:

// 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

// 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.

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:

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:

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!
await client.message(assistantId, "What is this dish?").attach(files).send()

🧵 Working with Threads

You can fetch a user's threads easily:

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:

const thread = client.thread("thread_123")

Stream a message into an existing thread:

await thread.message(assistantId, "Continue the story...").stream((event) => {
    if (event.type === "contentChunk") process.stdout.write(event.chunk)
})

Thread utilities

  • Fetch thread metadata:
const data = await thread.data
  • Fetch all messages in a thread:
const messages = await thread.messages
messages.forEach((msg) => {
    console.log(`[${msg.role}]`, msg.content)
})
  • Update thread metadata:
await thread.update({ topic: "Customer Support" })
  • Delete a thread:
await thread.delete()
0.0.24

6 months ago

0.0.23

6 months ago

0.0.22

6 months ago

0.0.21

6 months ago

0.0.20

6 months ago

0.0.19

6 months ago

0.0.18

7 months ago

0.0.17

7 months ago

0.0.16

7 months ago

0.0.15

7 months ago

0.0.14

7 months ago

0.0.13

7 months ago

0.0.12

7 months ago

0.0.11

8 months ago

0.0.10

8 months ago

0.0.9

8 months ago

0.0.8

8 months ago

0.0.7

8 months ago

0.0.6

8 months ago

0.0.5

8 months ago

0.0.4

8 months ago

0.0.3

8 months ago

0.0.2

8 months ago

0.0.1

9 months ago