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