dyna-record 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -212,7 +212,7 @@ class Store extends MyTable {
212
212
  }
213
213
  ```
214
214
 
215
- - **Supported field types:** `"string"`, `"number"`, `"boolean"`, `"date"` (stored as ISO strings, exposed as `Date` objects), `"enum"` (via `values`), nested `"object"` (via `fields`), and `"array"` (via `items`)
215
+ - **Supported field types:** `"string"`, `"number"`, `"boolean"`, `"date"` (stored as ISO strings, exposed as `Date` objects), `"enum"` (via `values`), nested `"object"` (via `fields`), `"array"` (via `items`), and `"discriminatedUnion"` (via `discriminator` + `variants`)
216
216
  - **Nullable fields:** Set `nullable: true` on individual non-object fields within the schema to make them optional
217
217
  - **Object attributes are never nullable:** DynamoDB cannot update nested document paths (e.g., `address.geo.lat`) if the parent object does not exist. To prevent this, `@ObjectAttribute` fields always exist as at least an empty object `{}`. Nested object fields within the schema are also never nullable. Non-object fields (primitives, enums, dates, arrays) can still be nullable.
218
218
  - **Alias support:** Use the `alias` option to map to a different DynamoDB attribute name
@@ -249,6 +249,48 @@ const schema = {
249
249
 
250
250
  The schema must be declared with `as const satisfies ObjectSchema` so TypeScript preserves the literal string values for type inference. At runtime, providing an invalid value (e.g., `status: "unknown"`) throws a `ValidationError`.
251
251
 
252
+ ##### Discriminated union fields
253
+
254
+ Use `{ type: "discriminatedUnion", discriminator: "...", variants: { ... } }` to define a field that is a tagged union of object types. Each variant is an `ObjectSchema` keyed by its discriminator value. The discriminator key is automatically included in each variant's inferred type as a string literal.
255
+
256
+ ```typescript
257
+ const drawingSchema = {
258
+ shape: {
259
+ type: "discriminatedUnion",
260
+ discriminator: "kind",
261
+ variants: {
262
+ circle: { radius: { type: "number" } },
263
+ square: { side: { type: "number" } }
264
+ }
265
+ }
266
+ } as const satisfies ObjectSchema;
267
+
268
+ @Entity
269
+ class Drawing extends MyTable {
270
+ declare readonly type: "Drawing";
271
+
272
+ @ObjectAttribute({ alias: "Drawing", schema: drawingSchema })
273
+ public readonly drawing: InferObjectSchema<typeof drawingSchema>;
274
+ }
275
+
276
+ // TypeScript infers:
277
+ // drawing.shape → { kind: "circle"; radius: number } | { kind: "square"; side: number }
278
+ ```
279
+
280
+ Unlike nested `"object"` fields, discriminated union fields **can be nullable** (`nullable: true`) because they always use **full replacement** on update rather than document path expressions:
281
+
282
+ ```typescript
283
+ // Replaces the entire shape field — no partial merge
284
+ await Drawing.update("123", {
285
+ drawing: { shape: { kind: "square", side: 10 } }
286
+ });
287
+ ```
288
+
289
+ **Scoping constraints (initial release):**
290
+
291
+ - Supported at the ObjectAttribute root level and as fields within an ObjectSchema
292
+ - Not supported inside array items or nested inside other discriminated unions
293
+
252
294
  ### Foreign Keys
253
295
 
254
296
  Define foreign keys in order to support [@BelongsTo](https://docs.dyna-record.com/functions/BelongsTo.html) relationships. A foreign key is required for [@HasOne](https://docs.dyna-record.com/functions/HasOne.html) and [@HasMany](https://docs.dyna-record.com/functions/HasMany.html) relationships.
@@ -990,6 +1032,15 @@ await Store.update("123", {
990
1032
  });
991
1033
  ```
992
1034
 
1035
+ **Discriminated unions** within objects are also **full replacement** (not merged):
1036
+
1037
+ ```typescript
1038
+ // Replaces the entire shape — switches from circle to square
1039
+ await Drawing.update("123", {
1040
+ drawing: { shape: { kind: "square", side: 10 } }
1041
+ });
1042
+ ```
1043
+
993
1044
  The instance `update` method returns a deep-merged result, preserving existing fields:
994
1045
 
