@ftschopp/dynatable-core 1.4.2 → 1.4.4
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/CHANGELOG.md +14 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +19 -2
- package/dist/builders/update/create-update-builder.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.js +25 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/utils/model-utils.d.ts +9 -3
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +15 -7
- package/package.json +19 -2
- package/eslint.config.mjs +0 -4
- package/jest.config.js +0 -11
- package/src/builders/README.md +0 -339
- package/src/builders/batch-get/README.md +0 -43
- package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
- package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
- package/src/builders/batch-get/index.ts +0 -2
- package/src/builders/batch-get/types.ts +0 -28
- package/src/builders/batch-write/README.md +0 -204
- package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
- package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
- package/src/builders/batch-write/index.ts +0 -2
- package/src/builders/batch-write/types.ts +0 -33
- package/src/builders/delete/create-delete-builder.test.ts +0 -294
- package/src/builders/delete/create-delete-builder.ts +0 -100
- package/src/builders/delete/index.ts +0 -2
- package/src/builders/delete/types.ts +0 -17
- package/src/builders/get/create-get-builder.test.ts +0 -272
- package/src/builders/get/create-get-builder.ts +0 -117
- package/src/builders/get/index.ts +0 -2
- package/src/builders/get/types.ts +0 -39
- package/src/builders/index.ts +0 -14
- package/src/builders/put/create-put-builder.test.ts +0 -213
- package/src/builders/put/create-put-builder.ts +0 -160
- package/src/builders/put/index.ts +0 -2
- package/src/builders/put/types.ts +0 -24
- package/src/builders/query/create-query-builder.test.ts +0 -770
- package/src/builders/query/create-query-builder.ts +0 -455
- package/src/builders/query/index.ts +0 -2
- package/src/builders/query/types.ts +0 -109
- package/src/builders/scan/create-scan-builder.test.ts +0 -416
- package/src/builders/scan/create-scan-builder.ts +0 -265
- package/src/builders/scan/index.ts +0 -2
- package/src/builders/scan/types.ts +0 -88
- package/src/builders/shared/conditions.ts +0 -58
- package/src/builders/shared/index.ts +0 -4
- package/src/builders/shared/operators.test.ts +0 -270
- package/src/builders/shared/operators.ts +0 -208
- package/src/builders/shared/projection.ts +0 -28
- package/src/builders/shared/types.ts +0 -101
- package/src/builders/transact-get/README.md +0 -167
- package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
- package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
- package/src/builders/transact-get/index.ts +0 -2
- package/src/builders/transact-get/types.ts +0 -31
- package/src/builders/transact-write/README.md +0 -202
- package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
- package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
- package/src/builders/transact-write/index.ts +0 -2
- package/src/builders/transact-write/types.ts +0 -71
- package/src/builders/update/create-update-builder.test.ts +0 -573
- package/src/builders/update/create-update-builder.ts +0 -375
- package/src/builders/update/index.ts +0 -2
- package/src/builders/update/types.ts +0 -63
- package/src/core/types.test.ts +0 -641
- package/src/core/types.ts +0 -366
- package/src/entity/create-entity-api.ts +0 -224
- package/src/entity/index.ts +0 -19
- package/src/entity/middleware/factories.ts +0 -15
- package/src/entity/middleware/types.ts +0 -7
- package/src/entity/middleware/with-middleware.ts +0 -37
- package/src/entity/types.ts +0 -79
- package/src/entity/validation/key-validation.ts +0 -34
- package/src/index.ts +0 -23
- package/src/table.ts +0 -116
- package/src/utils/dynamodb-logger.test.ts +0 -246
- package/src/utils/dynamodb-logger.ts +0 -175
- package/src/utils/model-utils.test.ts +0 -452
- package/src/utils/model-utils.ts +0 -188
- package/src/utils/zod-utils.test.ts +0 -363
- package/src/utils/zod-utils.ts +0 -56
- package/tests/integration/instagram-clone.integration.test.ts +0 -1019
- package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
- package/tests/integration/transactions.integration.test.ts +0 -528
- package/tsconfig.json +0 -12
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { ExecutableBuilder, AttrBuilder, OpBuilder, Condition } from '../shared';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A single page of scan results plus metadata for manual pagination.
|
|
6
|
-
*/
|
|
7
|
-
export interface ScanResult<Model> {
|
|
8
|
-
/** Items returned in this page. */
|
|
9
|
-
items: Model[];
|
|
10
|
-
/** Cursor for the next page; `undefined` when there are no more results. */
|
|
11
|
-
lastEvaluatedKey?: Record<string, any>;
|
|
12
|
-
/** Number of items returned after applying the filter. */
|
|
13
|
-
count?: number;
|
|
14
|
-
/** Number of items examined before applying the filter. */
|
|
15
|
-
scannedCount?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Builder for DynamoDB Scan operations.
|
|
20
|
-
* Scans the entire table or index without requiring key conditions.
|
|
21
|
-
*/
|
|
22
|
-
export interface ScanBuilder<Model> extends ExecutableBuilder<Model[]> {
|
|
23
|
-
/**
|
|
24
|
-
* Adds a filter expression to the scan.
|
|
25
|
-
* Returns a new immutable builder.
|
|
26
|
-
*/
|
|
27
|
-
filter(fn: (attr: AttrBuilder<Model>, op: OpBuilder) => Condition): ScanBuilder<Model>;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Adds a projection expression to only return specific attributes.
|
|
31
|
-
* Returns a new immutable builder.
|
|
32
|
-
*/
|
|
33
|
-
select(attrs: (keyof Model)[]): ScanBuilder<Model>;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Sets the maximum number of items to evaluate (not necessarily return).
|
|
37
|
-
* Returns a new immutable builder.
|
|
38
|
-
*/
|
|
39
|
-
limit(count: number): ScanBuilder<Model>;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Enables consistent read (eventual consistency is default).
|
|
43
|
-
* Returns a new immutable builder.
|
|
44
|
-
*/
|
|
45
|
-
consistentRead(): ScanBuilder<Model>;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Specifies an index to scan.
|
|
49
|
-
* Returns a new immutable builder.
|
|
50
|
-
*/
|
|
51
|
-
usingIndex(indexName: string): ScanBuilder<Model>;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Sets the starting position for the scan (pagination).
|
|
55
|
-
* Returns a new immutable builder.
|
|
56
|
-
*/
|
|
57
|
-
startFrom(exclusiveStartKey: Record<string, any>): ScanBuilder<Model>;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Enables parallel scan by specifying segment and total segments.
|
|
61
|
-
* Returns a new immutable builder.
|
|
62
|
-
*/
|
|
63
|
-
segment(segmentNumber: number, totalSegments: number): ScanBuilder<Model>;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Executes a single Scan request and returns the page along with the
|
|
67
|
-
* `lastEvaluatedKey` cursor and counts. Use this when you want to drive
|
|
68
|
-
* pagination yourself (e.g. expose a cursor to a client).
|
|
69
|
-
*/
|
|
70
|
-
executeWithPagination(): Promise<ScanResult<Model>>;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Returns an async iterator that paginates internally and yields one item at
|
|
74
|
-
* a time. Memory stays at one page; you can `break` out of the loop early.
|
|
75
|
-
*
|
|
76
|
-
* `Limit` set via `.limit()` is forwarded to DynamoDB as a *per-request*
|
|
77
|
-
* cap, not a total cap. To cap the total, count yourself inside the loop:
|
|
78
|
-
*
|
|
79
|
-
* ```ts
|
|
80
|
-
* let n = 0;
|
|
81
|
-
* for await (const item of builder.iterate()) {
|
|
82
|
-
* if (n++ >= 1000) break;
|
|
83
|
-
* process(item);
|
|
84
|
-
* }
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
iterate(): AsyncIterableIterator<Model>;
|
|
88
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { Condition } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Builds a DynamoDB expression from a condition tree
|
|
5
|
-
*/
|
|
6
|
-
export function buildExpression(condition: Condition): {
|
|
7
|
-
expression: string;
|
|
8
|
-
names: Record<string, string>;
|
|
9
|
-
values: Record<string, any>;
|
|
10
|
-
} {
|
|
11
|
-
const names: Record<string, string> = {};
|
|
12
|
-
const values: Record<string, any> = {};
|
|
13
|
-
|
|
14
|
-
function processCondition(cond: Condition): string {
|
|
15
|
-
// If it's a combinator (AND/OR)
|
|
16
|
-
if (cond.operator && cond.children) {
|
|
17
|
-
const childExpressions = cond.children
|
|
18
|
-
.map((child) => processCondition(child))
|
|
19
|
-
.filter((expr) => expr.length > 0);
|
|
20
|
-
|
|
21
|
-
if (childExpressions.length === 0) return '';
|
|
22
|
-
if (childExpressions.length === 1) return childExpressions[0]!;
|
|
23
|
-
|
|
24
|
-
const combined = childExpressions.map((expr) => `(${expr})`).join(` ${cond.operator} `);
|
|
25
|
-
|
|
26
|
-
return cond.isNegated ? `NOT (${combined})` : combined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// It's a leaf condition
|
|
30
|
-
if (cond.names) {
|
|
31
|
-
Object.assign(names, cond.names);
|
|
32
|
-
}
|
|
33
|
-
if (cond.values) {
|
|
34
|
-
Object.assign(values, cond.values);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return cond.isNegated ? `NOT (${cond.expression})` : cond.expression;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const expression = processCondition(condition);
|
|
41
|
-
|
|
42
|
-
return { expression, names, values };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Combines multiple conditions into a DynamoDB-compatible expression object.
|
|
47
|
-
*/
|
|
48
|
-
export function buildConditionExpr(conditions: Condition[]) {
|
|
49
|
-
const expression = conditions.map((c) => c.expression).join(' AND ');
|
|
50
|
-
const names = Object.assign({}, ...conditions.map((c) => c.names ?? {}));
|
|
51
|
-
const values = Object.assign({}, ...conditions.map((c) => c.values ?? {}));
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
ConditionExpression: expression || undefined,
|
|
55
|
-
ExpressionAttributeNames: Object.keys(names).length ? names : undefined,
|
|
56
|
-
ExpressionAttributeValues: Object.keys(values).length ? values : undefined,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { createOpBuilder } from './operators';
|
|
2
|
-
|
|
3
|
-
describe('OpBuilder Counter Isolation', () => {
|
|
4
|
-
it('should have isolated counters for each opBuilder instance', () => {
|
|
5
|
-
const op1 = createOpBuilder();
|
|
6
|
-
const op2 = createOpBuilder();
|
|
7
|
-
|
|
8
|
-
const attr = { name: 'username' };
|
|
9
|
-
|
|
10
|
-
// Create conditions with both builders
|
|
11
|
-
const cond1a = op1.eq(attr, 'alice');
|
|
12
|
-
const cond1b = op1.eq(attr, 'bob');
|
|
13
|
-
|
|
14
|
-
const cond2a = op2.eq(attr, 'charlie');
|
|
15
|
-
const cond2b = op2.eq(attr, 'dave');
|
|
16
|
-
|
|
17
|
-
// Each builder should have its own counter starting at 0
|
|
18
|
-
expect(cond1a.values).toEqual({ ':username_0': 'alice' });
|
|
19
|
-
expect(cond1b.values).toEqual({ ':username_1': 'bob' });
|
|
20
|
-
|
|
21
|
-
// Second builder should also start at 0
|
|
22
|
-
expect(cond2a.values).toEqual({ ':username_0': 'charlie' });
|
|
23
|
-
expect(cond2b.values).toEqual({ ':username_1': 'dave' });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should not share state between different builder instances', () => {
|
|
27
|
-
const op1 = createOpBuilder();
|
|
28
|
-
const op2 = createOpBuilder();
|
|
29
|
-
|
|
30
|
-
const attr = { name: 'age' };
|
|
31
|
-
|
|
32
|
-
// Use first builder
|
|
33
|
-
op1.gt(attr, 18);
|
|
34
|
-
op1.lt(attr, 65);
|
|
35
|
-
|
|
36
|
-
// Second builder should start from 0, not continue from first
|
|
37
|
-
const cond = op2.eq(attr, 25);
|
|
38
|
-
expect(cond.values).toEqual({ ':age_0': 25 });
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should generate unique names for multiple values in same condition', () => {
|
|
42
|
-
const op = createOpBuilder();
|
|
43
|
-
const attr = { name: 'score' };
|
|
44
|
-
|
|
45
|
-
const between = op.between(attr, 10, 20);
|
|
46
|
-
|
|
47
|
-
expect(between.values).toEqual({
|
|
48
|
-
':score_low_0': 10,
|
|
49
|
-
':score_high_1': 20,
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('New Operators', () => {
|
|
55
|
-
describe('contains operator', () => {
|
|
56
|
-
it('should build contains expression', () => {
|
|
57
|
-
const op = createOpBuilder();
|
|
58
|
-
const attr = { name: 'tags' };
|
|
59
|
-
|
|
60
|
-
const condition = op.contains(attr, 'featured');
|
|
61
|
-
|
|
62
|
-
expect(condition.expression).toBe('contains(#tags, :tags_0)');
|
|
63
|
-
expect(condition.names).toEqual({ '#tags': 'tags' });
|
|
64
|
-
expect(condition.values).toEqual({ ':tags_0': 'featured' });
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('exists operator', () => {
|
|
69
|
-
it('should build attribute_exists expression', () => {
|
|
70
|
-
const op = createOpBuilder();
|
|
71
|
-
const attr = { name: 'email' };
|
|
72
|
-
|
|
73
|
-
const condition = op.exists(attr);
|
|
74
|
-
|
|
75
|
-
expect(condition.expression).toBe('attribute_exists(#email)');
|
|
76
|
-
expect(condition.names).toEqual({ '#email': 'email' });
|
|
77
|
-
expect(condition.values).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('notExists operator', () => {
|
|
82
|
-
it('should build attribute_not_exists expression', () => {
|
|
83
|
-
const op = createOpBuilder();
|
|
84
|
-
const attr = { name: 'deletedAt' };
|
|
85
|
-
|
|
86
|
-
const condition = op.notExists(attr);
|
|
87
|
-
|
|
88
|
-
expect(condition.expression).toBe('attribute_not_exists(#deletedAt)');
|
|
89
|
-
expect(condition.names).toEqual({ '#deletedAt': 'deletedAt' });
|
|
90
|
-
expect(condition.values).toBeUndefined();
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('attributeType operator', () => {
|
|
95
|
-
it('should build attribute_type expression for String', () => {
|
|
96
|
-
const op = createOpBuilder();
|
|
97
|
-
const attr = { name: 'name' };
|
|
98
|
-
|
|
99
|
-
const condition = op.attributeType(attr, 'S');
|
|
100
|
-
|
|
101
|
-
expect(condition.expression).toBe('attribute_type(#name, :name_type_0)');
|
|
102
|
-
expect(condition.names).toEqual({ '#name': 'name' });
|
|
103
|
-
expect(condition.values).toEqual({ ':name_type_0': 'S' });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should build attribute_type expression for Number', () => {
|
|
107
|
-
const op = createOpBuilder();
|
|
108
|
-
const attr = { name: 'age' };
|
|
109
|
-
|
|
110
|
-
const condition = op.attributeType(attr, 'N');
|
|
111
|
-
|
|
112
|
-
expect(condition.expression).toBe('attribute_type(#age, :age_type_0)');
|
|
113
|
-
expect(condition.names).toEqual({ '#age': 'age' });
|
|
114
|
-
expect(condition.values).toEqual({ ':age_type_0': 'N' });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should build attribute_type expression for List', () => {
|
|
118
|
-
const op = createOpBuilder();
|
|
119
|
-
const attr = { name: 'items' };
|
|
120
|
-
|
|
121
|
-
const condition = op.attributeType(attr, 'L');
|
|
122
|
-
|
|
123
|
-
expect(condition.expression).toBe('attribute_type(#items, :items_type_0)');
|
|
124
|
-
expect(condition.names).toEqual({ '#items': 'items' });
|
|
125
|
-
expect(condition.values).toEqual({ ':items_type_0': 'L' });
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('in operator', () => {
|
|
130
|
-
it('should build IN expression with multiple values', () => {
|
|
131
|
-
const op = createOpBuilder();
|
|
132
|
-
const attr = { name: 'status' };
|
|
133
|
-
|
|
134
|
-
const condition = op.in(attr, ['active', 'pending', 'approved']);
|
|
135
|
-
|
|
136
|
-
expect(condition.expression).toBe('#status IN (:status_in0_0, :status_in1_1, :status_in2_2)');
|
|
137
|
-
expect(condition.names).toEqual({ '#status': 'status' });
|
|
138
|
-
expect(condition.values).toEqual({
|
|
139
|
-
':status_in0_0': 'active',
|
|
140
|
-
':status_in1_1': 'pending',
|
|
141
|
-
':status_in2_2': 'approved',
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should build IN expression with single value', () => {
|
|
146
|
-
const op = createOpBuilder();
|
|
147
|
-
const attr = { name: 'category' };
|
|
148
|
-
|
|
149
|
-
const condition = op.in(attr, ['books']);
|
|
150
|
-
|
|
151
|
-
expect(condition.expression).toBe('#category IN (:category_in0_0)');
|
|
152
|
-
expect(condition.names).toEqual({ '#category': 'category' });
|
|
153
|
-
expect(condition.values).toEqual({
|
|
154
|
-
':category_in0_0': 'books',
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('size operator', () => {
|
|
160
|
-
it('should build size() = expression', () => {
|
|
161
|
-
const op = createOpBuilder();
|
|
162
|
-
const attr = { name: 'tags' };
|
|
163
|
-
|
|
164
|
-
const condition = op.size(attr).eq(5);
|
|
165
|
-
|
|
166
|
-
expect(condition.expression).toBe('size(#tags) = :tags_size_0');
|
|
167
|
-
expect(condition.names).toEqual({ '#tags': 'tags' });
|
|
168
|
-
expect(condition.values).toEqual({ ':tags_size_0': 5 });
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('should build size() <> expression', () => {
|
|
172
|
-
const op = createOpBuilder();
|
|
173
|
-
const attr = { name: 'items' };
|
|
174
|
-
|
|
175
|
-
const condition = op.size(attr).ne(0);
|
|
176
|
-
|
|
177
|
-
expect(condition.expression).toBe('size(#items) <> :items_size_0');
|
|
178
|
-
expect(condition.names).toEqual({ '#items': 'items' });
|
|
179
|
-
expect(condition.values).toEqual({ ':items_size_0': 0 });
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should build size() < expression', () => {
|
|
183
|
-
const op = createOpBuilder();
|
|
184
|
-
const attr = { name: 'name' };
|
|
185
|
-
|
|
186
|
-
const condition = op.size(attr).lt(100);
|
|
187
|
-
|
|
188
|
-
expect(condition.expression).toBe('size(#name) < :name_size_0');
|
|
189
|
-
expect(condition.names).toEqual({ '#name': 'name' });
|
|
190
|
-
expect(condition.values).toEqual({ ':name_size_0': 100 });
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('should build size() <= expression', () => {
|
|
194
|
-
const op = createOpBuilder();
|
|
195
|
-
const attr = { name: 'description' };
|
|
196
|
-
|
|
197
|
-
const condition = op.size(attr).lte(500);
|
|
198
|
-
|
|
199
|
-
expect(condition.expression).toBe('size(#description) <= :description_size_0');
|
|
200
|
-
expect(condition.names).toEqual({ '#description': 'description' });
|
|
201
|
-
expect(condition.values).toEqual({ ':description_size_0': 500 });
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('should build size() > expression', () => {
|
|
205
|
-
const op = createOpBuilder();
|
|
206
|
-
const attr = { name: 'password' };
|
|
207
|
-
|
|
208
|
-
const condition = op.size(attr).gt(8);
|
|
209
|
-
|
|
210
|
-
expect(condition.expression).toBe('size(#password) > :password_size_0');
|
|
211
|
-
expect(condition.names).toEqual({ '#password': 'password' });
|
|
212
|
-
expect(condition.values).toEqual({ ':password_size_0': 8 });
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should build size() >= expression', () => {
|
|
216
|
-
const op = createOpBuilder();
|
|
217
|
-
const attr = { name: 'content' };
|
|
218
|
-
|
|
219
|
-
const condition = op.size(attr).gte(10);
|
|
220
|
-
|
|
221
|
-
expect(condition.expression).toBe('size(#content) >= :content_size_0');
|
|
222
|
-
expect(condition.names).toEqual({ '#content': 'content' });
|
|
223
|
-
expect(condition.values).toEqual({ ':content_size_0': 10 });
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
describe('Combined operators', () => {
|
|
228
|
-
it('should combine exists and other conditions with AND', () => {
|
|
229
|
-
const op = createOpBuilder();
|
|
230
|
-
const attr1 = { name: 'email' };
|
|
231
|
-
const attr2 = { name: 'verified' };
|
|
232
|
-
|
|
233
|
-
const condition = op.and(op.exists(attr1), op.eq(attr2, true));
|
|
234
|
-
|
|
235
|
-
expect(condition.operator).toBe('AND');
|
|
236
|
-
expect(condition.children).toBeDefined();
|
|
237
|
-
expect(condition.children).toHaveLength(2);
|
|
238
|
-
expect(condition.children![0]!.expression).toBe('attribute_exists(#email)');
|
|
239
|
-
expect(condition.children![1]!.expression).toBe('#verified = :verified_0');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('should combine size with comparison', () => {
|
|
243
|
-
const op = createOpBuilder();
|
|
244
|
-
const attr1 = { name: 'tags' };
|
|
245
|
-
const attr2 = { name: 'status' };
|
|
246
|
-
|
|
247
|
-
const condition = op.and(op.size(attr1).gt(0), op.eq(attr2, 'active'));
|
|
248
|
-
|
|
249
|
-
expect(condition.operator).toBe('AND');
|
|
250
|
-
expect(condition.children).toBeDefined();
|
|
251
|
-
expect(condition.children).toHaveLength(2);
|
|
252
|
-
expect(condition.children![0]!.expression).toBe('size(#tags) > :tags_size_0');
|
|
253
|
-
expect(condition.children![1]!.expression).toBe('#status = :status_1');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('should use IN with OR', () => {
|
|
257
|
-
const op = createOpBuilder();
|
|
258
|
-
const attr1 = { name: 'status' };
|
|
259
|
-
const attr2 = { name: 'priority' };
|
|
260
|
-
|
|
261
|
-
const condition = op.or(op.in(attr1, ['active', 'pending']), op.eq(attr2, 'high'));
|
|
262
|
-
|
|
263
|
-
expect(condition.operator).toBe('OR');
|
|
264
|
-
expect(condition.children).toBeDefined();
|
|
265
|
-
expect(condition.children).toHaveLength(2);
|
|
266
|
-
expect(condition.children![0]!.expression).toContain('IN');
|
|
267
|
-
expect(condition.children![1]!.expression).toBe('#priority = :priority_2');
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
});
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { OpBuilder } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Creates a scoped OpBuilder with its own counter for unique value placeholders.
|
|
5
|
-
* Each builder instance maintains isolated state to prevent naming conflicts.
|
|
6
|
-
*/
|
|
7
|
-
export function createOpBuilder(): OpBuilder {
|
|
8
|
-
let valueCounter = 0;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generates a unique value placeholder name within this builder's scope
|
|
12
|
-
*/
|
|
13
|
-
function getUniqueValueName(baseName: string): string {
|
|
14
|
-
return `${baseName}_${valueCounter++}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
eq: (attr, value) => {
|
|
19
|
-
const valueName = getUniqueValueName(attr.name);
|
|
20
|
-
return {
|
|
21
|
-
expression: `#${attr.name} = :${valueName}`,
|
|
22
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
23
|
-
values: { [`:${valueName}`]: value },
|
|
24
|
-
keyOperator: 'eq',
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
ne: (attr, value) => {
|
|
28
|
-
const valueName = getUniqueValueName(attr.name);
|
|
29
|
-
return {
|
|
30
|
-
expression: `#${attr.name} <> :${valueName}`,
|
|
31
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
32
|
-
values: { [`:${valueName}`]: value },
|
|
33
|
-
keyOperator: 'ne',
|
|
34
|
-
};
|
|
35
|
-
},
|
|
36
|
-
lt: (attr, value) => {
|
|
37
|
-
const valueName = getUniqueValueName(attr.name);
|
|
38
|
-
return {
|
|
39
|
-
expression: `#${attr.name} < :${valueName}`,
|
|
40
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
41
|
-
values: { [`:${valueName}`]: value },
|
|
42
|
-
keyOperator: 'lt',
|
|
43
|
-
};
|
|
44
|
-
},
|
|
45
|
-
lte: (attr, value) => {
|
|
46
|
-
const valueName = getUniqueValueName(attr.name);
|
|
47
|
-
return {
|
|
48
|
-
expression: `#${attr.name} <= :${valueName}`,
|
|
49
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
50
|
-
values: { [`:${valueName}`]: value },
|
|
51
|
-
keyOperator: 'lte',
|
|
52
|
-
};
|
|
53
|
-
},
|
|
54
|
-
gt: (attr, value) => {
|
|
55
|
-
const valueName = getUniqueValueName(attr.name);
|
|
56
|
-
return {
|
|
57
|
-
expression: `#${attr.name} > :${valueName}`,
|
|
58
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
59
|
-
values: { [`:${valueName}`]: value },
|
|
60
|
-
keyOperator: 'gt',
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
gte: (attr, value) => {
|
|
64
|
-
const valueName = getUniqueValueName(attr.name);
|
|
65
|
-
return {
|
|
66
|
-
expression: `#${attr.name} >= :${valueName}`,
|
|
67
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
68
|
-
values: { [`:${valueName}`]: value },
|
|
69
|
-
keyOperator: 'gte',
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
between: (attr, low, high) => {
|
|
73
|
-
const lowName = getUniqueValueName(`${attr.name}_low`);
|
|
74
|
-
const highName = getUniqueValueName(`${attr.name}_high`);
|
|
75
|
-
return {
|
|
76
|
-
expression: `#${attr.name} BETWEEN :${lowName} AND :${highName}`,
|
|
77
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
78
|
-
values: {
|
|
79
|
-
[`:${lowName}`]: low,
|
|
80
|
-
[`:${highName}`]: high,
|
|
81
|
-
},
|
|
82
|
-
keyOperator: 'between',
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
beginsWith: (attr, value) => {
|
|
86
|
-
const valueName = getUniqueValueName(attr.name);
|
|
87
|
-
return {
|
|
88
|
-
expression: `begins_with(#${attr.name}, :${valueName})`,
|
|
89
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
90
|
-
values: { [`:${valueName}`]: value },
|
|
91
|
-
keyOperator: 'beginsWith',
|
|
92
|
-
};
|
|
93
|
-
},
|
|
94
|
-
contains: (attr, value) => {
|
|
95
|
-
const valueName = getUniqueValueName(attr.name);
|
|
96
|
-
return {
|
|
97
|
-
expression: `contains(#${attr.name}, :${valueName})`,
|
|
98
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
99
|
-
values: { [`:${valueName}`]: value },
|
|
100
|
-
};
|
|
101
|
-
},
|
|
102
|
-
exists: (attr) => ({
|
|
103
|
-
expression: `attribute_exists(#${attr.name})`,
|
|
104
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
105
|
-
}),
|
|
106
|
-
notExists: (attr) => ({
|
|
107
|
-
expression: `attribute_not_exists(#${attr.name})`,
|
|
108
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
109
|
-
}),
|
|
110
|
-
attributeType: (attr, type) => {
|
|
111
|
-
const valueName = getUniqueValueName(`${attr.name}_type`);
|
|
112
|
-
return {
|
|
113
|
-
expression: `attribute_type(#${attr.name}, :${valueName})`,
|
|
114
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
115
|
-
values: { [`:${valueName}`]: type },
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
in: (attr, values) => {
|
|
119
|
-
const valueNames = values.map((_, i) => getUniqueValueName(`${attr.name}_in${i}`));
|
|
120
|
-
const placeholders = valueNames.map((name) => `:${name}`).join(', ');
|
|
121
|
-
const valuesObj: Record<string, any> = {};
|
|
122
|
-
valueNames.forEach((name, i) => {
|
|
123
|
-
valuesObj[`:${name}`] = values[i];
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
expression: `#${attr.name} IN (${placeholders})`,
|
|
128
|
-
names: { [`#${attr.name}`]: attr.name },
|
|
129
|
-
values: valuesObj,
|
|
130
|
-
};
|
|
131
|
-
},
|
|
132
|
-
size: (attr) => {
|
|
133
|
-
const sizeExpr = `size(#${attr.name})`;
|
|
134
|
-
const names = { [`#${attr.name}`]: attr.name };
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
eq: (value: number) => {
|
|
138
|
-
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
139
|
-
return {
|
|
140
|
-
expression: `${sizeExpr} = :${valueName}`,
|
|
141
|
-
names,
|
|
142
|
-
values: { [`:${valueName}`]: value },
|
|
143
|
-
};
|
|
144
|
-
},
|
|
145
|
-
ne: (value: number) => {
|
|
146
|
-
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
147
|
-
return {
|
|
148
|
-
expression: `${sizeExpr} <> :${valueName}`,
|
|
149
|
-
names,
|
|
150
|
-
values: { [`:${valueName}`]: value },
|
|
151
|
-
};
|
|
152
|
-
},
|
|
153
|
-
lt: (value: number) => {
|
|
154
|
-
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
155
|
-
return {
|
|
156
|
-
expression: `${sizeExpr} < :${valueName}`,
|
|
157
|
-
names,
|
|
158
|
-
values: { [`:${valueName}`]: value },
|
|
159
|
-
};
|
|
160
|
-
},
|
|
161
|
-
lte: (value: number) => {
|
|
162
|
-
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
163
|
-
return {
|
|
164
|
-
expression: `${sizeExpr} <= :${valueName}`,
|
|
165
|
-
names,
|
|
166
|
-
values: { [`:${valueName}`]: value },
|
|
167
|
-
};
|
|
168
|
-
},
|
|
169
|
-
gt: (value: number) => {
|
|
170
|
-
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
171
|
-
return {
|
|
172
|
-
expression: `${sizeExpr} > :${valueName}`,
|
|
173
|
-
names,
|
|
174
|
-
values: { [`:${valueName}`]: value },
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
gte: (value: number) => {
|
|
178
|
-
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
179
|
-
return {
|
|
180
|
-
expression: `${sizeExpr} >= :${valueName}`,
|
|
181
|
-
names,
|
|
182
|
-
values: { [`:${valueName}`]: value },
|
|
183
|
-
};
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
},
|
|
187
|
-
and: (...conditions) => ({
|
|
188
|
-
expression: '',
|
|
189
|
-
operator: 'AND',
|
|
190
|
-
children: conditions,
|
|
191
|
-
}),
|
|
192
|
-
or: (...conditions) => ({
|
|
193
|
-
expression: '',
|
|
194
|
-
operator: 'OR',
|
|
195
|
-
children: conditions,
|
|
196
|
-
}),
|
|
197
|
-
not: (condition) => ({
|
|
198
|
-
...condition,
|
|
199
|
-
isNegated: true,
|
|
200
|
-
}),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @deprecated Use createOpBuilder() instead to avoid global state.
|
|
206
|
-
* This export is maintained for backward compatibility.
|
|
207
|
-
*/
|
|
208
|
-
export const opBuilder: OpBuilder = createOpBuilder();
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds a `ProjectionExpression` together with the matching
|
|
3
|
-
* `ExpressionAttributeNames` map so that any attribute — including
|
|
4
|
-
* DynamoDB reserved words like `name`, `date`, `status`, `type`, `data`,
|
|
5
|
-
* `count`, `size`, `value`, `time`, `year`, `source`, … — is referenced
|
|
6
|
-
* via a `#placeholder`.
|
|
7
|
-
*
|
|
8
|
-
* Without this indirection DynamoDB rejects the request with
|
|
9
|
-
* `ValidationException: Attribute name is a reserved keyword`.
|
|
10
|
-
*/
|
|
11
|
-
export function buildProjectionExpression(attrs: string[]): {
|
|
12
|
-
ProjectionExpression: string;
|
|
13
|
-
ExpressionAttributeNames: Record<string, string>;
|
|
14
|
-
} {
|
|
15
|
-
const names: Record<string, string> = {};
|
|
16
|
-
const projectionParts: string[] = [];
|
|
17
|
-
|
|
18
|
-
attrs.forEach((attr) => {
|
|
19
|
-
const placeholder = `#${attr}`;
|
|
20
|
-
names[placeholder] = attr;
|
|
21
|
-
projectionParts.push(placeholder);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
ProjectionExpression: projectionParts.join(', '),
|
|
26
|
-
ExpressionAttributeNames: names,
|
|
27
|
-
};
|
|
28
|
-
}
|