@jaypie/dynamodb 0.4.4 → 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/cjs/mcp/index.cjs
CHANGED
|
@@ -77,85 +77,45 @@ function isInitialized() {
|
|
|
77
77
|
return docClient !== null && tableName !== null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Re-export shared constants from fabric
|
|
81
|
-
// GSI names
|
|
82
|
-
const INDEX_ALIAS = "indexAlias";
|
|
83
|
-
const INDEX_CATEGORY = "indexCategory";
|
|
84
|
-
const INDEX_SCOPE = "indexScope";
|
|
85
|
-
const INDEX_TYPE = "indexType";
|
|
86
|
-
const INDEX_XID = "indexXid";
|
|
87
|
-
|
|
88
80
|
// =============================================================================
|
|
89
81
|
// Key Builders
|
|
90
82
|
// =============================================================================
|
|
91
83
|
/**
|
|
92
|
-
* Build
|
|
93
|
-
*
|
|
94
|
-
* @param
|
|
95
|
-
* @
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return `${scope}${fabric.SEPARATOR}${model}`;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Build the indexAlias key for human-friendly lookups
|
|
102
|
-
* @param scope - The scope
|
|
103
|
-
* @param model - The entity model name
|
|
104
|
-
* @param alias - The human-friendly alias
|
|
105
|
-
* @returns Composite key: "{scope}#{model}#{alias}"
|
|
106
|
-
*/
|
|
107
|
-
function buildIndexAlias(scope, model, alias) {
|
|
108
|
-
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${alias}`;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Build the indexCategory key for category filtering
|
|
112
|
-
* @param scope - The scope
|
|
113
|
-
* @param model - The entity model name
|
|
114
|
-
* @param category - The category classification
|
|
115
|
-
* @returns Composite key: "{scope}#{model}#{category}"
|
|
116
|
-
*/
|
|
117
|
-
function buildIndexCategory(scope, model, category) {
|
|
118
|
-
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${category}`;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Build the indexType key for type filtering
|
|
122
|
-
* @param scope - The scope
|
|
123
|
-
* @param model - The entity model name
|
|
124
|
-
* @param type - The type classification
|
|
125
|
-
* @returns Composite key: "{scope}#{model}#{type}"
|
|
126
|
-
*/
|
|
127
|
-
function buildIndexType(scope, model, type) {
|
|
128
|
-
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${type}`;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Build the indexXid key for external ID lookups
|
|
132
|
-
* @param scope - The scope
|
|
133
|
-
* @param model - The entity model name
|
|
134
|
-
* @param xid - The external ID
|
|
135
|
-
* @returns Composite key: "{scope}#{model}#{xid}"
|
|
84
|
+
* Build a composite key from entity fields
|
|
85
|
+
*
|
|
86
|
+
* @param entity - Entity with fields to extract
|
|
87
|
+
* @param fields - Field names to combine with SEPARATOR
|
|
88
|
+
* @param suffix - Optional suffix to append (e.g., "#deleted")
|
|
89
|
+
* @returns Composite key string
|
|
136
90
|
*/
|
|
137
|
-
function
|
|
138
|
-
return
|
|
91
|
+
function buildCompositeKey(entity, fields, suffix) {
|
|
92
|
+
return fabric.buildCompositeKey(entity, fields, suffix);
|
|
139
93
|
}
|
|
140
94
|
/**
|
|
141
|
-
* Auto-populate GSI index keys on an entity
|
|
95
|
+
* Auto-populate GSI index keys on an entity and advance its write timestamps.
|
|
142
96
|
*
|
|
143
|
-
*
|
|
97
|
+
* - Bumps `updatedAt` to now on every call.
|
|
98
|
+
* - Backfills `createdAt` to the same now if not already set.
|
|
99
|
+
* - Populates GSI attributes (pk composite and sk composite when applicable)
|
|
100
|
+
* using the indexes registered for the entity's model.
|
|
144
101
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* - indexType is populated only when type is present
|
|
149
|
-
* - indexXid is populated only when xid is present
|
|
102
|
+
* Callers (putEntity, updateEntity, deleteEntity, archiveEntity,
|
|
103
|
+
* transactWriteEntities) go through this one function so `updatedAt` is
|
|
104
|
+
* always fresh and never forgotten.
|
|
150
105
|
*
|
|
151
|
-
* @param entity - The entity to
|
|
152
|
-
* @param suffix - Optional suffix
|
|
153
|
-
* @returns
|
|
106
|
+
* @param entity - The entity to index
|
|
107
|
+
* @param suffix - Optional suffix override (defaults to archived/deleted state)
|
|
108
|
+
* @returns A new entity with timestamps bumped and index keys populated
|
|
154
109
|
*/
|
|
155
|
-
function indexEntity(entity, suffix
|
|
110
|
+
function indexEntity(entity, suffix) {
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
const bumped = {
|
|
113
|
+
...entity,
|
|
114
|
+
createdAt: entity.createdAt ?? now,
|
|
115
|
+
updatedAt: now,
|
|
116
|
+
};
|
|
156
117
|
const indexes = fabric.getModelIndexes(entity.model);
|
|
157
|
-
|
|
158
|
-
return fabric.populateIndexKeys(entity, indexes, suffix);
|
|
118
|
+
return fabric.populateIndexKeys(bumped, indexes, suffix);
|
|
159
119
|
}
|
|
160
120
|
|
|
161
121
|
/**
|
|
@@ -176,20 +136,19 @@ function calculateEntitySuffix(entity) {
|
|
|
176
136
|
return "";
|
|
177
137
|
}
|
|
178
138
|
/**
|
|
179
|
-
* Get a single entity by primary key
|
|
139
|
+
* Get a single entity by primary key (id)
|
|
180
140
|
*/
|
|
181
141
|
const getEntity = fabric.fabricService({
|
|
182
142
|
alias: "getEntity",
|
|
183
|
-
description: "Get a single entity by
|
|
143
|
+
description: "Get a single entity by id",
|
|
184
144
|
input: {
|
|
185
|
-
id: { type: String, description: "Entity
|
|
186
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
145
|
+
id: { type: String, description: "Entity id (partition key)" },
|
|
187
146
|
},
|
|
188
|
-
service: async ({ id
|
|
147
|
+
service: async ({ id }) => {
|
|
189
148
|
const docClient = getDocClient();
|
|
190
149
|
const tableName = getTableName();
|
|
191
150
|
const command = new libDynamodb.GetCommand({
|
|
192
|
-
Key: { id
|
|
151
|
+
Key: { id },
|
|
193
152
|
TableName: tableName,
|
|
194
153
|
});
|
|
195
154
|
const response = await docClient.send(command);
|
|
@@ -197,16 +156,12 @@ const getEntity = fabric.fabricService({
|
|
|
197
156
|
},
|
|
198
157
|
});
|
|
199
158
|
/**
|
|
200
|
-
* Put (create or replace) an entity
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
204
|
-
* complex StorableEntity objects that can't be coerced by vocabulary's type system.
|
|
159
|
+
* Put (create or replace) an entity.
|
|
160
|
+
* `indexEntity` auto-bumps `updatedAt` and backfills `createdAt`.
|
|
205
161
|
*/
|
|
206
162
|
async function putEntity({ entity, }) {
|
|
207
163
|
const docClient = getDocClient();
|
|
208
164
|
const tableName = getTableName();
|
|
209
|
-
// Auto-populate index keys
|
|
210
165
|
const indexedEntity = indexEntity(entity);
|
|
211
166
|
const command = new libDynamodb.PutCommand({
|
|
212
167
|
Item: indexedEntity,
|
|
@@ -216,20 +171,13 @@ async function putEntity({ entity, }) {
|
|
|
216
171
|
return indexedEntity;
|
|
217
172
|
}
|
|
218
173
|
/**
|
|
219
|
-
* Update an existing entity
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
223
|
-
* complex StorableEntity objects that can't be coerced by vocabulary's type system.
|
|
174
|
+
* Update an existing entity.
|
|
175
|
+
* `indexEntity` auto-bumps `updatedAt` — callers never set it manually.
|
|
224
176
|
*/
|
|
225
177
|
async function updateEntity({ entity, }) {
|
|
226
178
|
const docClient = getDocClient();
|
|
227
179
|
const tableName = getTableName();
|
|
228
|
-
|
|
229
|
-
const updatedEntity = indexEntity({
|
|
230
|
-
...entity,
|
|
231
|
-
updatedAt: new Date().toISOString(),
|
|
232
|
-
});
|
|
180
|
+
const updatedEntity = indexEntity(entity);
|
|
233
181
|
const command = new libDynamodb.PutCommand({
|
|
234
182
|
Item: updatedEntity,
|
|
235
183
|
TableName: tableName,
|
|
@@ -245,25 +193,21 @@ const deleteEntity = fabric.fabricService({
|
|
|
245
193
|
alias: "deleteEntity",
|
|
246
194
|
description: "Soft delete an entity (sets deletedAt timestamp)",
|
|
247
195
|
input: {
|
|
248
|
-
id: { type: String, description: "Entity
|
|
249
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
196
|
+
id: { type: String, description: "Entity id" },
|
|
250
197
|
},
|
|
251
|
-
service: async ({ id
|
|
198
|
+
service: async ({ id }) => {
|
|
252
199
|
const docClient = getDocClient();
|
|
253
200
|
const tableName = getTableName();
|
|
254
|
-
|
|
255
|
-
const existing = await getEntity({ id, model });
|
|
201
|
+
const existing = await getEntity({ id });
|
|
256
202
|
if (!existing) {
|
|
257
203
|
return false;
|
|
258
204
|
}
|
|
259
205
|
const now = new Date().toISOString();
|
|
260
|
-
//
|
|
206
|
+
// indexEntity will bump updatedAt again; set deletedAt here.
|
|
261
207
|
const updatedEntity = {
|
|
262
208
|
...existing,
|
|
263
209
|
deletedAt: now,
|
|
264
|
-
updatedAt: now,
|
|
265
210
|
};
|
|
266
|
-
// Calculate suffix based on combined state (may already be archived)
|
|
267
211
|
const suffix = calculateEntitySuffix(updatedEntity);
|
|
268
212
|
const deletedEntity = indexEntity(updatedEntity, suffix);
|
|
269
213
|
const command = new libDynamodb.PutCommand({
|
|
@@ -282,25 +226,20 @@ const archiveEntity = fabric.fabricService({
|
|
|
282
226
|
alias: "archiveEntity",
|
|
283
227
|
description: "Archive an entity (sets archivedAt timestamp)",
|
|
284
228
|
input: {
|
|
285
|
-
id: { type: String, description: "Entity
|
|
286
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
229
|
+
id: { type: String, description: "Entity id" },
|
|
287
230
|
},
|
|
288
|
-
service: async ({ id
|
|
231
|
+
service: async ({ id }) => {
|
|
289
232
|
const docClient = getDocClient();
|
|
290
233
|
const tableName = getTableName();
|
|
291
|
-
|
|
292
|
-
const existing = await getEntity({ id, model });
|
|
234
|
+
const existing = await getEntity({ id });
|
|
293
235
|
if (!existing) {
|
|
294
236
|
return false;
|
|
295
237
|
}
|
|
296
238
|
const now = new Date().toISOString();
|
|
297
|
-
// Build updated entity with archivedAt
|
|
298
239
|
const updatedEntity = {
|
|
299
240
|
...existing,
|
|
300
241
|
archivedAt: now,
|
|
301
|
-
updatedAt: now,
|
|
302
242
|
};
|
|
303
|
-
// Calculate suffix based on combined state (may already be deleted)
|
|
304
243
|
const suffix = calculateEntitySuffix(updatedEntity);
|
|
305
244
|
const archivedEntity = indexEntity(updatedEntity, suffix);
|
|
306
245
|
const command = new libDynamodb.PutCommand({
|
|
@@ -319,14 +258,13 @@ const destroyEntity = fabric.fabricService({
|
|
|
319
258
|
alias: "destroyEntity",
|
|
320
259
|
description: "Hard delete an entity (permanently removes from table)",
|
|
321
260
|
input: {
|
|
322
|
-
id: { type: String, description: "Entity
|
|
323
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
261
|
+
id: { type: String, description: "Entity id" },
|
|
324
262
|
},
|
|
325
|
-
service: async ({ id
|
|
263
|
+
service: async ({ id }) => {
|
|
326
264
|
const docClient = getDocClient();
|
|
327
265
|
const tableName = getTableName();
|
|
328
266
|
const command = new libDynamodb.DeleteCommand({
|
|
329
|
-
Key: { id
|
|
267
|
+
Key: { id },
|
|
330
268
|
TableName: tableName,
|
|
331
269
|
});
|
|
332
270
|
await docClient.send(command);
|
|
@@ -334,9 +272,13 @@ const destroyEntity = fabric.fabricService({
|
|
|
334
272
|
},
|
|
335
273
|
});
|
|
336
274
|
|
|
275
|
+
// =============================================================================
|
|
276
|
+
// Helpers
|
|
277
|
+
// =============================================================================
|
|
337
278
|
/**
|
|
338
|
-
* Calculate the suffix based on archived/deleted
|
|
339
|
-
*
|
|
279
|
+
* Calculate the suffix for the GSI partition key based on archived/deleted
|
|
280
|
+
* flags. Suffix stays on pk so deleted/archived entities are queried as their
|
|
281
|
+
* own partition (active queries skip them naturally).
|
|
340
282
|
*/
|
|
341
283
|
function calculateSuffix({ archived, deleted, }) {
|
|
342
284
|
if (archived && deleted) {
|
|
@@ -351,22 +293,51 @@ function calculateSuffix({ archived, deleted, }) {
|
|
|
351
293
|
return "";
|
|
352
294
|
}
|
|
353
295
|
/**
|
|
354
|
-
*
|
|
296
|
+
* Find the registered index for a model that matches a given partition-key
|
|
297
|
+
* shape. The matching index is the first one whose pk equals the expected
|
|
298
|
+
* fields. Throws ConfigurationError if no match is found.
|
|
299
|
+
*/
|
|
300
|
+
function requireIndex(model, pkFields) {
|
|
301
|
+
const indexes = fabric.getModelIndexes(model);
|
|
302
|
+
const match = indexes.find((index) => index.pk.length === pkFields.length &&
|
|
303
|
+
index.pk.every((field, i) => field === pkFields[i]));
|
|
304
|
+
if (!match) {
|
|
305
|
+
throw new errors.ConfigurationError(`Model "${model}" has no index with pk=[${pkFields.join(", ")}]. ` +
|
|
306
|
+
`Register one with fabricIndex(${pkFields.length > 1 ? `"${pkFields[1]}"` : ""}).`);
|
|
307
|
+
}
|
|
308
|
+
return match;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Execute a GSI query.
|
|
312
|
+
*
|
|
313
|
+
* - pk: exact match on the index partition key
|
|
314
|
+
* - skPrefix: optional begins_with on the index sort key (used when the index
|
|
315
|
+
* has a composite sk like [scope, updatedAt])
|
|
355
316
|
*/
|
|
356
|
-
async function executeQuery(
|
|
357
|
-
const { ascending = false, limit, startKey } = options;
|
|
317
|
+
async function executeQuery(index, pkValue, options = {}) {
|
|
318
|
+
const { ascending = false, limit, skPrefix, startKey } = options;
|
|
319
|
+
const attrs = fabric.getGsiAttributeNames(index);
|
|
320
|
+
const indexName = attrs.pk;
|
|
321
|
+
const expressionAttributeNames = {
|
|
322
|
+
"#pk": indexName,
|
|
323
|
+
};
|
|
324
|
+
const expressionAttributeValues = {
|
|
325
|
+
":pkValue": pkValue,
|
|
326
|
+
};
|
|
327
|
+
let keyConditionExpression = "#pk = :pkValue";
|
|
328
|
+
if (skPrefix !== undefined && attrs.sk) {
|
|
329
|
+
expressionAttributeNames["#sk"] = attrs.sk;
|
|
330
|
+
expressionAttributeValues[":skPrefix"] = skPrefix;
|
|
331
|
+
keyConditionExpression += " AND begins_with(#sk, :skPrefix)";
|
|
332
|
+
}
|
|
358
333
|
const docClient = getDocClient();
|
|
359
334
|
const tableName = getTableName();
|
|
360
335
|
const command = new libDynamodb.QueryCommand({
|
|
361
336
|
ExclusiveStartKey: startKey,
|
|
362
|
-
ExpressionAttributeNames:
|
|
363
|
-
|
|
364
|
-
},
|
|
365
|
-
ExpressionAttributeValues: {
|
|
366
|
-
":pkValue": keyValue,
|
|
367
|
-
},
|
|
337
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
338
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
368
339
|
IndexName: indexName,
|
|
369
|
-
KeyConditionExpression:
|
|
340
|
+
KeyConditionExpression: keyConditionExpression,
|
|
370
341
|
...(limit && { Limit: limit }),
|
|
371
342
|
ScanIndexForward: ascending,
|
|
372
343
|
TableName: tableName,
|
|
@@ -377,25 +348,30 @@ async function executeQuery(indexName, keyValue, options = {}) {
|
|
|
377
348
|
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
378
349
|
};
|
|
379
350
|
}
|
|
351
|
+
function scopePrefix(scope) {
|
|
352
|
+
return scope === undefined ? undefined : `${scope}${fabric.SEPARATOR}`;
|
|
353
|
+
}
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// Query Functions
|
|
356
|
+
// =============================================================================
|
|
380
357
|
/**
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
385
|
-
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
358
|
+
* List entities of a model, optionally narrowed to a scope.
|
|
359
|
+
* Requires the model to register `fabricIndex()` (pk=[model]).
|
|
386
360
|
*/
|
|
387
361
|
async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
|
|
362
|
+
const index = requireIndex(model, ["model"]);
|
|
388
363
|
const suffix = calculateSuffix({ archived, deleted });
|
|
389
|
-
const
|
|
390
|
-
return executeQuery(
|
|
364
|
+
const pkValue = buildCompositeKey({ model }, ["model"], suffix);
|
|
365
|
+
return executeQuery(index, pkValue, {
|
|
391
366
|
ascending,
|
|
392
367
|
limit,
|
|
368
|
+
skPrefix: scopePrefix(scope),
|
|
393
369
|
startKey,
|
|
394
370
|
});
|
|
395
371
|
}
|
|
396
372
|
/**
|
|
397
|
-
* Query a single entity by human-friendly alias
|
|
398
|
-
*
|
|
373
|
+
* Query a single entity by human-friendly alias.
|
|
374
|
+
* Requires the model to register `fabricIndex("alias")`.
|
|
399
375
|
*/
|
|
400
376
|
const queryByAlias = fabric.fabricService({
|
|
401
377
|
alias: "queryByAlias",
|
|
@@ -415,7 +391,11 @@ const queryByAlias = fabric.fabricService({
|
|
|
415
391
|
description: "Query deleted entities instead of active ones",
|
|
416
392
|
},
|
|
417
393
|
model: { type: String, description: "Entity model name" },
|
|
418
|
-
scope: {
|
|
394
|
+
scope: {
|
|
395
|
+
type: String,
|
|
396
|
+
required: false,
|
|
397
|
+
description: "Optional scope narrower (begins_with on sk)",
|
|
398
|
+
},
|
|
419
399
|
},
|
|
420
400
|
service: async ({ alias, archived, deleted, model, scope, }) => {
|
|
421
401
|
const aliasStr = alias;
|
|
@@ -423,52 +403,52 @@ const queryByAlias = fabric.fabricService({
|
|
|
423
403
|
const deletedBool = deleted;
|
|
424
404
|
const modelStr = model;
|
|
425
405
|
const scopeStr = scope;
|
|
406
|
+
const index = requireIndex(modelStr, ["model", "alias"]);
|
|
426
407
|
const suffix = calculateSuffix({
|
|
427
408
|
archived: archivedBool,
|
|
428
409
|
deleted: deletedBool,
|
|
429
410
|
});
|
|
430
|
-
const
|
|
431
|
-
const result = await executeQuery(
|
|
411
|
+
const pkValue = buildCompositeKey({ model: modelStr, alias: aliasStr }, ["model", "alias"], suffix);
|
|
412
|
+
const result = await executeQuery(index, pkValue, {
|
|
432
413
|
limit: 1,
|
|
414
|
+
skPrefix: scopePrefix(scopeStr),
|
|
433
415
|
});
|
|
434
416
|
return result.items[0] ?? null;
|
|
435
417
|
},
|
|
436
418
|
});
|
|
437
419
|
/**
|
|
438
|
-
* Query entities by category classification
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
442
|
-
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
420
|
+
* Query entities by category classification.
|
|
421
|
+
* Requires the model to register `fabricIndex("category")`.
|
|
443
422
|
*/
|
|
444
423
|
async function queryByCategory({ archived = false, ascending = false, category, deleted = false, limit, model, scope, startKey, }) {
|
|
424
|
+
const index = requireIndex(model, ["model", "category"]);
|
|
445
425
|
const suffix = calculateSuffix({ archived, deleted });
|
|
446
|
-
const
|
|
447
|
-
return executeQuery(
|
|
426
|
+
const pkValue = buildCompositeKey({ model, category }, ["model", "category"], suffix);
|
|
427
|
+
return executeQuery(index, pkValue, {
|
|
448
428
|
ascending,
|
|
449
429
|
limit,
|
|
430
|
+
skPrefix: scopePrefix(scope),
|
|
450
431
|
startKey,
|
|
451
432
|
});
|
|
452
433
|
}
|
|
453
434
|
/**
|
|
454
|
-
* Query entities by type classification
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* Note: This is a regular async function (not fabricService) because it accepts
|
|
458
|
-
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
435
|
+
* Query entities by type classification.
|
|
436
|
+
* Requires the model to register `fabricIndex("type")`.
|
|
459
437
|
*/
|
|
460
438
|
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, type, }) {
|
|
439
|
+
const index = requireIndex(model, ["model", "type"]);
|
|
461
440
|
const suffix = calculateSuffix({ archived, deleted });
|
|
462
|
-
const
|
|
463
|
-
return executeQuery(
|
|
441
|
+
const pkValue = buildCompositeKey({ model, type }, ["model", "type"], suffix);
|
|
442
|
+
return executeQuery(index, pkValue, {
|
|
464
443
|
ascending,
|
|
465
444
|
limit,
|
|
445
|
+
skPrefix: scopePrefix(scope),
|
|
466
446
|
startKey,
|
|
467
447
|
});
|
|
468
448
|
}
|
|
469
449
|
/**
|
|
470
|
-
* Query a single entity by external ID
|
|
471
|
-
*
|
|
450
|
+
* Query a single entity by external ID.
|
|
451
|
+
* Requires the model to register `fabricIndex("xid")`.
|
|
472
452
|
*/
|
|
473
453
|
const queryByXid = fabric.fabricService({
|
|
474
454
|
alias: "queryByXid",
|
|
@@ -487,7 +467,11 @@ const queryByXid = fabric.fabricService({
|
|
|
487
467
|
description: "Query deleted entities instead of active ones",
|
|
488
468
|
},
|
|
489
469
|
model: { type: String, description: "Entity model name" },
|
|
490
|
-
scope: {
|
|
470
|
+
scope: {
|
|
471
|
+
type: String,
|
|
472
|
+
required: false,
|
|
473
|
+
description: "Optional scope narrower (begins_with on sk)",
|
|
474
|
+
},
|
|
491
475
|
xid: { type: String, description: "External ID" },
|
|
492
476
|
},
|
|
493
477
|
service: async ({ archived, deleted, model, scope, xid, }) => {
|
|
@@ -496,13 +480,15 @@ const queryByXid = fabric.fabricService({
|
|
|
496
480
|
const modelStr = model;
|
|
497
481
|
const scopeStr = scope;
|
|
498
482
|
const xidStr = xid;
|
|
483
|
+
const index = requireIndex(modelStr, ["model", "xid"]);
|
|
499
484
|
const suffix = calculateSuffix({
|
|
500
485
|
archived: archivedBool,
|
|
501
486
|
deleted: deletedBool,
|
|
502
487
|
});
|
|
503
|
-
const
|
|
504
|
-
const result = await executeQuery(
|
|
488
|
+
const pkValue = buildCompositeKey({ model: modelStr, xid: xidStr }, ["model", "xid"], suffix);
|
|
489
|
+
const result = await executeQuery(index, pkValue, {
|
|
505
490
|
limit: 1,
|
|
491
|
+
skPrefix: scopePrefix(scopeStr),
|
|
506
492
|
});
|
|
507
493
|
return result.items[0] ?? null;
|
|
508
494
|
},
|
|
@@ -512,53 +498,26 @@ const DEFAULT_ENDPOINT = "http://127.0.0.1:8000";
|
|
|
512
498
|
const DEFAULT_REGION$1 = "us-east-1";
|
|
513
499
|
const DEFAULT_TABLE_NAME$1 = "jaypie-local";
|
|
514
500
|
// =============================================================================
|
|
515
|
-
// Index
|
|
501
|
+
// Index → GSI Conversion
|
|
516
502
|
// =============================================================================
|
|
517
503
|
/**
|
|
518
|
-
*
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const suffix = pk
|
|
522
|
-
.map((field) => field.charAt(0).toUpperCase() + field.slice(1))
|
|
523
|
-
.join("");
|
|
524
|
-
return `index${suffix}`;
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Collect all unique indexes from registered models
|
|
528
|
-
*/
|
|
529
|
-
function collectAllIndexes() {
|
|
530
|
-
const indexMap = new Map();
|
|
531
|
-
for (const index of fabric.getAllRegisteredIndexes()) {
|
|
532
|
-
const name = index.name ?? generateIndexName(index.pk);
|
|
533
|
-
if (!indexMap.has(name)) {
|
|
534
|
-
indexMap.set(name, { ...index, name });
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return Array.from(indexMap.values());
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Build attribute definitions from indexes
|
|
504
|
+
* Build attribute definitions from registered indexes.
|
|
505
|
+
* Primary key is `id` (STRING) only; GSI pk and composite sk attributes
|
|
506
|
+
* are all STRING. A single-field sk (e.g., raw `updatedAt`) is also STRING.
|
|
541
507
|
*/
|
|
542
508
|
function buildAttributeDefinitions(indexes) {
|
|
543
509
|
const attrs = new Map();
|
|
544
|
-
// Primary key
|
|
545
|
-
attrs.set("model", "S");
|
|
510
|
+
// Primary key: id only
|
|
546
511
|
attrs.set("id", "S");
|
|
547
|
-
attrs.set("sequence", "N");
|
|
548
|
-
// GSI attributes (partition keys are always strings)
|
|
549
|
-
for (const index of indexes) {
|
|
550
|
-
const indexName = index.name ?? generateIndexName(index.pk);
|
|
551
|
-
attrs.set(indexName, "S");
|
|
552
|
-
}
|
|
553
|
-
// Sort keys (sequence is always a number, others would be strings)
|
|
554
|
-
// Note: Currently all indexes use sequence as SK, so this is mostly future-proofing
|
|
555
512
|
for (const index of indexes) {
|
|
556
|
-
const sk = index
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
513
|
+
const { pk, sk } = fabric.getGsiAttributeNames(index);
|
|
514
|
+
// All pk attributes are composite strings
|
|
515
|
+
if (!attrs.has(pk))
|
|
516
|
+
attrs.set(pk, "S");
|
|
517
|
+
if (sk && !attrs.has(sk)) {
|
|
518
|
+
// Single-field `sequence` remains NUMBER for back-compat callers;
|
|
519
|
+
// every other sk attribute (composite or not) is STRING.
|
|
520
|
+
attrs.set(sk, sk === "sequence" ? "N" : "S");
|
|
562
521
|
}
|
|
563
522
|
}
|
|
564
523
|
return Array.from(attrs.entries())
|
|
@@ -569,22 +528,19 @@ function buildAttributeDefinitions(indexes) {
|
|
|
569
528
|
}));
|
|
570
529
|
}
|
|
571
530
|
/**
|
|
572
|
-
* Build GSI definitions from indexes
|
|
531
|
+
* Build GSI definitions from registered indexes
|
|
573
532
|
*/
|
|
574
533
|
function buildGSIs(indexes) {
|
|
575
534
|
const gsiProjection = { ProjectionType: "ALL" };
|
|
576
535
|
return indexes.map((index) => {
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
536
|
+
const { pk, sk } = fabric.getGsiAttributeNames(index);
|
|
537
|
+
const keySchema = [{ AttributeName: pk, KeyType: "HASH" }];
|
|
538
|
+
if (sk) {
|
|
539
|
+
keySchema.push({ AttributeName: sk, KeyType: "RANGE" });
|
|
540
|
+
}
|
|
581
541
|
return {
|
|
582
|
-
IndexName:
|
|
583
|
-
KeySchema:
|
|
584
|
-
{ AttributeName: indexName, KeyType: "HASH" },
|
|
585
|
-
// Use first SK field as the range key attribute
|
|
586
|
-
{ AttributeName: sk[0], KeyType: "RANGE" },
|
|
587
|
-
],
|
|
542
|
+
IndexName: pk,
|
|
543
|
+
KeySchema: keySchema,
|
|
588
544
|
Projection: gsiProjection,
|
|
589
545
|
};
|
|
590
546
|
});
|
|
@@ -593,20 +549,17 @@ function buildGSIs(indexes) {
|
|
|
593
549
|
// Table Creation
|
|
594
550
|
// =============================================================================
|
|
595
551
|
/**
|
|
596
|
-
* DynamoDB table schema with Jaypie GSI pattern
|
|
597
|
-
*
|
|
598
|
-
*
|
|
552
|
+
* DynamoDB table schema with Jaypie GSI pattern.
|
|
553
|
+
* Primary key is `id` only. GSIs come from models registered via
|
|
554
|
+
* `registerModel()`, shaped by `fabricIndex()`.
|
|
599
555
|
*/
|
|
600
556
|
function createTableParams(tableName, billingMode) {
|
|
601
|
-
const allIndexes =
|
|
557
|
+
const allIndexes = fabric.getAllRegisteredIndexes();
|
|
602
558
|
return {
|
|
603
559
|
AttributeDefinitions: buildAttributeDefinitions(allIndexes),
|
|
604
560
|
BillingMode: billingMode,
|
|
605
561
|
GlobalSecondaryIndexes: buildGSIs(allIndexes),
|
|
606
|
-
KeySchema: [
|
|
607
|
-
{ AttributeName: "model", KeyType: "HASH" },
|
|
608
|
-
{ AttributeName: "id", KeyType: "RANGE" },
|
|
609
|
-
],
|
|
562
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
610
563
|
TableName: tableName,
|
|
611
564
|
};
|
|
612
565
|
}
|
|
@@ -646,7 +599,6 @@ const createTableHandler = fabric.fabricService({
|
|
|
646
599
|
region: DEFAULT_REGION$1,
|
|
647
600
|
});
|
|
648
601
|
try {
|
|
649
|
-
// Check if table already exists
|
|
650
602
|
await client.send(new clientDynamodb.DescribeTableCommand({ TableName: tableNameStr }));
|
|
651
603
|
return {
|
|
652
604
|
message: `Table "${tableNameStr}" already exists`,
|
|
@@ -659,7 +611,6 @@ const createTableHandler = fabric.fabricService({
|
|
|
659
611
|
throw error;
|
|
660
612
|
}
|
|
661
613
|
}
|
|
662
|
-
// Create the table
|
|
663
614
|
const tableParams = createTableParams(tableNameStr, billingModeStr);
|
|
664
615
|
await client.send(new clientDynamodb.CreateTableCommand(tableParams));
|
|
665
616
|
return {
|