Skip to content

fix(seed-loader): resolve seed references for marketplace-installed objects (engine-registry fallback)#3422

Merged
os-zhuang merged 2 commits into
mainfrom
claude/modest-curie-056317
Jul 24, 2026
Merged

fix(seed-loader): resolve seed references for marketplace-installed objects (engine-registry fallback)#3422
os-zhuang merged 2 commits into
mainfrom
claude/modest-curie-056317

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

Marketplace-installed template packages (e.g. app.objectstack.hotcrm) seeded their lookup / master_detail reference fields with the raw externalId string instead of the target record's id: crm_contact.crm_account and crm_opportunity.crm_account held "Acme Corporation" rather than the crm_account row id.

The damage compounded under RLS: crm_contact declares sharingModel: controlled_by_parent, whose row filter compiles to a join on the parent reference. With every reference dangling, the join matched nothing and the whole object went invisible to everyone — platform admins included (REST list total=0, single GET 404) while 5 rows sat in the SQLite table.

Root cause

SeedLoaderService.buildDependencyGraph built the reference graph from metadata.getObject() only. Marketplace packages register their objects through the manifest service straight into the ObjectQL registry (ql.registerApp) — and both the install endpoint and boot-time rehydrate run after bridgeObjectsToMetadataService did its one-shot copy at kernel:ready. The metadata service therefore never lists crm_* objects, the reference graph came back empty, and every reference value fell through the "no known refs" path and was written verbatim.

Showcase's own config-declared apps were unaffected because AppPlugin registers before the bridge runs.

Fix

buildDependencyGraph now resolves object definitions via the metadata service first, then falls back to the engine's own schema registry (feature-detected engine.getSchema(), which the ObjectQL engine exposes; the IDataEngine contract is unchanged). Engines without a schema registry keep the old behavior. One change in @objectstack/metadata-protocol covers every seed path (marketplace install/reseed, AppPlugin, dispatcher) since objectql/runtime re-export the same class.

Regression tests

  • packages/metadata-protocol/src/seed-loader-engine-schema-fallback.test.ts — engine-registry-only objects resolve (master_detail, lookup, self-reference, dependency ordering); metadata still wins when present; engines without getSchema degrade exactly as before. Red-checked: reverting the fix fails the marketplace-scenario case.
  • packages/cloud-connection/src/marketplace-install-local-seed-lookup.test.ts — drives the real POST /api/v1/marketplace/install-local endpoint with the real SeedLoaderService (not mocked, unlike the reseed spec), a faithful engine stub, and a metadata service that has never heard of the package's objects; pins that reference columns hold parent ids, never the authored natural-key strings. Also red-checked.

E2E verification

examples/app-showcase + the shipped app.objectstack.hotcrm@2.2.2 install artifact on a fresh DB, objectstack dev --seed-admin (workspace CLI), signed in as the dev admin, reseeded, then:

  • GET /api/v1/data/crm_contacttotal: 5 (was 0), every crm_account an internal id
  • single GET /api/v1/data/crm_contact/:id → 200 (was 404), id maps back to Acme Corporation
  • GET /api/v1/data/crm_opportunity → 61 rows, 0 dangling account references
  • reseed summary: inserted=162, errors=0

Related: #3421 heals the missing rows case on rehydrate — orthogonal files, complementary fixes (once it lands, its heal path benefits from this resolution fix too).

🤖 Generated with Claude Code

…egistry (marketplace installs)

Marketplace package objects register through the manifest service straight
into the ObjectQL registry — after the boot-time bridgeObjectsToMetadataService
pass — so the metadata service never lists them. buildDependencyGraph consulted
only metadata.getObject(), got an empty reference graph for crm_*, and wrote
every lookup/master_detail seed value verbatim (the raw externalId string,
e.g. crm_contact.crm_account = 'Acme Corporation') instead of the target
record's id. Under sharingModel: controlled_by_parent the dangling parent join
then hid every contact from every user (REST total=0, GET 404) while the rows
sat in the table.

The loader now falls back to the engine's feature-detected getSchema() when
the metadata service has no definition for a seeded object. Metadata stays the
preferred source; contract-minimal engines keep the old behavior.

Regression tests: a metadata-protocol unit spec (engine-registry-only objects
resolve, metadata still wins when present, no-getSchema engines degrade as
before) and a cloud-connection integration spec driving the real install
endpoint + real SeedLoaderService, pinning that reference columns hold ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 24, 2026 11:40am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/metadata-protocol.

1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

os-zhuang added a commit that referenced this pull request Jul 24, 2026
…ata service (#3428)

Marketplace-installed template packages register through the manifest
service on kernel:ready (rehydrate) or an HTTP request (install), but the
one-shot SchemaRegistry-to-metadata bridge runs once during
ObjectQLPlugin.start() - so their objects only ever reached the ObjectQL
registry. Every IMetadataService consumer (AI describe_object, Studio
object lists, metadata.listObjects) missed them; only the seed loader had
grown an engine-side fallback (#3422).

The manifest service's register now bridges the manifest's own objects
(registry-resolved, so both objects forms and extension merges come out
canonical with _packageId stamped) into the metadata service:

- resolved at call time: at objectql init the metadata service may not be
  registered yet
- register('object', name, obj, { notify: false }) - same #3112 rationale
  as the startup bridge: announcing would loop the definitions back
  through our own subscribe('object') handler and overwrite provenance
  with 'metadata-service'
- entries it did not bridge itself are never clobbered; its own copy is
  refreshed on same-package re-install so a hot marketplace upgrade stays
  fresh
- armed only after start() ran the one-shot bridge, and never on project
  kernels (same environmentId gate) - boot-time behavior is unchanged

register returns the bridge promise; marketplace install/rehydrate await
it so metadata reads right after a 200 are deterministic.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ld CI runners

The spec drives the real install handler, whose dynamic
import('@objectstack/runtime') alone can take several seconds on a fresh
runner — the 5s vitest default timed out on CI while passing warm locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant