@jaypie/dynamodb 0.1.2 → 0.2.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.
@@ -1,9 +1,6 @@
1
- export declare const APEX = "@";
2
- export declare const SEPARATOR = "#";
1
+ export { APEX, ARCHIVED_SUFFIX, DELETED_SUFFIX, SEPARATOR } from "@jaypie/vocabulary";
3
2
  export declare const INDEX_ALIAS = "indexAlias";
4
3
  export declare const INDEX_CLASS = "indexClass";
5
4
  export declare const INDEX_OU = "indexOu";
6
5
  export declare const INDEX_TYPE = "indexType";
7
6
  export declare const INDEX_XID = "indexXid";
8
- export declare const ARCHIVED_SUFFIX = "#archived";
9
- export declare const DELETED_SUFFIX = "#deleted";
@@ -1,28 +1,28 @@
1
- import type { FabricEntity } from "./types.js";
1
+ import type { StorableEntity } from "./types.js";
2
2
  /**
3
3
  * Get a single entity by primary key
4
4
  */
5
- export declare const getEntity: import("@jaypie/vocabulary").ServiceHandlerFunction<Record<string, unknown>, FabricEntity | null>;
5
+ export declare const getEntity: import("@jaypie/vocabulary").ServiceHandlerFunction<Record<string, unknown>, StorableEntity | null>;
6
6
  /**
7
7
  * Put (create or replace) an entity
8
8
  * Auto-populates GSI index keys via indexEntity
9
9
  *
10
10
  * Note: This is a regular async function (not serviceHandler) because it accepts
11
- * complex FabricEntity objects that can't be coerced by vocabulary's type system.
11
+ * complex StorableEntity objects that can't be coerced by vocabulary's type system.
12
12
  */
13
13
  export declare function putEntity({ entity, }: {
14
- entity: FabricEntity;
15
- }): Promise<FabricEntity>;
14
+ entity: StorableEntity;
15
+ }): Promise<StorableEntity>;
16
16
  /**
17
17
  * Update an existing entity
18
18
  * Auto-populates GSI index keys and sets updatedAt
19
19
  *
20
20
  * Note: This is a regular async function (not serviceHandler) because it accepts
21
- * complex FabricEntity objects that can't be coerced by vocabulary's type system.
21
+ * complex StorableEntity objects that can't be coerced by vocabulary's type system.
22
22
  */
23
23
  export declare function updateEntity({ entity, }: {
24
- entity: FabricEntity;
25
- }): Promise<FabricEntity>;
24
+ entity: StorableEntity;
25
+ }): Promise<StorableEntity>;
26
26
  /**
27
27
  * Soft delete an entity by setting deletedAt timestamp
28
28
  * Re-indexes with appropriate suffix based on archived/deleted state
@@ -83,19 +83,17 @@ 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 vocabulary
87
+ // GSI names (derived from DEFAULT_INDEXES)
90
88
  const INDEX_ALIAS = "indexAlias";
91
89
  const INDEX_CLASS = "indexClass";
92
90
  const INDEX_OU = "indexOu";
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
98
  * Build the indexOu key for hierarchical queries
101
99
  * @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
@@ -103,7 +101,7 @@ const DELETED_SUFFIX = "#deleted";
103
101
  * @returns Composite key: "{ou}#{model}"
104
102
  */
105
103
  function buildIndexOu(ou, model) {
106
- return `${ou}${SEPARATOR}${model}`;
104
+ return `${ou}${vocabulary.SEPARATOR}${model}`;
107
105
  }
108
106
  /**
109
107
  * Build the indexAlias key for human-friendly lookups
@@ -113,7 +111,7 @@ function buildIndexOu(ou, model) {
113
111
  * @returns Composite key: "{ou}#{model}#{alias}"
114
112
  */
115
113
  function buildIndexAlias(ou, model, alias) {
116
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;
114
+ return `${ou}${vocabulary.SEPARATOR}${model}${vocabulary.SEPARATOR}${alias}`;
117
115
  }
118
116
  /**
119
117
  * Build the indexClass key for category filtering
@@ -123,7 +121,7 @@ function buildIndexAlias(ou, model, alias) {
123
121
  * @returns Composite key: "{ou}#{model}#{class}"
124
122
  */
