@jaypie/dynamodb 0.1.3 → 0.3.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.
@@ -3,7 +3,7 @@
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
+ var fabric = require('@jaypie/fabric');
7
7
 
8
8
  // Environment variable names
9
9
  const ENV_AWS_REGION = "AWS_REGION";
@@ -83,82 +83,98 @@ function resetClient() {
83
83
  tableName = null;
84
84
  }
85
85
 
86
- // Primary markers
87
- const APEX = "@"; // Root-level marker (DynamoDB prohibits empty strings)
88
- const SEPARATOR = "#"; // Composite key separator
89
- // GSI names
86
+ // Re-export shared constants from fabric
87
+ // GSI names (derived from DEFAULT_INDEXES)
90
88
  const INDEX_ALIAS = "indexAlias";
91
89
  const INDEX_CLASS = "indexClass";
92
- const INDEX_OU = "indexOu";
90
+ const INDEX_SCOPE = "indexScope";
93
91
  const INDEX_TYPE = "indexType";
94
92
  const INDEX_XID = "indexXid";
95
- // Index suffixes for soft state
96
- const ARCHIVED_SUFFIX = "#archived";
97
- const DELETED_SUFFIX = "#deleted";
98
93
 
94
+ // =============================================================================
95
+ // Key Builders
96
+ // =============================================================================
99
97
  /**
100
- * Build the indexOu key for hierarchical queries
101
- * @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
98
+ * Build the indexScope key for hierarchical queries
99
+ * @param scope - The scope (APEX or "{parent.model}#{parent.id}")
102
100
  * @param model - The entity model name
103
- * @returns Composite key: "{ou}#{model}"
101
+ * @returns Composite key: "{scope}#{model}"
104
102
  */
