@jaypie/dynamodb 0.4.4 → 0.6.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 +8 -13
- package/dist/cjs/index.cjs +210 -324
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/keyBuilders.d.ts +11 -49
- package/dist/cjs/mcp/index.cjs +184 -223
- 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 +8 -13
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +211 -315
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keyBuilders.d.ts +11 -49
- package/dist/esm/mcp/index.js +185 -224
- 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 (createEntity, 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,39 +156,38 @@ const getEntity = fabric.fabricService({
|
|
|
197
156
|
},
|
|
198
157
|
});
|
|
199
158
|
/**
|
|
200
|
-
*
|
|
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
|
+
* Create an entity. Fails the conditional write if `id` already exists,
|
|
160
|
+
* returning `null` instead of throwing. Use `updateEntity` to overwrite.
|
|
161
|
+
* `indexEntity` auto-bumps `updatedAt` and backfills `createdAt`.
|
|
205
162
|
*/
|
|
206
|
-
async function
|
|
163
|
+
async function createEntity({ entity, }) {
|
|
207
164
|
const docClient = getDocClient();
|
|
208
165
|
const tableName = getTableName();
|
|
209
|
-
// Auto-populate index keys
|
|
210
166
|
const indexedEntity = indexEntity(entity);
|
|
211
167
|
const command = new libDynamodb.PutCommand({
|
|
168
|
+
ConditionExpression: "attribute_not_exists(id)",
|
|
212
169
|
Item: indexedEntity,
|
|
213
170
|
TableName: tableName,
|
|
214
171
|
});
|
|
215
|
-
|
|
172
|
+
try {
|
|
173
|
+
await docClient.send(command);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error?.name === "ConditionalCheckFailedException") {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
216
181
|
return indexedEntity;
|
|
217
182
|
}
|
|
218
183
|
/**
|
|
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.
|
|
184
|
+
* Update an existing entity.
|
|
185
|
+
* `indexEntity` auto-bumps `updatedAt` — callers never set it manually.
|
|
224
186
|
*/
|
|
225
187
|
async function updateEntity({ entity, }) {
|
|
226
188
|
const docClient = getDocClient();
|
|
227
189
|
const tableName = getTableName();
|
|
228
|
-
|
|
229
|
-
const updatedEntity = indexEntity({
|
|
230
|
-
...entity,
|
|
231
|
-
updatedAt: new Date().toISOString(),
|
|
232
|
-
});
|
|
190
|
+
const updatedEntity = indexEntity(entity);
|
|
233
191
|
const command = new libDynamodb.PutCommand({
|
|
234
192
|
Item: updatedEntity,
|
|
235
193
|
TableName: tableName,
|
|
@@ -245,25 +203,21 @@ const deleteEntity = fabric.fabricService({
|
|
|
245
203
|
alias: "deleteEntity",
|
|
246
204
|
description: "Soft delete an entity (sets deletedAt timestamp)",
|
|
247
205
|
input: {
|
|
248
|
-
id: { type: String, description: "Entity
|
|
249
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
206
|
+
id: { type: String, description: "Entity id" },
|
|
250
207
|
},
|
|
251
|
-
service: async ({ id
|
|
208
|
+
service: async ({ id }) => {
|
|
252
209
|
const docClient = getDocClient();
|
|
253
210
|
const tableName = getTableName();
|
|
254
|
-
|
|
255
|
-
const existing = await getEntity({ id, model });
|
|
211
|
+
const existing = await getEntity({ id });
|
|
256
212
|
if (!existing) {
|
|
257
213
|
return false;
|
|
258
214
|
}
|
|
259
215
|
const now = new Date().toISOString();
|
|
260
|
-
//
|
|
216
|
+
// indexEntity will bump updatedAt again; set deletedAt here.
|
|
261
217
|
const updatedEntity = {
|
|
262
218
|
...existing,
|
|
263
219
|
deletedAt: now,
|
|
264
|
-
updatedAt: now,
|
|
265
220
|
};
|
|
266
|
-
// Calculate suffix based on combined state (may already be archived)
|
|
267
221
|
const suffix = calculateEntitySuffix(updatedEntity);
|
|
268
222
|
const deletedEntity = indexEntity(updatedEntity, suffix);
|
|
269
223
|
const command = new libDynamodb.PutCommand({
|
|
@@ -282,25 +236,20 @@ const archiveEntity = fabric.fabricService({
|
|
|
282
236
|
alias: "archiveEntity",
|
|
283
237
|
description: "Archive an entity (sets archivedAt timestamp)",
|
|
284
238
|
input: {
|
|
285
|
-
id: { type: String, description: "Entity
|
|
286
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
239
|
+
id: { type: String, description: "Entity id" },
|
|
287
240
|
},
|
|
288
|
-
service: async ({ id
|
|
241
|
+
service: async ({ id }) => {
|
|
289
242
|
const docClient = getDocClient();
|
|
290
243
|
const tableName = getTableName();
|
|
291
|
-
|
|
292
|
-
const existing = await getEntity({ id, model });
|
|
244
|
+
const existing = await getEntity({ id });
|
|
293
245
|
if (!existing) {
|
|
294
246
|
return false;
|
|
295
247
|
}
|
|
296
248
|
const now = new Date().toISOString();
|
|
297
|
-
// Build updated entity with archivedAt
|
|
298
249
|
const updatedEntity = {
|
|
299
250
|
...existing,
|
|
300
251
|
archivedAt: now,
|
|
301
|
-
updatedAt: now,
|
|
302
252
|
};
|
|
303
|
-
// Calculate suffix based on combined state (may already be deleted)
|
|
304
253
|
const suffix = calculateEntitySuffix(updatedEntity);
|
|
305
254
|
const archivedEntity = indexEntity(updatedEntity, suffix);
|
|
306
255
|
const command = new libDynamodb.PutCommand({
|
|
@@ -319,14 +268,13 @@ const destroyEntity = fabric.fabricService({
|
|
|
319
268
|
alias: "destroyEntity",
|
|
320
269
|
description: "Hard delete an entity (permanently removes from table)",
|
|
321
270
|
input: {
|
|
322
|
-
id: { type: String, description: "Entity
|
|
323
|
-
model: { type: String, description: "Entity model (partition key)" },
|
|
271
|
+
id: { type: String, description: "Entity id" },
|
|
324
272
|
},
|
|
325
|
-
service: async ({ id
|
|
273
|
+
service: async ({ id }) => {
|
|
326
274
|
const docClient = getDocClient();
|
|
327
275
|
const tableName = getTableName();
|
|
328
276
|
const command = new libDynamodb.DeleteCommand({
|
|
329
|
-
Key: { id
|
|
277
|
+
Key: { id },
|
|
330
278
|
TableName: tableName,
|
|
331
279
|
});
|
|
332
280
|
await docClient.send(command);
|
|
@@ -334,9 +282,13 @@ const destroyEntity = fabric.fabricService({
|
|
|
334
282
|
},
|
|
335
283
|
});
|
|
336
284
|
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// Helpers
|
|
287
|
+
// =============================================================================
|
|
337
288
|
/**
|
|
338
|
-
* Calculate the suffix based on archived/deleted
|
|
339
|
-
*
|
|
289
|
+
* Calculate the suffix for the GSI partition key based on archived/deleted
|
|
290
|
+
* flags. Suffix stays on pk so deleted/archived entities are queried as their
|
|
291
|
+
* own partition (active queries skip them naturally).
|
|
340
292
|
*/
|
|
341
293
|
function calculateSuffix({ archived, deleted, }) {
|
|
342
294
|
if (archived && deleted) {
|
|
@@ -351,22 +303,51 @@ function calculateSuffix({ archived, deleted, }) {
|
|
|
351
303
|
return "";
|
|
352
304
|
}
|
|
353
305
|
/**
|
|
354
|
-
*
|
|
306
|
+
* Find the registered index for a model that matches a given partition-key
|
|
307
|
+
* shape. The matching index is the first one whose pk equals the expected
|
|
308
|
+
* fields. Throws ConfigurationError if no match is found.
|
|
309
|
+
*/
|
|
310
|
+
function requireIndex(model, pkFields) {
|
|
311
|
+
const indexes = fabric.getModelIndexes(model);
|
|
312
|
+
const match = indexes.find((index) => index.pk.length === pkFields.length &&
|
|
313
|
+
index.pk.every((field, i) => field === pkFields[i]));
|
|
314
|
+
if (!match) {
|
|
315
|
+
throw new errors.ConfigurationError(`Model "${model}" has no index with pk=[${pkFields.join(", ")}]. ` +
|
|
316
|
+
`Register one with fabricIndex(${pkFields.length > 1 ? `"${pkFields[1]}"` : ""}).`);
|
|
317
|
+
}
|
|
318
|
+
return match;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Execute a GSI query.
|
|
322
|
+
*
|
|
323
|
+
* - pk: exact match on the index partition key
|
|
324
|
+
* - skPrefix: optional begins_with on the index sort key (used when the index
|
|
325
|
+
* has a composite sk like [scope, updatedAt])
|
|
355
326
|
*/
|
|
356
|
-
async function executeQuery(
|
|
357
|
-
const { ascending = false, limit, startKey } = options;
|
|
327
|
+
async function executeQuery(index, pkValue, options = {}) {
|
|
328
|
+
const { ascending = false, limit, skPrefix, startKey } = options;
|
|
329
|
+
const attrs = fabric.getGsiAttributeNames(index);
|
|
330
|
+
const indexName = attrs.pk;
|
|
331
|
+
const expressionAttributeNames = {
|
|
332
|
+
"#pk": indexName,
|
|
333
|
+
};
|
|
334
|
+
const expressionAttributeValues = {
|
|
335
|
+
":pkValue": pkValue,
|
|
336
|
+
};
|
|
337
|
+
let keyConditionExpression = "#pk = :pkValue";
|
|
338
|
+
if (skPrefix !== undefined && attrs.sk) {
|
|
339
|
+
expressionAttributeNames["#sk"] = attrs.sk;
|
|
340
|
+
expressionAttributeValues[":skPrefix"] = skPrefix;
|
|
341
|
+
keyConditionExpression += " AND begins_with(#sk, :skPrefix)";
|
|
342
|
+
}
|
|
358
343
|
const docClient = getDocClient();
|
|
359
344
|
const tableName = getTableName();
|
|
360
345
|
const command = new libDynamodb.QueryCommand({
|
|
361
346
|
ExclusiveStartKey: startKey,
|
|
362
|
-
ExpressionAttributeNames:
|
|
363
|
-
|
|
364
|
-
},
|
|
365
|
-
ExpressionAttributeValues: {
|
|
366
|
-
":pkValue": keyValue,
|
|
367
|
-
},
|
|
347
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
348
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
368
349
|
IndexName: indexName,
|
|
369
|
-
KeyConditionExpression:
|
|
350
|
+
KeyConditionExpression: keyConditionExpression,
|
|
370
351
|
...(limit && { Limit: limit }),
|
|
371
352
|
ScanIndexForward: ascending,
|
|
372
353
|
TableName: tableName,
|
|
@@ -377,25 +358,30 @@ async function executeQuery(indexName, keyValue, options = {}) {
|
|
|
377
358
|
lastEvaluatedKey: response.LastEvaluatedKey,
|
|
378
359
|
};
|
|
379
360
|
}
|
|
361
|
+
function scopePrefix(scope) {
|
|
362
|
+
return scope === undefined ? undefined : `${scope}${fabric.SEPARATOR}`;
|
|
363
|
+
}
|
|
364
|
+
// =============================================================================
|
|
365
|
+
// Query Functions
|
|
366
|
+
// =============================================================================
|
|
380
367
|
/**
|
|
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.
|
|
368
|
+
* List entities of a model, optionally narrowed to a scope.
|
|
369
|
+
* Requires the model to register `fabricIndex()` (pk=[model]).
|
|
386
370
|
*/
|
|
387
371
|
async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
|
|
372
|
+
const index = requireIndex(model, ["model"]);
|
|
388
373
|
const suffix = calculateSuffix({ archived, deleted });
|
|
389
|
-
const
|
|
390
|
-
return executeQuery(
|
|
374
|
+
const pkValue = buildCompositeKey({ model }, ["model"], suffix);
|
|
375
|
+
return executeQuery(index, pkValue, {
|
|
391
376
|
ascending,
|
|
392
377
|
limit,
|
|
378
|
+
skPrefix: scopePrefix(scope),
|
|
393
379
|
startKey,
|
|
394
380
|
});
|
|
395
381
|
}
|
|
396
382
|
/**
|
|
397
|
-
* Query a single entity by human-friendly alias
|
|
398
|
-
*
|
|
383
|
+
* Query a single entity by human-friendly alias.
|
|
384
|
+
* Requires the model to register `fabricIndex("alias")`.
|
|
399
385
|
*/
|
|
400
386
|
const queryByAlias = fabric.fabricService({
|
|
401
387
|
alias: "queryByAlias",
|
|
@@ -415,7 +401,11 @@ const queryByAlias = fabric.fabricService({
|
|
|
415
401
|
description: "Query deleted entities instead of active ones",
|
|
416
402
|
},
|
|
417
403
|
model: { type: String, description: "Entity model name" },
|
|
418
|
-
scope: {
|
|
404
|
+
scope: {
|
|
405
|
+
type: String,
|
|
406
|
+
required: false,
|
|
407
|
+
description: "Optional scope narrower (begins_with on sk)",
|
|
408
|
+
},
|
|
419
409
|
},
|
|
420
410
|
service: async ({ alias, archived, deleted, model, scope, }) => {
|
|
421
411
|
const aliasStr = alias;
|
|
@@ -423,52 +413,52 @@ const queryByAlias = fabric.fabricService({
|
|
|
423
413
|
const deletedBool = deleted;
|
|
424
414
|
const modelStr = model;
|
|
425
415
|
const scopeStr = scope;
|
|
416
|
+
const index = requireIndex(modelStr, ["model", "alias"]);
|
|
426
417
|
const suffix = calculateSuffix({
|
|
427
418
|
archived: archivedBool,
|
|
428
419
|
deleted: deletedBool,
|
|
429
420
|
});
|
|
430
|
-
const
|
|
431
|
-
const result = await executeQuery(
|
|
421
|
+
const pkValue = buildCompositeKey({ model: modelStr, alias: aliasStr }, ["model", "alias"], suffix);
|
|
422
|
+
const result = await executeQuery(index, pkValue, {
|
|
432
423
|
limit: 1,
|
|
424
|
+
skPrefix: scopePrefix(scopeStr),
|
|
433
425
|
});
|
|
434
426
|
return result.items[0] ?? null;
|
|
435
427
|
},
|
|
436
428
|
});
|
|
437
429
|
/**
|
|
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.
|
|
430
|
+
* Query entities by category classification.
|
|
431
|
+
* Requires the model to register `fabricIndex("category")`.
|
|
443
432
|
*/
|
|
444
433
|
async function queryByCategory({ archived = false, ascending = false, category, deleted = false, limit, model, scope, startKey, }) {
|
|
434
|
+
const index = requireIndex(model, ["model", "category"]);
|
|
445
435
|
const suffix = calculateSuffix({ archived, deleted });
|
|
446
|
-
const
|
|
447
|
-
return executeQuery(
|
|
436
|
+
const pkValue = buildCompositeKey({ model, category }, ["model", "category"], suffix);
|
|
437
|
+
return executeQuery(index, pkValue, {
|
|
448
438
|
ascending,
|
|
449
439
|
limit,
|
|
440
|
+
skPrefix: scopePrefix(scope),
|
|
450
441
|
startKey,
|
|
451
442
|
});
|
|
452
443
|
}
|
|
453
444
|
/**
|
|
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.
|
|
445
|
+
* Query entities by type classification.
|
|
446
|
+
* Requires the model to register `fabricIndex("type")`.
|
|
459
447
|
*/
|
|
460
448
|
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, type, }) {
|
|
449
|
+
const index = requireIndex(model, ["model", "type"]);
|
|
461
450
|
const suffix = calculateSuffix({ archived, deleted });
|
|
462
|
-
const
|
|
463
|
-
return executeQuery(
|
|
451
|
+
const pkValue = buildCompositeKey({ model, type }, ["model", "type"], suffix);
|
|
452
|
+
return executeQuery(index, pkValue, {
|
|
464
453
|
ascending,
|
|
465
454
|
limit,
|
|
455
|
+
skPrefix: scopePrefix(scope),
|
|
466
456
|
startKey,
|
|
467
457
|
});
|
|
468
458
|
}
|
|
469
459
|
/**
|
|
470
|
-
* Query a single entity by external ID
|
|
471
|
-
*
|
|
460
|
+
* Query a single entity by external ID.
|
|
461
|
+
* Requires the model to register `fabricIndex("xid")`.
|
|
472
462
|
*/
|
|
473
463
|
const queryByXid = fabric.fabricService({
|
|
474
464
|
alias: "queryByXid",
|
|
@@ -487,7 +477,11 @@ const queryByXid = fabric.fabricService({
|
|
|
487
477
|
description: "Query deleted entities instead of active ones",
|
|
488
478
|
},
|
|
489
479
|
model: { type: String, description: "Entity model name" },
|
|
490
|
-
scope: {
|
|
480
|
+
scope: {
|
|
481
|
+
type: String,
|
|
482
|
+
required: false,
|
|
483
|
+
description: "Optional scope narrower (begins_with on sk)",
|
|
484
|
+
},
|
|
491
485
|
xid: { type: String, description: "External ID" },
|
|
492
486
|
},
|
|
493
487
|
service: async ({ archived, deleted, model, scope, xid, }) => {
|
|
@@ -496,13 +490,15 @@ const queryByXid = fabric.fabricService({
|
|
|
496
490
|
const modelStr = model;
|
|
497
491
|
const scopeStr = scope;
|
|
498
492
|
const xidStr = xid;
|
|
493
|
+
const index = requireIndex(modelStr, ["model", "xid"]);
|
|
499
494
|
const suffix = calculateSuffix({
|
|
500
495
|
archived: archivedBool,
|
|
501
496
|
deleted: deletedBool,
|
|
502
497
|
});
|
|
503
|
-
const
|
|
504
|
-
const result = await executeQuery(
|
|
498
|
+
const pkValue = buildCompositeKey({ model: modelStr, xid: xidStr }, ["model", "xid"], suffix);
|
|
499
|
+
const result = await executeQuery(index, pkValue, {
|
|
505
500
|
limit: 1,
|
|
501
|
+
skPrefix: scopePrefix(scopeStr),
|
|
506
502
|
});
|
|
507
503
|
return result.items[0] ?? null;
|
|
508
504
|
},
|
|
@@ -512,53 +508,26 @@ const DEFAULT_ENDPOINT = "http://127.0.0.1:8000";
|
|
|
512
508
|
const DEFAULT_REGION$1 = "us-east-1";
|
|
513
509
|
const DEFAULT_TABLE_NAME$1 = "jaypie-local";
|
|
514
510
|
// =============================================================================
|
|
515
|
-
// Index
|
|
511
|
+
// Index → GSI Conversion
|
|
516
512
|
// =============================================================================
|
|
517
513
|
/**
|
|
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
|
|
514
|
+
* Build attribute definitions from registered indexes.
|
|
515
|
+
* Primary key is `id` (STRING) only; GSI pk and composite sk attributes
|
|
516
|
+
* are all STRING. A single-field sk (e.g., raw `updatedAt`) is also STRING.
|
|
541
517
|
*/
|
|
542
518
|
function buildAttributeDefinitions(indexes) {
|
|
543
519
|
const attrs = new Map();
|
|
544
|
-
// Primary key
|
|
545
|
-
attrs.set("model", "S");
|
|
520
|
+
// Primary key: id only
|
|
546
521
|
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
522
|
for (const index of indexes) {
|
|
556
|
-
const sk = index
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
523
|
+
const { pk, sk } = fabric.getGsiAttributeNames(index);
|
|
524
|
+
// All pk attributes are composite strings
|
|
525
|
+
if (!attrs.has(pk))
|
|
526
|
+
attrs.set(pk, "S");
|
|
527
|
+
if (sk && !attrs.has(sk)) {
|
|
528
|
+
// Single-field `sequence` remains NUMBER for back-compat callers;
|
|
529
|
+
// every other sk attribute (composite or not) is STRING.
|
|
530
|
+
attrs.set(sk, sk === "sequence" ? "N" : "S");
|
|
562
531
|
}
|
|
563
532
|
}
|
|
564
533
|
return Array.from(attrs.entries())
|
|
@@ -569,22 +538,19 @@ function buildAttributeDefinitions(indexes) {
|
|
|
569
538
|
}));
|
|
570
539
|
}
|
|
571
540
|
/**
|
|
572
|
-
* Build GSI definitions from indexes
|
|
541
|
+
* Build GSI definitions from registered indexes
|
|
573
542
|
*/
|
|
574
543
|
function buildGSIs(indexes) {
|
|
575
544
|
const gsiProjection = { ProjectionType: "ALL" };
|
|
576
545
|
return indexes.map((index) => {
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
546
|
+
const { pk, sk } = fabric.getGsiAttributeNames(index);
|
|
547
|
+
const keySchema = [{ AttributeName: pk, KeyType: "HASH" }];
|
|
548
|
+
if (sk) {
|
|
549
|
+
keySchema.push({ AttributeName: sk, KeyType: "RANGE" });
|
|
550
|
+
}
|
|
581
551
|
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
|
-
],
|
|
552
|
+
IndexName: pk,
|
|
553
|
+
KeySchema: keySchema,
|
|
588
554
|
Projection: gsiProjection,
|
|
589
555
|
};
|
|
590
556
|
});
|
|
@@ -593,20 +559,17 @@ function buildGSIs(indexes) {
|
|
|
593
559
|
// Table Creation
|
|
594
560
|
// =============================================================================
|
|
595
561
|
/**
|
|
596
|
-
* DynamoDB table schema with Jaypie GSI pattern
|
|
597
|
-
*
|
|
598
|
-
*
|
|
562
|
+
* DynamoDB table schema with Jaypie GSI pattern.
|
|
563
|
+
* Primary key is `id` only. GSIs come from models registered via
|
|
564
|
+
* `registerModel()`, shaped by `fabricIndex()`.
|
|
599
565
|
*/
|
|
600
566
|
function createTableParams(tableName, billingMode) {
|
|
601
|
-
const allIndexes =
|
|
567
|
+
const allIndexes = fabric.getAllRegisteredIndexes();
|
|
602
568
|
return {
|
|
603
569
|
AttributeDefinitions: buildAttributeDefinitions(allIndexes),
|
|
604
570
|
BillingMode: billingMode,
|
|
605
571
|
GlobalSecondaryIndexes: buildGSIs(allIndexes),
|
|
606
|
-
KeySchema: [
|
|
607
|
-
{ AttributeName: "model", KeyType: "HASH" },
|
|
608
|
-
{ AttributeName: "id", KeyType: "RANGE" },
|
|
609
|
-
],
|
|
572
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
610
573
|
TableName: tableName,
|
|
611
574
|
};
|
|
612
575
|
}
|
|
@@ -646,7 +609,6 @@ const createTableHandler = fabric.fabricService({
|
|
|
646
609
|
region: DEFAULT_REGION$1,
|
|
647
610
|
});
|
|
648
611
|
try {
|
|
649
|
-
// Check if table already exists
|
|
650
612
|
await client.send(new clientDynamodb.DescribeTableCommand({ TableName: tableNameStr }));
|
|
651
613
|
return {
|
|
652
614
|
message: `Table "${tableNameStr}" already exists`,
|
|
@@ -659,7 +621,6 @@ const createTableHandler = fabric.fabricService({
|
|
|
659
621
|
throw error;
|
|
660
622
|
}
|
|
661
623
|
}
|
|
662
|
-
// Create the table
|
|
663
624
|
const tableParams = createTableParams(tableNameStr, billingModeStr);
|
|
664
625
|
await client.send(new clientDynamodb.CreateTableCommand(tableParams));
|
|
665
626
|
return {
|
|
@@ -809,12 +770,12 @@ function wrapWithInit(handler) {
|
|
|
809
770
|
// MCP-specific serviceHandler wrappers for functions with complex inputs
|
|
810
771
|
// Note: These wrap the regular async functions to make them work with fabricMcp
|
|
811
772
|
/**
|
|
812
|
-
* MCP wrapper for
|
|
773
|
+
* MCP wrapper for createEntity
|
|
813
774
|
* Accepts entity JSON directly from LLM
|
|
814
775
|
*/
|
|
815
|
-
const
|
|
816
|
-
alias: "
|
|
817
|
-
description: "Create
|
|
776
|
+
const mcpCreateEntity = fabric.fabricService({
|
|
777
|
+
alias: "dynamodb_create",
|
|
778
|
+
description: "Create an entity in DynamoDB (auto-indexes GSI keys; returns null if id exists)",
|
|
818
779
|
input: {
|
|
819
780
|
// Required entity fields
|
|
820
781
|
id: { type: String, description: "Entity ID (sort key)" },
|
|
@@ -850,7 +811,7 @@ const mcpPutEntity = fabric.fabricService({
|
|
|
850
811
|
updatedAt: now,
|
|
851
812
|
xid: input.xid,
|
|
852
813
|
};
|
|
853
|
-
return
|
|
814
|
+
return createEntity({ entity });
|
|
854
815
|
},
|
|
855
816
|
});
|
|
856
817
|
/**
|
|
@@ -1057,11 +1018,11 @@ function registerDynamoDbTools(config) {
|
|
|
1057
1018
|
});
|
|
1058
1019
|
tools.push("dynamodb_get");
|
|
1059
1020
|
mcp.fabricMcp({
|
|
1060
|
-
service: wrapWithInit(
|
|
1061
|
-
name: "
|
|
1021
|
+
service: wrapWithInit(mcpCreateEntity),
|
|
1022
|
+
name: "dynamodb_create",
|
|
1062
1023
|
server,
|
|
1063
1024
|
});
|
|
1064
|
-
tools.push("
|
|
1025
|
+
tools.push("dynamodb_create");
|
|
1065
1026
|
mcp.fabricMcp({
|
|
1066
1027
|
service: wrapWithInit(mcpUpdateEntity),
|
|
1067
1028
|
name: "dynamodb_update",
|