diff --git a/CHANGELOG.md b/CHANGELOG.md index b1721edd..aeb5ea3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`apps/demo`** — added explicit `@objectstack/spec` and `zod` devDependencies as defense-in-depth for Vercel deployment. - **`@objectql/types`** — moved `@objectstack/spec` and `zod` from `devDependencies` to `dependencies`. The compiled JS output contains runtime imports of `@objectstack/spec` (via `z.infer` patterns), so they must be declared as production dependencies. +### Changed + +- **`apps/demo`** — switched default data driver from `@objectstack/driver-memory` (InMemoryDriver) to `@objectql/driver-turso` (TursoDriver). When `TURSO_DATABASE_URL` is set, the demo uses a persistent Turso/libSQL database; otherwise falls back to InMemoryDriver for zero-config local development. + - `objectstack.config.ts` — environment-aware `createDefaultDriver()` selects Turso or MemoryDriver. + - `api/[[...route]].ts` — Vercel serverless handler uses TursoDriver with `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, `TURSO_SYNC_URL`, and `TURSO_SYNC_INTERVAL` env vars. + - `scripts/build-vercel.sh` — now builds `@objectql/driver-turso` alongside other drivers. + - `README.md` — documents new Turso environment variables and architecture. + ### Added - **`apps/demo`** — standalone Vercel-deployable demo application ([#issue](https://github.com/objectstack-ai/objectql/issues)): diff --git a/apps/demo/README.md b/apps/demo/README.md index c7afec0b..3fd6cd42 100644 --- a/apps/demo/README.md +++ b/apps/demo/README.md @@ -5,7 +5,8 @@ Runs locally with `@objectstack/cli` and deploys to **Vercel** as a serverless f ## Features -- **In-memory driver** — zero external database required; data persists across warm Vercel invocations. +- **Turso/libSQL driver** — persistent, edge-first SQLite via `@objectql/driver-turso` when `TURSO_DATABASE_URL` is set. +- **In-memory fallback** — zero external database required for quick local development. - **Console UI** — full ObjectStack Console available at `/console/`. - **Studio UI** — ObjectStack Studio available at `/_studio/`. - **Project-Tracker showcase** — ships with the `examples/showcase/project-tracker` metadata (objects, views, permissions) so the demo has real data structures out of the box. @@ -44,6 +45,10 @@ The development server starts on `http://localhost:3000`. |---|---|---| | `AUTH_SECRET` | **Yes** (production) | Secret key for signing auth tokens. Generate with `openssl rand -base64 32`. | | `AUTH_TRUSTED_ORIGINS` | No | Comma-separated list of additional trusted origins (e.g. `https://myapp.example.com`). | +| `TURSO_DATABASE_URL` | No | Turso/libSQL database URL (e.g. `libsql://my-db-org.turso.io`). When set, uses Turso instead of in-memory driver. | +| `TURSO_AUTH_TOKEN` | When using Turso | JWT auth token for the Turso database. | +| `TURSO_SYNC_URL` | No | Remote sync URL for embedded replica mode. | +| `TURSO_SYNC_INTERVAL` | No | Periodic sync interval in seconds (default: 60). | 4. Deploy: @@ -58,7 +63,7 @@ vercel --cwd apps/demo --prod ### How It Works - **`vercel.json`** — Configures Vercel to use a custom build command, allocate 1 GiB memory to the serverless function, and rewrite all requests to the catch-all `api/[[...route]].ts` handler. -- **`api/[[...route]].ts`** — Bootstraps the full ObjectStack kernel with ObjectQL plugins, the in-memory driver, auth, Console, and Studio. Uses `@hono/node-server`'s `getRequestListener()` to bridge the Vercel serverless runtime with the Hono HTTP framework. +- **`api/[[...route]].ts`** — Bootstraps the full ObjectStack kernel with ObjectQL plugins, the Turso driver (or in-memory fallback), auth, Console, and Studio. Uses `@hono/node-server`'s `getRequestListener()` to bridge the Vercel serverless runtime with the Hono HTTP framework. - **`scripts/build-vercel.sh`** — Builds all required workspace packages (foundation, drivers, plugins, protocols, examples) in the correct dependency order. - **`scripts/patch-symlinks.cjs`** — Replaces pnpm workspace symlinks with real copies so Vercel can bundle the function without symlink errors. @@ -110,5 +115,5 @@ apps/demo/ │ ObjectStack Kernel (ObjectQL + Auth + - InMemoryDriver) + TursoDriver / InMemoryDriver) ``` diff --git a/apps/demo/api/[[...route]].ts b/apps/demo/api/[[...route]].ts index 35f1bbb3..0c222ca7 100644 --- a/apps/demo/api/[[...route]].ts +++ b/apps/demo/api/[[...route]].ts @@ -2,8 +2,9 @@ * Vercel Serverless Function — ObjectQL Demo Handler * * Bootstraps the ObjectStack kernel with ObjectQL plugins and the - * project-tracker demo metadata, using @objectstack/driver-memory - * for zero-config in-memory data. + * project-tracker demo metadata, using @objectql/driver-turso when + * TURSO_DATABASE_URL is set, or @objectstack/driver-memory as a + * zero-config fallback. * * Uses `getRequestListener()` from `@hono/node-server` together with * an `extractBody()` helper to handle Vercel's pre-buffered request @@ -14,8 +15,10 @@ * a fresh `Request` object prevents POST/PUT/PATCH requests (e.g. * login) from hanging indefinitely. * - * Data lives in the function instance's memory and persists across - * warm invocations (Vercel Fluid Compute) but resets on cold start. + * When using Turso, data is persisted in the Turso cloud database. + * When using InMemoryDriver, data lives in the function instance's + * memory and persists across warm invocations (Vercel Fluid Compute) + * but resets on cold start. * * Both Console (/) and Studio (/_studio/) UIs are served as static SPAs. * @@ -30,6 +33,7 @@ import { ObjectKernel, DriverPlugin, AppPlugin, createDispatcherPlugin, createRe import { HonoHttpServer } from '@objectstack/plugin-hono-server'; import { AuthPlugin } from '@objectstack/plugin-auth'; import { InMemoryDriver } from '@objectstack/driver-memory'; +import { createTursoDriver, type TursoDriver } from '@objectql/driver-turso'; import { ObjectQLPlugin } from '@objectstack/objectql'; import { getRequestListener } from '@hono/node-server'; import type { Hono } from 'hono'; @@ -278,10 +282,38 @@ async function bootstrap(): Promise { await withTimeout(kernel.use(new ObjectQLPlugin()), PLUGIN_TIMEOUT_MS, 'ObjectQLPlugin'); log('ObjectQLPlugin registered.'); - // 2. In-memory data driver (no external DB required) - log('Registering DriverPlugin (InMemoryDriver)…'); - await withTimeout(kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')), PLUGIN_TIMEOUT_MS, 'DriverPlugin'); - log('DriverPlugin registered.'); + // 2. Data driver — Turso when TURSO_DATABASE_URL is set, InMemoryDriver otherwise + const tursoUrl = process.env.TURSO_DATABASE_URL; + let tursoDriver: TursoDriver | null = null; + if (tursoUrl) { + log(`Registering TursoDriver (${tursoUrl})…`); + const syncUrl = process.env.TURSO_SYNC_URL; + const rawSyncInterval = process.env.TURSO_SYNC_INTERVAL; + const parsedSyncInterval = + rawSyncInterval !== undefined ? Number(rawSyncInterval) : NaN; + const syncIntervalSeconds = Number.isFinite(parsedSyncInterval) + ? parsedSyncInterval + : 60; + tursoDriver = createTursoDriver({ + url: tursoUrl, + authToken: process.env.TURSO_AUTH_TOKEN, + syncUrl, + sync: syncUrl + ? { + intervalSeconds: syncIntervalSeconds, + onConnect: true, + } + : undefined, + }); + // DriverPlugin from @objectstack/runtime accepts any driver; TursoDriver + // implements @objectql/types Driver which is structurally compatible. + await withTimeout(kernel.use(new DriverPlugin(tursoDriver, 'turso')), PLUGIN_TIMEOUT_MS, 'DriverPlugin-turso'); + log('TursoDriver registered.'); + } else { + log('Registering DriverPlugin (InMemoryDriver)…'); + await withTimeout(kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')), PLUGIN_TIMEOUT_MS, 'DriverPlugin-memory'); + log('InMemoryDriver registered.'); + } // 3. HTTP server adapter — register the Hono app without TCP listener const httpServer = new HonoHttpServer(); @@ -445,7 +477,14 @@ async function bootstrap(): Promise { log('Studio SPA registered.'); } - // 12. Bootstrap kernel (init + start all plugins, fire kernel:ready) + // 12. Connect Turso driver (if applicable) before kernel bootstrap + if (tursoDriver) { + log('Connecting TursoDriver…'); + await withTimeout(tursoDriver.connect(), PLUGIN_TIMEOUT_MS, 'TursoDriver.connect()'); + log('TursoDriver connected.'); + } + + // 13. Bootstrap kernel (init + start all plugins, fire kernel:ready) log('Running kernel.bootstrap()…'); await withTimeout(kernel.bootstrap(), KERNEL_BOOTSTRAP_TIMEOUT_MS, 'kernel.bootstrap()'); log(`Bootstrap complete in ${elapsed()}.`); diff --git a/apps/demo/objectstack.config.ts b/apps/demo/objectstack.config.ts index 608411ad..85d46827 100644 --- a/apps/demo/objectstack.config.ts +++ b/apps/demo/objectstack.config.ts @@ -1,8 +1,9 @@ /** * ObjectQL Demo — Application Configuration * - * Minimal ObjectStack configuration for the demo application. - * Uses in-memory driver with the project-tracker showcase example. + * ObjectStack configuration for the demo application. + * Uses @objectql/driver-turso when TURSO_DATABASE_URL is set, + * falls back to MemoryDriver for zero-config local development. * * For local development: `pnpm dev` (uses @objectstack/cli) * For Vercel deployment: configured via api/[[...route]].ts @@ -29,10 +30,39 @@ import { FormulaPlugin } from '@objectql/plugin-formula'; import { ObjectQLSecurityPlugin } from '@objectql/plugin-security'; import { createApiRegistryPlugin } from '@objectstack/core'; import { MemoryDriver } from '@objectql/driver-memory'; +import { createTursoDriver } from '@objectql/driver-turso'; import { createAppPlugin } from '@objectql/platform-node'; -// In-memory driver — zero-config, no external DB required. -const defaultDriver = new MemoryDriver(); +// Choose driver based on environment — Turso when TURSO_DATABASE_URL is set, +// MemoryDriver otherwise (zero-config fallback for quick starts). +function createDefaultDriver() { + const tursoUrl = process.env.TURSO_DATABASE_URL; + if (tursoUrl) { + console.log(`🗄️ Driver: Turso (${tursoUrl})`); + const syncUrl = process.env.TURSO_SYNC_URL; + const rawSyncInterval = process.env.TURSO_SYNC_INTERVAL; + const parsedSyncInterval = + rawSyncInterval !== undefined ? Number(rawSyncInterval) : NaN; + const syncIntervalSeconds = Number.isFinite(parsedSyncInterval) + ? parsedSyncInterval + : 60; + return createTursoDriver({ + url: tursoUrl, + authToken: process.env.TURSO_AUTH_TOKEN, + syncUrl, + sync: syncUrl + ? { + intervalSeconds: syncIntervalSeconds, + onConnect: true, + } + : undefined, + }); + } + console.log('🗄️ Driver: Memory (in-memory, non-persistent)'); + return new MemoryDriver(); +} + +const defaultDriver = createDefaultDriver(); // Load the project-tracker showcase metadata. const projectTrackerPlugin = createAppPlugin({ @@ -51,13 +81,17 @@ export default { createApiRegistryPlugin(), new HonoServerPlugin({}), new ConsolePlugin(), - // Register the driver as 'driver.default' service. + // Register the active driver as 'driver.default' service so upstream + // ObjectQLPlugin can discover it during start() phase. { name: 'driver-default', init: async (ctx: any) => { ctx.registerService('driver.default', defaultDriver); }, - start: async () => {}, + start: async () => { + // Driver.connect() is optional in the interface; call if present + await defaultDriver.connect?.(); + }, }, projectTrackerPlugin, new ObjectQLPlugin(), diff --git a/apps/demo/package.json b/apps/demo/package.json index fd9ef345..71bf7efd 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -12,9 +12,15 @@ }, "devDependencies": { "@hono/node-server": "^1.19.11", + "@libsql/client": "^0.17.2", + "@libsql/core": "^0.17.2", + "@libsql/hrana-client": "^0.9.0", + "@libsql/isomorphic-ws": "^0.1.5", + "@neon-rs/load": "^0.0.4", "@object-ui/console": "^3.1.3", "@objectql/core": "workspace:*", "@objectql/driver-memory": "workspace:*", + "@objectql/driver-turso": "workspace:*", "@objectql/example-project-tracker": "workspace:*", "@objectql/platform-node": "workspace:*", "@objectql/plugin-formula": "workspace:*", @@ -35,8 +41,16 @@ "@objectstack/runtime": "^3.2.8", "@objectstack/studio": "^3.2.8", "@types/node": "^20.19.37", + "detect-libc": "^2.0.2", "hono": "^4.12.8", + "js-base64": "^3.7.8", + "libsql": "^0.5.28", + "nanoid": "^3.3.11", + "promise-limit": "^2.7.0", "typescript": "^5.9.3", + "ws": "^8.19.0", + "cross-fetch": "^4.1.0", + "node-fetch": "^2.7.0", "zod": "^4.3.6" }, "engines": { diff --git a/apps/demo/scripts/build-vercel.sh b/apps/demo/scripts/build-vercel.sh index 05cdbc43..3cd915c4 100755 --- a/apps/demo/scripts/build-vercel.sh +++ b/apps/demo/scripts/build-vercel.sh @@ -35,6 +35,7 @@ pnpm --filter @objectql/platform-node build echo "▸ Building drivers…" pnpm --filter @objectql/driver-memory \ + --filter @objectql/driver-turso \ --filter @objectql/driver-sql \ build diff --git a/apps/demo/vercel.json b/apps/demo/vercel.json index e81dd1e6..60e6dbf4 100644 --- a/apps/demo/vercel.json +++ b/apps/demo/vercel.json @@ -7,7 +7,7 @@ "api/**/*.ts": { "memory": 1024, "maxDuration": 60, - "includeFiles": "{node_modules/@object-ui/console/dist,node_modules/@objectstack/plugin-auth/dist,node_modules/@objectstack/studio/dist,node_modules/@objectql/example-project-tracker/dist}/**" + "includeFiles": "node_modules/{@object-ui/*/dist,@objectstack/*/dist,@objectql/*/dist,@libsql,@neon-rs,libsql,detect-libc,js-base64,promise-limit,nanoid,ws,cross-fetch,node-fetch}/**" } }, "rewrites": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a09523a..5be4698b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,6 +144,21 @@ importers: '@hono/node-server': specifier: ^1.19.11 version: 1.19.11(hono@4.12.8) + '@libsql/client': + specifier: ^0.17.2 + version: 0.17.2(encoding@0.1.13) + '@libsql/core': + specifier: ^0.17.2 + version: 0.17.2 + '@libsql/hrana-client': + specifier: ^0.9.0 + version: 0.9.0(encoding@0.1.13) + '@libsql/isomorphic-ws': + specifier: ^0.1.5 + version: 0.1.5 + '@neon-rs/load': + specifier: ^0.0.4 + version: 0.0.4 '@object-ui/console': specifier: ^3.1.3 version: 3.1.3 @@ -153,6 +168,9 @@ importers: '@objectql/driver-memory': specifier: workspace:* version: link:../../packages/drivers/memory + '@objectql/driver-turso': + specifier: workspace:* + version: link:../../packages/drivers/turso '@objectql/example-project-tracker': specifier: workspace:* version: link:../../examples/showcase/project-tracker @@ -213,12 +231,36 @@ importers: '@types/node': specifier: ^20.19.37 version: 20.19.37 + cross-fetch: + specifier: ^4.1.0 + version: 4.1.0(encoding@0.1.13) + detect-libc: + specifier: ^2.0.2 + version: 2.1.2 hono: specifier: ^4.12.8 version: 4.12.8 + js-base64: + specifier: ^3.7.8 + version: 3.7.8 + libsql: + specifier: ^0.5.28 + version: 0.5.28 + nanoid: + specifier: ^3.3.11 + version: 3.3.11 + node-fetch: + specifier: ^2.7.0 + version: 2.7.0(encoding@0.1.13) + promise-limit: + specifier: ^2.7.0 + version: 2.7.0 typescript: specifier: ^5.9.3 version: 5.9.3 + ws: + specifier: ^8.19.0 + version: 8.19.0 zod: specifier: ^4.3.6 version: 4.3.6