105
- function buildIndexOu(ou, model) {
106
- return `${ou}${SEPARATOR}${model}`;
103
+ function buildIndexScope(scope, model) {
104
+ return `${scope}${fabric.SEPARATOR}${model}`;
107
105
  }
108
106
  /**
109
107
  * Build the indexAlias key for human-friendly lookups
110
- * @param ou - The organizational unit
108
+ * @param scope - The scope
111
109
  * @param model - The entity model name
112
110
  * @param alias - The human-friendly alias
113
- * @returns Composite key: "{ou}#{model}#{alias}"
111
+ * @returns Composite key: "{scope}#{model}#{alias}"
114
112
  */
115
- function buildIndexAlias(ou, model, alias) {
116
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;
113
+ function buildIndexAlias(scope, model, alias) {
114
+ return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${alias}`;
117
115
  }
118
116
  /**
119
117
  * Build the indexClass key for category filtering
120
- * @param ou - The organizational unit
118
+ * @param scope - The scope
121
119
  * @param model - The entity model name
122
120
  * @param recordClass - The category classification
123
- * @returns Composite key: "{ou}#{model}#{class}"
121
+ * @returns Composite key: "{scope}#{model}#{class}"
124
122
  */
125
- function buildIndexClass(ou, model, recordClass) {
126
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;
123
+ function buildIndexClass(scope, model, recordClass) {
124
+ return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${recordClass}`;
127
125
  }
128
126
  /**
129
127
  * Build the indexType key for type filtering
130
- * @param ou - The organizational unit
128
+ * @param scope - The scope
131
129
  * @param model - The entity model name
132
130
  * @param type - The type classification
133
- * @returns Composite key: "{ou}#{model}#{type}"
131
+ * @returns Composite key: "{scope}#{model}#{type}"
134
132
  */
135
- function buildIndexType(ou, model, type) {
136
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;
133
+ function buildIndexType(scope, model, type) {
134
+ return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${type}`;
137
135
  }
138
136
  /**
139
137
  * Build the indexXid key for external ID lookups
140
- * @param ou - The organizational unit
138
+ * @param scope - The scope
141
139
  * @param model - The entity model name
142
140
  * @param xid - The external ID
143
- * @returns Composite key: "{ou}#{model}#{xid}"
141
+ * @returns Composite key: "{scope}#{model}#{xid}"
144
142
  */
145
- function buildIndexXid(ou, model, xid) {
146
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
143
+ function buildIndexXid(scope, model, xid) {
144
+ return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${xid}`;
147
145
  }
146
+ // =============================================================================
147
+ // New Vocabulary-Based Functions
148
+ // =============================================================================
148
149
  /**
149
- * Calculate the organizational unit from a parent reference
150
+ * Build a composite key from entity fields
151
+ *
152
+ * @param entity - Entity with fields to extract
153
+ * @param fields - Field names to combine with SEPARATOR
154
+ * @param suffix - Optional suffix to append (e.g., "#deleted")
155
+ * @returns Composite key string
156
+ */
157
+ function buildCompositeKey(entity, fields, suffix) {
158
+ return fabric.buildCompositeKey(entity, fields, suffix);
159
+ }
160
+ /**
161
+ * Calculate the scope from a parent reference
150
162
  * @param parent - Optional parent entity reference
151
163
  * @returns APEX ("@") if no parent, otherwise "{parent.model}#{parent.id}"
152
164
  */
153
- function calculateOu(parent) {
165
+ function calculateScope(parent) {
154
166
  if (!parent) {
155
- return APEX;
167
+ return fabric.APEX;
156
168
  }
157
- return `${parent.model}${SEPARATOR}${parent.id}`;
169
+ return fabric.calculateScope(parent);
158
170
  }
159
171
  /**
160
172
  * Auto-populate GSI index keys on an entity
161
- * - indexOu is always populated from ou + model
173
+ *
174
+ * Uses the model's registered indexes (from vocabulary registry) or
175
+ * DEFAULT_INDEXES if no custom indexes are registered.
176
+ *
177
+ * - indexScope is always populated from scope + model
162
178
  * - indexAlias is populated only when alias is present
163
179
  * - indexClass is populated only when class is present
164
180
  * - indexType is populated only when type is present
@@ -169,27 +185,9 @@ function calculateOu(parent) {
169
185
  * @returns The entity with populated index keys
170
186
  */
171
187
  function indexEntity(entity, suffix = "") {
172
- const result = { ...entity };
173
- // indexOu is always set (from ou + model)
174
- result.indexOu = buildIndexOu(entity.ou, entity.model) + suffix;
175
- // Optional indexes - only set when the source field is present
176
- if (entity.alias !== undefined) {
177
- result.indexAlias =
178
- buildIndexAlias(entity.ou, entity.model, entity.alias) + suffix;
179
- }
180
- if (entity.class !== undefined) {
181
- result.indexClass =
182
- buildIndexClass(entity.ou, entity.model, entity.class) + suffix;
183
- }
184
- if (entity.type !== undefined) {
185
- result.indexType =
186
- buildIndexType(entity.ou, entity.model, entity.type) + suffix;
187
- }
188
- if (entity.xid !== undefined) {
189
- result.indexXid =
190
- buildIndexXid(entity.ou, entity.model, entity.xid) + suffix;
191
- }
192
- return result;
188
+ const indexes = fabric.getModelIndexes(entity.model);
189
+ // Cast through unknown to bridge the type gap between StorableEntity and IndexableModel
190
+ return fabric.populateIndexKeys(entity, indexes, suffix);
193
191
  }
194
192
 
195
193
  /**
@@ -199,20 +197,20 @@ function calculateEntitySuffix(entity) {
199
197
  const hasArchived = Boolean(entity.archivedAt);
200
198
  const hasDeleted = Boolean(entity.deletedAt);
201
199
  if (hasArchived && hasDeleted) {
202
- return ARCHIVED_SUFFIX + DELETED_SUFFIX;
200
+ return fabric.ARCHIVED_SUFFIX + fabric.DELETED_SUFFIX;
203
201
  }
204
202
  if (hasArchived) {
205
- return ARCHIVED_SUFFIX;
203
+ return fabric.ARCHIVED_SUFFIX;
206
204
  }
207
205
  if (hasDeleted) {
208
- return DELETED_SUFFIX;
206
+ return fabric.DELETED_SUFFIX;
209
207
  }
210
208
  return "";
211
209
  }
212
210
  /**
213
211
  * Get a single entity by primary key
214
212
  */
215
- const getEntity = vocabulary.serviceHandler({
213
+ const getEntity = fabric.fabricService({
216
214
  alias: "getEntity",
217
215
  description: "Get a single entity by primary key",
218
216
  input: {
@@ -234,8 +232,8 @@ const getEntity = vocabulary.serviceHandler({
234
232
  * Put (create or replace) an entity
235
233
  * Auto-populates GSI index keys via indexEntity
236
234
  *
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.
235
+ * Note: This is a regular async function (not fabricService) because it accepts
236
+ * complex StorableEntity objects that can't be coerced by vocabulary's type system.
239
237
  */
240
238
  async function putEntity({ entity, }) {
241
239
  const docClient = getDocClient();
@@ -253,8 +251,8 @@ async function putEntity({ entity, }) {
253
251
  * Update an existing entity
254
252
  * Auto-populates GSI index keys and sets updatedAt
255
253
  *
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.
254
+ * Note: This is a regular async function (not fabricService) because it accepts
255
+ * complex StorableEntity objects that can't be coerced by vocabulary's type system.
258
256
  */
259
257
  async function updateEntity({ entity, }) {
260
258
  const docClient = getDocClient();
@@ -275,7 +273,7 @@ async function updateEntity({ entity, }) {
275
273
  * Soft delete an entity by setting deletedAt timestamp
276
274
  * Re-indexes with appropriate suffix based on archived/deleted state
277
275
  */
278
- const deleteEntity = vocabulary.serviceHandler({
276
+ const deleteEntity = fabric.fabricService({
279
277
  alias: "deleteEntity",
280
278
  description: "Soft delete an entity (sets deletedAt timestamp)",
281
279
  input: {
@@ -312,7 +310,7 @@ const deleteEntity = vocabulary.serviceHandler({
312
310
  * Archive an entity by setting archivedAt timestamp
313
311
  * Re-indexes with appropriate suffix based on archived/deleted state
314
312
  */
315
- const archiveEntity = vocabulary.serviceHandler({
313
+ const archiveEntity = fabric.fabricService({
316
314
  alias: "archiveEntity",
317
315
  description: "Archive an entity (sets archivedAt timestamp)",
318
316
  input: {
@@ -349,7 +347,7 @@ const archiveEntity = vocabulary.serviceHandler({
349
347
  * Hard delete an entity (permanently removes from table)
350
348
  * Use with caution - prefer deleteEntity for soft delete
351
349
  */
352
- const destroyEntity = vocabulary.serviceHandler({
350
+ const destroyEntity = fabric.fabricService({
353
351
  alias: "destroyEntity",
354
352
  description: "Hard delete an entity (permanently removes from table)",
355
353
  input: {
@@ -372,15 +370,15 @@ const destroyEntity = vocabulary.serviceHandler({
372
370
  * Calculate the suffix based on archived/deleted flags
373
371
  * When both are true, returns combined suffix (archived first, alphabetically)
374
372
  */
375
- function calculateSuffix({ archived, deleted, }) {
373
+ function calculateSuffix$1({ archived, deleted, }) {
376
374
  if (archived && deleted) {
377
- return ARCHIVED_SUFFIX + DELETED_SUFFIX;
375
+ return fabric.ARCHIVED_SUFFIX + fabric.DELETED_SUFFIX;
378
376
  }
379
377
  if (archived) {
380
- return ARCHIVED_SUFFIX;
378
+ return fabric.ARCHIVED_SUFFIX;
381
379
  }
382
380
  if (deleted) {
383
- return DELETED_SUFFIX;
381
+ return fabric.DELETED_SUFFIX;
384
382
  }
385
383
  return "";
386
384
  }
@@ -412,16 +410,16 @@ async function executeQuery(indexName, keyValue, options = {}) {
412
410
  };
413
411
  }
414
412
  /**
415
- * Query entities by organizational unit (parent hierarchy)
416
- * Uses indexOu GSI
413
+ * Query entities by scope (parent hierarchy)
414
+ * Uses indexScope GSI
417
415
  *
418
- * Note: This is a regular async function (not serviceHandler) because it accepts
416
+ * Note: This is a regular async function (not fabricService) because it accepts
419
417
  * complex startKey objects that can't be coerced by vocabulary's type system.
420
418
  */
421
- async function queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
422
- const suffix = calculateSuffix({ archived, deleted });
423
- const keyValue = buildIndexOu(ou, model) + suffix;
424
- return executeQuery(INDEX_OU, keyValue, {
419
+ async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
420
+ const suffix = calculateSuffix$1({ archived, deleted });
421
+ const keyValue = buildIndexScope(scope, model) + suffix;
422
+ return executeQuery(INDEX_SCOPE, keyValue, {
425
423
  ascending,
426
424
  limit,
427
425
  startKey,
@@ -431,7 +429,7 @@ async function queryByOu({ archived = false, ascending = false, deleted = false,
431
429
  * Query a single entity by human-friendly alias
432
430
  * Uses indexAlias GSI
433
431
  */
434
- const queryByAlias = vocabulary.serviceHandler({
432
+ const queryByAlias = fabric.fabricService({
435
433
  alias: "queryByAlias",
436
434
  description: "Query a single entity by human-friendly alias",
437
435
  input: {
@@ -449,16 +447,19 @@ const queryByAlias = vocabulary.serviceHandler({
449
447
  description: "Query deleted entities instead of active ones",
450
448
  },
451
449
  model: { type: String, description: "Entity model name" },
452
- ou: { type: String, description: "Organizational unit (@ for root)" },
450
+ scope: { type: String, description: "Scope (@ for root)" },
453
451
  },
454
- service: async ({ alias, archived, deleted, model, ou, }) => {
452
+ service: async ({ alias, archived, deleted, model, scope, }) => {
455
453
  const aliasStr = alias;
456
454
  const archivedBool = archived;
457
455
  const deletedBool = deleted;
458
456
  const modelStr = model;
459
- const ouStr = ou;
460
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
461
- const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
457
+ const scopeStr = scope;
458
+ const suffix = calculateSuffix$1({
459
+ archived: archivedBool,
460
+ deleted: deletedBool,
461
+ });
462
+ const keyValue = buildIndexAlias(scopeStr, modelStr, aliasStr) + suffix;
462
463
  const result = await executeQuery(INDEX_ALIAS, keyValue, {
463
464
  limit: 1,
464
465
  });
@@ -469,12 +470,12 @@ const queryByAlias = vocabulary.serviceHandler({
469
470
  * Query entities by category classification
470
471
  * Uses indexClass GSI
471
472
  *
472
- * Note: This is a regular async function (not serviceHandler) because it accepts
473
+ * Note: This is a regular async function (not fabricService) because it accepts
473
474
  * complex startKey objects that can't be coerced by vocabulary's type system.
474
475
  */
475
- async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, ou, recordClass, startKey, }) {
476
- const suffix = calculateSuffix({ archived, deleted });
477
- const keyValue = buildIndexClass(ou, model, recordClass) + suffix;
476
+ async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, scope, recordClass, startKey, }) {
477
+ const suffix = calculateSuffix$1({ archived, deleted });
478
+ const keyValue = buildIndexClass(scope, model, recordClass) + suffix;
478
479
  return executeQuery(INDEX_CLASS, keyValue, {
479
480
  ascending,
480
481
  limit,
@@ -485,12 +486,12 @@ async function queryByClass({ archived = false, ascending = false, deleted = fal
485
486
  * Query entities by type classification
486
487
  * Uses indexType GSI
487
488
  *
488
- * Note: This is a regular async function (not serviceHandler) because it accepts
489
+ * Note: This is a regular async function (not fabricService) because it accepts
489
490
  * complex startKey objects that can't be coerced by vocabulary's type system.
490
491
  */
491
- async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, type, }) {
492
- const suffix = calculateSuffix({ archived, deleted });
493
- const keyValue = buildIndexType(ou, model, type) + suffix;
492
+ async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, type, }) {
493
+ const suffix = calculateSuffix$1({ archived, deleted });
494
+ const keyValue = buildIndexType(scope, model, type) + suffix;
494
495
  return executeQuery(INDEX_TYPE, keyValue, {
495
496
  ascending,
496
497
  limit,
@@ -501,7 +502,7 @@ async function queryByType({ archived = false, ascending = false, deleted = fals
501
502
  * Query a single entity by external ID
502
503
  * Uses indexXid GSI
503
504
  */
504
- const queryByXid = vocabulary.serviceHandler({
505
+ const queryByXid = fabric.fabricService({
505
506
  alias: "queryByXid",
506
507
  description: "Query a single entity by external ID",
507
508
  input: {
@@ -518,17 +519,20 @@ const queryByXid = vocabulary.serviceHandler({
518
519
  description: "Query deleted entities instead of active ones",
519
520
  },
520
521
  model: { type: String, description: "Entity model name" },
521
- ou: { type: String, description: "Organizational unit (@ for root)" },
522
+ scope: { type: String, description: "Scope (@ for root)" },
522
523
  xid: { type: String, description: "External ID" },
523
524
  },
524
- service: async ({ archived, deleted, model, ou, xid, }) => {
525
+ service: async ({ archived, deleted, model, scope, xid, }) => {
525
526
  const archivedBool = archived;
526
527
  const deletedBool = deleted;
527
528
  const modelStr = model;
528
- const ouStr = ou;
529
+ const scopeStr = scope;
529
530
  const xidStr = xid;
530
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
531
- const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
531
+ const suffix = calculateSuffix$1({
532
+ archived: archivedBool,
533
+ deleted: deletedBool,
534
+ });
535
+ const keyValue = buildIndexXid(scopeStr, modelStr, xidStr) + suffix;
532
536
  const result = await executeQuery(INDEX_XID, keyValue, {
533
537
  limit: 1,
534
538
  });
@@ -536,21 +540,187 @@ const queryByXid = vocabulary.serviceHandler({
536
540
  },
537
541
  });
538
542
 
543
+ /**
544
+ * Unified Query Function with Auto-Detect Index Selection
545
+ *
546
+ * The query() function automatically selects the best index based on
547
+ * the filter fields provided. This simplifies query construction by
548
+ * removing the need to know which specific GSI to use.
549
+ */
550
+ // =============================================================================
551
+ // Helper Functions
552
+ // =============================================================================
553
+ /**
554
+ * Calculate the suffix based on archived/deleted flags
555
+ */
556
+ function calculateSuffix(archived, deleted) {
557
+ if (archived && deleted) {
558
+ return fabric.ARCHIVED_SUFFIX + fabric.DELETED_SUFFIX;
559
+ }
560
+ if (archived) {
561
+ return fabric.ARCHIVED_SUFFIX;
562
+ }
563
+ if (deleted) {
564
+ return fabric.DELETED_SUFFIX;
565
+ }
566
+ return "";
567
+ }
568
+ /**
569
+ * Build a combined filter object from params
570
+ */
571
+ function buildFilterObject(params) {
572
+ const result = {
573
+ model: params.model,
574
+ };
575
+ if (params.scope !== undefined) {
576
+ result.scope = params.scope;
577
+ }
578
+ if (params.filter) {
579
+ Object.assign(result, params.filter);
580
+ }
581
+ return result;
582
+ }
583
+ /**
584
+ * Score an index based on how well it matches the filter fields
585
+ */
586
+ function scoreIndex(index, filterFields) {
587
+ let matchedFields = 0;
588
+ let pkComplete = true;
589
+ for (const field of index.pk) {
590
+ if (filterFields[field] !== undefined) {
591
+ matchedFields++;
592
+ }
593
+ else {
594
+ pkComplete = false;
595
+ }
596
+ }
597
+ return {
598
+ index,
599
+ matchedFields,
600
+ pkComplete,
601
+ };
602
+ }
603
+ /**
604
+ * Select the best index for the given filter
605
+ *
606
+ * Scoring criteria:
607
+ * 1. Index must have all pk fields present (pkComplete)
608
+ * 2. Prefer indexes with more matched fields
609
+ * 3. Prefer more specific indexes (more pk fields)
610
+ */
611
+ function selectBestIndex(indexes, filterFields) {
612
+ const scores = indexes.map((index) => scoreIndex(index, filterFields));
613
+ // Filter to only complete matches
614
+ const completeMatches = scores.filter((s) => s.pkComplete);
615
+ if (completeMatches.length === 0) {
616
+ const availableIndexes = indexes
617
+ .map((i) => i.name ?? `[${i.pk.join(", ")}]`)
618
+ .join(", ");
619
+ const providedFields = Object.keys(filterFields).join(", ");
620
+ throw new errors.ConfigurationError(`No index matches filter fields. ` +
621
+ `Provided: ${providedFields}. ` +
622
+ `Available indexes: ${availableIndexes}`);
623
+ }
624
+ // Sort by:
625
+ // 1. More matched fields first (descending)
626
+ // 2. More pk fields (more specific) first (descending)
627
+ completeMatches.sort((a, b) => {
628
+ const fieldDiff = b.matchedFields - a.matchedFields;
629
+ if (fieldDiff !== 0)
630
+ return fieldDiff;
631
+ return b.index.pk.length - a.index.pk.length;
632
+ });
633
+ return completeMatches[0].index;
634
+ }
635
+ // =============================================================================
636
+ // Main Query Function
637
+ // =============================================================================
638
+ /**
639
+ * Query entities with automatic index selection
640
+ *
641
+ * The query function automatically selects the best GSI based on
642
+ * the filter fields provided. This removes the need to know which
643
+ * specific query function (queryByOu, queryByAlias, etc.) to use.
644
+ *
645
+ * @example
646
+ * // Uses indexScope (pk: ["scope", "model"])
647
+ * const allMessages = await query({ model: "message", scope: `chat#${chatId}` });
648
+ *
649
+ * @example
650
+ * // Uses indexAlias (pk: ["scope", "model", "alias"])
651
+ * const byAlias = await query({
652
+ * model: "record",
653
+ * scope: "@",
654
+ * filter: { alias: "my-record" },
655
+ * });
656
+ *
657
+ * @example
658
+ * // Uses a custom registered index if model has one
659
+ * const byChat = await query({
660
+ * model: "message",
661
+ * filter: { chatId: "abc-123" },
662
+ * });
663
+ */
664
+ async function query(params) {
665
+ const { archived = false, ascending = false, deleted = false, limit, model, startKey, } = params;
666
+ // Build the combined filter object
667
+ const filterFields = buildFilterObject(params);
668
+ // Get indexes for this model (custom or DEFAULT_INDEXES)
669
+ const indexes = fabric.getModelIndexes(model);
670
+ // Select the best matching index
671
+ const selectedIndex = selectBestIndex(indexes, filterFields);
672
+ const indexName = selectedIndex.name ?? generateIndexName(selectedIndex.pk);
673
+ // Build the partition key value
674
+ const suffix = calculateSuffix(archived, deleted);
675
+ const keyValue = buildCompositeKey(filterFields, selectedIndex.pk, suffix);
676
+ // Execute the query
677
+ const docClient = getDocClient();
678
+ const tableName = getTableName();
679
+ const command = new libDynamodb.QueryCommand({
680
+ ExclusiveStartKey: startKey,
681
+ ExpressionAttributeNames: {
682
+ "#pk": indexName,
683
+ },
684
+ ExpressionAttributeValues: {
685
+ ":pkValue": keyValue,
686
+ },
687
+ IndexName: indexName,
688
+ KeyConditionExpression: "#pk = :pkValue",
689
+ ...(limit && { Limit: limit }),
690
+ ScanIndexForward: ascending,
691
+ TableName: tableName,
692
+ });
693
+ const response = await docClient.send(command);
694
+ return {
695
+ items: (response.Items ?? []),
696
+ lastEvaluatedKey: response.LastEvaluatedKey,
697
+ };
698
+ }
699
+ /**
700
+ * Generate an index name from pk fields
701
+ */
702
+ function generateIndexName(pk) {
703
+ const suffix = pk
704
+ .map((field) => field.charAt(0).toUpperCase() + field.slice(1))
705
+ .join("");
706
+ return `index${suffix}`;
707
+ }
708
+
539
709
  /**
540
710
  * Seed a single entity if it doesn't already exist
541
711
  *
542
- * @param entity - Partial entity with at least alias, model, and ou
712
+ * @param entity - Partial entity with at least alias, model, and scope
543
713
  * @returns true if entity was created, false if it already exists
544
714
  */
545
715
  async function seedEntityIfNotExists(entity) {
546
- if (!entity.alias || !entity.model || !entity.ou) {
547
- throw new Error("Entity must have alias, model, and ou to check existence");
716
+ if (!entity.alias || !entity.model || !entity.scope) {
717
+ throw new Error("Entity must have alias, model, and scope to check existence");
548
718
  }
549
719
  // Check if entity already exists
550
720
  const existing = await queryByAlias({
551
721
  alias: entity.alias,
552
722
  model: entity.model,
553
- ou: entity.ou,
723
+ scope: entity.scope,
554
724
  });
555
725
  if (existing) {
556
726
  return false;
@@ -562,7 +732,7 @@ async function seedEntityIfNotExists(entity) {
562
732
  id: entity.id ?? crypto.randomUUID(),
563
733
  model: entity.model,
564
734
  name: entity.name ?? entity.alias,
565
- ou: entity.ou,
735
+ scope: entity.scope,
566
736
  sequence: entity.sequence ?? Date.now(),
567
737
  updatedAt: entity.updatedAt ?? now,
568
738
  ...entity,
@@ -592,15 +762,15 @@ async function seedEntities(entities, options = {}) {
592
762
  for (const entity of entities) {
593
763
  const alias = entity.alias ?? entity.name ?? "unknown";
594
764
  try {
595
- if (!entity.model || !entity.ou) {
596
- throw new Error("Entity must have model and ou");
765
+ if (!entity.model || !entity.scope) {
766
+ throw new Error("Entity must have model and scope");
597
767
  }
598
768
  // For entities with alias, check existence
599
769
  if (entity.alias) {
600
770
  const existing = await queryByAlias({
601
771
  alias: entity.alias,
602
772
  model: entity.model,
603
- ou: entity.ou,
773
+ scope: entity.scope,
604
774
  });
605
775
  if (existing && !replace) {
606
776
  result.skipped.push(alias);
@@ -622,7 +792,7 @@ async function seedEntities(entities, options = {}) {
622
792
  id: entity.id ?? crypto.randomUUID(),
623
793
  model: entity.model,
624
794
  name: entity.name ?? entity.alias ?? "Unnamed",
625
- ou: entity.ou,
795
+ scope: entity.scope,
626
796
  sequence: entity.sequence ?? Date.now(),
627
797
  updatedAt: entity.updatedAt ?? now,
628
798
  ...entity,
@@ -638,27 +808,27 @@ async function seedEntities(entities, options = {}) {
638
808
  return result;
639
809
  }
640
810
  /**
641
- * Export entities by model and organizational unit
811
+ * Export entities by model and scope
642
812
  *
643
- * - Paginates through all matching entities via queryByOu
813
+ * - Paginates through all matching entities via queryByScope
644
814
  * - Returns entities sorted by sequence (ascending)
645
815
  *
646
816
  * @param model - The entity model name
647
- * @param ou - The organizational unit
817
+ * @param scope - The scope (APEX or "{parent.model}#{parent.id}")
648
818
  * @param limit - Optional maximum number of entities to export
649
819
  * @returns Export result with entities and count
650
820
  */
651
- async function exportEntities(model, ou, limit) {
821
+ async function exportEntities(model, scope, limit) {
652
822
  const entities = [];
653
823
  let startKey;
654
824
  let remaining = limit;
655
825
  do {
656
826
  const batchLimit = remaining !== undefined ? Math.min(remaining, 100) : undefined;
657
- const { items, lastEvaluatedKey } = await queryByOu({
827
+ const { items, lastEvaluatedKey } = await queryByScope({
658
828
  ascending: true,
659
829
  limit: batchLimit,
660
830
  model,
661
- ou,
831
+ scope,
662
832
  startKey,
663
833
  });
664
834
  entities.push(...items);
@@ -676,31 +846,48 @@ async function exportEntities(model, ou, limit) {
676
846
  * Export entities as a JSON string
677
847
  *
678
848
  * @param model - The entity model name
679
- * @param ou - The organizational unit
849
+ * @param scope - The scope (APEX or "{parent.model}#{parent.id}")
680
850
  * @param pretty - Format JSON with indentation (default: true)
681
851
  * @returns JSON string of exported entities
682
852
  */
683
- async function exportEntitiesToJson(model, ou, pretty = true) {
684
- const { entities } = await exportEntities(model, ou);
853
+ async function exportEntitiesToJson(model, scope, pretty = true) {
854
+ const { entities } = await exportEntities(model, scope);
685
855
  return pretty ? JSON.stringify(entities, null, 2) : JSON.stringify(entities);
686
856
  }
687
857
 
688
- exports.APEX = APEX;
689
- exports.ARCHIVED_SUFFIX = ARCHIVED_SUFFIX;
690
- exports.DELETED_SUFFIX = DELETED_SUFFIX;
858
+ Object.defineProperty(exports, "APEX", {
859
+ enumerable: true,
860
+ get: function () { return fabric.APEX; }
861
+ });
862
+ Object.defineProperty(exports, "ARCHIVED_SUFFIX", {
863
+ enumerable: true,
864
+ get: function () { return fabric.ARCHIVED_SUFFIX; }
865
+ });
866
+ Object.defineProperty(exports, "DEFAULT_INDEXES", {
867
+ enumerable: true,
868
+ get: function () { return fabric.DEFAULT_INDEXES; }
869
+ });
870
+ Object.defineProperty(exports, "DELETED_SUFFIX", {
871
+ enumerable: true,
872
+ get: function () { return fabric.DELETED_SUFFIX; }
873
+ });
874
+ Object.defineProperty(exports, "SEPARATOR", {
875
+ enumerable: true,
876
+ get: function () { return fabric.SEPARATOR; }
877
+ });
691
878
  exports.INDEX_ALIAS = INDEX_ALIAS;
692
879
  exports.INDEX_CLASS = INDEX_CLASS;
693
- exports.INDEX_OU = INDEX_OU;
880
+ exports.INDEX_SCOPE = INDEX_SCOPE;
694
881
  exports.INDEX_TYPE = INDEX_TYPE;
695
882
  exports.INDEX_XID = INDEX_XID;
696
- exports.SEPARATOR = SEPARATOR;
697
883
  exports.archiveEntity = archiveEntity;
884
+ exports.buildCompositeKey = buildCompositeKey;
698
885
  exports.buildIndexAlias = buildIndexAlias;
699
886
  exports.buildIndexClass = buildIndexClass;
700
- exports.buildIndexOu = buildIndexOu;
887
+ exports.buildIndexScope = buildIndexScope;
701
888
  exports.buildIndexType = buildIndexType;
702
889
  exports.buildIndexXid = buildIndexXid;
703
- exports.calculateOu = calculateOu;
890
+ exports.calculateScope = calculateScope;
704
891
  exports.deleteEntity = deleteEntity;
705
892
  exports.destroyEntity = destroyEntity;
706
893
  exports.exportEntities = exportEntities;
@@ -712,9 +899,10 @@ exports.indexEntity = indexEntity;
712
899
  exports.initClient = initClient;
713
900
  exports.isInitialized = isInitialized;
714
901
  exports.putEntity = putEntity;
902
+ exports.query = query;
715
903
  exports.queryByAlias = queryByAlias;
716
904
  exports.queryByClass = queryByClass;
717
- exports.queryByOu = queryByOu;
905
+ exports.queryByScope = queryByScope;
718
906
  exports.queryByType = queryByType;
719
907
  exports.queryByXid = queryByXid;
720
908
  exports.resetClient = resetClient;