@ftschopp/dynatable-core 1.1.0 → 1.2.1
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 +21 -0
- package/README.md +105 -19
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +25 -7
- 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/builders/query/create-query-builder.test.ts +70 -0
- package/src/builders/query/create-query-builder.ts +29 -7
- 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,24 @@
|
|
|
1
|
+
## @ftschopp/dynatable-core [1.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.0...@ftschopp/dynatable-core@1.2.1) (2026-04-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* resolve GSI key attributes to KeyConditionExpression when using useIndex ([8b2ee35](https://github.com/ftschopp/dynatable/commit/8b2ee35ca56bae9a32c8f1fec764365e8496674d))
|
|
7
|
+
|
|
8
|
+
## @ftschopp/dynatable-core [1.2.1](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.2.0...@ftschopp/dynatable-core@1.2.1) (2026-04-15)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* resolve GSI key attributes to KeyConditionExpression when using useIndex ([8b2ee35](https://github.com/ftschopp/dynatable/commit/8b2ee35ca56bae9a32c8f1fec764365e8496674d))
|
|
14
|
+
|
|
15
|
+
# @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)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* support nested objects and arrays schema ([c06e43e](https://github.com/ftschopp/dynatable/commit/c06e43e6b425bae09f127d2f767b5c8c62dd1142))
|
|
21
|
+
|
|
1
22
|
# @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
23
|
|
|
3
24
|
|
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
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAuU7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,GACtB,YAAY,CAAC,KAAK,CAAC,CA4BrB"}
|
|
@@ -6,11 +6,20 @@ const operators_1 = require("../shared/operators");
|
|
|
6
6
|
const conditions_1 = require("../shared/conditions");
|
|
7
7
|
const model_utils_1 = require("../../utils/model-utils");
|
|
8
8
|
/**
|
|
9
|
-
* Determines if a field is a key field (used in pk/sk templates)
|
|
9
|
+
* Determines if a field is a key field (used in pk/sk or index key templates)
|
|
10
10
|
*/
|
|
11
|
-
function isKeyField(fieldName, model) {
|
|
11
|
+
function isKeyField(fieldName, model, indexName) {
|
|
12
12
|
if (!model?.key)
|
|
13
13
|
return false;
|
|
14
|
+
// When an index is specified, also check model.index for key templates
|
|
15
|
+
if (indexName && model.index) {
|
|
16
|
+
for (const [, keyDef] of Object.entries(model.index)) {
|
|
17
|
+
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
18
|
+
if (templateVars.includes(fieldName)) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
const keyEntries = Object.entries(model.key);
|
|
15
24
|
for (const [, keyDef] of keyEntries) {
|
|
16
25
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
@@ -24,13 +33,22 @@ function isKeyField(fieldName, model) {
|
|
|
24
33
|
* Maps a model attribute name to its corresponding DynamoDB key name (PK/SK)
|
|
25
34
|
* Also returns the key template for value transformation
|
|
26
35
|
*/
|
|
27
|
-
function getKeyNameForAttribute(fieldName, model) {
|
|
36
|
+
function getKeyNameForAttribute(fieldName, model, indexName) {
|
|
28
37
|
if (!model?.key)
|
|
29
38
|
return null;
|
|
39
|
+
// When an index is specified, check model.index first for key templates
|
|
40
|
+
if (indexName && model.index) {
|
|
41
|
+
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
42
|
+
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
43
|
+
if (templateVars.includes(fieldName)) {
|
|
44
|
+
return { keyName, template: keyDef.value };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
30
48
|
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
31
49
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
32
50
|
if (templateVars.includes(fieldName)) {
|
|
33
|
-
return { keyName, template: keyDef.value };
|
|
51
|
+
return { keyName, template: keyDef.value };
|
|
34
52
|
}
|
|
35
53
|
}
|
|
36
54
|
return null;
|
|
@@ -46,7 +64,7 @@ function applyKeyTemplate(template, fieldName, fieldValue) {
|
|
|
46
64
|
* Separates a condition tree into key conditions and filter conditions
|
|
47
65
|
* Key conditions are rewritten to use actual DynamoDB key names (PK/SK)
|
|
48
66
|
*/
|
|
49
|
-
function separateConditions(condition, model) {
|
|
67
|
+
function separateConditions(condition, model, indexName) {
|
|
50
68
|
const keyConditions = [];
|
|
51
69
|
const filterConditions = [];
|
|
52
70
|
function traverse(cond) {
|
|
@@ -76,7 +94,7 @@ function separateConditions(condition, model) {
|
|
|
76
94
|
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
77
95
|
if (fieldMatch && fieldMatch[1]) {
|
|
78
96
|
const fieldName = fieldMatch[1];
|
|
79
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model);
|
|
97
|
+
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
80
98
|
if (keyInfo) {
|
|
81
99
|
// This is a key field - rewrite the condition to use the actual key name
|
|
82
100
|
// and apply the template to the value
|
|
@@ -178,7 +196,7 @@ function createQueryExecutor(state) {
|
|
|
178
196
|
throw new Error('No where condition specified');
|
|
179
197
|
}
|
|
180
198
|
// Separate key conditions from filter conditions
|
|
181
|
-
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model);
|
|
199
|
+
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
|
|
182
200
|
// Build KeyConditionExpression (simple AND)
|
|
183
201
|
const keyExpr = buildSimpleAndExpression(keyConditions);
|
|
184
202
|
// Build FilterExpression (can be complex with OR, NOT, etc.)
|
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
|
@@ -228,3 +228,73 @@ describe('QueryBuilder - Pagination', () => {
|
|
|
228
228
|
});
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('QueryBuilder - GSI key recognition', () => {
|
|
233
|
+
const client = new DynamoDBClient({});
|
|
234
|
+
const tableName = 'TestTable';
|
|
235
|
+
|
|
236
|
+
interface UserModel {
|
|
237
|
+
id: string;
|
|
238
|
+
email: string;
|
|
239
|
+
name?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const modelWithIndex: ModelDefinition = {
|
|
243
|
+
key: {
|
|
244
|
+
PK: { type: String, value: 'USER#${id}' },
|
|
245
|
+
SK: { type: String, value: 'USER#${id}' },
|
|
246
|
+
},
|
|
247
|
+
index: {
|
|
248
|
+
GSI1PK: { type: String, value: 'EMAIL#${email}' },
|
|
249
|
+
GSI1SK: { type: String, value: 'EMAIL#${email}' },
|
|
250
|
+
},
|
|
251
|
+
attributes: {
|
|
252
|
+
id: { type: String, required: true },
|
|
253
|
+
email: { type: String, required: true },
|
|
254
|
+
name: { type: String },
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
test('should place GSI index attribute into KeyConditionExpression when useIndex is set', () => {
|
|
259
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
260
|
+
.where((attr, op) => op.eq(attr.email, 'alice@example.com'))
|
|
261
|
+
.useIndex('GSI1')
|
|
262
|
+
.dbParams();
|
|
263
|
+
|
|
264
|
+
// email should be recognized as a key field via model.index and placed in KeyConditionExpression
|
|
265
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
266
|
+
expect(params.KeyConditionExpression).toContain('#GSI1PK');
|
|
267
|
+
expect(params.ExpressionAttributeValues).toBeDefined();
|
|
268
|
+
// The value should have the template applied
|
|
269
|
+
expect(Object.values(params.ExpressionAttributeValues!)).toContain('EMAIL#alice@example.com');
|
|
270
|
+
// It should NOT be in FilterExpression
|
|
271
|
+
expect(params.FilterExpression).toBeUndefined();
|
|
272
|
+
expect(params.IndexName).toBe('GSI1');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should place GSI attribute into FilterExpression when NO index is specified', () => {
|
|
276
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
277
|
+
.where((attr, op) => op.and(op.eq(attr.id, '123'), op.eq(attr.email, 'alice@example.com')))
|
|
278
|
+
.dbParams();
|
|
279
|
+
|
|
280
|
+
// id maps to PK -> KeyConditionExpression
|
|
281
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
282
|
+
expect(params.KeyConditionExpression).toContain('#PK');
|
|
283
|
+
// email is NOT a primary key field, so without useIndex it goes to filter
|
|
284
|
+
expect(params.FilterExpression).toBeDefined();
|
|
285
|
+
expect(params.FilterExpression).toContain('#email');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('should handle both primary key and GSI key fields together with useIndex', () => {
|
|
289
|
+
const params = createQueryBuilder<UserModel>(tableName, client, modelWithIndex)
|
|
290
|
+
.where((attr, op) => op.and(op.eq(attr.email, 'alice@example.com'), op.eq(attr.id, '123')))
|
|
291
|
+
.useIndex('GSI1')
|
|
292
|
+
.dbParams();
|
|
293
|
+
|
|
294
|
+
// Both should be key conditions: email via index, id via primary key
|
|
295
|
+
expect(params.KeyConditionExpression).toBeDefined();
|
|
296
|
+
expect(params.KeyConditionExpression).toContain('#GSI1PK');
|
|
297
|
+
expect(params.KeyConditionExpression).toContain('#PK');
|
|
298
|
+
expect(params.FilterExpression).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -27,11 +27,21 @@ type QueryState<Model> = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Determines if a field is a key field (used in pk/sk templates)
|
|
30
|
+
* Determines if a field is a key field (used in pk/sk or index key templates)
|
|
31
31
|
*/
|
|
32
|
-
function isKeyField(fieldName: string, model?: ModelDefinition): boolean {
|
|
32
|
+
function isKeyField(fieldName: string, model?: ModelDefinition, indexName?: string): boolean {
|
|
33
33
|
if (!model?.key) return false;
|
|
34
34
|
|
|
35
|
+
// When an index is specified, also check model.index for key templates
|
|
36
|
+
if (indexName && model.index) {
|
|
37
|
+
for (const [, keyDef] of Object.entries(model.index)) {
|
|
38
|
+
const templateVars = extractTemplateVars(keyDef.value);
|
|
39
|
+
if (templateVars.includes(fieldName)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
const keyEntries = Object.entries(model.key);
|
|
36
46
|
for (const [, keyDef] of keyEntries) {
|
|
37
47
|
const templateVars = extractTemplateVars(keyDef.value);
|
|
@@ -48,14 +58,25 @@ function isKeyField(fieldName: string, model?: ModelDefinition): boolean {
|
|
|
48
58
|
*/
|
|
49
59
|
function getKeyNameForAttribute(
|
|
50
60
|
fieldName: string,
|
|
51
|
-
model?: ModelDefinition
|
|
61
|
+
model?: ModelDefinition,
|
|
62
|
+
indexName?: string
|
|
52
63
|
): { keyName: string; template: string } | null {
|
|
53
64
|
if (!model?.key) return null;
|
|
54
65
|
|
|
66
|
+
// When an index is specified, check model.index first for key templates
|
|
67
|
+
if (indexName && model.index) {
|
|
68
|
+
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
69
|
+
const templateVars = extractTemplateVars(keyDef.value);
|
|
70
|
+
if (templateVars.includes(fieldName)) {
|
|
71
|
+
return { keyName, template: keyDef.value };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
55
76
|
for (const [keyName, keyDef] of Object.entries(model.key)) {
|
|
56
77
|
const templateVars = extractTemplateVars(keyDef.value);
|
|
57
78
|
if (templateVars.includes(fieldName)) {
|
|
58
|
-
return { keyName, template: keyDef.value };
|
|
79
|
+
return { keyName, template: keyDef.value };
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
return null;
|
|
@@ -75,7 +96,8 @@ function applyKeyTemplate(template: string, fieldName: string, fieldValue: any):
|
|
|
75
96
|
*/
|
|
76
97
|
function separateConditions(
|
|
77
98
|
condition: Condition,
|
|
78
|
-
model?: ModelDefinition
|
|
99
|
+
model?: ModelDefinition,
|
|
100
|
+
indexName?: string
|
|
79
101
|
): { keyConditions: Condition[]; filterConditions: Condition[] } {
|
|
80
102
|
const keyConditions: Condition[] = [];
|
|
81
103
|
const filterConditions: Condition[] = [];
|
|
@@ -106,7 +128,7 @@ function separateConditions(
|
|
|
106
128
|
const fieldMatch = cond.expression.match(/#(\w+)/);
|
|
107
129
|
if (fieldMatch && fieldMatch[1]) {
|
|
108
130
|
const fieldName = fieldMatch[1];
|
|
109
|
-
const keyInfo = getKeyNameForAttribute(fieldName, model);
|
|
131
|
+
const keyInfo = getKeyNameForAttribute(fieldName, model, indexName);
|
|
110
132
|
|
|
111
133
|
if (keyInfo) {
|
|
112
134
|
// This is a key field - rewrite the condition to use the actual key name
|
|
@@ -231,7 +253,7 @@ function createQueryExecutor<Model>(state: QueryState<Model>): QueryExecutor<Mod
|
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
// Separate key conditions from filter conditions
|
|
234
|
-
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model);
|
|
256
|
+
const { keyConditions, filterConditions } = separateConditions(state.condition, state.model, state.indexName);
|
|
235
257
|
|
|
236
258
|
// Build KeyConditionExpression (simple AND)
|
|
237
259
|
const keyExpr = buildSimpleAndExpression(keyConditions);
|
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
|
};
|