995
1046
  ```typescript
@@ -10,7 +10,7 @@ import type { ObjectSchema, InferObjectSchema } from "./types";
10
10
  * `ValidationException: The document path provided in the update expression is invalid for update`.
11
11
  * To avoid this, `@ObjectAttribute` fields always exist as at least an empty object `{}`.
12
12
  *
13
- * The schema supports all {@link FieldDef} types: primitives, enums, nested objects, and arrays.
13
+ * The schema supports all {@link FieldDef} types: primitives, enums, nested objects, arrays, and discriminated unions.
14
14
  * Non-object fields within the schema may still be nullable.
15
15
  *
16
16
  * @template S The specific ObjectSchema type used for type inference
@@ -47,6 +47,7 @@ export interface ObjectAttributeOptions<S extends ObjectSchema> extends NonNullA
47
47
  * - `"date"` — dates stored as ISO strings (support `nullable: true`)
48
48
  * - `"object"` — nested objects, arbitrarily deep (**never nullable**)
49
49
  * - `"array"` — lists of any field type (support `nullable: true`, full replacement on update)
50
+ * - `"discriminatedUnion"` — tagged unions via `discriminator` + `variants` (support `nullable: true`, full replacement on update)
50
51
  *
51
52
  * Objects within arrays are not subject to the document path limitation because arrays
52
53
  * use full replacement on update. Partial updates of individual objects within arrays
@@ -102,6 +103,10 @@ export interface ObjectAttributeOptions<S extends ObjectSchema> extends NonNullA
102
103
  * await MyEntity.update("id", { address: { zip: null } });
103
104
  * ```
104
105
  *
106
+ * **Discriminated union fields** always use **full replacement** on update — the user
107
+ * must provide a complete variant object. See {@link DiscriminatedUnionFieldDef} for
108
+ * the rationale.
109
+ *
105
110
  * Object attributes support filtering in queries using dot-path notation for nested fields
106
111
  * and the {@link ContainsFilter | $contains} operator for List membership checks.
107
112
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ObjectAttribute.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/ObjectAttribute.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EACV,yBAAyB,EACzB,uBAAuB,EACxB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAY,MAAM,SAAS,CAAC;AAGzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,YAAY,CAC5D,SAAQ,uBAAuB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC;CACX;AA6GD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,iBAAS,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,KAAK,CAAC,CAAC,SAAS,YAAY,EACzE,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,YAGtB,SAAS,WACR,yBAAyB,CAChC,CAAC,EACD,iBAAiB,CAAC,CAAC,CAAC,EACpB,sBAAsB,CAAC,CAAC,CAAC,CAC1B,UAmBJ;AAED,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"ObjectAttribute.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/ObjectAttribute.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EACV,yBAAyB,EACzB,uBAAuB,EACxB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAGlB,MAAM,SAAS,CAAC;AAGjB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,YAAY,CAC5D,SAAQ,uBAAuB;IAC/B;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC;CACX;AAgKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AACH,iBAAS,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,KAAK,CAAC,CAAC,SAAS,YAAY,EACzE,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,YAGtB,SAAS,WACR,yBAAyB,CAChC,CAAC,EACD,iBAAiB,CAAC,CAAC,CAAC,EACpB,sBAAsB,CAAC,CAAC,CAAC,CAC1B,UAoBJ;AAED,eAAe,eAAe,CAAC"}
@@ -27,11 +27,17 @@ function objectSchemaToZodPartial(schema) {
27
27
  * Converts a single {@link FieldDef} to the corresponding partial Zod type.
28
28
  * Nested objects use partial schemas; all other types use the standard schema.
29
29
  * Object fields are never nullable — they always exist as at least `{}`.
30
+ * Discriminated union fields use the full schema (not partial) since they
31
+ * always use full replacement on update.
30
32
  */
31
33
  function fieldDefToZodPartial(fieldDef) {
32
34
  if (fieldDef.type === "object") {
33
35
  return objectSchemaToZodPartial(fieldDef.fields);
34
36
  }
37
+ if (fieldDef.type === "discriminatedUnion") {
38
+ // Discriminated unions use full replacement — same schema as create
39
+ return discriminatedUnionToZod(fieldDef);
40
+ }
35
41
  // For non-object fields, use the standard schema (includes nullable wrapping)
36
42
  return fieldDefToZod(fieldDef);
37
43
  }
@@ -48,12 +54,39 @@ function objectSchemaToZod(schema) {
48
54
  }
49
55
  return zod_1.z.object(shape);
50
56
  }
