@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.
- package/dist/cjs/constants.d.ts +1 -4
- package/dist/cjs/entities.d.ts +8 -8
- package/dist/cjs/index.cjs +388 -53
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +6 -2
- package/dist/cjs/keyBuilders.d.ts +17 -2
- package/dist/cjs/mcp/index.cjs +119 -93
- package/dist/cjs/mcp/index.cjs.map +1 -1
- package/dist/cjs/queries.d.ts +6 -6
- package/dist/cjs/query.d.ts +58 -0
- package/dist/cjs/seedExport.d.ts +74 -0
- package/dist/cjs/types.d.ts +9 -11
- package/dist/esm/constants.d.ts +1 -4
- package/dist/esm/entities.d.ts +8 -8
- package/dist/esm/index.d.ts +6 -2
- package/dist/esm/index.js +353 -39
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keyBuilders.d.ts +17 -2
- package/dist/esm/mcp/index.js +109 -83
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/queries.d.ts +6 -6
- package/dist/esm/query.d.ts +58 -0
- package/dist/esm/seedExport.d.ts +74 -0
- package/dist/esm/types.d.ts +9 -11
- package/package.json +2 -2
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { getDocClient, getTableName, initClient, isInitialized, resetClient, } from "./client.js";
|
|
2
2
|
export { APEX, ARCHIVED_SUFFIX, DELETED_SUFFIX, INDEX_ALIAS, INDEX_CLASS, INDEX_OU, INDEX_TYPE, INDEX_XID, SEPARATOR, } from "./constants.js";
|
|
3
3
|
export { archiveEntity, deleteEntity, destroyEntity, getEntity, putEntity, updateEntity, } from "./entities.js";
|
|
4
|
-
export { buildIndexAlias, buildIndexClass, buildIndexOu, buildIndexType, buildIndexXid, calculateOu, indexEntity, } from "./keyBuilders.js";
|
|
4
|
+
export { buildCompositeKey, buildIndexAlias, buildIndexClass, buildIndexOu, buildIndexType, buildIndexXid, calculateOu, DEFAULT_INDEXES, indexEntity, } from "./keyBuilders.js";
|
|
5
5
|
export { queryByAlias, queryByClass, queryByOu, queryByType, queryByXid, } from "./queries.js";
|
|
6
|
-
export
|
|
6
|
+
export { query } from "./query.js";
|
|
7
|
+
export type { QueryParams } from "./query.js";
|
|
8
|
+
export { exportEntities, exportEntitiesToJson, seedEntities, seedEntityIfNotExists, } from "./seedExport.js";
|
|
9
|
+
export type { BaseQueryOptions, DynamoClientConfig, ParentReference, QueryResult, StorableEntity, } from "./types.js";
|
|
10
|
+
export type { ExportResult, SeedOptions, SeedResult } from "./seedExport.js";
|
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, calculateOu as calculateOu$1, serviceHandler, ARCHIVED_SUFFIX, DELETED_SUFFIX } from '@jaypie/vocabulary';
|
|
5
|
+
export { APEX, ARCHIVED_SUFFIX, DEFAULT_INDEXES, DELETED_SUFFIX, SEPARATOR } from '@jaypie/vocabulary';
|
|
5
6
|
|
|
6
7
|
// Environment variable names
|
|
7
8
|
const ENV_AWS_REGION = "AWS_REGION";
|
|
@@ -81,19 +82,17 @@ function resetClient() {
|
|
|
81
82
|
tableName = null;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
const SEPARATOR = "#"; // Composite key separator
|
|
87
|
-
// GSI names
|
|
85
|
+
// Re-export shared constants from vocabulary
|
|
86
|
+
// GSI names (derived from DEFAULT_INDEXES)
|
|
88
87
|
const INDEX_ALIAS = "indexAlias";
|
|
89
88
|
const INDEX_CLASS = "indexClass";
|
|
90
89
|
const INDEX_OU = "indexOu";
|
|
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
97
|
* Build the indexOu key for hierarchical queries
|
|
99
98
|
* @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
|
|
@@ -143,6 +142,20 @@ function buildIndexType(ou, model, type) {
|
|
|
143
142
|
function buildIndexXid(ou, model, xid) {
|
|
144
143
|
return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
|
|
145
144
|
}
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// New Vocabulary-Based Functions
|
|
147
|
+
// =============================================================================
|
|
148
|
+
/**
|
|
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
|
+
}
|
|
146
159
|
/**
|
|
147
160
|
* Calculate the organizational unit from a parent reference
|
|
148
161
|
* @param parent - Optional parent entity reference
|
|
@@ -152,10 +165,14 @@ function calculateOu(parent) {
|
|
|
152
165
|
if (!parent) {
|
|
153
166
|
return APEX;
|
|
154
167
|
}
|
|
155
|
-
return
|
|
168
|
+
return calculateOu$1(parent);
|
|
156
169
|
}
|
|
157
170
|
/**
|
|
158
171
|
* Auto-populate GSI index keys on an entity
|
|
172
|
+
*
|
|
173
|
+
* Uses the model's registered indexes (from vocabulary registry) or
|
|
174
|
+
* DEFAULT_INDEXES if no custom indexes are registered.
|
|
175
|
+
*
|
|
159
176
|
* - indexOu is always populated from ou + model
|
|
160
177
|
* - indexAlias is populated only when alias is present
|
|
161
178
|
* - indexClass is populated only when class 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
|
|
171
|
-
//
|
|
172
|
-
|
|
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 IndexableEntity
|
|
189
|
+
return populateIndexKeys(entity, indexes, suffix);
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
/**
|
|
@@ -233,7 +232,7 @@ const getEntity = serviceHandler({
|
|
|
233
232
|
* Auto-populates GSI index keys via indexEntity
|
|
234
233
|
*
|
|
235
234
|
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
236
|
-
* complex
|
|
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();
|
|
@@ -252,7 +251,7 @@ async function putEntity({ entity, }) {
|
|
|
252
251
|
* Auto-populates GSI index keys and sets updatedAt
|
|
253
252
|
*
|
|
254
253
|
* Note: This is a regular async function (not serviceHandler) because it accepts
|
|
255
|
-
* complex
|
|
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();
|
|
@@ -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
|
}
|
|
@@ -417,7 +416,7 @@ async function executeQuery(indexName, keyValue, options = {}) {
|
|
|
417
416
|
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
418
417
|
*/
|
|
419
418
|
async function queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
|
|
420
|
-
const suffix = calculateSuffix({ archived, deleted });
|
|
419
|
+
const suffix = calculateSuffix$1({ archived, deleted });
|
|
421
420
|
const keyValue = buildIndexOu(ou, model) + suffix;
|
|
422
421
|
return executeQuery(INDEX_OU, keyValue, {
|
|
423
422
|
ascending,
|
|
@@ -455,7 +454,7 @@ const queryByAlias = serviceHandler({
|
|
|
455
454
|
const deletedBool = deleted;
|
|
456
455
|
const modelStr = model;
|
|
457
456
|
const ouStr = ou;
|
|
458
|
-
const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
|
|
457
|
+
const suffix = calculateSuffix$1({ archived: archivedBool, deleted: deletedBool });
|
|
459
458
|
const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
|
|
460
459
|
const result = await executeQuery(INDEX_ALIAS, keyValue, {
|
|
461
460
|
limit: 1,
|
|
@@ -471,7 +470,7 @@ const queryByAlias = serviceHandler({
|
|
|
471
470
|
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
472
471
|
*/
|
|
473
472
|
async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, ou, recordClass, startKey, }) {
|
|
474
|
-
const suffix = calculateSuffix({ archived, deleted });
|
|
473
|
+
const suffix = calculateSuffix$1({ archived, deleted });
|
|
475
474
|
const keyValue = buildIndexClass(ou, model, recordClass) + suffix;
|
|
476
475
|
return executeQuery(INDEX_CLASS, keyValue, {
|
|
477
476
|
ascending,
|
|
@@ -487,7 +486,7 @@ async function queryByClass({ archived = false, ascending = false, deleted = fal
|
|
|
487
486
|
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
488
487
|
*/
|
|
489
488
|
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, type, }) {
|
|
490
|
-
const suffix = calculateSuffix({ archived, deleted });
|
|
489
|
+
const suffix = calculateSuffix$1({ archived, deleted });
|
|
491
490
|
const keyValue = buildIndexType(ou, model, type) + suffix;
|
|
492
491
|
return executeQuery(INDEX_TYPE, keyValue, {
|
|
493
492
|
ascending,
|
|
@@ -525,7 +524,7 @@ const queryByXid = serviceHandler({
|
|
|
525
524
|
const modelStr = model;
|
|
526
525
|
const ouStr = ou;
|
|
527
526
|
const xidStr = xid;
|
|
528
|
-
const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
|
|
527
|
+
const suffix = calculateSuffix$1({ archived: archivedBool, deleted: deletedBool });
|
|
529
528
|
const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
|
|
530
529
|
const result = await executeQuery(INDEX_XID, keyValue, {
|
|
531
530
|
limit: 1,
|
|
@@ -534,5 +533,320 @@ const queryByXid = serviceHandler({
|
|
|
534
533
|
},
|
|
535
534
|
});
|
|
536
535
|
|
|
537
|
-
|
|
536
|
+
/**
|
|
537
|
+
* Unified Query Function with Auto-Detect Index Selection
|
|
538
|
+
*
|
|
539
|
+
* The query() function automatically selects the best index based on
|
|
540
|
+
* the filter fields provided. This simplifies query construction by
|
|
541
|
+
* removing the need to know which specific GSI to use.
|
|
542
|
+
*/
|
|
543
|
+
// =============================================================================
|
|
544
|
+
// Helper Functions
|
|
545
|
+
// =============================================================================
|
|
546
|
+
/**
|
|
547
|
+
* Calculate the suffix based on archived/deleted flags
|
|
548
|
+
*/
|
|
549
|
+
function calculateSuffix(archived, deleted) {
|
|
550
|
+
if (archived && deleted) {
|
|
551
|
+
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
552
|
+
}
|
|
553
|
+
if (archived) {
|
|
554
|
+
return ARCHIVED_SUFFIX;
|
|
555
|
+
}
|
|
556
|
+
if (deleted) {
|
|
557
|
+
return DELETED_SUFFIX;
|
|
558
|
+
}
|
|
559
|
+
return "";
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Build a combined filter object from params
|
|
563
|
+
*/
|
|
564
|
+
function buildFilterObject(params) {
|
|
565
|
+
const result = {
|
|
566
|
+
model: params.model,
|
|
567
|
+
};
|
|
568
|
+
if (params.ou !== undefined) {
|
|
569
|
+
result.ou = params.ou;
|
|
570
|
+
}
|
|
571
|
+
if (params.filter) {
|
|
572
|
+
Object.assign(result, params.filter);
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Score an index based on how well it matches the filter fields
|
|
578
|
+
*/
|
|
579
|
+
function scoreIndex(index, filterFields) {
|
|
580
|
+
let matchedFields = 0;
|
|
581
|
+
let pkComplete = true;
|
|
582
|
+
for (const field of index.pk) {
|
|
583
|
+
if (filterFields[field] !== undefined) {
|
|
584
|
+
matchedFields++;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
pkComplete = false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
index,
|
|
592
|
+
matchedFields,
|
|
593
|
+
pkComplete,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Select the best index for the given filter
|
|
598
|
+
*
|
|
599
|
+
* Scoring criteria:
|
|
600
|
+
* 1. Index must have all pk fields present (pkComplete)
|
|
601
|
+
* 2. Prefer indexes with more matched fields
|
|
602
|
+
* 3. Prefer more specific indexes (more pk fields)
|
|
603
|
+
*/
|
|
604
|
+
function selectBestIndex(indexes, filterFields) {
|
|
605
|
+
const scores = indexes.map((index) => scoreIndex(index, filterFields));
|
|
606
|
+
// Filter to only complete matches
|
|
607
|
+
const completeMatches = scores.filter((s) => s.pkComplete);
|
|
608
|
+
if (completeMatches.length === 0) {
|
|
609
|
+
const availableIndexes = indexes
|
|
610
|
+
.map((i) => i.name ?? `[${i.pk.join(", ")}]`)
|
|
611
|
+
.join(", ");
|
|
612
|
+
const providedFields = Object.keys(filterFields).join(", ");
|
|
613
|
+
throw new ConfigurationError(`No index matches filter fields. ` +
|
|
614
|
+
`Provided: ${providedFields}. ` +
|
|
615
|
+
`Available indexes: ${availableIndexes}`);
|
|
616
|
+
}
|
|
617
|
+
// Sort by:
|
|
618
|
+
// 1. More matched fields first (descending)
|
|
619
|
+
// 2. More pk fields (more specific) first (descending)
|
|
620
|
+
completeMatches.sort((a, b) => {
|
|
621
|
+
const fieldDiff = b.matchedFields - a.matchedFields;
|
|
622
|
+
if (fieldDiff !== 0)
|
|
623
|
+
return fieldDiff;
|
|
624
|
+
return b.index.pk.length - a.index.pk.length;
|
|
625
|
+
});
|
|
626
|
+
return completeMatches[0].index;
|
|
627
|
+
}
|
|
628
|
+
// =============================================================================
|
|
629
|
+
// Main Query Function
|
|
630
|
+
// =============================================================================
|
|
631
|
+
/**
|
|
632
|
+
* Query entities with automatic index selection
|
|
633
|
+
*
|
|
634
|
+
* The query function automatically selects the best GSI based on
|
|
635
|
+
* the filter fields provided. This removes the need to know which
|
|
636
|
+
* specific query function (queryByOu, queryByAlias, etc.) to use.
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* // Uses indexOu (pk: ["ou", "model"])
|
|
640
|
+
* const allMessages = await query({ model: "message", ou: `chat#${chatId}` });
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* // Uses indexAlias (pk: ["ou", "model", "alias"])
|
|
644
|
+
* const byAlias = await query({
|
|
645
|
+
* model: "record",
|
|
646
|
+
* ou: "@",
|
|
647
|
+
* filter: { alias: "my-record" },
|
|
648
|
+
* });
|
|
649
|
+
*
|
|
650
|
+
* @example
|
|
651
|
+
* // Uses a custom registered index if model has one
|
|
652
|
+
* const byChat = await query({
|
|
653
|
+
* model: "message",
|
|
654
|
+
* filter: { chatId: "abc-123" },
|
|
655
|
+
* });
|
|
656
|
+
*/
|
|
657
|
+
async function query(params) {
|
|
658
|
+
const { archived = false, ascending = false, deleted = false, limit, model, startKey, } = params;
|
|
659
|
+
// Build the combined filter object
|
|
660
|
+
const filterFields = buildFilterObject(params);
|
|
661
|
+
// Get indexes for this model (custom or DEFAULT_INDEXES)
|
|
662
|
+
const indexes = getModelIndexes(model);
|
|
663
|
+
// Select the best matching index
|
|
664
|
+
const selectedIndex = selectBestIndex(indexes, filterFields);
|
|
665
|
+
const indexName = selectedIndex.name ?? generateIndexName(selectedIndex.pk);
|
|
666
|
+
// Build the partition key value
|
|
667
|
+
const suffix = calculateSuffix(archived, deleted);
|
|
668
|
+
const keyValue = buildCompositeKey(filterFields, selectedIndex.pk, suffix);
|
|
669
|
+
// Execute the query
|
|
670
|
+
const docClient = getDocClient();
|
|
671
|
+
const tableName = getTableName();
|
|
672
|
+
const command = new QueryCommand({
|
|
673
|
+
ExclusiveStartKey: startKey,
|
|
674
|
+
ExpressionAttributeNames: {
|
|
675
|
+
"#pk": indexName,
|
|
676
|
+
},
|
|
677
|
+
ExpressionAttributeValues: {
|
|
678
|
+
":pkValue": keyValue,
|
|
679
|
+
},
|
|
680
|
+
IndexName: indexName,
|
|
681
|
+
KeyConditionExpression: "#pk = :pkValue",
|
|
682
|
+
...(limit && { Limit: limit }),
|
|
683
|
+
ScanIndexForward: ascending,
|
|
684
|
+
TableName: tableName,
|
|
685
|
+
});
|
|
686
|
+
const response = await docClient.send(command);
|
|
687
|
+
return {
|
|
688
|
+
items: (response.Items ?? []),
|
|
689
|
+
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Generate an index name from pk fields
|
|
694
|
+
*/
|
|
695
|
+
function generateIndexName(pk) {
|
|
696
|
+
const suffix = pk
|
|
697
|
+
.map((field) => field.charAt(0).toUpperCase() + field.slice(1))
|
|
698
|
+
.join("");
|
|
699
|
+
return `index${suffix}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Seed a single entity if it doesn't already exist
|
|
704
|
+
*
|
|
705
|
+
* @param entity - Partial entity with at least alias, model, and ou
|
|
706
|
+
* @returns true if entity was created, false if it already exists
|
|
707
|
+
*/
|
|
708
|
+
async function seedEntityIfNotExists(entity) {
|
|
709
|
+
if (!entity.alias || !entity.model || !entity.ou) {
|
|
710
|
+
throw new Error("Entity must have alias, model, and ou to check existence");
|
|
711
|
+
}
|
|
712
|
+
// Check if entity already exists
|
|
713
|
+
const existing = await queryByAlias({
|
|
714
|
+
alias: entity.alias,
|
|
715
|
+
model: entity.model,
|
|
716
|
+
ou: entity.ou,
|
|
717
|
+
});
|
|
718
|
+
if (existing) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
// Generate required fields if missing
|
|
722
|
+
const now = new Date().toISOString();
|
|
723
|
+
const completeEntity = {
|
|
724
|
+
createdAt: entity.createdAt ?? now,
|
|
725
|
+
id: entity.id ?? crypto.randomUUID(),
|
|
726
|
+
model: entity.model,
|
|
727
|
+
name: entity.name ?? entity.alias,
|
|
728
|
+
ou: entity.ou,
|
|
729
|
+
sequence: entity.sequence ?? Date.now(),
|
|
730
|
+
updatedAt: entity.updatedAt ?? now,
|
|
731
|
+
...entity,
|
|
732
|
+
};
|
|
733
|
+
await putEntity({ entity: completeEntity });
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Seed multiple entities (idempotent)
|
|
738
|
+
*
|
|
739
|
+
* - Checks existence by alias (via queryByAlias) before creating
|
|
740
|
+
* - Auto-generates id (UUID), createdAt, updatedAt if missing
|
|
741
|
+
* - Skip existing unless replace: true
|
|
742
|
+
* - Returns counts of created/skipped/errors
|
|
743
|
+
*
|
|
744
|
+
* @param entities - Array of partial entities to seed
|
|
745
|
+
* @param options - Seed options
|
|
746
|
+
* @returns Result with created, skipped, and errors arrays
|
|
747
|
+
*/
|
|
748
|
+
async function seedEntities(entities, options = {}) {
|
|
749
|
+
const { dryRun = false, replace = false } = options;
|
|
750
|
+
const result = {
|
|
751
|
+
created: [],
|
|
752
|
+
errors: [],
|
|
753
|
+
skipped: [],
|
|
754
|
+
};
|
|
755
|
+
for (const entity of entities) {
|
|
756
|
+
const alias = entity.alias ?? entity.name ?? "unknown";
|
|
757
|
+
try {
|
|
758
|
+
if (!entity.model || !entity.ou) {
|
|
759
|
+
throw new Error("Entity must have model and ou");
|
|
760
|
+
}
|
|
761
|
+
// For entities with alias, check existence
|
|
762
|
+
if (entity.alias) {
|
|
763
|
+
const existing = await queryByAlias({
|
|
764
|
+
alias: entity.alias,
|
|
765
|
+
model: entity.model,
|
|
766
|
+
ou: entity.ou,
|
|
767
|
+
});
|
|
768
|
+
if (existing && !replace) {
|
|
769
|
+
result.skipped.push(alias);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
// If replacing, use existing ID to update rather than create new
|
|
773
|
+
if (existing && replace) {
|
|
774
|
+
entity.id = existing.id;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (dryRun) {
|
|
778
|
+
result.created.push(alias);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
// Generate required fields if missing
|
|
782
|
+
const now = new Date().toISOString();
|
|
783
|
+
const completeEntity = {
|
|
784
|
+
createdAt: entity.createdAt ?? now,
|
|
785
|
+
id: entity.id ?? crypto.randomUUID(),
|
|
786
|
+
model: entity.model,
|
|
787
|
+
name: entity.name ?? entity.alias ?? "Unnamed",
|
|
788
|
+
ou: entity.ou,
|
|
789
|
+
sequence: entity.sequence ?? Date.now(),
|
|
790
|
+
updatedAt: entity.updatedAt ?? now,
|
|
791
|
+
...entity,
|
|
792
|
+
};
|
|
793
|
+
await putEntity({ entity: completeEntity });
|
|
794
|
+
result.created.push(alias);
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
798
|
+
result.errors.push({ alias, error: errorMessage });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return result;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Export entities by model and organizational unit
|
|
805
|
+
*
|
|
806
|
+
* - Paginates through all matching entities via queryByOu
|
|
807
|
+
* - Returns entities sorted by sequence (ascending)
|
|
808
|
+
*
|
|
809
|
+
* @param model - The entity model name
|
|
810
|
+
* @param ou - The organizational unit
|
|
811
|
+
* @param limit - Optional maximum number of entities to export
|
|
812
|
+
* @returns Export result with entities and count
|
|
813
|
+
*/
|
|
814
|
+
async function exportEntities(model, ou, limit) {
|
|
815
|
+
const entities = [];
|
|
816
|
+
let startKey;
|
|
817
|
+
let remaining = limit;
|
|
818
|
+
do {
|
|
819
|
+
const batchLimit = remaining !== undefined ? Math.min(remaining, 100) : undefined;
|
|
820
|
+
const { items, lastEvaluatedKey } = await queryByOu({
|
|
821
|
+
ascending: true,
|
|
822
|
+
limit: batchLimit,
|
|
823
|
+
model,
|
|
824
|
+
ou,
|
|
825
|
+
startKey,
|
|
826
|
+
});
|
|
827
|
+
entities.push(...items);
|
|
828
|
+
startKey = lastEvaluatedKey;
|
|
829
|
+
if (remaining !== undefined) {
|
|
830
|
+
remaining -= items.length;
|
|
831
|
+
}
|
|
832
|
+
} while (startKey && (remaining === undefined || remaining > 0));
|
|
833
|
+
return {
|
|
834
|
+
count: entities.length,
|
|
835
|
+
entities,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Export entities as a JSON string
|
|
840
|
+
*
|
|
841
|
+
* @param model - The entity model name
|
|
842
|
+
* @param ou - The organizational unit
|
|
843
|
+
* @param pretty - Format JSON with indentation (default: true)
|
|
844
|
+
* @returns JSON string of exported entities
|
|
845
|
+
*/
|
|
846
|
+
async function exportEntitiesToJson(model, ou, pretty = true) {
|
|
847
|
+
const { entities } = await exportEntities(model, ou);
|
|
848
|
+
return pretty ? JSON.stringify(entities, null, 2) : JSON.stringify(entities);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export { INDEX_ALIAS, INDEX_CLASS, INDEX_OU, INDEX_TYPE, INDEX_XID, archiveEntity, buildCompositeKey, buildIndexAlias, buildIndexClass, buildIndexOu, buildIndexType, buildIndexXid, calculateOu, deleteEntity, destroyEntity, exportEntities, exportEntitiesToJson, getDocClient, getEntity, getTableName, indexEntity, initClient, isInitialized, putEntity, query, queryByAlias, queryByClass, queryByOu, queryByType, queryByXid, resetClient, seedEntities, seedEntityIfNotExists, updateEntity };
|
|
538
852
|
//# sourceMappingURL=index.js.map
|