// Generation strategies: // increment → driver-generated by default; caller MAY supply PK (e.g., seeding fixtures, importing legacy data). // uuid → generated here via crypto.randomUUID() unless caller supplies one. // assigned → caller must provide PK. // composite → always treated as 'assigned' regardless of the @generation hint. import type { MetaData } from "@metaobjectsdev/metadata"; import { TYPE_IDENTITY, IDENTITY_SUBTYPE_PRIMARY, IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION, GENERATION_INCREMENT, GENERATION_UUID, GENERATION_ASSIGNED, } from "@metaobjectsdev/metadata"; import { MetadataError, ValidationError } from "./errors.js"; export type IdentityResolution = | { kind: "driver-generated"; values: Record } | { kind: "preset"; values: Record }; export function resolveIdentity(entity: MetaData, data: Record): IdentityResolution { // Effective children so a TPH subtype resolves the inherited primary identity. const primary = entity.children().find( (c) => c.type === TYPE_IDENTITY && c.subType === IDENTITY_SUBTYPE_PRIMARY, ); if (!primary) { throw new MetadataError( `Entity '${entity.name}' has no primary identity — cannot resolve PK for create`, { entity: entity.name }, ); } // ADR-0039: effective attr — @fields may be inherited via the identity's extends. const fieldsAttr = primary.attr(IDENTITY_ATTR_FIELDS); if (!Array.isArray(fieldsAttr) || fieldsAttr.length === 0) { throw new MetadataError( `Entity '${entity.name}' primary identity has no @fields`, { entity: entity.name }, ); } const pkFields = fieldsAttr.map(String); const isComposite = pkFields.length > 1; // ADR-0039: effective attr — @generation may be inherited via the identity's extends. const generation = primary.attr(IDENTITY_ATTR_GENERATION); if (isComposite || generation === undefined || generation === GENERATION_ASSIGNED) { const missing = pkFields.filter((f) => data[f] === undefined || data[f] === null); if (missing.length > 0) { throw new ValidationError( `Missing required PK field(s) on '${entity.name}': ${missing.join(", ")}`, { entity: entity.name, errors: missing.map((field) => ({ field, rule: "required", message: `PK field '${field}' must be provided when @generation is 'assigned' (or unset, or composite)`, })), }, ); } const values: Record = {}; for (const f of pkFields) values[f] = data[f]; return { kind: "preset", values }; } if (generation === GENERATION_INCREMENT) { const provided = pkFields.filter((f) => data[f] !== undefined && data[f] !== null); if (provided.length === pkFields.length) { const values: Record = {}; for (const f of pkFields) values[f] = data[f]; return { kind: "preset", values }; } return { kind: "driver-generated", values: {} }; } if (generation === GENERATION_UUID) { const values: Record = {}; for (const f of pkFields) { values[f] = data[f] !== undefined && data[f] !== null ? data[f] : crypto.randomUUID(); } return { kind: "preset", values }; } // Unknown @generation values fall back to assigned-style: require the caller to supply the PK. const missing = pkFields.filter((f) => data[f] === undefined || data[f] === null); if (missing.length > 0) { throw new ValidationError( `Missing PK field(s) on '${entity.name}': ${missing.join(", ")}`, { entity: entity.name, errors: missing.map((field) => ({ field, rule: "required", message: `PK field '${field}' is required` })), }, ); } const values: Record = {}; for (const f of pkFields) values[f] = data[f]; return { kind: "preset", values }; }