dynoquery 0.1.19 → 0.1.22

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
@@ -188,16 +188,16 @@ DynoQuery provides `batchWrite` and `batchRead` for processing multiple items ac
188
188
 
189
189
  ```typescript
190
190
  // 1. Batch Write (Create/Replace multiple items)
191
- const user1 = db.User('john', 'METADATA');
191
+ const user1 = db.User('john@example.com', 'METADATA');
192
192
  user1.name = 'John Doe';
193
193
 
194
- const user2 = db.User('jane', 'METADATA');
194
+ const user2 = db.User('jane@example.com', 'METADATA');
195
195
  user2.name = 'Jane Doe';
196
196
 
197
197
  await db.batchWrite([user1, user2]);
198
198
 
199
199
  // 2. Batch Read (Fetch multiple items or index queries)
200
- const userDraft = db.User('john', 'METADATA');
200
+ const userDraft = db.User('john@example.com', 'METADATA');
201
201
  const categoryQuery = db.findByCategory('ELECTRONICS', 'p123');
202
202
 
203
203
  const results = await db.batchRead([userDraft, categoryQuery]);
@@ -209,6 +209,37 @@ results.forEach(item => {
209
209
  item.save(); // Second-level methods are available
210
210
  }
211
211
  });
212
+
213
+ // 3. Batch Delete
214
+ const userToDelete = db.User('john@example.com').draftDelete('METADATA');
215
+ await db.batchWrite([userToDelete]);
216
+ ```
217
+
218
+ ### 5. Transaction Operations
219
+
220
+ DynoQuery supports `transactWrite` and `transactRead` for atomic operations across multiple items.
221
+
222
+ ```typescript
223
+ // 1. Transaction Write (All operations succeed or all fail)
224
+ const user1 = db.User('john@example.com', 'METADATA');
225
+ user1.name = 'John Doe';
226
+
227
+ const userToDelete = db.User('olduser@example.com').draftDelete('METADATA');
228
+
229
+ // You can even set conditions for items in a transaction
230
+ const criticalItem = db.User('admin@example.com', 'METADATA');
231
+ criticalItem.lastLogin = new Date().toISOString();
232
+ criticalItem.setCondition(attr('status').equals('ACTIVE'));
233
+
234
+ await db.transactWrite([user1, userToDelete, criticalItem]);
235
+
236
+ // 2. Transaction Read (Read multiple items atomically)
237
+ const userDraft = db.User('john@example.com', 'METADATA');
238
+ const items = await db.transactRead([userDraft]);
239
+
240
+ if (items[0]) {
241
+ console.log('Found user:', items[0].name);
242
+ }
212
243
  ```
213
244
 
214
245
  ## Optional Configuration Parameters
@@ -255,6 +286,55 @@ if (token) {
255
286
  }
