dinah 0.11.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 CHANGED
@@ -99,16 +99,29 @@ const UserTable = new Table(
99
99
 
100
100
  ### Using the Repository Class
101
101
 
102
- `Repository` is the recommended way to interact with DynamoDB. Created from a `Table`, it provides full type inference for keys, items, queries, and projections:
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.createRepo(UserTable);
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 - fails if item already exists)
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
- // Update
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 admins = await userRepo.query({ userId: "u1" });
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
- // Paginated query with async iteration
159
- for await (const page of userRepo.queryGsiPaged("byRole", { role: "admin" })) {
160
- console.log(page); // each page is an array of items
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
- { userId: "u1" },
170
- { userId: "u2" },
171
- { userId: "u3" },
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: "u4",
180
- email: "bob@example.com",
181
- name: "Bob",
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: "DELETE", key: { userId: "u3" } },
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
- #### Transactions
271
+ Convenience methods operate on multiple keys/items atomically:
191
272
 
192
273
  ```typescript
193
- await userRepo.trxWrite(
194
- userRepo.trxPut({
195
- userId: "u5",
196
- email: "eve@example.com",
197
- name: "Eve",
198
- role: "user",
199
- createdAt: Date.now(),
200
- }),
201
- userRepo.trxUpdate({ userId: "u1" }, { role: "superadmin" }),
202
- userRepo.trxDelete({ userId: "u2" }),
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
- ### Default Values
294
+ ## Repository Configuration
207
295
 
208
- To attach defaults like timestamps to every write, subclass `AbstractRepo` and override `defaultPutData` and/or `defaultUpdateData`:
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
- import { AbstractRepo } from "dinah";
299
+ class UserRepo extends makeRepo(UserTable, {
300
+ defaultPutData: () => ({ createdAt: Date.now() }),
301
+ defaultUpdateData: () => ({ updatedAt: Date.now() }),
302
+ }) {}
303
+ ```
212
304
 
213
- class UserRepo extends AbstractRepo<typeof UserTable> {
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
- override get defaultPutData() {
217
- return { createdAt: Date.now() };
218
- }
307
+ ### transformInput / transformOutput
219
308
 
220
- override get defaultUpdateData() {
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
- const userRepo = new UserRepo(db);
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
- `defaultPutData` is merged under every `put` / `create` / `batchWrite` put / `trxPut` / `trxCreate`. `defaultUpdateData` is merged under every `update` / `trxUpdate`. Caller-provided values always win on conflict.
348
+ Both arrays are inferred as literal types no `as const` needed.
229
349
 
230
350
  ## License
231
351
 
package/dist/index.cjs CHANGED
@@ -620,8 +620,8 @@ var Db = class {
620
620
  }
621
621
  this.config = dbConfig;
622
622
  }
623
- createRepo(table) {
624
- return new Repo(this, table);
623
+ makeRepo(table, config) {
624
+ return new Repo(this, table, config);
625
625
  }
626
626
  async createTable(table) {
627
627
  await this.client.send(new _aws_sdk_client_dynamodb.CreateTableCommand(require_util.extractTableDesc(table)));