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