57
+ /**
58
+ * Builds a Zod `discriminatedUnion` schema from a {@link DiscriminatedUnionFieldDef}.
59
+ *
60
+ * Each variant's ObjectSchema is converted to a `z.object()` and extended with a
61
+ * `z.literal()` for the discriminator key. The resulting schemas are wrapped in
62
+ * `z.discriminatedUnion()`.
63
+ *
64
+ * @param fieldDef The discriminated union field definition
65
+ * @returns A ZodType that validates discriminated union values
66
+ */
67
+ function discriminatedUnionToZod(fieldDef) {
68
+ if (Object.keys(fieldDef.variants).length === 0) {
69
+ throw new Error("DiscriminatedUnionFieldDef requires at least one variant");
70
+ }
71
+ const variantSchemas = Object.entries(fieldDef.variants).map(([variantKey, variantObjectSchema]) => {
72
+ const variantZod = objectSchemaToZod(variantObjectSchema);
73
+ return variantZod.extend({
74
+ [fieldDef.discriminator]: zod_1.z.literal(variantKey)
75
+ });
76
+ });
77
+ let zodType = zod_1.z.discriminatedUnion(fieldDef.discriminator, variantSchemas);
78
+ if (fieldDef.nullable === true) {
79
+ zodType = zodType.optional().nullable();
80
+ }
81
+ return zodType;
82
+ }
51
83
  /**
52
84
  * Converts a single {@link FieldDef} to the corresponding Zod type for runtime validation.
53
85
  *
54
86
  * Handles all field types:
55
87
  * - `"object"` → recursively builds a `z.object()` via {@link objectSchemaToZod}.
56
88
  * Object fields are never nullable — DynamoDB requires them to exist for document path updates.
89
+ * - `"discriminatedUnion"` → `z.discriminatedUnion()` via {@link discriminatedUnionToZod}
57
90
  * - `"array"` → `z.array()` wrapping a recursive call for the `items` type
58
91
  * - `"string"` → `z.string()`
59
92
  * - `"number"` → `z.number()`
@@ -70,6 +103,10 @@ function fieldDefToZod(fieldDef) {
70
103
  if (fieldDef.type === "object") {
71
104
  return objectSchemaToZod(fieldDef.fields);
72
105
  }
106
+ // Discriminated union fields handle their own nullable wrapping
107
+ if (fieldDef.type === "discriminatedUnion") {
108
+ return discriminatedUnionToZod(fieldDef);
109
+ }
73
110
  let zodType;
74
111
  switch (fieldDef.type) {
75
112
  case "array":
@@ -118,6 +155,7 @@ function fieldDefToZod(fieldDef) {
118
155
  * - `"date"` — dates stored as ISO strings (support `nullable: true`)
119
156
  * - `"object"` — nested objects, arbitrarily deep (**never nullable**)
120
157
  * - `"array"` — lists of any field type (support `nullable: true`, full replacement on update)
158
+ * - `"discriminatedUnion"` — tagged unions via `discriminator` + `variants` (support `nullable: true`, full replacement on update)
121
159
  *
122
160
  * Objects within arrays are not subject to the document path limitation because arrays
123
161
  * use full replacement on update. Partial updates of individual objects within arrays
@@ -173,6 +211,10 @@ function fieldDefToZod(fieldDef) {
173
211
  * await MyEntity.update("id", { address: { zip: null } });
174
212
  * ```
175
213
  *
214
+ * **Discriminated union fields** always use **full replacement** on update — the user
215
+ * must provide a complete variant object. See {@link DiscriminatedUnionFieldDef} for
216
+ * the rationale.
217
+ *
176
218
  * Object attributes support filtering in queries using dot-path notation for nested fields
177
219
  * and the {@link ContainsFilter | $contains} operator for List membership checks.
178
220
  *
@@ -188,11 +230,12 @@ function fieldDefToZod(fieldDef) {
188
230
  */
