Skip to content

Commit ae64947

Browse files
claudeos-zhuang
authored andcommitted
fix(spec): let defineSeed type a multi-value lookup as an array of keys
CI caught the other half of #3911: the RUNTIME resolved a natural-key array fine, but `defineSeed`'s record typing still declared every lookup as `string | null`, so authoring the array the loader now wants was a compile error (`Type 'string[]' is not assignable to type 'string'`) — the showcase seed added in the previous commit is exactly that case. `SeedFieldValue` widens a `multiple: true` lookup to `string | string[] | null`. A lone string stays legal there (the loader accepts it as one-element shorthand for the array shape the field stores); `master_detail` is inherently single and is not widened; an array on a single-value lookup and a wrapper object inside a multi array both remain compile errors — the guard that comment promises is intact, verified in both directions. Reaching that required `Field.lookup` to stop widening its config: with `config: FieldInput` the literal `multiple: true` collapses to `boolean` before it ever reaches the seed type. It is now generic over a `const` type parameter, with the return type intersected with `FieldInput` so the optional surface is unchanged — narrowing to just the passed keys broke consumers that read `.options` off a field union (app-todo). Type-level only; the returned object is identical at runtime. Repo-wide `pnpm -r typecheck` is clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gusro3gdGv4wgbBnaFy9ah
1 parent 627b188 commit ae64947

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

.changeset/seed-loader-multi-value-lookup.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ rejected, now with advice an author can act on: declare the field
2929
`ReferenceResolution` (`@objectstack/spec/data`) gains an optional `multiple`
3030
flag carrying the field's array-ness into resolution; it is additive and
3131
defaulted-absent, so existing dependency graphs are unaffected.
32+
33+
**Authoring types.** `defineSeed`'s per-field value type now widens a
34+
`multiple: true` lookup to `string | string[] | null` (a lone string stays legal
35+
— the loader accepts it as one-element shorthand). `master_detail` is inherently
36+
single and is not widened, and an array on a single-value lookup is still a
37+
compile error. To make that reachable, `Field.lookup` became generic over its
38+
config (`<const C extends FieldInput>`) so `multiple: true` survives as a
39+
literal instead of widening to `boolean`; the return type is intersected with
40+
`FieldInput` so its optional surface is unchanged. Type-level only — the
41+
returned object is byte-identical at runtime.

packages/spec/src/data/field.zod.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -717,11 +717,25 @@ export const Field = {
717717
},
718718

719719

720-
lookup: (reference: string, config: FieldInput = {}) => ({
721-
type: 'lookup',
722-
reference,
723-
...config
724-
} as const),
720+
/**
721+
* Lookup — a reference to another object's record.
722+
*
723+
* Generic over `config` (with a `const` type parameter) so literal values
724+
* SURVIVE into the returned field definition. A plain `config: FieldInput`
725+
* widens `multiple: true` to `boolean`, which erases exactly the fact
726+
* `defineSeed` needs to know: a `multiple: true` lookup is seeded from an
727+
* ARRAY of natural keys, a single-value one from a lone string
728+
* (framework#3911). Widening is purely a type-level change — the returned
729+
* object is identical at runtime.
730+
*/
731+
lookup: <const C extends FieldInput>(
732+
reference: string,
733+
config: C = {} as C,
734+
): FieldInput & C & { readonly type: 'lookup'; readonly reference: string } => ({
735+
type: 'lookup',
736+
reference,
737+
...config,
738+
} as FieldInput & C & { readonly type: 'lookup'; readonly reference: string }),
725739

726740
masterDetail: (reference: string, config: FieldInput = {}) => ({
727741
type: 'master_detail',

packages/spec/src/data/seed.zod.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,19 @@ export type SeedImportMode = z.infer<typeof SeedMode>;
9595
* strings, bigints, buffers, and null" (silently masked on an always-empty
9696
* `:memory:` DB, fatal-looking on a persistent one). Constrain those fields to
9797
* `string | null` at compile time; every other field stays `unknown`.
98+
*
99+
* A `multiple: true` lookup is the one exception: it stores an ARRAY of ids, so
100+
* it is seeded from an ARRAY of natural keys — one per element (framework#3911).
101+
* A lone string stays legal there because the loader accepts it as one-element
102+
* shorthand for the array shape the field stores. `master_detail` is inherently
103+
* single-valued and so is never widened.
98104
*/
99105
type SeedFieldValue<TFieldDef> =
100-
TFieldDef extends { type: 'lookup' | 'master_detail' } ? string | null : unknown;
106+
TFieldDef extends { type: 'lookup' | 'master_detail' }
107+
? TFieldDef extends { multiple: true }
108+
? string | string[] | null
109+
: string | null
110+
: unknown;
101111

102112
/** Shape of a single seed record, derived from the object's field definitions. */
103113
type SeedRecord<TFields> = {

0 commit comments

Comments
 (0)