@stonyx/orm 0.3.2-beta.64 → 0.3.2-beta.66

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.
@@ -0,0 +1,556 @@
1
+ /**
2
+ * DynamoDB driver implementing the SqlDb PAL contract.
3
+ *
4
+ * Drop-in replacement for PostgresDB / MysqlDB — zero ORM core changes.
5
+ * Selected via config.orm.dynamodb.
6
+ */
7
+ import { createDocumentClient, destroyDocumentClient } from './connection.js';
8
+ import { buildPutItem, buildGetItem, buildUpdateItem, buildDeleteItem, buildScan, buildQuery, } from './operation-builder.js';
9
+ import { introspectModels, getTopologicalOrder } from '../postgres/schema-introspector.js';
10
+ import { getDynamoKeyType } from './type-map.js';
11
+ import { store } from '@stonyx/orm';
12
+ import { createRecord } from '../manage-record.js';
13
+ import { getPluralName } from '../plural-registry.js';
14
+ import { sanitizeTableName } from '../schema-helpers.js';
15
+ import config from 'stonyx/config';
16
+ import log from 'stonyx/log';
17
+ // ---------------------------------------------------------------------------
18
+ // ULID — monotonic, inline implementation (avoids heavy dep)
19
+ // ---------------------------------------------------------------------------
20
+ const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
21
+ function generateUlid() {
22
+ const now = Date.now();
23
+ let id = '';
24
+ // 10-char timestamp (48-bit millisecond precision)
25
+ let t = now;
26
+ for (let i = 9; i >= 0; i--) {
27
+ id = CROCKFORD[t % 32] + id;
28
+ t = Math.floor(t / 32);
29
+ }
30
+ // 16-char random
31
+ for (let i = 0; i < 16; i++) {
32
+ id += CROCKFORD[Math.floor(Math.random() * 32)];
33
+ }
34
+ return id;
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // SDK Command factories (injectable for testing without real AWS SDK)
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Load the DynamoDB DocumentClient command constructors via dynamic import.
41
+ * Returns a frozen object so it can be cached in deps.
42
+ */
43
+ export async function loadDocClientCommands() {
44
+ return import('@aws-sdk/lib-dynamodb');
45
+ }
46
+ export async function loadTableCommands() {
47
+ return import('@aws-sdk/client-dynamodb');
48
+ }
49
+ const defaultDeps = {
50
+ createDocumentClient,
51
+ destroyDocumentClient,
52
+ loadDocClientCommands,
53
+ loadTableCommands,
54
+ buildPutItem,
55
+ buildGetItem,
56
+ buildUpdateItem,
57
+ buildDeleteItem,
58
+ buildScan,
59
+ buildQuery,
60
+ introspectModels,
61
+ getTopologicalOrder,
62
+ getDynamoKeyType,
63
+ createRecord,
64
+ store,
65
+ getPluralName,
66
+ config,
67
+ log,
68
+ };
69
+ // ---------------------------------------------------------------------------
70
+ // DynamoDB driver
71
+ // ---------------------------------------------------------------------------
72
+ export default class DynamoDBDB {
73
+ static instance;
74
+ deps;
75
+ client;
76
+ dbConfig;
77
+ /** GSI registry built during init from model introspection. */
78
+ _gsiRegistry = new Map();
79
+ constructor(deps = {}) {
80
+ const Ctor = this.constructor;
81
+ if (Ctor.instance)
82
+ return Ctor.instance;
83
+ Ctor.instance = this;
84
+ this.deps = { ...defaultDeps, ...deps };
85
+ this.client = null;
86
+ const dynamoConfig = this.deps.config.orm.dynamodb;
87
+ if (!dynamoConfig)
88
+ throw new Error('DynamoDB configuration (config.orm.dynamodb) is required');
89
+ this.dbConfig = dynamoConfig;
90
+ }
91
+ requireClient() {
92
+ if (!this.client)
93
+ throw new Error('DynamoDBDB client not initialized — call init() first');
94
+ return this.client;
95
+ }
96
+ /** Resolve Orm singleton — falls back to real import in production. */
97
+ async _getOrm() {
98
+ if (this.deps._importOrm)
99
+ return this.deps._importOrm();
100
+ return import('@stonyx/orm');
101
+ }
102
+ // -------------------------------------------------------------------------
103
+ // SqlDb contract — init
104
+ // -------------------------------------------------------------------------
105
+ async init() {
106
+ this.client = await this.deps.createDocumentClient(this.dbConfig);
107
+ this._buildGsiRegistry();
108
+ await this.loadMemoryRecords();
109
+ }
110
+ // -------------------------------------------------------------------------
111
+ // SqlDb contract — startup (table provisioning)
112
+ // -------------------------------------------------------------------------
113
+ /**
114
+ * For each model, DescribeTable — CreateTable if missing (with GSIs, PAY_PER_REQUEST).
115
+ * For existing tables, check for missing GSIs and UpdateTable + poll for ACTIVE.
116
+ */
117
+ async startup() {
118
+ const schemas = this.deps.introspectModels();
119
+ const { DynamoDBClient, DescribeTableCommand, CreateTableCommand, UpdateTableCommand } = await this.deps.loadTableCommands();
120
+ const clientOptions = {};
121
+ if (this.dbConfig.region)
122
+ clientOptions.region = this.dbConfig.region;
123
+ if (this.dbConfig.endpoint)
124
+ clientOptions.endpoint = this.dbConfig.endpoint;
125
+ const rawClient = new DynamoDBClient(clientOptions);
126
+ for (const [modelName, schema] of Object.entries(schemas)) {
127
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
128
+ const gsis = this._buildGsiDefinitions(modelName, schema);
129
+ try {
130
+ const desc = await rawClient.send(new DescribeTableCommand({ TableName: tableName }));
131
+ // Table exists — check for missing GSIs
132
+ const existingGsiNames = new Set((desc.Table?.GlobalSecondaryIndexes ?? []).map((g) => g.IndexName ?? ''));
133
+ for (const gsi of gsis) {
134
+ const gsiName = gsi.IndexName;
135
+ if (gsiName && !existingGsiNames.has(gsiName)) {
136
+ this.deps.log.db?.(`Adding missing GSI '${gsiName}' to table '${tableName}'`);
137
+ await rawClient.send(new UpdateTableCommand({
138
+ TableName: tableName,
139
+ GlobalSecondaryIndexUpdates: [{ Create: gsi }],
140
+ AttributeDefinitions: this._buildAttributeDefinitions(schema),
141
+ }));
142
+ await this._waitForTableActive(rawClient, tableName, DescribeTableCommand);
143
+ }
144
+ }
145
+ this.deps.log.db?.(`DynamoDB table '${tableName}' is ready`);
146
+ }
147
+ catch (err) {
148
+ const code = err.name;
149
+ if (code !== 'ResourceNotFoundException')
150
+ throw err;
151
+ // Table does not exist — create it
152
+ this.deps.log.db?.(`Creating DynamoDB table '${tableName}'`);
153
+ await rawClient.send(new CreateTableCommand({
154
+ TableName: tableName,
155
+ BillingMode: 'PAY_PER_REQUEST',
156
+ AttributeDefinitions: this._buildAttributeDefinitions(schema),
157
+ KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
158
+ ...(gsis.length > 0 ? { GlobalSecondaryIndexes: gsis } : {}),
159
+ }));
160
+ await this._waitForTableActive(rawClient, tableName, DescribeTableCommand);
161
+ this.deps.log.db?.(`Created DynamoDB table '${tableName}'`);
162
+ }
163
+ }
164
+ }
165
+ // -------------------------------------------------------------------------
166
+ // SqlDb contract — shutdown
167
+ // -------------------------------------------------------------------------
168
+ async shutdown() {
169
+ this.client = this.deps.destroyDocumentClient(this.client);
170
+ }
171
+ // -------------------------------------------------------------------------
172
+ // SqlDb contract — persist
173
+ // -------------------------------------------------------------------------
174
+ async persist(operation, modelName, context, response) {
175
+ const OrmModule = await this._getOrm();
176
+ if (OrmModule.default?.instance?.isView?.(modelName))
177
+ return;
178
+ switch (operation) {
179
+ case 'create':
180
+ return this._persistCreate(modelName, context, response);
181
+ case 'update':
182
+ return this._persistUpdate(modelName, context);
183
+ case 'delete':
184
+ return this._persistDelete(modelName, context);
185
+ }
186
+ }
187
+ // -------------------------------------------------------------------------
188
+ // SqlDb contract — findRecord
189
+ // -------------------------------------------------------------------------
190
+ async findRecord(modelName, id) {
191
+ const schemas = this.deps.introspectModels();
192
+ const schema = schemas[modelName];
193
+ if (!schema)
194
+ return undefined;
195
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
196
+ const { GetCommand } = await this.deps.loadDocClientCommands();
197
+ const params = this.deps.buildGetItem(tableName, { id });
198
+ try {
199
+ const result = await this.requireClient().send(new GetCommand(params));
200
+ if (!result.Item)
201
+ return undefined;
202
+ const rawData = this._itemToRawData(result.Item, schema);
203
+ const record = this.deps.createRecord(modelName, rawData, {
204
+ isDbRecord: true, serialize: false, transform: false,
205
+ });
206
+ this._evictIfNotMemory(modelName, record);
207
+ return record;
208
+ }
209
+ catch (err) {
210
+ if (err.name === 'ResourceNotFoundException')
211
+ return undefined;
212
+ throw err;
213
+ }
214
+ }
215
+ // -------------------------------------------------------------------------
216
+ // SqlDb contract — findAll
217
+ // -------------------------------------------------------------------------
218
+ async findAll(modelName, conditions) {
219
+ const schemas = this.deps.introspectModels();
220
+ const schema = schemas[modelName];
221
+ if (!schema)
222
+ return [];
223
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
224
+ try {
225
+ let items;
226
+ if (!conditions || Object.keys(conditions).length === 0) {
227
+ items = await this._paginatedScan(tableName);
228
+ }
229
+ else {
230
+ // Try to route through a GSI if one matches a condition key
231
+ const gsiMatch = this._findGsiMatch(modelName, conditions);
232
+ if (gsiMatch) {
233
+ const { indexName, keyConditions, remainingConditions } = gsiMatch;
234
+ items = await this._paginatedQuery(tableName, indexName, keyConditions);
235
+ // Filter remaining non-key conditions in memory
236
+ if (Object.keys(remainingConditions).length > 0) {
237
+ items = items.filter(item => Object.entries(remainingConditions).every(([k, v]) => item[k] === v));
238
+ }
239
+ }
240
+ else {
241
+ // No GSI — fall back to Scan + FilterExpression (warn)
242
+ this.deps.log.warn?.(`[DynamoDB] findAll('${modelName}') using Scan+FilterExpression — no GSI for conditions: ${JSON.stringify(conditions)}. Add a GSI index for better performance.`);
243
+ items = await this._paginatedScan(tableName, conditions);
244
+ }
245
+ }
246
+ const records = items.map(item => {
247
+ const rawData = this._itemToRawData(item, schema);
248
+ return this.deps.createRecord(modelName, rawData, {
249
+ isDbRecord: true, serialize: false, transform: false,
250
+ });
251
+ });
252
+ for (const record of records) {
253
+ this._evictIfNotMemory(modelName, record);
254
+ }
255
+ return records;
256
+ }
257
+ catch (err) {
258
+ if (err.name === 'ResourceNotFoundException')
259
+ return [];
260
+ throw err;
261
+ }
262
+ }
263
+ // -------------------------------------------------------------------------
264
+ // loadMemoryRecords
265
+ // -------------------------------------------------------------------------
266
+ async loadMemoryRecords() {
267
+ const schemas = this.deps.introspectModels();
268
+ const order = this.deps.getTopologicalOrder(schemas);
269
+ const OrmModule = await this._getOrm();
270
+ const Orm = OrmModule.default;
271
+ for (const modelName of order) {
272
+ const { modelClass } = Orm.instance.getRecordClasses(modelName);
273
+ if (modelClass?.memory === false) {
274
+ this.deps.log.db?.(`Skipping memory load for '${modelName}' (memory: false)`);
275
+ continue;
276
+ }
277
+ const schema = schemas[modelName];
278
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
279
+ try {
280
+ const items = await this._paginatedScan(tableName);
281
+ for (const item of items) {
282
+ const rawData = this._itemToRawData(item, schema);
283
+ this.deps.createRecord(modelName, rawData, {
284
+ isDbRecord: true, serialize: false, transform: false,
285
+ });
286
+ }
287
+ }
288
+ catch (err) {
289
+ if (err.name === 'ResourceNotFoundException') {
290
+ this.deps.log.db?.(`Table '${tableName}' does not exist yet. Skipping load for '${modelName}'.`);
291
+ continue;
292
+ }
293
+ throw err;
294
+ }
295
+ }
296
+ }
297
+ // -------------------------------------------------------------------------
298
+ // Private — persist helpers
299
+ // -------------------------------------------------------------------------
300
+ async _persistCreate(modelName, context, response) {
301
+ const schemas = this.deps.introspectModels();
302
+ const schema = schemas[modelName];
303
+ if (!schema)
304
+ return;
305
+ const recordId = response?.data?.id;
306
+ const storeRef = this.deps.store;
307
+ const record = recordId != null ? storeRef.get(modelName, recordId) : null;
308
+ if (!record)
309
+ return;
310
+ const isPendingId = context.rawData?.__pendingSqlId === true;
311
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
312
+ // For numeric-ID models with a pending ID, generate a ULID
313
+ let finalId = record.id;
314
+ if (isPendingId) {
315
+ finalId = generateUlid();
316
+ }
317
+ const item = this._recordToItem(record, schema, context.rawData);
318
+ item.id = finalId;
319
+ const { PutCommand } = await this.deps.loadDocClientCommands();
320
+ const params = this.deps.buildPutItem(tableName, item, 'attribute_not_exists(id)');
321
+ await this.requireClient().send(new PutCommand(params));
322
+ // Re-key the store record if we generated a new ID
323
+ if (isPendingId) {
324
+ const pendingId = record.id;
325
+ const modelStoreMap = this.deps.store.get(modelName);
326
+ modelStoreMap.delete(pendingId);
327
+ record.__data.id = finalId;
328
+ record.id = finalId;
329
+ modelStoreMap.set(finalId, record);
330
+ if (response?.data)
331
+ response.data.id = finalId;
332
+ delete record.__data.__pendingSqlId;
333
+ }
334
+ }
335
+ async _persistUpdate(modelName, context) {
336
+ const schemas = this.deps.introspectModels();
337
+ const schema = schemas[modelName];
338
+ if (!schema)
339
+ return;
340
+ const record = context.record;
341
+ if (!record)
342
+ return;
343
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
344
+ const id = record.id;
345
+ const oldState = context.oldState || {};
346
+ const currentData = record.__data;
347
+ // Build diff of changed columns
348
+ const changedData = {};
349
+ for (const col of Object.keys(schema.columns)) {
350
+ if (currentData[col] !== oldState[col]) {
351
+ changedData[col] = currentData[col] ?? null;
352
+ }
353
+ }
354
+ // FK changes
355
+ for (const fkCol of Object.keys(schema.foreignKeys)) {
356
+ const relName = fkCol.replace(/_id$/, '');
357
+ const relValue = record.__relationships[relName];
358
+ const currentFkValue = relValue && typeof relValue === 'object' && relValue !== null
359
+ ? relValue.id ?? null
360
+ : relValue ?? record.__data[relName] ?? null;
361
+ const oldFkValue = oldState[relName] ?? null;
362
+ if (currentFkValue !== oldFkValue)
363
+ changedData[fkCol] = currentFkValue;
364
+ }
365
+ if (Object.keys(changedData).length === 0)
366
+ return;
367
+ const { UpdateCommand } = await this.deps.loadDocClientCommands();
368
+ const params = this.deps.buildUpdateItem(tableName, { id }, changedData);
369
+ await this.requireClient().send(new UpdateCommand(params));
370
+ }
371
+ async _persistDelete(modelName, context) {
372
+ const schemas = this.deps.introspectModels();
373
+ const schema = schemas[modelName];
374
+ if (!schema)
375
+ return;
376
+ const id = context.recordId;
377
+ if (id == null)
378
+ return;
379
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
380
+ const { DeleteCommand } = await this.deps.loadDocClientCommands();
381
+ const params = this.deps.buildDeleteItem(tableName, { id });
382
+ await this.requireClient().send(new DeleteCommand(params));
383
+ }
384
+ // -------------------------------------------------------------------------
385
+ // Private — pagination helpers
386
+ // -------------------------------------------------------------------------
387
+ async _paginatedScan(tableName, conditions) {
388
+ const { ScanCommand } = await this.deps.loadDocClientCommands();
389
+ const items = [];
390
+ let lastKey;
391
+ do {
392
+ const params = this.deps.buildScan(tableName, conditions, lastKey);
393
+ const result = await this.requireClient().send(new ScanCommand(params));
394
+ if (result.Items)
395
+ items.push(...result.Items);
396
+ lastKey = result.LastEvaluatedKey;
397
+ } while (lastKey);
398
+ return items;
399
+ }
400
+ async _paginatedQuery(tableName, indexName, keyConditions) {
401
+ const { QueryCommand } = await this.deps.loadDocClientCommands();
402
+ const items = [];
403
+ let lastKey;
404
+ do {
405
+ const params = this.deps.buildQuery(tableName, indexName, keyConditions, lastKey);
406
+ const result = await this.requireClient().send(new QueryCommand(params));
407
+ if (result.Items)
408
+ items.push(...result.Items);
409
+ lastKey = result.LastEvaluatedKey;
410
+ } while (lastKey);
411
+ return items;
412
+ }
413
+ // -------------------------------------------------------------------------
414
+ // Private — GSI registry
415
+ // -------------------------------------------------------------------------
416
+ /**
417
+ * Build the GSI registry from model introspection.
418
+ * Registry: modelName → attrName → gsiName
419
+ *
420
+ * FK columns (belonging to belongsTo relationships) get a GSI automatically.
421
+ */
422
+ _buildGsiRegistry() {
423
+ const schemas = this.deps.introspectModels();
424
+ for (const [modelName, schema] of Object.entries(schemas)) {
425
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
426
+ const modelGsis = new Map();
427
+ for (const fkCol of Object.keys(schema.foreignKeys)) {
428
+ const gsiName = `${tableName}-${fkCol}-index`;
429
+ modelGsis.set(fkCol, gsiName);
430
+ }
431
+ this._gsiRegistry.set(modelName, modelGsis);
432
+ }
433
+ }
434
+ /**
435
+ * Find a GSI that can serve the given conditions.
436
+ */
437
+ _findGsiMatch(modelName, conditions) {
438
+ const modelGsis = this._gsiRegistry.get(modelName);
439
+ if (!modelGsis)
440
+ return null;
441
+ for (const [attrName, indexName] of modelGsis) {
442
+ if (conditions[attrName] !== undefined) {
443
+ const keyConditions = { [attrName]: conditions[attrName] };
444
+ const remainingConditions = {};
445
+ for (const [k, v] of Object.entries(conditions)) {
446
+ if (k !== attrName)
447
+ remainingConditions[k] = v;
448
+ }
449
+ return { indexName, keyConditions, remainingConditions };
450
+ }
451
+ }
452
+ return null;
453
+ }
454
+ // -------------------------------------------------------------------------
455
+ // Private — table provisioning helpers
456
+ // -------------------------------------------------------------------------
457
+ _buildAttributeDefinitions(schema) {
458
+ const defs = [
459
+ { AttributeName: 'id', AttributeType: this.deps.getDynamoKeyType(schema.idType) },
460
+ ];
461
+ for (const fkCol of Object.keys(schema.foreignKeys)) {
462
+ defs.push({ AttributeName: fkCol, AttributeType: 'S' });
463
+ }
464
+ // Deduplicate
465
+ const seen = new Set();
466
+ return defs.filter(d => {
467
+ if (seen.has(d.AttributeName))
468
+ return false;
469
+ seen.add(d.AttributeName);
470
+ return true;
471
+ });
472
+ }
473
+ _buildGsiDefinitions(modelName, schema) {
474
+ const tableName = sanitizeTableName(this.deps.getPluralName(modelName));
475
+ const gsis = [];
476
+ for (const fkCol of Object.keys(schema.foreignKeys)) {
477
+ const gsiName = `${tableName}-${fkCol}-index`;
478
+ gsis.push({
479
+ IndexName: gsiName,
480
+ KeySchema: [{ AttributeName: fkCol, KeyType: 'HASH' }],
481
+ Projection: { ProjectionType: 'ALL' },
482
+ });
483
+ }
484
+ return gsis;
485
+ }
486
+ async _waitForTableActive(rawClient, tableName, DescribeTableCommand) {
487
+ const MAX_POLLS = 60;
488
+ const POLL_INTERVAL = 2000;
489
+ for (let i = 0; i < MAX_POLLS; i++) {
490
+ const desc = await rawClient.send(new DescribeTableCommand({ TableName: tableName }));
491
+ if (desc.Table?.TableStatus === 'ACTIVE')
492
+ return;
493
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
494
+ }
495
+ throw new Error(`DynamoDB table '${tableName}' did not become ACTIVE within ${MAX_POLLS * POLL_INTERVAL / 1000}s`);
496
+ }
497
+ // -------------------------------------------------------------------------
498
+ // Private — data conversion
499
+ // -------------------------------------------------------------------------
500
+ _itemToRawData(item, schema) {
501
+ const rawData = { ...item };
502
+ // Map FK columns back to relationship keys (matching PostgresDB pattern)
503
+ for (const [fkCol] of Object.entries(schema.foreignKeys)) {
504
+ const relName = fkCol.replace(/_id$/, '');
505
+ if (rawData[fkCol] !== undefined) {
506
+ rawData[relName] = rawData[fkCol];
507
+ delete rawData[fkCol];
508
+ }
509
+ }
510
+ return rawData;
511
+ }
512
+ _recordToItem(record, schema, rawData) {
513
+ const item = {};
514
+ const data = record.__data;
515
+ if (data.id !== undefined)
516
+ item.id = data.id;
517
+ for (const col of Object.keys(schema.columns)) {
518
+ if (data[col] !== undefined)
519
+ item[col] = data[col];
520
+ }
521
+ for (const fkCol of Object.keys(schema.foreignKeys)) {
522
+ const relName = fkCol.replace(/_id$/, '');
523
+ const related = record.__relationships[relName];
524
+ if (related && typeof related === 'object' && related !== null) {
525
+ item[fkCol] = related.id;
526
+ }
527
+ else if (related != null) {
528
+ item[fkCol] = related;
529
+ }
530
+ else if (data[relName] !== undefined) {
531
+ item[fkCol] = data[relName];
532
+ }
533
+ else if (rawData?.[relName] !== undefined) {
534
+ item[fkCol] = rawData[relName];
535
+ }
536
+ }
537
+ return item;
538
+ }
539
+ // -------------------------------------------------------------------------
540
+ // Private — store eviction
541
+ // -------------------------------------------------------------------------
542
+ _evictIfNotMemory(modelName, record) {
543
+ const storeRef = this.deps.store;
544
+ if (storeRef._memoryResolver && !storeRef._memoryResolver(modelName)) {
545
+ const modelStore = storeRef.get?.(modelName) ?? storeRef.data?.get(modelName);
546
+ if (modelStore)
547
+ modelStore.delete(record.id);
548
+ }
549
+ }
550
+ // -------------------------------------------------------------------------
551
+ // Deprecated alias
552
+ // -------------------------------------------------------------------------
553
+ async loadAllRecords() {
554
+ return this.loadMemoryRecords();
555
+ }
556
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * DynamoDB operation parameter builders.
3
+ *
4
+ * Each function returns a plain-object "params" bag that can be passed
5
+ * directly to the corresponding DocumentClient command
6
+ * (PutCommand, GetCommand, UpdateCommand, DeleteCommand, ScanCommand, QueryCommand).
7
+ *
8
+ * All functions are pure — no SDK imports here; the caller wraps params
9
+ * in the appropriate Command class.
10
+ */
11
+ export interface PutItemParams {
12
+ TableName: string;
13
+ Item: Record<string, unknown>;
14
+ ConditionExpression?: string;
15
+ }
16
+ export interface GetItemParams {
17
+ TableName: string;
18
+ Key: Record<string, unknown>;
19
+ }
20
+ export interface UpdateItemParams {
21
+ TableName: string;
22
+ Key: Record<string, unknown>;
23
+ UpdateExpression: string;
24
+ ExpressionAttributeNames: Record<string, string>;
25
+ ExpressionAttributeValues: Record<string, unknown>;
26
+ ReturnValues: string;
27
+ }
28
+ export interface DeleteItemParams {
29
+ TableName: string;
30
+ Key: Record<string, unknown>;
31
+ }
32
+ export interface ScanParams {
33
+ TableName: string;
34
+ FilterExpression?: string;
35
+ ExpressionAttributeNames?: Record<string, string>;
36
+ ExpressionAttributeValues?: Record<string, unknown>;
37
+ ExclusiveStartKey?: Record<string, unknown>;
38
+ }
39
+ export interface QueryParams {
40
+ TableName: string;
41
+ IndexName: string;
42
+ KeyConditionExpression: string;
43
+ ExpressionAttributeNames: Record<string, string>;
44
+ ExpressionAttributeValues: Record<string, unknown>;
45
+ ExclusiveStartKey?: Record<string, unknown>;
46
+ }
47
+ /**
48
+ * PutItem — optionally with a condition expression.
49
+ *
50
+ * Pass conditionExpression = 'attribute_not_exists(id)' to enforce uniqueness.
51
+ */
52
+ export declare function buildPutItem(tableName: string, item: Record<string, unknown>, conditionExpression?: string): PutItemParams;
53
+ /**
54
+ * GetItem by primary key.
55
+ */
56
+ export declare function buildGetItem(tableName: string, key: Record<string, unknown>): GetItemParams;
57
+ /**
58
+ * UpdateItem with a SET expression built from the `updates` object.
59
+ * Only the supplied attributes are updated (diff-based call site).
60
+ */
61
+ export declare function buildUpdateItem(tableName: string, key: Record<string, unknown>, updates: Record<string, unknown>): UpdateItemParams;
62
+ /**
63
+ * DeleteItem by primary key.
64
+ */
65
+ export declare function buildDeleteItem(tableName: string, key: Record<string, unknown>): DeleteItemParams;
66
+ /**
67
+ * ScanCommand params.
68
+ * If conditions are supplied they are rendered as a FilterExpression using AND.
69
+ */
70
+ export declare function buildScan(tableName: string, conditions?: Record<string, unknown>, exclusiveStartKey?: Record<string, unknown>): ScanParams;
71
+ /**
72
+ * QueryCommand params for a GSI.
73
+ * keyConditions must be in the form { attrName: value } and will be rendered
74
+ * as equality expressions joined by AND.
75
+ */
76
+ export declare function buildQuery(tableName: string, indexName: string, keyConditions: Record<string, unknown>, exclusiveStartKey?: Record<string, unknown>): QueryParams;