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