@jaypie/dynamodb 0.0.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.
@@ -0,0 +1,450 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { DynamoDBDocumentClient, PutCommand, DeleteCommand, GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
3
+ import { ConfigurationError } from '@jaypie/errors';
4
+
5
+ const DEFAULT_REGION = "us-east-1";
6
+ const LOCAL_CREDENTIALS = {
7
+ accessKeyId: "local",
8
+ secretAccessKey: "local",
9
+ };
10
+ // Module-level state
11
+ let docClient = null;
12
+ let tableName = null;
13
+ /**
14
+ * Check if endpoint indicates local development mode
15
+ */
16
+ function isLocalEndpoint(endpoint) {
17
+ if (!endpoint)
18
+ return false;
19
+ return endpoint.includes("127.0.0.1") || endpoint.includes("localhost");
20
+ }
21
+ /**
22
+ * Initialize the DynamoDB client
23
+ * Must be called once at application startup before using query functions
24
+ *
25
+ * @param config - Client configuration
26
+ */
27
+ function initClient(config) {
28
+ const { endpoint, region = DEFAULT_REGION } = config;
29
+ // Auto-detect local mode and use dummy credentials
30
+ const credentials = config.credentials ??
31
+ (isLocalEndpoint(endpoint) ? LOCAL_CREDENTIALS : undefined);
32
+ const dynamoClient = new DynamoDBClient({
33
+ ...(credentials && { credentials }),
34
+ ...(endpoint && { endpoint }),
35
+ region,
36
+ });
37
+ docClient = DynamoDBDocumentClient.from(dynamoClient, {
38
+ marshallOptions: {
39
+ removeUndefinedValues: true,
40
+ },
41
+ });
42
+ tableName = config.tableName;
43
+ }
44
+ /**
45
+ * Get the initialized DynamoDB Document Client
46
+ * @throws ConfigurationError if client has not been initialized
47
+ */
48
+ function getDocClient() {
49
+ if (!docClient) {
50
+ throw new ConfigurationError("DynamoDB client not initialized. Call initClient() first.");
51
+ }
52
+ return docClient;
53
+ }
54
+ /**
55
+ * Get the configured table name
56
+ * @throws ConfigurationError if client has not been initialized
57
+ */
58
+ function getTableName() {
59
+ if (!tableName) {
60
+ throw new ConfigurationError("DynamoDB client not initialized. Call initClient() first.");
61
+ }
62
+ return tableName;
63
+ }
64
+ /**
65
+ * Check if the client has been initialized
66
+ */
67
+ function isInitialized() {
68
+ return docClient !== null && tableName !== null;
69
+ }
70
+ /**
71
+ * Reset the client state (primarily for testing)
72
+ */
73
+ function resetClient() {
74
+ docClient = null;
75
+ tableName = null;
76
+ }
77
+
78
+ // Primary markers
79
+ const APEX = "@"; // Root-level marker (DynamoDB prohibits empty strings)
80
+ const SEPARATOR = "#"; // Composite key separator
81
+ // GSI names
82
+ const INDEX_ALIAS = "indexAlias";
83
+ const INDEX_CLASS = "indexClass";
84
+ const INDEX_OU = "indexOu";
85
+ const INDEX_TYPE = "indexType";
86
+ const INDEX_XID = "indexXid";
87
+ // Index suffixes for soft state
88
+ const ARCHIVED_SUFFIX = "#archived";
89
+ const DELETED_SUFFIX = "#deleted";
90
+
91
+ /**
92
+ * Build the indexOu key for hierarchical queries
93
+ * @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
94
+ * @param model - The entity model name
95
+ * @returns Composite key: "{ou}#{model}"
96
+ */
97
+ function buildIndexOu(ou, model) {
98
+ return `${ou}${SEPARATOR}${model}`;
99
+ }
100
+ /**
101
+ * Build the indexAlias key for human-friendly lookups
102
+ * @param ou - The organizational unit
103
+ * @param model - The entity model name
104
+ * @param alias - The human-friendly alias
105
+ * @returns Composite key: "{ou}#{model}#{alias}"
106
+ */
107
+ function buildIndexAlias(ou, model, alias) {
108
+ return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;
109
+ }
110
+ /**
111
+ * Build the indexClass key for category filtering
112
+ * @param ou - The organizational unit
113
+ * @param model - The entity model name
114
+ * @param recordClass - The category classification
115
+ * @returns Composite key: "{ou}#{model}#{class}"
116
+ */
117
+ function buildIndexClass(ou, model, recordClass) {
118
+ return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;
119
+ }
120
+ /**
121
+ * Build the indexType key for type filtering
122
+ * @param ou - The organizational unit
123
+ * @param model - The entity model name
124
+ * @param type - The type classification
125
+ * @returns Composite key: "{ou}#{model}#{type}"
126
+ */
127
+ function buildIndexType(ou, model, type) {
128
+ return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;
129
+ }
130
+ /**
131
+ * Build the indexXid key for external ID lookups
132
+ * @param ou - The organizational unit
133
+ * @param model - The entity model name
134
+ * @param xid - The external ID
135
+ * @returns Composite key: "{ou}#{model}#{xid}"
136
+ */
137
+ function buildIndexXid(ou, model, xid) {
138
+ return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
139
+ }
140
+ /**
141
+ * Calculate the organizational unit from a parent reference
142
+ * @param parent - Optional parent entity reference
143
+ * @returns APEX ("@") if no parent, otherwise "{parent.model}#{parent.id}"
144
+ */
145
+ function calculateOu(parent) {
146
+ if (!parent) {
147
+ return APEX;
148
+ }
149
+ return `${parent.model}${SEPARATOR}${parent.id}`;
150
+ }
151
+ /**
152
+ * Auto-populate GSI index keys on an entity
153
+ * - indexOu is always populated from ou + model
154
+ * - indexAlias is populated only when alias is present
155
+ * - indexClass is populated only when class is present
156
+ * - indexType is populated only when type is present
157
+ * - indexXid is populated only when xid is present
158
+ *
159
+ * @param entity - The entity to populate index keys for
160
+ * @param suffix - Optional suffix to append to all index keys (e.g., "#deleted", "#archived")
161
+ * @returns The entity with populated index keys
162
+ */
163
+ function indexEntity(entity, suffix = "") {
164
+ const result = { ...entity };
165
+ // indexOu is always set (from ou + model)
166
+ result.indexOu = buildIndexOu(entity.ou, entity.model) + suffix;
167
+ // Optional indexes - only set when the source field is present
168
+ if (entity.alias !== undefined) {
169
+ result.indexAlias =
170
+ buildIndexAlias(entity.ou, entity.model, entity.alias) + suffix;
171
+ }
172
+ if (entity.class !== undefined) {
173
+ result.indexClass =
174
+ buildIndexClass(entity.ou, entity.model, entity.class) + suffix;
175
+ }
176
+ if (entity.type !== undefined) {
177
+ result.indexType =
178
+ buildIndexType(entity.ou, entity.model, entity.type) + suffix;
179
+ }
180
+ if (entity.xid !== undefined) {
181
+ result.indexXid =
182
+ buildIndexXid(entity.ou, entity.model, entity.xid) + suffix;
183
+ }
184
+ return result;
185
+ }
186
+
187
+ /**
188
+ * Get a single entity by primary key
189
+ */
190
+ async function getEntity(params) {
191
+ const { id, model } = params;
192
+ const docClient = getDocClient();
193
+ const tableName = getTableName();
194
+ const command = new GetCommand({
195
+ Key: { id, model },
196
+ TableName: tableName,
197
+ });
198
+ const response = await docClient.send(command);
199
+ return response.Item ?? null;
200
+ }
201
+ /**
202
+ * Put (create or replace) an entity
203
+ * Auto-populates GSI index keys via indexEntity
204
+ */
205
+ async function putEntity(params) {
206
+ const { entity } = params;
207
+ const docClient = getDocClient();
208
+ const tableName = getTableName();
209
+ // Auto-populate index keys
210
+ const indexedEntity = indexEntity(entity);
211
+ const command = new PutCommand({
212
+ Item: indexedEntity,
213
+ TableName: tableName,
214
+ });
215
+ await docClient.send(command);
216
+ return indexedEntity;
217
+ }
218
+ /**
219
+ * Update an existing entity
220
+ * Auto-populates GSI index keys and sets updatedAt
221
+ */
222
+ async function updateEntity(params) {
223
+ const { entity } = params;
224
+ const docClient = getDocClient();
225
+ const tableName = getTableName();
226
+ // Update timestamp and re-index
227
+ const updatedEntity = indexEntity({
228
+ ...entity,
229
+ updatedAt: new Date().toISOString(),
230
+ });
231
+ const command = new PutCommand({
232
+ Item: updatedEntity,
233
+ TableName: tableName,
234
+ });
235
+ await docClient.send(command);
236
+ return updatedEntity;
237
+ }
238
+ /**
239
+ * Calculate suffix based on entity's archived/deleted state
240
+ */
241
+ function calculateEntitySuffix(entity) {
242
+ const hasArchived = Boolean(entity.archivedAt);
243
+ const hasDeleted = Boolean(entity.deletedAt);
244
+ if (hasArchived && hasDeleted) {
245
+ return ARCHIVED_SUFFIX + DELETED_SUFFIX;
246
+ }
247
+ if (hasArchived) {
248
+ return ARCHIVED_SUFFIX;
249
+ }
250
+ if (hasDeleted) {
251
+ return DELETED_SUFFIX;
252
+ }
253
+ return "";
254
+ }
255
+ /**
256
+ * Soft delete an entity by setting deletedAt timestamp
257
+ * Re-indexes with appropriate suffix based on archived/deleted state
258
+ */
259
+ async function deleteEntity(params) {
260
+ const { id, model } = params;
261
+ const docClient = getDocClient();
262
+ const tableName = getTableName();
263
+ // Fetch the current entity
264
+ const existing = await getEntity({ id, model });
265
+ if (!existing) {
266
+ return false;
267
+ }
268
+ const now = new Date().toISOString();
269
+ // Build updated entity with deletedAt
270
+ const updatedEntity = {
271
+ ...existing,
272
+ deletedAt: now,
273
+ updatedAt: now,
274
+ };
275
+ // Calculate suffix based on combined state (may already be archived)
276
+ const suffix = calculateEntitySuffix(updatedEntity);
277
+ const deletedEntity = indexEntity(updatedEntity, suffix);
278
+ const command = new PutCommand({
279
+ Item: deletedEntity,
280
+ TableName: tableName,
281
+ });
282
+ await docClient.send(command);
283
+ return true;
284
+ }
285
+ /**
286
+ * Archive an entity by setting archivedAt timestamp
287
+ * Re-indexes with appropriate suffix based on archived/deleted state
288
+ */
289
+ async function archiveEntity(params) {
290
+ const { id, model } = params;
291
+ const docClient = getDocClient();
292
+ const tableName = getTableName();
293
+ // Fetch the current entity
294
+ const existing = await getEntity({ id, model });
295
+ if (!existing) {
296
+ return false;
297
+ }
298
+ const now = new Date().toISOString();
299
+ // Build updated entity with archivedAt
300
+ const updatedEntity = {
301
+ ...existing,
302
+ archivedAt: now,
303
+ updatedAt: now,
304
+ };
305
+ // Calculate suffix based on combined state (may already be deleted)
306
+ const suffix = calculateEntitySuffix(updatedEntity);
307
+ const archivedEntity = indexEntity(updatedEntity, suffix);
308
+ const command = new PutCommand({
309
+ Item: archivedEntity,
310
+ TableName: tableName,
311
+ });
312
+ await docClient.send(command);
313
+ return true;
314
+ }
315
+ /**
316
+ * Hard delete an entity (permanently removes from table)
317
+ * Use with caution - prefer deleteEntity for soft delete
318
+ */
319
+ async function destroyEntity(params) {
320
+ const { id, model } = params;
321
+ const docClient = getDocClient();
322
+ const tableName = getTableName();
323
+ const command = new DeleteCommand({
324
+ Key: { id, model },
325
+ TableName: tableName,
326
+ });
327
+ await docClient.send(command);
328
+ return true;
329
+ }
330
+
331
+ /**
332
+ * Calculate the suffix based on archived/deleted flags
333
+ * When both are true, returns combined suffix (archived first, alphabetically)
334
+ */
335
+ function calculateSuffix({ archived, deleted, }) {
336
+ if (archived && deleted) {
337
+ return ARCHIVED_SUFFIX + DELETED_SUFFIX;
338
+ }
339
+ if (archived) {
340
+ return ARCHIVED_SUFFIX;
341
+ }
342
+ if (deleted) {
343
+ return DELETED_SUFFIX;
344
+ }
345
+ return "";
346
+ }
347
+ /**
348
+ * Execute a GSI query with common options
349
+ */
350
+ async function executeQuery(indexName, keyValue, options = {}) {
351
+ const { ascending = false, limit, startKey } = options;
352
+ const docClient = getDocClient();
353
+ const tableName = getTableName();
354
+ const command = new QueryCommand({
355
+ ExclusiveStartKey: startKey,
356
+ ExpressionAttributeNames: {
357
+ "#pk": indexName,
358
+ },
359
+ ExpressionAttributeValues: {
360
+ ":pkValue": keyValue,
361
+ },
362
+ IndexName: indexName,
363
+ KeyConditionExpression: "#pk = :pkValue",
364
+ ...(limit && { Limit: limit }),
365
+ ScanIndexForward: ascending,
366
+ TableName: tableName,
367
+ });
368
+ const response = await docClient.send(command);
369
+ return {
370
+ items: (response.Items ?? []),
371
+ lastEvaluatedKey: response.LastEvaluatedKey,
372
+ };
373
+ }
374
+ /**
375
+ * Query entities by organizational unit (parent hierarchy)
376
+ * Uses indexOu GSI
377
+ *
378
+ * @param params.archived - Query archived entities instead of active ones
379
+ * @param params.deleted - Query deleted entities instead of active ones
380
+ * @throws ConfigurationError if both archived and deleted are true
381
+ */
382
+ async function queryByOu(params) {
383
+ const { archived, deleted, model, ou, ...options } = params;
384
+ const suffix = calculateSuffix({ archived, deleted });
385
+ const keyValue = buildIndexOu(ou, model) + suffix;
386
+ return executeQuery(INDEX_OU, keyValue, options);
387
+ }
388
+ /**
389
+ * Query a single entity by human-friendly alias
390
+ * Uses indexAlias GSI
391
+ *
392
+ * @param params.archived - Query archived entities instead of active ones
393
+ * @param params.deleted - Query deleted entities instead of active ones
394
+ * @throws ConfigurationError if both archived and deleted are true
395
+ * @returns The matching entity or null if not found
396
+ */
397
+ async function queryByAlias(params) {
398
+ const { alias, archived, deleted, model, ou } = params;
399
+ const suffix = calculateSuffix({ archived, deleted });
400
+ const keyValue = buildIndexAlias(ou, model, alias) + suffix;
401
+ const result = await executeQuery(INDEX_ALIAS, keyValue, { limit: 1 });
402
+ return result.items[0] ?? null;
403
+ }
404
+ /**
405
+ * Query entities by category classification
406
+ * Uses indexClass GSI
407
+ *
408
+ * @param params.archived - Query archived entities instead of active ones
409
+ * @param params.deleted - Query deleted entities instead of active ones
410
+ * @throws ConfigurationError if both archived and deleted are true
411
+ */
412
+ async function queryByClass(params) {
413
+ const { archived, deleted, model, ou, recordClass, ...options } = params;
414
+ const suffix = calculateSuffix({ archived, deleted });
415
+ const keyValue = buildIndexClass(ou, model, recordClass) + suffix;
416
+ return executeQuery(INDEX_CLASS, keyValue, options);
417
+ }
418
+ /**
419
+ * Query entities by type classification
420
+ * Uses indexType GSI
421
+ *
422
+ * @param params.archived - Query archived entities instead of active ones
423
+ * @param params.deleted - Query deleted entities instead of active ones
424
+ * @throws ConfigurationError if both archived and deleted are true
425
+ */
426
+ async function queryByType(params) {
427
+ const { archived, deleted, model, ou, type, ...options } = params;
428
+ const suffix = calculateSuffix({ archived, deleted });
429
+ const keyValue = buildIndexType(ou, model, type) + suffix;
430
+ return executeQuery(INDEX_TYPE, keyValue, options);
431
+ }
432
+ /**
433
+ * Query a single entity by external ID
434
+ * Uses indexXid GSI
435
+ *
436
+ * @param params.archived - Query archived entities instead of active ones
437
+ * @param params.deleted - Query deleted entities instead of active ones
438
+ * @throws ConfigurationError if both archived and deleted are true
439
+ * @returns The matching entity or null if not found
440
+ */
441
+ async function queryByXid(params) {
442
+ const { archived, deleted, model, ou, xid } = params;
443
+ const suffix = calculateSuffix({ archived, deleted });
444
+ const keyValue = buildIndexXid(ou, model, xid) + suffix;
445
+ const result = await executeQuery(INDEX_XID, keyValue, { limit: 1 });
446
+ return result.items[0] ?? null;
447
+ }
448
+
449
+ export { APEX, ARCHIVED_SUFFIX, DELETED_SUFFIX, INDEX_ALIAS, INDEX_CLASS, INDEX_OU, INDEX_TYPE, INDEX_XID, SEPARATOR, archiveEntity, buildIndexAlias, buildIndexClass, buildIndexOu, buildIndexType, buildIndexXid, calculateOu, deleteEntity, destroyEntity, getDocClient, getEntity, getTableName, indexEntity, initClient, isInitialized, putEntity, queryByAlias, queryByClass, queryByOu, queryByType, queryByXid, resetClient, updateEntity };
450
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../src/client.ts","../../../src/constants.ts","../../../src/keyBuilders.ts","../../../src/entities.ts","../../../src/queries.ts"],"sourcesContent":["import { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\nimport { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\nimport { ConfigurationError } from \"@jaypie/errors\";\n\nimport type { DynamoClientConfig } from \"./types.js\";\n\nconst DEFAULT_REGION = \"us-east-1\";\nconst LOCAL_CREDENTIALS = {\n accessKeyId: \"local\",\n secretAccessKey: \"local\",\n};\n\n// Module-level state\nlet docClient: DynamoDBDocumentClient | null = null;\nlet tableName: string | null = null;\n\n/**\n * Check if endpoint indicates local development mode\n */\nfunction isLocalEndpoint(endpoint?: string): boolean {\n if (!endpoint) return false;\n return endpoint.includes(\"127.0.0.1\") || endpoint.includes(\"localhost\");\n}\n\n/**\n * Initialize the DynamoDB client\n * Must be called once at application startup before using query functions\n *\n * @param config - Client configuration\n */\nexport function initClient(config: DynamoClientConfig): void {\n const { endpoint, region = DEFAULT_REGION } = config;\n\n // Auto-detect local mode and use dummy credentials\n const credentials =\n config.credentials ??\n (isLocalEndpoint(endpoint) ? LOCAL_CREDENTIALS : undefined);\n\n const dynamoClient = new DynamoDBClient({\n ...(credentials && { credentials }),\n ...(endpoint && { endpoint }),\n region,\n });\n\n docClient = DynamoDBDocumentClient.from(dynamoClient, {\n marshallOptions: {\n removeUndefinedValues: true,\n },\n });\n\n tableName = config.tableName;\n}\n\n/**\n * Get the initialized DynamoDB Document Client\n * @throws ConfigurationError if client has not been initialized\n */\nexport function getDocClient(): DynamoDBDocumentClient {\n if (!docClient) {\n throw new ConfigurationError(\n \"DynamoDB client not initialized. Call initClient() first.\",\n );\n }\n return docClient;\n}\n\n/**\n * Get the configured table name\n * @throws ConfigurationError if client has not been initialized\n */\nexport function getTableName(): string {\n if (!tableName) {\n throw new ConfigurationError(\n \"DynamoDB client not initialized. Call initClient() first.\",\n );\n }\n return tableName;\n}\n\n/**\n * Check if the client has been initialized\n */\nexport function isInitialized(): boolean {\n return docClient !== null && tableName !== null;\n}\n\n/**\n * Reset the client state (primarily for testing)\n */\nexport function resetClient(): void {\n docClient = null;\n tableName = null;\n}\n","// Primary markers\nexport const APEX = \"@\"; // Root-level marker (DynamoDB prohibits empty strings)\nexport const SEPARATOR = \"#\"; // Composite key separator\n\n// GSI names\nexport const INDEX_ALIAS = \"indexAlias\";\nexport const INDEX_CLASS = \"indexClass\";\nexport const INDEX_OU = \"indexOu\";\nexport const INDEX_TYPE = \"indexType\";\nexport const INDEX_XID = \"indexXid\";\n\n// Index suffixes for soft state\nexport const ARCHIVED_SUFFIX = \"#archived\";\nexport const DELETED_SUFFIX = \"#deleted\";\n","import { APEX, SEPARATOR } from \"./constants.js\";\nimport type { FabricEntity, ParentReference } from \"./types.js\";\n\n/**\n * Build the indexOu key for hierarchical queries\n * @param ou - The organizational unit (APEX or \"{parent.model}#{parent.id}\")\n * @param model - The entity model name\n * @returns Composite key: \"{ou}#{model}\"\n */\nexport function buildIndexOu(ou: string, model: string): string {\n return `${ou}${SEPARATOR}${model}`;\n}\n\n/**\n * Build the indexAlias key for human-friendly lookups\n * @param ou - The organizational unit\n * @param model - The entity model name\n * @param alias - The human-friendly alias\n * @returns Composite key: \"{ou}#{model}#{alias}\"\n */\nexport function buildIndexAlias(\n ou: string,\n model: string,\n alias: string,\n): string {\n return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;\n}\n\n/**\n * Build the indexClass key for category filtering\n * @param ou - The organizational unit\n * @param model - The entity model name\n * @param recordClass - The category classification\n * @returns Composite key: \"{ou}#{model}#{class}\"\n */\nexport function buildIndexClass(\n ou: string,\n model: string,\n recordClass: string,\n): string {\n return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;\n}\n\n/**\n * Build the indexType key for type filtering\n * @param ou - The organizational unit\n * @param model - The entity model name\n * @param type - The type classification\n * @returns Composite key: \"{ou}#{model}#{type}\"\n */\nexport function buildIndexType(\n ou: string,\n model: string,\n type: string,\n): string {\n return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;\n}\n\n/**\n * Build the indexXid key for external ID lookups\n * @param ou - The organizational unit\n * @param model - The entity model name\n * @param xid - The external ID\n * @returns Composite key: \"{ou}#{model}#{xid}\"\n */\nexport function buildIndexXid(ou: string, model: string, xid: string): string {\n return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;\n}\n\n/**\n * Calculate the organizational unit from a parent reference\n * @param parent - Optional parent entity reference\n * @returns APEX (\"@\") if no parent, otherwise \"{parent.model}#{parent.id}\"\n */\nexport function calculateOu(parent?: ParentReference): string {\n if (!parent) {\n return APEX;\n }\n return `${parent.model}${SEPARATOR}${parent.id}`;\n}\n\n/**\n * Auto-populate GSI index keys on an entity\n * - indexOu is always populated from ou + model\n * - indexAlias is populated only when alias is present\n * - indexClass is populated only when class is present\n * - indexType is populated only when type is present\n * - indexXid is populated only when xid is present\n *\n * @param entity - The entity to populate index keys for\n * @param suffix - Optional suffix to append to all index keys (e.g., \"#deleted\", \"#archived\")\n * @returns The entity with populated index keys\n */\nexport function indexEntity<T extends FabricEntity>(\n entity: T,\n suffix: string = \"\",\n): T {\n const result = { ...entity };\n\n // indexOu is always set (from ou + model)\n result.indexOu = buildIndexOu(entity.ou, entity.model) + suffix;\n\n // Optional indexes - only set when the source field is present\n if (entity.alias !== undefined) {\n result.indexAlias =\n buildIndexAlias(entity.ou, entity.model, entity.alias) + suffix;\n }\n\n if (entity.class !== undefined) {\n result.indexClass =\n buildIndexClass(entity.ou, entity.model, entity.class) + suffix;\n }\n\n if (entity.type !== undefined) {\n result.indexType =\n buildIndexType(entity.ou, entity.model, entity.type) + suffix;\n }\n\n if (entity.xid !== undefined) {\n result.indexXid =\n buildIndexXid(entity.ou, entity.model, entity.xid) + suffix;\n }\n\n return result;\n}\n","import { DeleteCommand, GetCommand, PutCommand } from \"@aws-sdk/lib-dynamodb\";\n\nimport { getDocClient, getTableName } from \"./client.js\";\nimport { ARCHIVED_SUFFIX, DELETED_SUFFIX } from \"./constants.js\";\nimport { indexEntity } from \"./keyBuilders.js\";\nimport type { FabricEntity } from \"./types.js\";\n\n/**\n * Parameters for getEntity\n */\nexport interface GetEntityParams {\n /** Entity ID (sort key) */\n id: string;\n /** Entity model (partition key) */\n model: string;\n}\n\n/**\n * Parameters for putEntity\n */\nexport interface PutEntityParams<T extends FabricEntity> {\n /** The entity to save */\n entity: T;\n}\n\n/**\n * Parameters for updateEntity\n */\nexport interface UpdateEntityParams<T extends FabricEntity> {\n /** The entity with updated fields */\n entity: T;\n}\n\n/**\n * Parameters for deleteEntity (soft delete)\n */\nexport interface DeleteEntityParams {\n /** Entity ID (sort key) */\n id: string;\n /** Entity model (partition key) */\n model: string;\n}\n\n/**\n * Parameters for archiveEntity\n */\nexport interface ArchiveEntityParams {\n /** Entity ID (sort key) */\n id: string;\n /** Entity model (partition key) */\n model: string;\n}\n\n/**\n * Get a single entity by primary key\n */\nexport async function getEntity<T extends FabricEntity = FabricEntity>(\n params: GetEntityParams,\n): Promise<T | null> {\n const { id, model } = params;\n const docClient = getDocClient();\n const tableName = getTableName();\n\n const command = new GetCommand({\n Key: { id, model },\n TableName: tableName,\n });\n\n const response = await docClient.send(command);\n return (response.Item as T) ?? null;\n}\n\n/**\n * Put (create or replace) an entity\n * Auto-populates GSI index keys via indexEntity\n */\nexport async function putEntity<T extends FabricEntity>(\n params: PutEntityParams<T>,\n): Promise<T> {\n const { entity } = params;\n const docClient = getDocClient();\n const tableName = getTableName();\n\n // Auto-populate index keys\n const indexedEntity = indexEntity(entity);\n\n const command = new PutCommand({\n Item: indexedEntity,\n TableName: tableName,\n });\n\n await docClient.send(command);\n return indexedEntity;\n}\n\n/**\n * Update an existing entity\n * Auto-populates GSI index keys and sets updatedAt\n */\nexport async function updateEntity<T extends FabricEntity>(\n params: UpdateEntityParams<T>,\n): Promise<T> {\n const { entity } = params;\n const docClient = getDocClient();\n const tableName = getTableName();\n\n // Update timestamp and re-index\n const updatedEntity = indexEntity({\n ...entity,\n updatedAt: new Date().toISOString(),\n });\n\n const command = new PutCommand({\n Item: updatedEntity,\n TableName: tableName,\n });\n\n await docClient.send(command);\n return updatedEntity;\n}\n\n/**\n * Calculate suffix based on entity's archived/deleted state\n */\nfunction calculateEntitySuffix(entity: {\n archivedAt?: string;\n deletedAt?: string;\n}): string {\n const hasArchived = Boolean(entity.archivedAt);\n const hasDeleted = Boolean(entity.deletedAt);\n\n if (hasArchived && hasDeleted) {\n return ARCHIVED_SUFFIX + DELETED_SUFFIX;\n }\n if (hasArchived) {\n return ARCHIVED_SUFFIX;\n }\n if (hasDeleted) {\n return DELETED_SUFFIX;\n }\n return \"\";\n}\n\n/**\n * Soft delete an entity by setting deletedAt timestamp\n * Re-indexes with appropriate suffix based on archived/deleted state\n */\nexport async function deleteEntity(\n params: DeleteEntityParams,\n): Promise<boolean> {\n const { id, model } = params;\n const docClient = getDocClient();\n const tableName = getTableName();\n\n // Fetch the current entity\n const existing = await getEntity({ id, model });\n if (!existing) {\n return false;\n }\n\n const now = new Date().toISOString();\n\n // Build updated entity with deletedAt\n const updatedEntity = {\n ...existing,\n deletedAt: now,\n updatedAt: now,\n };\n\n // Calculate suffix based on combined state (may already be archived)\n const suffix = calculateEntitySuffix(updatedEntity);\n const deletedEntity = indexEntity(updatedEntity, suffix);\n\n const command = new PutCommand({\n Item: deletedEntity,\n TableName: tableName,\n });\n\n await docClient.send(command);\n return true;\n}\n\n/**\n * Archive an entity by setting archivedAt timestamp\n * Re-indexes with appropriate suffix based on archived/deleted state\n */\nexport async function archiveEntity(\n params: ArchiveEntityParams,\n): Promise<boolean> {\n const { id, model } = params;\n const docClient = getDocClient();\n const tableName = getTableName();\n\n // Fetch the current entity\n const existing = await getEntity({ id, model });\n if (!existing) {\n return false;\n }\n\n const now = new Date().toISOString();\n\n // Build updated entity with archivedAt\n const updatedEntity = {\n ...existing,\n archivedAt: now,\n updatedAt: now,\n };\n\n // Calculate suffix based on combined state (may already be deleted)\n const suffix = calculateEntitySuffix(updatedEntity);\n const archivedEntity = indexEntity(updatedEntity, suffix);\n\n const command = new PutCommand({\n Item: archivedEntity,\n TableName: tableName,\n });\n\n await docClient.send(command);\n return true;\n}\n\n/**\n * Hard delete an entity (permanently removes from table)\n * Use with caution - prefer deleteEntity for soft delete\n */\nexport async function destroyEntity(\n params: DeleteEntityParams,\n): Promise<boolean> {\n const { id, model } = params;\n const docClient = getDocClient();\n const tableName = getTableName();\n\n const command = new DeleteCommand({\n Key: { id, model },\n TableName: tableName,\n });\n\n await docClient.send(command);\n return true;\n}\n","import { QueryCommand } from \"@aws-sdk/lib-dynamodb\";\n\nimport { getDocClient, getTableName } from \"./client.js\";\nimport {\n ARCHIVED_SUFFIX,\n DELETED_SUFFIX,\n INDEX_ALIAS,\n INDEX_CLASS,\n INDEX_OU,\n INDEX_TYPE,\n INDEX_XID,\n} from \"./constants.js\";\nimport {\n buildIndexAlias,\n buildIndexClass,\n buildIndexOu,\n buildIndexType,\n buildIndexXid,\n} from \"./keyBuilders.js\";\nimport type {\n BaseQueryOptions,\n FabricEntity,\n QueryByAliasParams,\n QueryByClassParams,\n QueryByOuParams,\n QueryByTypeParams,\n QueryByXidParams,\n QueryResult,\n} from \"./types.js\";\n\n/**\n * Calculate the suffix based on archived/deleted flags\n * When both are true, returns combined suffix (archived first, alphabetically)\n */\nfunction calculateSuffix({\n archived,\n deleted,\n}: {\n archived?: boolean;\n deleted?: boolean;\n}): string {\n if (archived && deleted) {\n return ARCHIVED_SUFFIX + DELETED_SUFFIX;\n }\n if (archived) {\n return ARCHIVED_SUFFIX;\n }\n if (deleted) {\n return DELETED_SUFFIX;\n }\n return \"\";\n}\n\n/**\n * Execute a GSI query with common options\n */\nasync function executeQuery<T extends FabricEntity>(\n indexName: string,\n keyValue: string,\n options: BaseQueryOptions = {},\n): Promise<QueryResult<T>> {\n const { ascending = false, limit, startKey } = options;\n\n const docClient = getDocClient();\n const tableName = getTableName();\n\n const command = new QueryCommand({\n ExclusiveStartKey: startKey as Record<string, unknown> | undefined,\n ExpressionAttributeNames: {\n \"#pk\": indexName,\n },\n ExpressionAttributeValues: {\n \":pkValue\": keyValue,\n },\n IndexName: indexName,\n KeyConditionExpression: \"#pk = :pkValue\",\n ...(limit && { Limit: limit }),\n ScanIndexForward: ascending,\n TableName: tableName,\n });\n\n const response = await docClient.send(command);\n\n return {\n items: (response.Items ?? []) as T[],\n lastEvaluatedKey: response.LastEvaluatedKey,\n };\n}\n\n/**\n * Query entities by organizational unit (parent hierarchy)\n * Uses indexOu GSI\n *\n * @param params.archived - Query archived entities instead of active ones\n * @param params.deleted - Query deleted entities instead of active ones\n * @throws ConfigurationError if both archived and deleted are true\n */\nexport async function queryByOu<T extends FabricEntity = FabricEntity>(\n params: QueryByOuParams,\n): Promise<QueryResult<T>> {\n const { archived, deleted, model, ou, ...options } = params;\n const suffix = calculateSuffix({ archived, deleted });\n const keyValue = buildIndexOu(ou, model) + suffix;\n return executeQuery<T>(INDEX_OU, keyValue, options);\n}\n\n/**\n * Query a single entity by human-friendly alias\n * Uses indexAlias GSI\n *\n * @param params.archived - Query archived entities instead of active ones\n * @param params.deleted - Query deleted entities instead of active ones\n * @throws ConfigurationError if both archived and deleted are true\n * @returns The matching entity or null if not found\n */\nexport async function queryByAlias<T extends FabricEntity = FabricEntity>(\n params: QueryByAliasParams,\n): Promise<T | null> {\n const { alias, archived, deleted, model, ou } = params;\n const suffix = calculateSuffix({ archived, deleted });\n const keyValue = buildIndexAlias(ou, model, alias) + suffix;\n const result = await executeQuery<T>(INDEX_ALIAS, keyValue, { limit: 1 });\n return result.items[0] ?? null;\n}\n\n/**\n * Query entities by category classification\n * Uses indexClass GSI\n *\n * @param params.archived - Query archived entities instead of active ones\n * @param params.deleted - Query deleted entities instead of active ones\n * @throws ConfigurationError if both archived and deleted are true\n */\nexport async function queryByClass<T extends FabricEntity = FabricEntity>(\n params: QueryByClassParams,\n): Promise<QueryResult<T>> {\n const { archived, deleted, model, ou, recordClass, ...options } = params;\n const suffix = calculateSuffix({ archived, deleted });\n const keyValue = buildIndexClass(ou, model, recordClass) + suffix;\n return executeQuery<T>(INDEX_CLASS, keyValue, options);\n}\n\n/**\n * Query entities by type classification\n * Uses indexType GSI\n *\n * @param params.archived - Query archived entities instead of active ones\n * @param params.deleted - Query deleted entities instead of active ones\n * @throws ConfigurationError if both archived and deleted are true\n */\nexport async function queryByType<T extends FabricEntity = FabricEntity>(\n params: QueryByTypeParams,\n): Promise<QueryResult<T>> {\n const { archived, deleted, model, ou, type, ...options } = params;\n const suffix = calculateSuffix({ archived, deleted });\n const keyValue = buildIndexType(ou, model, type) + suffix;\n return executeQuery<T>(INDEX_TYPE, keyValue, options);\n}\n\n/**\n * Query a single entity by external ID\n * Uses indexXid GSI\n *\n * @param params.archived - Query archived entities instead of active ones\n * @param params.deleted - Query deleted entities instead of active ones\n * @throws ConfigurationError if both archived and deleted are true\n * @returns The matching entity or null if not found\n */\nexport async function queryByXid<T extends FabricEntity = FabricEntity>(\n params: QueryByXidParams,\n): Promise<T | null> {\n const { archived, deleted, model, ou, xid } = params;\n const suffix = calculateSuffix({ archived, deleted });\n const keyValue = buildIndexXid(ou, model, xid) + suffix;\n const result = await executeQuery<T>(INDEX_XID, keyValue, { limit: 1 });\n return result.items[0] ?? null;\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import type { FabricEntity, ParentReference } from "./types.js";
2
+ /**
3
+ * Build the indexOu key for hierarchical queries
4
+ * @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
5
+ * @param model - The entity model name
6
+ * @returns Composite key: "{ou}#{model}"
7
+ */
8
+ export declare function buildIndexOu(ou: string, model: string): string;
9
+ /**
10
+ * Build the indexAlias key for human-friendly lookups
11
+ * @param ou - The organizational unit
12
+ * @param model - The entity model name
13
+ * @param alias - The human-friendly alias
14
+ * @returns Composite key: "{ou}#{model}#{alias}"
15
+ */
16
+ export declare function buildIndexAlias(ou: string, model: string, alias: string): string;
17
+ /**
18
+ * Build the indexClass key for category filtering
19
+ * @param ou - The organizational unit
20
+ * @param model - The entity model name
21
+ * @param recordClass - The category classification
22
+ * @returns Composite key: "{ou}#{model}#{class}"
23
+ */
24
+ export declare function buildIndexClass(ou: string, model: string, recordClass: string): string;
25
+ /**
26
+ * Build the indexType key for type filtering
27
+ * @param ou - The organizational unit
28
+ * @param model - The entity model name
29
+ * @param type - The type classification
30
+ * @returns Composite key: "{ou}#{model}#{type}"
31
+ */
32
+ export declare function buildIndexType(ou: string, model: string, type: string): string;
33
+ /**
34
+ * Build the indexXid key for external ID lookups
35
+ * @param ou - The organizational unit
36
+ * @param model - The entity model name
37
+ * @param xid - The external ID
38
+ * @returns Composite key: "{ou}#{model}#{xid}"
39
+ */
40
+ export declare function buildIndexXid(ou: string, model: string, xid: string): string;
41
+ /**
42
+ * Calculate the organizational unit from a parent reference
43
+ * @param parent - Optional parent entity reference
44
+ * @returns APEX ("@") if no parent, otherwise "{parent.model}#{parent.id}"
45
+ */
46
+ export declare function calculateOu(parent?: ParentReference): string;
47
+ /**
48
+ * Auto-populate GSI index keys on an entity
49
+ * - indexOu is always populated from ou + model
50
+ * - indexAlias is populated only when alias is present
51
+ * - indexClass is populated only when class is present
52
+ * - indexType is populated only when type is present
53
+ * - indexXid is populated only when xid is present
54
+ *
55
+ * @param entity - The entity to populate index keys for
56
+ * @param suffix - Optional suffix to append to all index keys (e.g., "#deleted", "#archived")
57
+ * @returns The entity with populated index keys
58
+ */
59
+ export declare function indexEntity<T extends FabricEntity>(entity: T, suffix?: string): T;
@@ -0,0 +1,48 @@
1
+ import type { FabricEntity, QueryByAliasParams, QueryByClassParams, QueryByOuParams, QueryByTypeParams, QueryByXidParams, QueryResult } from "./types.js";
2
+ /**
3
+ * Query entities by organizational unit (parent hierarchy)
4
+ * Uses indexOu GSI
5
+ *
6
+ * @param params.archived - Query archived entities instead of active ones
7
+ * @param params.deleted - Query deleted entities instead of active ones
8
+ * @throws ConfigurationError if both archived and deleted are true
9
+ */
10
+ export declare function queryByOu<T extends FabricEntity = FabricEntity>(params: QueryByOuParams): Promise<QueryResult<T>>;
11
+ /**
12
+ * Query a single entity by human-friendly alias
13
+ * Uses indexAlias GSI
14
+ *
15
+ * @param params.archived - Query archived entities instead of active ones
16
+ * @param params.deleted - Query deleted entities instead of active ones
17
+ * @throws ConfigurationError if both archived and deleted are true
18
+ * @returns The matching entity or null if not found
19
+ */
20
+ export declare function queryByAlias<T extends FabricEntity = FabricEntity>(params: QueryByAliasParams): Promise<T | null>;
21
+ /**
22
+ * Query entities by category classification
23
+ * Uses indexClass GSI
24
+ *
25
+ * @param params.archived - Query archived entities instead of active ones
26
+ * @param params.deleted - Query deleted entities instead of active ones
27
+ * @throws ConfigurationError if both archived and deleted are true
28
+ */
29
+ export declare function queryByClass<T extends FabricEntity = FabricEntity>(params: QueryByClassParams): Promise<QueryResult<T>>;
30
+ /**
31
+ * Query entities by type classification
32
+ * Uses indexType GSI
33
+ *
34
+ * @param params.archived - Query archived entities instead of active ones
35
+ * @param params.deleted - Query deleted entities instead of active ones
36
+ * @throws ConfigurationError if both archived and deleted are true
37
+ */
38
+ export declare function queryByType<T extends FabricEntity = FabricEntity>(params: QueryByTypeParams): Promise<QueryResult<T>>;
39
+ /**
40
+ * Query a single entity by external ID
41
+ * Uses indexXid GSI
42
+ *
43
+ * @param params.archived - Query archived entities instead of active ones
44
+ * @param params.deleted - Query deleted entities instead of active ones
45
+ * @throws ConfigurationError if both archived and deleted are true
46
+ * @returns The matching entity or null if not found
47
+ */
48
+ export declare function queryByXid<T extends FabricEntity = FabricEntity>(params: QueryByXidParams): Promise<T | null>;