@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +442 -2
  3. package/dist/builders/delete/types.d.ts +6 -1
  4. package/dist/builders/delete/types.d.ts.map +1 -1
  5. package/dist/builders/get/types.d.ts +6 -1
  6. package/dist/builders/get/types.d.ts.map +1 -1
  7. package/dist/builders/put/types.d.ts +6 -1
  8. package/dist/builders/put/types.d.ts.map +1 -1
  9. package/dist/builders/query/create-query-builder.d.ts +1 -1
  10. package/dist/builders/query/create-query-builder.d.ts.map +1 -1
  11. package/dist/builders/query/types.d.ts +9 -10
  12. package/dist/builders/query/types.d.ts.map +1 -1
  13. package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -1
  14. package/dist/builders/transact-write/create-transact-write-builder.js +1 -1
  15. package/dist/builders/transact-write/types.d.ts +33 -6
  16. package/dist/builders/transact-write/types.d.ts.map +1 -1
  17. package/dist/builders/update/create-update-builder.d.ts.map +1 -1
  18. package/dist/builders/update/create-update-builder.js +30 -9
  19. package/dist/builders/update/types.d.ts +8 -2
  20. package/dist/builders/update/types.d.ts.map +1 -1
  21. package/dist/core/types.d.ts +1 -0
  22. package/dist/core/types.d.ts.map +1 -1
  23. package/dist/entity/create-entity-api.d.ts +15 -0
  24. package/dist/entity/create-entity-api.d.ts.map +1 -0
  25. package/dist/entity/create-entity-api.js +124 -0
  26. package/dist/entity/index.d.ts +12 -0
  27. package/dist/entity/index.d.ts.map +1 -0
  28. package/dist/entity/index.js +18 -0
  29. package/dist/entity/middleware/factories.d.ts +6 -0
  30. package/dist/entity/middleware/factories.d.ts.map +1 -0
  31. package/dist/entity/middleware/factories.js +15 -0
  32. package/dist/entity/middleware/types.d.ts +8 -0
  33. package/dist/entity/middleware/types.d.ts.map +1 -0
  34. package/dist/entity/middleware/types.js +2 -0
  35. package/dist/entity/middleware/with-middleware.d.ts +8 -0
  36. package/dist/entity/middleware/with-middleware.d.ts.map +1 -0
  37. package/dist/entity/middleware/with-middleware.js +29 -0
  38. package/dist/{entity.d.ts → entity/types.d.ts} +6 -17
  39. package/dist/entity/types.d.ts.map +1 -0
  40. package/dist/entity/types.js +2 -0
  41. package/dist/entity/validation/key-validation.d.ts +7 -0
  42. package/dist/entity/validation/key-validation.d.ts.map +1 -0
  43. package/dist/entity/validation/key-validation.js +25 -0
  44. package/dist/table.d.ts +8 -1
  45. package/dist/table.d.ts.map +1 -1
  46. package/dist/table.js +1 -0
  47. package/dist/utils/model-utils.d.ts +7 -1
  48. package/dist/utils/model-utils.d.ts.map +1 -1
  49. package/dist/utils/model-utils.js +29 -1
  50. package/dist/utils/zod-utils.d.ts +1 -1
  51. package/package.json +3 -2
  52. package/src/builders/README.md +68 -1
  53. package/src/builders/delete/types.ts +7 -1
  54. package/src/builders/get/types.ts +7 -1
  55. package/src/builders/put/types.ts +7 -1
  56. package/src/builders/query/create-query-builder.ts +4 -6
  57. package/src/builders/query/types.ts +9 -12
  58. package/src/builders/transact-write/README.md +37 -1
  59. package/src/builders/transact-write/create-transact-write-builder.ts +20 -14
  60. package/src/builders/transact-write/types.ts +44 -6
  61. package/src/builders/update/create-update-builder.test.ts +43 -0
  62. package/src/builders/update/create-update-builder.ts +47 -9
  63. package/src/builders/update/types.ts +9 -2
  64. package/src/core/types.ts +1 -0
  65. package/src/entity/create-entity-api.ts +212 -0
  66. package/src/entity/index.ts +19 -0
  67. package/src/entity/middleware/factories.ts +15 -0
  68. package/src/entity/middleware/types.ts +7 -0
  69. package/src/entity/middleware/with-middleware.ts +37 -0
  70. package/src/entity/types.ts +79 -0
  71. package/src/entity/validation/key-validation.ts +34 -0
  72. package/src/table.ts +10 -3
  73. package/src/utils/model-utils.test.ts +131 -1
  74. package/src/utils/model-utils.ts +32 -0
  75. package/dist/entity.d.ts.map +0 -1
  76. package/dist/entity.js +0 -161
  77. 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
@@ -95,6 +95,7 @@ export type ModelDefinition = {
95
95
  export type SchemaParams = {
96
96
  isoDates?: boolean;
97
97
  timestamps?: boolean;
98
+ cleanInternalKeys?: boolean;
98
99
  };
99
100
 
100
101
  /**
@@ -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,7 @@
1
+ /**
2
+ * Middleware hooks for builder execution
3
+ */
4
+ export type ExecutionMiddleware<T = any> = {
5
+ before?: () => void | Promise<void>;
6
+ after?: (result: T) => T | Promise<T>;
7
+ };
@@ -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
+ });
@@ -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
+ };
@@ -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"}