dinah 0.10.0 → 0.11.1
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/README.md +167 -47
- package/dist/index.cjs +44 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -77
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +102 -77
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +44 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,16 +99,29 @@ const UserTable = new Table(
|
|
|
99
99
|
|
|
100
100
|
### Using the Repository Class
|
|
101
101
|
|
|
102
|
-
`
|
|
102
|
+
`Repo` is the recommended way to interact with DynamoDB. For a plain repo with no configuration, use `db.createRepo`:
|
|
103
103
|
|
|
104
104
|
```typescript
|
|
105
|
-
const userRepo = db.
|
|
105
|
+
const userRepo = db.makeRepo(UserTable);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For repos with defaults, transforms, or attribute rules, use `makeRepo` to define a class (see [Repository Configuration](#repository-configuration)):
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { makeRepo } from "dinah";
|
|
112
|
+
|
|
113
|
+
class UserRepo extends makeRepo(UserTable, {
|
|
114
|
+
defaultPutData: () => ({ createdAt: Date.now() }),
|
|
115
|
+
defaultUpdateData: () => ({ updatedAt: Date.now() }),
|
|
116
|
+
}) {}
|
|
117
|
+
|
|
118
|
+
const userRepo = new UserRepo(db);
|
|
106
119
|
```
|
|
107
120
|
|
|
108
121
|
#### CRUD
|
|
109
122
|
|
|
110
123
|
```typescript
|
|
111
|
-
// Create (conditional put
|
|
124
|
+
// Create (conditional put — fails if item already exists)
|
|
112
125
|
const user = await userRepo.create({
|
|
113
126
|
userId: "u1",
|
|
114
127
|
email: "alice@example.com",
|
|
@@ -126,14 +139,47 @@ const partial = await userRepo.get({ userId: "u1" }, { projection: ["name", "ema
|
|
|
126
139
|
// getOrThrow (throws if item not found)
|
|
127
140
|
const aliceOrThrow = await userRepo.getOrThrow({ userId: "u1" });
|
|
128
141
|
|
|
129
|
-
//
|
|
142
|
+
// Put (upsert)
|
|
143
|
+
await userRepo.put({
|
|
144
|
+
userId: "u1",
|
|
145
|
+
email: "alice@example.com",
|
|
146
|
+
name: "Alice",
|
|
147
|
+
role: "admin",
|
|
148
|
+
createdAt: Date.now(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Update (throws if item does not exist)
|
|
130
152
|
const updated = await userRepo.update(
|
|
131
153
|
{ userId: "u1" },
|
|
132
154
|
{ name: "Alice Smith", updatedAt: Date.now() },
|
|
133
155
|
);
|
|
134
156
|
|
|
135
|
-
// Delete
|
|
136
|
-
await userRepo.delete({ userId: "u1" });
|
|
157
|
+
// Delete (returns old item or undefined)
|
|
158
|
+
const deleted = await userRepo.delete({ userId: "u1" });
|
|
159
|
+
|
|
160
|
+
// deleteOrThrow (throws if item not found)
|
|
161
|
+
const item = await userRepo.deleteOrThrow({ userId: "u1" });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### Update Expressions
|
|
165
|
+
|
|
166
|
+
The update argument supports MongoDB-style operators:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
await userRepo.update(
|
|
170
|
+
{ userId: "u1" },
|
|
171
|
+
{
|
|
172
|
+
name: "Alice", // set
|
|
173
|
+
age: undefined, // remove
|
|
174
|
+
score: { $plus: 10 }, // increment
|
|
175
|
+
score: { $minus: 5 }, // decrement
|
|
176
|
+
score: { $ifNotExists: 0 }, // set only if missing
|
|
177
|
+
tags: { $append: "vip" }, // list_append to end
|
|
178
|
+
tags: { $prepend: "featured" }, // list_append to front
|
|
179
|
+
followers: { $setAdd: "user-99" }, // ADD to DynamoDB set
|
|
180
|
+
followers: { $setDel: "user-99" }, // DELETE from DynamoDB set
|
|
181
|
+
},
|
|
182
|
+
);
|
|
137
183
|
```
|
|
138
184
|
|
|
139
185
|
#### Querying
|
|
@@ -142,7 +188,10 @@ Queries use a MongoDB-like syntax with operators like `$gt`, `$between`, `$prefi
|
|
|
142
188
|
|
|
143
189
|
```typescript
|
|
144
190
|
// Query by partition key
|
|
145
|
-
const
|
|
191
|
+
const posts = await postRepo.query({ authorId: "u1" });
|
|
192
|
+
|
|
193
|
+
// Query with sort key condition
|
|
194
|
+
const recent = await postRepo.query({ authorId: "u1" }, { postId: { $gte: "2024-" } });
|
|
146
195
|
|
|
147
196
|
// Query a GSI
|
|
148
197
|
const adminsByDate = await userRepo.queryGsi("byRole", {
|
|
@@ -150,82 +199,153 @@ const adminsByDate = await userRepo.queryGsi("byRole", {
|
|
|
150
199
|
createdAt: { $gt: 1700000000000 },
|
|
151
200
|
});
|
|
152
201
|
|
|
202
|
+
// Paginated query (async generator, one page at a time)
|
|
203
|
+
for await (const page of postRepo.queryPaged({ authorId: "u1" }, { limit: 20 })) {
|
|
204
|
+
console.log(page);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Paginated GSI query
|
|
208
|
+
for await (const page of userRepo.queryGsiPaged("byRole", { role: "admin" })) {
|
|
209
|
+
console.log(page);
|
|
210
|
+
}
|
|
211
|
+
|
|
153
212
|
// Scan with filters
|
|
154
213
|
const recentUsers = await userRepo.scan({
|
|
155
214
|
filter: { createdAt: { $gte: 1700000000000 } },
|
|
156
215
|
});
|
|
157
216
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
217
|
+
// Scan a GSI
|
|
218
|
+
const allByStatus = await postRepo.scanGsi("byStatus");
|
|
219
|
+
|
|
220
|
+
// Check existence (uses query or scan, no data returned)
|
|
221
|
+
const hasAdmins = await userRepo.existsGsi("byRole", { query: { role: "admin" } });
|
|
222
|
+
const exists = await postRepo.exists({ query: { authorId: "u1" } });
|
|
162
223
|
```
|
|
163
224
|
|
|
225
|
+
Filter operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`, `$in`, `$nin`, `$prefix`, `$includes`, `$exists`, `$size`, `$type`.
|
|
226
|
+
|
|
164
227
|
#### Batch Operations
|
|
165
228
|
|
|
166
229
|
```typescript
|
|
167
230
|
// Batch get
|
|
168
|
-
const { items, unprocessed } = await userRepo.batchGet([
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
]);
|
|
231
|
+
const { items, unprocessed } = await userRepo.batchGet([{ userId: "u1" }, { userId: "u2" }]);
|
|
232
|
+
|
|
233
|
+
// Batch get (throws on missing or unprocessed)
|
|
234
|
+
const items = await userRepo.batchGetOrThrow([{ userId: "u1" }, { userId: "u2" }]);
|
|
173
235
|
|
|
174
|
-
// Batch write (puts and deletes)
|
|
236
|
+
// Batch write (puts and deletes mixed)
|
|
175
237
|
await userRepo.batchWrite([
|
|
238
|
+
{
|
|
239
|
+
type: "PUT",
|
|
240
|
+
item: { userId: "u3", email: "c@d.com", name: "Carol", role: "user", createdAt: Date.now() },
|
|
241
|
+
},
|
|
242
|
+
{ type: "DELETE", key: { userId: "u2" } },
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
// Batch update (same update applied to multiple keys via PartiQL)
|
|
246
|
+
await userRepo.batchUpdate([{ userId: "u1" }, { userId: "u3" }], { role: "admin" });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Transactions
|
|
250
|
+
|
|
251
|
+
`trxWrite` accepts plain request objects and executes them as a single DynamoDB transaction:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
await userRepo.trxWrite(
|
|
176
255
|
{
|
|
177
256
|
type: "PUT",
|
|
178
257
|
item: {
|
|
179
|
-
userId: "
|
|
180
|
-
email: "
|
|
181
|
-
name: "
|
|
258
|
+
userId: "u5",
|
|
259
|
+
email: "eve@example.com",
|
|
260
|
+
name: "Eve",
|
|
182
261
|
role: "user",
|
|
183
262
|
createdAt: Date.now(),
|
|
184
263
|
},
|
|
185
264
|
},
|
|
186
|
-
{ type: "
|
|
187
|
-
|
|
265
|
+
{ type: "UPDATE", key: { userId: "u1" }, update: { role: "superadmin" } },
|
|
266
|
+
{ type: "DELETE", key: { userId: "u2" } },
|
|
267
|
+
{ type: "CONDITION", key: { userId: "u3" }, condition: { role: "admin" } },
|
|
268
|
+
);
|
|
188
269
|
```
|
|
189
270
|
|
|
190
|
-
|
|
271
|
+
Convenience methods operate on multiple keys/items atomically:
|
|
191
272
|
|
|
192
273
|
```typescript
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
274
|
+
// Transactional get
|
|
275
|
+
const [u1, u2] = await userRepo.trxGet([{ userId: "u1" }, { userId: "u2" }]);
|
|
276
|
+
const items = await userRepo.trxGetOrThrow([{ userId: "u1" }, { userId: "u2" }]);
|
|
277
|
+
|
|
278
|
+
// Transactional writes
|
|
279
|
+
await userRepo.trxPut([item1, item2]);
|
|
280
|
+
await userRepo.trxUpdate([{ userId: "u1" }, { userId: "u2" }], { role: "admin" });
|
|
281
|
+
await userRepo.trxDelete([{ userId: "u1" }, { userId: "u2" }]);
|
|
282
|
+
await userRepo.trxCreate([item1, item2]); // fails if any item already exists
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
To build cross-repo transactions, use the `*Request` methods to produce request objects and pass them to `db.trxWrite`:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
await db.trxWrite(
|
|
289
|
+
userRepo.trxPutRequest(userItem),
|
|
290
|
+
postRepo.trxDeleteRequest({ authorId: "u1", postId: "p1" }),
|
|
203
291
|
);
|
|
204
292
|
```
|
|
205
293
|
|
|
206
|
-
|
|
294
|
+
## Repository Configuration
|
|
207
295
|
|
|
208
|
-
|
|
296
|
+
`makeRepo` accepts a config object that controls defaults, transforms, and attribute rules. Extend the result to create a named repo class:
|
|
209
297
|
|
|
210
298
|
```typescript
|
|
211
|
-
|
|
299
|
+
class UserRepo extends makeRepo(UserTable, {
|
|
300
|
+
defaultPutData: () => ({ createdAt: Date.now() }),
|
|
301
|
+
defaultUpdateData: () => ({ updatedAt: Date.now() }),
|
|
302
|
+
}) {}
|
|
303
|
+
```
|
|
212
304
|
|
|
213
|
-
|
|
214
|
-
readonly table = UserTable;
|
|
305
|
+
`defaultPutData` is merged under every `put` / `create` / `batchWrite` put / `trxPut` / `trxCreate`. `defaultUpdateData` is merged under every `update` / `batchUpdate` / `trxUpdate`. Caller-provided values always win.
|
|
215
306
|
|
|
216
|
-
|
|
217
|
-
return { createdAt: Date.now() };
|
|
218
|
-
}
|
|
307
|
+
### transformInput / transformOutput
|
|
219
308
|
|
|
220
|
-
|
|
221
|
-
return { updatedAt: Date.now() };
|
|
222
|
-
}
|
|
223
|
-
}
|
|
309
|
+
`transformInput` runs on every write (after defaults are merged) and receives a partial of the schema. `transformOutput` runs on every read and maps the stored shape to your desired return type:
|
|
224
310
|
|
|
225
|
-
|
|
311
|
+
```typescript
|
|
312
|
+
class UserRepo extends makeRepo(UserTable, {
|
|
313
|
+
transformInput: (item) => ({
|
|
314
|
+
...item,
|
|
315
|
+
email: item.email?.toLowerCase(),
|
|
316
|
+
}),
|
|
317
|
+
transformOutput: (item): UserWithDisplayName => ({
|
|
318
|
+
...item,
|
|
319
|
+
displayName: `${item.name} <${item.email}>`,
|
|
320
|
+
}),
|
|
321
|
+
}) {}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Transforms are skipped when a `projection` option is provided, since only a subset of fields is available.
|
|
325
|
+
|
|
326
|
+
### derivedAttributes / immutableAttributes
|
|
327
|
+
|
|
328
|
+
`derivedAttributes` lists fields that are computed by `transformInput` and should never be written directly by the caller. They are stripped from put and update inputs:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
class UserRepo extends makeRepo(UserTable, {
|
|
332
|
+
transformInput: (item) => ({
|
|
333
|
+
...item,
|
|
334
|
+
emailDomain: item.email ? item.email.split("@")[1] : undefined,
|
|
335
|
+
}),
|
|
336
|
+
derivedAttributes: ["emailDomain"],
|
|
337
|
+
}) {}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`immutableAttributes` lists fields that may be set on create but must not be changed by updates:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
class UserRepo extends makeRepo(UserTable, {
|
|
344
|
+
immutableAttributes: ["createdAt"],
|
|
345
|
+
}) {}
|
|
226
346
|
```
|
|
227
347
|
|
|
228
|
-
|
|
348
|
+
Both arrays are inferred as literal types — no `as const` needed.
|
|
229
349
|
|
|
230
350
|
## License
|
|
231
351
|
|
package/dist/index.cjs
CHANGED
|
@@ -216,24 +216,26 @@ var ExpressionBuilder = class {
|
|
|
216
216
|
var Repo = class {
|
|
217
217
|
table;
|
|
218
218
|
db;
|
|
219
|
-
|
|
219
|
+
config;
|
|
220
|
+
constructor(db, table, config) {
|
|
220
221
|
this.db = db;
|
|
221
222
|
this.table = table;
|
|
223
|
+
this.config = config ?? {};
|
|
222
224
|
}
|
|
223
225
|
get tableName() {
|
|
224
226
|
return `${this.db.config?.tableNamePrefix ?? ""}${this.table.def.name}`;
|
|
225
227
|
}
|
|
226
228
|
get defaultPutData() {
|
|
227
|
-
return {};
|
|
229
|
+
return this.config.defaultPutData?.() ?? {};
|
|
228
230
|
}
|
|
229
231
|
get defaultUpdateData() {
|
|
230
|
-
return {};
|
|
232
|
+
return this.config.defaultUpdateData?.() ?? {};
|
|
231
233
|
}
|
|
232
234
|
transformOutput(item) {
|
|
233
|
-
return item;
|
|
235
|
+
return this.config.transformOutput?.(item) ?? item;
|
|
234
236
|
}
|
|
235
237
|
transformInput(item) {
|
|
236
|
-
return item;
|
|
238
|
+
return this.config.transformInput?.(item) ?? item;
|
|
237
239
|
}
|
|
238
240
|
extractKey(item) {
|
|
239
241
|
const { partitionKey, sortKey } = this.table.def;
|
|
@@ -260,10 +262,7 @@ var Repo = class {
|
|
|
260
262
|
return this.applyTransformIfNeeded(item, options);
|
|
261
263
|
}
|
|
262
264
|
async put(item, options) {
|
|
263
|
-
const itemWithDefaults = this.
|
|
264
|
-
...this.defaultPutData,
|
|
265
|
-
...item
|
|
266
|
-
});
|
|
265
|
+
const itemWithDefaults = this.applyPutTransforms(item);
|
|
267
266
|
const result = await this.db.put({
|
|
268
267
|
table: this.tableName,
|
|
269
268
|
item: itemWithDefaults,
|
|
@@ -422,10 +421,7 @@ var Repo = class {
|
|
|
422
421
|
};
|
|
423
422
|
else return {
|
|
424
423
|
type: "PUT",
|
|
425
|
-
item: this.
|
|
426
|
-
...this.defaultPutData,
|
|
427
|
-
...request.item
|
|
428
|
-
})
|
|
424
|
+
item: this.applyPutTransforms(request.item)
|
|
429
425
|
};
|
|
430
426
|
}) });
|
|
431
427
|
return {
|
|
@@ -518,10 +514,7 @@ var Repo = class {
|
|
|
518
514
|
};
|
|
519
515
|
}
|
|
520
516
|
trxPutRequest(item, options) {
|
|
521
|
-
const itemWithDefaults = this.
|
|
522
|
-
...this.defaultPutData,
|
|
523
|
-
...item
|
|
524
|
-
});
|
|
517
|
+
const itemWithDefaults = this.applyPutTransforms(item);
|
|
525
518
|
return {
|
|
526
519
|
table: this.tableName,
|
|
527
520
|
type: "PUT",
|
|
@@ -546,10 +539,7 @@ var Repo = class {
|
|
|
546
539
|
const { condition: otherCondition, ...otherOptions } = options ?? {};
|
|
547
540
|
const condition = { $and: [{ [this.table.def.partitionKey]: { $exists: false } }] };
|
|
548
541
|
if (otherCondition) condition.$and.push(otherCondition);
|
|
549
|
-
const itemWithDefaults = this.
|
|
550
|
-
...this.defaultPutData,
|
|
551
|
-
...item
|
|
552
|
-
});
|
|
542
|
+
const itemWithDefaults = this.applyPutTransforms(item);
|
|
553
543
|
return {
|
|
554
544
|
table: this.tableName,
|
|
555
545
|
type: "PUT",
|
|
@@ -558,28 +548,39 @@ var Repo = class {
|
|
|
558
548
|
...otherOptions
|
|
559
549
|
};
|
|
560
550
|
}
|
|
551
|
+
applyPutTransforms(item) {
|
|
552
|
+
const result = { ...this.transformInput({
|
|
553
|
+
...this.defaultPutData,
|
|
554
|
+
...item
|
|
555
|
+
}) };
|
|
556
|
+
for (const key of this.config.derivedAttributes ?? []) delete result[key];
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
561
559
|
applyNormalizersToExpression(expression) {
|
|
562
560
|
const partial = {};
|
|
563
|
-
for (const [key, val] of Object.entries(expression))
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
else if (val.$ifNotExists !== void 0) partial[key] = Array.isArray(val.$ifNotExists) ? val.$ifNotExists[1] : val.$ifNotExists;
|
|
568
|
-
}
|
|
561
|
+
for (const [key, val] of Object.entries(expression)) if (val === void 0 || isOperation(val) && val.$remove === true) partial[key] = void 0;
|
|
562
|
+
else if (!isOperation(val)) partial[key] = val;
|
|
563
|
+
else if (val.$set !== void 0) partial[key] = val.$set;
|
|
564
|
+
else if (val.$ifNotExists !== void 0) partial[key] = Array.isArray(val.$ifNotExists) ? val.$ifNotExists[1] : val.$ifNotExists;
|
|
569
565
|
const normalized = this.transformInput(partial);
|
|
570
566
|
const result = { ...expression };
|
|
571
|
-
for (const [key, normalizedVal] of Object.entries(normalized))
|
|
572
|
-
|
|
573
|
-
if (!
|
|
574
|
-
else
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
567
|
+
for (const [key, normalizedVal] of Object.entries(normalized)) {
|
|
568
|
+
if (normalizedVal === void 0) continue;
|
|
569
|
+
if (key in partial && partial[key] === void 0 || !(key in partial)) result[key] = normalizedVal;
|
|
570
|
+
else {
|
|
571
|
+
const original = expression[key];
|
|
572
|
+
if (!isOperation(original)) result[key] = normalizedVal;
|
|
573
|
+
else if (original.$set !== void 0) result[key] = {
|
|
574
|
+
...original,
|
|
575
|
+
$set: normalizedVal
|
|
576
|
+
};
|
|
577
|
+
else if (original.$ifNotExists !== void 0) result[key] = {
|
|
578
|
+
...original,
|
|
579
|
+
$ifNotExists: Array.isArray(original.$ifNotExists) ? [original.$ifNotExists[0], normalizedVal] : normalizedVal
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
for (const key of [...this.config.derivedAttributes ?? [], ...this.config.immutableAttributes ?? []]) delete result[key];
|
|
583
584
|
return result;
|
|
584
585
|
}
|
|
585
586
|
applyTransformsIfNeeded(items, options) {
|
|
@@ -595,11 +596,11 @@ var Repo = class {
|
|
|
595
596
|
return transformedItem;
|
|
596
597
|
}
|
|
597
598
|
};
|
|
598
|
-
const makeRepo = (table) => {
|
|
599
|
+
const makeRepo = (table, config) => {
|
|
599
600
|
return class extends Repo {
|
|
600
601
|
static table = table;
|
|
601
602
|
constructor(db) {
|
|
602
|
-
super(db, table);
|
|
603
|
+
super(db, table, config);
|
|
603
604
|
}
|
|
604
605
|
};
|
|
605
606
|
};
|
|
@@ -619,8 +620,8 @@ var Db = class {
|
|
|
619
620
|
}
|
|
620
621
|
this.config = dbConfig;
|
|
621
622
|
}
|
|
622
|
-
|
|
623
|
-
return new Repo(this, table);
|
|
623
|
+
makeRepo(table, config) {
|
|
624
|
+
return new Repo(this, table, config);
|
|
624
625
|
}
|
|
625
626
|
async createTable(table) {
|
|
626
627
|
await this.client.send(new _aws_sdk_client_dynamodb.CreateTableCommand(require_util.extractTableDesc(table)));
|