Skip to content

Commit d11a880

Browse files
Copilothotlong
andauthored
merge: integrate latest main (plugin-setup, vercel fix) and resolve CHANGELOG conflict
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
2 parents 2436e95 + 999ea4f commit d11a880

16 files changed

Lines changed: 866 additions & 83 deletions

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@objectstack/plugin-msw",
2323
"@objectstack/plugin-dev",
2424
"@objectstack/plugin-security",
25+
"@objectstack/plugin-setup",
2526
"@objectstack/express",
2627
"@objectstack/fastify",
2728
"@objectstack/hono",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/studio": patch
3+
---
4+
5+
Fix Vercel deployment API endpoints returning HTML instead of JSON.
6+
7+
Replace the custom `getRequestListener` export in `server/index.ts` with the
8+
standard `handle()` adapter from `@hono/node-server/vercel` and the
9+
outer→inner Hono delegation pattern (`inner.fetch(c.req.raw)`).
10+
11+
- The `handle()` adapter correctly wraps the Hono app with the
12+
`(IncomingMessage, ServerResponse) => Promise<void>` signature that
13+
Vercel's Node.js runtime expects for serverless functions in `api/`.
14+
- `@hono/node-server/vercel`'s `getRequestListener()` already handles
15+
Vercel's pre-buffered `rawBody` natively, removing the need for the
16+
custom body-extraction helper.
17+
- The outer→inner delegation pattern matches the documented ObjectStack
18+
Vercel deployment guide and the `@objectstack/hono` adapter test suite.

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
(`AIToolDefinition`, `AIToolCall`, `AIToolResult`, `AIMessageWithTools`,
2424
`AIRequestOptionsWithTools`, `AIStreamEvent`), and conversation management
2525
(`IAIConversationService`, `AIConversation`) to `packages/spec/src/contracts/ai-service.ts`
26+
- **`@objectstack/plugin-setup` — Platform Setup App plugin** — New internal plugin
27+
(`packages/plugins/plugin-setup`) that owns and finalizes the platform Setup App.
28+
Ships four built-in Setup Areas (Administration, Platform, System, AI) as empty
29+
skeletons. Other plugins contribute navigation items via the `setupNav` service
30+
during their `init` phase. At `start`, SetupPlugin merges all contributions,
31+
filters out empty areas, and registers the finalized Setup App as an internal
32+
platform app. This establishes clear architectural separation: **spec** = protocol
33+
only, **objectql** = data/query only, **plugins** = system feature and UI composition.
2634

2735
### Documentation
2836
- **Unified API query syntax documentation with Spec canonical format** — Rewrote

ROADMAP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ the ecosystem for enterprise workloads.
5353
| Authentication (better-auth) || `@objectstack/plugin-auth` |
5454
| Auth in MSW/Mock Mode || `@objectstack/plugin-auth` + `@objectstack/runtime` |
5555
| RBAC / RLS / FLS Security || `@objectstack/plugin-security` |
56+
| Platform Setup App || `@objectstack/plugin-setup` |
5657
| CLI (16 commands) || `@objectstack/cli` |
5758
| Dev Mode Plugin || `@objectstack/plugin-dev` |
5859
| Next.js Adapter || `@objectstack/nextjs` |
@@ -900,6 +901,7 @@ Final polish and advanced features.
900901
| `@objectstack/driver-memory` | 3.0.8 || ✅ Stable | 9/10 |
901902
| `@objectstack/plugin-auth` | 3.0.8 || ✅ Stable | 9/10 |
902903
| `@objectstack/plugin-security` | 3.0.8 || ✅ Stable | 9/10 |
904+
| `@objectstack/plugin-setup` | 3.3.1 || ✅ Stable | 8/10 |
903905
| `@objectstack/plugin-dev` | 3.0.8 || ✅ Stable | 10/10 |
904906
| `@objectstack/plugin-hono-server` | 3.0.8 || ✅ Stable | 9/10 |
905907
| `@objectstack/plugin-msw` | 3.0.8 || ✅ Stable | 9/10 |

apps/studio/server/index.ts

