From 37106f6a5a0b64391d602c19e08c03515f4a58fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:08:00 +0000 Subject: [PATCH] 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> --- CHANGELOG.md | 3 + .../service-tenant/src/tenant-plugin.ts | 76 +++++++++++++------ 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4439debff..3e875912c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **`@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. + ### Changed - **`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: ''`. 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. diff --git a/packages/services/service-tenant/src/tenant-plugin.ts b/packages/services/service-tenant/src/tenant-plugin.ts index 18e75db0f..ec29f77ad 100644 --- a/packages/services/service-tenant/src/tenant-plugin.ts +++ b/packages/services/service-tenant/src/tenant-plugin.ts @@ -54,21 +54,17 @@ export function createTenantPlugin(config: TenantPluginConfig = {}): Plugin { return { name: '@objectstack/service-tenant', version: '0.2.0', - - objects: config.registerSystemObjects !== false - ? [ - // Control-plane objects (project-per-database model). - SysProject, - SysProjectCredential, - SysProjectMember, - // Package registry (ADR-0003). - SysPackage, - SysPackageVersion, - SysPackageInstallation, - // v4.x deprecation shim — opt out via `registerLegacyTenantDatabase: false`. - ...(config.registerLegacyTenantDatabase !== false ? [SysTenantDatabase] : []), - ] - : [], + // NOTE: System objects are registered inside `init()` via the `manifest` + // service — the kernel does NOT consume a top-level `objects:` field on + // plugins added via `kernel.use(plugin)`. Only nested plugins inside a + // parent manifest are picked up that way (see + // `packages/objectql/src/engine.ts` → `registerPlugin()`). + // + // Without going through the manifest service, schemas never reach + // `SchemaRegistry`, which means `ObjectQL.getDriver()` cannot match the + // `namespace: 'sys' → turso` datasourceMapping rule and silently falls + // back to the default driver — losing every write across lambda + // invocations on Vercel. async init(ctx: PluginContext) { // Register the physical-DB adapter registry so HTTP dispatcher can @@ -109,18 +105,50 @@ export function createTenantPlugin(config: TenantPluginConfig = {}): Plugin { } if (config.registerSystemObjects !== false) { - const registered = [ - 'sys_project', - 'sys_project_credential', - 'sys_project_member', - 'sys_package', - 'sys_package_version', - 'sys_package_installation', + // Register system objects via the `manifest` service. This is the + // ONLY supported path — see the class-level note above for why a + // top-level `objects:` field on the plugin object would be silently + // ignored. Mirrors the convention used by AuthPlugin / SecurityPlugin + // / SetupPlugin / etc. + const manifestObjects: any[] = [ + // Control-plane objects (project-per-database model). + SysProject, + SysProjectCredential, + SysProjectMember, + // Package registry (ADR-0003). + SysPackage, + SysPackageVersion, + SysPackageInstallation, ]; if (config.registerLegacyTenantDatabase !== false) { - registered.push('sys_tenant_database (deprecated)'); + // v4.x deprecation shim — opt out via `registerLegacyTenantDatabase: false`. + manifestObjects.push(SysTenantDatabase); + } + + try { + const manifestService = ctx.getService<{ register(m: any): void }>('manifest'); + manifestService.register({ + id: 'com.objectstack.tenant', + name: 'Tenant', + version: '0.2.0', + type: 'plugin', + scope: 'platform', + namespace: 'sys', + objects: manifestObjects, + }); + ctx.logger.info('[TenantPlugin] System objects registered via manifest service', { + objects: manifestObjects.map((o: any) => `${o?.namespace ?? 'sys'}__${o?.name}`), + }); + } catch (err: any) { + // Without the manifest service we cannot register schemas — fail + // loudly because every downstream control-plane write would + // silently route to the default driver and lose data on cold + // starts (see Vercel "create project then 404" failure mode). + throw new Error( + `[TenantPlugin] Failed to register system objects via manifest service. ` + + `Ensure ObjectQLPlugin is registered before TenantPlugin. Cause: ${err?.message ?? String(err)}`, + ); } - ctx.logger.info('[TenantPlugin] System objects registered', { objects: registered }); } },