diff --git a/.changeset/persistence-packages.md b/.changeset/persistence-packages.md index 3bfd0d152..a284760a0 100644 --- a/.changeset/persistence-packages.md +++ b/.changeset/persistence-packages.md @@ -10,7 +10,7 @@ Add server-side persistence for `chat()`: durable thread messages, run records, `withPersistence(persistence)` is a chat middleware that stores the conversation transcript, tracks each run's status, and records interrupt state so a paused run (tool approval, client-tool execution, generic interrupt) survives a server restart. Point it at any backend that implements the store contract: - `@tanstack/ai-persistence` — the store contracts, the `withPersistence` / `withGenerationPersistence` middleware, and an in-memory reference store (`memoryPersistence`) plus a conformance testkit. -- `@tanstack/ai-persistence-drizzle` — Drizzle-backed stores (SQLite via `node:sqlite`, plus a bring-your-own-schema contract) with migration and schema CLIs. +- `@tanstack/ai-persistence-drizzle` — Drizzle-backed stores for SQLite and Postgres behind one `drizzlePersistence(db, { provider, schema })` entry (plus a `node:sqlite` convenience factory), sharing one bring-your-own-schema contract. Get the tables into your drizzle-kit journal by re-exporting the `/sqlite-schema` or `/pg-schema` subpath (stock tables, tracks upgrades) or by emitting an owned starter with the dialect-aware schema CLI (renames, extra columns, index tuning). - `@tanstack/ai-persistence-prisma` — Prisma-backed stores with a models CLI that emits a provider-neutral schema fragment. Works with both Prisma 6 (`prisma-client-js`) and Prisma 7 (`prisma-client`): the client argument is typed structurally, so it accepts a client generated to any output path. - `@tanstack/ai-persistence-cloudflare` — D1-backed stores (delegating to the Drizzle backend) plus a Durable-Object lock store for cross-instance locking. diff --git a/docs/config.json b/docs/config.json index b39e2e398..1a571c9af 100644 --- a/docs/config.json +++ b/docs/config.json @@ -270,12 +270,14 @@ { "label": "SQL Backends", "to": "persistence/sql-backends", - "addedAt": "2026-07-22" + "addedAt": "2026-07-22", + "updatedAt": "2026-07-24" }, { "label": "Drizzle", "to": "persistence/drizzle", - "addedAt": "2026-07-22" + "addedAt": "2026-07-22", + "updatedAt": "2026-07-24" }, { "label": "Prisma", @@ -291,12 +293,14 @@ { "label": "Migrations", "to": "persistence/migrations", - "addedAt": "2026-07-22" + "addedAt": "2026-07-22", + "updatedAt": "2026-07-24" }, { "label": "Internals", "to": "persistence/internals", - "addedAt": "2026-07-22" + "addedAt": "2026-07-22", + "updatedAt": "2026-07-24" } ] }, diff --git a/docs/persistence/chat-persistence.md b/docs/persistence/chat-persistence.md index 752482407..ff06c2d3f 100644 --- a/docs/persistence/chat-persistence.md +++ b/docs/persistence/chat-persistence.md @@ -26,10 +26,9 @@ import { openaiText } from '@tanstack/ai-openai' import { withPersistence } from '@tanstack/ai-persistence' import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' -// One store for the whole process. `migrate: true` applies the bundled schema. +// One store for the whole process. Stock defaults + runtime table bootstrap. const persistence = sqlitePersistence({ url: 'file:.data/chat.sqlite', - migrate: true, }) export async function POST(request: Request) { @@ -55,9 +54,9 @@ The middleware uses whichever stores the backend provides, no feature flags: requires `runs`. - `locks` is handed to other middleware for cross-worker coordination. -`migrate: true` is convenient for local development. In production, apply the -bundled migrations through your deployment workflow instead. See -[Migrations](./migrations). +The `/sqlite` factory bootstraps stock tables for local development. In +production, emit a schema with `tanstack-ai-drizzle-schema` and migrate via +your own drizzle-kit journal. See [Migrations](./migrations). ## Send the full transcript, or none of it diff --git a/docs/persistence/client-persistence.md b/docs/persistence/client-persistence.md index 5b5867f70..b3160ff50 100644 --- a/docs/persistence/client-persistence.md +++ b/docs/persistence/client-persistence.md @@ -95,6 +95,7 @@ loader (or equivalent). See [Chat persistence](./chat-persistence). ```ts import { reconstructChat } from '@tanstack/ai-persistence' +import { persistence } from './persistence' import type { Scope } from '@tanstack/ai' export async function GET(request: Request) { @@ -162,7 +163,7 @@ and hand it to `useChat` as `initialResumeSnapshot`. A bare in-flight snapshot i rejoined just like a persisted pointer; a client that started the run still rejoins via its own pointer, so pass this only when the running client is a different one. -```tsx +```tsx ignore // Server: the loader (or your GET endpoint) reports the active run alongside history. loader: async () => ({ messages: await loadHistory(threadId), // reconstructChat, etc. diff --git a/docs/persistence/drizzle.md b/docs/persistence/drizzle.md index 806587aca..de393bc14 100644 --- a/docs/persistence/drizzle.md +++ b/docs/persistence/drizzle.md @@ -5,47 +5,194 @@ id: drizzle # Persistence with Drizzle -`@tanstack/ai-persistence-drizzle` supports SQLite-family Drizzle databases. -It has two entry points: +`@tanstack/ai-persistence-drizzle` is **schema-first**. It does not ship SQL +migrations. You own the schema file (or accept stock defaults), generate DDL +with **your** drizzle-kit journal, and pass the schema into the runtime. -- the package root accepts an already-created, migrated `DrizzleSqliteDb` and is +Two entry points: + +- the package root accepts an already-created, migrated Drizzle database plus + a required `provider` (`'sqlite'` or `'pg'`) and matching `schema`, and is safe to import in edge runtimes; -- `/sqlite` is a Node-only convenience factory built on `node:sqlite`. +- `/sqlite` is a Node-only convenience factory built on `node:sqlite` with stock + defaults and optional runtime table bootstrap for local/dev. + +The `provider` discriminates the whole call: with `provider: 'sqlite'` the +compiler only accepts a SQLite Drizzle database and a SQLite schema, with +`provider: 'pg'` only a Postgres database and schema, and the runtime assert +verifies the passed tables really are that dialect. + +## Schema first + +There are two ways to get the tables into **your** drizzle-kit journal: + +### Stock tables: re-export the package schema + +If you don't need to rename or extend the tables, don't copy anything — create +a one-line module the package keeps up to date across upgrades: + +```ts +// src/db/tanstack-ai-schema.ts +export * from '@tanstack/ai-persistence-drizzle/sqlite-schema' +``` + +Add that file to your drizzle-kit `schema` paths and generate migrations as +usual. When a package upgrade adds a column or index, `drizzle-kit generate` +picks it up automatically — there is no copied file to go stale. Use +`@tanstack/ai-persistence-drizzle/pg-schema` for Postgres. + +### Custom tables: emit an owned starter + +To rename tables or columns, add app-owned columns, or tune indexes, own the +definition instead. Emit a starter schema into your project: + +```bash +pnpm exec tanstack-ai-drizzle-schema --out src/db +``` + +Add it to drizzle-kit so **your** journal owns the DDL: + +```ts ignore +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + dialect: 'sqlite', + schema: ['./src/db/schema.ts', './src/db/tanstack-ai-schema.ts'], + out: './drizzle', +}) +``` + +```bash +pnpm exec drizzle-kit generate +pnpm exec drizzle-kit migrate +``` + +Then wire the runtime: + +```ts +import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle' +import { schema } from './db/tanstack-ai-schema' +import { db } from './db' + +export const persistence = drizzlePersistence(db, { + provider: 'sqlite', + schema, +}) +``` + +Because the runtime operates on the table objects you pass, the file is yours +to shape: + +- **Rename tables and columns**, or drop the explicit column names and rely on + your drizzle `casing` configuration — the stores read database names from + your objects, so the generated SQL follows your conventions. +- **Add app-owned columns** — for example a `userId` column on `messages` to + scope threads to users. Keep added columns nullable or defaulted so the + store inserts succeed; the TanStack AI stores never read or write them. +- **Tune indexes** — the starter ships lookup indexes on + `interrupts.thread_id` and `interrupts.run_id` (the columns the stores list + by); add composite or partial indexes as your query patterns demand. +- **Keep the contract columns** with their data shapes. The + `TanstackAiSqliteSchema` type enforces the shapes at compile time, and + `drizzlePersistence` validates the tables and columns exist at construction. + +## Node SQLite convenience + +For local development without a project schema file yet: + +```ts +import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' -There is no dialect option. For MySQL, PostgreSQL, or another Drizzle dialect, -implement the `AIPersistence` stores for that database or use the Prisma -backend. +export const persistence = sqlitePersistence({ + url: 'file:.tanstack-ai/state.sqlite', +}) +``` -## Node SQLite +This uses `createDefaultSqliteSchema()` and, by default, creates missing tables +with `CREATE TABLE IF NOT EXISTS` derived from that schema. That is a **bootstrap +convenience**, not a migration system — for production, emit the schema, migrate +with drizzle-kit, and pass your schema with `ensureTables: false`: ```ts import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' +import { schema } from './db/tanstack-ai-schema' export const persistence = sqlitePersistence({ url: 'file:.tanstack-ai/state.sqlite', - migrate: true, + schema, + ensureTables: false, }) ``` `url` may be `:memory:`, a filesystem path, or a `file:`-prefixed path. -`migrate: true` applies the bundled migrations before creating stores. Prefer -deployment-time migrations in production. ## Bring your own SQLite Drizzle database ```ts ignore import { drizzle } from 'drizzle-orm/d1' -import { drizzlePersistence, schema } from '@tanstack/ai-persistence-drizzle' +import { + createDefaultSqliteSchema, + drizzlePersistence, +} from '@tanstack/ai-persistence-drizzle' export function createPersistence(state: D1Database) { + const schema = createDefaultSqliteSchema() const db = drizzle(state, { schema }) - return drizzlePersistence(db) + return drizzlePersistence(db, { provider: 'sqlite', schema }) } ``` -The root entry does not import Node built-ins and works with Cloudflare D1 and -other SQLite-compatible Drizzle drivers. The application owns connection -lifecycle and migration timing. +Prefer emitting and owning the schema in production. The root entry does not +import Node built-ins and works with Cloudflare D1 and other SQLite-compatible +Drizzle drivers. The application owns connection lifecycle and migration timing. + +## Postgres + +Postgres uses the same root entry with `provider: 'pg'`: bring your own +migrated Drizzle Postgres database (node-postgres, postgres.js, Neon, +PGlite, …) and your schema. For stock tables, re-export +`@tanstack/ai-persistence-drizzle/pg-schema` as shown above; to own the +definition, emit the Postgres starter: + +```bash +pnpm exec tanstack-ai-drizzle-schema --out src/db --dialect pg +``` + +Add it to your drizzle-kit config (`dialect: 'postgresql'`), generate and run +migrations, then wire the runtime: + +```ts ignore +import { drizzle } from 'drizzle-orm/node-postgres' +import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle' +import { schema } from './db/tanstack-ai-schema' + +const db = drizzle(process.env.DATABASE_URL!) +export const persistence = drizzlePersistence(db, { provider: 'pg', schema }) +``` + +The same schema freedoms apply: rename tables and columns, lean on drizzle +`casing`, and add nullable or defaulted app-owned columns. The +`TanstackAiPgSchema` type enforces the contract's column data shapes at compile +time. For local development without migrations, `createDefaultPgSchema()` +provides the stock tables and `ensurePgTables` bootstraps them with +`CREATE TABLE IF NOT EXISTS`: + +```ts ignore +import { drizzle } from 'drizzle-orm/node-postgres' +import { pool } from './db' +import { + createDefaultPgSchema, + drizzlePersistence, + ensurePgTables, +} from '@tanstack/ai-persistence-drizzle' + +const schema = createDefaultPgSchema() +await ensurePgTables((sql) => pool.query(sql), schema) +export const persistence = drizzlePersistence(drizzle(pool), { + provider: 'pg', + schema, +}) +``` ## Use the middleware @@ -79,86 +226,3 @@ no distributed lock, so consumers that need one fall back to an in-process lock. When multiple workers must share a lock service, use `composePersistence` to add a distributed `locks` implementation — for example the Cloudflare Durable Object lock from `@tanstack/ai-persistence-cloudflare`. - -## Get the migrations - -The package exports the ordered `sqliteMigrations` manifest: - -```ts -import { sqliteMigrations } from '@tanstack/ai-persistence-drizzle' - -for (const migration of sqliteMigrations) { - console.log(migration.id, migration.filename) -} -``` - -Or copy canonical SQL files with the CLI: - -```bash -pnpm exec tanstack-ai-drizzle-migrations --out migrations/tanstack-ai -``` - -Use `--stdout` to print the SQL. The CLI refuses to replace a divergent file -unless `--force` is passed. Commit the copied files and apply them using your -normal SQLite, D1, or Drizzle deployment workflow. - -## Schema ownership - -The exported `schema` contains `messages`, `runs`, `interrupts`, and -`metadata`. Application tables may live beside these tables in the same -database. - -## Own the schema - -If your project already uses drizzle-kit, you can own the TanStack AI schema -outright instead of applying the bundled SQL. Emit the schema module into your -project: - -```bash -pnpm exec tanstack-ai-drizzle-schema --out src/db -``` - -This writes `src/db/tanstack-ai-schema.ts` — a regular Drizzle schema file that -imports from **your** installed `drizzle-orm`. The CLI refuses to replace a -divergent file unless `--force` is passed; `--stdout` prints the module instead. - -Add the file to your drizzle-kit schema paths so your own migration journal -owns the DDL: - -```ts ignore -import { defineConfig } from 'drizzle-kit' - -export default defineConfig({ - dialect: 'sqlite', - schema: ['./src/db/schema.ts', './src/db/tanstack-ai-schema.ts'], - out: './drizzle', -}) -``` - -Then pass the schema back so the runtime reads and writes through your copy: - -```ts -import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle' -import { schema } from './tanstack-ai-schema' -import { db } from './db' - -export const persistence = drizzlePersistence(db, { schema }) -``` - -Because the runtime operates on the table objects you pass, the file is truly -yours to shape: - -- **Rename tables and columns**, or drop the explicit column names and rely on - your drizzle `casing` configuration — the stores read database names from - your objects, so the generated SQL follows your conventions. -- **Add app-owned columns** — for example a `userId` column on `messages` to - scope threads to users. Keep added columns nullable or defaulted so the - store inserts succeed; the TanStack AI stores never read or write them. -- **Keep the contract columns** with their data shapes. The - `TanstackAiSqliteSchema` type enforces the shapes at compile time, and - `drizzlePersistence` validates the tables and columns exist at construction. - -When you own the schema this way, migrations flow entirely through your -drizzle-kit journal — package upgrades that change the schema surface as -drizzle-kit diffs when you update the emitted file. Don't mix this with the -bundled SQL migrations: pick one DDL owner per database. diff --git a/docs/persistence/internals.md b/docs/persistence/internals.md index 7aa96c4c7..10e93df63 100644 --- a/docs/persistence/internals.md +++ b/docs/persistence/internals.md @@ -95,8 +95,9 @@ explicitly widened types can bypass static guarantees. Packaged backends own resources differently: -- Drizzle accepts a migrated SQLite-family database; its root import is - edge-safe. The `/sqlite` entry creates a Node SQLite connection. +- Drizzle is schema-first: pass a required `schema` (emit via CLI; your + drizzle-kit owns migrations). The root import is edge-safe. The `/sqlite` + entry creates a Node SQLite connection with stock defaults. - Prisma accepts the application's generated and migrated client. - Cloudflare maps D1 to structured stores and Durable Objects to locks. diff --git a/docs/persistence/migrations.md b/docs/persistence/migrations.md index f68d64f1b..5ad5d9077 100644 --- a/docs/persistence/migrations.md +++ b/docs/persistence/migrations.md @@ -10,45 +10,50 @@ deploying code that reads or writes the corresponding stores. ## Drizzle SQLite -The Drizzle package bundles ordered canonical SQLite migrations. Copy them into -your repository: +The Drizzle package is **schema-first** and does **not** ship SQL migrations. +Own the schema in your project, then generate DDL with drizzle-kit: ```bash -pnpm exec tanstack-ai-drizzle-migrations --out migrations/tanstack-ai +pnpm exec tanstack-ai-drizzle-schema --out src/db ``` -Or print the ordered SQL: +Point drizzle-kit at the emitted file, generate, and migrate: -```bash -pnpm exec tanstack-ai-drizzle-migrations --stdout +```ts ignore +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + dialect: 'sqlite', + schema: ['./src/db/schema.ts', './src/db/tanstack-ai-schema.ts'], + out: './drizzle', +}) ``` -The CLI preserves existing identical files and refuses to overwrite divergent -files without `--force`. Apply the copied SQL using your normal SQLite, -Drizzle, or D1 deployment process. +```bash +pnpm exec drizzle-kit generate +pnpm exec drizzle-kit migrate +``` -For local Node development, the `/sqlite` factory can apply the same manifest: +Pass the schema into the runtime: ```ts -import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' +import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle' +import { schema } from './db/tanstack-ai-schema' +import { db } from './db' -const persistence = sqlitePersistence({ - url: 'file:.tanstack-ai/state.sqlite', - migrate: true, +export const persistence = drizzlePersistence(db, { + provider: 'sqlite', + schema, }) ``` -Avoid request-time migrations in production. - -Projects that already run drizzle-kit can skip the bundled SQL entirely: emit -the schema module with `tanstack-ai-drizzle-schema`, add it to your drizzle-kit -schema paths, and let your own journal generate the DDL. See -[Own the schema](./drizzle#own-the-schema). Pick one DDL owner per database — -bundled SQL or your journal, not both. +For local Node development without a kit journal yet, the `/sqlite` factory can +bootstrap stock tables at runtime (`ensureTables`, default `true`). That is not +a migration system — see [Drizzle](./drizzle). ## Cloudflare D1 -The Cloudflare package provides D1-specific migration assets: +The Cloudflare package provides D1-specific migration assets for Wrangler: ```bash pnpm exec tanstack-ai-cloudflare-migrations --out migrations @@ -83,9 +88,9 @@ TanStack AI does not inspect your table layout. ## Upgrade discipline -1. Read the package release notes for schema changes. -2. Refresh copied assets in a reviewable branch. -3. Inspect the diff rather than using `--force` blindly. +1. Read the package release notes for schema contract changes. +2. Refresh emitted schema / models fragments in a reviewable branch. +3. Inspect the drizzle-kit or Prisma diff rather than forcing overwrites. 4. Back up production state where required. 5. Apply migrations before deploying code that depends on them. 6. Keep rollback and partial-deployment behavior explicit. diff --git a/docs/persistence/overview.md b/docs/persistence/overview.md index e3b543621..651521d21 100644 --- a/docs/persistence/overview.md +++ b/docs/persistence/overview.md @@ -70,7 +70,6 @@ import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' const persistence = sqlitePersistence({ url: 'file:.tanstack-ai/state.sqlite', - migrate: true, }) export async function POST(request: Request) { @@ -170,7 +169,6 @@ import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' const persistence = sqlitePersistence({ url: 'file:.tanstack-ai/state.sqlite', - migrate: true, }) export async function POST(request: Request) { diff --git a/docs/persistence/sql-backends.md b/docs/persistence/sql-backends.md index 0e783907a..56c254fa7 100644 --- a/docs/persistence/sql-backends.md +++ b/docs/persistence/sql-backends.md @@ -10,12 +10,12 @@ models. | Adapter | Database support | Connection ownership | Schema workflow | | --- | --- | --- | --- | -| `@tanstack/ai-persistence-drizzle` | SQLite-family only | Bring a migrated Drizzle DB, or use Node `/sqlite` | Bundled SQLite migration manifest and CLI | +| `@tanstack/ai-persistence-drizzle` | SQLite-family | Bring a migrated Drizzle DB, or use Node `/sqlite` | Emit schema via CLI; your drizzle-kit owns migrations | | `@tanstack/ai-persistence-prisma` | Providers supported by your Prisma schema | Bring your generated `PrismaClient` | Copy models fragment, then use Prisma migrate | -The Drizzle adapter does not accept a dialect selector. Its schema and stores -use SQLite APIs. For a non-SQLite Drizzle database, implement the public -`AIPersistence` store interfaces for that dialect. +The Drizzle adapter does not ship SQL migrations. Pass a required `schema` to +`drizzlePersistence`. For a non-SQLite Drizzle database, implement the public +`AIPersistence` store interfaces for that dialect (or use Prisma). ## Local SQLite @@ -24,23 +24,33 @@ import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' export const persistence = sqlitePersistence({ url: 'file:.tanstack-ai/state.sqlite', - migrate: true, }) ``` +Uses the default schema and runtime table bootstrap. For production, emit the +schema, migrate with drizzle-kit, and pass `{ schema, ensureTables: false }`. + ## Existing SQLite or D1 Drizzle database ```ts ignore import { drizzle } from 'drizzle-orm/d1' -import { drizzlePersistence, schema } from '@tanstack/ai-persistence-drizzle' +import { + createDefaultSqliteSchema, + drizzlePersistence, +} from '@tanstack/ai-persistence-drizzle' export function createPersistence(state: D1Database) { - const db = drizzle(state, { schema }) - return drizzlePersistence(db) + const schema = createDefaultSqliteSchema() + return drizzlePersistence(drizzle(state, { schema }), { + provider: 'sqlite', + schema, + }) } ``` -The package root is edge-safe; `/sqlite` is Node-only. +The package root is edge-safe; `/sqlite` is Node-only. Prefer a project-owned +schema from `tanstack-ai-drizzle-schema` over the default factory when you run +drizzle-kit. ## Prisma diff --git a/examples/ts-react-chat/src/lib/persistent-chat-store.ts b/examples/ts-react-chat/src/lib/persistent-chat-store.ts index 63bd41022..3405c3ebe 100644 --- a/examples/ts-react-chat/src/lib/persistent-chat-store.ts +++ b/examples/ts-react-chat/src/lib/persistent-chat-store.ts @@ -10,13 +10,16 @@ let instance: ReturnType | undefined * the API route (POST writes the transcript, GET replays / reconstructs it) and * the history server function the page loader calls. Lazily opened so importing * this module (e.g. from a server-fn module that a client route also imports) - * never opens the database in the browser bundle. `migrate: true` applies the - * bundled TanStack AI schema on first open. `.data/` is gitignored. + * never opens the database in the browser bundle. + * + * Uses the stock default schema and runtime table bootstrap (not a shipped + * migration journal). Production apps should emit a schema with + * `tanstack-ai-drizzle-schema` and migrate via their own drizzle-kit journal. + * `.data/` is gitignored. */ export function persistentChatPersistence() { return (instance ??= sqlitePersistence({ url: './.data/persistent-chat.db', - migrate: true, })) } diff --git a/examples/ts-react-chat/src/lib/server-fns.ts b/examples/ts-react-chat/src/lib/server-fns.ts index 7bcd2ca9c..c2f912537 100644 --- a/examples/ts-react-chat/src/lib/server-fns.ts +++ b/examples/ts-react-chat/src/lib/server-fns.ts @@ -23,7 +23,7 @@ import { openaiText, openaiVideo, } from '@tanstack/ai-openai' -import type { UIMessage } from '@tanstack/ai' +import type { ToolCallState, ToolResultState, UIMessage } from '@tanstack/ai' import { InvalidModelOverrideError, UnknownProviderError, @@ -441,18 +441,89 @@ export const chatFn = createServerFn({ method: 'POST' }) * hydrated snapshot. Undefined once the run finishes (the transcript then holds * the complete reply). */ +/** + * Server functions require a fully serializable payload, but several + * `UIMessage` part fields are typed `unknown`/`any` (part `metadata`, + * tool-call `input`/`output`). Project the hydrated history onto an explicit + * JSON-safe wire shape: keep the fields the page renders, stringify non-string + * tool-result content, and drop media parts (this thread never produces + * them). Every wire part remains assignable to its `UIMessage` counterpart, + * so the result seeds `useChat({ initialMessages })` unchanged. + */ +interface WireMessage { + id: string + role: UIMessage['role'] + parts: Array< + | { type: 'text'; content: string } + | { type: 'thinking'; content: string } + | { + type: 'tool-call' + id: string + name: string + arguments: string + state: ToolCallState + } + | { + type: 'tool-result' + toolCallId: string + content: string + state: ToolResultState + } + > + createdAt?: Date +} + +function toSerializableMessages( + messages: Array, +): Array { + return messages.map(({ id, role, parts, createdAt }) => ({ + id, + role, + parts: parts.flatMap((part): WireMessage['parts'] => { + switch (part.type) { + case 'text': + return [{ type: 'text', content: part.content }] + case 'thinking': + return [{ type: 'thinking', content: part.content }] + case 'tool-call': + return [ + { + type: 'tool-call', + id: part.id, + name: part.name, + arguments: part.arguments, + state: part.state, + }, + ] + case 'tool-result': + return [ + { + type: 'tool-result', + toolCallId: part.toolCallId, + content: + typeof part.content === 'string' + ? part.content + : JSON.stringify(part.content), + state: part.state, + }, + ] + default: + return [] + } + }), + ...(createdAt ? { createdAt } : {}), + })) +} + export const loadPersistentChatHistoryFn = createServerFn().handler( - async (): Promise<{ - messages: Array - activeRunId: string | undefined - }> => { + async () => { const persistence = persistentChatPersistence() const stored = (await persistence.stores.messages?.loadThread( PERSISTENT_CHAT_THREAD_ID, )) ?? [] return { - messages: modelMessagesToUIMessages(stored), + messages: toSerializableMessages(modelMessagesToUIMessages(stored)), activeRunId: activeRunForThread(PERSISTENT_CHAT_THREAD_ID), } }, diff --git a/examples/ts-react-chat/src/routes/api.persistent-chat.ts b/examples/ts-react-chat/src/routes/api.persistent-chat.ts index 68fd13730..14e83e090 100644 --- a/examples/ts-react-chat/src/routes/api.persistent-chat.ts +++ b/examples/ts-react-chat/src/routes/api.persistent-chat.ts @@ -61,7 +61,7 @@ const rollDice = toolDefinition({ rolls: z.array(z.number()), total: z.number(), }), -}).server(({ sides, count }) => { +}).server(({ sides = 6, count = 1 }) => { const rolls = Array.from( { length: count }, () => Math.floor(Math.random() * sides) + 1, diff --git a/packages/ai-client/tests/resume-snapshot.test.ts b/packages/ai-client/tests/resume-snapshot.test.ts index 2ac27c785..b29f81b77 100644 --- a/packages/ai-client/tests/resume-snapshot.test.ts +++ b/packages/ai-client/tests/resume-snapshot.test.ts @@ -195,11 +195,12 @@ describe('normalizeConnectionAdapter joinRun passthrough', () => { }) it('omits joinRun when the property is present but not a function', () => { - const normalized = normalizeConnectionAdapter({ - connect: async function* () {}, - // Explicit undefined must not produce a wrapper that throws on rejoin. - joinRun: undefined, - } as ResumableConnectConnectionAdapter) + // Explicit undefined must not produce a wrapper that throws on rejoin. + // Object.assign sidesteps the literal excess-property check to model a JS + // caller passing `joinRun: undefined` against the typed interface. + const normalized = normalizeConnectionAdapter( + Object.assign({ connect: async function* () {} }, { joinRun: undefined }), + ) expect(normalized.joinRun).toBeUndefined() }) }) @@ -582,11 +583,9 @@ describe('ChatClient auto-rejoin after reload', () => { if (stored && !Array.isArray(stored)) { expect(stored.resume).toBeUndefined() } else { - // messages may still be present without resume, or key removed only if - // messages were also empty — with cached messages, record stays without resume. - expect( - stored && !Array.isArray(stored) ? stored.resume : undefined, - ).toBe(undefined) + // A bare message array (or a removed key) carries no resume pointer by + // construction — nothing further to assert on the record shape. + expect(stored === undefined || Array.isArray(stored)).toBe(true) } }) void client diff --git a/packages/ai-persistence-cloudflare/src/d1.ts b/packages/ai-persistence-cloudflare/src/d1.ts index 011e70ce4..c274dced4 100644 --- a/packages/ai-persistence-cloudflare/src/d1.ts +++ b/packages/ai-persistence-cloudflare/src/d1.ts @@ -1,9 +1,21 @@ import { drizzle } from 'drizzle-orm/d1' -import { drizzlePersistence, schema } from '@tanstack/ai-persistence-drizzle' +import { + createDefaultSqliteSchema, + drizzlePersistence, +} from '@tanstack/ai-persistence-drizzle' -/** Create the structured stores owned by a migrated Cloudflare D1 binding. */ +/** + * Create the structured stores owned by a migrated Cloudflare D1 binding. + * + * Apply this package's D1 migrations (or equivalent DDL matching the default + * schema) before use — the drizzle adapter does not ship or apply migrations. + */ export function createD1Stores(d1: D1Database) { - const persistence = drizzlePersistence(drizzle(d1, { schema })) + const schema = createDefaultSqliteSchema() + const persistence = drizzlePersistence(drizzle(d1, { schema }), { + provider: 'sqlite', + schema, + }) return { messages: persistence.stores.messages, runs: persistence.stores.runs, diff --git a/packages/ai-persistence-cloudflare/src/index.ts b/packages/ai-persistence-cloudflare/src/index.ts index f1661ac18..0479a4616 100644 --- a/packages/ai-persistence-cloudflare/src/index.ts +++ b/packages/ai-persistence-cloudflare/src/index.ts @@ -4,11 +4,11 @@ import type { AIPersistence, AIPersistenceStores, InterruptStore, + LockStore, MessageStore, MetadataStore, RunStore, } from '@tanstack/ai-persistence' -import type { LockStore } from '@tanstack/ai-persistence' import type { DurableObjectLockStoreOptions } from './locks' export { createD1Stores } from './d1' diff --git a/packages/ai-persistence-cloudflare/src/migrations.ts b/packages/ai-persistence-cloudflare/src/migrations.ts index 0d24c5b49..780ef6195 100644 --- a/packages/ai-persistence-cloudflare/src/migrations.ts +++ b/packages/ai-persistence-cloudflare/src/migrations.ts @@ -7,15 +7,13 @@ export interface D1Migration { } /** - * PROVENANCE: `assets/0000_tanstack_ai_initial.sql` is a byte-for-byte copy of - * the Drizzle asset - * (`@tanstack/ai-persistence-drizzle`'s `src/assets/0000_tanstack_ai_initial.sql`). - * `createD1Stores` runs the Drizzle stores against a D1 database migrated with - * this SQL, so the copy MUST stay identical to Drizzle's. The test "cloudflare - * D1 asset matches the drizzle asset" (`tests/migrations.test.ts`) fails on any - * drift. + * Ordered D1 migrations for messages, runs, interrupts, and metadata. + * + * Owned by this package for Wrangler D1 deploy workflows. Schema evolution for + * non-Cloudflare apps goes through the app's own drizzle-kit journal after + * emitting a schema with `tanstack-ai-drizzle-schema` — the drizzle package + * does not ship SQL migrations. */ -/** Ordered D1 migrations for messages, runs, interrupts, and metadata. */ export const d1Migrations: ReadonlyArray = [ { id: '0000_tanstack_ai_initial', diff --git a/packages/ai-persistence-cloudflare/tests/migrations.test.ts b/packages/ai-persistence-cloudflare/tests/migrations.test.ts index f2ddeeed6..675a6b9d5 100644 --- a/packages/ai-persistence-cloudflare/tests/migrations.test.ts +++ b/packages/ai-persistence-cloudflare/tests/migrations.test.ts @@ -3,10 +3,6 @@ import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { d1Migrations } from '../src/index' -async function readAsset(url: string): Promise { - return readFile(fileURLToPath(new URL(url, import.meta.url)), 'utf8') -} - describe('D1 migrations', () => { it('exports the canonical structured-state migration', () => { expect(d1Migrations).toHaveLength(1) @@ -37,17 +33,4 @@ describe('D1 migrations', () => { expect(published).toBe(embedded) expect(d1Migrations[0]?.sql).toBe(embedded) }) - - // `createD1Stores` runs the Drizzle stores against a D1 database migrated - // with this copy of the Drizzle migration, so it must stay byte-for-byte - // identical to the Drizzle asset. Drift breaks the stores at runtime. - it('cloudflare D1 asset matches the drizzle asset', async () => { - const cloudflareSql = await readAsset( - '../src/assets/0000_tanstack_ai_initial.sql', - ) - const drizzleSql = await readAsset( - '../../ai-persistence-drizzle/src/assets/0000_tanstack_ai_initial.sql', - ) - expect(cloudflareSql).toBe(drizzleSql) - }) }) diff --git a/packages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-migrations.mjs b/packages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-migrations.mjs deleted file mode 100755 index 0a4de6588..000000000 --- a/packages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-migrations.mjs +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -import '../dist/esm/cli.js' diff --git a/packages/ai-persistence-drizzle/drizzle.config.ts b/packages/ai-persistence-drizzle/drizzle.config.ts deleted file mode 100644 index a3602a3d5..000000000 --- a/packages/ai-persistence-drizzle/drizzle.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'drizzle-kit' - -/** - * drizzle-kit config for the SQLite schema. Migrations are generated from - * `src/schema.ts` into `drizzle/`, shipped with the package, exposed through - * `sqliteMigrations`, and applied by the Node-only `sqlitePersistence` factory. - * - * Regenerate with `pnpm db:generate` after any schema change. `sqliteMigrations` - * and the D1 sibling load the DUPLICATE at `src/assets/0000_tanstack_ai_initial.sql`, - * which `db:generate` does NOT update — copy the regenerated - * `drizzle/0000_tanstack_ai_initial.sql` over it (the package-contract test - * "keeps the shipped drizzle-kit migration equal to the embedded asset" guards - * the two against drift). - */ -export default defineConfig({ - dialect: 'sqlite', - schema: './src/schema.ts', - out: './drizzle', -}) diff --git a/packages/ai-persistence-drizzle/drizzle/0000_tanstack_ai_initial.sql b/packages/ai-persistence-drizzle/drizzle/0000_tanstack_ai_initial.sql deleted file mode 100644 index 3e5570ec1..000000000 --- a/packages/ai-persistence-drizzle/drizzle/0000_tanstack_ai_initial.sql +++ /dev/null @@ -1,32 +0,0 @@ -CREATE TABLE `interrupts` ( - `interrupt_id` text PRIMARY KEY NOT NULL, - `run_id` text NOT NULL, - `thread_id` text NOT NULL, - `status` text NOT NULL, - `requested_at` integer NOT NULL, - `resolved_at` integer, - `payload_json` text NOT NULL, - `response_json` text -); ---> statement-breakpoint -CREATE TABLE `messages` ( - `thread_id` text PRIMARY KEY NOT NULL, - `messages_json` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `metadata` ( - `scope` text NOT NULL, - `key` text NOT NULL, - `value_json` text NOT NULL, - PRIMARY KEY(`scope`, `key`) -); ---> statement-breakpoint -CREATE TABLE `runs` ( - `run_id` text PRIMARY KEY NOT NULL, - `thread_id` text NOT NULL, - `status` text NOT NULL, - `started_at` integer NOT NULL, - `finished_at` integer, - `error` text, - `usage_json` text -); diff --git a/packages/ai-persistence-drizzle/drizzle/meta/0000_snapshot.json b/packages/ai-persistence-drizzle/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 2f24bec81..000000000 --- a/packages/ai-persistence-drizzle/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,203 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "7c2d61ca-85d3-486d-be8c-ff358edba179", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "interrupts": { - "name": "interrupts", - "columns": { - "interrupt_id": { - "name": "interrupt_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "run_id": { - "name": "run_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "thread_id": { - "name": "thread_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "requested_at": { - "name": "requested_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "response_json": { - "name": "response_json", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "messages": { - "name": "messages", - "columns": { - "thread_id": { - "name": "thread_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "messages_json": { - "name": "messages_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "metadata": { - "name": "metadata", - "columns": { - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value_json": { - "name": "value_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "metadata_scope_key_pk": { - "columns": ["scope", "key"], - "name": "metadata_scope_key_pk" - } - }, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "runs": { - "name": "runs", - "columns": { - "run_id": { - "name": "run_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "thread_id": { - "name": "thread_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "started_at": { - "name": "started_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "finished_at": { - "name": "finished_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "error": { - "name": "error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "usage_json": { - "name": "usage_json", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} diff --git a/packages/ai-persistence-drizzle/drizzle/meta/_journal.json b/packages/ai-persistence-drizzle/drizzle/meta/_journal.json deleted file mode 100644 index 29a26ea4b..000000000 --- a/packages/ai-persistence-drizzle/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1784347380743, - "tag": "0000_tanstack_ai_initial", - "breakpoints": true - } - ] -} diff --git a/packages/ai-persistence-drizzle/package.json b/packages/ai-persistence-drizzle/package.json index c2c836a5f..f6229798a 100644 --- a/packages/ai-persistence-drizzle/package.json +++ b/packages/ai-persistence-drizzle/package.json @@ -1,7 +1,7 @@ { "name": "@tanstack/ai-persistence-drizzle", "version": "0.0.0", - "description": "SQLite-family Drizzle persistence for TanStack AI, with an edge-safe bring-your-own database adapter, a Node SQLite factory, and bundled migrations.", + "description": "Schema-first Drizzle persistence for TanStack AI (SQLite and Postgres). Emit a schema, own migrations with drizzle-kit, or use the Node SQLite convenience factory with stock defaults.", "author": "", "license": "MIT", "repository": { @@ -14,7 +14,8 @@ "tanstack", "persistence", "drizzle", - "sqlite" + "sqlite", + "postgres" ], "type": "module", "module": "./dist/esm/index.js", @@ -27,23 +28,27 @@ "./sqlite": { "types": "./dist/esm/sqlite.d.ts", "import": "./dist/esm/sqlite.js" + }, + "./sqlite-schema": { + "types": "./dist/esm/default-sqlite-schema.d.ts", + "import": "./dist/esm/default-sqlite-schema.js" + }, + "./pg-schema": { + "types": "./dist/esm/default-pg-schema.d.ts", + "import": "./dist/esm/default-pg-schema.js" } }, "bin": { - "tanstack-ai-drizzle-migrations": "./bin/tanstack-ai-drizzle-migrations.mjs", "tanstack-ai-drizzle-schema": "./bin/tanstack-ai-drizzle-schema.mjs" }, "files": [ "bin", "dist", - "src", - "drizzle", - "drizzle.config.ts" + "src" ], "scripts": { "build": "vite build", "clean": "premove ./build ./dist", - "db:generate": "drizzle-kit generate", "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:lib": "vitest", @@ -57,10 +62,10 @@ "drizzle-orm": ">=0.44.0" }, "devDependencies": { + "@electric-sql/pglite": "^0.3.0", "@tanstack/ai": "workspace:*", "@tanstack/ai-persistence": "workspace:*", "@vitest/coverage-v8": "4.0.14", - "drizzle-kit": "^0.31.0", "drizzle-orm": "^0.45.0" } } diff --git a/packages/ai-persistence-drizzle/src/assets.d.ts b/packages/ai-persistence-drizzle/src/assets.d.ts index b02bb85ed..780e40110 100644 --- a/packages/ai-persistence-drizzle/src/assets.d.ts +++ b/packages/ai-persistence-drizzle/src/assets.d.ts @@ -1,8 +1,3 @@ -declare module '*.sql?raw' { - const contents: string - export default contents -} - declare module '*.ts?raw' { const contents: string export default contents diff --git a/packages/ai-persistence-drizzle/src/assets/0000_tanstack_ai_initial.sql b/packages/ai-persistence-drizzle/src/assets/0000_tanstack_ai_initial.sql deleted file mode 100644 index 3e5570ec1..000000000 --- a/packages/ai-persistence-drizzle/src/assets/0000_tanstack_ai_initial.sql +++ /dev/null @@ -1,32 +0,0 @@ -CREATE TABLE `interrupts` ( - `interrupt_id` text PRIMARY KEY NOT NULL, - `run_id` text NOT NULL, - `thread_id` text NOT NULL, - `status` text NOT NULL, - `requested_at` integer NOT NULL, - `resolved_at` integer, - `payload_json` text NOT NULL, - `response_json` text -); ---> statement-breakpoint -CREATE TABLE `messages` ( - `thread_id` text PRIMARY KEY NOT NULL, - `messages_json` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `metadata` ( - `scope` text NOT NULL, - `key` text NOT NULL, - `value_json` text NOT NULL, - PRIMARY KEY(`scope`, `key`) -); ---> statement-breakpoint -CREATE TABLE `runs` ( - `run_id` text PRIMARY KEY NOT NULL, - `thread_id` text NOT NULL, - `status` text NOT NULL, - `started_at` integer NOT NULL, - `finished_at` integer, - `error` text, - `usage_json` text -); diff --git a/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema-pg.ts b/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema-pg.ts new file mode 100644 index 000000000..d2f11a092 --- /dev/null +++ b/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema-pg.ts @@ -0,0 +1,91 @@ +/** + * TanStack AI persistence schema — emitted by + * `tanstack-ai-drizzle-schema --dialect pg`. + * + * This file is yours. Add it to your drizzle-kit `schema` paths so your own + * migration journal owns the DDL, then pass it back to the runtime: + * + * ```ts + * import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle' + * import { schema } from './tanstack-ai-schema' + * + * const persistence = drizzlePersistence(db, { provider: 'pg', schema }) + * ``` + * + * You may rename tables and columns (or drop the explicit column names below + * and rely on your drizzle `casing` configuration) and add extra app-owned + * columns — keep added columns nullable or defaulted so the runtime's inserts + * succeed, and keep the columns below with these data shapes. + * + * This package does not ship SQL migrations. Generate and apply them with + * drizzle-kit in this project. + */ +import { + bigint, + index, + jsonb, + pgTable, + primaryKey, + text, +} from 'drizzle-orm/pg-core' +import type { InterruptRecord, RunStatus } from '@tanstack/ai-persistence' +import type { ModelMessage, TokenUsage } from '@tanstack/ai' + +/** Thread message history (`MessageStore`). */ +export const messages = pgTable('messages', { + threadId: text('thread_id').primaryKey(), + messagesJson: jsonb('messages_json').$type>().notNull(), +}) + +/** Run lifecycle records (`RunStore`). */ +export const runs = pgTable('runs', { + runId: text('run_id').primaryKey(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + startedAt: bigint('started_at', { mode: 'number' }).notNull(), + finishedAt: bigint('finished_at', { mode: 'number' }), + error: text('error'), + usageJson: jsonb('usage_json').$type(), +}) + +/** Interrupt / approval records (`InterruptStore`). */ +export const interrupts = pgTable( + 'interrupts', + { + interruptId: text('interrupt_id').primaryKey(), + runId: text('run_id').notNull(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + requestedAt: bigint('requested_at', { mode: 'number' }).notNull(), + resolvedAt: bigint('resolved_at', { mode: 'number' }), + payloadJson: jsonb('payload_json') + .$type>() + .notNull(), + responseJson: jsonb('response_json').$type(), + }, + // The runtime lists interrupts by thread and by run; keep (or extend) these + // lookup indexes to taste — this file is yours. + (table) => [ + index('interrupts_thread_id_idx').on(table.threadId), + index('interrupts_run_id_idx').on(table.runId), + ], +) + +/** Scoped key/value metadata (`MetadataStore`). */ +export const metadata = pgTable( + 'metadata', + { + scope: text('scope').notNull(), + key: text('key').notNull(), + valueJson: jsonb('value_json').$type().notNull(), + }, + (table) => [primaryKey({ columns: [table.scope, table.key] })], +) + +/** The full state schema, for `drizzlePersistence(db, { provider: 'pg', schema })` and drizzle-kit. */ +export const schema = { + messages, + runs, + interrupts, + metadata, +} diff --git a/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema.ts b/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema.ts index 86cb944e1..15a3af11c 100644 --- a/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema.ts +++ b/packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema.ts @@ -8,15 +8,24 @@ * import { drizzlePersistence } from '@tanstack/ai-persistence-drizzle' * import { schema } from './tanstack-ai-schema' * - * const persistence = drizzlePersistence(db, { schema }) + * const persistence = drizzlePersistence(db, { provider: 'sqlite', schema }) * ``` * * You may rename tables and columns (or drop the explicit column names below * and rely on your drizzle `casing` configuration) and add extra app-owned * columns — keep added columns nullable or defaulted so the runtime's inserts * succeed, and keep the columns below with these data shapes. + * + * This package does not ship SQL migrations. Generate and apply them with + * drizzle-kit in this project. */ -import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { + index, + integer, + primaryKey, + sqliteTable, + text, +} from 'drizzle-orm/sqlite-core' import type { InterruptRecord, RunStatus } from '@tanstack/ai-persistence' import type { ModelMessage, TokenUsage } from '@tanstack/ai' @@ -40,18 +49,27 @@ export const runs = sqliteTable('runs', { }) /** Interrupt / approval records (`InterruptStore`). */ -export const interrupts = sqliteTable('interrupts', { - interruptId: text('interrupt_id').primaryKey(), - runId: text('run_id').notNull(), - threadId: text('thread_id').notNull(), - status: text('status').$type().notNull(), - requestedAt: integer('requested_at').notNull(), - resolvedAt: integer('resolved_at'), - payloadJson: text('payload_json', { mode: 'json' }) - .$type>() - .notNull(), - responseJson: text('response_json', { mode: 'json' }).$type(), -}) +export const interrupts = sqliteTable( + 'interrupts', + { + interruptId: text('interrupt_id').primaryKey(), + runId: text('run_id').notNull(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + requestedAt: integer('requested_at').notNull(), + resolvedAt: integer('resolved_at'), + payloadJson: text('payload_json', { mode: 'json' }) + .$type>() + .notNull(), + responseJson: text('response_json', { mode: 'json' }).$type(), + }, + // The runtime lists interrupts by thread and by run; keep (or extend) these + // lookup indexes to taste — this file is yours. + (table) => [ + index('interrupts_thread_id_idx').on(table.threadId), + index('interrupts_run_id_idx').on(table.runId), + ], +) /** Scoped key/value metadata (`MetadataStore`). */ export const metadata = sqliteTable( diff --git a/packages/ai-persistence-drizzle/src/cli.ts b/packages/ai-persistence-drizzle/src/cli.ts deleted file mode 100644 index 6b9378a08..000000000 --- a/packages/ai-persistence-drizzle/src/cli.ts +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node -import { runDrizzleMigrationsCli } from './migration-cli' - -try { - await runDrizzleMigrationsCli(process.argv.slice(2)) -} catch (error) { - const message = error instanceof Error ? error.message : String(error) - process.stderr.write(`${message}\n`) - process.exitCode = 1 -} diff --git a/packages/ai-persistence-drizzle/src/default-pg-schema.ts b/packages/ai-persistence-drizzle/src/default-pg-schema.ts new file mode 100644 index 000000000..a91b656d8 --- /dev/null +++ b/packages/ai-persistence-drizzle/src/default-pg-schema.ts @@ -0,0 +1,89 @@ +/** + * Default Postgres table definitions for TanStack AI state stores. + * + * Prefer owning this shape in your project via + * `tanstack-ai-drizzle-schema --dialect pg` and generating DDL with **your** + * drizzle-kit journal. This factory is the runtime convenience default for + * apps that want the stock tables without copying a file first. + * + * This package does **not** ship SQL migrations. Schema ownership and + * migration generation live in the application. + */ +import { + bigint, + index, + jsonb, + pgTable, + primaryKey, + text, +} from 'drizzle-orm/pg-core' +import type { InterruptRecord, RunStatus } from '@tanstack/ai-persistence' +import type { ModelMessage, TokenUsage } from '@tanstack/ai' +import type { TanstackAiPgSchema } from './schema-contract' + +/** Thread message history (`MessageStore`). */ +export const messages = pgTable('messages', { + threadId: text('thread_id').primaryKey(), + messagesJson: jsonb('messages_json').$type>().notNull(), +}) + +/** Run lifecycle records (`RunStore`). */ +export const runs = pgTable('runs', { + runId: text('run_id').primaryKey(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + startedAt: bigint('started_at', { mode: 'number' }).notNull(), + finishedAt: bigint('finished_at', { mode: 'number' }), + error: text('error'), + usageJson: jsonb('usage_json').$type(), +}) + +/** Interrupt / approval records (`InterruptStore`). */ +export const interrupts = pgTable( + 'interrupts', + { + interruptId: text('interrupt_id').primaryKey(), + runId: text('run_id').notNull(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + requestedAt: bigint('requested_at', { mode: 'number' }).notNull(), + resolvedAt: bigint('resolved_at', { mode: 'number' }), + payloadJson: jsonb('payload_json') + .$type>() + .notNull(), + responseJson: jsonb('response_json').$type(), + }, + // The stores list interrupts by thread and by run (`list*`, + // `listPending*`); index both foreign lookups. + (table) => [ + index('interrupts_thread_id_idx').on(table.threadId), + index('interrupts_run_id_idx').on(table.runId), + ], +) + +/** Scoped key/value metadata (`MetadataStore`). */ +export const metadata = pgTable( + 'metadata', + { + scope: text('scope').notNull(), + key: text('key').notNull(), + valueJson: jsonb('value_json').$type().notNull(), + }, + (table) => [primaryKey({ columns: [table.scope, table.key] })], +) + +/** + * Build a fresh copy of the default Postgres schema tables. + * + * Pass the result to {@link pgPersistence}, or prefer the file emitted by + * `tanstack-ai-drizzle-schema --dialect pg` when you want drizzle-kit to own + * DDL in your repo. + */ +export function createDefaultPgSchema(): TanstackAiPgSchema { + return { + messages, + runs, + interrupts, + metadata, + } +} diff --git a/packages/ai-persistence-drizzle/src/default-sqlite-schema.ts b/packages/ai-persistence-drizzle/src/default-sqlite-schema.ts new file mode 100644 index 000000000..217927be8 --- /dev/null +++ b/packages/ai-persistence-drizzle/src/default-sqlite-schema.ts @@ -0,0 +1,90 @@ +/** + * Default SQLite table definitions for TanStack AI state stores. + * + * Prefer owning this shape in your project via `tanstack-ai-drizzle-schema` and + * generating DDL with **your** drizzle-kit journal. This factory is the runtime + * convenience default used by {@link sqlitePersistence} and by apps that want + * the stock tables without copying a file first. + * + * This package does **not** ship SQL migrations. Schema ownership and + * migration generation live in the application. + */ +import { + index, + integer, + primaryKey, + sqliteTable, + text, +} from 'drizzle-orm/sqlite-core' +import type { InterruptRecord, RunStatus } from '@tanstack/ai-persistence' +import type { ModelMessage, TokenUsage } from '@tanstack/ai' +import type { TanstackAiSqliteSchema } from './schema-contract' + +/** Thread message history (`MessageStore`). */ +export const messages = sqliteTable('messages', { + threadId: text('thread_id').primaryKey(), + messagesJson: text('messages_json', { mode: 'json' }) + .$type>() + .notNull(), +}) + +/** Run lifecycle records (`RunStore`). */ +export const runs = sqliteTable('runs', { + runId: text('run_id').primaryKey(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + startedAt: integer('started_at').notNull(), + finishedAt: integer('finished_at'), + error: text('error'), + usageJson: text('usage_json', { mode: 'json' }).$type(), +}) + +/** Interrupt / approval records (`InterruptStore`). */ +export const interrupts = sqliteTable( + 'interrupts', + { + interruptId: text('interrupt_id').primaryKey(), + runId: text('run_id').notNull(), + threadId: text('thread_id').notNull(), + status: text('status').$type().notNull(), + requestedAt: integer('requested_at').notNull(), + resolvedAt: integer('resolved_at'), + payloadJson: text('payload_json', { mode: 'json' }) + .$type>() + .notNull(), + responseJson: text('response_json', { mode: 'json' }).$type(), + }, + // The stores list interrupts by thread and by run (`list*`, + // `listPending*`); index both foreign lookups. + (table) => [ + index('interrupts_thread_id_idx').on(table.threadId), + index('interrupts_run_id_idx').on(table.runId), + ], +) + +/** Scoped key/value metadata (`MetadataStore`). */ +export const metadata = sqliteTable( + 'metadata', + { + scope: text('scope').notNull(), + key: text('key').notNull(), + valueJson: text('value_json', { mode: 'json' }).$type().notNull(), + }, + (table) => [primaryKey({ columns: [table.scope, table.key] })], +) + +/** + * Build a fresh copy of the default SQLite schema tables. + * + * Pass the result to {@link drizzlePersistence} or {@link sqlitePersistence}, + * or prefer the file emitted by `tanstack-ai-drizzle-schema` when you want + * drizzle-kit to own DDL in your repo. + */ +export function createDefaultSqliteSchema(): TanstackAiSqliteSchema { + return { + messages, + runs, + interrupts, + metadata, + } +} diff --git a/packages/ai-persistence-drizzle/src/ensure-pg-tables.ts b/packages/ai-persistence-drizzle/src/ensure-pg-tables.ts new file mode 100644 index 000000000..c9aef3e42 --- /dev/null +++ b/packages/ai-persistence-drizzle/src/ensure-pg-tables.ts @@ -0,0 +1,69 @@ +/** + * Derive `CREATE TABLE IF NOT EXISTS` statements from a user-supplied (or + * default) Postgres schema. A local/dev bootstrap convenience — production + * apps should emit the schema with `tanstack-ai-drizzle-schema --dialect pg` + * and generate migrations via their own drizzle-kit journal. + */ +import { getTableConfig } from 'drizzle-orm/pg-core' +import type { PgTable } from 'drizzle-orm/pg-core' +import type { TanstackAiPgSchema } from './schema-contract' + +function quoteIdent(name: string): string { + return `"${name.replaceAll('"', '""')}"` +} + +function createTableSql(table: PgTable): string { + const config = getTableConfig(table) + const columnSql = config.columns.map((column) => { + const parts = [`${quoteIdent(column.name)} ${column.getSQLType()}`] + if (column.primary) parts.push('PRIMARY KEY') + if (column.notNull && !column.primary) parts.push('NOT NULL') + return parts.join(' ') + }) + + for (const primaryKey of config.primaryKeys) { + const cols = primaryKey.columns.map((column) => quoteIdent(column.name)) + columnSql.push(`PRIMARY KEY(${cols.join(', ')})`) + } + + return `CREATE TABLE IF NOT EXISTS ${quoteIdent(config.name)} (${columnSql.join(', ')})` +} + +function createIndexSql(table: PgTable): Array { + const config = getTableConfig(table) + const statements: Array = [] + for (const index of config.indexes) { + const name = index.config.name + // Index columns are `IndexedColumn` wrappers (or raw SQL, which a + // bootstrap can't reproduce); emit only plain named-column indexes and + // leave anything fancier to drizzle-kit migrations. + const cols = index.config.columns + .map((column) => + 'name' in column && typeof column.name === 'string' + ? quoteIdent(column.name) + : null, + ) + .filter((column): column is string => column !== null) + if (!name || cols.length !== index.config.columns.length) continue + const unique = index.config.unique ? 'UNIQUE ' : '' + statements.push( + `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdent(name)} ON ${quoteIdent(config.name)} (${cols.join(', ')})`, + ) + } + return statements +} + +/** + * Create missing tables (and their indexes) for every table in `schema`. + * Idempotent. Does not alter existing tables — use drizzle-kit migrations for + * evolution. + */ +export async function ensurePgTables( + exec: (sql: string) => Promise | unknown, + schema: TanstackAiPgSchema, +): Promise { + for (const table of Object.values(schema)) { + await exec(createTableSql(table)) + for (const indexSql of createIndexSql(table)) await exec(indexSql) + } +} diff --git a/packages/ai-persistence-drizzle/src/ensure-sqlite-tables.ts b/packages/ai-persistence-drizzle/src/ensure-sqlite-tables.ts new file mode 100644 index 000000000..958ee7000 --- /dev/null +++ b/packages/ai-persistence-drizzle/src/ensure-sqlite-tables.ts @@ -0,0 +1,71 @@ +/** + * Derive `CREATE TABLE IF NOT EXISTS` statements from a user-supplied (or + * default) SQLite schema. Used by the Node convenience factory so local/dev + * can bootstrap without shipping versioned migrations from this package. + * + * Production apps should emit the schema with `tanstack-ai-drizzle-schema` and + * generate migrations via their own drizzle-kit journal instead of relying on + * runtime table creation for schema evolution. + */ +import { getTableConfig } from 'drizzle-orm/sqlite-core' +import type { SQLiteTable } from 'drizzle-orm/sqlite-core' +import type { TanstackAiSqliteSchema } from './schema-contract' + +function quoteIdent(name: string): string { + return `"${name.replaceAll('"', '""')}"` +} + +function createTableSql(table: SQLiteTable): string { + const config = getTableConfig(table) + const columnSql = config.columns.map((column) => { + const parts = [`${quoteIdent(column.name)} ${column.getSQLType()}`] + if (column.primary) parts.push('PRIMARY KEY') + if (column.notNull && !column.primary) parts.push('NOT NULL') + return parts.join(' ') + }) + + for (const primaryKey of config.primaryKeys) { + const cols = primaryKey.columns.map((column) => quoteIdent(column.name)) + columnSql.push(`PRIMARY KEY(${cols.join(', ')})`) + } + + return `CREATE TABLE IF NOT EXISTS ${quoteIdent(config.name)} (${columnSql.join(', ')})` +} + +function createIndexSql(table: SQLiteTable): Array { + const config = getTableConfig(table) + const statements: Array = [] + for (const index of config.indexes) { + const name = index.config.name + // Emit only plain named-column indexes (not raw SQL expressions); leave + // anything fancier to drizzle-kit migrations. + const cols = index.config.columns + .map((column) => + 'name' in column && typeof column.name === 'string' + ? quoteIdent(column.name) + : null, + ) + .filter((column): column is string => column !== null) + if (!name || cols.length !== index.config.columns.length) continue + const unique = index.config.unique ? 'UNIQUE ' : '' + statements.push( + `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdent(name)} ON ${quoteIdent(config.name)} (${cols.join(', ')})`, + ) + } + return statements +} + +/** + * Create missing tables (and their indexes) for every table in `schema`. + * Idempotent. Does not alter existing tables — use drizzle-kit migrations for + * evolution. + */ +export function ensureSqliteTables( + exec: (sql: string) => void, + schema: TanstackAiSqliteSchema, +): void { + for (const table of Object.values(schema)) { + exec(createTableSql(table)) + for (const indexSql of createIndexSql(table)) exec(indexSql) + } +} diff --git a/packages/ai-persistence-drizzle/src/index.ts b/packages/ai-persistence-drizzle/src/index.ts index 01b4eca9c..09832305a 100644 --- a/packages/ai-persistence-drizzle/src/index.ts +++ b/packages/ai-persistence-drizzle/src/index.ts @@ -1,12 +1,18 @@ /** - * Drizzle-backed SQLite-family persistence for TanStack AI. + * Drizzle-backed persistence for TanStack AI (SQLite and Postgres). * - * This root entry is safe to import in edge runtimes. Pass an already-created - * and migrated SQLite-compatible Drizzle database, including Cloudflare D1, to - * {@link drizzlePersistence}. Node's built-in SQLite convenience factory lives - * at `@tanstack/ai-persistence-drizzle/sqlite`. + * Schema-first: this package does **not** ship SQL migrations. Emit a schema + * into your project with `tanstack-ai-drizzle-schema`, let **your** drizzle-kit + * journal own the DDL, then pass the schema into {@link drizzlePersistence} + * together with the matching `provider`. + * + * This root entry is safe to import in edge runtimes. Node's SQLite + * convenience factory (default schema + optional runtime table bootstrap) + * lives at `@tanstack/ai-persistence-drizzle/sqlite`. */ -import { schema } from './schema' +import { is } from 'drizzle-orm' +import { PgDatabase } from 'drizzle-orm/pg-core' +import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core' import { assertTanstackAiSchema } from './schema-contract' import { createInterruptStore, @@ -14,64 +20,150 @@ import { createMetadataStore, createRunStore, } from './stores' -import type { TanstackAiSqliteSchema } from './schema-contract' +import type { PgQueryResultHKT } from 'drizzle-orm/pg-core' +import type { + InterruptStore, + MessageStore, + MetadataStore, + RunStore, +} from '@tanstack/ai-persistence' +import type { + TanstackAiPgSchema, + TanstackAiSqliteSchema, +} from './schema-contract' import type { DrizzleSqliteDb, TanstackAiTables } from './stores' -export { schema } from './schema' -export { sqliteMigrations } from './migrations' -export { drizzleSchemaFilename, drizzleSchemaSource } from './schema-source' -export { DrizzleSchemaError } from './schema-contract' -export type { TanstackAiSqliteSchema } from './schema-contract' -export type { SqliteMigration } from './migrations' +export { createDefaultSqliteSchema } from './default-sqlite-schema' +export { createDefaultPgSchema } from './default-pg-schema' +export { ensureSqliteTables } from './ensure-sqlite-tables' +export { ensurePgTables } from './ensure-pg-tables' +export { drizzleSchemaFilename, drizzleSchemaSources } from './schema-source' +export { DrizzleSchemaError, assertTanstackAiSchema } from './schema-contract' +export type { + DrizzleProvider, + TanstackAiPgSchema, + TanstackAiSqliteSchema, + TanstackAiTableShapes, +} from './schema-contract' export type { DrizzleSqliteDb } from './stores' -// Deprecated aliases kept for backward compatibility (both are SQLite-only). +/** @deprecated Use {@link drizzleSchemaSources}.sqlite. */ +export { drizzleSchemaSource } from './schema-source' +/** @deprecated Use {@link TanstackAiSqliteSchema}. */ export type { TanstackAiSchema } from './schema-contract' +/** @deprecated Use {@link DrizzleSqliteDb}. */ export type { DrizzleDb } from './stores' -export interface DrizzlePersistenceOptions { +/** + * Any Drizzle Postgres database (node-postgres, postgres.js, neon, pglite, …). + * + * Typed as the schema-agnostic slice of the query builder the stores actually + * use, so a BYO `db` constructed with any `{ schema }` is assignable + * regardless of its `TFullSchema`. + */ +export type DrizzlePgDb = Pick< + PgDatabase, + 'select' | 'insert' | 'update' | 'delete' +> + +export interface SqlitePersistenceConfig { + /** The database dialect of `db` and `schema`. */ + provider: 'sqlite' /** - * Operate over your project's own copy of the TanStack AI schema instead of - * the bundled one — emit it with `tanstack-ai-drizzle-schema`, add it to - * your drizzle-kit schema paths, and pass it here. Table and column database - * names are yours to change (drizzle `casing` transforms included), and - * tables may carry extra app-owned columns as long as they are nullable or - * defaulted. Defaults to the bundled `schema` export. + * Your TanStack AI schema tables. Emit a starter with + * `tanstack-ai-drizzle-schema`, add it to drizzle-kit, generate migrations + * in your project, and pass the module here. Table/column database names are + * yours (including drizzle `casing`); extra app-owned columns are fine when + * nullable or defaulted. + * + * For the stock tables without a project file, use + * {@link createDefaultSqliteSchema}. */ - schema?: TanstackAiSqliteSchema + schema: TanstackAiSqliteSchema +} + +export interface PgPersistenceConfig { + /** The database dialect of `db` and `schema`. */ + provider: 'pg' + /** + * Your TanStack AI schema tables. Emit a starter with + * `tanstack-ai-drizzle-schema --dialect pg`, add it to drizzle-kit, generate + * migrations in your project, and pass the module here. Table/column + * database names are yours (including drizzle `casing`); extra app-owned + * columns are fine when nullable or defaulted. + * + * For the stock tables without a project file, use + * {@link createDefaultPgSchema}. + */ + schema: TanstackAiPgSchema +} + +export type DrizzlePersistenceOptions = + | SqlitePersistenceConfig + | PgPersistenceConfig + +export interface DrizzlePersistence { + stores: { + messages: MessageStore + runs: RunStore + interrupts: InterruptStore + metadata: MetadataStore + } } /** - * Wire TanStack AI persistence stores over a migrated Drizzle SQLite database. + * Wire TanStack AI persistence stores over a migrated Drizzle database. * - * No `locks` store is returned: this backend has no distributed lock primitive, - * and bundling an `InMemoryLockStore` would silently hand multi-instance - * deployments a lock that does not lock across instances. Consumers that need a - * lock (e.g. `withSandbox`) transparently fall back to an in-process - * `InMemoryLockStore`; for cross-instance locking use a distributed backend such - * as the Cloudflare Durable Object lock (`@tanstack/ai-persistence-cloudflare`). + * `provider` declares the dialect; `db` and `schema` must match it (checked at + * compile time by the overloads and at runtime by the schema assertion). + * `schema` is required — this package never applies bundled DDL. Own + * migrations via drizzle-kit (after emitting the schema), or bootstrap a known + * schema locally with {@link ensureSqliteTables} / {@link ensurePgTables}. + * + * No `locks` store is returned: this backend has no distributed lock + * primitive. Consumers that need cross-instance locking should compose one in + * (for example the Cloudflare Durable Object lock). */ export function drizzlePersistence( db: DrizzleSqliteDb, - options?: DrizzlePersistenceOptions, -) { - const tables = resolveTables(options?.schema) + options: SqlitePersistenceConfig, +): DrizzlePersistence +export function drizzlePersistence( + db: DrizzlePgDb, + options: PgPersistenceConfig, +): DrizzlePersistence +export function drizzlePersistence( + db: DrizzleSqliteDb | DrizzlePgDb, + options: DrizzlePersistenceOptions, +): DrizzlePersistence { + assertTanstackAiSchema(options.schema, options.provider) + // The overloads guarantee db/provider/schema agree for typed callers; + // re-verify the db dialect at runtime for anyone calling through `any`. + const dialectOk = + options.provider === 'pg' ? is(db, PgDatabase) : is(db, BaseSQLiteDatabase) + if (!dialectOk) { + throw new TypeError( + `drizzlePersistence: provider is '${options.provider}' but \`db\` is not a Drizzle ${ + options.provider === 'pg' ? 'Postgres' : 'SQLite' + } database.`, + ) + } + // THE SEAM. The stores are implemented once against the SQLite builder + // types, but the Postgres and SQLite query builders share the exact method + // surface the stores use (select/insert/update/delete, onConflictDo*, + // eq/and/asc), and the generated SQL dialect comes from the runtime + // db/table objects — which the checks above have just proven match + // `provider`. TypeScript cannot correlate the db/schema unions per provider + // (no correlated union types), so the pg pair is re-faced onto the SQLite + // signatures here — the single place dialect typing is erased. The pg + // conformance suite runs the result against a real Postgres engine. + const tables = options.schema as TanstackAiTables + const storeDb = db as DrizzleSqliteDb return { stores: { - messages: createMessageStore(db, tables), - runs: createRunStore(db, tables), - interrupts: createInterruptStore(db, tables), - metadata: createMetadataStore(db, tables), + messages: createMessageStore(storeDb, tables), + runs: createRunStore(storeDb, tables), + interrupts: createInterruptStore(storeDb, tables), + metadata: createMetadataStore(storeDb, tables), }, } } - -function resolveTables(input?: TanstackAiSqliteSchema): TanstackAiTables { - if (!input) return schema - assertTanstackAiSchema(input) - // Safe widening: the stores only depend on the column *data* shapes, which - // `TanstackAiSqliteSchema` pins at the call site, and on the runtime table/column - // objects, which carry their own database names into the generated SQL. The - // concrete `TanstackAiTables` name literals are phantom types the store code - // never relies on. - return input as TanstackAiTables -} diff --git a/packages/ai-persistence-drizzle/src/migration-cli.ts b/packages/ai-persistence-drizzle/src/migration-cli.ts deleted file mode 100644 index c55b2b6cb..000000000 --- a/packages/ai-persistence-drizzle/src/migration-cli.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { mkdir, readFile, writeFile } from 'node:fs/promises' -import { join } from 'node:path' -import { sqliteMigrations } from './migrations' - -export interface MigrationCliOutput { - writeStdout: (value: string) => void -} - -export class MigrationCliError extends Error { - constructor(message: string) { - super(message) - this.name = 'MigrationCliError' - } -} - -const usage = `Usage: tanstack-ai-drizzle-migrations (--out | --stdout) [--force] - -Options: - --out Copy the ordered SQLite migration files. - --stdout Print the ordered migration SQL. - --force Replace divergent files when copying. - --help Show this help. -` - -interface ParsedArguments { - force: boolean - help: boolean - outDirectory?: string - stdout: boolean -} - -function parseArguments(args: ReadonlyArray): ParsedArguments { - let force = false - let help = false - let outDirectory: string | undefined - let stdout = false - - for (let index = 0; index < args.length; index++) { - const argument = args[index] - if (argument === '--force') { - force = true - } else if (argument === '--help' || argument === '-h') { - help = true - } else if (argument === '--stdout') { - stdout = true - } else if (argument === '--out') { - const value = args[index + 1] - if (!value || value.startsWith('-')) { - throw new MigrationCliError('--out requires a directory.') - } - if (outDirectory !== undefined) { - throw new MigrationCliError('--out may only be provided once.') - } - outDirectory = value - index++ - } else { - throw new MigrationCliError(`Unknown argument: ${argument ?? ''}`) - } - } - - return { force, help, outDirectory, stdout } -} - -function isMissingFileError(error: unknown): boolean { - return error instanceof Error && 'code' in error && error.code === 'ENOENT' -} - -async function readExistingFile(path: string): Promise { - try { - return await readFile(path, 'utf8') - } catch (error) { - if (isMissingFileError(error)) return undefined - throw error - } -} - -/** Execute the migration asset CLI. Exported for deterministic CLI tests. */ -export async function runDrizzleMigrationsCli( - args: ReadonlyArray, - output: MigrationCliOutput = { - writeStdout(value) { - process.stdout.write(value) - }, - }, -): Promise { - const parsed = parseArguments(args) - if (parsed.help) { - output.writeStdout(usage) - return - } - - const outputCount = - Number(parsed.stdout) + Number(parsed.outDirectory != null) - if (outputCount !== 1) { - throw new MigrationCliError( - 'Provide exactly one output mode: --out or --stdout.', - ) - } - if (parsed.stdout) { - if (parsed.force) { - throw new MigrationCliError('--force can only be used with --out.') - } - output.writeStdout( - `${sqliteMigrations.map((migration) => migration.sql.trimEnd()).join('\n\n')}\n`, - ) - return - } - - const outDirectory = parsed.outDirectory - if (!outDirectory) { - throw new MigrationCliError('--out requires a directory.') - } - await mkdir(outDirectory, { recursive: true }) - for (const migration of sqliteMigrations) { - const destination = join(outDirectory, migration.filename) - const existing = await readExistingFile(destination) - if (existing === migration.sql) continue - if (existing !== undefined && !parsed.force) { - throw new MigrationCliError( - `Refusing to overwrite divergent migration file: ${destination}. Re-run with --force to replace it.`, - ) - } - await writeFile(destination, migration.sql, 'utf8') - } -} diff --git a/packages/ai-persistence-drizzle/src/migrations.ts b/packages/ai-persistence-drizzle/src/migrations.ts deleted file mode 100644 index 889f6426f..000000000 --- a/packages/ai-persistence-drizzle/src/migrations.ts +++ /dev/null @@ -1,20 +0,0 @@ -import initialMigrationSql from './assets/0000_tanstack_ai_initial.sql?raw' - -/** A canonical SQLite migration bundled with the adapter. */ -export interface SqliteMigration { - /** Stable identifier recorded in the TanStack AI migration table. */ - id: string - /** Filename used by the migration copy CLI. */ - filename: string - /** SQL applied atomically with its migration bookkeeping row. */ - sql: string -} - -/** Ordered canonical migrations for the TanStack AI SQLite schema. */ -export const sqliteMigrations: ReadonlyArray = [ - { - id: '0000_tanstack_ai_initial', - filename: '0000_tanstack_ai_initial.sql', - sql: initialMigrationSql, - }, -] diff --git a/packages/ai-persistence-drizzle/src/schema-cli.ts b/packages/ai-persistence-drizzle/src/schema-cli.ts index 9d9de0930..6ac0503a8 100644 --- a/packages/ai-persistence-drizzle/src/schema-cli.ts +++ b/packages/ai-persistence-drizzle/src/schema-cli.ts @@ -1,6 +1,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' -import { drizzleSchemaFilename, drizzleSchemaSource } from './schema-source' +import { drizzleSchemaFilename, drizzleSchemaSources } from './schema-source' +import type { DrizzleSchemaDialect } from './schema-source' export interface SchemaCliOutput { writeStdout: (value: string) => void @@ -13,26 +14,41 @@ export class SchemaCliError extends Error { } } -const usage = `Usage: tanstack-ai-drizzle-schema (--out | --stdout) [--force] +const usage = `Usage: tanstack-ai-drizzle-schema (--out | --stdout) [--dialect ] [--force] -Emits the TanStack AI Drizzle schema module so your project owns it: add the -file to your drizzle-kit schema paths and pass it to drizzlePersistence(db, { schema }). +Emits the TanStack AI Drizzle schema module so **your** project owns it. + +This package does not ship SQL migrations. After emitting: + + 1. Add the file to your drizzle-kit schema paths + 2. Run drizzle-kit generate / migrate in your app + 3. Pass the schema to drizzlePersistence(db, { provider, schema }) Options: - --out Copy the schema module into the directory. - --stdout Print the schema module. - --force Replace a divergent file when copying. - --help Show this help. + --out Write ${drizzleSchemaFilename} into the directory. + --stdout Print the schema module. + --dialect Schema dialect: sqlite (default) or pg. + --force Replace a divergent file when copying. + --help Show this help. ` interface ParsedArguments { + dialect: DrizzleSchemaDialect force: boolean help: boolean outDirectory?: string stdout: boolean } +function parseDialect(value: string | undefined): DrizzleSchemaDialect { + if (value === 'sqlite' || value === 'pg') return value + throw new SchemaCliError( + `--dialect must be "sqlite" or "pg"; received: ${value ?? ''}`, + ) +} + function parseArguments(args: ReadonlyArray): ParsedArguments { + let dialect: DrizzleSchemaDialect | undefined let force = false let help = false let outDirectory: string | undefined @@ -46,6 +62,12 @@ function parseArguments(args: ReadonlyArray): ParsedArguments { help = true } else if (argument === '--stdout') { stdout = true + } else if (argument === '--dialect') { + if (dialect !== undefined) { + throw new SchemaCliError('--dialect may only be provided once.') + } + dialect = parseDialect(args[index + 1]) + index++ } else if (argument === '--out') { const value = args[index + 1] if (!value || value.startsWith('-')) { @@ -61,7 +83,7 @@ function parseArguments(args: ReadonlyArray): ParsedArguments { } } - return { force, help, outDirectory, stdout } + return { dialect: dialect ?? 'sqlite', force, help, outDirectory, stdout } } function isMissingFileError(error: unknown): boolean { @@ -77,7 +99,7 @@ async function readExistingFile(path: string): Promise { } } -/** Execute the schema asset CLI. Exported for deterministic CLI tests. */ +/** Execute the schema emit CLI. Exported for deterministic CLI tests. */ export async function runDrizzleSchemaCli( args: ReadonlyArray, output: SchemaCliOutput = { @@ -99,11 +121,12 @@ export async function runDrizzleSchemaCli( 'Provide exactly one output mode: --out or --stdout.', ) } + const source = drizzleSchemaSources[parsed.dialect] if (parsed.stdout) { if (parsed.force) { throw new SchemaCliError('--force can only be used with --out.') } - output.writeStdout(`${drizzleSchemaSource.trimEnd()}\n`) + output.writeStdout(`${source.trimEnd()}\n`) return } @@ -114,11 +137,11 @@ export async function runDrizzleSchemaCli( await mkdir(outDirectory, { recursive: true }) const destination = join(outDirectory, drizzleSchemaFilename) const existing = await readExistingFile(destination) - if (existing === drizzleSchemaSource) return + if (existing === source) return if (existing !== undefined && !parsed.force) { throw new SchemaCliError( `Refusing to overwrite divergent schema file: ${destination}. Re-run with --force to replace it.`, ) } - await writeFile(destination, drizzleSchemaSource, 'utf8') + await writeFile(destination, source, 'utf8') } diff --git a/packages/ai-persistence-drizzle/src/schema-contract.ts b/packages/ai-persistence-drizzle/src/schema-contract.ts index 9a8d53319..90d7772f5 100644 --- a/packages/ai-persistence-drizzle/src/schema-contract.ts +++ b/packages/ai-persistence-drizzle/src/schema-contract.ts @@ -1,10 +1,15 @@ /** * Structural contract for a user-supplied TanStack AI Drizzle schema. * + * The contract itself is dialect-neutral: {@link TanstackAiTableShapes} lists + * the logical columns and their decoded data shapes, nothing else. Table and + * column **database names are free**, and any Drizzle dialect can project the + * shapes into concrete tables — {@link TanstackAiSqliteSchema} is the SQLite + * projection this package's stores consume today. + * * `drizzlePersistence` accepts any schema whose tables and columns carry the - * required data shapes; table and column **database names are free**. This lets - * a project own the schema file (emitted by `tanstack-ai-drizzle-schema`), - * generate DDL through its own drizzle-kit journal — including projects using + * required data shapes. Emit a starter with `tanstack-ai-drizzle-schema`, + * generate DDL through your own drizzle-kit journal — including projects using * drizzle's `casing` name transforms — and extend tables with extra columns * (for example an ownership `user_id`) without falling out of contract. * @@ -12,64 +17,96 @@ * columns must therefore be nullable or defaulted so inserts succeed. */ import { Column, is } from 'drizzle-orm' +import { PgTable } from 'drizzle-orm/pg-core' import { SQLiteTable } from 'drizzle-orm/sqlite-core' +import type { AnyPgColumn } from 'drizzle-orm/pg-core' import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core' import type { InterruptRecord, RunStatus } from '@tanstack/ai-persistence' import type { ModelMessage, TokenUsage } from '@tanstack/ai' +/** Database dialects the Drizzle backend supports. */ +export type DrizzleProvider = 'sqlite' | 'pg' + /** - * A column whose decoded (`data`) type is `TData`. Name, table name, and - * driver specifics are intentionally unconstrained. + * Decoded (`data`) column shapes per store table — the dialect-neutral single + * source of truth for the schema contract. Nullability, database names, and + * driver specifics are intentionally unconstrained; only the base data shape + * of each column is fixed. */ -type AiColumn = AnySQLiteColumn<{ data: TData }> - -type AiTable = SQLiteTable & TColumns +export interface TanstackAiTableShapes { + /** Thread message history (`MessageStore`). */ + messages: { + threadId: string + messagesJson: Array + } + /** Run lifecycle records (`RunStore`). */ + runs: { + runId: string + threadId: string + status: RunStatus + startedAt: number + finishedAt: number + error: string + usageJson: TokenUsage + } + /** Interrupt / approval records (`InterruptStore`). */ + interrupts: { + interruptId: string + runId: string + threadId: string + status: InterruptRecord['status'] + requestedAt: number + resolvedAt: number + payloadJson: Record + responseJson: unknown + } + /** Scoped key/value metadata (`MetadataStore`). */ + metadata: { + scope: string + key: string + valueJson: unknown + } +} /** - * The schema shape `drizzlePersistence` can operate over. `schema` exported - * from this package satisfies it, as does the file emitted by the - * `tanstack-ai-drizzle-schema` CLI. SQLite-only (the columns are Drizzle - * SQLite columns), which the name makes explicit. + * The SQLite projection of {@link TanstackAiTableShapes}: what + * `drizzlePersistence` can operate over. Satisfied by + * {@link createDefaultSqliteSchema} and by the file emitted by + * `tanstack-ai-drizzle-schema`. Other dialects can project the same shapes + * over their own table/column types when their stores land. */ -export interface TanstackAiSqliteSchema { - messages: AiTable<{ - threadId: AiColumn - messagesJson: AiColumn> - }> - runs: AiTable<{ - runId: AiColumn - threadId: AiColumn - status: AiColumn - startedAt: AiColumn - finishedAt: AiColumn - error: AiColumn - usageJson: AiColumn - }> - interrupts: AiTable<{ - interruptId: AiColumn - runId: AiColumn - threadId: AiColumn - status: AiColumn - requestedAt: AiColumn - resolvedAt: AiColumn - payloadJson: AiColumn> - responseJson: AiColumn - }> - metadata: AiTable<{ - scope: AiColumn - key: AiColumn - valueJson: AiColumn - }> +export type TanstackAiSqliteSchema = { + [TableKey in keyof TanstackAiTableShapes]: SQLiteTable & { + [ColumnKey in keyof TanstackAiTableShapes[TableKey]]: AnySQLiteColumn<{ + data: TanstackAiTableShapes[TableKey][ColumnKey] + }> + } } /** - * @deprecated Renamed to {@link TanstackAiSqliteSchema} — the contract is - * SQLite-only, which the old name did not signal. + * The Postgres projection of {@link TanstackAiTableShapes}: what + * `pgPersistence` (the `/pg` entry) can operate over. Satisfied by + * {@link createDefaultPgSchema} and by the file emitted by + * `tanstack-ai-drizzle-schema --dialect pg`. */ +export type TanstackAiPgSchema = { + [TableKey in keyof TanstackAiTableShapes]: PgTable & { + [ColumnKey in keyof TanstackAiTableShapes[TableKey]]: AnyPgColumn<{ + data: TanstackAiTableShapes[TableKey][ColumnKey] + }> + } +} + +/** Any dialect projection accepted by the runtime schema assertion. */ +export type TanstackAiAnySchema = TanstackAiSqliteSchema | TanstackAiPgSchema + +/** @deprecated Use {@link TanstackAiSqliteSchema}. */ export type TanstackAiSchema = TanstackAiSqliteSchema const requiredColumns: { - [K in keyof TanstackAiSqliteSchema]: ReadonlyArray + [TableKey in keyof TanstackAiTableShapes]: ReadonlyArray< + keyof TanstackAiTableShapes[TableKey] & string + > } = { messages: ['threadId', 'messagesJson'], runs: [ @@ -95,7 +132,7 @@ const requiredColumns: { } const tableKeys = Object.keys(requiredColumns) as Array< - keyof TanstackAiSqliteSchema + keyof TanstackAiTableShapes > /** A user-supplied schema failed the {@link TanstackAiSqliteSchema} contract. */ @@ -110,18 +147,30 @@ export class DrizzleSchemaError extends Error { } } +const providerTables = { + sqlite: { table: SQLiteTable, label: 'SQLite' }, + pg: { table: PgTable, label: 'Postgres' }, +} as const + /** * Assert `input` structurally satisfies the store contract at runtime: every - * table is a Drizzle SQLite table and carries every column property the - * stores reference. Column data shapes are enforced by the - * {@link TanstackAiSqliteSchema} type at compile time and are not re-checked here. + * table is a Drizzle table of the declared `provider`'s dialect and carries + * every column property the stores reference. Column data shapes are enforced + * by the dialect projection types ({@link TanstackAiSqliteSchema}, + * {@link TanstackAiPgSchema}) at compile time and are not re-checked here. */ -export function assertTanstackAiSchema(input: TanstackAiSqliteSchema): void { +export function assertTanstackAiSchema( + input: TanstackAiAnySchema, + provider: DrizzleProvider, +): void { + const dialect = providerTables[provider] const problems: Array = [] for (const tableKey of tableKeys) { const table: unknown = input[tableKey] - if (!is(table, SQLiteTable)) { - problems.push(`\`${tableKey}\` is not a Drizzle SQLite table.`) + if (!is(table, dialect.table)) { + problems.push( + `\`${tableKey}\` is not a Drizzle ${dialect.label} table (provider '${provider}').`, + ) continue } for (const columnKey of requiredColumns[tableKey]) { diff --git a/packages/ai-persistence-drizzle/src/schema-source.ts b/packages/ai-persistence-drizzle/src/schema-source.ts index fbce6e2cd..e00bc5f9e 100644 --- a/packages/ai-persistence-drizzle/src/schema-source.ts +++ b/packages/ai-persistence-drizzle/src/schema-source.ts @@ -1,12 +1,23 @@ -import source from './assets/tanstack-ai-schema.ts?raw' +import sqliteSource from './assets/tanstack-ai-schema.ts?raw' +import pgSource from './assets/tanstack-ai-schema-pg.ts?raw' -/** Filename used by the schema copy CLI. */ +/** Filename used by the schema emit CLI (both dialects). */ export const drizzleSchemaFilename = 'tanstack-ai-schema.ts' +/** Dialects the schema emit CLI can target. */ +export type DrizzleSchemaDialect = 'sqlite' | 'pg' + /** - * Source text of the user-facing TanStack AI Drizzle schema module, emitted by - * `tanstack-ai-drizzle-schema`. Structurally identical to this package's - * bundled `schema` (a test enforces it); the copy exists so a project's own - * drizzle-kit journal can own the DDL. + * Source text of the user-facing TanStack AI Drizzle schema module per + * dialect, emitted by `tanstack-ai-drizzle-schema`. Each is structurally + * identical to its default-schema factory (a test enforces it). The file is + * written into the project so **their** drizzle-kit journal owns the DDL — + * this package never ships or applies SQL migrations. */ -export const drizzleSchemaSource: string = source +export const drizzleSchemaSources: Record = { + sqlite: sqliteSource, + pg: pgSource, +} + +/** @deprecated Use {@link drizzleSchemaSources}.sqlite. */ +export const drizzleSchemaSource: string = sqliteSource diff --git a/packages/ai-persistence-drizzle/src/schema.ts b/packages/ai-persistence-drizzle/src/schema.ts deleted file mode 100644 index 97ffc79a4..000000000 --- a/packages/ai-persistence-drizzle/src/schema.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Drizzle schema for the TanStack AI **state** persistence contract. - * - * Each table mirrors the corresponding record in - * `@tanstack/ai-persistence`'s `types.ts` column-for-column. JSON-valued fields - * are stored in `*_json` text columns; epoch millisecond timestamps are stored - * as integers. - * - * This schema is the single source of truth for migrations: run - * `pnpm db:generate` (drizzle-kit) after any change here. It is also exported - * from the package so bring-your-own-drizzle users can drive their own - * migration workflow against it. - * - * PROVENANCE: `db:generate` emits DDL into `drizzle/`, but the runtime's - * `sqliteMigrations` (and the D1 sibling) load the DUPLICATE at - * `src/assets/0000_tanstack_ai_initial.sql`. `db:generate` alone does NOT touch - * that asset — copy the regenerated `drizzle/0000_tanstack_ai_initial.sql` over - * it, or the shipped migration goes stale. The package-contract test "keeps the - * shipped drizzle-kit migration equal to the embedded asset" fails on drift. - * - * Also keep this in sync with the sibling Prisma schema fragment - * (`@tanstack/ai-persistence-prisma`'s `src/assets/tanstack-ai.prisma`). - */ -import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' -import type { InterruptRecord, RunStatus } from '@tanstack/ai-persistence' -import type { ModelMessage, TokenUsage } from '@tanstack/ai' - -/** Thread message history (`MessageStore`). */ -export const messages = sqliteTable('messages', { - threadId: text('thread_id').primaryKey(), - messagesJson: text('messages_json', { mode: 'json' }) - .$type>() - .notNull(), -}) - -/** Run lifecycle records (`RunStore`). */ -export const runs = sqliteTable('runs', { - runId: text('run_id').primaryKey(), - threadId: text('thread_id').notNull(), - status: text('status').$type().notNull(), - startedAt: integer('started_at').notNull(), - finishedAt: integer('finished_at'), - error: text('error'), - usageJson: text('usage_json', { mode: 'json' }).$type(), -}) - -/** Interrupt / approval records (`InterruptStore`). */ -export const interrupts = sqliteTable('interrupts', { - interruptId: text('interrupt_id').primaryKey(), - runId: text('run_id').notNull(), - threadId: text('thread_id').notNull(), - status: text('status').$type().notNull(), - requestedAt: integer('requested_at').notNull(), - resolvedAt: integer('resolved_at'), - payloadJson: text('payload_json', { mode: 'json' }) - .$type>() - .notNull(), - responseJson: text('response_json', { mode: 'json' }).$type(), -}) - -/** Scoped key/value metadata (`MetadataStore`). */ -export const metadata = sqliteTable( - 'metadata', - { - scope: text('scope').notNull(), - key: text('key').notNull(), - valueJson: text('value_json', { mode: 'json' }).$type().notNull(), - }, - (table) => [primaryKey({ columns: [table.scope, table.key] })], -) - -/** The full state schema, for `drizzlePersistence(db)` and drizzle-kit. */ -export const schema = { - messages, - runs, - interrupts, - metadata, -} diff --git a/packages/ai-persistence-drizzle/src/sqlite-migrations.ts b/packages/ai-persistence-drizzle/src/sqlite-migrations.ts deleted file mode 100644 index 1866c339d..000000000 --- a/packages/ai-persistence-drizzle/src/sqlite-migrations.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { DatabaseSync } from 'node:sqlite' -import type { SqliteMigration } from './migrations' - -const MIGRATIONS_TABLE = '__tanstack_ai_migrations' - -/** Error raised when a bundled SQLite migration cannot be applied atomically. */ -export class SqliteMigrationError extends Error { - readonly migrationId: string - - constructor(migrationId: string, cause: unknown) { - const detail = cause instanceof Error ? cause.message : String(cause) - super(`Failed to apply SQLite migration "${migrationId}": ${detail}`, { - cause, - }) - this.name = 'SqliteMigrationError' - this.migrationId = migrationId - } -} - -/** - * Apply ordered migrations with each migration SQL and bookkeeping insert in - * the same transaction. A failed migration leaves no partial schema changes or - * applied marker, so retrying after the underlying issue is corrected is safe. - */ -export function applySqliteMigrations( - database: DatabaseSync, - migrations: ReadonlyArray, -): void { - database.exec(` - CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} ( - migration_id TEXT PRIMARY KEY NOT NULL, - applied_at INTEGER NOT NULL - ) - `) - - const findApplied = database.prepare( - `SELECT migration_id FROM ${MIGRATIONS_TABLE} WHERE migration_id = ?`, - ) - const recordApplied = database.prepare( - `INSERT INTO ${MIGRATIONS_TABLE} (migration_id, applied_at) VALUES (?, ?)`, - ) - - for (const migration of migrations) { - database.exec('BEGIN IMMEDIATE') - try { - if (findApplied.get(migration.id) !== undefined) { - database.exec('COMMIT') - continue - } - database.exec(migration.sql) - recordApplied.run(migration.id, Date.now()) - database.exec('COMMIT') - } catch (error) { - try { - database.exec('ROLLBACK') - } catch (rollbackError) { - throw new SqliteMigrationError( - migration.id, - new AggregateError( - [error, rollbackError], - 'Migration failed and its transaction could not be rolled back.', - ), - ) - } - throw new SqliteMigrationError(migration.id, error) - } - } -} diff --git a/packages/ai-persistence-drizzle/src/sqlite.ts b/packages/ai-persistence-drizzle/src/sqlite.ts index 30342e890..1a2b0a5a7 100644 --- a/packages/ai-persistence-drizzle/src/sqlite.ts +++ b/packages/ai-persistence-drizzle/src/sqlite.ts @@ -1,37 +1,77 @@ /** * Node-only SQLite convenience factory for TanStack AI persistence. * + * Schema-first: pass your schema (or accept {@link createDefaultSqliteSchema}), + * and optionally bootstrap tables from that schema at runtime. This package + * does **not** ship versioned SQL migrations — production apps should emit the + * schema with `tanstack-ai-drizzle-schema` and migrate via their own drizzle-kit + * journal. + * * Uses Node's built-in `node:sqlite` (`DatabaseSync`). That module is still a - * **release candidate** in current Node versions (not marked stable); pin a - * Node release that documents the API you rely on, and prefer - * `drizzlePersistence(db)` with your own driver if you need a fully stable - * SQLite path. + * release candidate in some Node versions; pin a release that documents the API + * you rely on, or use {@link drizzlePersistence} with a stable driver. */ import { mkdirSync } from 'node:fs' import { dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { DatabaseSync } from 'node:sqlite' import { drizzle } from 'drizzle-orm/sqlite-proxy' -import { sqliteMigrations } from './migrations' -import { applySqliteMigrations } from './sqlite-migrations' +import { createDefaultSqliteSchema } from './default-sqlite-schema' +import { ensureSqliteTables } from './ensure-sqlite-tables' import { drizzlePersistence } from './index' +import type { TanstackAiSqliteSchema } from './schema-contract' -export { SqliteMigrationError } from './sqlite-migrations' +export { createDefaultSqliteSchema } from './default-sqlite-schema' +export { ensureSqliteTables } from './ensure-sqlite-tables' export interface SqlitePersistenceOptions { /** `:memory:`, a filesystem path, or a `file:`-prefixed filesystem path. */ url: string - /** Apply the bundled TanStack AI migrations before creating stores. */ - migrate?: boolean + /** + * Schema tables the stores operate on. Defaults to + * {@link createDefaultSqliteSchema}. Prefer a project-owned copy emitted by + * `tanstack-ai-drizzle-schema` when you use drizzle-kit for migrations. + */ + schema?: TanstackAiSqliteSchema + /** + * When true (default), create missing tables derived from `schema` via + * `CREATE TABLE IF NOT EXISTS`. This is a local bootstrap convenience, not a + * migration system — set `false` when your drizzle-kit migrations already + * created the tables. + */ + ensureTables?: boolean } -/** Build persistence over Node's built-in SQLite driver. */ +/** + * Build persistence over Node's built-in SQLite driver with stock defaults. + * + * @example Zero-config local file + * ```ts + * const persistence = sqlitePersistence({ + * url: 'file:.data/ai.sqlite', + * }) + * ``` + * + * @example Project-owned schema (after `tanstack-ai-drizzle-schema` + kit migrate) + * ```ts + * import { schema } from './db/tanstack-ai-schema' + * + * const persistence = sqlitePersistence({ + * url: 'file:.data/ai.sqlite', + * schema, + * ensureTables: false, + * }) + * ``` + */ export function sqlitePersistence(options: SqlitePersistenceOptions) { + const schema = options.schema ?? createDefaultSqliteSchema() const filename = normalizeSqliteUrl(options.url) ensureParentDirectory(filename) const sqlite = new DatabaseSync(filename) try { - if (options.migrate) applySqliteMigrations(sqlite, sqliteMigrations) + if (options.ensureTables !== false) { + ensureSqliteTables((sql) => sqlite.exec(sql), schema) + } } catch (error) { sqlite.close() throw error @@ -51,7 +91,7 @@ export function sqlitePersistence(options: SqlitePersistenceOptions) { return Promise.resolve({ rows: rows.map((row) => Object.values(row)) }) }) - const persistence = drizzlePersistence(db) + const persistence = drizzlePersistence(db, { provider: 'sqlite', schema }) let closed = false return { ...persistence, diff --git a/packages/ai-persistence-drizzle/src/stores.ts b/packages/ai-persistence-drizzle/src/stores.ts index c840cdb2a..1947fd235 100644 --- a/packages/ai-persistence-drizzle/src/stores.ts +++ b/packages/ai-persistence-drizzle/src/stores.ts @@ -1,24 +1,26 @@ /** - * AIPersistence store implementations over a Drizzle sqlite database. + * AIPersistence store implementations over a Drizzle database. * - * Each method mirrors the reference in-memory backend - * (`@tanstack/ai-persistence`'s `memory.ts`), including the insert-if-absent - * `InterruptStore.create` and `RunStore.createOrResume` semantics - * (`onConflictDoNothing`). JSON columns are handled by Drizzle's - * `text({ mode: 'json' })`. + * Written once against the SQLite builder types; the Postgres path re-faces + * its runtime objects onto these signatures in `drizzlePersistence` (see the + * comment there). Stores operate on injected table objects from the caller's + * schema. JSON columns are expected to use Drizzle's json-decoding column + * types (SQLite `text({ mode: 'json' })`, Postgres `jsonb`) so values decode + * to the contract data shapes. */ import { and, asc, eq } from 'drizzle-orm' -import type { interrupts, runs, schema } from './schema' import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core' -import type { ModelMessage } from '@tanstack/ai' +import type { ModelMessage, TokenUsage } from '@tanstack/ai' import type { InterruptRecord, InterruptStore, MessageStore, MetadataStore, RunRecord, + RunStatus, RunStore, } from '@tanstack/ai-persistence' +import type { TanstackAiSqliteSchema } from './schema-contract' /** * Any Drizzle sqlite database (better-sqlite3, libsql, node:sqlite proxy, D1, …). @@ -32,21 +34,35 @@ export type DrizzleSqliteDb = Pick< 'select' | 'insert' | 'update' | 'delete' > -/** - * @deprecated Renamed to {@link DrizzleSqliteDb} — this backend is SQLite-only - * (better-sqlite3, libsql, node:sqlite, D1). The old name did not signal that. - */ +/** @deprecated Use {@link DrizzleSqliteDb}. */ export type DrizzleDb = DrizzleSqliteDb /** - * The concrete table set the stores are written against. A user-supplied - * {@link TanstackAiSchema} is validated by `assertTanstackAiSchema` and then - * used through this type: only column *data* shapes matter to the store code — - * table/column database names are carried by the runtime objects, so a schema - * with different names (drizzle `casing` transforms, renamed tables, extra - * app-owned columns) produces correct SQL through the same code paths. + * Table set the stores read/write. Only column *data* shapes matter; table and + * column database names are carried by the runtime objects. */ -export type TanstackAiTables = typeof schema +export type TanstackAiTables = TanstackAiSqliteSchema + +type RunRow = { + runId: string + threadId: string + status: RunStatus + startedAt: number + finishedAt: number | null + error: string | null + usageJson: TokenUsage | null +} + +type InterruptRow = { + interruptId: string + runId: string + threadId: string + status: InterruptRecord['status'] + requestedAt: number + resolvedAt: number | null + payloadJson: Record + responseJson: unknown | null +} export function createMessageStore( db: DrizzleSqliteDb, @@ -72,7 +88,7 @@ export function createMessageStore( } } -function mapRun(row: typeof runs.$inferSelect): RunRecord { +function mapRun(row: RunRow): RunRecord { return { runId: row.runId, threadId: row.threadId, @@ -110,7 +126,12 @@ export function createRunStore( return (await store.get(input.runId)) ?? record }, async update(runId, patch) { - const set: Partial = {} + const set: { + status?: RunStatus + finishedAt?: number | null + error?: string | null + usageJson?: TokenUsage | null + } = {} if (patch.status !== undefined) set.status = patch.status if (patch.finishedAt !== undefined) set.finishedAt = patch.finishedAt if (patch.error !== undefined) set.error = patch.error @@ -120,14 +141,14 @@ export function createRunStore( }, async get(runId) { const rows = await db.select().from(runs).where(eq(runs.runId, runId)) - const row = rows[0] + const row = rows[0] as RunRow | undefined return row ? mapRun(row) : null }, } return store } -function mapInterrupt(row: typeof interrupts.$inferSelect): InterruptRecord { +function mapInterrupt(row: InterruptRow): InterruptRecord { return { interruptId: row.interruptId, runId: row.runId, @@ -180,7 +201,7 @@ export function createInterruptStore( .select() .from(interrupts) .where(eq(interrupts.interruptId, interruptId)) - const row = rows[0] + const row = rows[0] as InterruptRow | undefined return row ? mapInterrupt(row) : null }, async list(threadId) { @@ -189,7 +210,7 @@ export function createInterruptStore( .from(interrupts) .where(eq(interrupts.threadId, threadId)) .orderBy(asc(interrupts.requestedAt)) - return rows.map(mapInterrupt) + return (rows as Array).map(mapInterrupt) }, async listPending(threadId) { const rows = await db @@ -202,7 +223,7 @@ export function createInterruptStore( ), ) .orderBy(asc(interrupts.requestedAt)) - return rows.map(mapInterrupt) + return (rows as Array).map(mapInterrupt) }, async listByRun(runId) { const rows = await db @@ -210,7 +231,7 @@ export function createInterruptStore( .from(interrupts) .where(eq(interrupts.runId, runId)) .orderBy(asc(interrupts.requestedAt)) - return rows.map(mapInterrupt) + return (rows as Array).map(mapInterrupt) }, async listPendingByRun(runId) { const rows = await db @@ -220,7 +241,7 @@ export function createInterruptStore( and(eq(interrupts.runId, runId), eq(interrupts.status, 'pending')), ) .orderBy(asc(interrupts.requestedAt)) - return rows.map(mapInterrupt) + return (rows as Array).map(mapInterrupt) }, } } @@ -249,9 +270,9 @@ export function createMetadataStore( return row ? row.valueJson : null }, async set(scope, key, value) { - // SQL backends store JSON text in a NOT NULL column and cannot persist a - // nullish value: `text({ mode: 'json' })` binds a JS `null` as SQL NULL - // (it never serializes it to the text `"null"`), and `undefined` has no + // SQL backends store JSON in a NOT NULL column and cannot persist a + // nullish value: the json column types bind a JS `null` as SQL NULL + // (they never serialize it to the text `"null"`), and `undefined` has no // JSON at all — both violate NOT NULL. Reject nullish with a clear error // (consistent with the sibling Prisma backend) instead of a cryptic // driver failure. Unlike the in-memory reference, which round-trips diff --git a/packages/ai-persistence-drizzle/tests/api-types.test-d.ts b/packages/ai-persistence-drizzle/tests/api-types.test-d.ts index bf414cb40..0700df58f 100644 --- a/packages/ai-persistence-drizzle/tests/api-types.test-d.ts +++ b/packages/ai-persistence-drizzle/tests/api-types.test-d.ts @@ -1,19 +1,31 @@ import { expectTypeOf } from 'vitest' import type { DrizzleD1Database } from 'drizzle-orm/d1' +import type { PgliteDatabase } from 'drizzle-orm/pglite' import type { InterruptStore, MessageStore, MetadataStore, RunStore, } from '@tanstack/ai-persistence' -import { drizzlePersistence, schema } from '../src/index' +import { + createDefaultPgSchema, + createDefaultSqliteSchema, + drizzlePersistence, +} from '../src/index' import { sqlitePersistence } from '../src/sqlite' import { schema as emittedSchema } from '../src/assets/tanstack-ai-schema' +import { schema as emittedPgSchema } from '../src/assets/tanstack-ai-schema-pg' import { variantSchema } from './variant-schema' -import type { TanstackAiSqliteSchema } from '../src/index' +import type { TanstackAiPgSchema, TanstackAiSqliteSchema } from '../src/index' declare const d1Database: DrizzleD1Database -const d1Persistence = drizzlePersistence(d1Database) +declare const pgDatabase: PgliteDatabase +const defaultSchema = createDefaultSqliteSchema() +const defaultPgSchema = createDefaultPgSchema() +const d1Persistence = drizzlePersistence(d1Database, { + provider: 'sqlite', + schema: defaultSchema, +}) expectTypeOf(d1Persistence.stores.messages).toEqualTypeOf() expectTypeOf(d1Persistence.stores.runs).toEqualTypeOf() expectTypeOf(d1Persistence.stores.interrupts).toEqualTypeOf() @@ -21,17 +33,38 @@ expectTypeOf(d1Persistence.stores.metadata).toEqualTypeOf() // No `locks` store: this backend has no distributed lock (see drizzlePersistence). expectTypeOf(d1Persistence.stores).not.toHaveProperty('locks') -const sqlite = sqlitePersistence({ url: ':memory:', migrate: true }) +const pgPersistence = drizzlePersistence(pgDatabase, { + provider: 'pg', + schema: defaultPgSchema, +}) +expectTypeOf(pgPersistence.stores).toEqualTypeOf() + +// Provider, db, and schema dialects must agree. +// @ts-expect-error sqlite db cannot be used with provider 'pg' +drizzlePersistence(d1Database, { provider: 'pg', schema: defaultPgSchema }) +// @ts-expect-error pg db cannot be used with provider 'sqlite' +drizzlePersistence(pgDatabase, { provider: 'sqlite', schema: defaultSchema }) +// @ts-expect-error pg schema cannot be used with provider 'sqlite' +drizzlePersistence(d1Database, { provider: 'sqlite', schema: defaultPgSchema }) +// @ts-expect-error sqlite schema cannot be used with provider 'pg' +drizzlePersistence(pgDatabase, { provider: 'pg', schema: defaultSchema }) +// @ts-expect-error provider is required +drizzlePersistence(d1Database, { schema: defaultSchema }) + +const sqlite = sqlitePersistence({ url: ':memory:' }) expectTypeOf(sqlite.stores).toEqualTypeOf() expectTypeOf(sqlite.close).toEqualTypeOf<() => void>() -// The bundled schema, the emitted schema asset, and a renamed/extended variant -// must all satisfy the injectable schema contract. -expectTypeOf(schema).toExtend() +// Default factories, emitted assets, and renamed/extended variant all satisfy +// the injectable schema contracts. +expectTypeOf(defaultSchema).toExtend() expectTypeOf(emittedSchema).toExtend() expectTypeOf(variantSchema).toExtend() +expectTypeOf(defaultPgSchema).toExtend() +expectTypeOf(emittedPgSchema).toExtend() const customPersistence = drizzlePersistence(d1Database, { + provider: 'sqlite', schema: variantSchema, }) expectTypeOf(customPersistence.stores).toEqualTypeOf< diff --git a/packages/ai-persistence-drizzle/tests/custom-schema.test.ts b/packages/ai-persistence-drizzle/tests/custom-schema.test.ts index 7f94e73d0..b21457469 100644 --- a/packages/ai-persistence-drizzle/tests/custom-schema.test.ts +++ b/packages/ai-persistence-drizzle/tests/custom-schema.test.ts @@ -4,7 +4,12 @@ import { eq } from 'drizzle-orm' import { sqliteTable, text } from 'drizzle-orm/sqlite-core' import { drizzle } from 'drizzle-orm/sqlite-proxy' import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit' -import { DrizzleSchemaError, drizzlePersistence, schema } from '../src/index' +import { + createDefaultPgSchema, + createDefaultSqliteSchema, + DrizzleSchemaError, + drizzlePersistence, +} from '../src/index' import { variantDdl, variantSchema } from './variant-schema' import type { TanstackAiSqliteSchema } from '../src/index' import type { DrizzleSqliteDb } from '../src/index' @@ -29,10 +34,14 @@ function createVariantDb(): { db: DrizzleSqliteDb; sqlite: DatabaseSync } { } // The full store contract must hold when the runtime operates over a schema -// whose table and column database names all differ from the bundled ones. +// whose table and column database names all differ from the defaults. runPersistenceConformance( 'drizzle-sqlite (injected variant schema)', - () => drizzlePersistence(createVariantDb().db, { schema: variantSchema }), + () => + drizzlePersistence(createVariantDb().db, { + provider: 'sqlite', + schema: variantSchema, + }), // This backend has no distributed lock primitive. { skip: ['locks'] }, ) @@ -40,7 +49,10 @@ runPersistenceConformance( describe('drizzlePersistence with an injected schema', () => { it('writes through the injected table and column names', async () => { const { db, sqlite } = createVariantDb() - const persistence = drizzlePersistence(db, { schema: variantSchema }) + const persistence = drizzlePersistence(db, { + provider: 'sqlite', + schema: variantSchema, + }) await persistence.stores.messages.saveThread('thread-1', [ { role: 'user', content: 'hello' }, @@ -57,7 +69,10 @@ describe('drizzlePersistence with an injected schema', () => { it('coexists with app-owned columns on the same tables', async () => { const { db, sqlite } = createVariantDb() - const persistence = drizzlePersistence(db, { schema: variantSchema }) + const persistence = drizzlePersistence(db, { + provider: 'sqlite', + schema: variantSchema, + }) await persistence.stores.messages.saveThread('thread-owned', [ { role: 'user', content: 'mine' }, @@ -85,29 +100,44 @@ describe('drizzlePersistence with an injected schema', () => { it('rejects a schema with missing tables or columns', () => { const { db } = createVariantDb() + const defaults = createDefaultSqliteSchema() - const { metadata: _dropped, ...withoutMetadata } = schema + const { metadata: _dropped, ...withoutMetadata } = defaults expect(() => drizzlePersistence(db, { + provider: 'sqlite', schema: withoutMetadata as unknown as TanstackAiSqliteSchema, }), ).toThrow(DrizzleSchemaError) expect(() => drizzlePersistence(db, { + provider: 'sqlite', schema: withoutMetadata as unknown as TanstackAiSqliteSchema, }), - ).toThrow(/`metadata` is not a Drizzle SQLite table/) + ).toThrow(/`metadata` is not a Drizzle SQLite table \(provider 'sqlite'\)/) const incompleteMessages = sqliteTable('messages', { threadId: text('thread_id').primaryKey(), }) expect(() => drizzlePersistence(db, { + provider: 'sqlite', schema: { - ...schema, + ...defaults, messages: incompleteMessages, } as unknown as TanstackAiSqliteSchema, }), ).toThrow(/`messages\.messagesJson` is missing/) }) + + it('rejects a schema whose tables are the wrong dialect for the provider', () => { + const { db } = createVariantDb() + const pgDefaults = createDefaultPgSchema() + expect(() => + drizzlePersistence(db, { + provider: 'sqlite', + schema: pgDefaults as unknown as TanstackAiSqliteSchema, + }), + ).toThrow(/`messages` is not a Drizzle SQLite table \(provider 'sqlite'\)/) + }) }) diff --git a/packages/ai-persistence-drizzle/tests/drizzle.conformance.test.ts b/packages/ai-persistence-drizzle/tests/drizzle.conformance.test.ts index 7c6e03df4..ce4109432 100644 --- a/packages/ai-persistence-drizzle/tests/drizzle.conformance.test.ts +++ b/packages/ai-persistence-drizzle/tests/drizzle.conformance.test.ts @@ -3,7 +3,7 @@ import { sqlitePersistence } from '../src/sqlite' runPersistenceConformance( 'drizzle-sqlite', - () => sqlitePersistence({ url: ':memory:', migrate: true }), + () => sqlitePersistence({ url: ':memory:' }), // This backend has no distributed lock primitive. { skip: ['locks'] }, ) diff --git a/packages/ai-persistence-drizzle/tests/migration-cli.test.ts b/packages/ai-persistence-drizzle/tests/migration-cli.test.ts deleted file mode 100644 index 2743a2cbb..000000000 --- a/packages/ai-persistence-drizzle/tests/migration-cli.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { afterEach, describe, expect, it } from 'vitest' -import { sqliteMigrations } from '../src/migrations' -import { runDrizzleMigrationsCli } from '../src/migration-cli' - -const temporaryDirectories: Array = [] - -async function createTemporaryDirectory(): Promise { - const directory = await mkdtemp(join(tmpdir(), 'tanstack-ai-drizzle-cli-')) - temporaryDirectories.push(directory) - return directory -} - -afterEach(async () => { - await Promise.all( - temporaryDirectories - .splice(0) - .map((directory) => rm(directory, { recursive: true, force: true })), - ) -}) - -describe('drizzle migrations CLI', () => { - it('prints canonical migration SQL to stdout', async () => { - let output = '' - await runDrizzleMigrationsCli(['--stdout'], { - writeStdout(value) { - output += value - }, - }) - - expect(output).toBe(`${sqliteMigrations[0]?.sql.trimEnd()}\n`) - }) - - it('copies migrations without overwriting divergent files by default', async () => { - const directory = await createTemporaryDirectory() - await runDrizzleMigrationsCli(['--out', directory]) - - const migration = sqliteMigrations[0] - expect(migration).toBeDefined() - if (!migration) return - const destination = join(directory, migration.filename) - expect(await readFile(destination, 'utf8')).toBe(migration.sql) - - await writeFile(destination, 'user-owned contents', 'utf8') - await expect(runDrizzleMigrationsCli(['--out', directory])).rejects.toThrow( - /refusing to overwrite/i, - ) - expect(await readFile(destination, 'utf8')).toBe('user-owned contents') - - await runDrizzleMigrationsCli(['--out', directory, '--force']) - expect(await readFile(destination, 'utf8')).toBe(migration.sql) - }) - - it('fails on ambiguous or incomplete output options', async () => { - const directory = await createTemporaryDirectory() - await expect(runDrizzleMigrationsCli([])).rejects.toThrow( - /--out.*--stdout/i, - ) - await expect( - runDrizzleMigrationsCli(['--stdout', '--out', directory]), - ).rejects.toThrow(/exactly one/i) - }) -}) diff --git a/packages/ai-persistence-drizzle/tests/migrations.test.ts b/packages/ai-persistence-drizzle/tests/migrations.test.ts deleted file mode 100644 index f9a016907..000000000 --- a/packages/ai-persistence-drizzle/tests/migrations.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { DatabaseSync } from 'node:sqlite' -import { afterEach, describe, expect, it } from 'vitest' -import { sqliteMigrations } from '../src/migrations' -import { applySqliteMigrations } from '../src/sqlite-migrations' - -const databases: Array = [] - -function createDatabase(): DatabaseSync { - const database = new DatabaseSync(':memory:') - databases.push(database) - return database -} - -afterEach(() => { - for (const database of databases.splice(0)) database.close() -}) - -describe('sqlite migrations', () => { - it('exposes an ordered canonical manifest and applies it idempotently', () => { - expect(sqliteMigrations.map((migration) => migration.id)).toEqual([ - '0000_tanstack_ai_initial', - ]) - - const database = createDatabase() - applySqliteMigrations(database, sqliteMigrations) - applySqliteMigrations(database, sqliteMigrations) - - const tableNames = database - .prepare( - "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", - ) - .all() - .map((row) => row.name) - expect(tableNames).toEqual( - expect.arrayContaining([ - '__tanstack_ai_migrations', - 'interrupts', - 'messages', - 'metadata', - 'runs', - ]), - ) - expect( - database - .prepare('SELECT migration_id FROM __tanstack_ai_migrations') - .all(), - ).toEqual([{ migration_id: '0000_tanstack_ai_initial' }]) - }) - - it('rolls back migration SQL and bookkeeping together, then permits retry', () => { - const database = createDatabase() - const failingMigration = { - id: '0000_retry_test', - filename: '0000_retry_test.sql', - sql: ` - CREATE TABLE retry_target (id INTEGER PRIMARY KEY); - CREATE TRIGGER reject_migration_bookkeeping - BEFORE INSERT ON __tanstack_ai_migrations - BEGIN - SELECT RAISE(ABORT, 'bookkeeping failed'); - END; - `, - } - - expect(() => applySqliteMigrations(database, [failingMigration])).toThrow( - 'bookkeeping failed', - ) - expect( - database - .prepare( - "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'retry_target'", - ) - .get(), - ).toBeUndefined() - expect( - database - .prepare('SELECT migration_id FROM __tanstack_ai_migrations') - .all(), - ).toEqual([]) - - applySqliteMigrations(database, [ - { - ...failingMigration, - sql: 'CREATE TABLE retry_target (id INTEGER PRIMARY KEY);', - }, - ]) - - expect( - database - .prepare( - "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'retry_target'", - ) - .get(), - ).toEqual({ name: 'retry_target' }) - expect( - database - .prepare('SELECT migration_id FROM __tanstack_ai_migrations') - .all(), - ).toEqual([{ migration_id: '0000_retry_test' }]) - }) -}) diff --git a/packages/ai-persistence-drizzle/tests/package-contract.test.ts b/packages/ai-persistence-drizzle/tests/package-contract.test.ts index dfcdb7a6b..83161a1fd 100644 --- a/packages/ai-persistence-drizzle/tests/package-contract.test.ts +++ b/packages/ai-persistence-drizzle/tests/package-contract.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest' import packageJson from '../package.json' describe('drizzle package contract', () => { - it('publishes the edge root, Node sqlite subpath, CLI, and migration assets', () => { + it('publishes the edge root, Node sqlite subpath, and schema-emit CLI only', () => { expect(packageJson.exports).toEqual({ '.': { types: './dist/esm/index.d.ts', @@ -14,22 +14,40 @@ describe('drizzle package contract', () => { types: './dist/esm/sqlite.d.ts', import: './dist/esm/sqlite.js', }, + './sqlite-schema': { + types: './dist/esm/default-sqlite-schema.d.ts', + import: './dist/esm/default-sqlite-schema.js', + }, + './pg-schema': { + types: './dist/esm/default-pg-schema.d.ts', + import: './dist/esm/default-pg-schema.js', + }, }) expect(packageJson.bin).toEqual({ - 'tanstack-ai-drizzle-migrations': - './bin/tanstack-ai-drizzle-migrations.mjs', 'tanstack-ai-drizzle-schema': './bin/tanstack-ai-drizzle-schema.mjs', }) - expect(packageJson.files).toEqual( - expect.arrayContaining(['bin', 'dist', 'src', 'drizzle']), - ) + expect(packageJson.files).toEqual(['bin', 'dist', 'src']) + expect(packageJson.bin).not.toHaveProperty('tanstack-ai-drizzle-migrations') + expect(packageJson.description.toLowerCase()).toMatch(/schema-first/) + }) + + it('does not ship SQL migrations or drizzle-kit journals', async () => { + const root = fileURLToPath(new URL('..', import.meta.url)) + await expect( + readFile(`${root}/src/assets/0000_tanstack_ai_initial.sql`, 'utf8'), + ).rejects.toMatchObject({ code: 'ENOENT' }) + await expect( + readFile(`${root}/drizzle/0000_tanstack_ai_initial.sql`, 'utf8'), + ).rejects.toMatchObject({ code: 'ENOENT' }) }) it('keeps Node built-ins and Buffer out of the root import graph', async () => { const rootFiles = [ 'index.ts', - 'migrations.ts', - 'schema.ts', + 'default-sqlite-schema.ts', + 'default-pg-schema.ts', + 'ensure-sqlite-tables.ts', + 'ensure-pg-tables.ts', 'schema-contract.ts', 'schema-source.ts', 'stores.ts', @@ -46,22 +64,9 @@ describe('drizzle package contract', () => { fileURLToPath(new URL('../src/index.ts', import.meta.url)), 'utf8', ) - expect(root).not.toMatch(/from ['"].*sqlite/) - }) - - it('keeps the shipped drizzle-kit migration equal to the embedded asset', async () => { - const embedded = await readFile( - fileURLToPath( - new URL('../src/assets/0000_tanstack_ai_initial.sql', import.meta.url), - ), - 'utf8', - ) - const shipped = await readFile( - fileURLToPath( - new URL('../drizzle/0000_tanstack_ai_initial.sql', import.meta.url), - ), - 'utf8', - ) - expect(shipped).toBe(embedded) + // Root must not pull the Node-only `/sqlite` entry or `node:sqlite`. + expect(root).not.toMatch(/from ['"]\.\/sqlite['"]/) + expect(root).not.toMatch(/from ['"]node:sqlite['"]/) + expect(root).not.toMatch(/sqliteMigrations/) }) }) diff --git a/packages/ai-persistence-drizzle/tests/pg.conformance.test.ts b/packages/ai-persistence-drizzle/tests/pg.conformance.test.ts new file mode 100644 index 000000000..6108611fc --- /dev/null +++ b/packages/ai-persistence-drizzle/tests/pg.conformance.test.ts @@ -0,0 +1,28 @@ +import { PGlite } from '@electric-sql/pglite' +import { drizzle } from 'drizzle-orm/pglite' +import { runPersistenceConformance } from '@tanstack/ai-persistence/testkit' +import { afterAll } from 'vitest' +import { + createDefaultPgSchema, + drizzlePersistence, + ensurePgTables, +} from '../src/index' + +const clients: Array = [] + +runPersistenceConformance( + 'drizzle-pg', + async () => { + const client = new PGlite() + clients.push(client) + const schema = createDefaultPgSchema() + await ensurePgTables((sql) => client.exec(sql), schema) + return drizzlePersistence(drizzle(client), { provider: 'pg', schema }) + }, + // This backend has no distributed lock primitive. + { skip: ['locks'] }, +) + +afterAll(async () => { + await Promise.all(clients.map((client) => client.close())) +}) diff --git a/packages/ai-persistence-drizzle/tests/schema-cli.test.ts b/packages/ai-persistence-drizzle/tests/schema-cli.test.ts index c24afee87..a618dc113 100644 --- a/packages/ai-persistence-drizzle/tests/schema-cli.test.ts +++ b/packages/ai-persistence-drizzle/tests/schema-cli.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { drizzleSchemaFilename, drizzleSchemaSource } from '../src/index' +import { drizzleSchemaSources } from '../src/schema-source' import { runDrizzleSchemaCli } from '../src/schema-cli' const temporaryDirectories: Array = [] @@ -31,7 +32,9 @@ describe('Drizzle schema CLI', () => { }) expect(output).toBe(`${drizzleSchemaSource.trimEnd()}\n`) expect(output).toContain('sqliteTable') - expect(output).toContain('drizzlePersistence(db, { schema })') + expect(output).toContain( + "drizzlePersistence(db, { provider: 'sqlite', schema })", + ) }) it('copies the schema without overwriting divergent files by default', async () => { @@ -54,6 +57,35 @@ describe('Drizzle schema CLI', () => { expect(await readFile(destination, 'utf8')).toBe(drizzleSchemaSource) }) + it('emits the pg schema with --dialect pg', async () => { + let output = '' + await runDrizzleSchemaCli(['--stdout', '--dialect', 'pg'], { + writeStdout(value) { + output += value + }, + }) + expect(output).toBe(`${drizzleSchemaSources.pg.trimEnd()}\n`) + expect(output).toContain('pgTable') + expect(output).toContain( + "drizzlePersistence(db, { provider: 'pg', schema })", + ) + + const directory = await createTemporaryDirectory() + await runDrizzleSchemaCli(['--out', directory, '--dialect', 'pg']) + expect(await readFile(join(directory, drizzleSchemaFilename), 'utf8')).toBe( + drizzleSchemaSources.pg, + ) + }) + + it('rejects unknown or repeated dialects', async () => { + await expect( + runDrizzleSchemaCli(['--stdout', '--dialect', 'mysql']), + ).rejects.toThrow(/--dialect must be "sqlite" or "pg"/) + await expect( + runDrizzleSchemaCli(['--stdout', '--dialect', 'pg', '--dialect', 'pg']), + ).rejects.toThrow(/--dialect may only be provided once/) + }) + it('fails on ambiguous or incomplete output options', async () => { const directory = await createTemporaryDirectory() await expect(runDrizzleSchemaCli([])).rejects.toThrow(/--out.*--stdout/i) diff --git a/packages/ai-persistence-drizzle/tests/schema-source.test.ts b/packages/ai-persistence-drizzle/tests/schema-source.test.ts index f72716a3f..f93a04b95 100644 --- a/packages/ai-persistence-drizzle/tests/schema-source.test.ts +++ b/packages/ai-persistence-drizzle/tests/schema-source.test.ts @@ -1,11 +1,18 @@ import { describe, expect, it } from 'vitest' import { getTableConfig } from 'drizzle-orm/sqlite-core' -import { schema as bundled } from '../src/schema' +import { getTableConfig as getPgTableConfig } from 'drizzle-orm/pg-core' +import { createDefaultSqliteSchema } from '../src/default-sqlite-schema' +import { createDefaultPgSchema } from '../src/default-pg-schema' import { schema as emitted } from '../src/assets/tanstack-ai-schema' +import { schema as emittedPg } from '../src/assets/tanstack-ai-schema-pg' +import type { PgTable } from 'drizzle-orm/pg-core' import type { SQLiteTable } from 'drizzle-orm/sqlite-core' -function normalize(table: SQLiteTable) { - const config = getTableConfig(table) +function normalizeConfig( + config: + | ReturnType + | ReturnType, +) { return { name: config.name, columns: config.columns.map((column) => ({ @@ -17,15 +24,44 @@ function normalize(table: SQLiteTable) { primaryKeys: config.primaryKeys.map((primaryKey) => primaryKey.columns.map((column) => column.name), ), + indexes: config.indexes.map((index) => ({ + name: index.config.name, + unique: index.config.unique, + columns: index.config.columns.map((column) => + 'name' in column ? column.name : String(column), + ), + })), } } +function normalize(table: SQLiteTable) { + return normalizeConfig(getTableConfig(table)) +} + +function normalizePg(table: PgTable) { + return normalizeConfig(getPgTableConfig(table)) +} + describe('emitted schema asset', () => { - it('is structurally identical to the bundled schema', () => { - const tableKeys = Object.keys(bundled) as Array - expect(Object.keys(emitted)).toEqual(Object.keys(bundled)) + it('is structurally identical to createDefaultSqliteSchema()', () => { + const defaults = createDefaultSqliteSchema() + const tableKeys = Object.keys(defaults) as Array + expect(Object.keys(emitted)).toEqual(Object.keys(defaults)) + for (const key of tableKeys) { + expect(normalize(emitted[key]), key).toEqual(normalize(defaults[key])) + } + }) +}) + +describe('emitted pg schema asset', () => { + it('is structurally identical to createDefaultPgSchema()', () => { + const defaults = createDefaultPgSchema() + const tableKeys = Object.keys(defaults) as Array + expect(Object.keys(emittedPg)).toEqual(Object.keys(defaults)) for (const key of tableKeys) { - expect(normalize(emitted[key]), key).toEqual(normalize(bundled[key])) + expect(normalizePg(emittedPg[key]), key).toEqual( + normalizePg(defaults[key]), + ) } }) }) diff --git a/packages/ai-persistence-drizzle/tests/sqlite.test.ts b/packages/ai-persistence-drizzle/tests/sqlite.test.ts index 299081cc0..e17426eff 100644 --- a/packages/ai-persistence-drizzle/tests/sqlite.test.ts +++ b/packages/ai-persistence-drizzle/tests/sqlite.test.ts @@ -1,7 +1,10 @@ +import { DatabaseSync } from 'node:sqlite' import { mkdtemp, rm, stat } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' +import { createDefaultSqliteSchema } from '../src/default-sqlite-schema' +import { ensureSqliteTables } from '../src/ensure-sqlite-tables' import { sqlitePersistence } from '../src/sqlite' const temporaryDirectories: Array = [] @@ -24,7 +27,6 @@ describe('sqlitePersistence', () => { const databasePath = join(root, 'nested', 'state.sqlite') const persistence = sqlitePersistence({ url: `file:${databasePath}`, - migrate: true, }) try { @@ -50,4 +52,47 @@ describe('sqlitePersistence', () => { sqlitePersistence({ url: 'https://example.test/state.sqlite' }), ).toThrow(/unsupported SQLite URL/i) }) + + it('bootstraps tables from the schema when ensureTables is true', async () => { + const persistence = sqlitePersistence({ url: ':memory:' }) + try { + await persistence.stores.messages.saveThread('t1', [ + { role: 'user', content: 'hi' }, + ]) + expect(await persistence.stores.messages.loadThread('t1')).toEqual([ + { role: 'user', content: 'hi' }, + ]) + } finally { + persistence.close() + } + }) + + it('skips runtime bootstrap when ensureTables is false', () => { + expect(() => + sqlitePersistence({ + url: ':memory:', + ensureTables: false, + }), + ).not.toThrow() + }) +}) + +describe('ensureSqliteTables', () => { + it('is idempotent for the default schema', () => { + const sqlite = new DatabaseSync(':memory:') + const schema = createDefaultSqliteSchema() + ensureSqliteTables((sql) => sqlite.exec(sql), schema) + ensureSqliteTables((sql) => sqlite.exec(sql), schema) + + const names = sqlite + .prepare( + "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", + ) + .all() + .map((row) => row.name) + expect(names).toEqual( + expect.arrayContaining(['interrupts', 'messages', 'metadata', 'runs']), + ) + sqlite.close() + }) }) diff --git a/packages/ai-persistence-drizzle/tests/store-behavior.test.ts b/packages/ai-persistence-drizzle/tests/store-behavior.test.ts index f5e680a9b..69d26704a 100644 --- a/packages/ai-persistence-drizzle/tests/store-behavior.test.ts +++ b/packages/ai-persistence-drizzle/tests/store-behavior.test.ts @@ -6,7 +6,7 @@ type Persistence = ReturnType const open: Array = [] function persistence(): Persistence { - const created = sqlitePersistence({ url: ':memory:', migrate: true }) + const created = sqlitePersistence({ url: ':memory:' }) open.push(created) return created } diff --git a/packages/ai-persistence-drizzle/vite.config.ts b/packages/ai-persistence-drizzle/vite.config.ts index 05551030c..d1ed167da 100644 --- a/packages/ai-persistence-drizzle/vite.config.ts +++ b/packages/ai-persistence-drizzle/vite.config.ts @@ -30,12 +30,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: [ - './src/index.ts', - './src/sqlite.ts', - './src/cli.ts', - './src/schema-cli-main.ts', - ], + entry: ['./src/index.ts', './src/sqlite.ts', './src/schema-cli-main.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/ai-persistence/tests/with-persistence.test.ts b/packages/ai-persistence/tests/with-persistence.test.ts index e5d80b47f..77d846e02 100644 --- a/packages/ai-persistence/tests/with-persistence.test.ts +++ b/packages/ai-persistence/tests/with-persistence.test.ts @@ -198,6 +198,7 @@ describe('withPersistence (state-only)', () => { { type: EventType.TEXT_MESSAGE_START, messageId: 'assistant-42', + role: 'assistant' as const, timestamp: 1, }, ev.text('hello'), diff --git a/packages/ai/skills/ai-core/middleware/SKILL.md b/packages/ai/skills/ai-core/middleware/SKILL.md index 3d451bd22..348364cc4 100644 --- a/packages/ai/skills/ai-core/middleware/SKILL.md +++ b/packages/ai/skills/ai-core/middleware/SKILL.md @@ -394,7 +394,6 @@ import { sqlitePersistence } from '@tanstack/ai-persistence-drizzle/sqlite' const persistence = sqlitePersistence({ url: 'file:.tanstack-ai/state.sqlite', - migrate: true, }) export async function POST(request: Request) { @@ -434,14 +433,20 @@ Every backend returns an `AIPersistence` you pass straight to | Backend | Factory | Import | | -------------------------------------- | ----------------------------------------------- | ----------------------------------------- | | In-memory (dev/tests) | `memoryPersistence()` | `@tanstack/ai-persistence` | -| Drizzle SQLite-family (edge-safe) | `drizzlePersistence(db)` | `@tanstack/ai-persistence-drizzle` | -| Node SQLite convenience factory | `sqlitePersistence({ url, migrate })` | `@tanstack/ai-persistence-drizzle/sqlite` | +| Drizzle SQLite/Postgres (edge-safe) | `drizzlePersistence(db, { provider, schema })` | `@tanstack/ai-persistence-drizzle` | +| Node SQLite convenience factory | `sqlitePersistence({ url })` | `@tanstack/ai-persistence-drizzle/sqlite` | | Prisma | `prismaPersistence(prisma)` | `@tanstack/ai-persistence-prisma` | | Cloudflare (D1 + Durable Object locks) | `cloudflarePersistence({ d1, durableObjects })` | `@tanstack/ai-persistence-cloudflare` | -`drizzlePersistence(db)` is the edge-safe root — pass an already-migrated -SQLite-compatible Drizzle database (including Cloudflare D1). `sqlitePersistence` -is Node-only and lives at the `/sqlite` subpath. Compose backends per store with +`drizzlePersistence(db, { provider, schema })` is the edge-safe root — +`provider` is `'sqlite'` (any SQLite-compatible Drizzle database, including +Cloudflare D1) or `'pg'` (node-postgres, postgres.js, Neon, PGlite), and `db` +plus the required `schema` must match it (enforced by overloads and a runtime +dialect check). Get the schema by re-exporting the `/sqlite-schema` or +`/pg-schema` subpath (stock tables), emitting an owned copy via +`tanstack-ai-drizzle-schema [--dialect pg]`, or `createDefaultSqliteSchema()` / +`createDefaultPgSchema()`. `sqlitePersistence` is Node-only and lives at the +`/sqlite` subpath. Compose backends per store with `composePersistence(base, { overrides })`. ### Resume reconstruction is the middleware's job (server-authoritative path) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c15f8e3b..56b3a444e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -357,7 +357,7 @@ importers: version: 17.2.3 nitro: specifier: 3.0.260610-beta - version: 3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + version: 3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -514,7 +514,7 @@ importers: version: 15.0.12 nitro: specifier: 3.0.260610-beta - version: 3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + version: 3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) puppeteer: specifier: ^24.34.0 version: 24.39.1(supports-color@7.2.0)(typescript@5.9.3) @@ -608,7 +608,7 @@ importers: version: 0.10.0 nitro: specifier: 3.0.260610-beta - version: 3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + version: 3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) react: specifier: ^19.2.3 version: 19.2.3 @@ -804,7 +804,7 @@ importers: version: 0.561.0(react@19.2.3) nitro: specifier: 3.0.260610-beta - version: 3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + version: 3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) react: specifier: ^19.2.3 version: 19.2.3 @@ -907,7 +907,7 @@ importers: version: 0.561.0(react@19.2.3) nitro: specifier: 3.0.260610-beta - version: 3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + version: 3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) react: specifier: ^19.2.3 version: 19.2.3 @@ -1041,7 +1041,7 @@ importers: version: 0.561.0(react@19.2.3) nitro: specifier: 3.0.260610-beta - version: 3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + version: 3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -1138,7 +1138,7 @@ importers: version: link:../../packages/ai-solid-ui '@tanstack/nitro-v2-vite-plugin': specifier: ^1.155.0 - version: 1.155.0(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 1.155.0(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/router-plugin': specifier: ^1.158.4 version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) @@ -2108,13 +2108,16 @@ importers: version: 4.0.14(supports-color@7.2.0)(vitest@4.1.10) drizzle-orm: specifier: ^0.45.0 - version: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2))(prisma@6.19.3(typescript@7.0.2)) + version: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2))(prisma@6.19.3(typescript@7.0.2)) miniflare: specifier: ^4.20260609.0 version: 4.20260617.1 packages/ai-persistence-drizzle: devDependencies: + '@electric-sql/pglite': + specifier: ^0.3.0 + version: 0.3.16 '@tanstack/ai': specifier: workspace:* version: link:../ai @@ -2124,12 +2127,9 @@ importers: '@vitest/coverage-v8': specifier: 4.0.14 version: 4.0.14(supports-color@7.2.0)(vitest@4.1.10) - drizzle-kit: - specifier: ^0.31.0 - version: 0.31.10 drizzle-orm: specifier: ^0.45.0 - version: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2))(prisma@6.19.3(typescript@7.0.2)) + version: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2))(prisma@6.19.3(typescript@7.0.2)) packages/ai-persistence-prisma: devDependencies: @@ -2716,7 +2716,7 @@ importers: version: 0.4.1 '@tanstack/nitro-v2-vite-plugin': specifier: ^1.155.0 - version: 1.155.0(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 1.155.0(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/react-ai-devtools': specifier: workspace:* version: link:../../packages/react-ai-devtools @@ -2828,7 +2828,7 @@ importers: version: link:../../packages/ai-react-ui '@tanstack/nitro-v2-vite-plugin': specifier: ^1.155.0 - version: 1.155.0(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 1.155.0(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/react-ai-devtools': specifier: workspace:* version: link:../../packages/react-ai-devtools @@ -2843,7 +2843,7 @@ importers: version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/start': specifier: ^1.120.20 - version: 1.120.20(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + version: 1.120.20(6924bf65bc643a6fedf94a6c1c3a9e2c) highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -4035,8 +4035,8 @@ packages: '@deno/shim-deno@0.19.2': resolution: {integrity: sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q==} - '@drizzle-team/brocli@0.10.2': - resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@electric-sql/pglite@0.3.16': + resolution: {integrity: sha512-mZkZfOd9OqTMHsK+1cje8OSzfAQcpD7JmILXTl5ahdempjUDdmg4euf1biDex5/LfQIDJ3gvCu6qDgdnDxfJmA==} '@elevenlabs/client@1.3.1': resolution: {integrity: sha512-bQUxA/X7TZRSSZ6UM6a6A+1qQy5Wh7vMn+zbZP6Yl1WrupxHL4M0XMnl/n9+fsol1Ib4tN/2Nhx1E5JDS7QdKw==} @@ -4072,14 +4072,6 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' - '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} @@ -4110,12 +4102,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.20.2': resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} @@ -4146,12 +4132,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.20.2': resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} @@ -4182,12 +4162,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.20.2': resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} @@ -4218,12 +4192,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.20.2': resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} @@ -4254,12 +4222,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.20.2': resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} @@ -4290,12 +4252,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.20.2': resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} @@ -4326,12 +4282,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} @@ -4362,12 +4312,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.20.2': resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} @@ -4398,12 +4342,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.20.2': resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} @@ -4434,12 +4372,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.20.2': resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} @@ -4470,12 +4402,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.20.2': resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} @@ -4506,12 +4432,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.20.2': resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} @@ -4542,12 +4462,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.20.2': resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} @@ -4578,12 +4492,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.20.2': resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} @@ -4614,12 +4522,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.20.2': resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} @@ -4650,12 +4552,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.20.2': resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} @@ -4710,12 +4606,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.20.2': resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} @@ -4770,12 +4660,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.20.2': resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} @@ -4830,12 +4714,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} @@ -4866,12 +4744,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.20.2': resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} @@ -4902,12 +4774,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.20.2': resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} @@ -4938,12 +4804,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.20.2': resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} @@ -11129,10 +10989,6 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} - drizzle-kit@0.31.10: - resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} - hasBin: true - drizzle-orm@0.45.2: resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} peerDependencies: @@ -11383,11 +11239,6 @@ packages: esbuild: '>=0.12' solid-js: '>= 1.0' - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -18429,7 +18280,7 @@ snapshots: '@deno/shim-deno-test': 0.5.0 which: 4.0.0 - '@drizzle-team/brocli@0.10.2': {} + '@electric-sql/pglite@0.3.16': {} '@elevenlabs/client@1.3.1(@types/dom-mediacapture-record@1.0.22)': dependencies: @@ -18487,16 +18338,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.14.0 - '@esbuild/aix-ppc64@0.20.2': optional: true @@ -18512,9 +18353,6 @@ snapshots: '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - '@esbuild/android-arm64@0.20.2': optional: true @@ -18530,9 +18368,6 @@ snapshots: '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - '@esbuild/android-arm@0.20.2': optional: true @@ -18548,9 +18383,6 @@ snapshots: '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - '@esbuild/android-x64@0.20.2': optional: true @@ -18566,9 +18398,6 @@ snapshots: '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - '@esbuild/darwin-arm64@0.20.2': optional: true @@ -18584,9 +18413,6 @@ snapshots: '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - '@esbuild/darwin-x64@0.20.2': optional: true @@ -18602,9 +18428,6 @@ snapshots: '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - '@esbuild/freebsd-arm64@0.20.2': optional: true @@ -18620,9 +18443,6 @@ snapshots: '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - '@esbuild/freebsd-x64@0.20.2': optional: true @@ -18638,9 +18458,6 @@ snapshots: '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - '@esbuild/linux-arm64@0.20.2': optional: true @@ -18656,9 +18473,6 @@ snapshots: '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - '@esbuild/linux-arm@0.20.2': optional: true @@ -18674,9 +18488,6 @@ snapshots: '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - '@esbuild/linux-ia32@0.20.2': optional: true @@ -18692,9 +18503,6 @@ snapshots: '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - '@esbuild/linux-loong64@0.20.2': optional: true @@ -18710,9 +18518,6 @@ snapshots: '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - '@esbuild/linux-mips64el@0.20.2': optional: true @@ -18728,9 +18533,6 @@ snapshots: '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - '@esbuild/linux-ppc64@0.20.2': optional: true @@ -18746,9 +18548,6 @@ snapshots: '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - '@esbuild/linux-riscv64@0.20.2': optional: true @@ -18764,9 +18563,6 @@ snapshots: '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - '@esbuild/linux-s390x@0.20.2': optional: true @@ -18782,9 +18578,6 @@ snapshots: '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - '@esbuild/linux-x64@0.20.2': optional: true @@ -18812,9 +18605,6 @@ snapshots: '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - '@esbuild/netbsd-x64@0.20.2': optional: true @@ -18842,9 +18632,6 @@ snapshots: '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - '@esbuild/openbsd-x64@0.20.2': optional: true @@ -18872,9 +18659,6 @@ snapshots: '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - '@esbuild/sunos-x64@0.20.2': optional: true @@ -18890,9 +18674,6 @@ snapshots: '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - '@esbuild/win32-arm64@0.20.2': optional: true @@ -18908,9 +18689,6 @@ snapshots: '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - '@esbuild/win32-ia32@0.20.2': optional: true @@ -18926,9 +18704,6 @@ snapshots: '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - '@esbuild/win32-x64@0.20.2': optional: true @@ -22795,9 +22570,9 @@ snapshots: '@tanstack/history@1.154.14': {} - '@tanstack/nitro-v2-vite-plugin@1.155.0(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@tanstack/nitro-v2-vite-plugin@1.155.0(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - nitropack: 2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5) + nitropack: 2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5) pathe: 2.0.3 vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: @@ -22831,9 +22606,9 @@ snapshots: - uploadthing - xml2js - '@tanstack/nitro-v2-vite-plugin@1.155.0(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@tanstack/nitro-v2-vite-plugin@1.155.0(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - nitropack: 2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) + nitropack: 2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) pathe: 2.0.3 vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: @@ -22968,9 +22743,9 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-start-plugin@1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@tanstack/react-start-plugin@1.131.50(@electric-sql/pglite@0.3.16)(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@tanstack/start-plugin-core': 1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@tanstack/start-plugin-core': 1.131.50(@electric-sql/pglite@0.3.16)(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitejs/plugin-react': 4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) pathe: 2.0.3 vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) @@ -23010,11 +22785,11 @@ snapshots: - webpack - xml2js - '@tanstack/react-start-router-manifest@1.120.19(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)': + '@tanstack/react-start-router-manifest@1.120.19(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)': dependencies: '@tanstack/router-core': 1.157.16 tiny-invariant: 1.3.3 - vinxi: 0.5.3(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vinxi: 0.5.3(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23495,11 +23270,11 @@ snapshots: '@tanstack/store': 0.8.0 solid-js: 1.9.10 - '@tanstack/start-api-routes@1.120.19(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)': + '@tanstack/start-api-routes@1.120.19(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)': dependencies: '@tanstack/router-core': 1.157.16 '@tanstack/start-server-core': 1.141.1(crossws@0.4.6(srvx@0.11.17)) - vinxi: 0.5.3(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vinxi: 0.5.3(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23571,21 +23346,21 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/start-config@1.120.20(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)': + '@tanstack/start-config@1.120.20(6924bf65bc643a6fedf94a6c1c3a9e2c)': dependencies: '@tanstack/react-router': 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-plugin': 1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@tanstack/react-start-plugin': 1.131.50(@electric-sql/pglite@0.3.16)(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/router-generator': 1.141.1 '@tanstack/router-plugin': 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/server-functions-plugin': 1.141.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/start-server-functions-handler': 1.120.19(crossws@0.4.6(srvx@0.11.17)) '@vitejs/plugin-react': 4.7.0(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) import-meta-resolve: 4.2.0 - nitropack: 2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) + nitropack: 2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) ofetch: 1.5.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - vinxi: 0.5.3(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vinxi: 0.5.3(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) zod: 3.25.76 transitivePeerDependencies: @@ -23639,7 +23414,7 @@ snapshots: '@tanstack/start-fn-stubs@1.154.7': {} - '@tanstack/start-plugin-core@1.131.50(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@tanstack/start-plugin-core@1.131.50(@electric-sql/pglite@0.3.16)(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.0 @@ -23655,7 +23430,7 @@ snapshots: babel-dead-code-elimination: 1.0.10 cheerio: 1.1.2 h3: 1.13.0 - nitropack: 2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) + nitropack: 2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) pathe: 2.0.3 ufo: 1.6.3 vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) @@ -23879,13 +23654,13 @@ snapshots: dependencies: '@tanstack/router-core': 1.159.4 - '@tanstack/start@1.120.20(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)': + '@tanstack/start@1.120.20(6924bf65bc643a6fedf94a6c1c3a9e2c)': dependencies: '@tanstack/react-start-client': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-router-manifest': 1.120.19(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@tanstack/react-start-router-manifest': 1.120.19(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) '@tanstack/react-start-server': 1.141.1(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/start-api-routes': 1.120.19(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@tanstack/start-config': 1.120.20(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + '@tanstack/start-api-routes': 1.120.19(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(crossws@0.4.6(srvx@0.11.17))(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@tanstack/start-config': 1.120.20(6924bf65bc643a6fedf94a6c1c3a9e2c) '@tanstack/start-server-functions-client': 1.131.50(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/start-server-functions-handler': 1.120.19(crossws@0.4.6(srvx@0.11.17)) '@tanstack/start-server-functions-server': 1.131.2(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) @@ -25906,13 +25681,15 @@ snapshots: dayjs@1.11.19: {} - db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))): + db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))): optionalDependencies: - drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)) + '@electric-sql/pglite': 0.3.16 + drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)) - db0@0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))): + db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))): optionalDependencies: - drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)) + '@electric-sql/pglite': 0.3.16 + drizzle-orm: 0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)) de-indent@1.0.2: {} @@ -26093,32 +25870,28 @@ snapshots: dotenv@8.6.0: {} - drizzle-kit@0.31.10: - dependencies: - '@drizzle-team/brocli': 0.10.2 - '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.25.12 - tsx: 4.21.0 - - drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)): + drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)): optionalDependencies: '@cloudflare/workers-types': 4.20260317.1 + '@electric-sql/pglite': 0.3.16 '@opentelemetry/api': 1.9.1 '@prisma/client': 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) prisma: 6.19.3(magicast@0.5.2)(typescript@5.9.3) optional: true - drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)): + drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)): optionalDependencies: '@cloudflare/workers-types': 4.20260317.1 + '@electric-sql/pglite': 0.3.16 '@opentelemetry/api': 1.9.1 '@prisma/client': 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) prisma: 6.19.3(magicast@0.5.2)(typescript@5.9.3) optional: true - drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2))(prisma@6.19.3(typescript@7.0.2)): + drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2))(prisma@6.19.3(typescript@7.0.2)): optionalDependencies: '@cloudflare/workers-types': 4.20260317.1 + '@electric-sql/pglite': 0.3.16 '@opentelemetry/api': 1.9.1 '@prisma/client': 6.19.3(prisma@6.19.3(typescript@7.0.2))(typescript@7.0.2) prisma: 6.19.3(typescript@7.0.2) @@ -26268,31 +26041,6 @@ snapshots: transitivePeerDependencies: - supports-color - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 @@ -29205,11 +28953,11 @@ snapshots: rollup: 4.60.1 tailwindcss: 4.1.18 - nitro@3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0): + nitro@3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0): dependencies: consola: 3.4.2 crossws: 0.4.6(srvx@0.11.17) - db0: 0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) env-runner: 0.1.14(miniflare@4.20260617.1)(wrangler@4.103.0) h3: 2.0.1-rc.22(crossws@0.4.6(srvx@0.11.17)) hookable: 6.1.1 @@ -29220,7 +28968,7 @@ snapshots: rolldown: 1.1.5 srvx: 0.11.17 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.2.3 giget: 2.0.0 @@ -29259,11 +29007,11 @@ snapshots: - uploadthing - wrangler - nitro@3.0.260610-beta(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0): + nitro@3.0.260610-beta(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(chokidar@5.0.0)(dotenv@17.2.3)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260617.1)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0): dependencies: consola: 3.4.2 crossws: 0.4.6(srvx@0.11.17) - db0: 0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) env-runner: 0.1.14(miniflare@4.20260617.1)(wrangler@4.103.0) h3: 2.0.1-rc.22(crossws@0.4.6(srvx@0.11.17)) hookable: 6.1.1 @@ -29274,7 +29022,7 @@ snapshots: rolldown: 1.1.5 srvx: 0.11.17 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.2.3 giget: 2.0.0 @@ -29313,7 +29061,7 @@ snapshots: - uploadthing - wrangler - nitropack@2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5): + nitropack@2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3)))(rolldown@1.1.5): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.60.1) @@ -29334,7 +29082,7 @@ snapshots: cookie-es: 2.0.1 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))) defu: 6.1.4 destr: 2.0.5 dot-prop: 10.1.0 @@ -29380,7 +29128,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.6.0 unplugin-utils: 0.3.1 - unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))))(ioredis@5.9.2) + unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))))(ioredis@5.9.2) untyped: 2.0.0 unwasm: 0.5.3 youch: 4.1.0-beta.13 @@ -29415,7 +29163,7 @@ snapshots: - supports-color - uploadthing - nitropack@2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5): + nitropack@2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.60.1) @@ -29436,7 +29184,7 @@ snapshots: cookie-es: 2.0.1 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) defu: 6.1.4 destr: 2.0.5 dot-prop: 10.1.0 @@ -29482,7 +29230,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.6.0 unplugin-utils: 0.3.1 - unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ioredis@5.9.2) + unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ioredis@5.9.2) untyped: 2.0.0 unwasm: 0.5.3 youch: 4.1.0-beta.13 @@ -32268,7 +32016,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))))(ioredis@5.9.2): + unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))))(ioredis@5.9.2): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -32280,10 +32028,10 @@ snapshots: ufo: 1.6.3 optionalDependencies: aws4fetch: 1.0.20 - db0: 0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.3))) ioredis: 5.9.2 - unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ioredis@5.9.2): + unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ioredis@5.9.2): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 @@ -32295,14 +32043,14 @@ snapshots: ufo: 1.6.3 optionalDependencies: aws4fetch: 1.0.20 - db0: 0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) ioredis: 5.9.2 - unstorage@2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ofetch@2.0.0-alpha.3): optionalDependencies: aws4fetch: 1.0.20 chokidar: 5.0.0 - db0: 0.3.4(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) + db0: 0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))) ofetch: 2.0.0-alpha.3 untun@0.1.3: @@ -32403,7 +32151,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vinxi@0.5.3(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): + vinxi@0.5.3(@electric-sql/pglite@0.3.16)(@types/node@24.10.3)(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(ioredis@5.9.2)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(rolldown@1.1.5)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0) @@ -32425,7 +32173,7 @@ snapshots: hookable: 5.5.3 http-proxy: 1.18.1 micromatch: 4.0.8 - nitropack: 2.13.1(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) + nitropack: 2.13.1(@electric-sql/pglite@0.3.16)(aws4fetch@1.0.20)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3)))(rolldown@1.1.5) node-fetch-native: 1.6.7 path-to-regexp: 6.3.0 pathe: 1.1.2 @@ -32436,7 +32184,7 @@ snapshots: ufo: 1.6.1 unctx: 2.4.1 unenv: 1.10.0 - unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ioredis@5.9.2) + unstorage: 1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.3.16)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.3.16)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3))(prisma@6.19.3(typescript@5.9.3))))(ioredis@5.9.2) vite: 6.4.2(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) zod: 3.25.76 transitivePeerDependencies: diff --git a/scripts/scan-dangling-dts.mjs b/scripts/scan-dangling-dts.mjs index 66a5df03a..fc4aa2d50 100644 --- a/scripts/scan-dangling-dts.mjs +++ b/scripts/scan-dangling-dts.mjs @@ -62,6 +62,17 @@ function resolves(fromFile, specifier) { const IMPORT_RE = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+)['"]/g +/** + * Strip block and line comments so import statements inside JSDoc `@example` + * fences (which the declaration emit also rewrites to `.js` specifiers) are + * not scanned as real imports. + * + * @param {string} src + */ +function stripComments(src) { + return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') +} + const packageNames = existsSync(PACKAGES_DIR) ? readdirSync(PACKAGES_DIR).filter((name) => { try { @@ -94,7 +105,7 @@ let filesScanned = 0 for (const dist of dists) { for (const file of walkDts(dist)) { filesScanned += 1 - const src = readFileSync(file, 'utf8') + const src = stripComments(readFileSync(file, 'utf8')) IMPORT_RE.lastIndex = 0 let match while ((match = IMPORT_RE.exec(src))) {