@@ -159,6 +159,44 @@ function primaryKeyFieldNames(obj: MetaObject): Set<string> {
159159 return new Set ( primaryIdentityFieldNames ( obj ) ) ;
160160}
161161
162+ /** PK field names the CALLER must supply on insert: an ASSIGNED primary key —
163+ * a natural key or an externally-issued id, i.e. a primary identity carrying no
164+ * @generation — that also has no @default to fill it.
165+ *
166+ * These are insert-REQUIRED regardless of @required, and that is a typing
167+ * necessity rather than a policy choice: the Drizzle column for such a PK is
168+ * `text("id").primaryKey()` with no default, so its inferred insert type
169+ * requires the value. Deriving the PK's optionality from @required like any
170+ * other field emitted `id: z.string().optional()`, and the generated
171+ * `create<Entity>` — which pipes `InsertSchema.parse(data)` straight into
172+ * `.values()` — then failed to compile (TS2769). It was also a semantic hole:
173+ * the schema accepted a create payload with no primary key at all.
174+ *
175+ * Excluded, because each is already insert-optional for a real reason:
176+ * • @generation increment|uuid PKs — omitted from the schema entirely
177+ * (autoGenPkFieldNames), the caller never supplies them;
178+ * • a PK carrying @default — the column gets that default, so omitting is
179+ * legal and forcing it required would make callers repeat the default.
180+ * Mirrors primaryKeyFieldNames' reasoning for the UpdateSchema: a PK column is
181+ * never NULL, so PK optionality can never be read off @required alone. */
182+ function assignedPkFieldNames ( obj : MetaObject ) : Set < string > {
183+ const autoGen = autoGenPkFieldNames ( obj ) ;
184+ const out = new Set < string > ( ) ;
185+ for ( const name of primaryIdentityFieldNames ( obj ) ) {
186+ if ( autoGen . has ( name ) ) continue ;
187+ out . add ( name ) ;
188+ }
189+ return out ;
190+ }
191+
192+ /** True when this field is an assigned PK with no @default — see
193+ * assignedPkFieldNames. Kept as one predicate so the emitters and the
194+ * documented-shape function (insertSchemaFields) cannot drift. */
195+ function isInsertRequiredPk ( field : MetaField , assignedPk : Set < string > ) : boolean {
196+ if ( ! assignedPk . has ( field . name ) ) return false ;
197+ return field . attr ( FIELD_ATTR_DEFAULT ) === undefined ;
198+ }
199+
162200/**
163201 * Emit ONLY the `<Name>InsertSchema`. Used by the value-object file emitter
164202 * for metaobjects with no writable source.rdb — those have no PATCH/update
@@ -171,6 +209,7 @@ function primaryKeyFieldNames(obj: MetaObject): Set<string> {
171209export function renderInsertSchemaOnly ( obj : MetaObject , ctx ?: RenderContext ) : Code {
172210 const z = imp ( "z@zod" ) ;
173211 const autoGenPkFields = autoGenPkFieldNames ( obj ) ;
212+ const assignedPkFields = assignedPkFieldNames ( obj ) ;
174213 const tphPin = tphDiscriminatorPin ( obj ) ;
175214
176215 const insertFieldLines : Code [ ] = [ ] ;
@@ -204,7 +243,9 @@ export function renderInsertSchemaOnly(obj: MetaObject, ctx?: RenderContext): Co
204243 : code ` ${ child . name } : z.string().optional().transform(() => new Date().toISOString())` ,
205244 ) ;
206245 } else {
207- insertFieldLines . push ( code ` ${ child . name } : ${ zodFieldExpr ( child , obj , ctx ) } ` ) ;
246+ insertFieldLines . push (
247+ code ` ${ child . name } : ${ zodFieldExpr ( child , obj , ctx , isInsertRequiredPk ( child , assignedPkFields ) ) } ` ,
248+ ) ;
208249 }
209250 }
210251
@@ -246,11 +287,14 @@ export interface SchemaFieldShape {
246287 * • @readOnly fields are omitted (DB / replication owns the write path);
247288 * • a TPH subtype's @discriminator field is a pinned `z.literal(value)`;
248289 * • @autoSet fields are present but optional (server fills them);
290+ * • an ASSIGNED primary key with no @default is required (see
291+ * assignedPkFieldNames — its Drizzle column has no default);
249292 * • every other field's optionality is `fieldWillBeOptional` (not required, or
250293 * carries a @default).
251294 */
252295export function insertSchemaFields ( obj : MetaObject ) : SchemaFieldShape [ ] {
253296 const autoGenPkFields = autoGenPkFieldNames ( obj ) ;
297+ const assignedPkFields = assignedPkFieldNames ( obj ) ;
254298 const tphPin = tphDiscriminatorPin ( obj ) ;
255299 const out : SchemaFieldShape [ ] = [ ] ;
256300 for ( const child of obj . fields ( ) ) {
@@ -266,7 +310,10 @@ export function insertSchemaFields(obj: MetaObject): SchemaFieldShape[] {
266310 if ( autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE ) {
267311 out . push ( { name : child . name , optional : true , autoSet : true } ) ;
268312 } else {
269- out . push ( { name : child . name , optional : fieldWillBeOptional ( child ) } ) ;
313+ out . push ( {
314+ name : child . name ,
315+ optional : isInsertRequiredPk ( child , assignedPkFields ) ? false : fieldWillBeOptional ( child ) ,
316+ } ) ;
270317 }
271318 }
272319 return out ;
@@ -311,6 +358,7 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
311358 const z = imp ( "z@zod" ) ;
312359 const autoGenPkFields = autoGenPkFieldNames ( obj ) ;
313360 const pkFields = primaryKeyFieldNames ( obj ) ;
361+ const assignedPkFields = assignedPkFieldNames ( obj ) ;
314362 const tphPin = tphDiscriminatorPin ( obj ) ;
315363
316364 const insertFieldLines : Code [ ] = [ ] ;
@@ -358,7 +406,9 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
358406 // field expr) so an import/restore keeps the caller's original timestamp.
359407 preservingFieldLines . push ( code ` ${ child . name } : ${ zodFieldExpr ( child , obj , ctx ) } ` ) ;
360408 } else {
361- const fieldLine = code ` ${ child . name } : ${ zodFieldExpr ( child , obj , ctx ) } ` ;
409+ // The preserving schema is an INSERT shape (import / restore / replication),
410+ // so an assigned PK is required there for the same typing reason.
411+ const fieldLine = code ` ${ child . name } : ${ zodFieldExpr ( child , obj , ctx , isInsertRequiredPk ( child , assignedPkFields ) ) } ` ;
362412 insertFieldLines . push ( fieldLine ) ;
363413 preservingFieldLines . push ( fieldLine ) ;
364414 }
@@ -452,7 +502,14 @@ function zodScalarFor(subType: string): string {
452502 return "z.string()" ; // string/uuid/date/time/timestamp/decimal/enum on the wire
453503}
454504
455- function zodFieldExpr ( field : MetaField , owner ?: MetaObject , ctx ?: RenderContext ) : Code {
505+ function zodFieldExpr (
506+ field : MetaField ,
507+ owner ?: MetaObject ,
508+ ctx ?: RenderContext ,
509+ /** Suppress the trailing `.optional()` — see assignedPkFieldNames. Set ONLY
510+ * by the insert-shape emitters; the update/read shapes must stay optional. */
511+ forceRequired = false ,
512+ ) : Code {
456513 // `@dbColumnType: jsonb` on a scalar (legal only on field.string) is the
457514 // sanctioned "open JSON bag" escape hatch — a genuinely untyped JSON column
458515 // with no value-object to reference. Its Drizzle column is a bare `jsonb()`,
@@ -465,7 +522,7 @@ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext)
465522 if ( field . attr ( FIELD_ATTR_DB_COLUMN_TYPE ) === DB_COLUMN_TYPE_JSONB ) {
466523 let base : Code = code `z.unknown()` ;
467524 if ( field . resolvedIsArray ( ) ) base = code `z.array(${ base } )` ;
468- return appendValidatorChain ( base , field ) ;
525+ return appendValidatorChain ( base , field , forceRequired ) ;
469526 }
470527
471528 // FIELD_SUBTYPE_OBJECT: emit z.array(<Ref>InsertSchema) / <Ref>InsertSchema
@@ -490,13 +547,13 @@ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext)
490547 const refImp = imp ( `${ refName } InsertSchema@${ moduleSpec } ` ) ;
491548 let base : Code = code `${ refImp } ` ;
492549 if ( field . resolvedIsArray ( ) ) base = code `z.array(${ base } )` ;
493- return appendValidatorChain ( base , field ) ;
550+ return appendValidatorChain ( base , field , forceRequired ) ;
494551 }
495552 // No resolvable @objectRef — fall through to z.unknown(); downstream code
496553 // can still pass a value through but loses validation.
497554 let base : Code = code `z.unknown()` ;
498555 if ( field . resolvedIsArray ( ) ) base = code `z.array(${ base } )` ;
499- return appendValidatorChain ( base , field ) ;
556+ return appendValidatorChain ( base , field , forceRequired ) ;
500557 }
501558
502559 // field.map → z.record(z.string(), V): value is a VO's InsertSchema (@objectRef)
@@ -509,10 +566,10 @@ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext)
509566 ? valueObjectModuleSpecifier ( refName , ctx . packageOf , owner . package , ctx . outputLayout , ctx . extStyle )
510567 : `./${ refName } .js` ;
511568 const refImp = imp ( `${ refName } InsertSchema@${ moduleSpec } ` ) ;
512- return appendValidatorChain ( code `z.record(z.string(), ${ refImp } )` , field ) ;
569+ return appendValidatorChain ( code `z.record(z.string(), ${ refImp } )` , field , forceRequired ) ;
513570 }
514571 const vt = field . attr ( FIELD_ATTR_VALUE_TYPE ) ;
515- return appendValidatorChain ( code `z.record(z.string(), ${ zodScalarFor ( typeof vt === "string" ? vt : "string" ) } )` , field ) ;
572+ return appendValidatorChain ( code `z.record(z.string(), ${ zodScalarFor ( typeof vt === "string" ? vt : "string" ) } )` , field , forceRequired ) ;
516573 }
517574
518575 let baseStr : string ;
@@ -573,7 +630,7 @@ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext)
573630 const sharedConst = imp ( `${ constName } @${ spec } ` ) ;
574631 let base : Code = code `${ sharedConst } ` ;
575632 if ( field . resolvedIsArray ( ) ) base = code `z.array(${ base } )` ;
576- return appendValidatorChain ( base , field ) ;
633+ return appendValidatorChain ( base , field , forceRequired ) ;
577634 }
578635 }
579636 baseStr = zodEnumExpr ( values ) ;
@@ -612,7 +669,7 @@ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext)
612669 }
613670
614671 if ( field . resolvedIsArray ( ) ) baseStr = `z.array(${ baseStr } )` ;
615- return appendValidatorChain ( code `${ baseStr } ` , field ) ;
672+ return appendValidatorChain ( code `${ baseStr } ` , field , forceRequired ) ;
616673}
617674
618675/** Mirrors the optional-or-not decision inside appendValidatorChain so the update-schema
@@ -635,7 +692,7 @@ const NUMERIC_FIELD_SUBTYPES = new Set<string>([
635692 * - numeric (scalar) → .min/.max = numeric value (validator.numeric)
636693 * - array (any element) → .min/.max = element count (validator.array)
637694 */
638- function appendValidatorChain ( base : Code , field : MetaField ) : Code {
695+ function appendValidatorChain ( base : Code , field : MetaField , forceRequired = false ) : Code {
639696 let isRequired = field . attr ( FIELD_ATTR_REQUIRED ) === true ;
640697 let maxLen : number | undefined = field . attr ( FIELD_ATTR_MAX_LENGTH ) as number | undefined ;
641698 let minLen : number | undefined ;
@@ -709,6 +766,12 @@ function appendValidatorChain(base: Code, field: MetaField): Code {
709766 if ( numMax !== undefined ) chain = code `${ chain } .max(${ numMax } )` ;
710767 }
711768
769+ // An assigned PK is insert-REQUIRED whatever @required says — its Drizzle
770+ // column has no default, so an optional value cannot typecheck into
771+ // `.values()`. Only the caller knows this (the update/read shapes must stay
772+ // optional), hence the explicit opt-in. See assignedPkFieldNames.
773+ if ( forceRequired ) return chain ;
774+
712775 // Fields with DB-level defaults are optional in the InsertSchema: the caller
713776 // can omit them and the DB will fill in. Otherwise required-with-default
714777 // would force callers to repeat the default at every call site.
0 commit comments