189
231
  function ObjectAttribute(props) {
190
232
  return function (_value, context) {
233
+ // Fail fast: surface schema validation errors at class definition time.
234
+ const { schema, ...restProps } = props;
235
+ const zodSchema = objectSchemaToZod(schema);
236
+ const partialZodSchema = objectSchemaToZodPartial(schema);
237
+ const serializers = (0, serializers_1.createObjectSerializer)(schema);
191
238
  context.addInitializer(function () {
192
- const { schema, ...restProps } = props;
193
- const zodSchema = objectSchemaToZod(schema);
194
- const partialZodSchema = objectSchemaToZodPartial(schema);
195
- const serializers = (0, serializers_1.createObjectSerializer)(schema);
196
239
  metadata_1.default.addEntityAttribute(this.constructor.name, {
197
240
  attributeName: context.name.toString(),
198
241
  type: zodSchema,
@@ -10,5 +10,5 @@ export { default as IdAttribute } from "./IdAttribute";
10
10
  export { default as ObjectAttribute } from "./ObjectAttribute";
11
11
  export type { ObjectAttributeOptions } from "./ObjectAttribute";
12
12
  export * from "./serializers";
13
- export type { ObjectSchema, InferObjectSchema, FieldDef, PrimitiveFieldDef, ObjectFieldDef, ArrayFieldDef, EnumFieldDef, DateFieldDef } from "./types";
13
+ export type { ObjectSchema, NonUnionObjectSchema, InferObjectSchema, InferDiscriminatedUnion, FieldDef, NonUnionFieldDef, PrimitiveFieldDef, ObjectFieldDef, ArrayFieldDef, EnumFieldDef, DateFieldDef, DiscriminatedUnionFieldDef } from "./types";
14
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,cAAc,eAAe,CAAC;AAC9B,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,YAAY,EACZ,YAAY,EACb,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,cAAc,eAAe,CAAC;AAC9B,YAAY,EACV,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,uBAAuB,EACvB,QAAQ,EACR,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,0BAA0B,EAC3B,MAAM,SAAS,CAAC"}
@@ -19,6 +19,8 @@ export declare const dateSerializer: {
19
19
  * - `"date"` fields are converted from `Date` objects to ISO 8601 strings.
20
20
  * - `"object"` fields recurse into their nested schema.
21
21
  * - `"array"` fields map each item through the same conversion.
22
+ * - `"discriminatedUnion"` fields look up the variant schema by discriminator value
23
+ * and recurse into that variant's object schema, preserving the discriminator key.
22
24
  * - `null` and `undefined` values are stripped from the result so that nullable
23
25
  * fields set to `null` are removed from the stored object rather than persisted
24
26
  * as `null` in DynamoDB.
@@ -29,6 +31,21 @@ export declare const dateSerializer: {
29
31
  * @returns A new object suitable for DynamoDB storage
30
32
  */
31
33
  export declare function objectToTableItem(schema: ObjectSchema, value: Record<string, unknown>): Record<string, unknown>;
34
+ /**
35
+ * Converts a single field value to its DynamoDB table representation based on
36
+ * the field definition.
37
+ *
38
+ * - `"date"` → ISO 8601 string
39
+ * - `"object"` → recursively converts via {@link objectToTableItem}
40
+ * - `"array"` → maps each item through the same conversion
41
+ * - `"discriminatedUnion"` → looks up the variant schema by discriminator value,
42
+ * converts via {@link objectToTableItem}, and preserves the discriminator key
43
+ * - All other types pass through unchanged
44
+ *
45
+ * @param fieldDef The {@link FieldDef} describing the field's type
46
+ * @param val The entity-level value to convert
47
+ * @returns The DynamoDB-compatible value
48
+ */
32
49
  export declare function convertFieldToTableItem(fieldDef: FieldDef, val: unknown): unknown;
33
50
  /**
34
51
  * Recursively walks an {@link ObjectSchema} and converts a DynamoDB table item
@@ -37,6 +54,8 @@ export declare function convertFieldToTableItem(fieldDef: FieldDef, val: unknown
37
54
  * - `"date"` fields are converted from ISO 8601 strings to `Date` objects.
38
55
  * - `"object"` fields recurse into their nested schema.
39
56
  * - `"array"` fields map each item through the same conversion.
57
+ * - `"discriminatedUnion"` fields look up the variant schema by discriminator value
58
+ * and recurse into that variant's object schema, preserving the discriminator key.
40
59
  * - `null` and `undefined` values are stripped from the result so that absent
41
60
  * fields are represented as `undefined` (omitted) on the entity, consistent
42
61
  * with root-level nullable attribute behaviour.
@@ -1 +1 @@
1
- {"version":3,"file":"serializers.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/serializers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;6BACA,oBAAoB,KAAG,OAAO;4BAM/B,OAAO;CAEhC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAaT;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAiBD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW,CAOxE"}
1
+ {"version":3,"file":"serializers.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/serializers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;6BACA,oBAAoB,KAAG,OAAO;4BAM/B,OAAO;CAEhC,CAAC;AAuBF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,OAAO,GACX,OAAO,CAqBT;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAgCD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW,CAOxE"}
@@ -21,6 +21,20 @@ exports.dateSerializer = {
21
21
  },
22
22
  toTableAttribute: (val) => val instanceof Date ? val.toISOString() : undefined
23
23
  };
24
+ /**
25
+ * Resolves the variant schema for a discriminated union value by reading the
26
+ * discriminator key and looking up the corresponding variant in the field definition.
27
+ *
28
+ * Uses `Object.hasOwn` to guard against prototype property pollution — only
29
+ * own-enumerable variant keys are matched.
30
+ */
31
+ function resolveVariantSchema(fieldDef, value) {
32
+ const discriminatorValue = value[fieldDef.discriminator];
33
+ if (!Object.hasOwn(fieldDef.variants, discriminatorValue)) {
34
+ return undefined;
35
+ }
36
+ return fieldDef.variants[discriminatorValue];
37
+ }
24
38
  /**
25
39
  * Recursively walks an {@link ObjectSchema} and converts the entity value to its
26
40
  * DynamoDB representation.
@@ -28,6 +42,8 @@ exports.dateSerializer = {
28
42
  * - `"date"` fields are converted from `Date` objects to ISO 8601 strings.
29
43
  * - `"object"` fields recurse into their nested schema.
30
44
  * - `"array"` fields map each item through the same conversion.
45
+ * - `"discriminatedUnion"` fields look up the variant schema by discriminator value
46
+ * and recurse into that variant's object schema, preserving the discriminator key.
31
47
  * - `null` and `undefined` values are stripped from the result so that nullable
32
48
  * fields set to `null` are removed from the stored object rather than persisted
33
49
  * as `null` in DynamoDB.
@@ -48,6 +64,21 @@ function objectToTableItem(schema, value) {
48
64
  }
49
65
  return result;
50
66
  }
67
+ /**
68
+ * Converts a single field value to its DynamoDB table representation based on
69
+ * the field definition.
70
+ *
71
+ * - `"date"` → ISO 8601 string
72
+ * - `"object"` → recursively converts via {@link objectToTableItem}
73
+ * - `"array"` → maps each item through the same conversion
74
+ * - `"discriminatedUnion"` → looks up the variant schema by discriminator value,
75
+ * converts via {@link objectToTableItem}, and preserves the discriminator key
76
+ * - All other types pass through unchanged
77
+ *
78
+ * @param fieldDef The {@link FieldDef} describing the field's type
79
+ * @param val The entity-level value to convert
80
+ * @returns The DynamoDB-compatible value
81
+ */
51
82
  function convertFieldToTableItem(fieldDef, val) {
52
83
  switch (fieldDef.type) {
53
84
  case "date":
@@ -56,6 +87,15 @@ function convertFieldToTableItem(fieldDef, val) {
56
87
  return objectToTableItem(fieldDef.fields, val);
57
88
  case "array":
58
89
  return val.map(item => convertFieldToTableItem(fieldDef.items, item));
90
+ case "discriminatedUnion": {
91
+ const value = val;
92
+ const variantSchema = resolveVariantSchema(fieldDef, value);
93
+ if (variantSchema === undefined)
94
+ return val;
95
+ const result = objectToTableItem(variantSchema, value);
96
+ result[fieldDef.discriminator] = value[fieldDef.discriminator];
97
+ return result;
98
+ }
59
99
  default:
60
100
  return val;
61
101
  }
@@ -67,6 +107,8 @@ function convertFieldToTableItem(fieldDef, val) {
67
107
  * - `"date"` fields are converted from ISO 8601 strings to `Date` objects.
68
108
  * - `"object"` fields recurse into their nested schema.
69
109
  * - `"array"` fields map each item through the same conversion.
110
+ * - `"discriminatedUnion"` fields look up the variant schema by discriminator value
111
+ * and recurse into that variant's object schema, preserving the discriminator key.
70
112
  * - `null` and `undefined` values are stripped from the result so that absent
71
113
  * fields are represented as `undefined` (omitted) on the entity, consistent
72
114
  * with root-level nullable attribute behaviour.
@@ -87,6 +129,13 @@ function tableItemToObject(schema, value) {
87
129
  }
88
130
  return result;
89
131
  }
132
+ /**
133
+ * Converts a single DynamoDB field value back to its entity representation.
134
+ *
135
+ * Mirrors {@link convertFieldToTableItem} in reverse: dates become `Date` objects,
136
+ * objects and discriminated unions recurse through their schemas, arrays map each item,
137
+ * and all other types pass through unchanged.
138
+ */
90
139
  function convertFieldToEntityValue(fieldDef, val) {
91
140
  switch (fieldDef.type) {
92
141
  case "date":
@@ -95,6 +144,15 @@ function convertFieldToEntityValue(fieldDef, val) {
95
144
  return tableItemToObject(fieldDef.fields, val);
96
145
  case "array":
97
146
  return val.map(item => convertFieldToEntityValue(fieldDef.items, item));
147
+ case "discriminatedUnion": {
148
+ const value = val;
149
+ const variantSchema = resolveVariantSchema(fieldDef, value);
150
+ if (variantSchema === undefined)
151
+ return val;
152
+ const result = tableItemToObject(variantSchema, value);
153
+ result[fieldDef.discriminator] = value[fieldDef.discriminator];
154
+ return result;
155
+ }
98
156
  default:
99
157
  return val;
100
158
  }
@@ -91,8 +91,13 @@ export interface ObjectFieldDef {
91
91
  export interface ArrayFieldDef {
92
92
  /** Must be `"array"` to indicate a list/array field. */
93
93
  type: "array";
94
- /** A {@link FieldDef} describing the type of each array element. */
95
- items: FieldDef;
94
+ /**
95
+ * A {@link NonUnionFieldDef} describing the type of each array element.
96
+ * Discriminated unions are not supported as array items because arrays use
97
+ * full replacement on update and the variant-aware serialization semantics
98
+ * are not defined for array item context.
99
+ */
100
+ items: NonUnionFieldDef;
96
101
  /** When `true`, the field becomes optional. */
97
102
  nullable?: boolean;
98
103
  }
@@ -171,6 +176,71 @@ export interface DateFieldDef {
171
176
  /** When `true`, the field becomes optional (`Date | undefined`). */
172
177
  nullable?: boolean;
173
178
  }
179
+ /**
180
+ * A schema field definition for a discriminated union type.
181
+ *
182
+ * The `discriminator` names the key used to distinguish variants, and `variants`
183
+ * maps each discriminator value to an {@link ObjectSchema} describing that variant's
184
+ * fields. The discriminator key is automatically added to each variant's inferred type
185
+ * as a string literal.
186
+ *
187
+ * **Update semantics:** Discriminated union fields always use **full replacement** on
188
+ * update (`SET #field = :value`), never document path merging. This is because:
189
+ * - Different variants have different field sets — a partial document path update could
190
+ * leave orphaned fields from a previous variant leaking through when variants change.
191
+ * - Avoiding orphaned fields without full replacement would require a read-before-write
192
+ * to determine the current discriminator value.
193
+ * - This matches array update semantics (also full replacement).
194
+ *
195
+ * Unlike {@link ObjectFieldDef}, discriminated union fields **can be nullable** because
196
+ * they always use full replacement on update rather than document path expressions,
197
+ * so there is no risk of DynamoDB failing on a missing parent path.
198
+ *
199
+ * **Scoping constraints (initial release):**
200
+ * - Supported at the ObjectAttribute root level and as fields within an ObjectSchema
201
+ * - Not supported inside array items or nested inside other discriminated unions
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const schema = {
206
+ * shape: {
207
+ * type: "discriminatedUnion",
208
+ * discriminator: "kind",
209
+ * variants: {
210
+ * circle: { radius: { type: "number" } },
211
+ * square: { side: { type: "number" } }
212
+ * }
213
+ * }
214
+ * } as const satisfies ObjectSchema;
215
+ *
216
+ * type T = InferObjectSchema<typeof schema>;
217
+ * // { shape: { kind: "circle"; radius: number } | { kind: "square"; side: number } }
218
+ * ```
219
+ */
220
+ export interface DiscriminatedUnionFieldDef {
221
+ /** Must be `"discriminatedUnion"` to indicate a discriminated union field. */
222
+ type: "discriminatedUnion";
223
+ /** The key name used to discriminate between variants. */
224
+ discriminator: string;
225
+ /**
226
+ * A record mapping each discriminator value to a {@link NonUnionObjectSchema}
227
+ * describing that variant's fields (excluding the discriminator itself).
228
+ * Discriminated unions cannot be nested inside other discriminated unions.
229
+ */
230
+ variants: Readonly<Record<string, NonUnionObjectSchema>>;
231
+ /** When `true`, the field becomes optional (`T | undefined`). */
232
+ nullable?: boolean;
233
+ }
234
+ /**
235
+ * A field definition that excludes {@link DiscriminatedUnionFieldDef}.
236
+ *
237
+ * Used in contexts where discriminated unions are not allowed:
238
+ * - {@link ArrayFieldDef} `items` — arrays use full replacement and variant-aware
239
+ * serialization is not defined for array item context
240
+ * - {@link DiscriminatedUnionFieldDef} `variants` — discriminated unions cannot
241
+ * be nested inside other discriminated unions
242
+ */
243
+ export type NonUnionFieldDef = PrimitiveFieldDef | DateFieldDef | ObjectFieldDef | ArrayFieldDef | EnumFieldDef;
174
244
  /**
175
245
  * A field definition within an {@link ObjectSchema}.
176
246
  *
@@ -180,10 +250,17 @@ export interface DateFieldDef {
180
250
  * - {@link ObjectFieldDef} — nested objects via `fields`
181
251
  * - {@link ArrayFieldDef} — arrays/lists via `items`
182
252
  * - {@link EnumFieldDef} — string literal enums via `values`
253
+ * - {@link DiscriminatedUnionFieldDef} — discriminated unions via `discriminator` + `variants`
183
254
  *
184
255
  * Each variant is discriminated by the `type` property.
185
256
  */
186
- export type FieldDef = PrimitiveFieldDef | DateFieldDef | ObjectFieldDef | ArrayFieldDef | EnumFieldDef;
257
+ export type FieldDef = NonUnionFieldDef | DiscriminatedUnionFieldDef;
258
+ /**
259
+ * ObjectSchema that excludes discriminated union fields.
260
+ * Used for {@link DiscriminatedUnionFieldDef} variant schemas where
261
+ * nested discriminated unions are not allowed.
262
+ */
263
+ export type NonUnionObjectSchema = Record<string, NonUnionFieldDef>;
187
264
  /**
188
265
  * Declarative schema for describing the shape of an object attribute.
189
266
  *
@@ -205,18 +282,36 @@ export type FieldDef = PrimitiveFieldDef | DateFieldDef | ObjectFieldDef | Array
205
282
  * ```
206
283
  */
207
284
  export type ObjectSchema = Record<string, FieldDef>;
285
+ /**
286
+ * Infers the TypeScript type of a {@link DiscriminatedUnionFieldDef}.
287
+ *
288
+ * Iterates over the variant keys and for each produces a union member that is
289
+ * `{ [discriminator]: VariantKey } & InferObjectSchema<VariantSchema>`.
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * // Given: discriminator: "kind", variants: { circle: { radius: { type: "number" } }, square: { side: { type: "number" } } }
294
+ * // Produces: { kind: "circle"; radius: number } | { kind: "square"; side: number }
295
+ * ```
296
+ */
297
+ export type InferDiscriminatedUnion<F extends DiscriminatedUnionFieldDef> = {
298
+ [V in keyof F["variants"] & string]: {
299
+ [D in F["discriminator"]]: V;
300
+ } & InferObjectSchema<F["variants"][V]>;
301
+ }[keyof F["variants"] & string];
208
302
  /**
209
303
  * Infers the TypeScript type of a single {@link FieldDef}.
210
304
  *
211
305
  * Used internally by {@link InferObjectSchema} and for recursive array item inference.
212
306
  *
213
307
  * Resolution order:
214
- * 1. {@link ArrayFieldDef} → `Array<InferFieldDef<items>>`
215
- * 2. {@link ObjectFieldDef} → `InferObjectSchema<fields>`
216
- * 3. {@link EnumFieldDef} → `values[number]` (string literal union)
217
- * 4. {@link PrimitiveFieldDef} → `PrimitiveTypeMap[type]`
308
+ * 1. {@link DiscriminatedUnionFieldDef} → `InferDiscriminatedUnion<F>`
309
+ * 2. {@link ArrayFieldDef} → `Array<InferFieldDef<items>>`
310
+ * 3. {@link ObjectFieldDef} → `InferObjectSchema<fields>`
311
+ * 4. {@link EnumFieldDef} → `values[number]` (string literal union)
312
+ * 5. {@link PrimitiveFieldDef} → `PrimitiveTypeMap[type]`
218
313
  */
219
- export type InferFieldDef<F extends FieldDef> = F extends ArrayFieldDef ? Array<InferFieldDef<F["items"]>> : F extends ObjectFieldDef ? InferObjectSchema<F["fields"]> : F extends EnumFieldDef ? F["values"][number] : F extends PrimitiveFieldDef ? PrimitiveTypeMap[F["type"]] : never;
314
+ export type InferFieldDef<F extends FieldDef> = F extends DiscriminatedUnionFieldDef ? InferDiscriminatedUnion<F> : F extends ArrayFieldDef ? Array<InferFieldDef<F["items"]>> : F extends ObjectFieldDef ? InferObjectSchema<F["fields"]> : F extends EnumFieldDef ? F["values"][number] : F extends PrimitiveFieldDef ? PrimitiveTypeMap[F["type"]] : never;
220
315
  /**
221
316
  * Infers the TypeScript type from an {@link ObjectSchema} definition.
222
317
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,gBAAgB,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,mEAAmE;IACnE,IAAI,EAAE,kBAAkB,CAAC;IACzB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,IAAI,EAAE,QAAQ,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,YAAY,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,IAAI,EAAE,OAAO,CAAC;IACd,oEAAoE;IACpE,KAAK,EAAE,QAAQ,CAAC;IAChB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IACvC,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAChB,iBAAiB,GACjB,YAAY,GACZ,cAAc,GACd,aAAa,GACb,YAAY,CAAC;AAEjB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,QAAQ,IAAI,CAAC,SAAS,aAAa,GACnE,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAChC,CAAC,SAAS,cAAc,GACtB,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAC9B,CAAC,SAAS,YAAY,GACpB,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GACnB,CAAC,SAAS,iBAAiB,GACzB,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAC3B,KAAK,CAAC;AAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,YAAY,IAAI;KACrD,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,KAAK,GAAG,CAAC,GAAG,aAAa,CAC1E,CAAC,CAAC,CAAC,CAAC,CACL;CACF,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,aAAa,CAC3E,CAAC,CAAC,CAAC,CAAC,CACL;CACF,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/decorators/attributes/types.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,8 @@ import type { DocumentPathOperation } from "./types";
8
8
  * - `undefined` → skip (not being updated)
9
9
  * - `null` → REMOVE operation
10
10
  * - Nested object (`fieldDef.type === "object"`) → recurse, prepending parent path
11
- * - Everything else (primitives, arrays, dates, enums) → serialize and SET
11
+ * - Everything else (primitives, arrays, dates, enums, discriminated unions) → serialize and SET
12
+ *
12
13
  *
13
14
  * @param parentPath Path segments leading to this object (e.g. ["address"] or ["address", "geo"])
14
15
  * @param schema The ObjectSchema describing the object shape
@@ -1 +1 @@
1
- {"version":3,"file":"flattenObjectForUpdate.d.ts","sourceRoot":"","sources":["../../../../src/operations/utils/flattenObjectForUpdate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,qBAAqB,EAAE,CAmCzB"}
1
+ {"version":3,"file":"flattenObjectForUpdate.d.ts","sourceRoot":"","sources":["../../../../src/operations/utils/flattenObjectForUpdate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,qBAAqB,EAAE,CAkCzB"}
@@ -10,7 +10,8 @@ const serializers_1 = require("../../decorators/attributes/serializers");
10
10
  * - `undefined` → skip (not being updated)
11
11
  * - `null` → REMOVE operation
12
12
  * - Nested object (`fieldDef.type === "object"`) → recurse, prepending parent path
13
- * - Everything else (primitives, arrays, dates, enums) → serialize and SET
13
+ * - Everything else (primitives, arrays, dates, enums, discriminated unions) → serialize and SET
14
+ *
14
15
  *
15
16
  * @param parentPath Path segments leading to this object (e.g. ["address"] or ["address", "geo"])
16
17
  * @param schema The ObjectSchema describing the object shape
@@ -37,7 +38,6 @@ function flattenObjectForUpdate(parentPath, schema, partialValue) {
37
38
  ops.push(...nestedOps);
38
39
  continue;
39
40
  }
40
- // Primitives, arrays, dates, enums → serialize and SET
41
41
  const serialized = (0, serializers_1.convertFieldToTableItem)(fieldDef, val);
42
42
  ops.push({ type: "set", path: fieldPath, value: serialized });
43
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dyna-record",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Typescript Data Modeler and ORM for Dynamo",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",