@ftschopp/dynatable-core 1.0.0 → 1.1.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 +12 -0
- package/README.md +442 -2
- package/dist/builders/delete/types.d.ts +6 -1
- package/dist/builders/delete/types.d.ts.map +1 -1
- package/dist/builders/get/types.d.ts +6 -1
- package/dist/builders/get/types.d.ts.map +1 -1
- package/dist/builders/put/types.d.ts +6 -1
- package/dist/builders/put/types.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.d.ts +1 -1
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/types.d.ts +9 -10
- package/dist/builders/query/types.d.ts.map +1 -1
- package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -1
- package/dist/builders/transact-write/create-transact-write-builder.js +1 -1
- package/dist/builders/transact-write/types.d.ts +33 -6
- package/dist/builders/transact-write/types.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.js +30 -9
- package/dist/builders/update/types.d.ts +8 -2
- package/dist/builders/update/types.d.ts.map +1 -1
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/entity/create-entity-api.d.ts +15 -0
- package/dist/entity/create-entity-api.d.ts.map +1 -0
- package/dist/entity/create-entity-api.js +124 -0
- package/dist/entity/index.d.ts +12 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +18 -0
- package/dist/entity/middleware/factories.d.ts +6 -0
- package/dist/entity/middleware/factories.d.ts.map +1 -0
- package/dist/entity/middleware/factories.js +15 -0
- package/dist/entity/middleware/types.d.ts +8 -0
- package/dist/entity/middleware/types.d.ts.map +1 -0
- package/dist/entity/middleware/types.js +2 -0
- package/dist/entity/middleware/with-middleware.d.ts +8 -0
- package/dist/entity/middleware/with-middleware.d.ts.map +1 -0
- package/dist/entity/middleware/with-middleware.js +29 -0
- package/dist/{entity.d.ts → entity/types.d.ts} +6 -17
- package/dist/entity/types.d.ts.map +1 -0
- package/dist/entity/types.js +2 -0
- package/dist/entity/validation/key-validation.d.ts +7 -0
- package/dist/entity/validation/key-validation.d.ts.map +1 -0
- package/dist/entity/validation/key-validation.js +25 -0
- package/dist/table.d.ts +8 -1
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js +1 -0
- package/dist/utils/model-utils.d.ts +7 -1
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +29 -1
- package/dist/utils/zod-utils.d.ts +1 -1
- package/package.json +3 -2
- package/src/builders/README.md +68 -1
- package/src/builders/delete/types.ts +7 -1
- package/src/builders/get/types.ts +7 -1
- package/src/builders/put/types.ts +7 -1
- package/src/builders/query/create-query-builder.ts +4 -6
- package/src/builders/query/types.ts +9 -12
- package/src/builders/transact-write/README.md +37 -1
- package/src/builders/transact-write/create-transact-write-builder.ts +20 -14
- package/src/builders/transact-write/types.ts +44 -6
- package/src/builders/update/create-update-builder.test.ts +43 -0
- package/src/builders/update/create-update-builder.ts +47 -9
- package/src/builders/update/types.ts +9 -2
- package/src/core/types.ts +1 -0
- package/src/entity/create-entity-api.ts +212 -0
- package/src/entity/index.ts +19 -0
- package/src/entity/middleware/factories.ts +15 -0
- package/src/entity/middleware/types.ts +7 -0
- package/src/entity/middleware/with-middleware.ts +37 -0
- package/src/entity/types.ts +79 -0
- package/src/entity/validation/key-validation.ts +34 -0
- package/src/table.ts +10 -3
- package/src/utils/model-utils.test.ts +131 -1
- package/src/utils/model-utils.ts +32 -0
- package/dist/entity.d.ts.map +0 -1
- package/dist/entity.js +0 -161
- package/src/entity.ts +0 -337
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UpdateCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
1
2
|
import { OperationBuilder, AttrRef } from '../shared';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -12,11 +13,12 @@ export type UpdateAction = {
|
|
|
12
13
|
/**
|
|
13
14
|
* Builder interface for DynamoDB UpdateItem operations
|
|
14
15
|
*/
|
|
15
|
-
export interface UpdateBuilder<Model> extends OperationBuilder<Model> {
|
|
16
|
+
export interface UpdateBuilder<Model> extends Omit<OperationBuilder<Model>, 'dbParams'> {
|
|
16
17
|
/**
|
|
17
|
-
* Sets an attribute to a specific value
|
|
18
|
+
* Sets an attribute to a specific value, or sets multiple attributes at once
|
|
18
19
|
*/
|
|
19
20
|
set(attr: keyof Model | AttrRef, value: any): UpdateBuilder<Model>;
|
|
21
|
+
set(updates: Partial<Model>): UpdateBuilder<Model>;
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Removes an attribute from the item
|
|
@@ -39,4 +41,9 @@ export interface UpdateBuilder<Model> extends OperationBuilder<Model> {
|
|
|
39
41
|
returning(
|
|
40
42
|
mode: 'NONE' | 'ALL_OLD' | 'ALL_NEW' | 'UPDATED_OLD' | 'UPDATED_NEW'
|
|
41
43
|
): UpdateBuilder<Model>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Converts the builder state to DynamoDB UpdateItem parameters
|
|
47
|
+
*/
|
|
48
|
+
dbParams(): UpdateCommandInput;
|
|
42
49
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import { InferInput, InferKeyInput, InferModel, ModelDefinition } from '@/core/types';
|
|
4
|
+
import { applyPostDefaults, resolveKeys } from '@/utils/model-utils';
|
|
5
|
+
import { modelToZod } from '@/utils/zod-utils';
|
|
6
|
+
import {
|
|
7
|
+
createGetBuilder,
|
|
8
|
+
createPutBuilder,
|
|
9
|
+
createQueryBuilder,
|
|
10
|
+
createUpdateBuilder,
|
|
11
|
+
createDeleteBuilder,
|
|
12
|
+
createScanBuilder,
|
|
13
|
+
createBatchGetBuilder,
|
|
14
|
+
createBatchWriteBuilder,
|
|
15
|
+
WriteRequest,
|
|
16
|
+
} from '@/builders';
|
|
17
|
+
import { EntityAPI, EntityAPIOptions } from './types';
|
|
18
|
+
import { withMiddleware } from './middleware/with-middleware';
|
|
19
|
+
import { createCleanKeysMiddleware } from './middleware/factories';
|
|
20
|
+
import { validateKeyFields } from './validation/key-validation';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates an entity API instance with validation, key resolution, and builder creation.
|
|
24
|
+
*
|
|
25
|
+
* @param tableName - The name of the DynamoDB table
|
|
26
|
+
* @param modelName - The name of the model/entity
|
|
27
|
+
* @param model - The model definition
|
|
28
|
+
* @param client - DynamoDB client instance
|
|
29
|
+
* @param options - Optional configuration (logger, timestamps, cleanInternalKeys)
|
|
30
|
+
* @returns EntityAPI with get, put, query, scan, update, delete, batchGet, and batchWrite methods
|
|
31
|
+
*/
|
|
32
|
+
export const createEntityAPI = <Model extends ModelDefinition>(
|
|
33
|
+
tableName: string,
|
|
34
|
+
modelName: string,
|
|
35
|
+
model: Model,
|
|
36
|
+
client: DynamoDBClient,
|
|
37
|
+
options: EntityAPIOptions = {}
|
|
38
|
+
): EntityAPI<InferModel<Model>, InferInput<Model>, InferKeyInput<Model>> => {
|
|
39
|
+
const { logger, timestamps = false, cleanInternalKeys = false } = options;
|
|
40
|
+
|
|
41
|
+
// Build a Zod schema from the model
|
|
42
|
+
const zodSchema = modelToZod(model);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
get(key) {
|
|
46
|
+
// Validate key fields
|
|
47
|
+
validateKeyFields(modelName, model, key as Record<string, unknown>, 'get()');
|
|
48
|
+
|
|
49
|
+
// Resolve any key defaults or computed keys
|
|
50
|
+
const fullKey = resolveKeys(model, key);
|
|
51
|
+
|
|
52
|
+
const builder = createGetBuilder<InferKeyInput<Model>, InferModel<Model>>(
|
|
53
|
+
tableName,
|
|
54
|
+
fullKey,
|
|
55
|
+
client,
|
|
56
|
+
undefined,
|
|
57
|
+
logger
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return withMiddleware(builder, createCleanKeysMiddleware(cleanInternalKeys));
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
put(item) {
|
|
64
|
+
// Validate full input data
|
|
65
|
+
const parsed = zodSchema.parse(item);
|
|
66
|
+
|
|
67
|
+
// Apply post-processing defaults from the model (including timestamps for new items)
|
|
68
|
+
const withDefaults = applyPostDefaults(model, parsed, {
|
|
69
|
+
isUpdate: false,
|
|
70
|
+
timestamps,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Resolve keys again with defaults
|
|
74
|
+
const fullKey = resolveKeys(model, withDefaults);
|
|
75
|
+
|
|
76
|
+
// Combine keys and data into full item, adding _type field
|
|
77
|
+
const fullItem = {
|
|
78
|
+
...withDefaults,
|
|
79
|
+
...fullKey,
|
|
80
|
+
_type: modelName, // Add entity type identifier
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return createPutBuilder(tableName, fullItem, client, [], false, 'NONE', false, logger);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
query() {
|
|
87
|
+
// Query builder doesn't have execute() until after .where() is called
|
|
88
|
+
// The cleanInternalKeys will be handled in the builder itself
|
|
89
|
+
return createQueryBuilder<InferModel<Model>>(tableName, client, model, logger);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
scan() {
|
|
93
|
+
const builder = createScanBuilder<InferModel<Model>>(
|
|
94
|
+
tableName,
|
|
95
|
+
client,
|
|
96
|
+
[],
|
|
97
|
+
[],
|
|
98
|
+
undefined,
|
|
99
|
+
false,
|
|
100
|
+
undefined,
|
|
101
|
+
undefined,
|
|
102
|
+
undefined,
|
|
103
|
+
logger
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return withMiddleware(builder, createCleanKeysMiddleware(cleanInternalKeys));
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
update(key) {
|
|
110
|
+
// Validate key fields
|
|
111
|
+
validateKeyFields(modelName, model, key as Record<string, unknown>, 'update()');
|
|
112
|
+
|
|
113
|
+
// Resolve any key defaults or computed keys
|
|
114
|
+
const fullKey = resolveKeys(model, key);
|
|
115
|
+
|
|
116
|
+
const builder = createUpdateBuilder<InferModel<Model>>(
|
|
117
|
+
tableName,
|
|
118
|
+
fullKey as Partial<InferModel<Model>>,
|
|
119
|
+
client,
|
|
120
|
+
[],
|
|
121
|
+
{ set: [], remove: [], add: [], delete: [] },
|
|
122
|
+
'NONE',
|
|
123
|
+
0,
|
|
124
|
+
timestamps,
|
|
125
|
+
logger
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return withMiddleware(builder, createCleanKeysMiddleware(cleanInternalKeys));
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
delete(key) {
|
|
132
|
+
// Validate key fields
|
|
133
|
+
validateKeyFields(modelName, model, key as Record<string, unknown>, 'delete()');
|
|
134
|
+
|
|
135
|
+
// Resolve any key defaults or computed keys
|
|
136
|
+
const fullKey = resolveKeys(model, key);
|
|
137
|
+
|
|
138
|
+
const builder = createDeleteBuilder<InferModel<Model>>(
|
|
139
|
+
tableName,
|
|
140
|
+
fullKey as Partial<InferModel<Model>>,
|
|
141
|
+
client,
|
|
142
|
+
[],
|
|
143
|
+
'NONE',
|
|
144
|
+
logger
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return withMiddleware(builder, createCleanKeysMiddleware(cleanInternalKeys));
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
batchGet(keys) {
|
|
151
|
+
// Process all keys and validate them
|
|
152
|
+
const resolvedKeys = keys.map((key) => {
|
|
153
|
+
// Validate key fields
|
|
154
|
+
validateKeyFields(modelName, model, key as Record<string, unknown>, 'batchGet()');
|
|
155
|
+
|
|
156
|
+
// Resolve any key defaults or computed keys
|
|
157
|
+
return resolveKeys(model, key);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Create the request items in the format expected by BatchGetItem
|
|
161
|
+
const requestItems = {
|
|
162
|
+
[tableName]: {
|
|
163
|
+
Keys: resolvedKeys,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const builder = createBatchGetBuilder<InferModel<Model>>(
|
|
168
|
+
requestItems,
|
|
169
|
+
client,
|
|
170
|
+
undefined,
|
|
171
|
+
logger
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return withMiddleware(builder, createCleanKeysMiddleware(cleanInternalKeys));
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
batchWrite(items) {
|
|
178
|
+
// Validate and process all items
|
|
179
|
+
const processedItems = items.map((item) => {
|
|
180
|
+
// Validate full input data
|
|
181
|
+
const parsed = zodSchema.parse(item);
|
|
182
|
+
|
|
183
|
+
// Apply post-processing defaults from the model (including timestamps for new items)
|
|
184
|
+
const withDefaults = applyPostDefaults(model, parsed, {
|
|
185
|
+
isUpdate: false,
|
|
186
|
+
timestamps,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Resolve keys again with defaults
|
|
190
|
+
const fullKey = resolveKeys(model, withDefaults);
|
|
191
|
+
|
|
192
|
+
// Combine keys and data into full item, adding _type field
|
|
193
|
+
return {
|
|
194
|
+
...withDefaults,
|
|
195
|
+
...fullKey,
|
|
196
|
+
_type: modelName, // Add entity type identifier
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Create the request items in the format expected by BatchWriteItem
|
|
201
|
+
const requestItems: Record<string, WriteRequest[]> = {
|
|
202
|
+
[tableName]: processedItems.map((item) => ({
|
|
203
|
+
PutRequest: {
|
|
204
|
+
Item: item,
|
|
205
|
+
},
|
|
206
|
+
})),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return createBatchWriteBuilder(requestItems, client, logger);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity API module
|
|
3
|
+
*
|
|
4
|
+
* Provides high-level entity management with validation, key resolution, and builder creation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Core types
|
|
8
|
+
export type { EntityAPI, EntityAPIOptions } from './types';
|
|
9
|
+
|
|
10
|
+
// Middleware types and utilities
|
|
11
|
+
export type { ExecutionMiddleware } from './middleware/types';
|
|
12
|
+
export { withMiddleware } from './middleware/with-middleware';
|
|
13
|
+
export { createCleanKeysMiddleware } from './middleware/factories';
|
|
14
|
+
|
|
15
|
+
// Validation utilities
|
|
16
|
+
export { validateKeyFields } from './validation/key-validation';
|
|
17
|
+
|
|
18
|
+
// Main entity API factory
|
|
19
|
+
export { createEntityAPI } from './create-entity-api';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { stripInternalKeys } from '@/utils/model-utils';
|
|
2
|
+
import { ExecutionMiddleware } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates middleware configuration for cleaning internal keys
|
|
6
|
+
*/
|
|
7
|
+
export function createCleanKeysMiddleware(shouldClean: boolean): ExecutionMiddleware {
|
|
8
|
+
if (!shouldClean) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
after: (result) => stripInternalKeys(result),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { ExecutionMiddleware } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies middleware hooks to a builder's execute method
|
|
6
|
+
*/
|
|
7
|
+
export function withMiddleware<B extends { execute: () => Promise<any> }>(
|
|
8
|
+
builder: B,
|
|
9
|
+
middleware: ExecutionMiddleware
|
|
10
|
+
): B {
|
|
11
|
+
const { before, after } = middleware;
|
|
12
|
+
|
|
13
|
+
// If no middleware hooks provided, return builder as-is
|
|
14
|
+
if (!before && !after) {
|
|
15
|
+
return builder;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...builder,
|
|
20
|
+
execute: async () => {
|
|
21
|
+
// Before execution hook
|
|
22
|
+
if (before) {
|
|
23
|
+
await before();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Execute original builder
|
|
27
|
+
const result = await builder.execute();
|
|
28
|
+
|
|
29
|
+
// After execution hook
|
|
30
|
+
if (after) {
|
|
31
|
+
return await after(result);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DynamoDBLogger } from '@/utils/dynamodb-logger';
|
|
2
|
+
import {
|
|
3
|
+
GetBuilder,
|
|
4
|
+
PutBuilder,
|
|
5
|
+
QueryBuilder,
|
|
6
|
+
UpdateBuilder,
|
|
7
|
+
DeleteBuilder,
|
|
8
|
+
ScanBuilder,
|
|
9
|
+
BatchGetBuilder,
|
|
10
|
+
BatchWriteBuilder,
|
|
11
|
+
} from '@/builders';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for creating the Entity API
|
|
15
|
+
*/
|
|
16
|
+
export type EntityAPIOptions = {
|
|
17
|
+
logger?: DynamoDBLogger;
|
|
18
|
+
timestamps?: boolean;
|
|
19
|
+
cleanInternalKeys?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Entity API interface for a model
|
|
24
|
+
*/
|
|
25
|
+
export type EntityAPI<Model, Input, KeyInput> = {
|
|
26
|
+
/**
|
|
27
|
+
* Retrieves an item by its key.
|
|
28
|
+
* @param key - Partial or full key object to identify the item
|
|
29
|
+
* @returns GetBuilder configured for the item
|
|
30
|
+
*/
|
|
31
|
+
get: (key: KeyInput) => GetBuilder<KeyInput, Model>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Puts an item into the table after validation and applying defaults.
|
|
35
|
+
* @param item - The input data to put
|
|
36
|
+
* @returns PutBuilder configured for the item
|
|
37
|
+
*/
|
|
38
|
+
put: (item: Input) => PutBuilder<Model>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Queries items using key conditions.
|
|
42
|
+
* @returns QueryBuilder for building and executing the query
|
|
43
|
+
*/
|
|
44
|
+
query: () => QueryBuilder<Model>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Scans the entire table or index without key conditions.
|
|
48
|
+
* @returns ScanBuilder for building and executing the scan
|
|
49
|
+
*/
|
|
50
|
+
scan: () => ScanBuilder<Model>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Updates an item by its key.
|
|
54
|
+
* @param key - Partial or full key object to identify the item
|
|
55
|
+
* @returns UpdateBuilder configured for the item
|
|
56
|
+
*/
|
|
57
|
+
update: (key: KeyInput) => UpdateBuilder<Model>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Deletes an item by its key.
|
|
61
|
+
* @param key - Partial or full key object to identify the item
|
|
62
|
+
* @returns DeleteBuilder configured for the item
|
|
63
|
+
*/
|
|
64
|
+
delete: (key: KeyInput) => DeleteBuilder<Model>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves multiple items by their keys in a single batch operation.
|
|
68
|
+
* @param keys - Array of key objects to retrieve
|
|
69
|
+
* @returns BatchGetBuilder configured for the items
|
|
70
|
+
*/
|
|
71
|
+
batchGet: (keys: KeyInput[]) => BatchGetBuilder<Model>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Writes multiple items in a single batch operation (puts or deletes).
|
|
75
|
+
* @param items - Array of items to put
|
|
76
|
+
* @returns BatchWriteBuilder configured for the items
|
|
77
|
+
*/
|
|
78
|
+
batchWrite: (items: Input[]) => BatchWriteBuilder;
|
|
79
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
import { ModelDefinition } from '@/core/types';
|
|
4
|
+
import { extractTemplateVars } from '@/utils/model-utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validates that all required key fields are present in the key object
|
|
8
|
+
* @throws Error if any required key fields are missing
|
|
9
|
+
*/
|
|
10
|
+
export function validateKeyFields(
|
|
11
|
+
modelName: string,
|
|
12
|
+
model: ModelDefinition,
|
|
13
|
+
key: Record<string, unknown>,
|
|
14
|
+
operation?: string
|
|
15
|
+
): void {
|
|
16
|
+
// Extract required fields from key templates
|
|
17
|
+
const requiredFields = new Set<string>();
|
|
18
|
+
if (model.key) {
|
|
19
|
+
for (const keyDef of Object.values(model.key)) {
|
|
20
|
+
extractTemplateVars(keyDef.value).forEach((field) => requiredFields.add(field));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if all required fields are present
|
|
25
|
+
const missingFields = Array.from(requiredFields).filter((field) => key[field] === undefined);
|
|
26
|
+
|
|
27
|
+
if (missingFields.length > 0) {
|
|
28
|
+
const operationSuffix = operation ? ` for ${operation}` : '';
|
|
29
|
+
throw new Error(
|
|
30
|
+
`[${modelName}] Missing required key field(s)${operationSuffix}: ${missingFields.join(', ')}. ` +
|
|
31
|
+
`Required fields: ${Array.from(requiredFields).join(', ')}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/table.ts
CHANGED
|
@@ -25,14 +25,20 @@ export type TableConfig<S extends SchemaDefinition> = {
|
|
|
25
25
|
schema: S;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Helper type to force TypeScript to expand/simplify a type.
|
|
30
|
+
* This helps with type inference in callbacks by making the type "concrete".
|
|
31
|
+
*/
|
|
32
|
+
type Simplify<T> = { [K in keyof T]: T[K] } & {};
|
|
33
|
+
|
|
28
34
|
/**
|
|
29
35
|
* Internal helper type to infer all entity APIs from schema definition
|
|
30
36
|
*/
|
|
31
37
|
type EntityMap<S extends SchemaDefinition> = {
|
|
32
38
|
[K in keyof S['models']]: EntityAPI<
|
|
33
|
-
InferModelFromSchema<S, K
|
|
34
|
-
InferInputFromSchema<S, K
|
|
35
|
-
InferKeyInput<S['models'][K]
|
|
39
|
+
Simplify<InferModelFromSchema<S, K>>,
|
|
40
|
+
Simplify<InferInputFromSchema<S, K>>,
|
|
41
|
+
Simplify<InferKeyInput<S['models'][K]>>
|
|
36
42
|
>;
|
|
37
43
|
};
|
|
38
44
|
|
|
@@ -65,6 +71,7 @@ export class Table<S extends SchemaDefinition> {
|
|
|
65
71
|
rawEntities[modelName] = createEntityAPI(tableName, modelName, model, client, {
|
|
66
72
|
logger,
|
|
67
73
|
timestamps: schema.params?.timestamps ?? false,
|
|
74
|
+
cleanInternalKeys: schema.params?.cleanInternalKeys ?? false,
|
|
68
75
|
});
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { applyPostDefaults } from './model-utils';
|
|
2
|
+
import { applyPostDefaults, stripInternalKeys } from './model-utils';
|
|
3
3
|
import { ModelDefinition } from '../core/types';
|
|
4
4
|
|
|
5
5
|
describe('applyPostDefaults - Timestamps', () => {
|
|
@@ -230,3 +230,133 @@ describe('applyPostDefaults - Timestamps', () => {
|
|
|
230
230
|
});
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
|
+
|
|
234
|
+
describe('stripInternalKeys', () => {
|
|
235
|
+
test('should remove PK, SK, and _type from object', () => {
|
|
236
|
+
const input = {
|
|
237
|
+
PK: 'USER#alice',
|
|
238
|
+
SK: 'USER#alice',
|
|
239
|
+
_type: 'User',
|
|
240
|
+
username: 'alice',
|
|
241
|
+
name: 'Alice',
|
|
242
|
+
email: 'alice@example.com',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const result = stripInternalKeys(input);
|
|
246
|
+
|
|
247
|
+
expect(result).toEqual({
|
|
248
|
+
username: 'alice',
|
|
249
|
+
name: 'Alice',
|
|
250
|
+
email: 'alice@example.com',
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('should handle object without internal keys', () => {
|
|
255
|
+
const input = {
|
|
256
|
+
username: 'alice',
|
|
257
|
+
name: 'Alice',
|
|
258
|
+
email: 'alice@example.com',
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const result = stripInternalKeys(input);
|
|
262
|
+
|
|
263
|
+
expect(result).toEqual(input);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('should handle array of objects', () => {
|
|
267
|
+
const input = [
|
|
268
|
+
{
|
|
269
|
+
PK: 'USER#alice',
|
|
270
|
+
SK: 'USER#alice',
|
|
271
|
+
_type: 'User',
|
|
272
|
+
username: 'alice',
|
|
273
|
+
name: 'Alice',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
PK: 'USER#bob',
|
|
277
|
+
SK: 'USER#bob',
|
|
278
|
+
_type: 'User',
|
|
279
|
+
username: 'bob',
|
|
280
|
+
name: 'Bob',
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const result = stripInternalKeys(input);
|
|
285
|
+
|
|
286
|
+
expect(result).toEqual([
|
|
287
|
+
{ username: 'alice', name: 'Alice' },
|
|
288
|
+
{ username: 'bob', name: 'Bob' },
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('should handle undefined', () => {
|
|
293
|
+
const result = stripInternalKeys(undefined);
|
|
294
|
+
expect(result).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('should handle null', () => {
|
|
298
|
+
const result = stripInternalKeys(null);
|
|
299
|
+
expect(result).toBeNull();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('should handle empty object', () => {
|
|
303
|
+
const input = {};
|
|
304
|
+
const result = stripInternalKeys(input);
|
|
305
|
+
expect(result).toEqual({});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('should handle empty array', () => {
|
|
309
|
+
const input: any[] = [];
|
|
310
|
+
const result = stripInternalKeys(input);
|
|
311
|
+
expect(result).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('should preserve other fields with similar names', () => {
|
|
315
|
+
const input = {
|
|
316
|
+
PK: 'USER#alice',
|
|
317
|
+
SK: 'USER#alice',
|
|
318
|
+
_type: 'User',
|
|
319
|
+
PKG: 'package-name',
|
|
320
|
+
SKIP: 'skip-value',
|
|
321
|
+
type: 'customer',
|
|
322
|
+
username: 'alice',
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const result = stripInternalKeys(input);
|
|
326
|
+
|
|
327
|
+
expect(result).toEqual({
|
|
328
|
+
PKG: 'package-name',
|
|
329
|
+
SKIP: 'skip-value',
|
|
330
|
+
type: 'customer',
|
|
331
|
+
username: 'alice',
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('should handle nested objects (does not recurse)', () => {
|
|
336
|
+
const input = {
|
|
337
|
+
PK: 'USER#alice',
|
|
338
|
+
SK: 'USER#alice',
|
|
339
|
+
_type: 'User',
|
|
340
|
+
username: 'alice',
|
|
341
|
+
metadata: {
|
|
342
|
+
PK: 'METADATA#1',
|
|
343
|
+
SK: 'METADATA#1',
|
|
344
|
+
_type: 'Metadata',
|
|
345
|
+
value: 'test',
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const result = stripInternalKeys(input);
|
|
350
|
+
|
|
351
|
+
// stripInternalKeys does not recurse into nested objects
|
|
352
|
+
expect(result).toEqual({
|
|
353
|
+
username: 'alice',
|
|
354
|
+
metadata: {
|
|
355
|
+
PK: 'METADATA#1',
|
|
356
|
+
SK: 'METADATA#1',
|
|
357
|
+
_type: 'Metadata',
|
|
358
|
+
value: 'test',
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
package/src/utils/model-utils.ts
CHANGED
|
@@ -99,3 +99,35 @@ export const applyPostDefaults = <M extends ModelDefinition>(
|
|
|
99
99
|
|
|
100
100
|
return result as InferModel<M>;
|
|
101
101
|
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Internal DynamoDB keys that should be stripped when cleanInternalKeys is enabled
|
|
105
|
+
*/
|
|
106
|
+
const INTERNAL_KEYS = ['PK', 'SK', '_type'] as const;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Removes internal DynamoDB keys from an item or array of items
|
|
110
|
+
* @param data - Single item or array of items from DynamoDB
|
|
111
|
+
* @returns Data with internal keys removed
|
|
112
|
+
*/
|
|
113
|
+
export const stripInternalKeys = <T>(data: T | T[] | undefined): T | T[] | undefined => {
|
|
114
|
+
if (data === undefined || data === null) {
|
|
115
|
+
return data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(data)) {
|
|
119
|
+
return data.map((item) => stripInternalKeys(item)) as T[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof data === 'object') {
|
|
123
|
+
const cleaned: any = {};
|
|
124
|
+
for (const [key, value] of Object.entries(data)) {
|
|
125
|
+
if (!INTERNAL_KEYS.includes(key as any)) {
|
|
126
|
+
cleaned[key] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return cleaned as T;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return data;
|
|
133
|
+
};
|
package/dist/entity.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../src/entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGtF,OAAO,EASL,UAAU,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,WAAW,EACX,eAAe,EACf,iBAAiB,EAElB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,SAAS,eAAe,GAAG,GAAG,IAAI;IACtF;;;;OAIG;IACH,GAAG,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAEpD;;;;OAIG;IACH,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC;IAExC;;;OAGG;IACH,KAAK,EAAE,MAAM,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAE3C;;;OAGG;IACH,IAAI,EAAE,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC;IAE/B;;;;OAIG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,aAAa,CAAC,KAAK,CAAC,CAAC;IAEhD;;;;OAIG;IACH,MAAM,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,aAAa,CAAC,KAAK,CAAC,CAAC;IAEhD;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,eAAe,CAAC,KAAK,CAAC,CAAC;IAEvD;;;;OAIG;IACH,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,iBAAiB,CAAC;CACnD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,eAAe,EAC3D,WAAW,MAAM,EACjB,WAAW,MAAM,EACjB,OAAO,KAAK,EACZ,QAAQ,cAAc,EACtB,UAAS,gBAAqB,KAC7B,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,KAAK,CAqO7E,CAAC"}
|