125
123
  function buildIndexClass(ou, model, recordClass) {
126
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;
124
+ return `${ou}${vocabulary.SEPARATOR}${model}${vocabulary.SEPARATOR}${recordClass}`;
127
125
  }
128
126
  /**
129
127
  * Build the indexType key for type filtering
@@ -133,7 +131,7 @@ function buildIndexClass(ou, model, recordClass) {
133
131
  * @returns Composite key: "{ou}#{model}#{type}"
134
132
  */
135
133
  function buildIndexType(ou, model, type) {
136
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;
134
+ return `${ou}${vocabulary.SEPARATOR}${model}${vocabulary.SEPARATOR}${type}`;
137
135
  }
138
136
  /**
139
137
  * Build the indexXid key for external ID lookups
@@ -143,7 +141,21 @@ function buildIndexType(ou, model, type) {
143
141
  * @returns Composite key: "{ou}#{model}#{xid}"
144
142
  */
145
143
  function buildIndexXid(ou, model, xid) {
146
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
144
+ return `${ou}${vocabulary.SEPARATOR}${model}${vocabulary.SEPARATOR}${xid}`;
145
+ }
146
+ // =============================================================================
147
+ // New Vocabulary-Based Functions
148
+ // =============================================================================
149
+ /**
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 vocabulary.buildCompositeKey(entity, fields, suffix);
147
159
  }
148
160
  /**
149
161
  * Calculate the organizational unit from a parent reference
@@ -152,12 +164,16 @@ function buildIndexXid(ou, model, xid) {
152
164
  */
153
165
  function calculateOu(parent) {
154
166
  if (!parent) {
155
- return APEX;
167
+ return vocabulary.APEX;
156
168
  }
157
- return `${parent.model}${SEPARATOR}${parent.id}`;
169
+ return vocabulary.calculateOu(parent);
158
170
  }
159
171
  /**
160
172
  * Auto-populate GSI index keys on an entity
173
+ *
174
+ * Uses the model's registered indexes (from vocabulary registry) or
175
+ * DEFAULT_INDEXES if no custom indexes are registered.
176
+ *
161
177
  * - indexOu is always populated from ou + model
162
178
  * - indexAlias is populated only when alias is present
163
179
  * - indexClass is populated only when class 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 = vocabulary.getModelIndexes(entity.model);
189
+ // Cast through unknown to bridge the type gap between StorableEntity and IndexableEntity
190
+ return vocabulary.populateIndexKeys(entity, indexes, suffix);
193
191
  }
194
192
 
195
193
  /**
@@ -199,13 +197,13 @@ 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 vocabulary.ARCHIVED_SUFFIX + vocabulary.DELETED_SUFFIX;
203
201
  }
204
202
  if (hasArchived) {
205
- return ARCHIVED_SUFFIX;
203
+ return vocabulary.ARCHIVED_SUFFIX;
206
204
  }
207
205
  if (hasDeleted) {
208
- return DELETED_SUFFIX;
206
+ return vocabulary.DELETED_SUFFIX;
209
207
  }
210
208
  return "";
211
209
  }
@@ -235,7 +233,7 @@ const getEntity = vocabulary.serviceHandler({
235
233
  * Auto-populates GSI index keys via indexEntity
236
234
  *
237
235
  * 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.
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();
@@ -254,7 +252,7 @@ async function putEntity({ entity, }) {
254
252
  * Auto-populates GSI index keys and sets updatedAt
255
253
  *
256
254
  * 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.
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();
@@ -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 vocabulary.ARCHIVED_SUFFIX + vocabulary.DELETED_SUFFIX;
378
376
  }
379
377
  if (archived) {
380
- return ARCHIVED_SUFFIX;
378
+ return vocabulary.ARCHIVED_SUFFIX;
381
379
  }
382
380
  if (deleted) {
383
- return DELETED_SUFFIX;
381
+ return vocabulary.DELETED_SUFFIX;
384
382
  }
385
383
  return "";
386
384
  }
@@ -419,7 +417,7 @@ async function executeQuery(indexName, keyValue, options = {}) {
419
417
  * complex startKey objects that can't be coerced by vocabulary's type system.
420
418
  */
421
419
  async function queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
422
- const suffix = calculateSuffix({ archived, deleted });
420
+ const suffix = calculateSuffix$1({ archived, deleted });
423
421
  const keyValue = buildIndexOu(ou, model) + suffix;
