@jaypie/dynamodb 0.1.0 → 0.1.2

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.
@@ -6,7 +6,7 @@ import type { DynamoClientConfig } from "./types.js";
6
6
  *
7
7
  * @param config - Client configuration
8
8
  */
9
- export declare function initClient(config: DynamoClientConfig): void;
9
+ export declare function initClient(config?: DynamoClientConfig): void;
10
10
  /**
11
11
  * Get the initialized DynamoDB Document Client
12
12
  * @throws ConfigurationError if client has not been initialized
@@ -1,71 +1,40 @@
1
1
  import type { FabricEntity } from "./types.js";
2
- /**
3
- * Parameters for getEntity
4
- */
5
- export interface GetEntityParams {
6
- /** Entity ID (sort key) */
7
- id: string;
8
- /** Entity model (partition key) */
9
- model: string;
10
- }
11
- /**
12
- * Parameters for putEntity
13
- */
14
- export interface PutEntityParams<T extends FabricEntity> {
15
- /** The entity to save */
16
- entity: T;
17
- }
18
- /**
19
- * Parameters for updateEntity
20
- */
21
- export interface UpdateEntityParams<T extends FabricEntity> {
22
- /** The entity with updated fields */
23
- entity: T;
24
- }
25
- /**
26
- * Parameters for deleteEntity (soft delete)
27
- */
28
- export interface DeleteEntityParams {
29
- /** Entity ID (sort key) */
30
- id: string;
31
- /** Entity model (partition key) */
32
- model: string;
33
- }
34
- /**
35
- * Parameters for archiveEntity
36
- */
37
- export interface ArchiveEntityParams {
38
- /** Entity ID (sort key) */
39
- id: string;
40
- /** Entity model (partition key) */
41
- model: string;
42
- }
43
2
  /**
44
3
  * Get a single entity by primary key
45
4
  */
46
- export declare function getEntity<T extends FabricEntity = FabricEntity>(params: GetEntityParams): Promise<T | null>;
5
+ export declare const getEntity: import("@jaypie/vocabulary").ServiceHandlerFunction<Record<string, unknown>, FabricEntity | null>;
47
6
  /**
48
7
  * Put (create or replace) an entity
49
8
  * Auto-populates GSI index keys via indexEntity
9
+ *
10
+ * Note: This is a regular async function (not serviceHandler) because it accepts
11
+ * complex FabricEntity objects that can't be coerced by vocabulary's type system.
50
12
  */
51
- export declare function putEntity<T extends FabricEntity>(params: PutEntityParams<T>): Promise<T>;
13
+ export declare function putEntity({ entity, }: {
14
+ entity: FabricEntity;
15
+ }): Promise<FabricEntity>;
52
16
  /**
53
17
  * Update an existing entity
54
18
  * Auto-populates GSI index keys and sets updatedAt
19
+ *
20
+ * Note: This is a regular async function (not serviceHandler) because it accepts
21
+ * complex FabricEntity objects that can't be coerced by vocabulary's type system.
55
22
  */
56
- export declare function updateEntity<T extends FabricEntity>(params: UpdateEntityParams<T>): Promise<T>;
23
+ export declare function updateEntity({ entity, }: {
24
+ entity: FabricEntity;
25
+ }): Promise<FabricEntity>;
57
26
  /**
58
27
  * Soft delete an entity by setting deletedAt timestamp
59
28
  * Re-indexes with appropriate suffix based on archived/deleted state
60
29
  */
61
- export declare function deleteEntity(params: DeleteEntityParams): Promise<boolean>;
30
+ export declare const deleteEntity: import("@jaypie/vocabulary").ServiceHandlerFunction<Record<string, unknown>, boolean>;
62
31
  /**
63
32
  * Archive an entity by setting archivedAt timestamp
64
33
  * Re-indexes with appropriate suffix based on archived/deleted state
65
34
  */
