@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.
@@ -1,5 +1,5 @@
1
- import { registerMcpTool } from '@jaypie/vocabulary/mcp';
2
- import { serviceHandler } from '@jaypie/vocabulary';
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
- // Primary markers
79
- const SEPARATOR = "#"; // Composite key separator
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 INDEX_OU = "indexOu";
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 indexOu key for hierarchical queries
92
- * @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
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: "{ou}#{model}"
93
+ * @returns Composite key: "{scope}#{model}"
95
94
  */
96
- function buildIndexOu(ou, model) {
97
- return `${ou}${SEPARATOR}${model}`;
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 ou - The organizational unit
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: "{ou}#{model}#{alias}"
103
+ * @returns Composite key: "{scope}#{model}#{alias}"
105
104
  */
106
- function buildIndexAlias(ou, model, alias) {
107
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;
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 ou - The organizational unit
110
+ * @param scope - The scope
112
111
  * @param model - The entity model name
113
112
  * @param recordClass - The category classification
114
- * @returns Composite key: "{ou}#{model}#{class}"
113
+ * @returns Composite key: "{scope}#{model}#{class}"
115
114
  */
116
- function buildIndexClass(ou, model, recordClass) {
117
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;
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 ou - The organizational unit
120
+ * @param scope - The scope
122
121
  * @param model - The entity model name
123
122
  * @param type - The type classification
124
- * @returns Composite key: "{ou}#{model}#{type}"
123
+ * @returns Composite key: "{scope}#{model}#{type}"
125
124
  */
126
- function buildIndexType(ou, model, type) {
127
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;
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 ou - The organizational unit
130
+ * @param scope - The scope
132
131
  * @param model - The entity model name
133
132
  * @param xid - The external ID
134
- * @returns Composite key: "{ou}#{model}#{xid}"
133
+ * @returns Composite key: "{scope}#{model}#{xid}"
135
134
  */
136
- function buildIndexXid(ou, model, xid) {
137
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
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
- * - indexOu is always populated from ou + model
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 result = { ...entity };
153
- // indexOu is always set (from ou + model)
154
- result.indexOu = buildIndexOu(entity.ou, entity.model) + suffix;
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 = serviceHandler({
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 serviceHandler) because it accepts
218
- * complex FabricEntity objects that can't be coerced by vocabulary's type system.
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 serviceHandler) because it accepts
237
- * complex FabricEntity objects that can't be coerced by vocabulary's type system.
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 = serviceHandler({
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 = serviceHandler({
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 = serviceHandler({
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 organizational unit (parent hierarchy)
396
- * Uses indexOu GSI
380
+ * Query entities by scope (parent hierarchy)
381
+ * Uses indexScope GSI
397
382
  *
398
- * Note: This is a regular async function (not serviceHandler) because it accepts
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 queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
386
+ async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
402
387
  const suffix = calculateSuffix({ archived, deleted });
403
- const keyValue = buildIndexOu(ou, model) + suffix;
404
- return executeQuery(INDEX_OU, keyValue, {
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 = serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
417
+ scope: { type: String, description: "Scope (@ for root)" },
433
418
  },
434
- service: async ({ alias, archived, deleted, model, ou, }) => {
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 ouStr = ou;
440
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
441
- const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
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 serviceHandler) because it accepts
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, ou, recordClass, startKey, }) {
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(ou, model, recordClass) + suffix;
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 serviceHandler) because it accepts
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, ou, startKey, type, }) {
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(ou, model, type) + suffix;
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 = serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
489
+ scope: { type: String, description: "Scope (@ for root)" },
502
490
  xid: { type: String, description: "External ID" },
503
491
  },
504
- service: async ({ archived, deleted, model, ou, xid, }) => {
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 ouStr = ou;
496
+ const scopeStr = scope;
509
497
  const xidStr = xid;
510
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
511
- const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
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 gsiProjection = { ProjectionType: "ALL" };
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 = serviceHandler({
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 = serviceHandler({
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 = serviceHandler({
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 registerMcpTool
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 = serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
829
+ scope: { type: String, description: "Scope (@ for root)" },
798
830
  // Optional fields
799
- alias: { type: String, required: false, description: "Human-friendly alias" },
800
- class: { type: String, required: false, description: "Category classification" },
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
- ou: input.ou,
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 = serviceHandler({
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
- ou: { type: String, required: false, description: "Organizational unit" },
836
- alias: { type: String, required: false, description: "Human-friendly alias" },
837
- class: { type: String, required: false, description: "Category classification" },
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.ou !== undefined && { ou: input.ou }),
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 queryByOu
912
+ * MCP wrapper for queryByScope
865
913
  * Note: Pagination via startKey is not exposed to MCP; use limit instead
866
914
  */
867
- const mcpQueryByOu = serviceHandler({
868
- alias: "dynamodb_query_ou",
869
- description: "Query entities by organizational unit (parent hierarchy)",
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
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 queryByOu({
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
- ou: input.ou,
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 = serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
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
- ou: input.ou,
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 = serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
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
- ou: input.ou,
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
- registerMcpTool({
1010
- handler: wrapWithInit(getEntity),
1057
+ fabricMcp({
1058
+ service: wrapWithInit(getEntity),
1011
1059
  name: "dynamodb_get",
1012
1060
  server,
1013
1061
  });
1014
1062
  tools.push("dynamodb_get");
1015
- registerMcpTool({
1016
- handler: wrapWithInit(mcpPutEntity),
1063
+ fabricMcp({
1064
+ service: wrapWithInit(mcpPutEntity),
1017
1065
  name: "dynamodb_put",
1018
1066
  server,
1019
1067
  });
1020
1068
  tools.push("dynamodb_put");
1021
- registerMcpTool({
1022
- handler: wrapWithInit(mcpUpdateEntity),
1069
+ fabricMcp({
1070
+ service: wrapWithInit(mcpUpdateEntity),
1023
1071
  name: "dynamodb_update",
1024
1072
  server,
1025
1073
  });
1026
1074
  tools.push("dynamodb_update");
1027
- registerMcpTool({
1028
- handler: wrapWithInit(deleteEntity),
1075
+ fabricMcp({
1076
+ service: wrapWithInit(deleteEntity),
1029
1077
  name: "dynamodb_delete",
1030
1078
  server,
1031
1079
  });
1032
1080
  tools.push("dynamodb_delete");
1033
- registerMcpTool({
1034
- handler: wrapWithInit(archiveEntity),
1081
+ fabricMcp({
1082
+ service: wrapWithInit(archiveEntity),
1035
1083
  name: "dynamodb_archive",
1036
1084
  server,
1037
1085
  });
1038
1086
  tools.push("dynamodb_archive");
1039
- registerMcpTool({
1040
- handler: wrapWithInit(destroyEntity),
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
- registerMcpTool({
1047
- handler: wrapWithInit(mcpQueryByOu),
1048
- name: "dynamodb_query_ou",
1094
+ fabricMcp({
1095
+ service: wrapWithInit(mcpQueryByScope),
1096
+ name: "dynamodb_query_scope",
1049
1097
  server,
1050
1098
  });
1051
- tools.push("dynamodb_query_ou");
1052
- registerMcpTool({
1053
- handler: wrapWithInit(queryByAlias),
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
- registerMcpTool({
1059
- handler: wrapWithInit(mcpQueryByClass),
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
- registerMcpTool({
1065
- handler: wrapWithInit(mcpQueryByType),
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
- registerMcpTool({
1071
- handler: wrapWithInit(queryByXid),
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
- registerMcpTool({ handler: statusHandler, server });
1126
+ fabricMcp({ service: statusHandler, server });
1079
1127
  tools.push("dynamodb_status");
1080
- registerMcpTool({ handler: createTableHandler, server });
1128
+ fabricMcp({ service: createTableHandler, server });
1081
1129
  tools.push("dynamodb_create_table");
1082
- registerMcpTool({ handler: dockerComposeHandler, server });
1130
+ fabricMcp({ service: dockerComposeHandler, server });
1083
1131
  tools.push("dynamodb_generate_docker_compose");
1084
1132
  }
1085
1133
  return { tools };