@ftschopp/dynatable-core 1.1.0 → 1.2.0
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/CHANGELOG.md +7 -0
- package/README.md +105 -19
- package/dist/core/types.d.ts +95 -11
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +3 -2
- package/dist/utils/zod-utils.d.ts +2 -1
- package/dist/utils/zod-utils.d.ts.map +1 -1
- package/dist/utils/zod-utils.js +33 -11
- package/package.json +1 -1
- package/src/core/types.test.ts +137 -0
- package/src/core/types.ts +96 -20
- package/src/index.ts +1 -0
- package/src/utils/model-utils.ts +3 -2
- package/src/utils/zod-utils.test.ts +97 -2
- package/src/utils/zod-utils.ts +31 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# @ftschopp/dynatable-core [1.2.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.1.0...@ftschopp/dynatable-core@1.2.0) (2026-03-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* support nested objects and arrays schema ([c06e43e](https://github.com/ftschopp/dynatable/commit/c06e43e6b425bae09f127d2f767b5c8c62dd1142))
|
|
7
|
+
|
|
1
8
|
# @ftschopp/dynatable-core [1.1.0](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.0.0...@ftschopp/dynatable-core@1.1.0) (2026-01-20)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -130,6 +130,21 @@ const schema = {
|
|
|
130
130
|
email: { type: String },
|
|
131
131
|
userId: { type: String, generate: 'ulid' },
|
|
132
132
|
followerCount: { type: Number, default: 0 },
|
|
133
|
+
// Nested object with typed schema
|
|
134
|
+
address: {
|
|
135
|
+
type: Object,
|
|
136
|
+
schema: {
|
|
137
|
+
street: { type: String },
|
|
138
|
+
city: { type: String, required: true },
|
|
139
|
+
country: { type: String, required: true },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
// Typed array with item schema
|
|
143
|
+
tags: {
|
|
144
|
+
type: Array,
|
|
145
|
+
default: [],
|
|
146
|
+
items: { type: String },
|
|
147
|
+
},
|
|
133
148
|
},
|
|
134
149
|
},
|
|
135
150
|
},
|
|
@@ -142,24 +157,85 @@ const schema = {
|
|
|
142
157
|
} as const;
|
|
143
158
|
```
|
|
144
159
|
|
|
160
|
+
### Nested Objects and Arrays
|
|
161
|
+
|
|
162
|
+
Attributes support typed nested schemas for full TypeScript inference:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
Story: {
|
|
166
|
+
attributes: {
|
|
167
|
+
// Typed array of objects
|
|
168
|
+
frames: {
|
|
169
|
+
type: Array,
|
|
170
|
+
default: [],
|
|
171
|
+
items: {
|
|
172
|
+
type: Object,
|
|
173
|
+
schema: {
|
|
174
|
+
url: { type: String, required: true },
|
|
175
|
+
duration: { type: Number },
|
|
176
|
+
mediaType: { type: String },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
// Nested object with a typed schema
|
|
181
|
+
location: {
|
|
182
|
+
type: Object,
|
|
183
|
+
schema: {
|
|
184
|
+
city: { type: String },
|
|
185
|
+
country: { type: String },
|
|
186
|
+
lat: { type: Number },
|
|
187
|
+
lng: { type: Number },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use `ArrayItem<T>` to extract item types from array attributes:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import type { InferModelFromSchema, ArrayItem } from '@ftschopp/dynatable-core';
|
|
198
|
+
|
|
199
|
+
type StoryEntity = InferModelFromSchema<typeof schema, 'Story'>;
|
|
200
|
+
type StoryFrame = ArrayItem<StoryEntity['frames']>;
|
|
201
|
+
// → { url: string; duration?: number; mediaType?: string }
|
|
202
|
+
```
|
|
203
|
+
|
|
145
204
|
### Type Inference
|
|
146
205
|
|
|
147
206
|
Extract types from your schema:
|
|
148
207
|
|
|
149
208
|
```typescript
|
|
150
|
-
import type {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
209
|
+
import type {
|
|
210
|
+
InferModel,
|
|
211
|
+
InferInput,
|
|
212
|
+
InferKeyInput,
|
|
213
|
+
InferModelFromSchema,
|
|
214
|
+
InferInputFromSchema,
|
|
215
|
+
ArrayItem,
|
|
216
|
+
} from '@ftschopp/dynatable-core';
|
|
217
|
+
|
|
218
|
+
// Preferred: infer from the full schema (timestamps included automatically)
|
|
219
|
+
type User = InferModelFromSchema<typeof schema, 'User'>;
|
|
154
220
|
// { username: string; name: string; email?: string; userId: string; followerCount: number; createdAt: string; updatedAt: string }
|
|
155
221
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
222
|
+
type UserInput = InferInputFromSchema<typeof schema, 'User'>;
|
|
223
|
+
// { username: string; name: string; email?: string; followerCount?: number }
|
|
224
|
+
|
|
225
|
+
// Full model type (deprecated — use InferModelFromSchema)
|
|
226
|
+
type UserLegacy = InferModel<typeof schema.models.User>;
|
|
227
|
+
|
|
228
|
+
// Input type (deprecated — use InferInputFromSchema)
|
|
229
|
+
type UserInputLegacy = InferInput<typeof schema.models.User>;
|
|
159
230
|
|
|
160
231
|
// Key input type (only key template variables)
|
|
161
232
|
type UserKey = InferKeyInput<typeof schema.models.User>;
|
|
162
233
|
// { username: string }
|
|
234
|
+
|
|
235
|
+
// Extract item type from an array attribute
|
|
236
|
+
type StoryEntity = InferModelFromSchema<typeof schema, 'Story'>;
|
|
237
|
+
type StoryFrame = ArrayItem<StoryEntity['frames']>;
|
|
238
|
+
// → { url: string; duration?: number; mediaType?: string }
|
|
163
239
|
```
|
|
164
240
|
|
|
165
241
|
### Builder Operations
|
|
@@ -393,18 +469,28 @@ if (page1.lastEvaluatedKey) {
|
|
|
393
469
|
|
|
394
470
|
```typescript
|
|
395
471
|
export {
|
|
396
|
-
Table,
|
|
397
|
-
type SchemaDefinition,
|
|
398
|
-
type ModelDefinition,
|
|
399
|
-
type
|
|
400
|
-
type
|
|
401
|
-
type
|
|
402
|
-
type
|
|
403
|
-
type
|
|
404
|
-
type
|
|
405
|
-
|
|
406
|
-
type
|
|
407
|
-
type
|
|
472
|
+
Table, // Main Table class
|
|
473
|
+
type SchemaDefinition, // Schema type
|
|
474
|
+
type ModelDefinition, // Model type
|
|
475
|
+
type AttributeDefinition, // Union of all attribute types
|
|
476
|
+
type ScalarAttributeDefinition, // String/Number/Boolean/Date attribute
|
|
477
|
+
type ObjectAttributeDefinition, // Nested object attribute with schema
|
|
478
|
+
type ArrayAttributeDefinition, // Typed array attribute with items
|
|
479
|
+
type PrimaryKeyDefinition, // PK + SK key definition
|
|
480
|
+
type KeyDefinition, // Single key definition
|
|
481
|
+
type IndexDefinition, // Index (hash + optional sort)
|
|
482
|
+
type IndexesDefinition, // All table indexes
|
|
483
|
+
type SchemaParams, // Global schema params
|
|
484
|
+
type InferModel, // Infer model type (deprecated)
|
|
485
|
+
type InferInput, // Infer input type (deprecated)
|
|
486
|
+
type InferKeyInput, // Infer key type
|
|
487
|
+
type InferModelFromSchema, // Infer from full schema (preferred)
|
|
488
|
+
type InferInputFromSchema, // Infer input from full schema (preferred)
|
|
489
|
+
type TimestampFields, // createdAt/updatedAt fields type
|
|
490
|
+
type ArrayItem, // Extract item type from array attribute
|
|
491
|
+
createDynamoDBLogger, // Logger factory
|
|
492
|
+
type DynamoDBLogger, // Logger type
|
|
493
|
+
type DynamoDBLoggerConfig, // Logger config
|
|
408
494
|
};
|
|
409
495
|
```
|
|
410
496
|
|
package/dist/core/types.d.ts
CHANGED
|
@@ -23,19 +23,61 @@
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @property type - The JavaScript constructor for the attribute type
|
|
29
|
-
* @property [required] - Whether the attribute is required
|
|
30
|
-
* @property [generate] - Auto-generation strategy ('ulid', 'uuid')
|
|
31
|
-
* @property [default] - Default value or generator function
|
|
26
|
+
* Scalar attribute definition (String, Number, Boolean, Date)
|
|
32
27
|
*/
|
|
33
|
-
export type
|
|
28
|
+
export type ScalarAttributeDefinition = {
|
|
34
29
|
type: StringConstructor | NumberConstructor | BooleanConstructor | DateConstructor;
|
|
35
30
|
required?: boolean;
|
|
36
31
|
generate?: 'ulid' | 'uuid';
|
|
37
32
|
default?: any;
|
|
33
|
+
nulls?: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Object attribute definition with a nested schema
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* address: {
|
|
40
|
+
* type: Object,
|
|
41
|
+
* schema: {
|
|
42
|
+
* street: { type: String },
|
|
43
|
+
* city: { type: String, required: true },
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
export type ObjectAttributeDefinition = {
|
|
48
|
+
type: ObjectConstructor;
|
|
49
|
+
schema: Record<string, AttributeDefinition>;
|
|
50
|
+
required?: boolean;
|
|
51
|
+
default?: any;
|
|
52
|
+
nulls?: boolean;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Array attribute definition with a typed items schema
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* history: {
|
|
59
|
+
* type: Array,
|
|
60
|
+
* default: [],
|
|
61
|
+
* items: {
|
|
62
|
+
* type: Object,
|
|
63
|
+
* schema: {
|
|
64
|
+
* date: { type: String },
|
|
65
|
+
* status: { type: String },
|
|
66
|
+
* }
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
export type ArrayAttributeDefinition = {
|
|
71
|
+
type: ArrayConstructor;
|
|
72
|
+
items: AttributeDefinition;
|
|
73
|
+
required?: boolean;
|
|
74
|
+
default?: any;
|
|
75
|
+
nulls?: boolean;
|
|
38
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* Attribute definition for model attributes — supports scalars, nested objects, and arrays
|
|
79
|
+
*/
|
|
80
|
+
export type AttributeDefinition = ScalarAttributeDefinition | ObjectAttributeDefinition | ArrayAttributeDefinition;
|
|
39
81
|
/**
|
|
40
82
|
* Key definition for primary and secondary indexes
|
|
41
83
|
*
|
|
@@ -101,7 +143,39 @@ export type SchemaDefinition = {
|
|
|
101
143
|
models: Record<string, ModelDefinition>;
|
|
102
144
|
params?: SchemaParams;
|
|
103
145
|
};
|
|
104
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Recursively infers the TypeScript type from an AttributeDefinition.
|
|
148
|
+
* - Object → infers the nested schema shape
|
|
149
|
+
* - Array → infers an array of the items type
|
|
150
|
+
* - Scalars → maps to the corresponding primitive
|
|
151
|
+
*/
|
|
152
|
+
type InferAttr<A> = A extends {
|
|
153
|
+
type: ObjectConstructor;
|
|
154
|
+
schema: infer S;
|
|
155
|
+
} ? InferObjectSchema<S> : A extends {
|
|
156
|
+
type: ArrayConstructor;
|
|
157
|
+
items: infer I;
|
|
158
|
+
} ? Array<InferAttr<I>> : A extends {
|
|
159
|
+
type: StringConstructor;
|
|
160
|
+
} ? string : A extends {
|
|
161
|
+
type: NumberConstructor;
|
|
162
|
+
} ? number : A extends {
|
|
163
|
+
type: BooleanConstructor;
|
|
164
|
+
} ? boolean : A extends {
|
|
165
|
+
type: DateConstructor;
|
|
166
|
+
} ? Date : unknown;
|
|
167
|
+
/**
|
|
168
|
+
* Infers the shape of a nested object schema, respecting required/optional fields.
|
|
169
|
+
*/
|
|
170
|
+
type InferObjectSchema<S> = {
|
|
171
|
+
[K in keyof S as S[K] extends {
|
|
172
|
+
required: true;
|
|
173
|
+
} ? K : never]: InferAttr<S[K]>;
|
|
174
|
+
} & {
|
|
175
|
+
[K in keyof S as S[K] extends {
|
|
176
|
+
required: true;
|
|
177
|
+
} ? never : K]?: InferAttr<S[K]>;
|
|
178
|
+
};
|
|
105
179
|
/**
|
|
106
180
|
* Non-generated attributes for input
|
|
107
181
|
* Splits into required and optional based on the 'required' field
|
|
@@ -111,7 +185,7 @@ type NonGeneratedAttributes<M extends ModelDefinition> = {
|
|
|
111
185
|
generate: string;
|
|
112
186
|
} ? never : M['attributes'][K] extends {
|
|
113
187
|
required: true;
|
|
114
|
-
} ? K : never]: InferAttr<M['attributes'][K]
|
|
188
|
+
} ? K : never]: InferAttr<M['attributes'][K]>;
|
|
115
189
|
} & {
|
|
116
190
|
[K in keyof M['attributes'] as M['attributes'][K] extends {
|
|
117
191
|
generate: string;
|
|
@@ -119,7 +193,7 @@ type NonGeneratedAttributes<M extends ModelDefinition> = {
|
|
|
119
193
|
required: false;
|
|
120
194
|
} ? K : M['attributes'][K] extends {
|
|
121
195
|
required: true;
|
|
122
|
-
} ? never : K]?: InferAttr<M['attributes'][K]
|
|
196
|
+
} ? never : K]?: InferAttr<M['attributes'][K]>;
|
|
123
197
|
};
|
|
124
198
|
/**
|
|
125
199
|
* Generated-only attributes for internal use
|
|
@@ -127,7 +201,7 @@ type NonGeneratedAttributes<M extends ModelDefinition> = {
|
|
|
127
201
|
type GeneratedAttributes<M extends ModelDefinition> = {
|
|
128
202
|
[K in keyof M['attributes'] as M['attributes'][K] extends {
|
|
129
203
|
generate: string;
|
|
130
|
-
} ? K : never]: InferAttr<M['attributes'][K]
|
|
204
|
+
} ? K : never]: InferAttr<M['attributes'][K]>;
|
|
131
205
|
};
|
|
132
206
|
/**
|
|
133
207
|
* Template string variable extraction
|
|
@@ -221,5 +295,15 @@ type KeyTemplateVars<M extends ModelDefinition> = PrimaryKeyVars<M>;
|
|
|
221
295
|
export type InferKeyInput<M extends ModelDefinition> = {
|
|
222
296
|
[K in KeyTemplateVars<M>]: string;
|
|
223
297
|
};
|
|
298
|
+
/**
|
|
299
|
+
* Extracts the item type from an array attribute.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* type StoryFrame = ArrayItem<StoryEntity['frames']>;
|
|
303
|
+
* // → { url: string; duration?: number; mediaType?: string }
|
|
304
|
+
*
|
|
305
|
+
* type HistoryEntry = ArrayItem<TransactionEntity['history']>;
|
|
306
|
+
*/
|
|
307
|
+
export type ArrayItem<T extends readonly unknown[] | undefined> = NonNullable<T>[number];
|
|
224
308
|
export {};
|
|
225
309
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './table';
|
|
2
|
-
export { type SchemaDefinition, type ModelDefinition, type PrimaryKeyDefinition, type KeyDefinition, type AttributeDefinition, type IndexDefinition, type IndexesDefinition, type SchemaParams, type InferInput, type InferModel, type InferKeyInput, type InferModelFromSchema, type InferInputFromSchema, type TimestampFields, } from './core/types';
|
|
2
|
+
export { type SchemaDefinition, type ModelDefinition, type PrimaryKeyDefinition, type KeyDefinition, type AttributeDefinition, type IndexDefinition, type IndexesDefinition, type SchemaParams, type InferInput, type InferModel, type InferKeyInput, type InferModelFromSchema, type InferInputFromSchema, type TimestampFields, type ArrayItem, } from './core/types';
|
|
3
3
|
export { createDynamoDBLogger, type DynamoDBLogger, type DynamoDBLoggerConfig, } from './utils/dynamodb-logger';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,yBAAyB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-utils.d.ts","sourceRoot":"","sources":["../../src/utils/model-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAG1E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAAM,EAG5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAoB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,eAAe,EACnD,OAAO,CAAC,EACR,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,OAAM,KAAK,GAAG,OAAO,GAAG,MAAc,KACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAWvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,eAAe,EACzD,OAAO,CAAC,EACR,eAAe,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,UAAU;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KACrD,UAAU,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"model-utils.d.ts","sourceRoot":"","sources":["../../src/utils/model-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAG1E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAAM,EAG5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAoB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,eAAe,EACnD,OAAO,CAAC,EACR,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,OAAM,KAAK,GAAG,OAAO,GAAG,MAAc,KACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAWvB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,eAAe,EACzD,OAAO,CAAC,EACR,eAAe,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,UAAU;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KACrD,UAAU,CAAC,CAAC,CAiCd,CAAC;AAOF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,KAAG,CAAC,GAAG,CAAC,EAAE,GAAG,SAoB1E,CAAC"}
|
|
@@ -60,10 +60,11 @@ const applyPostDefaults = (model, validatedItem, options) => {
|
|
|
60
60
|
const result = { ...validatedItem };
|
|
61
61
|
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
62
62
|
if (result[key] === undefined) {
|
|
63
|
-
|
|
63
|
+
const generate = 'generate' in attr ? attr.generate : undefined;
|
|
64
|
+
if (generate === 'ulid') {
|
|
64
65
|
result[key] = (0, ulid_1.ulid)();
|
|
65
66
|
}
|
|
66
|
-
else if (
|
|
67
|
+
else if (generate === 'uuid') {
|
|
67
68
|
result[key] = crypto.randomUUID();
|
|
68
69
|
}
|
|
69
70
|
else if (attr.default !== undefined) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AttributeDefinition, ModelDefinition } from '../core/types';
|
|
2
2
|
import { ZodObject, ZodType } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
* Converts an attribute definition to a Zod schema type
|
|
4
|
+
* Converts an attribute definition to a Zod schema type.
|
|
5
|
+
* Supports scalars, nested objects (with schema), and arrays (with items).
|
|
5
6
|
*/
|
|
6
7
|
export declare const typeToZod: (attr: AttributeDefinition) => ZodType;
|
|
7
8
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod-utils.d.ts","sourceRoot":"","sources":["../../src/utils/zod-utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAK,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAE5C
|
|
1
|
+
{"version":3,"file":"zod-utils.d.ts","sourceRoot":"","sources":["../../src/utils/zod-utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAK,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,mBAAmB,KAAG,OA+BrD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,eAAe,KAAG,SAAS,CAAC,GAAG,CAMhE,CAAC"}
|
package/dist/utils/zod-utils.js
CHANGED
|
@@ -5,18 +5,40 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
5
5
|
exports.modelToZod = exports.typeToZod = void 0;
|
|
6
6
|
const zod_1 = require("zod");
|
|
7
7
|
/**
|
|
8
|
-
* Converts an attribute definition to a Zod schema type
|
|
8
|
+
* Converts an attribute definition to a Zod schema type.
|
|
9
|
+
* Supports scalars, nested objects (with schema), and arrays (with items).
|
|
9
10
|
*/
|
|
10
11
|
const typeToZod = (attr) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
let zodType;
|
|
13
|
+
if (attr.type === Object) {
|
|
14
|
+
const schema = attr.schema;
|
|
15
|
+
if (schema) {
|
|
16
|
+
const shape = {};
|
|
17
|
+
for (const [key, nestedAttr] of Object.entries(schema)) {
|
|
18
|
+
shape[key] = (0, exports.typeToZod)(nestedAttr);
|
|
19
|
+
}
|
|
20
|
+
zodType = zod_1.z.looseObject(shape);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
zodType = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (attr.type === Array) {
|
|
27
|
+
const items = attr.items;
|
|
28
|
+
zodType = items ? zod_1.z.array((0, exports.typeToZod)(items)) : zod_1.z.array(zod_1.z.unknown());
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
zodType =
|
|
32
|
+
attr.type === String
|
|
33
|
+
? zod_1.z.string()
|
|
34
|
+
: attr.type === Number
|
|
35
|
+
? zod_1.z.number()
|
|
36
|
+
: attr.type === Boolean
|
|
37
|
+
? zod_1.z.boolean()
|
|
38
|
+
: attr.type === Date
|
|
39
|
+
? zod_1.z.date()
|
|
40
|
+
: zod_1.z.unknown();
|
|
41
|
+
}
|
|
20
42
|
return attr.required ? zodType : zodType.optional();
|
|
21
43
|
};
|
|
22
44
|
exports.typeToZod = typeToZod;
|
|
@@ -30,6 +52,6 @@ const modelToZod = (model) => {
|
|
|
30
52
|
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
31
53
|
shape[key] = (0, exports.typeToZod)(attr);
|
|
32
54
|
}
|
|
33
|
-
return zod_1.z.
|
|
55
|
+
return zod_1.z.looseObject(shape);
|
|
34
56
|
};
|
|
35
57
|
exports.modelToZod = modelToZod;
|
package/package.json
CHANGED
package/src/core/types.test.ts
CHANGED
|
@@ -504,3 +504,140 @@ describe('Timestamp Type Inference', () => {
|
|
|
504
504
|
expect(timestamps.updatedAt).toBe('2024-01-01T00:00:00.000Z');
|
|
505
505
|
});
|
|
506
506
|
});
|
|
507
|
+
|
|
508
|
+
// ============================================================================
|
|
509
|
+
// Nested Object & Array Type Inference Tests
|
|
510
|
+
// ============================================================================
|
|
511
|
+
|
|
512
|
+
describe('Nested Object and Array Type Inference', () => {
|
|
513
|
+
const TransactionSchema = {
|
|
514
|
+
format: 'dynatable:1.0.0',
|
|
515
|
+
version: '1.0.0',
|
|
516
|
+
indexes: { primary: { hash: 'PK', sort: 'SK' } },
|
|
517
|
+
models: {
|
|
518
|
+
Transaction: {
|
|
519
|
+
key: {
|
|
520
|
+
PK: { type: String, value: 'USER#${userId}' },
|
|
521
|
+
SK: { type: String, value: 'TX#${txId}' },
|
|
522
|
+
},
|
|
523
|
+
attributes: {
|
|
524
|
+
userId: { type: String, required: true },
|
|
525
|
+
txId: { type: String, required: true },
|
|
526
|
+
status: { type: String, required: true },
|
|
527
|
+
// Nested object with schema
|
|
528
|
+
address: {
|
|
529
|
+
type: Object,
|
|
530
|
+
schema: {
|
|
531
|
+
street: { type: String },
|
|
532
|
+
city: { type: String, required: true },
|
|
533
|
+
country: { type: String, required: true },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
// Array of scalar items
|
|
537
|
+
tags: {
|
|
538
|
+
type: Array,
|
|
539
|
+
items: { type: String },
|
|
540
|
+
default: [],
|
|
541
|
+
},
|
|
542
|
+
// Array of objects with schema (like history in the real example)
|
|
543
|
+
history: {
|
|
544
|
+
type: Array,
|
|
545
|
+
default: [],
|
|
546
|
+
items: {
|
|
547
|
+
type: Object,
|
|
548
|
+
schema: {
|
|
549
|
+
date: { type: String },
|
|
550
|
+
description: { type: String },
|
|
551
|
+
status: { type: String, required: true },
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
// Deeply nested object
|
|
556
|
+
amount: {
|
|
557
|
+
type: Object,
|
|
558
|
+
schema: {
|
|
559
|
+
value: { type: Number, required: true },
|
|
560
|
+
currency: { type: String, required: true },
|
|
561
|
+
breakdown: {
|
|
562
|
+
type: Object,
|
|
563
|
+
schema: {
|
|
564
|
+
base: { type: Number, required: true },
|
|
565
|
+
fees: { type: Number, required: true },
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
} as const satisfies SchemaDefinition;
|
|
574
|
+
|
|
575
|
+
type Transaction = InferModelFromSchema<typeof TransactionSchema, 'Transaction'>;
|
|
576
|
+
|
|
577
|
+
test('schema with nested Object attribute compiles', () => {
|
|
578
|
+
expect(TransactionSchema.models.Transaction.attributes.address.type).toBe(Object);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('schema with Array of objects compiles', () => {
|
|
582
|
+
expect(TransactionSchema.models.Transaction.attributes.history.type).toBe(Array);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('inferred type accepts valid nested object', () => {
|
|
586
|
+
const tx: Transaction = {
|
|
587
|
+
userId: 'u1',
|
|
588
|
+
txId: 'tx1',
|
|
589
|
+
status: 'PENDING',
|
|
590
|
+
address: { city: 'Buenos Aires', country: 'AR' },
|
|
591
|
+
tags: ['fast', 'verified'],
|
|
592
|
+
history: [
|
|
593
|
+
{ date: '2024-01-01', description: 'Created', status: 'PENDING' },
|
|
594
|
+
{ status: 'COMPLETED' },
|
|
595
|
+
],
|
|
596
|
+
amount: {
|
|
597
|
+
value: 100,
|
|
598
|
+
currency: 'USD',
|
|
599
|
+
breakdown: { base: 90, fees: 10 },
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
expect(tx.userId).toBe('u1');
|
|
604
|
+
expect(tx.address?.city).toBe('Buenos Aires');
|
|
605
|
+
expect(tx.history?.[0]?.status).toBe('PENDING');
|
|
606
|
+
expect(tx.amount?.breakdown?.base).toBe(90);
|
|
607
|
+
expect(tx.tags?.[0]).toBe('fast');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('inferred nested object type requires its required fields', () => {
|
|
611
|
+
type Address = NonNullable<Transaction['address']>;
|
|
612
|
+
|
|
613
|
+
// city and country are required — this must compile
|
|
614
|
+
const validAddress: Address = { city: 'BA', country: 'AR' };
|
|
615
|
+
expect(validAddress.city).toBe('BA');
|
|
616
|
+
|
|
617
|
+
// Verify the shape: optional `street`, required `city` and `country`
|
|
618
|
+
type AddressKeys = keyof Required<Address>;
|
|
619
|
+
const _keys: AddressKeys[] = ['street', 'city', 'country'];
|
|
620
|
+
expect(_keys).toContain('city');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('model with nested attributes satisfies ModelDefinition', () => {
|
|
624
|
+
const model = {
|
|
625
|
+
key: {
|
|
626
|
+
PK: { type: String, value: 'ITEM#${id}' },
|
|
627
|
+
SK: { type: String, value: 'ITEM#${id}' },
|
|
628
|
+
},
|
|
629
|
+
attributes: {
|
|
630
|
+
id: { type: String, required: true },
|
|
631
|
+
meta: {
|
|
632
|
+
type: Object,
|
|
633
|
+
schema: {
|
|
634
|
+
createdBy: { type: String },
|
|
635
|
+
tags: { type: Array, items: { type: String } },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
} as const satisfies ModelDefinition;
|
|
640
|
+
|
|
641
|
+
expect(model.attributes.meta.type).toBe(Object);
|
|
642
|
+
});
|
|
643
|
+
});
|
package/src/core/types.ts
CHANGED
|
@@ -28,20 +28,68 @@
|
|
|
28
28
|
// -------------------- Type Definitions --------------------
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* @property type - The JavaScript constructor for the attribute type
|
|
34
|
-
* @property [required] - Whether the attribute is required
|
|
35
|
-
* @property [generate] - Auto-generation strategy ('ulid', 'uuid')
|
|
36
|
-
* @property [default] - Default value or generator function
|
|
31
|
+
* Scalar attribute definition (String, Number, Boolean, Date)
|
|
37
32
|
*/
|
|
38
|
-
export type
|
|
33
|
+
export type ScalarAttributeDefinition = {
|
|
39
34
|
type: StringConstructor | NumberConstructor | BooleanConstructor | DateConstructor;
|
|
40
35
|
required?: boolean;
|
|
41
36
|
generate?: 'ulid' | 'uuid';
|
|
42
37
|
default?: any;
|
|
38
|
+
nulls?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Object attribute definition with a nested schema
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* address: {
|
|
46
|
+
* type: Object,
|
|
47
|
+
* schema: {
|
|
48
|
+
* street: { type: String },
|
|
49
|
+
* city: { type: String, required: true },
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
*/
|
|
53
|
+
export type ObjectAttributeDefinition = {
|
|
54
|
+
type: ObjectConstructor;
|
|
55
|
+
schema: Record<string, AttributeDefinition>;
|
|
56
|
+
required?: boolean;
|
|
57
|
+
default?: any;
|
|
58
|
+
nulls?: boolean;
|
|
43
59
|
};
|
|
44
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Array attribute definition with a typed items schema
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* history: {
|
|
66
|
+
* type: Array,
|
|
67
|
+
* default: [],
|
|
68
|
+
* items: {
|
|
69
|
+
* type: Object,
|
|
70
|
+
* schema: {
|
|
71
|
+
* date: { type: String },
|
|
72
|
+
* status: { type: String },
|
|
73
|
+
* }
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
*/
|
|
77
|
+
export type ArrayAttributeDefinition = {
|
|
78
|
+
type: ArrayConstructor;
|
|
79
|
+
items: AttributeDefinition;
|
|
80
|
+
required?: boolean;
|
|
81
|
+
default?: any;
|
|
82
|
+
nulls?: boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Attribute definition for model attributes — supports scalars, nested objects, and arrays
|
|
87
|
+
*/
|
|
88
|
+
export type AttributeDefinition =
|
|
89
|
+
| ScalarAttributeDefinition
|
|
90
|
+
| ObjectAttributeDefinition
|
|
91
|
+
| ArrayAttributeDefinition;
|
|
92
|
+
|
|
45
93
|
/**
|
|
46
94
|
* Key definition for primary and secondary indexes
|
|
47
95
|
*
|
|
@@ -114,17 +162,34 @@ export type SchemaDefinition = {
|
|
|
114
162
|
params?: SchemaParams;
|
|
115
163
|
};
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Recursively infers the TypeScript type from an AttributeDefinition.
|
|
167
|
+
* - Object → infers the nested schema shape
|
|
168
|
+
* - Array → infers an array of the items type
|
|
169
|
+
* - Scalars → maps to the corresponding primitive
|
|
170
|
+
*/
|
|
171
|
+
type InferAttr<A> = A extends { type: ObjectConstructor; schema: infer S }
|
|
172
|
+
? InferObjectSchema<S>
|
|
173
|
+
: A extends { type: ArrayConstructor; items: infer I }
|
|
174
|
+
? Array<InferAttr<I>>
|
|
175
|
+
: A extends { type: StringConstructor }
|
|
176
|
+
? string
|
|
177
|
+
: A extends { type: NumberConstructor }
|
|
178
|
+
? number
|
|
179
|
+
: A extends { type: BooleanConstructor }
|
|
180
|
+
? boolean
|
|
181
|
+
: A extends { type: DateConstructor }
|
|
182
|
+
? Date
|
|
183
|
+
: unknown;
|
|
126
184
|
|
|
127
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Infers the shape of a nested object schema, respecting required/optional fields.
|
|
187
|
+
*/
|
|
188
|
+
type InferObjectSchema<S> = {
|
|
189
|
+
[K in keyof S as S[K] extends { required: true } ? K : never]: InferAttr<S[K]>;
|
|
190
|
+
} & {
|
|
191
|
+
[K in keyof S as S[K] extends { required: true } ? never : K]?: InferAttr<S[K]>;
|
|
192
|
+
};
|
|
128
193
|
|
|
129
194
|
/**
|
|
130
195
|
* Non-generated attributes for input
|
|
@@ -135,7 +200,7 @@ type NonGeneratedAttributes<M extends ModelDefinition> = {
|
|
|
135
200
|
? never
|
|
136
201
|
: M['attributes'][K] extends { required: true }
|
|
137
202
|
? K
|
|
138
|
-
: never]: InferAttr<M['attributes'][K]
|
|
203
|
+
: never]: InferAttr<M['attributes'][K]>;
|
|
139
204
|
} & {
|
|
140
205
|
[K in keyof M['attributes'] as M['attributes'][K] extends { generate: string }
|
|
141
206
|
? never
|
|
@@ -143,7 +208,7 @@ type NonGeneratedAttributes<M extends ModelDefinition> = {
|
|
|
143
208
|
? K
|
|
144
209
|
: M['attributes'][K] extends { required: true }
|
|
145
210
|
? never
|
|
146
|
-
: K]?: InferAttr<M['attributes'][K]
|
|
211
|
+
: K]?: InferAttr<M['attributes'][K]>;
|
|
147
212
|
};
|
|
148
213
|
|
|
149
214
|
/**
|
|
@@ -152,7 +217,7 @@ type NonGeneratedAttributes<M extends ModelDefinition> = {
|
|
|
152
217
|
type GeneratedAttributes<M extends ModelDefinition> = {
|
|
153
218
|
[K in keyof M['attributes'] as M['attributes'][K] extends { generate: string }
|
|
154
219
|
? K
|
|
155
|
-
: never]: InferAttr<M['attributes'][K]
|
|
220
|
+
: never]: InferAttr<M['attributes'][K]>;
|
|
156
221
|
};
|
|
157
222
|
|
|
158
223
|
/**
|
|
@@ -289,3 +354,14 @@ type KeyTemplateVars<M extends ModelDefinition> = PrimaryKeyVars<M>;
|
|
|
289
354
|
export type InferKeyInput<M extends ModelDefinition> = {
|
|
290
355
|
[K in KeyTemplateVars<M>]: string;
|
|
291
356
|
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Extracts the item type from an array attribute.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* type StoryFrame = ArrayItem<StoryEntity['frames']>;
|
|
363
|
+
* // → { url: string; duration?: number; mediaType?: string }
|
|
364
|
+
*
|
|
365
|
+
* type HistoryEntry = ArrayItem<TransactionEntity['history']>;
|
|
366
|
+
*/
|
|
367
|
+
export type ArrayItem<T extends readonly unknown[] | undefined> = NonNullable<T>[number];
|
package/src/index.ts
CHANGED
package/src/utils/model-utils.ts
CHANGED
|
@@ -71,9 +71,10 @@ export const applyPostDefaults = <M extends ModelDefinition>(
|
|
|
71
71
|
|
|
72
72
|
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
73
73
|
if (result[key] === undefined) {
|
|
74
|
-
|
|
74
|
+
const generate = 'generate' in attr ? attr.generate : undefined;
|
|
75
|
+
if (generate === 'ulid') {
|
|
75
76
|
result[key] = ulid();
|
|
76
|
-
} else if (
|
|
77
|
+
} else if (generate === 'uuid') {
|
|
77
78
|
result[key] = crypto.randomUUID();
|
|
78
79
|
} else if (attr.default !== undefined) {
|
|
79
80
|
result[key] = typeof attr.default === 'function' ? attr.default() : attr.default;
|
|
@@ -71,9 +71,9 @@ describe('zod-utils', () => {
|
|
|
71
71
|
expect(() => zodType.parse(undefined)).toThrow();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
it('should return z.unknown() for
|
|
74
|
+
it('should return z.unknown() for unrecognized types', () => {
|
|
75
75
|
const attr: AttributeDefinition = {
|
|
76
|
-
type:
|
|
76
|
+
type: Map as any,
|
|
77
77
|
required: true,
|
|
78
78
|
};
|
|
79
79
|
const zodType = typeToZod(attr);
|
|
@@ -83,6 +83,101 @@ describe('zod-utils', () => {
|
|
|
83
83
|
expect(() => zodType.parse(123)).not.toThrow();
|
|
84
84
|
expect(() => zodType.parse([])).not.toThrow();
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
it('should convert Object type with schema to z.looseObject()', () => {
|
|
88
|
+
const attr: AttributeDefinition = {
|
|
89
|
+
type: Object,
|
|
90
|
+
schema: {
|
|
91
|
+
city: { type: String, required: true },
|
|
92
|
+
zip: { type: String },
|
|
93
|
+
},
|
|
94
|
+
required: true,
|
|
95
|
+
};
|
|
96
|
+
const zodType = typeToZod(attr);
|
|
97
|
+
|
|
98
|
+
expect(() => zodType.parse({ city: 'BA' })).not.toThrow();
|
|
99
|
+
expect(() => zodType.parse({ city: 'BA', zip: '1000' })).not.toThrow();
|
|
100
|
+
expect(() => zodType.parse({ zip: '1000' })).toThrow(); // city required
|
|
101
|
+
expect(() => zodType.parse({})).toThrow();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should convert Object type without schema to z.record()', () => {
|
|
105
|
+
const attr: AttributeDefinition = {
|
|
106
|
+
type: Object,
|
|
107
|
+
schema: {},
|
|
108
|
+
};
|
|
109
|
+
const zodType = typeToZod(attr);
|
|
110
|
+
expect(() => zodType.parse({})).not.toThrow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should convert Array type with scalar items', () => {
|
|
114
|
+
const attr: AttributeDefinition = {
|
|
115
|
+
type: Array,
|
|
116
|
+
items: { type: String },
|
|
117
|
+
required: true,
|
|
118
|
+
};
|
|
119
|
+
const zodType = typeToZod(attr);
|
|
120
|
+
|
|
121
|
+
expect(() => zodType.parse(['a', 'b'])).not.toThrow();
|
|
122
|
+
expect(() => zodType.parse([])).not.toThrow();
|
|
123
|
+
expect(() => zodType.parse([1, 2])).toThrow(); // numbers, not strings
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should convert Array type with object items (like history)', () => {
|
|
127
|
+
const attr: AttributeDefinition = {
|
|
128
|
+
type: Array,
|
|
129
|
+
items: {
|
|
130
|
+
type: Object,
|
|
131
|
+
schema: {
|
|
132
|
+
date: { type: String },
|
|
133
|
+
status: { type: String, required: true },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: true,
|
|
137
|
+
};
|
|
138
|
+
const zodType = typeToZod(attr);
|
|
139
|
+
|
|
140
|
+
expect(() => zodType.parse([{ status: 'PENDING' }])).not.toThrow();
|
|
141
|
+
expect(() =>
|
|
142
|
+
zodType.parse([{ date: '2024-01-01', status: 'DONE' }])
|
|
143
|
+
).not.toThrow();
|
|
144
|
+
expect(() => zodType.parse([{ date: '2024-01-01' }])).toThrow(); // missing status
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle optional Object attribute', () => {
|
|
148
|
+
const attr: AttributeDefinition = {
|
|
149
|
+
type: Object,
|
|
150
|
+
schema: { name: { type: String, required: true } },
|
|
151
|
+
};
|
|
152
|
+
const zodType = typeToZod(attr);
|
|
153
|
+
|
|
154
|
+
expect(() => zodType.parse(undefined)).not.toThrow();
|
|
155
|
+
expect(() => zodType.parse({ name: 'test' })).not.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should handle deeply nested objects', () => {
|
|
159
|
+
const attr: AttributeDefinition = {
|
|
160
|
+
type: Object,
|
|
161
|
+
required: true,
|
|
162
|
+
schema: {
|
|
163
|
+
value: { type: Number, required: true },
|
|
164
|
+
breakdown: {
|
|
165
|
+
type: Object,
|
|
166
|
+
schema: {
|
|
167
|
+
base: { type: Number, required: true },
|
|
168
|
+
fees: { type: Number, required: true },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const zodType = typeToZod(attr);
|
|
174
|
+
|
|
175
|
+
expect(() =>
|
|
176
|
+
zodType.parse({ value: 100, breakdown: { base: 90, fees: 10 } })
|
|
177
|
+
).not.toThrow();
|
|
178
|
+
expect(() => zodType.parse({ value: 100 })).not.toThrow(); // breakdown optional
|
|
179
|
+
expect(() => zodType.parse({})).toThrow(); // value required
|
|
180
|
+
});
|
|
86
181
|
});
|
|
87
182
|
|
|
88
183
|
describe('modelToZod', () => {
|
package/src/utils/zod-utils.ts
CHANGED
|
@@ -5,19 +5,38 @@ import { AttributeDefinition, ModelDefinition } from '@/core/types';
|
|
|
5
5
|
import { z, ZodObject, ZodType } from 'zod';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Converts an attribute definition to a Zod schema type
|
|
8
|
+
* Converts an attribute definition to a Zod schema type.
|
|
9
|
+
* Supports scalars, nested objects (with schema), and arrays (with items).
|
|
9
10
|
*/
|
|
10
11
|
export const typeToZod = (attr: AttributeDefinition): ZodType => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
let zodType: ZodType;
|
|
13
|
+
|
|
14
|
+
if (attr.type === Object) {
|
|
15
|
+
const schema = (attr as { schema?: Record<string, AttributeDefinition> }).schema;
|
|
16
|
+
if (schema) {
|
|
17
|
+
const shape: Record<string, ZodType> = {};
|
|
18
|
+
for (const [key, nestedAttr] of Object.entries(schema)) {
|
|
19
|
+
shape[key] = typeToZod(nestedAttr);
|
|
20
|
+
}
|
|
21
|
+
zodType = z.looseObject(shape);
|
|
22
|
+
} else {
|
|
23
|
+
zodType = z.record(z.string(), z.unknown());
|
|
24
|
+
}
|
|
25
|
+
} else if (attr.type === Array) {
|
|
26
|
+
const items = (attr as { items?: AttributeDefinition }).items;
|
|
27
|
+
zodType = items ? z.array(typeToZod(items)) : z.array(z.unknown());
|
|
28
|
+
} else {
|
|
29
|
+
zodType =
|
|
30
|
+
attr.type === String
|
|
31
|
+
? z.string()
|
|
32
|
+
: attr.type === Number
|
|
33
|
+
? z.number()
|
|
34
|
+
: attr.type === Boolean
|
|
35
|
+
? z.boolean()
|
|
36
|
+
: attr.type === Date
|
|
37
|
+
? z.date()
|
|
38
|
+
: z.unknown();
|
|
39
|
+
}
|
|
21
40
|
|
|
22
41
|
return attr.required ? zodType : zodType.optional();
|
|
23
42
|
};
|
|
@@ -32,5 +51,5 @@ export const modelToZod = (model: ModelDefinition): ZodObject<any> => {
|
|
|
32
51
|
for (const [key, attr] of Object.entries(model.attributes)) {
|
|
33
52
|
shape[key] = typeToZod(attr);
|
|
34
53
|
}
|
|
35
|
-
return z.
|
|
54
|
+
return z.looseObject(shape);
|
|
36
55
|
};
|