66
- export declare function archiveEntity(params: ArchiveEntityParams): Promise<boolean>;
35
+ export declare const archiveEntity: import("@jaypie/vocabulary").ServiceHandlerFunction<Record<string, unknown>, boolean>;
67
36
  /**
68
37
  * Hard delete an entity (permanently removes from table)
69
38
  * Use with caution - prefer deleteEntity for soft delete
70
39
  */
71
- export declare function destroyEntity(params: DeleteEntityParams): Promise<boolean>;
40
+ export declare const destroyEntity: import("@jaypie/vocabulary").ServiceHandlerFunction<Record<string, unknown>, boolean>;
@@ -3,7 +3,12 @@
3
3
  var clientDynamodb = require('@aws-sdk/client-dynamodb');
4
4
  var libDynamodb = require('@aws-sdk/lib-dynamodb');
5
5
  var errors = require('@jaypie/errors');
6
+ var vocabulary = require('@jaypie/vocabulary');
6
7
 
8
+ // Environment variable names
9
+ const ENV_AWS_REGION = "AWS_REGION";
10
+ const ENV_DYNAMODB_TABLE_NAME = "DYNAMODB_TABLE_NAME";
11
+ // Defaults
7
12
  const DEFAULT_REGION = "us-east-1";
8
13
  const LOCAL_CREDENTIALS = {
9
14
  accessKeyId: "local",
@@ -26,8 +31,9 @@ function isLocalEndpoint(endpoint) {
26
31
  *
27
32
  * @param config - Client configuration
28
33
  */
29
- function initClient(config) {
30
- const { endpoint, region = DEFAULT_REGION } = config;
34
+ function initClient(config = {}) {
35
+ const { endpoint } = config;
36
+ const region = config.region ?? process.env[ENV_AWS_REGION] ?? DEFAULT_REGION;
31
37
  // Auto-detect local mode and use dummy credentials
32
38
  const credentials = config.credentials ??
33
39
  (isLocalEndpoint(endpoint) ? LOCAL_CREDENTIALS : undefined);
@@ -41,7 +47,7 @@ function initClient(config) {
41
47
  removeUndefinedValues: true,
42
48
  },
43
49
  });
44
- tableName = config.tableName;
50
+ tableName = config.tableName ?? process.env[ENV_DYNAMODB_TABLE_NAME] ?? null;
45
51
  }
46
52
  /**
47
53
  * Get the initialized DynamoDB Document Client
@@ -187,25 +193,51 @@ function indexEntity(entity, suffix = "") {
187
193
  }
188
194
 
189
195
  /**
190
- * Get a single entity by primary key
196
+ * Calculate suffix based on entity's archived/deleted state
191
197
  */
192
- async function getEntity(params) {
193
- const { id, model } = params;
194
- const docClient = getDocClient();
195
- const tableName = getTableName();
196
- const command = new libDynamodb.GetCommand({
197
- Key: { id, model },
198
- TableName: tableName,
199
- });
200
- const response = await docClient.send(command);
201
- return response.Item ?? null;
198
+ function calculateEntitySuffix(entity) {
199
+ const hasArchived = Boolean(entity.archivedAt);
200
+ const hasDeleted = Boolean(entity.deletedAt);
201
+ if (hasArchived && hasDeleted) {
202
+ return ARCHIVED_SUFFIX + DELETED_SUFFIX;
203
+ }
204
+ if (hasArchived) {
205
+ return ARCHIVED_SUFFIX;
206
+ }
207
+ if (hasDeleted) {
208
+ return DELETED_SUFFIX;
209
+ }
210
+ return "";
202
211
  }
212
+ /**
213
+ * Get a single entity by primary key
214
+ */
215
+ const getEntity = vocabulary.serviceHandler({
216
+ alias: "getEntity",
217
+ description: "Get a single entity by primary key",
218
+ input: {
219
+ id: { type: String, description: "Entity ID (sort key)" },
220
+ model: { type: String, description: "Entity model (partition key)" },
221
+ },
222
+ service: async ({ id, model }) => {
223
+ const docClient = getDocClient();
224
+ const tableName = getTableName();
225
+ const command = new libDynamodb.GetCommand({
226
+ Key: { id, model },
227
+ TableName: tableName,
228
+ });
229
+ const response = await docClient.send(command);
230
+ return response.Item ?? null;
231
+ },
232
+ });
203
233
  /**
204
234
  * Put (create or replace) an entity
205
235
  * Auto-populates GSI index keys via indexEntity
236
+ *
237
+ * Note: This is a regular async function (not serviceHandler) because it accepts
238
+ * complex FabricEntity objects that can't be coerced by vocabulary's type system.
206
239
  */
207
- async function putEntity(params) {
208
- const { entity } = params;
240
+ async function putEntity({ entity, }) {
209
241
  const docClient = getDocClient();
210
242
  const tableName = getTableName();
211
243
  // Auto-populate index keys
@@ -220,9 +252,11 @@ async function putEntity(params) {
220
252
  /**
221
253
  * Update an existing entity
222
254
  * Auto-populates GSI index keys and sets updatedAt
255
+ *
256
+ * Note: This is a regular async function (not serviceHandler) because it accepts
257
+ * complex FabricEntity objects that can't be coerced by vocabulary's type system.
223
258
  */
224
- async function updateEntity(params) {
225
- const { entity } = params;
259
+ async function updateEntity({ entity, }) {
226
260
  const docClient = getDocClient();
227
261
  const tableName = getTableName();
228
262
  // Update timestamp and re-index
@@ -237,98 +271,102 @@ async function updateEntity(params) {
237
271
  await docClient.send(command);
238
272
  return updatedEntity;
239
273
  }
240
- /**
241
- * Calculate suffix based on entity's archived/deleted state
242
- */
243
- function calculateEntitySuffix(entity) {
244
- const hasArchived = Boolean(entity.archivedAt);
245
- const hasDeleted = Boolean(entity.deletedAt);
246
- if (hasArchived && hasDeleted) {
247
- return ARCHIVED_SUFFIX + DELETED_SUFFIX;
248
- }
249
- if (hasArchived) {
250
- return ARCHIVED_SUFFIX;
251
- }
252
- if (hasDeleted) {
253
- return DELETED_SUFFIX;
254
- }
255
- return "";
256
- }
257
274
  /**
258
275
  * Soft delete an entity by setting deletedAt timestamp
259
276
  * Re-indexes with appropriate suffix based on archived/deleted state
260
277
  */
261
- async function deleteEntity(params) {
262
- const { id, model } = params;
263
- const docClient = getDocClient();
264
- const tableName = getTableName();
265
- // Fetch the current entity
266
- const existing = await getEntity({ id, model });
267
- if (!existing) {
268
- return false;
269
- }
270
- const now = new Date().toISOString();
271
- // Build updated entity with deletedAt
272
- const updatedEntity = {
273
- ...existing,
274
- deletedAt: now,
275
- updatedAt: now,
276
- };
277
- // Calculate suffix based on combined state (may already be archived)
278
- const suffix = calculateEntitySuffix(updatedEntity);
279
- const deletedEntity = indexEntity(updatedEntity, suffix);
280
- const command = new libDynamodb.PutCommand({
281
- Item: deletedEntity,
282
- TableName: tableName,
283
- });
284
- await docClient.send(command);
285
- return true;
286
- }
278
+ const deleteEntity = vocabulary.serviceHandler({
279
+ alias: "deleteEntity",
280
+ description: "Soft delete an entity (sets deletedAt timestamp)",
281
+ input: {
282
+ id: { type: String, description: "Entity ID (sort key)" },
283
+ model: { type: String, description: "Entity model (partition key)" },
284
+ },
285
+ service: async ({ id, model }) => {
286
+ const docClient = getDocClient();
287
+ const tableName = getTableName();
288
+ // Fetch the current entity
289
+ const existing = await getEntity({ id, model });
290
+ if (!existing) {
291
+ return false;
292
+ }
293
+ const now = new Date().toISOString();
294
+ // Build updated entity with deletedAt
295
+ const updatedEntity = {
296
+ ...existing,
297
+ deletedAt: now,
298
+ updatedAt: now,
299
+ };
300
+ // Calculate suffix based on combined state (may already be archived)
301
+ const suffix = calculateEntitySuffix(updatedEntity);
302
+ const deletedEntity = indexEntity(updatedEntity, suffix);
303
+ const command = new libDynamodb.PutCommand({
304
+ Item: deletedEntity,
305
+ TableName: tableName,
306
+ });
307
+ await docClient.send(command);
308
+ return true;
309
+ },
310
+ });
287
311
  /**
288
312
  * Archive an entity by setting archivedAt timestamp
289
313
  * Re-indexes with appropriate suffix based on archived/deleted state
290
314
  */
291
- async function archiveEntity(params) {
292
- const { id, model } = params;
293
- const docClient = getDocClient();
294
- const tableName = getTableName();
295
- // Fetch the current entity
296
- const existing = await getEntity({ id, model });
297
- if (!existing) {
298
- return false;
299
- }
300
- const now = new Date().toISOString();
301
- // Build updated entity with archivedAt
302
- const updatedEntity = {
303
- ...existing,
304
- archivedAt: now,
305
- updatedAt: now,
306
- };
307
- // Calculate suffix based on combined state (may already be deleted)
308
- const suffix = calculateEntitySuffix(updatedEntity);
309
- const archivedEntity = indexEntity(updatedEntity, suffix);
310
- const command = new libDynamodb.PutCommand({
311
- Item: archivedEntity,
312
- TableName: tableName,
313
- });
314
- await docClient.send(command);
315
- return true;
316
- }
315
+ const archiveEntity = vocabulary.serviceHandler({
316
+ alias: "archiveEntity",
317
+ description: "Archive an entity (sets archivedAt timestamp)",
318
+ input: {
319
+ id: { type: String, description: "Entity ID (sort key)" },
320
+ model: { type: String, description: "Entity model (partition key)" },
321
+ },
322
+ service: async ({ id, model }) => {
323
+ const docClient = getDocClient();
324
+ const tableName = getTableName();
325
+ // Fetch the current entity
326
+ const existing = await getEntity({ id, model });
327
+ if (!existing) {
328
+ return false;
329
+ }
330
+ const now = new Date().toISOString();
331
+ // Build updated entity with archivedAt
332
+ const updatedEntity = {
333
+ ...existing,
334
+ archivedAt: now,
335
+ updatedAt: now,
336
+ };
337
+ // Calculate suffix based on combined state (may already be deleted)
338
+ const suffix = calculateEntitySuffix(updatedEntity);
339
+ const archivedEntity = indexEntity(updatedEntity, suffix);
340
+ const command = new libDynamodb.PutCommand({
341
+ Item: archivedEntity,
342
+ TableName: tableName,
343
+ });
344
+ await docClient.send(command);
345
+ return true;
346
+ },
347
+ });
317
348
  /**
318
349
  * Hard delete an entity (permanently removes from table)
319
350
  * Use with caution - prefer deleteEntity for soft delete
320
351
  */
321
- async function destroyEntity(params) {
322
- const { id, model } = params;
323
- const docClient = getDocClient();
324
- const tableName = getTableName();
325
- const command = new libDynamodb.DeleteCommand({
326
- Key: { id, model },
327
- TableName: tableName,
328
- });
329
- await docClient.send(command);
330
- return true;
331
- }
352
+ const destroyEntity = vocabulary.serviceHandler({
353
+ alias: "destroyEntity",
354
+ description: "Hard delete an entity (permanently removes from table)",
355
+ input: {
356
+ id: { type: String, description: "Entity ID (sort key)" },
357
+ model: { type: String, description: "Entity model (partition key)" },
358
+ },
359
+ service: async ({ id, model }) => {
360
+ const docClient = getDocClient();
361
+ const tableName = getTableName();
362
+ const command = new libDynamodb.DeleteCommand({
363
+ Key: { id, model },
364
+ TableName: tableName,
365
+ });
366
+ await docClient.send(command);
367
+ return true;
368
+ },
369
+ });
332
370
 
333
371
  /**
334
372
  * Calculate the suffix based on archived/deleted flags
@@ -377,76 +415,126 @@ async function executeQuery(indexName, keyValue, options = {}) {
377
415
  * Query entities by organizational unit (parent hierarchy)
378
416
  * Uses indexOu GSI
379
417
  *
380
- * @param params.archived - Query archived entities instead of active ones
381
- * @param params.deleted - Query deleted entities instead of active ones
382
- * @throws ConfigurationError if both archived and deleted are true
418
+ * Note: This is a regular async function (not serviceHandler) because it accepts
419
+ * complex startKey objects that can't be coerced by vocabulary's type system.
383
420
  */
384
- async function queryByOu(params) {
385
- const { archived, deleted, model, ou, ...options } = params;
421
+ async function queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
386
422
  const suffix = calculateSuffix({ archived, deleted });
387
423
  const keyValue = buildIndexOu(ou, model) + suffix;
388
- return executeQuery(INDEX_OU, keyValue, options);
424
+ return executeQuery(INDEX_OU, keyValue, {
425
+ ascending,
426
+ limit,
427
+ startKey,
428
+ });
389
429
  }
390
430
  /**
391
431
  * Query a single entity by human-friendly alias
392
432
  * Uses indexAlias GSI
393
- *
394
- * @param params.archived - Query archived entities instead of active ones
395
- * @param params.deleted - Query deleted entities instead of active ones
396
- * @throws ConfigurationError if both archived and deleted are true
397
- * @returns The matching entity or null if not found
398
433
  */
399
- async function queryByAlias(params) {
400
- const { alias, archived, deleted, model, ou } = params;
401
- const suffix = calculateSuffix({ archived, deleted });
402
- const keyValue = buildIndexAlias(ou, model, alias) + suffix;
403
- const result = await executeQuery(INDEX_ALIAS, keyValue, { limit: 1 });
404
- return result.items[0] ?? null;
405
- }
434
+ const queryByAlias = vocabulary.serviceHandler({
435
+ alias: "queryByAlias",
436
+ description: "Query a single entity by human-friendly alias",
437
+ input: {
438
+ alias: { type: String, description: "Human-friendly alias" },
439
+ archived: {
440
+ type: Boolean,
441
+ default: false,
442
+ required: false,
443
+ description: "Query archived entities instead of active ones",
444
+ },
445
+ deleted: {
446
+ type: Boolean,
447
+ default: false,
448
+ required: false,
449
+ description: "Query deleted entities instead of active ones",
450
+ },
451
+ model: { type: String, description: "Entity model name" },
452
+ ou: { type: String, description: "Organizational unit (@ for root)" },
453
+ },
454
+ service: async ({ alias, archived, deleted, model, ou, }) => {
455
+ const aliasStr = alias;
456
+ const archivedBool = archived;
457
+ const deletedBool = deleted;
458
+ const modelStr = model;
459
+ const ouStr = ou;
460
+ const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
461
+ const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
462
+ const result = await executeQuery(INDEX_ALIAS, keyValue, {
463
+ limit: 1,
464
+ });
465
+ return result.items[0] ?? null;
466
+ },
467
+ });
406
468
  /**
407
469
  * Query entities by category classification
408
470
  * Uses indexClass GSI
409
471
  *
410
- * @param params.archived - Query archived entities instead of active ones
411
- * @param params.deleted - Query deleted entities instead of active ones
412
- * @throws ConfigurationError if both archived and deleted are true
472
+ * Note: This is a regular async function (not serviceHandler) because it accepts
473
+ * complex startKey objects that can't be coerced by vocabulary's type system.
413
474
  */
414
- async function queryByClass(params) {
415
- const { archived, deleted, model, ou, recordClass, ...options } = params;
475
+ async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, ou, recordClass, startKey, }) {
416
476
  const suffix = calculateSuffix({ archived, deleted });
417
477
  const keyValue = buildIndexClass(ou, model, recordClass) + suffix;
418
- return executeQuery(INDEX_CLASS, keyValue, options);
478
+ return executeQuery(INDEX_CLASS, keyValue, {
479
+ ascending,
480
+ limit,
481
+ startKey,
482
+ });
419
483
  }
420
484
  /**
421
485
  * Query entities by type classification
422
486
  * Uses indexType GSI
423
487
  *
424
- * @param params.archived - Query archived entities instead of active ones
425
- * @param params.deleted - Query deleted entities instead of active ones
426
- * @throws ConfigurationError if both archived and deleted are true
488
+ * Note: This is a regular async function (not serviceHandler) because it accepts
489
+ * complex startKey objects that can't be coerced by vocabulary's type system.
427
490
  */
428
- async function queryByType(params) {
429
- const { archived, deleted, model, ou, type, ...options } = params;
491
+ async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, type, }) {
430
492
  const suffix = calculateSuffix({ archived, deleted });
431
493
  const keyValue = buildIndexType(ou, model, type) + suffix;
432
- return executeQuery(INDEX_TYPE, keyValue, options);
494
+ return executeQuery(INDEX_TYPE, keyValue, {
495
+ ascending,
496
+ limit,
497
+ startKey,
498
+ });
433
499
  }
434
500
  /**
435
501
  * Query a single entity by external ID
436
502
  * Uses indexXid GSI
437
- *
438
- * @param params.archived - Query archived entities instead of active ones
439
- * @param params.deleted - Query deleted entities instead of active ones
440
- * @throws ConfigurationError if both archived and deleted are true
441
- * @returns The matching entity or null if not found
442
503
  */
443
- async function queryByXid(params) {
444
- const { archived, deleted, model, ou, xid } = params;
445
- const suffix = calculateSuffix({ archived, deleted });
446
- const keyValue = buildIndexXid(ou, model, xid) + suffix;
447
- const result = await executeQuery(INDEX_XID, keyValue, { limit: 1 });
448
- return result.items[0] ?? null;
449
- }
504
+ const queryByXid = vocabulary.serviceHandler({
505
+ alias: "queryByXid",
506
+ description: "Query a single entity by external ID",
507
+ input: {
508
+ archived: {
509
+ type: Boolean,
510
+ default: false,
511
+ required: false,
512
+ description: "Query archived entities instead of active ones",
513
+ },
514
+ deleted: {
515
+ type: Boolean,
516
+ default: false,
517
+ required: false,
518
+ description: "Query deleted entities instead of active ones",
519
+ },
520
+ model: { type: String, description: "Entity model name" },
521
+ ou: { type: String, description: "Organizational unit (@ for root)" },
522
+ xid: { type: String, description: "External ID" },
523
+ },
524
+ service: async ({ archived, deleted, model, ou, xid, }) => {
525
+ const archivedBool = archived;
526
+ const deletedBool = deleted;
527
+ const modelStr = model;
528
+ const ouStr = ou;
529
+ const xidStr = xid;
530
+ const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
531
+ const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
532
+ const result = await executeQuery(INDEX_XID, keyValue, {
533
+ limit: 1,
534
+ });
535
+ return result.items[0] ?? null;
536
+ },
537
+ });
450
538
 
451
539
  exports.APEX = APEX;
452
540
  exports.ARCHIVED_SUFFIX = ARCHIVED_SUFFIX;