@jaypie/dynamodb 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/constants.d.ts +2 -5
- package/dist/cjs/entities.d.ts +13 -13
- package/dist/cjs/index.cjs +312 -124
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +6 -4
- package/dist/cjs/keyBuilders.d.ts +36 -21
- package/dist/cjs/mcp/admin/createTable.d.ts +1 -1
- package/dist/cjs/mcp/admin/dockerCompose.d.ts +1 -1
- package/dist/cjs/mcp/admin/status.d.ts +1 -1
- package/dist/cjs/mcp/index.cjs +245 -197
- package/dist/cjs/mcp/index.cjs.map +1 -1
- package/dist/cjs/queries.d.ts +16 -16
- package/dist/cjs/query.d.ts +58 -0
- package/dist/cjs/seedExport.d.ts +11 -11
- package/dist/cjs/types.d.ts +25 -27
- package/dist/esm/constants.d.ts +2 -5
- package/dist/esm/entities.d.ts +13 -13
- package/dist/esm/index.d.ts +6 -4
- package/dist/esm/index.js +281 -110
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/keyBuilders.d.ts +36 -21
- package/dist/esm/mcp/admin/createTable.d.ts +1 -1
- package/dist/esm/mcp/admin/dockerCompose.d.ts +1 -1
- package/dist/esm/mcp/admin/status.d.ts +1 -1
- package/dist/esm/mcp/index.js +239 -191
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/queries.d.ts +16 -16
- package/dist/esm/query.d.ts +58 -0
- package/dist/esm/seedExport.d.ts +11 -11
- package/dist/esm/types.d.ts +25 -27
- package/package.json +2 -2
package/dist/cjs/mcp/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var mcp = require('@jaypie/
|
|
4
|
-
var
|
|
3
|
+
var mcp = require('@jaypie/fabric/mcp');
|
|
4
|
+
var fabric = require('@jaypie/fabric');
|
|
5
5
|
var clientDynamodb = require('@aws-sdk/client-dynamodb');
|
|
6
6
|
var libDynamodb = require('@aws-sdk/lib-dynamodb');
|
|
7
7
|
var errors = require('@jaypie/errors');
|
|
@@ -77,70 +77,73 @@ function isInitialized() {
|
|
|
77
77
|
return docClient !== null && tableName !== null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
// GSI names
|
|
80
|
+
// Re-export shared constants from fabric
|
|
81
|
+
// GSI names (derived from DEFAULT_INDEXES)
|
|
83
82
|
const INDEX_ALIAS = "indexAlias";
|
|
84
83
|
const INDEX_CLASS = "indexClass";
|
|
85
|
-
const
|
|
84
|
+
const INDEX_SCOPE = "indexScope";
|
|
86
85
|
const INDEX_TYPE = "indexType";
|
|
87
86
|
const INDEX_XID = "indexXid";
|
|
88
|
-
// Index suffixes for soft state
|
|
89
|
-
const ARCHIVED_SUFFIX = "#archived";
|
|
90
|
-
const DELETED_SUFFIX = "#deleted";
|
|
91
87
|
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Key Builders
|
|
90
|
+
// =============================================================================
|
|
92
91
|
/**
|
|
93
|
-
* Build the
|
|
94
|
-
* @param
|
|
92
|
+
* Build the indexScope key for hierarchical queries
|
|
93
|
+
* @param scope - The scope (APEX or "{parent.model}#{parent.id}")
|
|
95
94
|
* @param model - The entity model name
|
|
96
|
-
* @returns Composite key: "{
|
|
95
|
+
* @returns Composite key: "{scope}#{model}"
|
|
97
96
|
*/
|
|
98
|
-
function
|
|
99
|
-
return `${
|
|
97
|
+
function buildIndexScope(scope, model) {
|
|
98
|
+
return `${scope}${fabric.SEPARATOR}${model}`;
|
|
100
99
|
}
|
|
101
100
|
/**
|
|
102
101
|
* Build the indexAlias key for human-friendly lookups
|
|
103
|
-
* @param
|
|
102
|
+
* @param scope - The scope
|
|
104
103
|
* @param model - The entity model name
|
|
105
104
|
* @param alias - The human-friendly alias
|
|
106
|
-
* @returns Composite key: "{
|
|
105
|
+
* @returns Composite key: "{scope}#{model}#{alias}"
|
|
107
106
|
*/
|
|
108
|
-
function buildIndexAlias(
|
|
109
|
-
return `${
|
|
107
|
+
function buildIndexAlias(scope, model, alias) {
|
|
108
|
+
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${alias}`;
|
|
110
109
|
}
|
|
111
110
|
/**
|
|
112
111
|
* Build the indexClass key for category filtering
|
|
113
|
-
* @param
|
|
112
|
+
* @param scope - The scope
|
|
114
113
|
* @param model - The entity model name
|
|
115
114
|
* @param recordClass - The category classification
|
|
116
|
-
* @returns Composite key: "{
|
|
115
|
+
* @returns Composite key: "{scope}#{model}#{class}"
|
|
117
116
|
*/
|
|
118
|
-
function buildIndexClass(
|
|
119
|
-
return `${
|
|
117
|
+
function buildIndexClass(scope, model, recordClass) {
|
|
118
|
+
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${recordClass}`;
|
|
120
119
|
}
|
|
121
120
|
/**
|
|
122
121
|
* Build the indexType key for type filtering
|
|
123
|
-
* @param
|
|
122
|
+
* @param scope - The scope
|
|
124
123
|
* @param model - The entity model name
|
|
125
124
|
* @param type - The type classification
|
|
126
|
-
* @returns Composite key: "{
|
|
125
|
+
* @returns Composite key: "{scope}#{model}#{type}"
|
|
127
126
|
*/
|
|
128
|
-
function buildIndexType(
|
|
129
|
-
return `${
|
|
127
|
+
function buildIndexType(scope, model, type) {
|
|
128
|
+
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${type}`;
|
|
130
129
|
}
|
|
131
130
|
/**
|
|
132
131
|
* Build the indexXid key for external ID lookups
|
|
133
|
-
* @param
|
|
132
|
+
* @param scope - The scope
|
|
134
133
|
* @param model - The entity model name
|
|
135
134
|
* @param xid - The external ID
|
|
136
|
-
* @returns Composite key: "{
|
|
135
|
+
* @returns Composite key: "{scope}#{model}#{xid}"
|
|
137
136
|
*/
|
|
138
|
-
function buildIndexXid(
|
|
139
|
-
return `${
|
|
137
|
+
function buildIndexXid(scope, model, xid) {
|
|
138
|
+
return `${scope}${fabric.SEPARATOR}${model}${fabric.SEPARATOR}${xid}`;
|
|
140
139
|
}
|
|
141
140
|
/**
|
|
142
141
|
* Auto-populate GSI index keys on an entity
|
|
143
|
-
*
|
|
142
|
+
*
|
|
143
|
+
* Uses the model's registered indexes (from vocabulary registry) or
|
|
144
|
+
* DEFAULT_INDEXES if no custom indexes are registered.
|
|
145
|
+
*
|
|
146
|
+
* - indexScope is always populated from scope + model
|
|
144
147
|
* - indexAlias is populated only when alias is present
|
|
145
148
|
* - indexClass is populated only when class is present
|
|
146
149
|
* - indexType is populated only when type is present
|
|
@@ -151,27 +154,9 @@ function buildIndexXid(ou, model, xid) {
|
|
|
151
154
|
* @returns The entity with populated index keys
|
|
152
155
|
*/
|
|
153
156
|
function indexEntity(entity, suffix = "") {
|
|
154
|
-
const
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
// Optional indexes - only set when the source field is present
|
|
158
|
-
if (entity.alias !== undefined) {
|
|
159
|
-
result.indexAlias =
|
|
160
|
-
buildIndexAlias(entity.ou, entity.model, entity.alias) + suffix;
|
|
161
|
-
}
|
|
162
|
-
if (entity.class !== undefined) {
|
|
163
|
-
result.indexClass =
|
|
164
|
-
buildIndexClass(entity.ou, entity.model, entity.class) + suffix;
|
|
165
|
-
}
|
|
166
|
-
if (entity.type !== undefined) {
|
|
167
|
-
result.indexType =
|
|
168
|
-
buildIndexType(entity.ou, entity.model, entity.type) + suffix;
|
|
169
|
-
}
|
|
170
|
-
if (entity.xid !== undefined) {
|
|
171
|
-
result.indexXid =
|
|
172
|
-
buildIndexXid(entity.ou, entity.model, entity.xid) + suffix;
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
157
|
+
const indexes = fabric.getModelIndexes(entity.model);
|
|
158
|
+
// Cast through unknown to bridge the type gap between StorableEntity and IndexableModel
|
|
159
|
+
return fabric.populateIndexKeys(entity, indexes, suffix);
|
|
175
160
|
}
|
|
176
161
|
|
|
177
162
|
/**
|
|
@@ -181,20 +166,20 @@ function calculateEntitySuffix(entity) {
|
|
|
181
166
|
const hasArchived = Boolean(entity.archivedAt);
|
|
182
167
|
const hasDeleted = Boolean(entity.deletedAt);
|
|
183
168
|
if (hasArchived && hasDeleted) {
|
|
184
|
-
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
169
|
+
return fabric.ARCHIVED_SUFFIX + fabric.DELETED_SUFFIX;
|
|
185
170
|
}
|
|
186
171
|
if (hasArchived) {
|
|
187
|
-
return ARCHIVED_SUFFIX;
|
|
172
|
+
return fabric.ARCHIVED_SUFFIX;
|
|
188
173
|
}
|
|
189
174
|
if (hasDeleted) {
|
|
190
|
-
return DELETED_SUFFIX;
|
|
175
|
+
return fabric.DELETED_SUFFIX;
|
|
191
176
|
}
|
|
192
177
|
return "";
|
|
193
178
|
}
|
|
194
179
|
/**
|
|
195
180
|
* Get a single entity by primary key
|
|
196
181
|
*/
|
|
197
|
-
const getEntity =
|
|
182
|
+
const getEntity = fabric.fabricService({
|
|
198
183
|
alias: "getEntity",
|
|
199
184
|
description: "Get a single entity by primary key",
|
|
200
185
|
input: {
|
|
@@ -216,8 +201,8 @@ const getEntity = vocabulary.serviceHandler({
|
|
|
216
201
|
* Put (create or replace) an entity
|
|
217
202
|
* Auto-populates GSI index keys via indexEntity
|
|
218
203
|
*
|
|
219
|
-
* Note: This is a regular async function (not
|
|
220
|
-
* complex
|
|
204
|
+
* Note: This is a regular async function (not fabricService) because it accepts
|
|
205
|
+
* complex StorableEntity objects that can't be coerced by vocabulary's type system.
|
|
221
206
|
*/
|
|
222
207
|
async function putEntity({ entity, }) {
|
|
223
208
|
const docClient = getDocClient();
|
|
@@ -235,8 +220,8 @@ async function putEntity({ entity, }) {
|
|
|
235
220
|
* Update an existing entity
|
|
236
221
|
* Auto-populates GSI index keys and sets updatedAt
|
|
237
222
|
*
|
|
238
|
-
* Note: This is a regular async function (not
|
|
239
|
-
* complex
|
|
223
|
+
* Note: This is a regular async function (not fabricService) because it accepts
|
|
224
|
+
* complex StorableEntity objects that can't be coerced by vocabulary's type system.
|
|
240
225
|
*/
|
|
241
226
|
async function updateEntity({ entity, }) {
|
|
242
227
|
const docClient = getDocClient();
|
|
@@ -257,7 +242,7 @@ async function updateEntity({ entity, }) {
|
|
|
257
242
|
* Soft delete an entity by setting deletedAt timestamp
|
|
258
243
|
* Re-indexes with appropriate suffix based on archived/deleted state
|
|
259
244
|
*/
|
|
260
|
-
const deleteEntity =
|
|
245
|
+
const deleteEntity = fabric.fabricService({
|
|
261
246
|
alias: "deleteEntity",
|
|
262
247
|
description: "Soft delete an entity (sets deletedAt timestamp)",
|
|
263
248
|
input: {
|
|
@@ -294,7 +279,7 @@ const deleteEntity = vocabulary.serviceHandler({
|
|
|
294
279
|
* Archive an entity by setting archivedAt timestamp
|
|
295
280
|
* Re-indexes with appropriate suffix based on archived/deleted state
|
|
296
281
|
*/
|
|
297
|
-
const archiveEntity =
|
|
282
|
+
const archiveEntity = fabric.fabricService({
|
|
298
283
|
alias: "archiveEntity",
|
|
299
284
|
description: "Archive an entity (sets archivedAt timestamp)",
|
|
300
285
|
input: {
|
|
@@ -331,7 +316,7 @@ const archiveEntity = vocabulary.serviceHandler({
|
|
|
331
316
|
* Hard delete an entity (permanently removes from table)
|
|
332
317
|
* Use with caution - prefer deleteEntity for soft delete
|
|
333
318
|
*/
|
|
334
|
-
const destroyEntity =
|
|
319
|
+
const destroyEntity = fabric.fabricService({
|
|
335
320
|
alias: "destroyEntity",
|
|
336
321
|
description: "Hard delete an entity (permanently removes from table)",
|
|
337
322
|
input: {
|
|
@@ -356,13 +341,13 @@ const destroyEntity = vocabulary.serviceHandler({
|
|
|
356
341
|
*/
|
|
357
342
|
function calculateSuffix({ archived, deleted, }) {
|
|
358
343
|
if (archived && deleted) {
|
|
359
|
-
return ARCHIVED_SUFFIX + DELETED_SUFFIX;
|
|
344
|
+
return fabric.ARCHIVED_SUFFIX + fabric.DELETED_SUFFIX;
|
|
360
345
|
}
|
|
361
346
|
if (archived) {
|
|
362
|
-
return ARCHIVED_SUFFIX;
|
|
347
|
+
return fabric.ARCHIVED_SUFFIX;
|
|
363
348
|
}
|
|
364
349
|
if (deleted) {
|
|
365
|
-
return DELETED_SUFFIX;
|
|
350
|
+
return fabric.DELETED_SUFFIX;
|
|
366
351
|
}
|
|
367
352
|
return "";
|
|
368
353
|
}
|
|
@@ -394,16 +379,16 @@ async function executeQuery(indexName, keyValue, options = {}) {
|
|
|
394
379
|
};
|
|
395
380
|
}
|
|
396
381
|
/**
|
|
397
|
-
* Query entities by
|
|
398
|
-
* Uses
|
|
382
|
+
* Query entities by scope (parent hierarchy)
|
|
383
|
+
* Uses indexScope GSI
|
|
399
384
|
*
|
|
400
|
-
* Note: This is a regular async function (not
|
|
385
|
+
* Note: This is a regular async function (not fabricService) because it accepts
|
|
401
386
|
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
402
387
|
*/
|
|
403
|
-
async function
|
|
388
|
+
async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
|
|
404
389
|
const suffix = calculateSuffix({ archived, deleted });
|
|
405
|
-
const keyValue =
|
|
406
|
-
return executeQuery(
|
|
390
|
+
const keyValue = buildIndexScope(scope, model) + suffix;
|
|
391
|
+
return executeQuery(INDEX_SCOPE, keyValue, {
|
|
407
392
|
ascending,
|
|
408
393
|
limit,
|
|
409
394
|
startKey,
|
|
@@ -413,7 +398,7 @@ async function queryByOu({ archived = false, ascending = false, deleted = false,
|
|
|
413
398
|
* Query a single entity by human-friendly alias
|
|
414
399
|
* Uses indexAlias GSI
|
|
415
400
|
*/
|
|
416
|
-
const queryByAlias =
|
|
401
|
+
const queryByAlias = fabric.fabricService({
|
|
417
402
|
alias: "queryByAlias",
|
|
418
403
|
description: "Query a single entity by human-friendly alias",
|
|
419
404
|
input: {
|
|
@@ -431,16 +416,19 @@ const queryByAlias = vocabulary.serviceHandler({
|
|
|
431
416
|
description: "Query deleted entities instead of active ones",
|
|
432
417
|
},
|
|
433
418
|
model: { type: String, description: "Entity model name" },
|
|
434
|
-
|
|
419
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
435
420
|
},
|
|
436
|
-
service: async ({ alias, archived, deleted, model,
|
|
421
|
+
service: async ({ alias, archived, deleted, model, scope, }) => {
|
|
437
422
|
const aliasStr = alias;
|
|
438
423
|
const archivedBool = archived;
|
|
439
424
|
const deletedBool = deleted;
|
|
440
425
|
const modelStr = model;
|
|
441
|
-
const
|
|
442
|
-
const suffix = calculateSuffix({
|
|
443
|
-
|
|
426
|
+
const scopeStr = scope;
|
|
427
|
+
const suffix = calculateSuffix({
|
|
428
|
+
archived: archivedBool,
|
|
429
|
+
deleted: deletedBool,
|
|
430
|
+
});
|
|
431
|
+
const keyValue = buildIndexAlias(scopeStr, modelStr, aliasStr) + suffix;
|
|
444
432
|
const result = await executeQuery(INDEX_ALIAS, keyValue, {
|
|
445
433
|
limit: 1,
|
|
446
434
|
});
|
|
@@ -451,12 +439,12 @@ const queryByAlias = vocabulary.serviceHandler({
|
|
|
451
439
|
* Query entities by category classification
|
|
452
440
|
* Uses indexClass GSI
|
|
453
441
|
*
|
|
454
|
-
* Note: This is a regular async function (not
|
|
442
|
+
* Note: This is a regular async function (not fabricService) because it accepts
|
|
455
443
|
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
456
444
|
*/
|
|
457
|
-
async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model,
|
|
445
|
+
async function queryByClass({ archived = false, ascending = false, deleted = false, limit, model, scope, recordClass, startKey, }) {
|
|
458
446
|
const suffix = calculateSuffix({ archived, deleted });
|
|
459
|
-
const keyValue = buildIndexClass(
|
|
447
|
+
const keyValue = buildIndexClass(scope, model, recordClass) + suffix;
|
|
460
448
|
return executeQuery(INDEX_CLASS, keyValue, {
|
|
461
449
|
ascending,
|
|
462
450
|
limit,
|
|
@@ -467,12 +455,12 @@ async function queryByClass({ archived = false, ascending = false, deleted = fal
|
|
|
467
455
|
* Query entities by type classification
|
|
468
456
|
* Uses indexType GSI
|
|
469
457
|
*
|
|
470
|
-
* Note: This is a regular async function (not
|
|
458
|
+
* Note: This is a regular async function (not fabricService) because it accepts
|
|
471
459
|
* complex startKey objects that can't be coerced by vocabulary's type system.
|
|
472
460
|
*/
|
|
473
|
-
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model,
|
|
461
|
+
async function queryByType({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, type, }) {
|
|
474
462
|
const suffix = calculateSuffix({ archived, deleted });
|
|
475
|
-
const keyValue = buildIndexType(
|
|
463
|
+
const keyValue = buildIndexType(scope, model, type) + suffix;
|
|
476
464
|
return executeQuery(INDEX_TYPE, keyValue, {
|
|
477
465
|
ascending,
|
|
478
466
|
limit,
|
|
@@ -483,7 +471,7 @@ async function queryByType({ archived = false, ascending = false, deleted = fals
|
|
|
483
471
|
* Query a single entity by external ID
|
|
484
472
|
* Uses indexXid GSI
|
|
485
473
|
*/
|
|
486
|
-
const queryByXid =
|
|
474
|
+
const queryByXid = fabric.fabricService({
|
|
487
475
|
alias: "queryByXid",
|
|
488
476
|
description: "Query a single entity by external ID",
|
|
489
477
|
input: {
|
|
@@ -500,17 +488,20 @@ const queryByXid = vocabulary.serviceHandler({
|
|
|
500
488
|
description: "Query deleted entities instead of active ones",
|
|
501
489
|
},
|
|
502
490
|
model: { type: String, description: "Entity model name" },
|
|
503
|
-
|
|
491
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
504
492
|
xid: { type: String, description: "External ID" },
|
|
505
493
|
},
|
|
506
|
-
service: async ({ archived, deleted, model,
|
|
494
|
+
service: async ({ archived, deleted, model, scope, xid, }) => {
|
|
507
495
|
const archivedBool = archived;
|
|
508
496
|
const deletedBool = deleted;
|
|
509
497
|
const modelStr = model;
|
|
510
|
-
const
|
|
498
|
+
const scopeStr = scope;
|
|
511
499
|
const xidStr = xid;
|
|
512
|
-
const suffix = calculateSuffix({
|
|
513
|
-
|
|
500
|
+
const suffix = calculateSuffix({
|
|
501
|
+
archived: archivedBool,
|
|
502
|
+
deleted: deletedBool,
|
|
503
|
+
});
|
|
504
|
+
const keyValue = buildIndexXid(scopeStr, modelStr, xidStr) + suffix;
|
|
514
505
|
const result = await executeQuery(INDEX_XID, keyValue, {
|
|
515
506
|
limit: 1,
|
|
516
507
|
});
|
|
@@ -521,65 +512,106 @@ const queryByXid = vocabulary.serviceHandler({
|
|
|
521
512
|
const DEFAULT_ENDPOINT = "http://127.0.0.1:8000";
|
|
522
513
|
const DEFAULT_REGION$1 = "us-east-1";
|
|
523
514
|
const DEFAULT_TABLE_NAME$1 = "jaypie-local";
|
|
515
|
+
// =============================================================================
|
|
516
|
+
// Index to GSI Conversion
|
|
517
|
+
// =============================================================================
|
|
518
|
+
/**
|
|
519
|
+
* Generate an index name from pk fields (if not provided)
|
|
520
|
+
*/
|
|
521
|
+
function generateIndexName(pk) {
|
|
522
|
+
const suffix = pk
|
|
523
|
+
.map((field) => field.charAt(0).toUpperCase() + field.slice(1))
|
|
524
|
+
.join("");
|
|
525
|
+
return `index${suffix}`;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Collect all unique indexes from DEFAULT_INDEXES and registered models
|
|
529
|
+
*/
|
|
530
|
+
function collectAllIndexes() {
|
|
531
|
+
const indexMap = new Map();
|
|
532
|
+
// Add DEFAULT_INDEXES first
|
|
533
|
+
for (const index of fabric.DEFAULT_INDEXES) {
|
|
534
|
+
const name = index.name ?? generateIndexName(index.pk);
|
|
535
|
+
indexMap.set(name, { ...index, name });
|
|
536
|
+
}
|
|
537
|
+
// Add registered model indexes (will not overwrite if name already exists)
|
|
538
|
+
for (const index of fabric.getAllRegisteredIndexes()) {
|
|
539
|
+
const name = index.name ?? generateIndexName(index.pk);
|
|
540
|
+
if (!indexMap.has(name)) {
|
|
541
|
+
indexMap.set(name, { ...index, name });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return Array.from(indexMap.values());
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Build attribute definitions from indexes
|
|
548
|
+
*/
|
|
549
|
+
function buildAttributeDefinitions(indexes) {
|
|
550
|
+
const attrs = new Map();
|
|
551
|
+
// Primary key attributes
|
|
552
|
+
attrs.set("model", "S");
|
|
553
|
+
attrs.set("id", "S");
|
|
554
|
+
attrs.set("sequence", "N");
|
|
555
|
+
// GSI attributes (partition keys are always strings)
|
|
556
|
+
for (const index of indexes) {
|
|
557
|
+
const indexName = index.name ?? generateIndexName(index.pk);
|
|
558
|
+
attrs.set(indexName, "S");
|
|
559
|
+
}
|
|
560
|
+
// Sort keys (sequence is always a number, others would be strings)
|
|
561
|
+
// Note: Currently all indexes use sequence as SK, so this is mostly future-proofing
|
|
562
|
+
for (const index of indexes) {
|
|
563
|
+
const sk = index.sk ?? ["sequence"];
|
|
564
|
+
for (const skField of sk) {
|
|
565
|
+
if (!attrs.has(skField)) {
|
|
566
|
+
// Assume string unless it's sequence
|
|
567
|
+
attrs.set(skField, skField === "sequence" ? "N" : "S");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return Array.from(attrs.entries())
|
|
572
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
573
|
+
.map(([name, type]) => ({
|
|
574
|
+
AttributeName: name,
|
|
575
|
+
AttributeType: type,
|
|
576
|
+
}));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Build GSI definitions from indexes
|
|
580
|
+
*/
|
|
581
|
+
function buildGSIs(indexes) {
|
|
582
|
+
const gsiProjection = { ProjectionType: "ALL" };
|
|
583
|
+
return indexes.map((index) => {
|
|
584
|
+
const indexName = index.name ?? generateIndexName(index.pk);
|
|
585
|
+
const sk = index.sk ?? ["sequence"];
|
|
586
|
+
// For GSIs, the partition key attribute name IS the index name
|
|
587
|
+
// (e.g., indexOu stores the composite key value "@#record")
|
|
588
|
+
return {
|
|
589
|
+
IndexName: indexName,
|
|
590
|
+
KeySchema: [
|
|
591
|
+
{ AttributeName: indexName, KeyType: "HASH" },
|
|
592
|
+
// Use first SK field as the range key attribute
|
|
593
|
+
{ AttributeName: sk[0], KeyType: "RANGE" },
|
|
594
|
+
],
|
|
595
|
+
Projection: gsiProjection,
|
|
596
|
+
};
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
// =============================================================================
|
|
600
|
+
// Table Creation
|
|
601
|
+
// =============================================================================
|
|
524
602
|
/**
|
|
525
603
|
* DynamoDB table schema with Jaypie GSI pattern
|
|
604
|
+
*
|
|
605
|
+
* Collects indexes from:
|
|
606
|
+
* 1. DEFAULT_INDEXES (5 standard GSIs)
|
|
607
|
+
* 2. Any custom indexes registered via registerModel()
|
|
526
608
|
*/
|
|
527
609
|
function createTableParams(tableName, billingMode) {
|
|
528
|
-
const
|
|
610
|
+
const allIndexes = collectAllIndexes();
|
|
529
611
|
return {
|
|
530
|
-
AttributeDefinitions:
|
|
531
|
-
{ AttributeName: "id", AttributeType: "S" },
|
|
532
|
-
{ AttributeName: "indexAlias", AttributeType: "S" },
|
|
533
|
-
{ AttributeName: "indexClass", AttributeType: "S" },
|
|
534
|
-
{ AttributeName: "indexOu", AttributeType: "S" },
|
|
535
|
-
{ AttributeName: "indexType", AttributeType: "S" },
|
|
536
|
-
{ AttributeName: "indexXid", AttributeType: "S" },
|
|
537
|
-
{ AttributeName: "model", AttributeType: "S" },
|
|
538
|
-
{ AttributeName: "sequence", AttributeType: "N" },
|
|
539
|
-
],
|
|
612
|
+
AttributeDefinitions: buildAttributeDefinitions(allIndexes),
|
|
540
613
|
BillingMode: billingMode,
|
|
541
|
-
GlobalSecondaryIndexes:
|
|
542
|
-
{
|
|
543
|
-
IndexName: "indexOu",
|
|
544
|
-
KeySchema: [
|
|
545
|
-
{ AttributeName: "indexOu", KeyType: "HASH" },
|
|
546
|
-
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
547
|
-
],
|
|
548
|
-
Projection: gsiProjection,
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
IndexName: "indexAlias",
|
|
552
|
-
KeySchema: [
|
|
553
|
-
{ AttributeName: "indexAlias", KeyType: "HASH" },
|
|
554
|
-
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
555
|
-
],
|
|
556
|
-
Projection: gsiProjection,
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
IndexName: "indexClass",
|
|
560
|
-
KeySchema: [
|
|
561
|
-
{ AttributeName: "indexClass", KeyType: "HASH" },
|
|
562
|
-
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
563
|
-
],
|
|
564
|
-
Projection: gsiProjection,
|
|
565
|
-
},
|
|
566
|
-
{
|
|
567
|
-
IndexName: "indexType",
|
|
568
|
-
KeySchema: [
|
|
569
|
-
{ AttributeName: "indexType", KeyType: "HASH" },
|
|
570
|
-
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
571
|
-
],
|
|
572
|
-
Projection: gsiProjection,
|
|
573
|
-
},
|
|
574
|
-
{
|
|
575
|
-
IndexName: "indexXid",
|
|
576
|
-
KeySchema: [
|
|
577
|
-
{ AttributeName: "indexXid", KeyType: "HASH" },
|
|
578
|
-
{ AttributeName: "sequence", KeyType: "RANGE" },
|
|
579
|
-
],
|
|
580
|
-
Projection: gsiProjection,
|
|
581
|
-
},
|
|
582
|
-
],
|
|
614
|
+
GlobalSecondaryIndexes: buildGSIs(allIndexes),
|
|
583
615
|
KeySchema: [
|
|
584
616
|
{ AttributeName: "model", KeyType: "HASH" },
|
|
585
617
|
{ AttributeName: "id", KeyType: "RANGE" },
|
|
@@ -590,7 +622,7 @@ function createTableParams(tableName, billingMode) {
|
|
|
590
622
|
/**
|
|
591
623
|
* Create DynamoDB table with Jaypie GSI schema
|
|
592
624
|
*/
|
|
593
|
-
const createTableHandler =
|
|
625
|
+
const createTableHandler = fabric.fabricService({
|
|
594
626
|
alias: "dynamodb_create_table",
|
|
595
627
|
description: "Create DynamoDB table with Jaypie GSI schema",
|
|
596
628
|
input: {
|
|
@@ -654,7 +686,7 @@ const DEFAULT_TABLE_NAME = "jaypie-local";
|
|
|
654
686
|
/**
|
|
655
687
|
* Generate docker-compose.yml for local DynamoDB development
|
|
656
688
|
*/
|
|
657
|
-
const dockerComposeHandler =
|
|
689
|
+
const dockerComposeHandler = fabric.fabricService({
|
|
658
690
|
alias: "dynamodb_generate_docker_compose",
|
|
659
691
|
description: "Generate docker-compose.yml for local DynamoDB development",
|
|
660
692
|
input: {
|
|
@@ -752,7 +784,7 @@ function ensureInitialized() {
|
|
|
752
784
|
/**
|
|
753
785
|
* Check DynamoDB connection status and configuration
|
|
754
786
|
*/
|
|
755
|
-
const statusHandler =
|
|
787
|
+
const statusHandler = fabric.fabricService({
|
|
756
788
|
alias: "dynamodb_status",
|
|
757
789
|
description: "Check DynamoDB connection status and configuration",
|
|
758
790
|
service: async () => {
|
|
@@ -783,12 +815,12 @@ function wrapWithInit(handler) {
|
|
|
783
815
|
return wrapped;
|
|
784
816
|
}
|
|
785
817
|
// MCP-specific serviceHandler wrappers for functions with complex inputs
|
|
786
|
-
// Note: These wrap the regular async functions to make them work with
|
|
818
|
+
// Note: These wrap the regular async functions to make them work with fabricMcp
|
|
787
819
|
/**
|
|
788
820
|
* MCP wrapper for putEntity
|
|
789
821
|
* Accepts entity JSON directly from LLM
|
|
790
822
|
*/
|
|
791
|
-
const mcpPutEntity =
|
|
823
|
+
const mcpPutEntity = fabric.fabricService({
|
|
792
824
|
alias: "dynamodb_put",
|
|
793
825
|
description: "Create or replace an entity in DynamoDB (auto-indexes GSI keys)",
|
|
794
826
|
input: {
|
|
@@ -796,10 +828,18 @@ const mcpPutEntity = vocabulary.serviceHandler({
|
|
|
796
828
|
id: { type: String, description: "Entity ID (sort key)" },
|
|
797
829
|
model: { type: String, description: "Entity model name (partition key)" },
|
|
798
830
|
name: { type: String, description: "Entity name" },
|
|
799
|
-
|
|
831
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
800
832
|
// Optional fields
|
|
801
|
-
alias: {
|
|
802
|
-
|
|
833
|
+
alias: {
|
|
834
|
+
type: String,
|
|
835
|
+
required: false,
|
|
836
|
+
description: "Human-friendly alias",
|
|
837
|
+
},
|
|
838
|
+
class: {
|
|
839
|
+
type: String,
|
|
840
|
+
required: false,
|
|
841
|
+
description: "Category classification",
|
|
842
|
+
},
|
|
803
843
|
type: { type: String, required: false, description: "Type classification" },
|
|
804
844
|
xid: { type: String, required: false, description: "External ID" },
|
|
805
845
|
},
|
|
@@ -812,7 +852,7 @@ const mcpPutEntity = vocabulary.serviceHandler({
|
|
|
812
852
|
id: input.id,
|
|
813
853
|
model: input.model,
|
|
814
854
|
name: input.name,
|
|
815
|
-
|
|
855
|
+
scope: input.scope,
|
|
816
856
|
sequence: Date.now(),
|
|
817
857
|
type: input.type,
|
|
818
858
|
updatedAt: now,
|
|
@@ -825,7 +865,7 @@ const mcpPutEntity = vocabulary.serviceHandler({
|
|
|
825
865
|
* MCP wrapper for updateEntity
|
|
826
866
|
* Accepts entity JSON directly from LLM
|
|
827
867
|
*/
|
|
828
|
-
const mcpUpdateEntity =
|
|
868
|
+
const mcpUpdateEntity = fabric.fabricService({
|
|
829
869
|
alias: "dynamodb_update",
|
|
830
870
|
description: "Update an entity in DynamoDB (sets updatedAt, re-indexes GSI keys)",
|
|
831
871
|
input: {
|
|
@@ -834,9 +874,17 @@ const mcpUpdateEntity = vocabulary.serviceHandler({
|
|
|
834
874
|
model: { type: String, description: "Entity model name (partition key)" },
|
|
835
875
|
// Fields that can be updated
|
|
836
876
|
name: { type: String, required: false, description: "Entity name" },
|
|
837
|
-
|
|
838
|
-
alias: {
|
|
839
|
-
|
|
877
|
+
scope: { type: String, required: false, description: "Scope" },
|
|
878
|
+
alias: {
|
|
879
|
+
type: String,
|
|
880
|
+
required: false,
|
|
881
|
+
description: "Human-friendly alias",
|
|
882
|
+
},
|
|
883
|
+
class: {
|
|
884
|
+
type: String,
|
|
885
|
+
required: false,
|
|
886
|
+
description: "Category classification",
|
|
887
|
+
},
|
|
840
888
|
type: { type: String, required: false, description: "Type classification" },
|
|
841
889
|
xid: { type: String, required: false, description: "External ID" },
|
|
842
890
|
},
|
|
@@ -855,7 +903,7 @@ const mcpUpdateEntity = vocabulary.serviceHandler({
|
|
|
855
903
|
...(input.alias !== undefined && { alias: input.alias }),
|
|
856
904
|
...(input.class !== undefined && { class: input.class }),
|
|
857
905
|
...(input.name !== undefined && { name: input.name }),
|
|
858
|
-
...(input.
|
|
906
|
+
...(input.scope !== undefined && { scope: input.scope }),
|
|
859
907
|
...(input.type !== undefined && { type: input.type }),
|
|
860
908
|
...(input.xid !== undefined && { xid: input.xid }),
|
|
861
909
|
};
|
|
@@ -863,15 +911,15 @@ const mcpUpdateEntity = vocabulary.serviceHandler({
|
|
|
863
911
|
},
|
|
864
912
|
});
|
|
865
913
|
/**
|
|
866
|
-
* MCP wrapper for
|
|
914
|
+
* MCP wrapper for queryByScope
|
|
867
915
|
* Note: Pagination via startKey is not exposed to MCP; use limit instead
|
|
868
916
|
*/
|
|
869
|
-
const
|
|
870
|
-
alias: "
|
|
871
|
-
description: "Query entities by
|
|
917
|
+
const mcpQueryByScope = fabric.fabricService({
|
|
918
|
+
alias: "dynamodb_query_scope",
|
|
919
|
+
description: "Query entities by scope (parent hierarchy)",
|
|
872
920
|
input: {
|
|
873
921
|
model: { type: String, description: "Entity model name" },
|
|
874
|
-
|
|
922
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
875
923
|
archived: {
|
|
876
924
|
type: Boolean,
|
|
877
925
|
default: false,
|
|
@@ -897,13 +945,13 @@ const mcpQueryByOu = vocabulary.serviceHandler({
|
|
|
897
945
|
},
|
|
898
946
|
},
|
|
899
947
|
service: async (input) => {
|
|
900
|
-
return
|
|
948
|
+
return queryByScope({
|
|
901
949
|
archived: input.archived,
|
|
902
950
|
ascending: input.ascending,
|
|
903
951
|
deleted: input.deleted,
|
|
904
952
|
limit: input.limit,
|
|
905
953
|
model: input.model,
|
|
906
|
-
|
|
954
|
+
scope: input.scope,
|
|
907
955
|
});
|
|
908
956
|
},
|
|
909
957
|
});
|
|
@@ -911,12 +959,12 @@ const mcpQueryByOu = vocabulary.serviceHandler({
|
|
|
911
959
|
* MCP wrapper for queryByClass
|
|
912
960
|
* Note: Pagination via startKey is not exposed to MCP; use limit instead
|
|
913
961
|
*/
|
|
914
|
-
const mcpQueryByClass =
|
|
962
|
+
const mcpQueryByClass = fabric.fabricService({
|
|
915
963
|
alias: "dynamodb_query_class",
|
|
916
964
|
description: "Query entities by category classification",
|
|
917
965
|
input: {
|
|
918
966
|
model: { type: String, description: "Entity model name" },
|
|
919
|
-
|
|
967
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
920
968
|
recordClass: { type: String, description: "Category classification" },
|
|
921
969
|
archived: {
|
|
922
970
|
type: Boolean,
|
|
@@ -949,7 +997,7 @@ const mcpQueryByClass = vocabulary.serviceHandler({
|
|
|
949
997
|
deleted: input.deleted,
|
|
950
998
|
limit: input.limit,
|
|
951
999
|
model: input.model,
|
|
952
|
-
|
|
1000
|
+
scope: input.scope,
|
|
953
1001
|
recordClass: input.recordClass,
|
|
954
1002
|
});
|
|
955
1003
|
},
|
|
@@ -958,12 +1006,12 @@ const mcpQueryByClass = vocabulary.serviceHandler({
|
|
|
958
1006
|
* MCP wrapper for queryByType
|
|
959
1007
|
* Note: Pagination via startKey is not exposed to MCP; use limit instead
|
|
960
1008
|
*/
|
|
961
|
-
const mcpQueryByType =
|
|
1009
|
+
const mcpQueryByType = fabric.fabricService({
|
|
962
1010
|
alias: "dynamodb_query_type",
|
|
963
1011
|
description: "Query entities by type classification",
|
|
964
1012
|
input: {
|
|
965
1013
|
model: { type: String, description: "Entity model name" },
|
|
966
|
-
|
|
1014
|
+
scope: { type: String, description: "Scope (@ for root)" },
|
|
967
1015
|
type: { type: String, description: "Type classification" },
|
|
968
1016
|
archived: {
|
|
969
1017
|
type: Boolean,
|
|
@@ -996,7 +1044,7 @@ const mcpQueryByType = vocabulary.serviceHandler({
|
|
|
996
1044
|
deleted: input.deleted,
|
|
997
1045
|
limit: input.limit,
|
|
998
1046
|
model: input.model,
|
|
999
|
-
|
|
1047
|
+
scope: input.scope,
|
|
1000
1048
|
type: input.type,
|
|
1001
1049
|
});
|
|
1002
1050
|
},
|
|
@@ -1008,80 +1056,80 @@ function registerDynamoDbTools(config) {
|
|
|
1008
1056
|
const { includeAdmin = true, server } = config;
|
|
1009
1057
|
const tools = [];
|
|
1010
1058
|
// Entity operations
|
|
1011
|
-
mcp.
|
|
1012
|
-
|
|
1059
|
+
mcp.fabricMcp({
|
|
1060
|
+
service: wrapWithInit(getEntity),
|
|
1013
1061
|
name: "dynamodb_get",
|
|
1014
1062
|
server,
|
|
1015
1063
|
});
|
|
1016
1064
|
tools.push("dynamodb_get");
|
|
1017
|
-
mcp.
|
|
1018
|
-
|
|
1065
|
+
mcp.fabricMcp({
|
|
1066
|
+
service: wrapWithInit(mcpPutEntity),
|
|
1019
1067
|
name: "dynamodb_put",
|
|
1020
1068
|
server,
|
|
1021
1069
|
});
|
|
1022
1070
|
tools.push("dynamodb_put");
|
|
1023
|
-
mcp.
|
|
1024
|
-
|
|
1071
|
+
mcp.fabricMcp({
|
|
1072
|
+
service: wrapWithInit(mcpUpdateEntity),
|
|
1025
1073
|
name: "dynamodb_update",
|
|
1026
1074
|
server,
|
|
1027
1075
|
});
|
|
1028
1076
|
tools.push("dynamodb_update");
|
|
1029
|
-
mcp.
|
|
1030
|
-
|
|
1077
|
+
mcp.fabricMcp({
|
|
1078
|
+
service: wrapWithInit(deleteEntity),
|
|
1031
1079
|
name: "dynamodb_delete",
|
|
1032
1080
|
server,
|
|
1033
1081
|
});
|
|
1034
1082
|
tools.push("dynamodb_delete");
|
|
1035
|
-
mcp.
|
|
1036
|
-
|
|
1083
|
+
mcp.fabricMcp({
|
|
1084
|
+
service: wrapWithInit(archiveEntity),
|
|
1037
1085
|
name: "dynamodb_archive",
|
|
1038
1086
|
server,
|
|
1039
1087
|
});
|
|
1040
1088
|
tools.push("dynamodb_archive");
|
|
1041
|
-
mcp.
|
|
1042
|
-
|
|
1089
|
+
mcp.fabricMcp({
|
|
1090
|
+
service: wrapWithInit(destroyEntity),
|
|
1043
1091
|
name: "dynamodb_destroy",
|
|
1044
1092
|
server,
|
|
1045
1093
|
});
|
|
1046
1094
|
tools.push("dynamodb_destroy");
|
|
1047
1095
|
// Query operations
|
|
1048
|
-
mcp.
|
|
1049
|
-
|
|
1050
|
-
name: "
|
|
1096
|
+
mcp.fabricMcp({
|
|
1097
|
+
service: wrapWithInit(mcpQueryByScope),
|
|
1098
|
+
name: "dynamodb_query_scope",
|
|
1051
1099
|
server,
|
|
1052
1100
|
});
|
|
1053
|
-
tools.push("
|
|
1054
|
-
mcp.
|
|
1055
|
-
|
|
1101
|
+
tools.push("dynamodb_query_scope");
|
|
1102
|
+
mcp.fabricMcp({
|
|
1103
|
+
service: wrapWithInit(queryByAlias),
|
|
1056
1104
|
name: "dynamodb_query_alias",
|
|
1057
1105
|
server,
|
|
1058
1106
|
});
|
|
1059
1107
|
tools.push("dynamodb_query_alias");
|
|
1060
|
-
mcp.
|
|
1061
|
-
|
|
1108
|
+
mcp.fabricMcp({
|
|
1109
|
+
service: wrapWithInit(mcpQueryByClass),
|
|
1062
1110
|
name: "dynamodb_query_class",
|
|
1063
1111
|
server,
|
|
1064
1112
|
});
|
|
1065
1113
|
tools.push("dynamodb_query_class");
|
|
1066
|
-
mcp.
|
|
1067
|
-
|
|
1114
|
+
mcp.fabricMcp({
|
|
1115
|
+
service: wrapWithInit(mcpQueryByType),
|
|
1068
1116
|
name: "dynamodb_query_type",
|
|
1069
1117
|
server,
|
|
1070
1118
|
});
|
|
1071
1119
|
tools.push("dynamodb_query_type");
|
|
1072
|
-
mcp.
|
|
1073
|
-
|
|
1120
|
+
mcp.fabricMcp({
|
|
1121
|
+
service: wrapWithInit(queryByXid),
|
|
1074
1122
|
name: "dynamodb_query_xid",
|
|
1075
1123
|
server,
|
|
1076
1124
|
});
|
|
1077
1125
|
tools.push("dynamodb_query_xid");
|
|
1078
1126
|
// Admin tools (MCP-only)
|
|
1079
1127
|
if (includeAdmin) {
|
|
1080
|
-
mcp.
|
|
1128
|
+
mcp.fabricMcp({ service: statusHandler, server });
|
|
1081
1129
|
tools.push("dynamodb_status");
|
|
1082
|
-
mcp.
|
|
1130
|
+
mcp.fabricMcp({ service: createTableHandler, server });
|
|
1083
1131
|
tools.push("dynamodb_create_table");
|
|
1084
|
-
mcp.
|
|
1132
|
+
mcp.fabricMcp({ service: dockerComposeHandler, server });
|
|
1085
1133
|
tools.push("dynamodb_generate_docker_compose");
|
|
1086
1134
|
}
|
|
1087
1135
|
return { tools };
|