Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/studio/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @objectstack/studio

## 3.2.8

### Minor Changes

- Switch Vercel deployment from MSW (browser mock) to real server mode
- Add `api/[...path].ts` Vercel serverless catch-all using Hono + `@objectstack/hono`
- Add `api/_kernel.ts` server-side kernel singleton with broker shim
- Extract broker shim to `src/lib/create-broker-shim.ts` (shared by MSW and server modes)
- Update `vercel.json` to set `VITE_RUNTIME_MODE=server` and `VITE_SERVER_URL=""`
- Add `hono` and `@objectstack/hono` dependencies
- Update deployment documentation

## 3.2.7

### Patch Changes
Expand Down
30 changes: 30 additions & 0 deletions apps/studio/api/[...path].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Vercel Serverless Catch-All Route
*
* Handles all /api/* requests using the ObjectStack Hono application.
* The Hono app provides discovery, data CRUD, metadata, auth, and more
* endpoints under the /api/v1 prefix.
*
* File name convention: Vercel maps `[...path].ts` to a catch-all route
* that matches /api and any sub-paths (/api/v1/data/task, etc.).
*/

import { Hono } from 'hono';
import { handle } from 'hono/vercel';
import { getApp } from './_kernel';

/**
* Outer Hono app — synchronously created so it can be wrapped by handle().
* Delegates all requests to the lazy-initialized inner app (which boots
* the ObjectStack kernel on first invocation).
*/
const app = new Hono();

app.all('/*', async (c) => {
const inner = await getApp();
return inner.fetch(c.req.raw);
});

export default handle(app);
Comment on lines +23 to +30
109 changes: 109 additions & 0 deletions apps/studio/api/_kernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* Server-side Kernel Singleton for Vercel Serverless Functions
*
* Boots an ObjectKernel with ObjectQL + InMemoryDriver, seeds data from
* the Studio configuration, and exposes a Hono app via createHonoApp.
*
* The kernel instance survives across warm invocations of the same
* serverless function container, so boot cost is paid only on cold start.
*
* @module
*/

import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { createHonoApp } from '@objectstack/hono';
import { Hono } from 'hono';
import { createBrokerShim } from '../src/lib/create-broker-shim';
import studioConfig from '../objectstack.config';

// --- Singleton state (persists across warm invocations) ---
let _kernel: ObjectKernel | null = null;
let _app: Hono | null = null;

/**
* Boot the ObjectStack kernel (one-time cold-start cost).
*/
async function bootKernel(): Promise<ObjectKernel> {
if (_kernel) return _kernel;
Comment on lines +23 to +31

console.log('[Vercel] Booting ObjectStack Kernel (server mode)...');

const kernel = new ObjectKernel();

await kernel.use(new ObjectQLPlugin());
await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory'));
await kernel.use(new AppPlugin(studioConfig));

// Broker shim — bridges HttpDispatcher → ObjectQL engine
(kernel as any).broker = createBrokerShim(kernel);

await kernel.bootstrap();

// Seed data from config
await seedData(kernel, [studioConfig]);

_kernel = kernel;
console.log('[Vercel] Kernel ready.');
return kernel;
}

/**
* Seed records defined in app configs into the ObjectQL engine.
*/
async function seedData(kernel: ObjectKernel, configs: any[]) {
const ql = (kernel as any).context?.getService('objectql');
if (!ql) return;

// Reserved namespaces ('base', 'system') bypass FQN transformation —
// objects in these namespaces keep their short name as-is.
const RESERVED_NS = new Set(['base', 'system']);
const toFQN = (name: string, namespace?: string) => {
if (name.includes('__') || !namespace || RESERVED_NS.has(namespace)) return name;
return `${namespace}__${name}`;
};

for (const appConfig of configs) {
const namespace = (appConfig.manifest || appConfig)?.namespace as string | undefined;

const seedDatasets: any[] = [];
if (Array.isArray(appConfig.data)) {
seedDatasets.push(...appConfig.data);
}
if (appConfig.manifest && Array.isArray(appConfig.manifest.data)) {
seedDatasets.push(...appConfig.manifest.data);
}

for (const dataset of seedDatasets) {
if (!dataset.records || !dataset.object) continue;

const objectFQN = toFQN(dataset.object, namespace);

// Handle PaginatedResult wrapper — InMemoryDriver may return { value: [...] }
let existing = await ql.find(objectFQN);
if (existing && (existing as any).value) existing = (existing as any).value;

if (!existing || existing.length === 0) {
console.log(`[Vercel] Seeding ${dataset.records.length} records for ${objectFQN}`);
for (const record of dataset.records) {
await ql.insert(objectFQN, record);
}
}
}
}
}

/**
Comment on lines +46 to +99
* Get (or create) the Hono application backed by the ObjectStack kernel.
* The prefix `/api/v1` matches the client SDK's default API path.
*/
export async function getApp(): Promise<Hono> {
if (_app) return _app;

const kernel = await bootKernel();
_app = createHonoApp({ kernel, prefix: '/api/v1' });
return _app;
}
4 changes: 3 additions & 1 deletion apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@objectstack/client": "workspace:*",
"@objectstack/client-react": "workspace:*",
"@objectstack/driver-memory": "workspace:*",
"@objectstack/hono": "workspace:*",
"@objectstack/metadata": "workspace:*",
"@objectstack/objectql": "workspace:*",
"@objectstack/plugin-msw": "workspace:*",
Expand Down Expand Up @@ -53,9 +54,10 @@
"@tailwindcss/postcss": "^4.2.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^5.2.0",
"autoprefixer": "^10.4.27",
"happy-dom": "^20.8.4",
"hono": "^4.12.8",
"msw": "^2.12.13",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.1",
Expand Down
Loading
Loading