424
422
  return executeQuery(INDEX_OU, keyValue, {
425
423
  ascending,
@@ -457,7 +455,7 @@ const queryByAlias = vocabulary.serviceHandler({
457
455
  const deletedBool = deleted;
458
456
  const modelStr = model;
459
457
  const ouStr = ou;
460
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
458
+ const suffix = calculateSuffix$1({ archived: archivedBool, deleted: deletedBool });
461
459
  const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
462
460
  const result = await executeQuery(INDEX_ALIAS, keyValue, {
463
461
  limit: 1,
@@ -473,7 +471,7 @@ const queryByAlias = vocabulary.serviceHandler({
473
471
  * complex startKey objects that can't be coerced by vocabulary's type system.
474
472
  */
475
473
  async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, ou, recordClass, startKey, }) {
476
- const suffix = calculateSuffix({ archived, deleted });
474
+ const suffix = calculateSuffix$1({ archived, deleted });
477
475
  const keyValue = buildIndexClass(ou, model, recordClass) + suffix;
478
476
  return executeQuery(INDEX_CLASS, keyValue, {
479
477
  ascending,
@@ -489,7 +487,7 @@ async function queryByClass({ archived = false, ascending = false, deleted = fal
489
487
  * complex startKey objects that can't be coerced by vocabulary's type system.
490
488
  */
491
489
  async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, type, }) {
492
- const suffix = calculateSuffix({ archived, deleted });
490
+ const suffix = calculateSuffix$1({ archived, deleted });
493
491
  const keyValue = buildIndexType(ou, model, type) + suffix;
494
492
  return executeQuery(INDEX_TYPE, keyValue, {
495
493
  ascending,
@@ -527,7 +525,7 @@ const queryByXid = vocabulary.serviceHandler({
527
525
  const modelStr = model;
528
526
  const ouStr = ou;
529
527
  const xidStr = xid;
530
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
528
+ const suffix = calculateSuffix$1({ archived: archivedBool, deleted: deletedBool });
531
529
  const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
532
530
  const result = await executeQuery(INDEX_XID, keyValue, {
533
531
  limit: 1,
@@ -536,16 +534,348 @@ const queryByXid = vocabulary.serviceHandler({
536
534
  },
537
535
  });
538
536
 
539
- exports.APEX = APEX;
540
- exports.ARCHIVED_SUFFIX = ARCHIVED_SUFFIX;
541
- exports.DELETED_SUFFIX = DELETED_SUFFIX;
537
+ /**
538
+ * Unified Query Function with Auto-Detect Index Selection
539
+ *
540
+ * The query() function automatically selects the best index based on
541
+ * the filter fields provided. This simplifies query construction by
542
+ * removing the need to know which specific GSI to use.
543
+ */
544
+ // =============================================================================
545
+ // Helper Functions
546
+ // =============================================================================
547
+ /**
548
+ * Calculate the suffix based on archived/deleted flags
549
+ */
550
+ function calculateSuffix(archived, deleted) {
551
+ if (archived && deleted) {
552
+ return vocabulary.ARCHIVED_SUFFIX + vocabulary.DELETED_SUFFIX;
553
+ }
554
+ if (archived) {
555
+ return vocabulary.ARCHIVED_SUFFIX;
556
+ }
557
+ if (deleted) {
558
+ return vocabulary.DELETED_SUFFIX;
559
+ }
560
+ return "";
561
+ }
562
+ /**
563
+ * Build a combined filter object from params
564
+ */
565
+ function buildFilterObject(params) {
566
+ const result = {
567
+ model: params.model,
568
+ };
569
+ if (params.ou !== undefined) {
570
+ result.ou = params.ou;
571
+ }
572
+ if (params.filter) {
573
+ Object.assign(result, params.filter);
574
+ }
575
+ return result;
576
+ }
577
+ /**
578
+ * Score an index based on how well it matches the filter fields
579
+ */
580
+ function scoreIndex(index, filterFields) {
581
+ let matchedFields = 0;
582
+ let pkComplete = true;
583
+ for (const field of index.pk) {
584
+ if (filterFields[field] !== undefined) {
585
+ matchedFields++;
586
+ }
587
+ else {
588
+ pkComplete = false;
589
+ }
590
+ }
591
+ return {
592
+ index,
593
+ matchedFields,
594
+ pkComplete,
595
+ };
596
+ }
597
+ /**
598
+ * Select the best index for the given filter
599
+ *
600
+ * Scoring criteria:
601
+ * 1. Index must have all pk fields present (pkComplete)
602
+ * 2. Prefer indexes with more matched fields
603
+ * 3. Prefer more specific indexes (more pk fields)
604
+ */
605
+ function selectBestIndex(indexes, filterFields) {
606
+ const scores = indexes.map((index) => scoreIndex(index, filterFields));
607
+ // Filter to only complete matches
608
+ const completeMatches = scores.filter((s) => s.pkComplete);
609
+ if (completeMatches.length === 0) {
610
+ const availableIndexes = indexes
611
+ .map((i) => i.name ?? `[${i.pk.join(", ")}]`)
612
+ .join(", ");
613
+ const providedFields = Object.keys(filterFields).join(", ");
614
+ throw new errors.ConfigurationError(`No index matches filter fields. ` +
615
+ `Provided: ${providedFields}. ` +
616
+ `Available indexes: ${availableIndexes}`);
617
+ }
618
+ // Sort by:
619
+ // 1. More matched fields first (descending)
620
+ // 2. More pk fields (more specific) first (descending)
621
+ completeMatches.sort((a, b) => {
622
+ const fieldDiff = b.matchedFields - a.matchedFields;
623
+ if (fieldDiff !== 0)
624
+ return fieldDiff;
625
+ return b.index.pk.length - a.index.pk.length;
626
+ });
627
+ return completeMatches[0].index;
628
+ }
629
+ // =============================================================================
630
+ // Main Query Function
631
+ // =============================================================================
632
+ /**
633
+ * Query entities with automatic index selection
634
+ *
635
+ * The query function automatically selects the best GSI based on
636
+ * the filter fields provided. This removes the need to know which
637
+ * specific query function (queryByOu, queryByAlias, etc.) to use.
638
+ *
639
+ * @example
640
+ * // Uses indexOu (pk: ["ou", "model"])
641
+ * const allMessages = await query({ model: "message", ou: `chat#${chatId}` });
642
+ *
643
+ * @example
644
+ * // Uses indexAlias (pk: ["ou", "model", "alias"])
645
+ * const byAlias = await query({
646
+ * model: "record",
647
+ * ou: "@",
648
+ * filter: { alias: "my-record" },
649
+ * });
650
+ *
651
+ * @example
652
+ * // Uses a custom registered index if model has one
653
+ * const byChat = await query({
654
+ * model: "message",
655
+ * filter: { chatId: "abc-123" },
656
+ * });
657
+ */
658
+ async function query(params) {
659
+ const { archived = false, ascending = false, deleted = false, limit, model, startKey, } = params;
660
+ // Build the combined filter object
661
+ const filterFields = buildFilterObject(params);
662
+ // Get indexes for this model (custom or DEFAULT_INDEXES)
663
+ const indexes = vocabulary.getModelIndexes(model);
664
+ // Select the best matching index
665
+ const selectedIndex = selectBestIndex(indexes, filterFields);
666
+ const indexName = selectedIndex.name ?? generateIndexName(selectedIndex.pk);
667
+ // Build the partition key value
668
+ const suffix = calculateSuffix(archived, deleted);
669
+ const keyValue = buildCompositeKey(filterFields, selectedIndex.pk, suffix);
670
+ // Execute the query
671
+ const docClient = getDocClient();
672
+ const tableName = getTableName();
673
+ const command = new libDynamodb.QueryCommand({
674
+ ExclusiveStartKey: startKey,
675
+ ExpressionAttributeNames: {
676
+ "#pk": indexName,
677
+ },
678
+ ExpressionAttributeValues: {
679
+ ":pkValue": keyValue,
680
+ },
681
+ IndexName: indexName,
682
+ KeyConditionExpression: "#pk = :pkValue",
683
+ ...(limit && { Limit: limit }),
684
+ ScanIndexForward: ascending,
685
+ TableName: tableName,
686
+ });
687
+ const response = await docClient.send(command);
688
+ return {
689
+ items: (response.Items ?? []),
690
+ lastEvaluatedKey: response.LastEvaluatedKey,
691
+ };
692
+ }
693
+ /**
694
+ * Generate an index name from pk fields
695
+ */
696
+ function generateIndexName(pk) {
697
+ const suffix = pk
698
+ .map((field) => field.charAt(0).toUpperCase() + field.slice(1))
699
+ .join("");
700
+ return `index${suffix}`;
701
+ }
702
+
703
+ /**
704
+ * Seed a single entity if it doesn't already exist
705
+ *
706
+ * @param entity - Partial entity with at least alias, model, and ou
707
+ * @returns true if entity was created, false if it already exists
708
+ */
709
+ async function seedEntityIfNotExists(entity) {
710
+ if (!entity.alias || !entity.model || !entity.ou) {
711
+ throw new Error("Entity must have alias, model, and ou to check existence");
712
+ }
713
+ // Check if entity already exists
714
+ const existing = await queryByAlias({
715
+ alias: entity.alias,
716
+ model: entity.model,
717
+ ou: entity.ou,
718
+ });
719
+ if (existing) {
720
+ return false;
721
+ }
722
+ // Generate required fields if missing
723
+ const now = new Date().toISOString();
724
+ const completeEntity = {
725
+ createdAt: entity.createdAt ?? now,
726
+ id: entity.id ?? crypto.randomUUID(),
727
+ model: entity.model,
728
+ name: entity.name ?? entity.alias,
729
+ ou: entity.ou,
730
+ sequence: entity.sequence ?? Date.now(),
731
+ updatedAt: entity.updatedAt ?? now,
732
+ ...entity,
733
+ };
734
+ await putEntity({ entity: completeEntity });
735
+ return true;
736
+ }
737
+ /**
738
+ * Seed multiple entities (idempotent)
739
+ *
740
+ * - Checks existence by alias (via queryByAlias) before creating
741
+ * - Auto-generates id (UUID), createdAt, updatedAt if missing
742
+ * - Skip existing unless replace: true
743
+ * - Returns counts of created/skipped/errors
744
+ *
745
+ * @param entities - Array of partial entities to seed
746
+ * @param options - Seed options
747
+ * @returns Result with created, skipped, and errors arrays
748
+ */
749
+ async function seedEntities(entities, options = {}) {
750
+ const { dryRun = false, replace = false } = options;
751
+ const result = {
752
+ created: [],
753
+ errors: [],
754
+ skipped: [],
755
+ };
756
+ for (const entity of entities) {
757
+ const alias = entity.alias ?? entity.name ?? "unknown";
758
+ try {
759
+ if (!entity.model || !entity.ou) {
760
+ throw new Error("Entity must have model and ou");
761
+ }
762
+ // For entities with alias, check existence
763
+ if (entity.alias) {
764
+ const existing = await queryByAlias({
765
+ alias: entity.alias,
766
+ model: entity.model,
767
+ ou: entity.ou,
768
+ });
769
+ if (existing && !replace) {
770
+ result.skipped.push(alias);
771
+ continue;
772
+ }
773
+ // If replacing, use existing ID to update rather than create new
774
+ if (existing && replace) {
775
+ entity.id = existing.id;
776
+ }
777
+ }
778
+ if (dryRun) {
779
+ result.created.push(alias);
780
+ continue;
781
+ }
782
+ // Generate required fields if missing
783
+ const now = new Date().toISOString();
784
+ const completeEntity = {
785
+ createdAt: entity.createdAt ?? now,
786
+ id: entity.id ?? crypto.randomUUID(),
787
+ model: entity.model,
788
+ name: entity.name ?? entity.alias ?? "Unnamed",
789
+ ou: entity.ou,
790
+ sequence: entity.sequence ?? Date.now(),
791
+ updatedAt: entity.updatedAt ?? now,
792
+ ...entity,
793
+ };
794
+ await putEntity({ entity: completeEntity });
795
+ result.created.push(alias);
796
+ }
797
+ catch (error) {
798
+ const errorMessage = error instanceof Error ? error.message : String(error);
799
+ result.errors.push({ alias, error: errorMessage });
800
+ }
801
+ }
802
+ return result;
803
+ }
804
+ /**
805
+ * Export entities by model and organizational unit
806
+ *
807
+ * - Paginates through all matching entities via queryByOu
808
+ * - Returns entities sorted by sequence (ascending)
809
+ *
810
+ * @param model - The entity model name
811
+ * @param ou - The organizational unit
812
+ * @param limit - Optional maximum number of entities to export
813
+ * @returns Export result with entities and count
814
+ */
815
+ async function exportEntities(model, ou, limit) {
816
+ const entities = [];
817
+ let startKey;
818
+ let remaining = limit;
819
+ do {
820
+ const batchLimit = remaining !== undefined ? Math.min(remaining, 100) : undefined;
821
+ const { items, lastEvaluatedKey } = await queryByOu({
822
+ ascending: true,
823
+ limit: batchLimit,
824
+ model,
825
+ ou,
826
+ startKey,
827
+ });
828
+ entities.push(...items);
829
+ startKey = lastEvaluatedKey;
830
+ if (remaining !== undefined) {
831
+ remaining -= items.length;
832
+ }
833
+ } while (startKey && (remaining === undefined || remaining > 0));
834
+ return {
835
+ count: entities.length,
836
+ entities,
837
+ };
838
+ }
839
+ /**
840
+ * Export entities as a JSON string
841
+ *
842
+ * @param model - The entity model name
843
+ * @param ou - The organizational unit
844
+ * @param pretty - Format JSON with indentation (default: true)
845
+ * @returns JSON string of exported entities
846
+ */
847
+ async function exportEntitiesToJson(model, ou, pretty = true) {
848
+ const { entities } = await exportEntities(model, ou);
849
+ return pretty ? JSON.stringify(entities, null, 2) : JSON.stringify(entities);
850
+ }
851
+
852
+ Object.defineProperty(exports, "APEX", {
853
+ enumerable: true,
854
+ get: function () { return vocabulary.APEX; }
855
+ });
856
+ Object.defineProperty(exports, "ARCHIVED_SUFFIX", {
857
+ enumerable: true,
858
+ get: function () { return vocabulary.ARCHIVED_SUFFIX; }
859
+ });
860
+ Object.defineProperty(exports, "DEFAULT_INDEXES", {
861
+ enumerable: true,
862
+ get: function () { return vocabulary.DEFAULT_INDEXES; }
863
+ });
864
+ Object.defineProperty(exports, "DELETED_SUFFIX", {
865
+ enumerable: true,
866
+ get: function () { return vocabulary.DELETED_SUFFIX; }
867
+ });
868
+ Object.defineProperty(exports, "SEPARATOR", {
869
+ enumerable: true,
870
+ get: function () { return vocabulary.SEPARATOR; }
871
+ });
542
872
  exports.INDEX_ALIAS = INDEX_ALIAS;
543
873
  exports.INDEX_CLASS = INDEX_CLASS;
544
874
  exports.INDEX_OU = INDEX_OU;
545
875
  exports.INDEX_TYPE = INDEX_TYPE;
546
876
  exports.INDEX_XID = INDEX_XID;
547
- exports.SEPARATOR = SEPARATOR;
548
877
  exports.archiveEntity = archiveEntity;
878
+ exports.buildCompositeKey = buildCompositeKey;
549
879
  exports.buildIndexAlias = buildIndexAlias;
550
880
  exports.buildIndexClass = buildIndexClass;
551
881
  exports.buildIndexOu = buildIndexOu;
@@ -554,6 +884,8 @@ exports.buildIndexXid = buildIndexXid;
554
884
  exports.calculateOu = calculateOu;
555
885
  exports.deleteEntity = deleteEntity;
556
886
  exports.destroyEntity = destroyEntity;
887
+ exports.exportEntities = exportEntities;
888
+ exports.exportEntitiesToJson = exportEntitiesToJson;
557
889
  exports.getDocClient = getDocClient;
558
890
  exports.getEntity = getEntity;
559
891
  exports.getTableName = getTableName;
@@ -561,11 +893,14 @@ exports.indexEntity = indexEntity;
561
893
  exports.initClient = initClient;
562
894
  exports.isInitialized = isInitialized;
563
895
  exports.putEntity = putEntity;
896
+ exports.query = query;
564
897
  exports.queryByAlias = queryByAlias;
565
898
  exports.queryByClass = queryByClass;
566
899
  exports.queryByOu = queryByOu;
567
900
  exports.queryByType = queryByType;
568
901
  exports.queryByXid = queryByXid;
569
902
  exports.resetClient = resetClient;
903
+ exports.seedEntities = seedEntities;
904
+ exports.seedEntityIfNotExists = seedEntityIfNotExists;
570
905
  exports.updateEntity = updateEntity;
571
906
  //# sourceMappingURL=index.cjs.map