Lines changed: 30 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
* legacy `(IncomingMessage, ServerResponse)` signature — NOT the Web standard
1111
* `(Request) → Response` format.
1212
*
13-
* We use `getRequestListener()` from `@hono/node-server` which properly
14-
* converts `IncomingMessage → Request`, calls our fetch callback, then writes
15-
* the `Response` back to `ServerResponse`.
13+
* We use `handle()` from `@hono/node-server/vercel` which is the standard
14+
* Vercel adapter for Hono. It internally uses `getRequestListener()` to
15+
* convert `IncomingMessage → Request` (including Vercel's pre-buffered
16+
* `rawBody`) and writes the `Response` back to `ServerResponse`.
1617
*
17-
* For POST/PUT/PATCH requests, Vercel pre-buffers the body on the
18-
* IncomingMessage as `rawBody` (Buffer) or `body` (parsed). We extract it
19-
* directly and build a clean `Request` so the inner Hono app receives a
20-
* body it can `.json()` without depending on Node stream-to-ReadableStream
21-
* conversion (which can hang when the stream has already been consumed).
18+
* The outer Hono app delegates all requests to the inner ObjectStack Hono
19+
* app via `inner.fetch(c.req.raw)`, matching the pattern documented in
20+
* the ObjectStack deployment guide and validated by the hono adapter tests.
2221
*
2322
* All kernel/service initialisation is co-located here so there are no
2423
* extensionless relative module imports — which would break Node's ESM
@@ -34,8 +33,8 @@ import { SecurityPlugin } from '@objectstack/plugin-security';
3433
import { AuditPlugin } from '@objectstack/plugin-audit';
3534
import { FeedServicePlugin } from '@objectstack/service-feed';
3635
import { MetadataPlugin } from '@objectstack/metadata';
37-
import { getRequestListener } from '@hono/node-server';
38-
import type { Hono } from 'hono';
36+
import { handle } from '@hono/node-server/vercel';
37+
import { Hono } from 'hono';
3938
import { createBrokerShim } from '../src/lib/create-broker-shim.js';
4039
import studioConfig from '../objectstack.config.js';
4140

@@ -198,93 +197,41 @@ async function ensureApp(): Promise<Hono> {
198197
return _app;
199198
}
200199

201-
// ---------------------------------------------------------------------------
202-
// Body extraction helpers
203-
// ---------------------------------------------------------------------------
204-
205-
/**
206-
* Extract the request body from the Vercel IncomingMessage.
207-
*
208-
* Vercel's Node.js runtime pre-buffers the full request body and attaches it
209-
* to the IncomingMessage as `rawBody` (Buffer) and/or `body` (parsed).
210-
* Reading from these properties is synchronous and avoids the fragile
211-
* IncomingMessage → ReadableStream conversion that can hang when the
212-
* underlying Node stream has already been consumed.
213-
*
214-
* Returns `null` for GET/HEAD/OPTIONS or when no body is available.
215-
*/
216-
function extractBody(incoming: any, method: string, contentType: string | undefined): BodyInit | null {
217-
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
218-
return null;
219-
}
220-
221-
// 1. rawBody (Buffer or string) — most reliable, set by Vercel runtime
222-
if (incoming?.rawBody != null) {
223-
if (typeof incoming.rawBody === 'string') return incoming.rawBody;
224-
if (typeof incoming.rawBody.toString === 'function') return incoming.rawBody;
225-
return String(incoming.rawBody);
226-
}
227-
228-
// 2. body (parsed by Vercel) — re-serialize based on content-type
229-
if (incoming?.body != null) {
230-
if (typeof incoming.body === 'string') return incoming.body;
231-
if (contentType?.includes('application/json')) return JSON.stringify(incoming.body);
232-
return String(incoming.body);
233-
}
234-
235-
return null;
236-
}
237-
238200
// ---------------------------------------------------------------------------
239201
// Vercel handler
240202
// ---------------------------------------------------------------------------
241203

