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