Skip to content

Commit 37106f6

Browse files
Copilothotlong
andauthored
fix(service-tenant): register system objects via manifest service
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/abbcc526-9dc7-455c-b6b1-fbcac1fc5373 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a60d97f commit 37106f6

2 files changed

Lines changed: 55 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- **`@objectstack/service-tenant` — system objects now actually register** — `createTenantPlugin()` previously declared its control-plane schemas (`sys_project`, `sys_project_credential`, `sys_project_member`, `sys_package`, `sys_package_version`, `sys_package_installation`, `sys_tenant_database`) via a top-level `objects: [...]` field on the kernel plugin object. The kernel only consumes `plugin.objects` for **nested** plugins inside a parent manifest (`packages/objectql/src/engine.ts` → `registerPlugin()`), so plugins added via `kernel.use(plugin)` had to use the `manifest` service (as `AuthPlugin`/`SecurityPlugin`/`SetupPlugin` already do). The result was that `sys__project` etc. were never registered with `SchemaRegistry`, so `ObjectQL.getDriver('sys__project')` could not match the `namespace: 'sys' → turso` `datasourceMapping` rule (the lookup returned `undefined` and skipped past the namespace check), silently routing every control-plane write to the default driver — typically the in-memory driver. On Vercel each lambda instance has its own memory, so `POST /api/v1/cloud/projects` "succeeded" with HTTP 202 but the row evaporated on cold start, causing the subsequent `GET /api/v1/cloud/projects/:id` to return 404 even though the user/organization writes (registered through the proper path by `AuthPlugin`) were correctly persisted in Turso. The plugin now registers the same set of objects via `ctx.getService('manifest').register({ id: 'com.objectstack.tenant', namespace: 'sys', objects: [...] })` and throws if the manifest service is unavailable, fail-fast instead of silent data loss. Also affected: package install/upgrade endpoints, project credential rotation, project membership reads.
12+
1013
### Changed
1114
- **`examples/app-crm` — showcase `fieldGroups` MVP** — The CRM reference example (`Account`, `Contact`, `Opportunity`, `Lead`) now demonstrates the new `fieldGroups` protocol end to end. Each object declares logical groups (e.g., *Basic Information*, *Financials*, *Contact Information*, *Ownership & Status*, *System*) and every field opts in via `group: '<key>'`. No business logic changed — only field-layout metadata — so existing validations, workflows, indexes, and state machines are unaffected. Useful as a reference when designing multi-group forms and detail pages.
1215

packages/services/service-tenant/src/tenant-plugin.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,17 @@ export function createTenantPlugin(config: TenantPluginConfig = {}): Plugin {
5454
return {
5555
name: '@objectstack/service-tenant',
5656
version: '0.2.0',
57-
58-
objects: config.registerSystemObjects !== false
59-
? [
60-
// Control-plane objects (project-per-database model).
61-
SysProject,
62-
SysProjectCredential,
63-
SysProjectMember,
64-
// Package registry (ADR-0003).
65-
SysPackage,
66-
SysPackageVersion,
67-
SysPackageInstallation,
68-
// v4.x deprecation shim — opt out via `registerLegacyTenantDatabase: false`.
69-
...(config.registerLegacyTenantDatabase !== false ? [SysTenantDatabase] : []),
70-
]
71-
: [],
57+
// NOTE: System objects are registered inside `init()` via the `manifest`
58+
// service — the kernel does NOT consume a top-level `objects:` field on
59+
// plugins added via `kernel.use(plugin)`. Only nested plugins inside a
60+
// parent manifest are picked up that way (see
61+
// `packages/objectql/src/engine.ts` → `registerPlugin()`).
62+
//
63+
// Without going through the manifest service, schemas never reach
64+
// `SchemaRegistry`, which means `ObjectQL.getDriver()` cannot match the
65+
// `namespace: 'sys' → turso` datasourceMapping rule and silently falls
66+
// back to the default driver — losing every write across lambda
67+
// invocations on Vercel.
7268

7369
async init(ctx: PluginContext) {
7470
// Register the physical-DB adapter registry so HTTP dispatcher can
@@ -109,18 +105,50 @@ export function createTenantPlugin(config: TenantPluginConfig = {}): Plugin {
109105
}
110106

111107
if (config.registerSystemObjects !== false) {
112-
const registered = [
113-
'sys_project',
114-
'sys_project_credential',
115-
'sys_project_member',
116-
'sys_package',
117-
'sys_package_version',
118-
'sys_package_installation',
108+
// Register system objects via the `manifest` service. This is the
109+
// ONLY supported path — see the class-level note above for why a
110+
// top-level `objects:` field on the plugin object would be silently
111+
// ignored. Mirrors the convention used by AuthPlugin / SecurityPlugin
112+
// / SetupPlugin / etc.
113+
const manifestObjects: any[] = [
114+
// Control-plane objects (project-per-database model).
115+
SysProject,
116+
SysProjectCredential,
117+
SysProjectMember,
118+
// Package registry (ADR-0003).
119+
SysPackage,
120+
SysPackageVersion,
121+
SysPackageInstallation,
119122
];
120123
if (config.registerLegacyTenantDatabase !== false) {
121-
registered.push('sys_tenant_database (deprecated)');
124+
// v4.x deprecation shim — opt out via `registerLegacyTenantDatabase: false`.
125+
manifestObjects.push(SysTenantDatabase);
126+
}
127+
128+
try {
129+
const manifestService = ctx.getService<{ register(m: any): void }>('manifest');
130+
manifestService.register({
131+
id: 'com.objectstack.tenant',
132+
name: 'Tenant',
133+
version: '0.2.0',
134+
type: 'plugin',
135+
scope: 'platform',
136+
namespace: 'sys',
137+
objects: manifestObjects,
138+
});
139+
ctx.logger.info('[TenantPlugin] System objects registered via manifest service', {
140+
objects: manifestObjects.map((o: any) => `${o?.namespace ?? 'sys'}__${o?.name}`),
141+
});
142+
} catch (err: any) {
143+
// Without the manifest service we cannot register schemas — fail
144+
// loudly because every downstream control-plane write would
145+
// silently route to the default driver and lose data on cold
146+
// starts (see Vercel "create project then 404" failure mode).
147+
throw new Error(
148+
`[TenantPlugin] Failed to register system objects via manifest service. ` +
149+
`Ensure ObjectQLPlugin is registered before TenantPlugin. Cause: ${err?.message ?? String(err)}`,
150+
);
122151
}
123-
ctx.logger.info('[TenantPlugin] System objects registered', { objects: registered });
124152
}
125153
},
126154

0 commit comments

Comments
 (0)