@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/cjs/constants.d.ts +2 -5
- package/dist/cjs/entities.d.ts +13 -13
- package/dist/cjs/index.cjs +312 -124
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +6 -4
- package/dist/cjs/keyBuilders.d.ts +36 -21
- package/dist/cjs/mcp/admin/createTable.d.ts +1 -1
- package/dist/cjs/mcp/admin/dockerCompose.d.ts +1 -1
- package/dist/cjs/mcp/admin/status.d.ts +1 -1
- package/dist/cjs/mcp/index.cjs +245 -197
- package/dist/cjs/mcp/index.cjs.map +1 -1
- package/dist/cjs/queries.d.ts +16 -16
- package/dist/cjs/query.d.ts +58 -0
- package/dist/cjs/seedExport.d.ts +11 -11
- package/dist/cjs/types.d.ts +25 -27
- package/dist/esm/constants.d.ts +2 -5
- package/dist/esm/entities.d.ts +13 -13
- package/dist/esm/index.d.ts +6 -4
- package/dist/esm/index.js +281 -110
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keyBuilders.d.ts +36 -21
- package/dist/esm/mcp/admin/createTable.d.ts +1 -1
- package/dist/esm/mcp/admin/dockerCompose.d.ts +1 -1
- package/dist/esm/mcp/admin/status.d.ts +1 -1
- package/dist/esm/mcp/index.js +239 -191
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/queries.d.ts +16 -16
- package/dist/esm/query.d.ts +58 -0
- package/dist/esm/seedExport.d.ts +11 -11
- package/dist/esm/types.d.ts +25 -27
- package/package.json +2 -2
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 {
|
|
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
|
-
//
|
|
85
|
-
|
|
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
|
|
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
|
|
99
|
-
* @param
|
|
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: "{
|
|
100
|
+
* @returns Composite key: "{scope}#{model}"
|
|
102
101
|
*/
|
|
103
|
-
function
|
|
104
|
-
return `${
|
|
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
|
|
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: "{
|
|
110
|
+
* @returns Composite key: "{scope}#{model}#{alias}"
|
|
112
111
|
*/
|
|
113
|
-
function buildIndexAlias(
|
|
114
|
-
return `${
|
|
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
|
|
117
|
+
* @param scope - The scope
|
|
119
118
|
* @param model - The entity model name
|
|
120
119
|
* @param recordClass - The category classification
|
|
121
|
-
* @returns Composite key: "{
|
|
120
|
+
* @returns Composite key: "{scope}#{model}#{class}"
|
|
122
121
|
*/
|
|
123
|
-
function buildIndexClass(
|
|
124
|
-
return `${
|
|
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
|
|
127
|
+
* @param scope - The scope
|
|
129
128
|
* @param model - The entity model name
|
|
130
129
|
* @param type - The type classification
|
|
131
|
-
* @returns Composite key: "{
|
|
130
|
+
* @returns Composite key: "{scope}#{model}#{type}"
|
|
132
131
|
*/
|
|
133
|
-
function buildIndexType(
|
|
134
|
-
return `${
|
|
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
|
|
137
|
+
* @param scope - The scope
|
|
139
138
|
* @param model - The entity model name
|
|
140
139
|
* @param xid - The external ID
|
|
141
|
-
* @returns Composite key: "{
|
|
140
|
+
* @returns Composite key: "{scope}#{model}#{xid}"
|
|
142
141
|
*/
|
|
143
|
-
function buildIndexXid(
|
|
144
|
-
return `${
|
|
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
|
-
*
|
|
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
|
|
164
|
+
function calculateScope(parent) {
|
|
152
165
|
if (!parent) {
|
|
153
166
|
return APEX;
|
|
154
167
|
}
|
|
155
|
-
return
|
|
168
|
+
return calculateScope$1(parent);
|
|
156
169
|
}
|
|
157
170
|
/**
|
|
158
171
|
* Auto-populate GSI index keys on an entity
|
|
159
|
-
*
|
|
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
|
|
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 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 =
|
|
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
|
|
236
|
-
* complex
|
|
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
|
|
255
|
-
* complex
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
414
|
-
* Uses
|
|
412
|
+
* Query entities by scope (parent hierarchy)
|
|
413
|
+
* Uses indexScope GSI
|
|
415
414
|
*
|
|
416
|
-
* Note: This is a regular async function (not
|
|
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
|
|
420
|
-
const suffix = calculateSuffix({ archived, deleted });
|
|
421
|
-
const keyValue =
|
|
422
|
-
return executeQuery(
|
|
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 =
|
|
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
|
-
|
|
449
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
451
450
|
},
|
|
452
|
-
service: async ({ alias, archived, deleted, model,
|
|
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
|
|
458
|
-
const suffix = calculateSuffix({
|
|
459
|
-
|
|
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
|
|
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,
|
|
474
|
-
const suffix = calculateSuffix({ archived, deleted });
|
|
475
|
-
const keyValue = buildIndexClass(
|
|
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
|
|
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,
|
|
490
|
-
const suffix = calculateSuffix({ archived, deleted });
|
|
491
|
-
const keyValue = buildIndexType(
|
|
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 =
|
|
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
|
-
|
|
521
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
520
522
|
xid: { type: String, description: "External ID" },
|
|
521
523
|
},
|
|
522
|
-
service: async ({ archived, deleted, model,
|
|
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
|
|
528
|
+
const scopeStr = scope;
|
|
527
529
|
const xidStr = xid;
|
|
528
|
-
const suffix = calculateSuffix({
|
|
529
|
-
|
|
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
|
|
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.
|
|
545
|
-
throw new Error("Entity must have alias, model, and
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
594
|
-
throw new Error("Entity must have model and
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
810
|
+
* Export entities by model and scope
|
|
640
811
|
*
|
|
641
|
-
* - Paginates through all matching entities via
|
|
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
|
|
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,
|
|
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
|
|
826
|
+
const { items, lastEvaluatedKey } = await queryByScope({
|
|
656
827
|
ascending: true,
|
|
657
828
|
limit: batchLimit,
|
|
658
829
|
model,
|
|
659
|
-
|
|
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
|
|
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,
|
|
682
|
-
const { entities } = await exportEntities(model,
|
|
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 {
|
|
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
|