Skip to content

Commit 43e17b8

Browse files
huangyiireneclaude
andauthored
fix(service-datasource): emit the authorised ObjectSchema.create shape for object drafts (#19776)
Fixes #19723 Clause-②: no — this pulls the emitter back onto a shape a ruling already declared; it adds no exported symbol and no key on any published payload. Verified: `git diff` touches no `packages/spec` path and adds no `export` to `@objectstack/service-datasource`'s public surface. ## The defect, re-measured at `origin/main` (e99a14c) `renderObjectSource` (`packages/services/service-datasource/src/external-datasource-service.ts`) emitted the annotated-object-literal form — `import type { ServiceObject } …`, an unexported `const NAME : ServiceObject = { … }`, closed by `export default NAME`. Ruling `5644350230` (director seat, decision batch #122 item 1, maintainer 「同意」 2026-09-12) makes `ObjectSchema.create({ … })` the one authorised shape for a `*.object.ts`: > Derived first from the long-term axis: one authoring shape, validated against the protocol at the moment of definition. The factory parses the object against `ObjectSchema` when the file is evaluated, so an error surfaces where it was written; the typed literal defers everything to a build the author may never run. Both premises on the card hold as written — I re-read them on `origin/main` rather than inheriting them: - the shape at `:931` is the annotated literal (confirmed); - the function's own docblock at `:857` reads 「Render a reviewable `*.object.ts` source string for an object draft」 (confirmed), and `os datasource introspect --out objects/wh_order.object.ts` is the command's published example, so the bytes are destined for a committed file the ruling governs. ## What changed One emitter, three lines of rendered output: - `import type { ServiceObject } from '@objectstack/spec/data';` becomes a **value** import of `ObjectSchema`. `import type` is elided at compile time, so a type-only import beside a factory call would leave a module that throws on its own first line — this is the half a shape-only fix silently gets wrong. - `const NAME : ServiceObject = {` becomes `export const NAME = ObjectSchema.create({`, closed with `});`. - `export default NAME;` is dropped. The export is named only, which is what the scaffolded barrel re-exports (`packages/create-objectstack/src/templates/blank/src/objects/index.ts` is `export { Note } from './note.object.js';`). A second export form in a generated file is the parallel shape the ruling closed. Both authored comment blocks the card names are preserved byte-for-byte: the remote-primary-key tombstone and the ADR-0028 unprefixed-name TODO. The renderer's docblock moved with the code — it no longer claims `tsc` over the string is the whole acceptance instrument, and it names the pin that now holds the other half. The `REMOTE_PRIMARY_KEY_COMMENT` docblock, which described the emitted file as "annotated", was corrected in the same edit. Real generated output, from `generateObjectDraft('warehouse', 'fact_orders')` with a namespace resolved and a remote primary key reported: ```ts // Generated by `os datasource introspect` (ADR-0015). Review before committing. import { ObjectSchema } from '@objectstack/spec/data'; export const wh_fact_orders = ObjectSchema.create({ name: 'wh_fact_orders', label: 'Fact Orders', datasource: 'warehouse', external: { remoteSchema: 'mart', remoteName: 'fact_orders' }, // Remote primary key: order_id // Preserved as a COMMENT because 'ServiceObject' has no authorable key for a // federated object's remote primary key (#11000): 'fields.[f].primaryKey' is // not part of the field schema, so emitting it produced a draft that neither // 'tsc' nor 'ObjectSchema' accepted. Nothing below reads this line. // It names the column(s) THIS DRAFT WAS GIVEN as the key. For a COMPOSITE key // some drivers report only the first column (#10997), so treat the list as a // lower bound and check it against the remote table before relying on it. fields: { order_id: { type: 'text' }, customer: { type: 'text' }, placed_at: { type: 'datetime' }, geom: { type: 'text' }, // REVIEW: unrecognised remote type — defaulted to 'text', verify }, // Org-wide default (OWD): who can see records they do NOT own. ADR-0090 D1 // requires this to be an authored decision rather than an accident — the // `security-owd-unset` author-time rule refuses an object without it, so a // draft that omitted it could not compile. 'private' is the rule's own // recommended default: owner + explicit shares. Widen it deliberately. sharingModel: 'private', }); ``` (The remote-key block renders `fields.[f].primaryKey` with angle brackets in the real output; they are spelled with square brackets here only because this body is stored through an API that rewrites short angle-bracket fragments.) ## The pin — form AND round-trip, because the form alone can be satisfied by a broken file `packages/services/service-datasource/src/__tests__/external-object-draft-authorised-shape.test.ts` (new, 12 cases). It is not a snapshot: 1. **form** — asserts the value import, `export const wh_customers = ObjectSchema.create({`, the call's closing `});`, and — separately — the **absence** of each piece of the refused form by name (`: ServiceObject = {`, the `import type` of `ServiceObject`, the unexported `const`, `export default`). A pin that only asserts the new spelling cannot say the old one left. 2. **round-trip** — strips the import line, rewrites the single named export to a `return`, and evaluates the emitted module body through the **real** `ObjectSchema` from `@objectstack/spec/data` with `new Function`. That is the same factory call the committed file makes on the author's machine; if `create()` would throw there, it throws here. The evaluated value is asserted equal to `ObjectSchema.parse(draft.definition)`, so the rendered file and the structured definition are proven to describe one object. 3. **negative control** — the same harness is fed a source with one unknown top-level key spliced in and is required to throw. A round-trip that cannot fail is not a measurement. The round-trip earned its keep on the first run: it failed because `external.writable: false` is applied by the schema's own default, which is standing evidence that the file really parsed rather than an object literal being handed back. That key is now asserted explicitly instead of the assertion being loosened. `external-object-draft-os-build.test.ts` had the only other shape assertion; it moved to the factory spelling. Its sibling `not.toContain('_customers:')` leg is replaced rather than deleted: the old spelling read the annotated literal's `const _customers:` and goes **vacuous** once the type annotation leaves the file, and a bare `not.toContain('_customers')` cannot replace it because the no-namespace TODO block legitimately renders that substring. It is now `toContain('export const customers = ObjectSchema.create({')` plus `not.toContain('export const _customers')`. ## Lane split — it stays in this lane The card and ruling item 4 warn that the door is a `packages/cli` command. Measured: `packages/cli/src/commands/datasource/introspect.ts` is a pure pass-through — it POSTs to the draft route and writes `draft.source` verbatim (`await writeFile(target, draft.source, 'utf8')`), with no knowledge of the shape. Its description, its two `--out objects/wh_order.object.ts` examples and its `*.object.ts` prose describe the destination, not the form, and all stay true. **No `packages/cli` change is owed**, so no sibling PR is needed for this card. ## Verification, all at `78a7783cd5` | what | command | result | |:--|:--|:--| | dependency closure | `pnpm --filter '@objectstack/service-datasource^...' build --concurrency=2` | `VERDICT command-exit 0` | | package tests | `pnpm --filter @objectstack/service-datasource test` | `Test Files 34 passed (34)` · `Tests 693 passed (693)` | | package typecheck | `pnpm --filter @objectstack/service-datasource typecheck` | `VERDICT command-exit 0` | | repo lint | `pnpm lint` (full repo, `eslint . --no-inline-config`) | exit 0 | | derived gate families | `node scripts/pm/dispatch-gates.mjs --commands` then `--ran` | 61 derived, 59 run green, 0 unrun | `tsc --listFiles` confirms the new test file is inside the package's own tsc program (1 hit), so the typecheck above really covers it. Two of the 61 families are **NOT MEASURED**, not green and not red: `check:dual-build-cjs-loads` and `check:type-check-debt` both exit **3** — `PREREQUISITE NOT MET` — because each reads built output for the whole workspace ("Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured."). That is a full-farm build this lane does not own; CI runs both against a built tree. ## Acceptance notes - `packages/cli/src/commands/init.ts` (two `TEMPLATES` sites) and `packages/cli/src/commands/generate.ts` still render `Data.ServiceObject = {`. Those are ruling item 1 / the `domain:cli` half, tracked as #19722 — ⛔ deliberately not touched here, per ruling item 4's lane split. - `check:keyed-text-bounds` selects its population by filename suffix over the repo tree, and no `*.object.ts` file exists under `packages/cli` or `packages/services`. The strings changed here are template literals inside a `.ts` file, so the gate never reads them in any shape; this PR neither depends on nor unblocks #19720. --- _Generated by [Claude Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4b23e4f commit 43e17b8

4 files changed

Lines changed: 300 additions & 11 deletions

File tree

‎.changeset/tidy-emus-relate.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
`os datasource introspect` now generates the authorised `*.object.ts` shape
6+
7+
The Object draft rendered by `generateObjectDraft` (and served by
8+
`POST /api/v1/datasources/:name/external/tables/:remote/draft`) used the
9+
annotated-object-literal form. The director-seat ruling of 2026-09-12 (decision
10+
batch #122 item 1) makes `ObjectSchema.create({ … })` the one authorised shape
11+
for a `*.object.ts`, and the draft is destined for a committed `*.object.ts` —
12+
the command's own `--out objects/wh_order.object.ts` example says so. Drafts
13+
generated before this release were therefore written in the shape the platform
14+
refuses.
15+
16+
FROM → TO, for a draft you already committed — one mechanical rewrite:
17+
18+
```ts
19+
// FROM
20+
import type { ServiceObject } from '@objectstack/spec/data';
21+
22+
const wh_order: ServiceObject = {
23+
name: 'wh_order',
24+
// …
25+
};
26+
27+
export default wh_order;
28+
29+
// TO
30+
import { ObjectSchema } from '@objectstack/spec/data';
31+
32+
export const wh_order = ObjectSchema.create({
33+
name: 'wh_order',
34+
// …
35+
});
36+
```
37+
38+
Two things change beyond the wrapper. The spec import is now a **value**
39+
import, because the factory runs when the file is evaluated — an `import type`
40+
would be elided and the module would throw on its own first line. And the
41+
export is **named only**: the `export default` is gone, matching the barrel the
42+
scaffolder writes (`export { X } from './x.object.js'`). A barrel that imported
43+
the draft as a default (`import X from './x.object.js'`) becomes
44+
`import { X } from './x.object.js'`.
45+
46+
Regenerating the draft is the other route: re-run
47+
`os datasource introspect <datasource> --table <remote> --out <path>`.
48+
49+
The two authored comment blocks the draft carries — the remote-primary-key note
50+
and the ADR-0028 unprefixed-name TODO — are unchanged.
51+
52+
Clause-②: no
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The generated draft is emitted in the ONE authorised `*.object.ts` shape.
5+
*
6+
* Director-seat ruling, decision batch #122 item 1, maintainer 「同意」
7+
* 2026-09-12, verbatim:
8+
*
9+
* > `ObjectSchema.create({ … })` is the one authorised shape for a
10+
* > `.object.ts` … The factory parses the object against `ObjectSchema` when
11+
* > the file is evaluated, so an error surfaces where it was written; the typed
12+
* > literal defers everything to a build the author may never run.
13+
*
14+
* `renderObjectSource` wrote the other one — `const X: ServiceObject = { … }`
15+
* behind an `import type`, closed by `export default X`. Its own docblock calls
16+
* the output a `*.object.ts`, and `os datasource introspect --out
17+
* objects/x.object.ts` tells the author to commit it under that suffix, so the
18+
* bytes really do land where the ruling governs.
19+
*
20+
* ## Why this file is not one `toContain` line
21+
*
22+
* The failure mode a shape assertion invites is a generator that emits the
23+
* right-LOOKING call around a definition the factory refuses — the defect moved
24+
* one layer down, with every string assertion still green. Both halves are
25+
* therefore pinned separately, and they measure different things:
26+
*
27+
* 1. **the form** — the value import, the named export bound to
28+
* `ObjectSchema.create(`, and the ABSENCE of the refused annotated-literal
29+
* form. A shape pin that only asserts the new spelling cannot say the old
30+
* one left.
31+
* 2. **the round-trip** — the emitted module body is EVALUATED with the real
32+
* `ObjectSchema` from `@objectstack/spec/data`, which is the same factory
33+
* call the committed file makes on the author's machine. Nothing is
34+
* re-spelled here: if `create()` would throw in the author's file, it throws
35+
* in this test.
36+
*
37+
* ## Why the evaluation instrument carries its own negative control
38+
*
39+
* An evaluation harness that silently stopped running the factory — a transform
40+
* that no longer matched, an import that resolved to a stub — would report
41+
* every draft as valid forever. The last block feeds it a source with one
42+
* unknown top-level key spliced in and requires it to throw. A round-trip that
43+
* cannot fail is not a measurement.
44+
*/
45+
46+
import { describe, it, expect } from 'vitest';
47+
import type { IntrospectedSchema } from '@objectstack/spec/contracts';
48+
import { ObjectSchema } from '@objectstack/spec/data';
49+
import {
50+
ExternalDatasourceService,
51+
type DatasourceLike,
52+
} from '../external-datasource-service.js';
53+
54+
function remoteSchema(): IntrospectedSchema {
55+
return {
56+
dialect: 'postgres',
57+
introspectedAt: '2026-09-23T00:00:00.000Z',
58+
tables: {
59+
'mart.customers': {
60+
name: 'mart.customers',
61+
indexes: [],
62+
columns: [
63+
{ name: 'id', type: 'text', nullable: false, primaryKey: true },
64+
{ name: 'name', type: 'varchar(255)', nullable: true, primaryKey: false },
65+
{ name: 'signed_up_at', type: 'timestamptz', nullable: true, primaryKey: false },
66+
],
67+
},
68+
},
69+
};
70+
}
71+
72+
function serviceWith(namespace?: string): ExternalDatasourceService {
73+
return new ExternalDatasourceService({
74+
introspect: async () => remoteSchema(),
75+
getDatasource: async (name): Promise<DatasourceLike> => ({ name, schemaMode: 'external' }),
76+
getObject: async () => undefined,
77+
listObjects: async () => [],
78+
getNamespace: () => namespace,
79+
});
80+
}
81+
82+
const draftFor = (namespace?: string) =>
83+
serviceWith(namespace).generateObjectDraft('warehouse', 'customers');
84+
85+
/**
86+
* Evaluate the emitted module body and return what its single export is bound
87+
* to — i.e. run the author's own `ObjectSchema.create(…)` call.
88+
*
89+
* The transform is asserted rather than assumed: a silently non-matching
90+
* `replace` would hand `new Function` a body with no `return` in it, which
91+
* evaluates to `undefined` and throws nothing at all.
92+
*/
93+
function evaluateEmittedModule(source: string): unknown {
94+
const body = source
95+
.split('\n')
96+
.filter((line) => !line.startsWith('import '))
97+
.join('\n')
98+
.replace(/^export const [A-Za-z_$][\w$]* = /m, 'return ');
99+
expect(body, 'the emitted module has no single named export to evaluate').toContain(
100+
'return ObjectSchema.create(',
101+
);
102+
return new Function('ObjectSchema', body)(ObjectSchema) as unknown;
103+
}
104+
105+
describe('the emitted draft carries the authorised `ObjectSchema.create` shape', () => {
106+
it('imports the factory as a VALUE and binds a single named export to its call', async () => {
107+
const draft = await draftFor('wh');
108+
109+
expect(draft.source).toContain("import { ObjectSchema } from '@objectstack/spec/data';");
110+
expect(draft.source).toContain('export const wh_customers = ObjectSchema.create({');
111+
// The call is closed as a call, not as a bare object literal.
112+
expect(draft.source.trimEnd().endsWith('});')).toBe(true);
113+
});
114+
115+
it('carries the shape on the no-namespace path too, TODO block and all', async () => {
116+
const draft = await draftFor(undefined);
117+
118+
// The TODO block renders ABOVE the import; the shape must survive it.
119+
expect(draft.source).toContain('TODO(namespace)');
120+
expect(draft.source).toContain("import { ObjectSchema } from '@objectstack/spec/data';");
121+
expect(draft.source).toContain('export const customers = ObjectSchema.create({');
122+
});
123+
124+
it('emits a value import — an `import type` would be elided and the file would throw', async () => {
125+
const draft = await draftFor('wh');
126+
expect(draft.source).not.toContain('import type');
127+
});
128+
});
129+
130+
describe('the refused annotated-literal form is absent, by name', () => {
131+
it.each([
132+
['the `ServiceObject` type annotation', ': ServiceObject = {'],
133+
['the type-only spec import', "import type { ServiceObject } from '@objectstack/spec/data';"],
134+
['the unexported `const` binding', 'const wh_customers: ServiceObject'],
135+
['the default export that closed it', 'export default'],
136+
])('%s is gone', async (_label, refused) => {
137+
const draft = await draftFor('wh');
138+
expect(draft.source).not.toContain(refused);
139+
});
140+
141+
it('names no `ServiceObject` type at all outside the preserved remote-key note', async () => {
142+
const draft = await draftFor('wh');
143+
const mentions = draft.source
144+
.split('\n')
145+
.filter((l) => l.includes('ServiceObject'))
146+
.map((l) => l.trim());
147+
148+
// The one survivor is the remote-primary-key tombstone, which explains a
149+
// SPEC type rather than describing this file's shape.
150+
expect(mentions).toEqual([
151+
"// Preserved as a COMMENT because 'ServiceObject' has no authorable key for a",
152+
]);
153+
});
154+
});
155+
156+
describe('round-trip — the factory call inside the emitted file accepts the draft', () => {
157+
it('evaluates without throwing and yields the definition the draft reports', async () => {
158+
const draft = await draftFor('wh');
159+
const evaluated = evaluateEmittedModule(draft.source) as Record<string, unknown>;
160+
161+
expect(evaluated.name).toBe('wh_customers');
162+
expect(evaluated.label).toBe('Customers');
163+
expect(evaluated.datasource).toBe('warehouse');
164+
expect(evaluated.sharingModel).toBe('private');
165+
expect(Object.keys(evaluated.fields as Record<string, unknown>)).toEqual([
166+
'id',
167+
'name',
168+
'signed_up_at',
169+
]);
170+
// `writable` is NOT emitted by the renderer — it is the schema default,
171+
// applied because the evaluated file really parsed. Spelled out rather than
172+
// loosened to `toMatchObject`: this key is the cheapest standing evidence
173+
// that the factory ran instead of an object literal being handed back.
174+
expect(evaluated.external).toEqual({
175+
remoteSchema: 'mart',
176+
remoteName: 'customers',
177+
writable: false,
178+
});
179+
});
180+
181+
it('round-trips the no-namespace draft too — the TODO comment is inert to the factory', async () => {
182+
const draft = await draftFor(undefined);
183+
const evaluated = evaluateEmittedModule(draft.source) as Record<string, unknown>;
184+
expect(evaluated.name).toBe('customers');
185+
});
186+
187+
it('agrees with the structured definition the same draft carries', async () => {
188+
const draft = await draftFor('wh');
189+
const evaluated = evaluateEmittedModule(draft.source);
190+
// `create()` parses, so the comparison is against the parsed definition —
191+
// the rendered file and `draft.definition` must describe one object.
192+
expect(evaluated).toEqual(ObjectSchema.parse(draft.definition));
193+
});
194+
195+
it('NEGATIVE CONTROL — the evaluation really runs the factory', async () => {
196+
const draft = await draftFor('wh');
197+
const poisoned = draft.source.replace(
198+
" name: 'wh_customers',",
199+
" name: 'wh_customers',\n workflows: [],",
200+
);
201+
expect(poisoned).not.toBe(draft.source);
202+
203+
expect(() => evaluateEmittedModule(poisoned)).toThrow(/workflows/);
204+
});
205+
});

‎packages/services/service-datasource/src/__tests__/external-object-draft-os-build.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe('defect 1 — the generated object name carries the package namespace p
135135
// …and the rendered file agrees with the structured definition.
136136
expect(draft.definition.name).toBe('wh_customers');
137137
expect(draft.source).toContain("name: 'wh_customers'");
138-
expect(draft.source).toContain('const wh_customers: ServiceObject = {');
138+
expect(draft.source).toContain('export const wh_customers = ObjectSchema.create({');
139139
});
140140

141141
it('does NOT double-prefix a remote table that already carries the namespace', async () => {
@@ -227,7 +227,14 @@ describe('an absent or blank namespace must not trade one invalid draft for anot
227227

228228
expect(draft.name).toBe('customers');
229229
expect(draft.name.startsWith('_')).toBe(false);
230-
expect(draft.source).not.toContain('_customers:');
230+
// Spelled against the binding the authorised shape emits. The old spelling
231+
// (`'_customers:'`) read the annotated literal's `const _customers:` and
232+
// went vacuous the moment the type annotation left the file — it matches no
233+
// substring of `export const _customers = ObjectSchema.create({`. A bare
234+
// `not.toContain('_customers')` cannot replace it either: the no-namespace
235+
// TODO block legitimately renders `'<namespace>_customers'`.
236+
expect(draft.source).toContain('export const customers = ObjectSchema.create({');
237+
expect(draft.source).not.toContain('export const _customers');
231238
expect(ObjectSchema.safeParse(draft.definition).success).toBe(true);
232239
});
233240

‎packages/services/service-datasource/src/external-datasource-service.ts‎

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,19 @@ const GENERATED_SHARING_MODEL = 'private';
247247
* into the generated source — and the one place the reason is written down.
248248
*
249249
* `fields.<f>.primaryKey` is **not a key of the spec field schema**. Emitting
250-
* it produced a `*.object.ts` the platform's own toolchain refused on both
251-
* instruments it is annotated for: `tsc --noEmit` against `ServiceObject`
250+
* it produced a `*.object.ts` the platform's own toolchain refused on both of
251+
* its instruments: `tsc --noEmit` against `ServiceObject`
252252
* (`TS2353 … 'primaryKey' does not exist in type`) and
253253
* `ObjectSchema.safeParse` (`unrecognized_keys` at `["fields","<f>"]`). So the
254254
* generator had a pinned path that produced a draft neither the compiler nor
255255
* the validator would take (#11000).
256256
*
257+
* Both instruments still judge it, and the second one moved CLOSER: the
258+
* emitted file is now the authorised `ObjectSchema.create({ … })` shape (see
259+
* {@link renderObjectSource}), so the validator's verdict arrives when the
260+
* committed module is evaluated rather than only when somebody runs a build.
261+
* Re-emitting the key would therefore throw in the author's own file.
262+
*
257263
* Maintainer ruling, 2026-08-22 live session (「同意所有」, item 8) — **D**:
258264
*
259265
* > `generateObjectDraft`/`renderObjectSource` stop emitting
@@ -856,8 +862,26 @@ export class ExternalDatasourceService implements IExternalDatasourceService {
856862
/**
857863
* Render a reviewable `*.object.ts` source string for an object draft.
858864
*
859-
* The output is annotated `ServiceObject`, which makes `tsc` over this string
860-
* a complete acceptance instrument for the draft's shape — use it that way.
865+
* The output is the ONE authorised `*.object.ts` shape: a single named export
866+
* bound to `ObjectSchema.create({ … })` — director-seat ruling, decision batch
867+
* #122 item 1, maintainer 「同意」 2026-09-12. Its reasoning is what the two
868+
* halves of this renderer have to preserve:
869+
*
870+
* > The factory parses the object against `ObjectSchema` when the file is
871+
* > evaluated, so an error surfaces where it was written; the typed literal
872+
* > defers everything to a build the author may never run.
873+
*
874+
* ⇒ `tsc` over this string is NO LONGER the whole acceptance instrument, and a
875+
* renderer that emitted the factory CALL around a definition the factory
876+
* refuses would have moved the defect rather than fixed it. Both instruments
877+
* are owed a pin: the emitted call's shape, and an evaluation of the emitted
878+
* module body through the real `ObjectSchema.create`
879+
* (`external-object-draft-authorised-shape.test.ts` holds both).
880+
*
881+
* The export is NAMED, and there is deliberately no `export default` beside
882+
* it: the scaffolded barrel re-exports object modules by name
883+
* (`export { X } from './x.object.js'`), and a second export form in a
884+
* generated file is exactly the parallel shape the ruling closed.
861885
*
862886
* `namespace` is passed in rather than re-derived from `definition.name`,
863887
* because the two absent cases are NOT the same file: a name that is already
@@ -926,9 +950,12 @@ function renderObjectSource(
926950
return [
927951
`// Generated by \`os datasource introspect\` (ADR-0015). Review before committing.`,
928952
...namespaceTodo,
929-
`import type { ServiceObject } from '@objectstack/spec/data';`,
953+
// A VALUE import, not a type-only one: the factory runs when the committed
954+
// file is evaluated. `import type` here would be elided at compile time and
955+
// the emitted module would throw on its own first line.
956+
`import { ObjectSchema } from '@objectstack/spec/data';`,
930957
``,
931-
`const ${definition.name as string}: ServiceObject = {`,
958+
`export const ${definition.name as string} = ObjectSchema.create({`,
932959
` name: '${definition.name as string}',`,
933960
` label: '${definition.label as string}',`,
934961
` datasource: '${definition.datasource as string}',`,
@@ -943,9 +970,7 @@ function renderObjectSource(
943970
` // draft that omitted it could not compile. '${GENERATED_SHARING_MODEL}' is the rule's own`,
944971
` // recommended default: owner + explicit shares. Widen it deliberately.`,
945972
` sharingModel: '${definition.sharingModel as string}',`,
946-
`};`,
947-
``,
948-
`export default ${definition.name as string};`,
973+
`});`,
949974
``,
950975
].join('\n');
951976
}

0 commit comments

Comments
 (0)