242204
/**
243-
* `getRequestListener` from `@hono/node-server` converts
244-
* `IncomingMessage → Request`, calls our fetch callback, then writes the
245-
* `Response` back to `ServerResponse` (including `res.end()`).
205+
* Outer Hono app — delegates all requests to the inner ObjectStack app.
206+
*
207+
* `handle()` from `@hono/node-server/vercel` wraps any Hono app and returns
208+
* the `(IncomingMessage, ServerResponse) => Promise<void>` signature that
209+
* Vercel's Node.js runtime expects for serverless functions. Internally it
210+
* uses `getRequestListener()`, which already handles Vercel's pre-buffered
211+
* `rawBody` (Buffer) on the IncomingMessage for POST/PUT/PATCH requests.
246212
*
247-
* For requests with a body, we extract it from the IncomingMessage directly
248-
* (bypassing the Node stream → ReadableStream conversion) and create a new
249-
* Request that the inner Hono app can safely `.json()`.
213+
* The outer→inner delegation pattern (`inner.fetch(c.req.raw)`) is the
214+
* standard ObjectStack Vercel deployment pattern documented in the deployment
215+
* guide and covered by the @objectstack/hono adapter test suite.
250216
*/
251-
export default getRequestListener(async (request, env) => {
252-
const method = request.method;
253-
const url = request.url;
217+
const app = new Hono();
254218

255-
console.log(`[Vercel] ${method} ${url}`);
219+
app.all('*', async (c) => {
220+
console.log(`[Vercel] ${c.req.method} ${c.req.url}`);
256221

257222
try {
258-
const app = await ensureApp();
259-
const incoming = (env as any)?.incoming;
260-
261-
// For body methods, extract body from IncomingMessage and build a clean Request
262-
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && incoming) {
263-
const contentType = incoming.headers?.['content-type'];
264-
const body = extractBody(incoming, method, contentType);
265-
266-
if (body != null) {
267-
console.log(`[Vercel] Body extracted from IncomingMessage (${typeof body === 'string' ? body.length + ' chars' : 'Buffer'})`);
268-
const newReq = new Request(url, {
269-
method,
270-
headers: request.headers,
271-
body,
272-
});
273-
return await app.fetch(newReq);
274-
}
275-
276-
console.log('[Vercel] No rawBody/body on IncomingMessage — using proxy request');
277-
}
278-
279-
return await app.fetch(request);
223+
const inner = await ensureApp();
224+
return await inner.fetch(c.req.raw);
280225
} catch (err: any) {
281226
console.error('[Vercel] Handler error:', err?.message || err);
282-
return new Response(
283-
JSON.stringify({
227+
return c.json(
228+
{
284229
success: false,
285230
error: { message: err?.message || 'Internal Server Error', code: 500 },
286-
}),
287-
{ status: 500, headers: { 'Content-Type': 'application/json' } },
231+
},
232+
500,
288233
);
289234
}
290235
});
236+
237+
export default handle(app);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# @objectstack/plugin-setup
2+
3+
## 3.3.1
4+
5+
### Added
6+
7+
- Initial release of the Setup Plugin.
8+
- Defines the platform Setup App identity (name, label, icon, permissions, branding).
9+
- Ships 4 built-in Setup Areas: Administration, Platform, System, AI.
10+
- Provides `setupNav` service for contribution-based navigation composition.
11+
- Auto-filters empty areas and supports custom area extensions.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# @objectstack/plugin-setup
2+
3+
Setup Plugin for ObjectStack — owns and composes the platform **Setup App** with area-based navigation.
4+
5+
## Overview
6+
7+
The Setup App is the central administration interface of the ObjectStack platform (equivalent to Salesforce Setup or ServiceNow System Administration). Rather than scattering setup definitions across `spec` and `objectql`, this plugin provides clear ownership:
8+
9+
- **Spec** → protocol schemas only
10+
- **ObjectQL** → data engine only
11+
- **plugin-setup** → owns the Setup App identity, areas, and navigation composition
12+
13+
## Features
14+
15+
- **Four Built-in Areas**: Administration, Platform, System, and AI — shipped as empty skeletons.
16+
- **Contribution Model**: Any plugin can contribute navigation items to Setup areas via the `setupNav` service.
17+
- **Area Filtering**: Empty areas are automatically filtered out at finalization.
18+
- **Custom Areas**: Plugins can contribute to custom area IDs beyond the four built-in ones.
19+
- **I18n Labels**: All labels use the `I18nLabel` union type for internationalization.
20+
21+
## Usage
22+
23+
### Register the Plugin
24+
25+
```typescript
26+
import { ObjectKernel } from '@objectstack/core';
27+
import { SetupPlugin } from '@objectstack/plugin-setup';
28+
29+
const kernel = new ObjectKernel({
30+
plugins: [
31+
new SetupPlugin(),
32+
// ... other plugins
33+
],
34+
});
35+
```
36+
37+
### Contribute Navigation from Another Plugin
38+
39+
```typescript
40+
import type { Plugin, PluginContext } from '@objectstack/core';
41+
import type { SetupNavService } from '@objectstack/plugin-setup';
42+
import { SETUP_AREA_IDS } from '@objectstack/plugin-setup';
43+
44+
export class MyPlugin implements Plugin {
45+
name = 'com.example.my-plugin';
46+
47+
async init(ctx: PluginContext) {
48+
const setupNav = ctx.getService<SetupNavService>('setupNav');
49+
50+
setupNav.contribute({
51+
areaId: SETUP_AREA_IDS.administration,
52+
items: [
53+
{ id: 'nav_users', type: 'object', label: 'Users', objectName: 'sys_user' },
54+
{ id: 'nav_roles', type: 'object', label: 'Roles', objectName: 'sys_role' },
55+
],
56+
});
57+
}
58+
}
59+
```
60+
61+
### Exported Components
62+
63+
```typescript
64+
import {
65+
SetupPlugin,
66+
type SetupNavService,
67+
SETUP_APP_DEFAULTS,
68+
type SetupNavContribution,
69+
SETUP_AREAS,
70+
SETUP_AREA_IDS,
71+
type SetupAreaId,
72+
} from '@objectstack/plugin-setup';
73+
```
74+
75+
## Built-in Setup Areas
76+
77+
| Area | ID | Icon | Order | Description |
78+
|:-----|:---|:-----|:-----:|:------------|
79+
| Administration | `area_administration` | shield | 10 | Users, roles, permissions, security |
80+
| Platform | `area_platform` | layers | 20 | Objects, fields, layouts, automation |
81+
| System | `area_system` | settings | 30 | Datasources, integrations, jobs, logs |
82+
| AI | `area_ai` | brain | 40 | Agents, models, RAG pipelines |
83+
84+
## Architecture
85+
86+
```
87+
┌──────────────────────────────────────────┐
88+
│ SetupPlugin │
89+
│ │
90+
│ init(): │
91+
│ → registers 'setupNav' service │
92+
│ │
93+
│ start(): │
94+
│ → collects contributions │
95+
│ → merges into area skeletons │
96+
│ → filters empty areas │
97+
│ → registers finalized Setup App │
98+
│ │
99+
└──────────────────────────────────────────┘
100+
▲ ▲
101+
│ contribute() │ contribute()
102+
┌────┴────┐ ┌────┴────┐
103+
│ plugin │ │ plugin │
104+
│ auth │ │security │
105+
└─────────┘ └─────────┘
106+
```
107+
108+
## License
109+
110+
Apache-2.0 © ObjectStack
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineStack } from '@objectstack/spec';
4+
5+
/**
6+
* ObjectStack Configuration for plugin-setup
7+
*
8+
* This configuration defines the manifest for the platform Setup plugin.
9+
* The Setup App itself is composed at runtime by collecting setupNav
10+
* contributions from all registered plugins.
11+
*/
12+
export default defineStack({
13+
manifest: {
14+
id: 'com.objectstack.plugin-setup',
15+
namespace: 'setup',
16+
version: '3.3.1',
17+
type: 'plugin',
18+
name: 'Platform Setup Plugin',
19+
description: 'Owns and composes the platform Setup App with area-based navigation contributed by other plugins',
20+
},
21+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@objectstack/plugin-setup",
3+
"version": "3.3.1",
4+
"license": "Apache-2.0",
5+
"description": "Setup Plugin for ObjectStack — Platform Setup App with area-based navigation composition",
6+
"main": "dist/index.js",
7+
"types": "dist/index.d.ts",
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"import": "./dist/index.mjs",
12+
"require": "./dist/index.js"
13+
}
14+
},
15+
"scripts": {
16+
"build": "tsup --config ../../../tsup.config.ts",
17+
"test": "vitest run"
18+
},
19+
"dependencies": {
20+
"@objectstack/core": "workspace:*",
21+
"@objectstack/spec": "workspace:*"
22+
},
23+
"devDependencies": {
24+
"@types/node": "^25.5.0",
25+
"typescript": "^6.0.2",
26+
"vitest": "^4.1.2"
27+
}
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* @objectstack/plugin-setup
5+
*
6+
* Setup Plugin for ObjectStack — owns and composes the platform Setup App.
7+
* Other plugins contribute navigation items via the `setupNav` service.
8+
*/
9+
10+
export { SetupPlugin, type SetupNavService } from './setup-plugin.js';
11+
export { SETUP_APP_DEFAULTS, type SetupNavContribution } from './setup-app.js';
12+
export { SETUP_AREAS, SETUP_AREA_IDS, type SetupAreaId } from './setup-areas.js';

0 commit comments

Comments
 (0)