@jaypie/dynamodb 0.4.3 → 0.5.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 +0 -5
- package/dist/cjs/entities.d.ts +5 -11
- package/dist/cjs/index.cjs +188 -319
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/keyBuilders.d.ts +11 -49
- package/dist/cjs/mcp/index.cjs +164 -213
- package/dist/cjs/mcp/index.cjs.map +1 -1
- package/dist/cjs/queries.d.ts +39 -41
- package/dist/cjs/query.d.ts +20 -20
- package/dist/cjs/types.d.ts +26 -41
- package/dist/esm/constants.d.ts +0 -5
- package/dist/esm/entities.d.ts +5 -11
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +190 -311
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keyBuilders.d.ts +11 -49
- package/dist/esm/mcp/index.js +165 -214
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/queries.d.ts +39 -41
- package/dist/esm/query.d.ts +20 -20
- package/dist/esm/types.d.ts +26 -41
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
2
|
import { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, TransactWriteCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
|
|
3
3
|
import { ConfigurationError } from '@jaypie/errors';
|
|
4
|
-
import { getModelIndexes, populateIndexKeys,
|
|
4
|
+
import { getModelIndexes, populateIndexKeys, buildCompositeKey as buildCompositeKey$1, APEX, calculateScope as calculateScope$1, fabricService, ARCHIVED_SUFFIX, DELETED_SUFFIX, getGsiAttributeNames, SEPARATOR } from '@jaypie/fabric';
|
|
5
5
|
export { APEX, ARCHIVED_SUFFIX, DELETED_SUFFIX, SEPARATOR } from '@jaypie/fabric';
|
|
6
6
|
|
|
7
7
|
// Environment variable names
|
|
@@ -82,69 +82,9 @@ function resetClient() {
|
|
|
82
82
|
tableName = null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Re-export shared constants from fabric
|
|
86
|
-
// GSI names
|
|
87
|
-
const INDEX_ALIAS = "indexAlias";
|
|
88
|
-
const INDEX_CATEGORY = "indexCategory";
|
|
89
|
-
const INDEX_SCOPE = "indexScope";
|
|
90
|
-
const INDEX_TYPE = "indexType";
|
|
91
|
-
const INDEX_XID = "indexXid";
|
|
92
|
-
|
|
93
85
|
// =============================================================================
|
|
94
86
|
// Key Builders
|
|
95
87
|
// =============================================================================
|
|
96
|
-
/**
|
|
97
|
-
* Build the indexScope key for hierarchical queries
|
|
98
|
-
* @param scope - The scope (APEX or "{parent.model}#{parent.id}")
|
|
99
|
-
* @param model - The entity model name
|
|
100
|
-
* @returns Composite key: "{scope}#{model}"
|
|
101
|
-
*/
|
|
102
|
-
function buildIndexScope(scope, model) {
|
|
103
|
-
return `${scope}${SEPARATOR}${model}`;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Build the indexAlias key for human-friendly lookups
|
|
107
|
-
* @param scope - The scope
|
|
108
|
-
* @param model - The entity model name
|
|
109
|
-
* @param alias - The human-friendly alias
|
|
110
|
-
* @returns Composite key: "{scope}#{model}#{alias}"
|
|
111
|
-
*/
|
|
112
|
-
function buildIndexAlias(scope, model, alias) {
|
|
113
|
-
return `${scope}${SEPARATOR}${model}${SEPARATOR}${alias}`;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Build the indexCategory key for category filtering
|
|
117
|
-
* @param scope - The scope
|
|
118
|
-
* @param model - The entity model name
|
|
119
|
-
* @param category - The category classification
|
|
120
|
-
* @returns Composite key: "{scope}#{model}#{category}"
|
|
121
|
-
*/
|
|
122
|
-
function buildIndexCategory(scope, model, category) {
|
|
123
|
-
return `${scope}${SEPARATOR}${model}${SEPARATOR}${category}`;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Build the indexType key for type filtering
|
|
127
|
-
* @param scope - The scope
|
|
128
|
-
* @param model - The entity model name
|
|
129
|
-
* @param type - The type classification
|
|
130
|
-
* @returns Composite key: "{scope}#{model}#{type}"
|
|
131
|
-
*/
|
|
132
|
-
function buildIndexType(scope, model, type) {
|
|
133
|
-
return `${scope}${SEPARATOR}${model}${SEPARATOR}${type}`;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Build the indexXid key for external ID lookups
|
|
137
|
-
* @param scope - The scope
|
|
138
|
-
* @param model - The entity model name
|
|
139
|
-
* @param xid - The external ID
|
|
140
|
-
* @returns Composite key: "{scope}#{model}#{xid}"
|
|
141
|
-
*/
|
|
142
|
-
function buildIndexXid(scope, model, xid) {
|
|
143
|
-
return `${scope}${SEPARATOR}${model}${SEPARATOR}${xid}`;
|
|
144
|
-
}
|
|
145
|
-
// =============================================================================
|
|
146
|
-
// New Vocabulary-Based Functions
|
|
147
|
-
// =============================================================================
|
|
148
88
|
/**
|
|
149
89
|
* Build a composite key from entity fields
|
|
150
90
|
*
|
|
@@ -168,24 +108,30 @@ function calculateScope(parent) {
|
|
|
168
108
|
return calculateScope$1(parent);
|
|
169
109
|
}
|
|
170
110
|
/**
|
|
171
|
-
* Auto-populate GSI index keys on an entity
|
|
111
|
+
* Auto-populate GSI index keys on an entity and advance its write timestamps.
|
|
172
112
|
*
|
|
173
|
-
*
|
|
113
|
+
* - Bumps `updatedAt` to now on every call.
|
|
114
|
+
* - Backfills `createdAt` to the same now if not already set.
|
|
115
|
+
* - Populates GSI attributes (pk composite and sk composite when applicable)
|
|
116
|
+
* using the indexes registered for the entity's model.
|
|
174
117
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* - indexType is populated only when type is present
|
|
179
|
-
* - indexXid is populated only when xid is present
|
|
118
|
+
* Callers (putEntity, updateEntity, deleteEntity, archiveEntity,
|
|
119
|
+
* transactWriteEntities) go through this one function so `updatedAt` is
|
|
120
|
+
* always fresh and never forgotten.
|
|
180
121
|
*
|
|
181
|
-
* @param entity - The entity to
|
|
182
|
-
* @param suffix - Optional suffix
|
|
183
|
-
* @returns
|
|
122
|
+
* @param entity - The entity to index
|
|
123
|
+
* @param suffix - Optional suffix override (defaults to archived/deleted state)
|
|
124
|
+
* @returns A new entity with timestamps bumped and index keys populated
|
|
184
125
|
*/
|
|
185
|
-
function indexEntity(entity, suffix
|
|
126
|
+
function indexEntity(entity, suffix) {
|
|
127
|
+
const now = new Date().toISOString();
|
|
128
|
+
const bumped = {
|
|
129
|
+
...entity,
|
|
130
|
+
createdAt: entity.createdAt ?? now,
|
|
131
|
+
updatedAt: now,
|
|
132
|
+
};
|
|
186
133
|
const indexes = getModelIndexes(entity.model);
|
|
187
|
-
|
|
188
|
-
return populateIndexKeys(entity, indexes, suffix);
|
|
134
|
+
return populateIndexKeys(bumped, indexes, suffix);
|
|
189
135
|
}
|
|
190
136
|
|
|
191
137
|
/**
|
|
@@ -206,20 +152,19 @@ function calculateEntitySuffix(entity) {
|
|
|
206
152
|
return "";
|
|
207
153
|
}
|
|
208
154
|
/**
|
|
209
|
-
* Get a single entity by primary key
|
|
155
|
+
* Get a single entity by primary key (id)
|
|
210
156
|
*/
|
|
211
157
|
const getEntity = fabricService({
|
|
212
158
|
alias: "getEntity",
|
|
213
|
-
description: "Get a single entity by
|
|
159
|
+
description: "Get a single entity by id",
|
|
214
160
|
input: {
|
|
215
|
-
id: { type: String, description: "Entity
|
|
216
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
161
|
+
id: { type: String, description: "Entity id (partition key)" },
|
|
217
162
|
},
|
|
218
|
-
service: async ({ id
|
|
163
|
+
service: async ({ id }) => {
|
|
219
164
|
const docClient = getDocClient();
|
|
220
165
|
const tableName = getTableName();
|
|
221
166
|
const command = new GetCommand({
|
|
222
|
-
Key: { id
|
|
167
|
+
Key: { id },
|
|
223
168
|
TableName: tableName,
|
|
224
169
|
});
|
|
225
170
|
const response = await docClient.send(command);
|
|
@@ -227,16 +172,12 @@ const getEntity = fabricService({
|
|
|
227
172
|
},
|
|
228
173
|
});
|
|
229
174
|
/**
|
|
230
|
-
* Put (create or replace) an entity
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
234
|
-
* complex StorableEntity objects that can't be coerced by vocabulary's type system.
|
|
175
|
+
* Put (create or replace) an entity.
|
|
176
|
+
* `indexEntity` auto-bumps `updatedAt` and backfills `createdAt`.
|
|
235
177
|
*/
|
|
236
178
|
async function putEntity({ entity, }) {
|
|
237
179
|
const docClient = getDocClient();
|
|
238
180
|
const tableName = getTableName();
|
|
239
|
-
// Auto-populate index keys
|
|
240
181
|
const indexedEntity = indexEntity(entity);
|
|
241
182
|
const command = new PutCommand({
|
|
242
183
|
Item: indexedEntity,
|
|
@@ -246,20 +187,13 @@ async function putEntity({ entity, }) {
|
|
|
246
187
|
return indexedEntity;
|
|
247
188
|
}
|
|
248
189
|
/**
|
|
249
|
-
* Update an existing entity
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
253
|
-
* complex StorableEntity objects that can't be coerced by vocabulary's type system.
|
|
190
|
+
* Update an existing entity.
|
|
191
|
+
* `indexEntity` auto-bumps `updatedAt` — callers never set it manually.
|
|
254
192
|
*/
|
|
255
193
|
async function updateEntity({ entity, }) {
|
|
256
194
|
const docClient = getDocClient();
|
|
257
195
|
const tableName = getTableName();
|
|
258
|
-
|
|
259
|
-
const updatedEntity = indexEntity({
|
|
260
|
-
...entity,
|
|
261
|
-
updatedAt: new Date().toISOString(),
|
|
262
|
-
});
|
|
196
|
+
const updatedEntity = indexEntity(entity);
|
|
263
197
|
const command = new PutCommand({
|
|
264
198
|
Item: updatedEntity,
|
|
265
199
|
TableName: tableName,
|
|
@@ -275,25 +209,21 @@ const deleteEntity = fabricService({
|
|
|
275
209
|
alias: "deleteEntity",
|
|
276
210
|
description: "Soft delete an entity (sets deletedAt timestamp)",
|
|
277
211
|
input: {
|
|
278
|
-
id: { type: String, description: "Entity
|
|
279
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
212
|
+
id: { type: String, description: "Entity id" },
|
|
280
213
|
},
|
|
281
|
-
service: async ({ id
|
|
214
|
+
service: async ({ id }) => {
|
|
282
215
|
const docClient = getDocClient();
|
|
283
216
|
const tableName = getTableName();
|
|
284
|
-
|
|
285
|
-
const existing = await getEntity({ id, model });
|
|
217
|
+
const existing = await getEntity({ id });
|
|
286
218
|
if (!existing) {
|
|
287
219
|
return false;
|
|
288
220
|
}
|
|
289
221
|
const now = new Date().toISOString();
|
|
290
|
-
//
|
|
222
|
+
// indexEntity will bump updatedAt again; set deletedAt here.
|
|
291
223
|
const updatedEntity = {
|
|
292
224
|
...existing,
|
|
293
225
|
deletedAt: now,
|
|
294
|
-
updatedAt: now,
|
|
295
226
|
};
|
|
296
|
-
// Calculate suffix based on combined state (may already be archived)
|
|
297
227
|
const suffix = calculateEntitySuffix(updatedEntity);
|
|
298
228
|
const deletedEntity = indexEntity(updatedEntity, suffix);
|
|
299
229
|
const command = new PutCommand({
|
|
@@ -312,25 +242,20 @@ const archiveEntity = fabricService({
|
|
|
312
242
|
alias: "archiveEntity",
|
|
313
243
|
description: "Archive an entity (sets archivedAt timestamp)",
|
|
314
244
|
input: {
|
|
315
|
-
id: { type: String, description: "Entity
|
|
316
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
245
|
+
id: { type: String, description: "Entity id" },
|
|
317
246
|
},
|
|
318
|
-
service: async ({ id
|
|
247
|
+
service: async ({ id }) => {
|
|
319
248
|
const docClient = getDocClient();
|
|
320
249
|
const tableName = getTableName();
|
|
321
|
-
|
|
322
|
-
const existing = await getEntity({ id, model });
|
|
250
|
+
const existing = await getEntity({ id });
|
|
323
251
|
if (!existing) {
|
|
324
252
|
return false;
|
|
325
253
|
}
|
|
326
254
|
const now = new Date().toISOString();
|
|
327
|
-
// Build updated entity with archivedAt
|
|
328
255
|
const updatedEntity = {
|
|
329
256
|
...existing,
|
|
330
257
|
archivedAt: now,
|
|
331
|
-
updatedAt: now,
|
|
332
258
|
};
|
|
333
|
-
// Calculate suffix based on combined state (may already be deleted)
|
|
334
259
|
const suffix = calculateEntitySuffix(updatedEntity);
|
|
335
260
|
const archivedEntity = indexEntity(updatedEntity, suffix);
|
|
336
261
|
const command = new PutCommand({
|
|
@@ -349,14 +274,13 @@ const destroyEntity = fabricService({
|
|
|
349
274
|
alias: "destroyEntity",
|
|
350
275
|
description: "Hard delete an entity (permanently removes from table)",
|
|
351
276
|
input: {
|
|
352
|
-
id: { type: String, description: "Entity
|
|
353
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
277
|
+
id: { type: String, description: "Entity id" },
|
|
354
278
|
},
|
|
355
|
-
service: async ({ id
|
|
279
|
+
service: async ({ id }) => {
|
|
356
280
|
const docClient = getDocClient();
|
|
357
281
|
const tableName = getTableName();
|
|
358
282
|
const command = new DeleteCommand({
|
|
359
|
-
Key: { id
|
|
283
|
+
Key: { id },
|
|
360
284
|
TableName: tableName,
|
|
361
285
|
});
|
|
362
286
|
await docClient.send(command);
|
|
@@ -382,11 +306,15 @@ async function transactWriteEntities({ entities, }) {
|
|
|
382
306
|
await docClient.send(command);
|
|
383
307
|
}
|
|
384
308
|
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// Helpers
|
|
311
|
+
// =============================================================================
|
|
385
312
|
/**
|
|
386
|
-
* Calculate the suffix based on archived/deleted
|
|
387
|
-
*
|
|
313
|
+
* Calculate the suffix for the GSI partition key based on archived/deleted
|
|
314
|
+
* flags. Suffix stays on pk so deleted/archived entities are queried as their
|
|
315
|
+
* own partition (active queries skip them naturally).
|
|
388
316
|
*/
|
|
389
|
-
function calculateSuffix
|
|
317
|
+
function calculateSuffix({ archived, deleted, }) {
|
|
390
318
|
if (archived && deleted) {
|
|
391
319
|
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
392
320
|
}
|
|
@@ -399,22 +327,51 @@ function calculateSuffix$1({ archived, deleted, }) {
|
|
|
399
327
|
return "";
|
|
400
328
|
}
|
|
401
329
|
/**
|
|
402
|
-
*
|
|
330
|
+
* Find the registered index for a model that matches a given partition-key
|
|
331
|
+
* shape. The matching index is the first one whose pk equals the expected
|
|
332
|
+
* fields. Throws ConfigurationError if no match is found.
|
|
403
333
|
*/
|
|
404
|
-
|
|
405
|
-
const
|
|
334
|
+
function requireIndex(model, pkFields) {
|
|
335
|
+
const indexes = getModelIndexes(model);
|
|
336
|
+
const match = indexes.find((index) => index.pk.length === pkFields.length &&
|
|
337
|
+
index.pk.every((field, i) => field === pkFields[i]));
|
|
338
|
+
if (!match) {
|
|
339
|
+
throw new ConfigurationError(`Model "${model}" has no index with pk=[${pkFields.join(", ")}]. ` +
|
|
340
|
+
`Register one with fabricIndex(${pkFields.length > 1 ? `"${pkFields[1]}"` : ""}).`);
|
|
341
|
+
}
|
|
342
|
+
return match;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Execute a GSI query.
|
|
346
|
+
*
|
|
347
|
+
* - pk: exact match on the index partition key
|
|
348
|
+
* - skPrefix: optional begins_with on the index sort key (used when the index
|
|
349
|
+
* has a composite sk like [scope, updatedAt])
|
|
350
|
+
*/
|
|
351
|
+
async function executeQuery(index, pkValue, options = {}) {
|
|
352
|
+
const { ascending = false, limit, skPrefix, startKey } = options;
|
|
353
|
+
const attrs = getGsiAttributeNames(index);
|
|
354
|
+
const indexName = attrs.pk;
|
|
355
|
+
const expressionAttributeNames = {
|
|
356
|
+
"#pk": indexName,
|
|
357
|
+
};
|
|
358
|
+
const expressionAttributeValues = {
|
|
359
|
+
":pkValue": pkValue,
|
|
360
|
+
};
|
|
361
|
+
let keyConditionExpression = "#pk = :pkValue";
|
|
362
|
+
if (skPrefix !== undefined && attrs.sk) {
|
|
363
|
+
expressionAttributeNames["#sk"] = attrs.sk;
|
|
364
|
+
expressionAttributeValues[":skPrefix"] = skPrefix;
|
|
365
|
+
keyConditionExpression += " AND begins_with(#sk, :skPrefix)";
|
|
366
|
+
}
|
|
406
367
|
const docClient = getDocClient();
|
|
407
368
|
const tableName = getTableName();
|
|
408
369
|
const command = new QueryCommand({
|
|
409
370
|
ExclusiveStartKey: startKey,
|
|
410
|
-
ExpressionAttributeNames:
|
|
411
|
-
|
|
412
|
-
},
|
|
413
|
-
ExpressionAttributeValues: {
|
|
414
|
-
":pkValue": keyValue,
|
|
415
|
-
},
|
|
371
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
372
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
416
373
|
IndexName: indexName,
|
|
417
|
-
KeyConditionExpression:
|
|
374
|
+
KeyConditionExpression: keyConditionExpression,
|
|
418
375
|
...(limit && { Limit: limit }),
|
|
419
376
|
ScanIndexForward: ascending,
|
|
420
377
|
TableName: tableName,
|
|
@@ -425,25 +382,30 @@ async function executeQuery(indexName, keyValue, options = {}) {
|
|
|
425
382
|
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
426
383
|
};
|
|
427
384
|
}
|
|
385
|
+
function scopePrefix(scope) {
|
|
386
|
+
return scope === undefined ? undefined : `${scope}${SEPARATOR}`;
|
|
387
|
+
}
|
|
388
|
+
// =============================================================================
|
|
389
|
+
// Query Functions
|
|
390
|
+
// =============================================================================
|
|
428
391
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
433
|
-
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
392
|
+
* List entities of a model, optionally narrowed to a scope.
|
|
393
|
+
* Requires the model to register `fabricIndex()` (pk=[model]).
|
|
434
394
|
*/
|
|
435
395
|
async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
396
|
+
const index = requireIndex(model, ["model"]);
|
|
397
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
398
|
+
const pkValue = buildCompositeKey({ model }, ["model"], suffix);
|
|
399
|
+
return executeQuery(index, pkValue, {
|
|
439
400
|
ascending,
|
|
440
401
|
limit,
|
|
402
|
+
skPrefix: scopePrefix(scope),
|
|
441
403
|
startKey,
|
|
442
404
|
});
|
|
443
405
|
}
|
|
444
406
|
/**
|
|
445
|
-
* Query a single entity by human-friendly alias
|
|
446
|
-
*
|
|
407
|
+
* Query a single entity by human-friendly alias.
|
|
408
|
+
* Requires the model to register `fabricIndex("alias")`.
|
|
447
409
|
*/
|
|
448
410
|
const queryByAlias = fabricService({
|
|
449
411
|
alias: "queryByAlias",
|
|
@@ -463,7 +425,11 @@ const queryByAlias = fabricService({
|
|
|
463
425
|
description: "Query deleted entities instead of active ones",
|
|
464
426
|
},
|
|
465
427
|
model: { type: String, description: "Entity model name" },
|
|
466
|
-
scope: {
|
|
428
|
+
scope: {
|
|
429
|
+
type: String,
|
|
430
|
+
required: false,
|
|
431
|
+
description: "Optional scope narrower (begins_with on sk)",
|
|
432
|
+
},
|
|
467
433
|
},
|
|
468
434
|
service: async ({ alias, archived, deleted, model, scope, }) => {
|
|
469
435
|
const aliasStr = alias;
|
|
@@ -471,52 +437,52 @@ const queryByAlias = fabricService({
|
|
|
471
437
|
const deletedBool = deleted;
|
|
472
438
|
const modelStr = model;
|
|
473
439
|
const scopeStr = scope;
|
|
474
|
-
const
|
|
440
|
+
const index = requireIndex(modelStr, ["model", "alias"]);
|
|
441
|
+
const suffix = calculateSuffix({
|
|
475
442
|
archived: archivedBool,
|
|
476
443
|
deleted: deletedBool,
|
|
477
444
|
});
|
|
478
|
-
const
|
|
479
|
-
const result = await executeQuery(
|
|
445
|
+
const pkValue = buildCompositeKey({ model: modelStr, alias: aliasStr }, ["model", "alias"], suffix);
|
|
446
|
+
const result = await executeQuery(index, pkValue, {
|
|
480
447
|
limit: 1,
|
|
448
|
+
skPrefix: scopePrefix(scopeStr),
|
|
481
449
|
});
|
|
482
450
|
return result.items[0] ?? null;
|
|
483
451
|
},
|
|
484
452
|
});
|
|
485
453
|
/**
|
|
486
|
-
* Query entities by category classification
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
490
|
-
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
454
|
+
* Query entities by category classification.
|
|
455
|
+
* Requires the model to register `fabricIndex("category")`.
|
|
491
456
|
*/
|
|
492
457
|
async function queryByCategory({ archived = false, ascending = false, category, deleted = false, limit, model, scope, startKey, }) {
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
|
|
458
|
+
const index = requireIndex(model, ["model", "category"]);
|
|
459
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
460
|
+
const pkValue = buildCompositeKey({ model, category }, ["model", "category"], suffix);
|
|
461
|
+
return executeQuery(index, pkValue, {
|
|
496
462
|
ascending,
|
|
497
463
|
limit,
|
|
464
|
+
skPrefix: scopePrefix(scope),
|
|
498
465
|
startKey,
|
|
499
466
|
});
|
|
500
467
|
}
|
|
501
468
|
/**
|
|
502
|
-
* Query entities by type classification
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
506
|
-
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
469
|
+
* Query entities by type classification.
|
|
470
|
+
* Requires the model to register `fabricIndex("type")`.
|
|
507
471
|
*/
|
|
508
472
|
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, type, }) {
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
|
|
473
|
+
const index = requireIndex(model, ["model", "type"]);
|
|
474
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
475
|
+
const pkValue = buildCompositeKey({ model, type }, ["model", "type"], suffix);
|
|
476
|
+
return executeQuery(index, pkValue, {
|
|
512
477
|
ascending,
|
|
513
478
|
limit,
|
|
479
|
+
skPrefix: scopePrefix(scope),
|
|
514
480
|
startKey,
|
|
515
481
|
});
|
|
516
482
|
}
|
|
517
483
|
/**
|
|
518
|
-
* Query a single entity by external ID
|
|
519
|
-
*
|
|
484
|
+
* Query a single entity by external ID.
|
|
485
|
+
* Requires the model to register `fabricIndex("xid")`.
|
|
520
486
|
*/
|
|
521
487
|
const queryByXid = fabricService({
|
|
522
488
|
alias: "queryByXid",
|
|
@@ -535,7 +501,11 @@ const queryByXid = fabricService({
|
|
|
535
501
|
description: "Query deleted entities instead of active ones",
|
|
536
502
|
},
|
|
537
503
|
model: { type: String, description: "Entity model name" },
|
|
538
|
-
scope: {
|
|
504
|
+
scope: {
|
|
505
|
+
type: String,
|
|
506
|
+
required: false,
|
|
507
|
+
description: "Optional scope narrower (begins_with on sk)",
|
|
508
|
+
},
|
|
539
509
|
xid: { type: String, description: "External ID" },
|
|
540
510
|
},
|
|
541
511
|
service: async ({ archived, deleted, model, scope, xid, }) => {
|
|
@@ -544,13 +514,15 @@ const queryByXid = fabricService({
|
|
|
544
514
|
const modelStr = model;
|
|
545
515
|
const scopeStr = scope;
|
|
546
516
|
const xidStr = xid;
|
|
547
|
-
const
|
|
517
|
+
const index = requireIndex(modelStr, ["model", "xid"]);
|
|
518
|
+
const suffix = calculateSuffix({
|
|
548
519
|
archived: archivedBool,
|
|
549
520
|
deleted: deletedBool,
|
|
550
521
|
});
|
|
551
|
-
const
|
|
552
|
-
const result = await executeQuery(
|
|
522
|
+
const pkValue = buildCompositeKey({ model: modelStr, xid: xidStr }, ["model", "xid"], suffix);
|
|
523
|
+
const result = await executeQuery(index, pkValue, {
|
|
553
524
|
limit: 1,
|
|
525
|
+
skPrefix: scopePrefix(scopeStr),
|
|
554
526
|
});
|
|
555
527
|
return result.items[0] ?? null;
|
|
556
528
|
},
|
|
@@ -564,162 +536,77 @@ const queryByXid = fabricService({
|
|
|
564
536
|
* removing the need to know which specific GSI to use.
|
|
565
537
|
*/
|
|
566
538
|
// =============================================================================
|
|
567
|
-
//
|
|
539
|
+
// Index Selection
|
|
568
540
|
// =============================================================================
|
|
569
541
|
/**
|
|
570
|
-
*
|
|
571
|
-
*/
|
|
572
|
-
function calculateSuffix(archived, deleted) {
|
|
573
|
-
if (archived && deleted) {
|
|
574
|
-
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
575
|
-
}
|
|
576
|
-
if (archived) {
|
|
577
|
-
return ARCHIVED_SUFFIX;
|
|
578
|
-
}
|
|
579
|
-
if (deleted) {
|
|
580
|
-
return DELETED_SUFFIX;
|
|
581
|
-
}
|
|
582
|
-
return "";
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Build a combined filter object from params
|
|
586
|
-
*/
|
|
587
|
-
function buildFilterObject(params) {
|
|
588
|
-
const result = {
|
|
589
|
-
model: params.model,
|
|
590
|
-
};
|
|
591
|
-
if (params.scope !== undefined) {
|
|
592
|
-
result.scope = params.scope;
|
|
593
|
-
}
|
|
594
|
-
if (params.filter) {
|
|
595
|
-
Object.assign(result, params.filter);
|
|
596
|
-
}
|
|
597
|
-
return result;
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Score an index based on how well it matches the filter fields
|
|
601
|
-
*/
|
|
602
|
-
function scoreIndex(index, filterFields) {
|
|
603
|
-
let matchedFields = 0;
|
|
604
|
-
let pkComplete = true;
|
|
605
|
-
for (const field of index.pk) {
|
|
606
|
-
if (filterFields[field] !== undefined) {
|
|
607
|
-
matchedFields++;
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
pkComplete = false;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return {
|
|
614
|
-
index,
|
|
615
|
-
matchedFields,
|
|
616
|
-
pkComplete,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Select the best index for the given filter
|
|
542
|
+
* Select the best index for the given filter.
|
|
621
543
|
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
544
|
+
* Every model-level index has `model` as its first pk field. The picker
|
|
545
|
+
* prefers the most specific index whose remaining pk fields are all
|
|
546
|
+
* satisfied by the filter.
|
|
547
|
+
*/
|
|
548
|
+
function selectBestIndex(indexes, filter) {
|
|
549
|
+
// Candidates: indexes whose pk starts with "model" and whose remaining
|
|
550
|
+
// fields are all present in the filter.
|
|
551
|
+
const candidates = indexes.filter((index) => {
|
|
552
|
+
if (index.pk.length === 0 || index.pk[0] !== "model")
|
|
553
|
+
return false;
|
|
554
|
+
for (let i = 1; i < index.pk.length; i++) {
|
|
555
|
+
if (filter[index.pk[i]] === undefined)
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
return true;
|
|
559
|
+
});
|
|
560
|
+
if (candidates.length === 0) {
|
|
561
|
+
const available = indexes
|
|
562
|
+
.map((i) => `[${i.pk.join(", ")}]`)
|
|
634
563
|
.join(", ");
|
|
635
|
-
const
|
|
636
|
-
throw new ConfigurationError(`No index matches filter
|
|
637
|
-
`
|
|
638
|
-
`Available indexes: ${availableIndexes}`);
|
|
564
|
+
const provided = Object.keys(filter).join(", ") || "(none)";
|
|
565
|
+
throw new ConfigurationError(`No index matches filter for model. ` +
|
|
566
|
+
`Filter fields: ${provided}. Available indexes: ${available}`);
|
|
639
567
|
}
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
completeMatches.sort((a, b) => {
|
|
644
|
-
const fieldDiff = b.matchedFields - a.matchedFields;
|
|
645
|
-
if (fieldDiff !== 0)
|
|
646
|
-
return fieldDiff;
|
|
647
|
-
return b.index.pk.length - a.index.pk.length;
|
|
648
|
-
});
|
|
649
|
-
return completeMatches[0].index;
|
|
568
|
+
// Prefer the most specific index (longest pk).
|
|
569
|
+
candidates.sort((a, b) => b.pk.length - a.pk.length);
|
|
570
|
+
return candidates[0];
|
|
650
571
|
}
|
|
651
572
|
// =============================================================================
|
|
652
573
|
// Main Query Function
|
|
653
574
|
// =============================================================================
|
|
654
575
|
/**
|
|
655
|
-
* Query entities with automatic index selection
|
|
656
|
-
*
|
|
657
|
-
* The query function automatically selects the best GSI based on
|
|
658
|
-
* the filter fields provided. This removes the need to know which
|
|
659
|
-
* specific query function (queryByOu, queryByAlias, etc.) to use.
|
|
576
|
+
* Query entities with automatic index selection.
|
|
660
577
|
*
|
|
661
578
|
* @example
|
|
662
|
-
*
|
|
663
|
-
*
|
|
579
|
+
* // Uses indexModel (pk: ["model"]), optionally narrowed by scope
|
|
580
|
+
* const records = await query({ model: "record", scope: "@" });
|
|
664
581
|
*
|
|
665
582
|
* @example
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
583
|
+
* // Uses indexModelAlias (pk: ["model", "alias"])
|
|
584
|
+
* const byAlias = await query({
|
|
585
|
+
* model: "record",
|
|
586
|
+
* scope: "@",
|
|
587
|
+
* filter: { alias: "my-record" },
|
|
588
|
+
* });
|
|
672
589
|
*
|
|
673
590
|
* @example
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* model: "message",
|
|
677
|
-
* filter: { chatId: "abc-123" },
|
|
678
|
-
* });
|
|
591
|
+
* // Cross-scope listing (no scope narrower)
|
|
592
|
+
* const all = await query({ model: "record" });
|
|
679
593
|
*/
|
|
680
594
|
async function query(params) {
|
|
681
|
-
const { archived = false, ascending = false, deleted = false, limit, model, startKey, } = params;
|
|
682
|
-
// Build the combined filter object
|
|
683
|
-
const filterFields = buildFilterObject(params);
|
|
684
|
-
// Get indexes for this model
|
|
595
|
+
const { archived = false, ascending = false, deleted = false, filter, limit, model, scope, startKey, } = params;
|
|
685
596
|
const indexes = getModelIndexes(model);
|
|
686
|
-
|
|
597
|
+
const filterFields = {
|
|
598
|
+
model,
|
|
599
|
+
...filter,
|
|
600
|
+
};
|
|
687
601
|
const selectedIndex = selectBestIndex(indexes, filterFields);
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const command = new QueryCommand({
|
|
696
|
-
ExclusiveStartKey: startKey,
|
|
697
|
-
ExpressionAttributeNames: {
|
|
698
|
-
"#pk": indexName,
|
|
699
|
-
},
|
|
700
|
-
ExpressionAttributeValues: {
|
|
701
|
-
":pkValue": keyValue,
|
|
702
|
-
},
|
|
703
|
-
IndexName: indexName,
|
|
704
|
-
KeyConditionExpression: "#pk = :pkValue",
|
|
705
|
-
...(limit && { Limit: limit }),
|
|
706
|
-
ScanIndexForward: ascending,
|
|
707
|
-
TableName: tableName,
|
|
602
|
+
const suffix = calculateSuffix({ archived, deleted });
|
|
603
|
+
const pkValue = buildCompositeKey(filterFields, selectedIndex.pk, suffix);
|
|
604
|
+
return executeQuery(selectedIndex, pkValue, {
|
|
605
|
+
ascending,
|
|
606
|
+
limit,
|
|
607
|
+
skPrefix: scopePrefix(scope),
|
|
608
|
+
startKey,
|
|
708
609
|
});
|
|
709
|
-
const response = await docClient.send(command);
|
|
710
|
-
return {
|
|
711
|
-
items: (response.Items ?? []),
|
|
712
|
-
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
/**
|
|
716
|
-
* Generate an index name from pk fields
|
|
717
|
-
*/
|
|
718
|
-
function generateIndexName(pk) {
|
|
719
|
-
const suffix = pk
|
|
720
|
-
.map((field) => field.charAt(0).toUpperCase() + field.slice(1))
|
|
721
|
-
.join("");
|
|
722
|
-
return `index${suffix}`;
|
|
723
610
|
}
|
|
724
611
|
|
|
725
612
|
/**
|
|
@@ -741,16 +628,12 @@ async function seedEntityIfNotExists(entity) {
|
|
|
741
628
|
if (existing) {
|
|
742
629
|
return false;
|
|
743
630
|
}
|
|
744
|
-
// Generate required fields if missing
|
|
745
|
-
const now = new Date().toISOString();
|
|
631
|
+
// Generate required fields if missing; indexEntity manages timestamps
|
|
746
632
|
const completeEntity = {
|
|
747
|
-
createdAt: entity.createdAt ?? now,
|
|
748
633
|
id: entity.id ?? crypto.randomUUID(),
|
|
749
634
|
model: entity.model,
|
|
750
635
|
name: entity.name ?? entity.alias,
|
|
751
636
|
scope: entity.scope,
|
|
752
|
-
sequence: entity.sequence ?? Date.now(),
|
|
753
|
-
updatedAt: entity.updatedAt ?? now,
|
|
754
637
|
...entity,
|
|
755
638
|
};
|
|
756
639
|
await putEntity({ entity: completeEntity });
|
|
@@ -801,16 +684,12 @@ async function seedEntities(entities, options = {}) {
|
|
|
801
684
|
result.created.push(alias);
|
|
802
685
|
continue;
|
|
803
686
|
}
|
|
804
|
-
// Generate required fields if missing
|
|
805
|
-
const now = new Date().toISOString();
|
|
687
|
+
// Generate required fields if missing; indexEntity manages timestamps
|
|
806
688
|
const completeEntity = {
|
|
807
|
-
createdAt: entity.createdAt ?? now,
|
|
808
689
|
id: entity.id ?? crypto.randomUUID(),
|
|
809
690
|
model: entity.model,
|
|
810
691
|
name: entity.name ?? entity.alias ?? "Unnamed",
|
|
811
692
|
scope: entity.scope,
|
|
812
|
-
sequence: entity.sequence ?? Date.now(),
|
|
813
|
-
updatedAt: entity.updatedAt ?? now,
|
|
814
693
|
...entity,
|
|
815
694
|
};
|
|
816
695
|
await putEntity({ entity: completeEntity });
|
|
@@ -871,5 +750,5 @@ async function exportEntitiesToJson(model, scope, pretty = true) {
|
|
|
871
750
|
return pretty ? JSON.stringify(entities, null, 2) : JSON.stringify(entities);
|
|
872
751
|
}
|
|
873
752
|
|
|
874
|
-
export {
|
|
753
|
+
export { archiveEntity, buildCompositeKey, calculateScope, deleteEntity, destroyEntity, exportEntities, exportEntitiesToJson, getDocClient, getEntity, getTableName, indexEntity, initClient, isInitialized, putEntity, query, queryByAlias, queryByCategory, queryByScope, queryByType, queryByXid, resetClient, seedEntities, seedEntityIfNotExists, transactWriteEntities, updateEntity };
|
|
875
754
|
//# sourceMappingURL=index.js.map
|