256
287
  ```
257
288
 
289
+ ### Expression Builder (Filters & Conditions)
290
+
291
+ Use `ExpressionBuilder` to build complex filter and condition expressions in a type-safe way.
292
+
293
+ ```typescript
294
+ import { attr, ExpressionBuilder } from 'dynoquery';
295
+
296
+ // 1. Filtering in queries
297
+ const builder = attr('age').greaterThan(25).and(attr('status').equals('ACTIVE'));
298
+ const activeUsers = await db.User('some-id').getAll({ filterBuilder: builder });
299
+
300
+ // 2. Conditional Updates
301
+ const johnMeta = db.User('john@example.com', 'METADATA');
302
+ const condition = attr('version').equals(1);
303
+
304
+ johnMeta.setCondition(condition);
305
+ johnMeta.name = 'John New Name';
306
+ johnMeta.version = 2;
307
+
308
+ await johnMeta.save(); // Fails if version is not 1
309
+
310
+ // 3. Raw Condition Expressions
311
+ await db.User('john@example.com').update({ status: 'INACTIVE' }, [], {
312
+ ConditionExpression: '#v = :v',
313
+ ExpressionAttributeNames: { '#v': 'version' },
314
+ ExpressionAttributeValues: { ':v': 2 }
315
+ });
316
+
317
+ // Or using Item object
318
+ johnMeta.setCondition(attr('name').exists());
319
+ await johnMeta.save();
320
+
321
+ // 4. Supported Operators
322
+ // - attr('name').equals('val') / notEquals('val')
323
+ // - .lessThan(val) / lessThanOrEqual(val)
324
+ // - .greaterThan(val) / greaterThanOrEqual(val)
325
+ // - .between(start, end)
326
+ // - .in([val1, val2])
327
+ // - logical: .and(otherBuilder), .or(otherBuilder), ExpressionBuilder.not(builder)
328
+
329
+ // 4. Supported Functions
330
+ // - attr('field').exists()
331
+ // - attr('field').notExists()
332
+ // - attr('field').type('S')
333
+ // - attr('field').beginsWith('prefix')
334
+ // - attr('field').contains('value')
335
+ // - attr('field').size().greaterThan(5)
336
+ ```
337
+
258
338
  ## API Reference
259
339
 
260
340
  ### DynoQuery
@@ -266,29 +346,34 @@ if (token) {
266
346
  - `scan(params)`: Low-level ScanCommand wrapper.
267
347
  - `batchWrite(items)`: Batch persists multiple `Item` objects.
268
348
  - `batchRead(items)`: Batch fetches multiple `Item` or `IndexQuery` objects.
349
+ - `transactWrite(items)`: Performs atomic write operations (Put/Delete) for up to 100 items.
350
+ - `transactRead(items)`: Performs atomic read operations (Get) for up to 100 items.
269
351
  - `[ModelName](id, skValue?)`: Returns a `Partition` instance for the given ID. If `skValue` is provided, returns an `Item` object directly.
270
352
  - `findBy[IndexName](id, skValue?)`: Returns an `IndexQuery` instance.
271
353
 
272
354
  ### Partition
273
355
  - `get(skValue)`: Fetches data for a specific Sort Key value (returns a Promise).
274
- - `getAll(options?)`: Fetches items in the partition. Options: `{ limit, exclusiveStartKey }`.
275
- - `create(skValue, data, indices?)`: Creates an item. `indices` is an array of `IndexQuery` for GSI population.
276
- - `update(skValue, data)`: Partial update of an item.
277
- - `delete(skValue)`: Deletes an item.
356
+ - `getAll(options?)`: Fetches items in the partition. Options: `{ limit, exclusiveStartKey, filterBuilder, FilterExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
357
+ - `create(skValue, data, indices?, options?)`: Creates an item. `options`: `{ conditionBuilder, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
358
+ - `update(skValue, data, indices?, options?)`: Partial update of an item. `options`: `{ conditionBuilder, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
359
+ - `delete(skValue, options?)`: Deletes an item. `options`: `{ conditionBuilder, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
278
360
  - `draft(skValue, data?)`: Returns an `Item` object initialized with `data` (optional).
361
+ - `draftDelete(skValue)`: Returns an `Item` object marked for deletion (for use with `batchWrite`).
279
362
  - `deleteAll()`: Deletes all items in the partition.
280
363
  - `getLastEvaluatedKey()`: Returns the pagination token from the last `getAll()`.
281
364
 
282
365
  ### IndexQuery
283
366
  - `get(skValue?)`: Get a single item from the index.
284
- - `getAll(options?)`: Query index. Options: `{ limit, scanIndexForward, exclusiveStartKey, skValue }`.
367
+ - `getAll(options?)`: Query index. Options: `{ limit, scanIndexForward, exclusiveStartKey, skValue, filterBuilder, FilterExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
285
368
  - `getLastEvaluatedKey()`: Returns the pagination token from the last `getAll()`.
286
369
 
287
370
  ### Item (returned by Partition.get or draft)
288
- - `create(data?, indices?)`: Persists the item as a new record with the provided data. Supports GSI indices.
289
- - `update(data, indices?)`: Partial update of the item. Supports GSI indices.
290
- - `save()`: Persists the current state of the item (alias for update of all properties). Uses indices attached via `setIndex()`.
291
- - `setIndex(indices)`: Attaches one or more `IndexQuery` objects to the item for use with `save()` or `create()`.
371
+ - `create(data?, indices?)`: Persists the item as a new record with the provided data. Supports GSI indices and internal `conditionBuilder`.
372
+ - `update(data, indices?)`: Partial update of the item. Supports GSI indices and internal `conditionBuilder`.
373
+ - `save()`: Persists the current state of the item. Uses indices attached via `setIndex()` and internal `conditionBuilder`.
374
+ - `setIndex(indices)`: Attaches one or more `IndexQuery` objects to the item.
375
+ - `setFilter(builder)`: Sets a filter expression builder for the item.
376
+ - `setCondition(builder)`: Sets a condition for the item using an `ExpressionBuilder`.
292
377
 
293
378
  ## License
294
379
 
@@ -0,0 +1,41 @@
1
+ export type Operand = string | number | ExpressionBuilder;
2
+ export declare class ExpressionBuilder {
3
+ private parts;
4
+ private attributeNames;
5
+ private attributeValues;
6
+ private valueCounter;
7
+ private nameCounter;
8
+ constructor();
9
+ static field(name: string): ExpressionBuilder;
10
+ static value(val: any): ExpressionBuilder;
11
+ where(field: string, operator: string, value: any): ExpressionBuilder;
12
+ equals(value: any): ExpressionBuilder;
13
+ notEquals(value: any): ExpressionBuilder;
14
+ lessThan(value: any): ExpressionBuilder;
15
+ lessThanOrEqual(value: any): ExpressionBuilder;
16
+ greaterThan(value: any): ExpressionBuilder;
17
+ greaterThanOrEqual(value: any): ExpressionBuilder;
18
+ between(start: any, end: any): ExpressionBuilder;
19
+ in(values: any[]): ExpressionBuilder;
20
+ exists(): ExpressionBuilder;
21
+ notExists(): ExpressionBuilder;
22
+ type(type: string): ExpressionBuilder;
23
+ attributeExists(path: string): ExpressionBuilder;
24
+ attributeNotExists(path: string): ExpressionBuilder;
25
+ attributeType(path: string, type: string): ExpressionBuilder;
26
+ beginsWith(pathOrSubstr: string, substr?: string): ExpressionBuilder;
27
+ contains(pathOrValue: string, value?: any): ExpressionBuilder;
28
+ size(path?: string): ExpressionBuilder;
29
+ and(other: ExpressionBuilder): ExpressionBuilder;
30
+ or(other: ExpressionBuilder): ExpressionBuilder;
31
+ static not(condition: ExpressionBuilder): ExpressionBuilder;
32
+ build(attributeNames?: Record<string, string>, attributeValues?: Record<string, any>, counters?: {
33
+ name: number;
34
+ value: number;
35
+ }): {
36
+ expression: string;
37
+ attributeNames: Record<string, string>;
38
+ attributeValues: Record<string, any>;
39
+ };
40
+ }
41
+ export declare function attr(name: string): ExpressionBuilder;
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExpressionBuilder = void 0;
4
+ exports.attr = attr;
5
+ class ExpressionBuilder {
6
+ constructor() {
7
+ this.parts = [];
8
+ this.attributeNames = {};
9
+ this.attributeValues = {};
10
+ this.valueCounter = 0;
11
+ this.nameCounter = 0;
12
+ }
13
+ static field(name) {
14
+ const builder = new ExpressionBuilder();
15
+ builder.parts.push({ type: 'field', name });
16
+ return builder;
17
+ }
18
+ static value(val) {
19
+ const builder = new ExpressionBuilder();
20
+ builder.parts.push({ type: 'value', value: val });
21
+ return builder;
22
+ }
23
+ where(field, operator, value) {
24
+ this.parts.push({ type: 'condition', field, operator, value });
25
+ return this;
26
+ }
27
+ equals(value) {
28
+ this.parts.push({ type: 'operator', operator: '=', value });
29
+ return this;
30
+ }
31
+ notEquals(value) {
32
+ this.parts.push({ type: 'operator', operator: '<>', value });
33
+ return this;
34
+ }
35
+ lessThan(value) {
36
+ this.parts.push({ type: 'operator', operator: '<', value });
37
+ return this;
38
+ }
39
+ lessThanOrEqual(value) {
40
+ this.parts.push({ type: 'operator', operator: '<=', value });
41
+ return this;
42
+ }
43
+ greaterThan(value) {
44
+ this.parts.push({ type: 'operator', operator: '>', value });
45
+ return this;
46
+ }
47
+ greaterThanOrEqual(value) {
48
+ this.parts.push({ type: 'operator', operator: '>=', value });
49
+ return this;
50
+ }
51
+ between(start, end) {
52
+ this.parts.push({ type: 'between', start, end });
53
+ return this;
54
+ }
55
+ in(values) {
56
+ this.parts.push({ type: 'in', values });
57
+ return this;
58
+ }
59
+ exists() {
60
+ const last = this.parts.pop();
61
+ if (last && last.type === 'field') {
62
+ this.parts.push({ type: 'function', name: 'attribute_exists', args: [last] });
63
+ }
64
+ return this;
65
+ }
66
+ notExists() {
67
+ const last = this.parts.pop();
68
+ if (last && last.type === 'field') {
69
+ this.parts.push({ type: 'function', name: 'attribute_not_exists', args: [last] });
70
+ }
71
+ return this;
72
+ }
73
+ type(type) {
74
+ const last = this.parts.pop();
75
+ if (last && last.type === 'field') {
76
+ this.parts.push({ type: 'function', name: 'attribute_type', args: [last, { type: 'value', value: type }] });
77
+ }
78
+ return this;
79
+ }
80
+ attributeExists(path) {
81
+ this.parts.push({ type: 'function', name: 'attribute_exists', args: [{ type: 'field', name: path }] });
82
+ return this;
83
+ }
84
+ attributeNotExists(path) {
85
+ this.parts.push({ type: 'function', name: 'attribute_not_exists', args: [{ type: 'field', name: path }] });
86
+ return this;
87
+ }
88
+ attributeType(path, type) {
89
+ this.parts.push({ type: 'function', name: 'attribute_type', args: [{ type: 'field', name: path }, { type: 'value', value: type }] });
90
+ return this;
91
+ }
92
+ beginsWith(pathOrSubstr, substr) {
93
+ if (substr !== undefined) {
94
+ // Legacy static-like call: builder.beginsWith('field', 'prefix')
95
+ this.parts.push({ type: 'function', name: 'begins_with', args: [{ type: 'field', name: pathOrSubstr }, { type: 'value', value: substr }] });
96
+ }
97
+ else {
98
+ // Fluent call: attr('field').beginsWith('prefix')
99
+ const last = this.parts.pop();
100
+ if (last && last.type === 'field') {
101
+ this.parts.push({ type: 'function', name: 'begins_with', args: [last, { type: 'value', value: pathOrSubstr }] });
102
+ }
103
+ }
104
+ return this;
105
+ }
106
+ contains(pathOrValue, value) {
107
+ if (value !== undefined) {
108
+ // Legacy static-like call: builder.contains('field', 'value')
109
+ this.parts.push({ type: 'function', name: 'contains', args: [{ type: 'field', name: pathOrValue }, { type: 'value', value }] });
110
+ }
111
+ else {
112
+ // Fluent call: attr('field').contains('value')
113
+ const last = this.parts.pop();
114
+ if (last && last.type === 'field') {
115
+ this.parts.push({ type: 'function', name: 'contains', args: [last, { type: 'value', value: pathOrValue }] });
116
+ }
117
+ }
118
+ return this;
119
+ }
120
+ size(path) {
121
+ if (path) {
122
+ // Legacy: builder.size('field')
123
+ const builder = new ExpressionBuilder();
124
+ builder.parts.push({ type: 'function', name: 'size', args: [{ type: 'field', name: path }] });
125
+ return builder;
126
+ }
127
+ else {
128
+ // Fluent: attr('field').size()
129
+ const last = this.parts.pop();
130
+ if (last && last.type === 'field') {
131
+ this.parts.push({ type: 'function', name: 'size', args: [last] });
132
+ }
133
+ return this;
134
+ }
135
+ }
136
+ and(other) {
137
+ const newBuilder = new ExpressionBuilder();
138
+ newBuilder.parts.push({ type: 'group', builder: this });
139
+ newBuilder.parts.push({ type: 'logical', operator: 'AND' });
140
+ newBuilder.parts.push({ type: 'group', builder: other });
141
+ return newBuilder;
142
+ }
143
+ or(other) {
144
+ const newBuilder = new ExpressionBuilder();
145
+ newBuilder.parts.push({ type: 'group', builder: this });
146
+ newBuilder.parts.push({ type: 'logical', operator: 'OR' });
147
+ newBuilder.parts.push({ type: 'group', builder: other });
148
+ return newBuilder;
149
+ }
150
+ static not(condition) {
151
+ const builder = new ExpressionBuilder();
152
+ builder.parts.push({ type: 'logical', operator: 'NOT' });
153
+ builder.parts.push({ type: 'group', builder: condition });
154
+ return builder;
155
+ }
156
+ build(attributeNames = {}, attributeValues = {}, counters = { name: 0, value: 0 }) {
157
+ let expressionParts = [];
158
+ for (const part of this.parts) {
159
+ switch (part.type) {
160
+ case 'field': {
161
+ const namePlaceholder = `#n${counters.name++}`;
162
+ attributeNames[namePlaceholder] = part.name;
163
+ expressionParts.push(namePlaceholder);
164
+ break;
165
+ }
166
+ case 'value': {
167
+ const valuePlaceholder = `:v${counters.value++}`;
168
+ attributeValues[valuePlaceholder] = part.value;
169
+ expressionParts.push(valuePlaceholder);
170
+ break;
171
+ }
172
+ case 'condition': {
173
+ const namePlaceholder = `#n${counters.name++}`;
174
+ attributeNames[namePlaceholder] = part.field;
175
+ const valuePlaceholder = `:v${counters.value++}`;
176
+ attributeValues[valuePlaceholder] = part.value;
177
+ expressionParts.push(`${namePlaceholder} ${part.operator} ${valuePlaceholder}`);
178
+ break;
179
+ }
180
+ case 'operator': {
181
+ const valuePlaceholder = `:v${counters.value++}`;
182
+ attributeValues[valuePlaceholder] = part.value;
183
+ const last = expressionParts.pop();
184
+ expressionParts.push(`${last} ${part.operator} ${valuePlaceholder}`);
185
+ break;
186
+ }
187
+ case 'between': {
188
+ const startPlaceholder = `:v${counters.value++}`;
189
+ attributeValues[startPlaceholder] = part.start;
190
+ const endPlaceholder = `:v${counters.value++}`;
191
+ attributeValues[endPlaceholder] = part.end;
192
+ const last = expressionParts.pop();
193
+ expressionParts.push(`${last} BETWEEN ${startPlaceholder} AND ${endPlaceholder}`);
194
+ break;
195
+ }
196
+ case 'in': {
197
+ const placeholders = part.values.map((v) => {
198
+ const vp = `:v${counters.value++}`;
199
+ attributeValues[vp] = v;
200
+ return vp;
201
+ });
202
+ const last = expressionParts.pop();
203
+ expressionParts.push(`${last} IN (${placeholders.join(', ')})`);
204
+ break;
205
+ }
206
+ case 'function': {
207
+ const args = part.args.map((arg) => {
208
+ if (arg.type === 'field') {
209
+ const np = `#n${counters.name++}`;
210
+ attributeNames[np] = arg.name;
211
+ return np;
212
+ }
213
+ else if (arg.type === 'value') {
214
+ const vp = `:v${counters.value++}`;
215
+ attributeValues[vp] = arg.value;
216
+ return vp;
217
+ }
218
+ else if (arg instanceof ExpressionBuilder) {
219
+ const sub = arg.build(attributeNames, attributeValues, counters);
220
+ return sub.expression;
221
+ }
222
+ return arg;
223
+ });
224
+ expressionParts.push(`${part.name}(${args.join(', ')})`);
225
+ break;
226
+ }
227
+ case 'logical': {
228
+ expressionParts.push(part.operator);
229
+ break;
230
+ }
231
+ case 'group': {
232
+ const sub = part.builder.build(attributeNames, attributeValues, counters);
233
+ expressionParts.push(`(${sub.expression})`);
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ return {
239
+ expression: expressionParts.join(' '),
240
+ attributeNames,
241
+ attributeValues
242
+ };
243
+ }
244
+ }
245
+ exports.ExpressionBuilder = ExpressionBuilder;
246
+ function attr(name) {
247
+ return ExpressionBuilder.field(name);
248
+ }
@@ -1,4 +1,5 @@
1
1
  import { DynoQuery } from "./index";
2
+ import { ExpressionBuilder } from "./expression-builder";
2
3
  export interface IndexQueryConfig {
3
4
  tableName?: string;
4
5
  indexName: string;
@@ -23,9 +24,10 @@ export declare class IndexQuery {
23
24
  scanIndexForward?: boolean;
24
25
  exclusiveStartKey?: any;
25
26
  skValue?: string;
26
- filterExpression?: string;
27
- expressionAttributeNames?: Record<string, string>;
28
- expressionAttributeValues?: Record<string, any>;
27
+ filterBuilder?: ExpressionBuilder;
28
+ FilterExpression?: string;
29
+ ExpressionAttributeNames?: Record<string, string>;
30
+ ExpressionAttributeValues?: Record<string, any>;
29
31
  }): Promise<T[]>;
30
32
  get<T = any>(skValue?: string): Promise<T | null>;
31
33
  getPkValue(): string;
@@ -34,18 +34,25 @@ class IndexQuery {
34
34
  return __awaiter(this, void 0, void 0, function* () {
35
35
  const finalSkValue = (options === null || options === void 0 ? void 0 : options.skValue) || this.skValue;
36
36
  let keyCondition = "#pk = :pk";
37
- const expressionAttributeNames = Object.assign({ "#pk": this.pkName }, options === null || options === void 0 ? void 0 : options.expressionAttributeNames);
38
- const expressionAttributeValues = Object.assign({ ":pk": this.pkValue }, options === null || options === void 0 ? void 0 : options.expressionAttributeValues);
37
+ let expressionAttributeNames = Object.assign({ "#pk": this.pkName }, options === null || options === void 0 ? void 0 : options.ExpressionAttributeNames);
38
+ let expressionAttributeValues = Object.assign({ ":pk": this.pkValue }, options === null || options === void 0 ? void 0 : options.ExpressionAttributeValues);
39
39
  if (finalSkValue) {
40
40
  keyCondition += " AND begins_with(#sk, :sk)";
41
41
  expressionAttributeNames["#sk"] = this.skName;
42
42
  expressionAttributeValues[":sk"] = finalSkValue;
43
43
  }
44
+ let filterExpression = options === null || options === void 0 ? void 0 : options.FilterExpression;
45
+ if (options === null || options === void 0 ? void 0 : options.filterBuilder) {
46
+ const { expression, attributeNames, attributeValues } = options.filterBuilder.build();
47
+ filterExpression = expression;
48
+ expressionAttributeNames = Object.assign(Object.assign({}, expressionAttributeNames), attributeNames);
49
+ expressionAttributeValues = Object.assign(Object.assign({}, expressionAttributeValues), attributeValues);
50
+ }
44
51
  const response = yield this.db.query({
45
52
  TableName: this.tableName,
46
53
  IndexName: this.indexName,
47
54
  KeyConditionExpression: keyCondition,
48
- FilterExpression: options === null || options === void 0 ? void 0 : options.filterExpression,
55
+ FilterExpression: filterExpression,
49
56
  ExpressionAttributeNames: expressionAttributeNames,
50
57
  ExpressionAttributeValues: expressionAttributeValues,
51
58
  Limit: options === null || options === void 0 ? void 0 : options.limit,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { PutCommandInput, GetCommandInput, UpdateCommandInput, DeleteCommandInput, QueryCommandInput, ScanCommandInput } from "@aws-sdk/lib-dynamodb";
2
2
  import { Item } from "./partition";
3
3
  import { IndexQuery } from "./index-query";
4
+ import { ExpressionBuilder, attr } from "./expression-builder";
5
+ export { ExpressionBuilder, attr };
4
6
  export interface DynoQueryConfig {
5
7
  tableName?: string;
6
8
  pkName?: string;
@@ -65,6 +67,14 @@ export declare class DynoQuery {
65
67
  * Batch get items from the table.
66
68
  */
67
69
  batchRead(items: (Item | IndexQuery)[]): Promise<any[]>;
70
+ /**
71
+ * Transact write items to the table.
72
+ */
73
+ transactWrite(items: Item[]): Promise<Item[]>;
74
+ /**
75
+ * Transact get items from the table.
76
+ */
77
+ transactRead(items: (Item | IndexQuery)[]): Promise<any[]>;
68
78
  /**
69
79
  * Maps a raw DynamoDB item to a Model Item if it matches a registered model.
70
80
  */
package/dist/index.js CHANGED
@@ -34,11 +34,14 @@ var __rest = (this && this.__rest) || function (s, e) {
34
34
  return t;
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.Item = exports.DynoQuery = void 0;
37
+ exports.Item = exports.DynoQuery = exports.attr = exports.ExpressionBuilder = void 0;
38
38
  const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
39
39
  const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
40
40
  const partition_1 = require("./partition");
41
41
  const index_query_1 = require("./index-query");
42
+ const expression_builder_1 = require("./expression-builder");
43
+ Object.defineProperty(exports, "ExpressionBuilder", { enumerable: true, get: function () { return expression_builder_1.ExpressionBuilder; } });
44
+ Object.defineProperty(exports, "attr", { enumerable: true, get: function () { return expression_builder_1.attr; } });
42
45
  class DynoQuery {
43
46
  constructor(config = {}) {
44
47
  this.registeredModels = {};
@@ -167,22 +170,41 @@ class DynoQuery {
167
170
  if (!tableGroups[tableName]) {
168
171
  tableGroups[tableName] = [];
169
172
  }
170
- const dataToSave = {};
171
- for (const key in item) {
172
- if (Object.prototype.hasOwnProperty.call(item, key) &&
173
- !["_indices", "_partition", "_skValue"].includes(key) &&
174
- typeof item[key] !== "function") {
175
- dataToSave[key] = item[key];
173
+ if (item.toBeDeleted()) {
174
+ tableGroups[tableName].push({
175
+ DeleteRequest: {
176
+ Key: {
177
+ [this.pkName]: partition.getPkValue(),
178
+ [this.skName]: skValue,
179
+ },
180
+ },
181
+ });
182
+ }
183
+ else {
184
+ const dataToSave = {};
185
+ for (const key in item) {
186
+ if (Object.prototype.hasOwnProperty.call(item, key) &&
187
+ ![
188
+ "_indices",
189
+ "_partition",
190
+ "_skValue",
191
+ "_toBeDeleted",
192
+ "_filterBuilder",
193
+ "_conditionBuilder",
194
+ ].includes(key) &&
195
+ typeof item[key] !== "function") {
196
+ dataToSave[key] = item[key];
197
+ }
176
198
  }
199
+ // Ensure PK and SK are in the data
200
+ dataToSave[this.pkName] = partition.getPkValue();
201
+ dataToSave[this.skName] = skValue;
202
+ tableGroups[tableName].push({
203
+ PutRequest: {
204
+ Item: dataToSave,
205
+ },
206
+ });
177
207
  }
178
- // Ensure PK and SK are in the data
179
- dataToSave[this.pkName] = partition.getPkValue();
180
- dataToSave[this.skName] = skValue;
181
- tableGroups[tableName].push({
182
- PutRequest: {
183
- Item: dataToSave,
184
- },
185
- });
186
208
  });
187
209
  const results = [];
188
210
  const tableNames = Object.keys(tableGroups);
@@ -259,6 +281,130 @@ class DynoQuery {
259
281
  return allItems;
260
282
  });
261
283
  }
284
+ /**
285
+ * Transact write items to the table.
286
+ */
287
+ transactWrite(items) {
288
+ return __awaiter(this, void 0, void 0, function* () {
289
+ // Chunk items into 100
290
+ for (let i = 0; i < items.length; i += 100) {
291
+ const chunk = items.slice(i, i + 100);
292
+ const transactItems = chunk.map((item) => {
293
+ const partition = item.getPartition();
294
+ const tableName = partition.getTableName();
295
+ const skValue = item.getSkValue();
296
+ const pkValue = partition.getPkValue();
297
+ const conditionBuilder = item.getConditionBuilder();
298
+ if (item.toBeDeleted()) {
299
+ const deleteItem = {
300
+ Key: {
301
+ [this.pkName]: pkValue,
302
+ [this.skName]: skValue,
303
+ },
304
+ TableName: tableName,
305
+ };
306
+ if (conditionBuilder) {
307
+ const { expression, attributeNames, attributeValues } = conditionBuilder.build();
308
+ deleteItem.ConditionExpression = expression;
309
+ deleteItem.ExpressionAttributeNames = attributeNames;
310
+ deleteItem.ExpressionAttributeValues = attributeValues;
311
+ }
312
+ return {
313
+ Delete: deleteItem,
314
+ };
315
+ }
316
+ else {
317
+ const dataToSave = {};
318
+ for (const key in item) {
319
+ if (Object.prototype.hasOwnProperty.call(item, key) &&
320
+ ![
321
+ "_indices",
322
+ "_partition",
323
+ "_skValue",
324
+ "_toBeDeleted",
325
+ "_filterBuilder",
326
+ "_conditionBuilder",
327
+ ].includes(key) &&
328
+ typeof item[key] !== "function") {
329
+ dataToSave[key] = item[key];
330
+ }
331
+ }
332
+ // Ensure PK and SK are in the data
333
+ dataToSave[this.pkName] = pkValue;
334
+ dataToSave[this.skName] = skValue;
335
+ const putItem = {
336
+ Item: dataToSave,
337
+ TableName: tableName,
338
+ };
339
+ if (conditionBuilder) {
340
+ const { expression, attributeNames, attributeValues } = conditionBuilder.build();
341
+ putItem.ConditionExpression = expression;
342
+ putItem.ExpressionAttributeNames = attributeNames;
343
+ putItem.ExpressionAttributeValues = attributeValues;
344
+ }
345
+ return {
346
+ Put: putItem,
347
+ };
348
+ }
349
+ });
350
+ yield this.docClient.send(new lib_dynamodb_1.TransactWriteCommand({
351
+ TransactItems: transactItems,
352
+ }));
353
+ }
354
+ return items;
355
+ });
356
+ }
357
+ /**
358
+ * Transact get items from the table.
359
+ */
360
+ transactRead(items) {
361
+ return __awaiter(this, void 0, void 0, function* () {
362
+ const allItems = [];
363
+ // Chunk items into 100
364
+ for (let i = 0; i < items.length; i += 100) {
365
+ const chunk = items.slice(i, i + 100);
366
+ const transactItems = chunk.map((item) => {
367
+ let tableName;
368
+ let pkValue;
369
+ let skValue;
370
+ if (item instanceof index_query_1.IndexQuery) {
371
+ tableName = item.tableName;
372
+ pkValue = item.getPkValue();
373
+ skValue = item.getSkValue() || "";
374
+ }
375
+ else {
376
+ const partition = item.getPartition();
377
+ tableName = partition.getTableName();
378
+ pkValue = partition.getPkValue();
379
+ skValue = item.getSkValue();
380
+ }
381
+ return {
382
+ Get: {
383
+ TableName: tableName,
384
+ Key: {
385
+ [this.pkName]: pkValue,
386
+ [this.skName]: skValue,
387
+ },
388
+ },
389
+ };
390
+ });
391
+ const response = yield this.docClient.send(new lib_dynamodb_1.TransactGetCommand({
392
+ TransactItems: transactItems,
393
+ }));
394
+ if (response.Responses) {
395
+ response.Responses.forEach((res) => {
396
+ if (res.Item) {
397
+ allItems.push(this.mapItemToModelItem(res.Item));
398
+ }
399
+ else {
400
+ allItems.push(null);
401
+ }
402
+ });
403
+ }
404
+ }
405
+ return allItems;
406
+ });
407
+ }
262
408
  /**
263
409
  * Maps a raw DynamoDB item to a Model Item if it matches a registered model.
264
410
  */
@@ -1,5 +1,6 @@
1
1
  import { DynoQuery } from "./index";
2
2
  import { IndexQuery } from "./index-query";
3
+ import { ExpressionBuilder } from "./expression-builder";
3
4
  export interface PartitionConfig {
4
5
  tableName?: string;
5
6
  pk?: string;
@@ -10,6 +11,9 @@ export declare class Item {
10
11
  private _indices;
11
12
  private _partition;
12
13
  private _skValue;
14
+ private _toBeDeleted;
15
+ private _filterBuilder?;
16
+ private _conditionBuilder?;
13
17
  constructor(partition: Partition, skValue: string, data: any);
14
18
  }
15
19
  export declare class Partition {
@@ -29,14 +33,20 @@ export declare class Partition {
29
33
  getAll<T = any>(options?: {
30
34
  limit?: number;
31
35
  exclusiveStartKey?: any;
32
- filterExpression?: string;
33
- expressionAttributeNames?: Record<string, string>;
34
- expressionAttributeValues?: Record<string, any>;
36
+ filterBuilder?: ExpressionBuilder;
37
+ FilterExpression?: string;
38
+ ExpressionAttributeNames?: Record<string, string>;
39
+ ExpressionAttributeValues?: Record<string, any>;
35
40
  }): Promise<T[]>;
36
41
  /**
37
42
  * Create an item in this partition.
38
43
  */
39
- create<T = any>(skValue: string, data: T, indices?: IndexQuery[]): Promise<T>;
44
+ create<T = any>(skValue: string, data: T, indices?: IndexQuery[], options?: {
45
+ conditionBuilder?: ExpressionBuilder;
46
+ ConditionExpression?: string;
47
+ ExpressionAttributeNames?: Record<string, string>;
48
+ ExpressionAttributeValues?: Record<string, any>;
49
+ }): Promise<T>;
40
50
  /**
41
51
  * Internal method to get raw data for a specific SK.
42
52
  */
@@ -44,21 +54,36 @@ export declare class Partition {
44
54
  /**
45
55
  * Update an existing item in this partition.
46
56
  */
47
- update<T = any>(skValue: string, data: Partial<T>, indices?: IndexQuery[]): Promise<T>;
57
+ update<T = any>(skValue: string, data: Partial<T>, indices?: IndexQuery[], options?: {
58
+ conditionBuilder?: ExpressionBuilder;
59
+ ConditionExpression?: string;
60
+ ExpressionAttributeNames?: Record<string, string>;
61
+ ExpressionAttributeValues?: Record<string, any>;
62
+ }): Promise<T>;
48
63
  /**
49
64
  * Delete an item by its SK within this partition.
50
65
  */
51
- delete(skValue: string): Promise<void>;
66
+ delete(skValue: string, options?: {
67
+ conditionBuilder?: ExpressionBuilder;
68
+ ConditionExpression?: string;
69
+ ExpressionAttributeNames?: Record<string, string>;
70
+ ExpressionAttributeValues?: Record<string, any>;
71
+ }): Promise<void>;
52
72
  /**
53
73
  * Get data for a specific SK and return it wrapped in a Item object.
54
74
  */
55
75
  get<T = any>(skValue: string): Promise<T | null>;
56
76
  /**
57
77
  * Pre-draft an item for creation. Returns an Item object.
58
- * @param sk The sort key value
78
+ * @param skValue The sort key value
59
79
  * @param data Initial data for the row
60
80
  */
61
81
  draft<T = any>(skValue: string, data?: any): T;
82
+ /**
83
+ * Pre-draft an item for deletion. Returns an Item object marked for deletion.
84
+ * @param skValue The sort key value
85
+ */
86
+ draftDelete<T = any>(skValue: string): T;
62
87
  getTableName(): string;
63
88
  getPkValue(): string;
64
89
  getLastEvaluatedKey(): any;
package/dist/partition.js CHANGED
@@ -15,6 +15,7 @@ class Item {
15
15
  this._indices = [];
16
16
  this._partition = partition;
17
17
  this._skValue = skValue;
18
+ this._toBeDeleted = !!(data === null || data === void 0 ? void 0 : data._toBeDeleted);
18
19
  Object.assign(this, data);
19
20
  const self = this;
20
21
  return new Proxy(this, {
@@ -24,26 +25,50 @@ class Item {
24
25
  const dataToSave = {};
25
26
  for (const key in target) {
26
27
  if (Object.prototype.hasOwnProperty.call(target, key) &&
27
- !["_indices", "_partition", "_skValue"].includes(key) &&
28
+ !["_indices", "_partition", "_skValue", "_toBeDeleted", "_filterBuilder", "_conditionBuilder"].includes(key) &&
28
29
  typeof target[key] !== "function") {
29
30
  dataToSave[key] = target[key];
30
31
  }
31
32
  }
32
- return partition.update(skValue, dataToSave, self._indices);
33
+ return partition.update(skValue, dataToSave, self._indices, {
34
+ conditionBuilder: self._conditionBuilder,
35
+ });
33
36
  };
34
37
  }
35
38
  if (prop === "create") {
36
39
  return (data, indices) => {
37
40
  const dataToSave = data || {};
38
41
  const finalIndices = indices || self._indices;
39
- return partition.create(skValue, dataToSave, finalIndices);
42
+ return partition.create(skValue, dataToSave, finalIndices, {
43
+ conditionBuilder: self._conditionBuilder,
44
+ });
40
45
  };
41
46
  }
42
47
  if (prop === "update") {
43
48
  return (data, indices) => {
44
- return partition.update(skValue, data, indices);
49
+ return partition.update(skValue, data, indices, {
50
+ conditionBuilder: self._conditionBuilder,
51
+ });
45
52
  };
46
53
  }
54
+ if (prop === "setFilter") {
55
+ return (builder) => {
56
+ self._filterBuilder = builder;
57
+ return receiver;
58
+ };
59
+ }
60
+ if (prop === "setCondition") {
61
+ return (builder) => {
62
+ self._conditionBuilder = builder;
63
+ return receiver;
64
+ };
65
+ }
66
+ if (prop === "getFilterBuilder") {
67
+ return () => self._filterBuilder;
68
+ }
69
+ if (prop === "getConditionBuilder") {
70
+ return () => self._conditionBuilder;
71
+ }
47
72
  if (prop === "setIndex") {
48
73
  return (indexObj) => {
49
74
  if (Array.isArray(indexObj)) {
@@ -61,6 +86,9 @@ class Item {
61
86
  if (prop === "getSkValue") {
62
87
  return () => skValue;
63
88
  }
89
+ if (prop === "toBeDeleted") {
90
+ return () => self._toBeDeleted;
91
+ }
64
92
  return Reflect.get(target, prop, receiver);
65
93
  },
66
94
  });
@@ -114,12 +142,21 @@ class Partition {
114
142
  */
115
143
  getAll(options) {
116
144
  return __awaiter(this, void 0, void 0, function* () {
145
+ let filterExpression = options === null || options === void 0 ? void 0 : options.FilterExpression;
146
+ let expressionAttributeNames = Object.assign({ "#pk": this.pkName }, options === null || options === void 0 ? void 0 : options.ExpressionAttributeNames);
147
+ let expressionAttributeValues = Object.assign({ ":pk": this.pkValue }, options === null || options === void 0 ? void 0 : options.ExpressionAttributeValues);
148
+ if (options === null || options === void 0 ? void 0 : options.filterBuilder) {
149
+ const { expression, attributeNames, attributeValues } = options.filterBuilder.build();
150
+ filterExpression = expression;
151
+ expressionAttributeNames = Object.assign(Object.assign({}, expressionAttributeNames), attributeNames);
152
+ expressionAttributeValues = Object.assign(Object.assign({}, expressionAttributeValues), attributeValues);
153
+ }
117
154
  const response = yield this.db.query({
118
155
  TableName: this.tableName,
119
156
  KeyConditionExpression: "#pk = :pk",
120
- FilterExpression: options === null || options === void 0 ? void 0 : options.filterExpression,
121
- ExpressionAttributeNames: Object.assign({ "#pk": this.pkName }, options === null || options === void 0 ? void 0 : options.expressionAttributeNames),
122
- ExpressionAttributeValues: Object.assign({ ":pk": this.pkValue }, options === null || options === void 0 ? void 0 : options.expressionAttributeValues),
157
+ FilterExpression: filterExpression,
158
+ ExpressionAttributeNames: expressionAttributeNames,
159
+ ExpressionAttributeValues: expressionAttributeValues,
123
160
  Limit: options === null || options === void 0 ? void 0 : options.limit,
124
161
  ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.exclusiveStartKey,
125
162
  });
@@ -139,7 +176,7 @@ class Partition {
139
176
  /**
140
177
  * Create an item in this partition.
141
178
  */
142
- create(skValue, data, indices) {
179
+ create(skValue, data, indices, options) {
143
180
  return __awaiter(this, void 0, void 0, function* () {
144
181
  const item = Object.assign({ [this.pkName]: this.pkValue, [this.skName]: skValue }, data);
145
182
  if (indices) {
@@ -150,10 +187,22 @@ class Partition {
150
187
  }
151
188
  });
152
189
  }
153
- yield this.db.create({
190
+ const createParams = {
154
191
  TableName: this.tableName,
155
192
  Item: item,
156
- });
193
+ };
194
+ if (options === null || options === void 0 ? void 0 : options.conditionBuilder) {
195
+ const { expression, attributeNames, attributeValues } = options.conditionBuilder.build();
196
+ createParams.ConditionExpression = expression;
197
+ createParams.ExpressionAttributeNames = Object.assign(Object.assign({}, createParams.ExpressionAttributeNames), attributeNames);
198
+ createParams.ExpressionAttributeValues = Object.assign(Object.assign({}, createParams.ExpressionAttributeValues), attributeValues);
199
+ }
200
+ if (options === null || options === void 0 ? void 0 : options.ConditionExpression) {
201
+ createParams.ConditionExpression = options.ConditionExpression;
202
+ createParams.ExpressionAttributeNames = Object.assign(Object.assign({}, createParams.ExpressionAttributeNames), options.ExpressionAttributeNames);
203
+ createParams.ExpressionAttributeValues = Object.assign(Object.assign({}, createParams.ExpressionAttributeValues), options.ExpressionAttributeValues);
204
+ }
205
+ yield this.db.create(createParams);
157
206
  this.cache[skValue] = item;
158
207
  return new Item(this, skValue, item);
159
208
  });
@@ -186,25 +235,37 @@ class Partition {
186
235
  /**
187
236
  * Update an existing item in this partition.
188
237
  */
189
- update(skValue, data, indices) {
238
+ update(skValue, data, indices, options) {
190
239
  return __awaiter(this, void 0, void 0, function* () {
191
240
  const current = (yield this._getRaw(skValue)) || {};
192
241
  const updated = Object.assign(Object.assign({}, current), data);
193
- return yield this.create(skValue, updated, indices);
242
+ return yield this.create(skValue, updated, indices, options);
194
243
  });
195
244
  }
196
245
  /**
197
246
  * Delete an item by its SK within this partition.
198
247
  */
199
- delete(skValue) {
248
+ delete(skValue, options) {
200
249
  return __awaiter(this, void 0, void 0, function* () {
201
- yield this.db.delete({
250
+ const deleteParams = {
202
251
  TableName: this.tableName,
203
252
  Key: {
204
253
  [this.pkName]: this.pkValue,
205
254
  [this.skName]: skValue,
206
255
  },
207
- });
256
+ };
257
+ if (options === null || options === void 0 ? void 0 : options.conditionBuilder) {
258
+ const { expression, attributeNames, attributeValues } = options.conditionBuilder.build();
259
+ deleteParams.ConditionExpression = expression;
260
+ deleteParams.ExpressionAttributeNames = Object.assign(Object.assign({}, deleteParams.ExpressionAttributeNames), attributeNames);
261
+ deleteParams.ExpressionAttributeValues = Object.assign(Object.assign({}, deleteParams.ExpressionAttributeValues), attributeValues);
262
+ }
263
+ if (options === null || options === void 0 ? void 0 : options.ConditionExpression) {
264
+ deleteParams.ConditionExpression = options.ConditionExpression;
265
+ deleteParams.ExpressionAttributeNames = Object.assign(Object.assign({}, deleteParams.ExpressionAttributeNames), options.ExpressionAttributeNames);
266
+ deleteParams.ExpressionAttributeValues = Object.assign(Object.assign({}, deleteParams.ExpressionAttributeValues), options.ExpressionAttributeValues);
267
+ }
268
+ yield this.db.delete(deleteParams);
208
269
  delete this.cache[skValue];
209
270
  });
210
271
  }
@@ -219,12 +280,19 @@ class Partition {
219
280
  }
220
281
  /**
221
282
  * Pre-draft an item for creation. Returns an Item object.
222
- * @param sk The sort key value
283
+ * @param skValue The sort key value
223
284
  * @param data Initial data for the row
224
285
  */
225
286
  draft(skValue, data = {}) {
226
287
  return new Item(this, skValue, data);
227
288
  }
289
+ /**
290
+ * Pre-draft an item for deletion. Returns an Item object marked for deletion.
291
+ * @param skValue The sort key value
292
+ */
293
+ draftDelete(skValue) {
294
+ return new Item(this, skValue, { _toBeDeleted: true });
295
+ }
228
296
  getTableName() {
229
297
  return this.tableName || "";
230
298
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dynoquery",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/devspikejs/dynoquery.git"