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