@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,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var mcp = require('@jaypie/vocabulary/mcp');
4
- var vocabulary = require('@jaypie/vocabulary');
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
- // Primary markers
81
- const SEPARATOR = "#"; // Composite key separator
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 INDEX_OU = "indexOu";
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 indexOu key for hierarchical queries
94
- * @param ou - The organizational unit (APEX or "{parent.model}#{parent.id}")
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: "{ou}#{model}"
95
+ * @returns Composite key: "{scope}#{model}"
97
96
  */
98
- function buildIndexOu(ou, model) {
99
- return `${ou}${SEPARATOR}${model}`;
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 ou - The organizational unit
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: "{ou}#{model}#{alias}"
105
+ * @returns Composite key: "{scope}#{model}#{alias}"
107
106
  */
108
- function buildIndexAlias(ou, model, alias) {
109
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${alias}`;
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 ou - The organizational unit
112
+ * @param scope - The scope
114
113
  * @param model - The entity model name
115
114
  * @param recordClass - The category classification
116
- * @returns Composite key: "{ou}#{model}#{class}"
115
+ * @returns Composite key: "{scope}#{model}#{class}"
117
116
  */
118
- function buildIndexClass(ou, model, recordClass) {
119
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${recordClass}`;
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 ou - The organizational unit
122
+ * @param scope - The scope
124
123
  * @param model - The entity model name
125
124
  * @param type - The type classification
126
- * @returns Composite key: "{ou}#{model}#{type}"
125
+ * @returns Composite key: "{scope}#{model}#{type}"
127
126
  */
128
- function buildIndexType(ou, model, type) {
129
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${type}`;
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 ou - The organizational unit
132
+ * @param scope - The scope
134
133
  * @param model - The entity model name
135
134
  * @param xid - The external ID
136
- * @returns Composite key: "{ou}#{model}#{xid}"
135
+ * @returns Composite key: "{scope}#{model}#{xid}"
137
136
  */
138
- function buildIndexXid(ou, model, xid) {
139
- return `${ou}${SEPARATOR}${model}${SEPARATOR}${xid}`;
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
- * - indexOu is always populated from ou + model
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 result = { ...entity };
155
- // indexOu is always set (from ou + model)
156
- result.indexOu = buildIndexOu(entity.ou, entity.model) + suffix;
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 = vocabulary.serviceHandler({
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 serviceHandler) because it accepts
220
- * complex FabricEntity objects that can't be coerced by vocabulary's type system.
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 serviceHandler) because it accepts
239
- * complex FabricEntity objects that can't be coerced by vocabulary's type system.
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 = vocabulary.serviceHandler({
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 = vocabulary.serviceHandler({
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 = vocabulary.serviceHandler({
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 organizational unit (parent hierarchy)
398
- * Uses indexOu GSI
382
+ * Query entities by scope (parent hierarchy)
383
+ * Uses indexScope GSI
399
384
  *
400
- * Note: This is a regular async function (not serviceHandler) because it accepts
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 queryByOu({ archived = false, ascending = false, deleted = false, limit, model, ou, startKey, }) {
388
+ async function queryByScope({ archived = false, ascending = false, deleted = false, limit, model, scope, startKey, }) {
404
389
  const suffix = calculateSuffix({ archived, deleted });
405
- const keyValue = buildIndexOu(ou, model) + suffix;
406
- return executeQuery(INDEX_OU, keyValue, {
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 = vocabulary.serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
419
+ scope: { type: String, description: "Scope (@ for root)" },
435
420
  },
436
- service: async ({ alias, archived, deleted, model, ou, }) => {
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 ouStr = ou;
442
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
443
- const keyValue = buildIndexAlias(ouStr, modelStr, aliasStr) + suffix;
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 serviceHandler) because it accepts
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, ou, recordClass, startKey, }) {
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(ou, model, recordClass) + suffix;
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 serviceHandler) because it accepts
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, ou, startKey, type, }) {
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(ou, model, type) + suffix;
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 = vocabulary.serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
491
+ scope: { type: String, description: "Scope (@ for root)" },
504
492
  xid: { type: String, description: "External ID" },
505
493
  },
506
- service: async ({ archived, deleted, model, ou, xid, }) => {
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 ouStr = ou;
498
+ const scopeStr = scope;
511
499
  const xidStr = xid;
512
- const suffix = calculateSuffix({ archived: archivedBool, deleted: deletedBool });
513
- const keyValue = buildIndexXid(ouStr, modelStr, xidStr) + suffix;
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 gsiProjection = { ProjectionType: "ALL" };
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 = vocabulary.serviceHandler({
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 = vocabulary.serviceHandler({
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 = vocabulary.serviceHandler({
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 registerMcpTool
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 = vocabulary.serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
831
+ scope: { type: String, description: "Scope (@ for root)" },
800
832
  // Optional fields
801
- alias: { type: String, required: false, description: "Human-friendly alias" },
802
- class: { type: String, required: false, description: "Category classification" },
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
- ou: input.ou,
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 = vocabulary.serviceHandler({
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
- ou: { type: String, required: false, description: "Organizational unit" },
838
- alias: { type: String, required: false, description: "Human-friendly alias" },
839
- class: { type: String, required: false, description: "Category classification" },
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.ou !== undefined && { ou: input.ou }),
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 queryByOu
914
+ * MCP wrapper for queryByScope
867
915
  * Note: Pagination via startKey is not exposed to MCP; use limit instead
868
916
  */
869
- const mcpQueryByOu = vocabulary.serviceHandler({
870
- alias: "dynamodb_query_ou",
871
- description: "Query entities by organizational unit (parent hierarchy)",
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
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 queryByOu({
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
- ou: input.ou,
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 = vocabulary.serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
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
- ou: input.ou,
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 = vocabulary.serviceHandler({
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
- ou: { type: String, description: "Organizational unit (@ for root)" },
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
- ou: input.ou,
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.registerMcpTool({
1012
- handler: wrapWithInit(getEntity),
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.registerMcpTool({
1018
- handler: wrapWithInit(mcpPutEntity),
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.registerMcpTool({
1024
- handler: wrapWithInit(mcpUpdateEntity),
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.registerMcpTool({
1030
- handler: wrapWithInit(deleteEntity),
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.registerMcpTool({
1036
- handler: wrapWithInit(archiveEntity),
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.registerMcpTool({
1042
- handler: wrapWithInit(destroyEntity),
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.registerMcpTool({
1049
- handler: wrapWithInit(mcpQueryByOu),
1050
- name: "dynamodb_query_ou",
1096
+ mcp.fabricMcp({
1097
+ service: wrapWithInit(mcpQueryByScope),
1098
+ name: "dynamodb_query_scope",
1051
1099
  server,
1052
1100
  });
1053
- tools.push("dynamodb_query_ou");
1054
- mcp.registerMcpTool({
1055
- handler: wrapWithInit(queryByAlias),
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.registerMcpTool({
1061
- handler: wrapWithInit(mcpQueryByClass),
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.registerMcpTool({
1067
- handler: wrapWithInit(mcpQueryByType),
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.registerMcpTool({
1073
- handler: wrapWithInit(queryByXid),
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.registerMcpTool({ handler: statusHandler, server });
1128
+ mcp.fabricMcp({ service: statusHandler, server });
1081
1129
  tools.push("dynamodb_status");
1082
- mcp.registerMcpTool({ handler: createTableHandler, server });
1130
+ mcp.fabricMcp({ service: createTableHandler, server });
1083
1131
  tools.push("dynamodb_create_table");
1084
- mcp.registerMcpTool({ handler: dockerComposeHandler, server });
1132
+ mcp.fabricMcp({ service: dockerComposeHandler, server });
1085
1133
  tools.push("dynamodb_generate_docker_compose");
1086
1134
  }
1087
1135
  return { tools };