@ftschopp/dynatable-core 1.0.0
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 +62 -0
- package/README.md +17 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts +18 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -0
- package/dist/builders/batch-get/create-batch-get-builder.js +71 -0
- package/dist/builders/batch-get/index.d.ts +3 -0
- package/dist/builders/batch-get/index.d.ts.map +1 -0
- package/dist/builders/batch-get/index.js +18 -0
- package/dist/builders/batch-get/types.d.ts +25 -0
- package/dist/builders/batch-get/types.d.ts.map +1 -0
- package/dist/builders/batch-get/types.js +2 -0
- package/dist/builders/batch-write/create-batch-write-builder.d.ts +12 -0
- package/dist/builders/batch-write/create-batch-write-builder.d.ts.map +1 -0
- package/dist/builders/batch-write/create-batch-write-builder.js +35 -0
- package/dist/builders/batch-write/index.d.ts +3 -0
- package/dist/builders/batch-write/index.d.ts.map +1 -0
- package/dist/builders/batch-write/index.js +18 -0
- package/dist/builders/batch-write/types.d.ts +30 -0
- package/dist/builders/batch-write/types.d.ts.map +1 -0
- package/dist/builders/batch-write/types.js +2 -0
- package/dist/builders/delete/create-delete-builder.d.ts +9 -0
- package/dist/builders/delete/create-delete-builder.d.ts.map +1 -0
- package/dist/builders/delete/create-delete-builder.js +72 -0
- package/dist/builders/delete/index.d.ts +3 -0
- package/dist/builders/delete/index.d.ts.map +1 -0
- package/dist/builders/delete/index.js +18 -0
- package/dist/builders/delete/types.d.ts +11 -0
- package/dist/builders/delete/types.d.ts.map +1 -0
- package/dist/builders/delete/types.js +2 -0
- package/dist/builders/get/create-get-builder.d.ts +12 -0
- package/dist/builders/get/create-get-builder.d.ts.map +1 -0
- package/dist/builders/get/create-get-builder.js +95 -0
- package/dist/builders/get/index.d.ts +3 -0
- package/dist/builders/get/index.d.ts.map +1 -0
- package/dist/builders/get/index.js +18 -0
- package/dist/builders/get/types.d.ts +28 -0
- package/dist/builders/get/types.d.ts.map +1 -0
- package/dist/builders/get/types.js +2 -0
- package/dist/builders/index.d.ts +12 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +29 -0
- package/dist/builders/put/create-put-builder.d.ts +9 -0
- package/dist/builders/put/create-put-builder.d.ts.map +1 -0
- package/dist/builders/put/create-put-builder.js +104 -0
- package/dist/builders/put/index.d.ts +3 -0
- package/dist/builders/put/index.d.ts.map +1 -0
- package/dist/builders/put/index.js +18 -0
- package/dist/builders/put/types.d.ts +17 -0
- package/dist/builders/put/types.d.ts.map +1 -0
- package/dist/builders/put/types.js +2 -0
- package/dist/builders/query/create-query-builder.d.ts +9 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -0
- package/dist/builders/query/create-query-builder.js +283 -0
- package/dist/builders/query/index.d.ts +3 -0
- package/dist/builders/query/index.d.ts.map +1 -0
- package/dist/builders/query/index.js +18 -0
- package/dist/builders/query/types.d.ts +79 -0
- package/dist/builders/query/types.d.ts.map +1 -0
- package/dist/builders/query/types.js +2 -0
- package/dist/builders/scan/create-scan-builder.d.ts +12 -0
- package/dist/builders/scan/create-scan-builder.d.ts.map +1 -0
- package/dist/builders/scan/create-scan-builder.js +103 -0
- package/dist/builders/scan/index.d.ts +3 -0
- package/dist/builders/scan/index.d.ts.map +1 -0
- package/dist/builders/scan/index.js +18 -0
- package/dist/builders/scan/types.d.ts +43 -0
- package/dist/builders/scan/types.d.ts.map +1 -0
- package/dist/builders/scan/types.js +2 -0
- package/dist/builders/shared/conditions.d.ts +18 -0
- package/dist/builders/shared/conditions.d.ts.map +1 -0
- package/dist/builders/shared/conditions.js +48 -0
- package/dist/builders/shared/index.d.ts +4 -0
- package/dist/builders/shared/index.d.ts.map +1 -0
- package/dist/builders/shared/index.js +19 -0
- package/dist/builders/shared/operators.d.ts +12 -0
- package/dist/builders/shared/operators.d.ts.map +1 -0
- package/dist/builders/shared/operators.js +197 -0
- package/dist/builders/shared/types.d.ts +77 -0
- package/dist/builders/shared/types.d.ts.map +1 -0
- package/dist/builders/shared/types.js +6 -0
- package/dist/builders/transact-get/create-transact-get-builder.d.ts +18 -0
- package/dist/builders/transact-get/create-transact-get-builder.d.ts.map +1 -0
- package/dist/builders/transact-get/create-transact-get-builder.js +60 -0
- package/dist/builders/transact-get/index.d.ts +3 -0
- package/dist/builders/transact-get/index.d.ts.map +1 -0
- package/dist/builders/transact-get/index.js +18 -0
- package/dist/builders/transact-get/types.d.ts +28 -0
- package/dist/builders/transact-get/types.d.ts.map +1 -0
- package/dist/builders/transact-get/types.js +2 -0
- package/dist/builders/transact-write/create-transact-write-builder.d.ts +18 -0
- package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -0
- package/dist/builders/transact-write/create-transact-write-builder.js +97 -0
- package/dist/builders/transact-write/index.d.ts +3 -0
- package/dist/builders/transact-write/index.d.ts.map +1 -0
- package/dist/builders/transact-write/index.js +18 -0
- package/dist/builders/transact-write/types.d.ts +34 -0
- package/dist/builders/transact-write/types.d.ts.map +1 -0
- package/dist/builders/transact-write/types.js +2 -0
- package/dist/builders/update/create-update-builder.d.ts +14 -0
- package/dist/builders/update/create-update-builder.d.ts.map +1 -0
- package/dist/builders/update/create-update-builder.js +180 -0
- package/dist/builders/update/index.d.ts +3 -0
- package/dist/builders/update/index.d.ts.map +1 -0
- package/dist/builders/update/index.js +18 -0
- package/dist/builders/update/types.d.ts +35 -0
- package/dist/builders/update/types.d.ts.map +1 -0
- package/dist/builders/update/types.js +2 -0
- package/dist/core/types.d.ts +224 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/entity.d.ts +73 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +161 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/table.d.ts +70 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +69 -0
- package/dist/utils/dynamodb-logger.d.ts +118 -0
- package/dist/utils/dynamodb-logger.d.ts.map +1 -0
- package/dist/utils/dynamodb-logger.js +125 -0
- package/dist/utils/model-utils.d.ts +24 -0
- package/dist/utils/model-utils.d.ts.map +1 -0
- package/dist/utils/model-utils.js +88 -0
- package/dist/utils/zod-utils.d.ts +13 -0
- package/dist/utils/zod-utils.d.ts.map +1 -0
- package/dist/utils/zod-utils.js +35 -0
- package/eslint.config.mjs +4 -0
- package/jest.config.js +11 -0
- package/package.json +36 -0
- package/src/builders/README.md +272 -0
- package/src/builders/batch-get/README.md +98 -0
- package/src/builders/batch-get/create-batch-get-builder.test.ts +165 -0
- package/src/builders/batch-get/create-batch-get-builder.ts +106 -0
- package/src/builders/batch-get/index.ts +2 -0
- package/src/builders/batch-get/types.ts +29 -0
- package/src/builders/batch-write/README.md +204 -0
- package/src/builders/batch-write/create-batch-write-builder.test.ts +173 -0
- package/src/builders/batch-write/create-batch-write-builder.ts +49 -0
- package/src/builders/batch-write/index.ts +2 -0
- package/src/builders/batch-write/types.ts +33 -0
- package/src/builders/delete/create-delete-builder.test.ts +294 -0
- package/src/builders/delete/create-delete-builder.ts +100 -0
- package/src/builders/delete/index.ts +2 -0
- package/src/builders/delete/types.ts +11 -0
- package/src/builders/get/create-get-builder.test.ts +272 -0
- package/src/builders/get/create-get-builder.ts +140 -0
- package/src/builders/get/index.ts +2 -0
- package/src/builders/get/types.ts +30 -0
- package/src/builders/index.ts +14 -0
- package/src/builders/put/create-put-builder.test.ts +213 -0
- package/src/builders/put/create-put-builder.ts +151 -0
- package/src/builders/put/index.ts +2 -0
- package/src/builders/put/types.ts +18 -0
- package/src/builders/query/create-query-builder.test.ts +230 -0
- package/src/builders/query/create-query-builder.ts +353 -0
- package/src/builders/query/index.ts +2 -0
- package/src/builders/query/types.ts +95 -0
- package/src/builders/scan/create-scan-builder.test.ts +260 -0
- package/src/builders/scan/create-scan-builder.ts +217 -0
- package/src/builders/scan/index.ts +2 -0
- package/src/builders/scan/types.ts +49 -0
- package/src/builders/shared/conditions.ts +58 -0
- package/src/builders/shared/index.ts +3 -0
- package/src/builders/shared/operators.test.ts +270 -0
- package/src/builders/shared/operators.ts +200 -0
- package/src/builders/shared/types.ts +100 -0
- package/src/builders/transact-get/README.md +167 -0
- package/src/builders/transact-get/create-transact-get-builder.test.ts +239 -0
- package/src/builders/transact-get/create-transact-get-builder.ts +67 -0
- package/src/builders/transact-get/index.ts +2 -0
- package/src/builders/transact-get/types.ts +31 -0
- package/src/builders/transact-write/README.md +166 -0
- package/src/builders/transact-write/create-transact-write-builder.test.ts +288 -0
- package/src/builders/transact-write/create-transact-write-builder.ts +118 -0
- package/src/builders/transact-write/index.ts +2 -0
- package/src/builders/transact-write/types.ts +33 -0
- package/src/builders/update/create-update-builder.test.ts +333 -0
- package/src/builders/update/create-update-builder.ts +284 -0
- package/src/builders/update/index.ts +2 -0
- package/src/builders/update/types.ts +42 -0
- package/src/core/types.test.ts +506 -0
- package/src/core/types.ts +290 -0
- package/src/entity.ts +337 -0
- package/src/index.ts +22 -0
- package/src/table.ts +109 -0
- package/src/utils/dynamodb-logger.test.ts +246 -0
- package/src/utils/dynamodb-logger.ts +175 -0
- package/src/utils/model-utils.test.ts +232 -0
- package/src/utils/model-utils.ts +101 -0
- package/src/utils/zod-utils.test.ts +272 -0
- package/src/utils/zod-utils.ts +36 -0
- package/tests/integration/instagram-clone.integration.test.ts +966 -0
- package/tests/integration/pagination-timestamps.integration.test.ts +375 -0
- package/tests/integration/transactions.integration.test.ts +529 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
};
|
|
25
|
+
},
|
|
26
|
+
ne: (attr, value) => {
|
|
27
|
+
const valueName = getUniqueValueName(attr.name);
|
|
28
|
+
return {
|
|
29
|
+
expression: `#${attr.name} <> :${valueName}`,
|
|
30
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
31
|
+
values: { [`:${valueName}`]: value },
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
lt: (attr, value) => {
|
|
35
|
+
const valueName = getUniqueValueName(attr.name);
|
|
36
|
+
return {
|
|
37
|
+
expression: `#${attr.name} < :${valueName}`,
|
|
38
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
39
|
+
values: { [`:${valueName}`]: value },
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
lte: (attr, value) => {
|
|
43
|
+
const valueName = getUniqueValueName(attr.name);
|
|
44
|
+
return {
|
|
45
|
+
expression: `#${attr.name} <= :${valueName}`,
|
|
46
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
47
|
+
values: { [`:${valueName}`]: value },
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
gt: (attr, value) => {
|
|
51
|
+
const valueName = getUniqueValueName(attr.name);
|
|
52
|
+
return {
|
|
53
|
+
expression: `#${attr.name} > :${valueName}`,
|
|
54
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
55
|
+
values: { [`:${valueName}`]: value },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
gte: (attr, value) => {
|
|
59
|
+
const valueName = getUniqueValueName(attr.name);
|
|
60
|
+
return {
|
|
61
|
+
expression: `#${attr.name} >= :${valueName}`,
|
|
62
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
63
|
+
values: { [`:${valueName}`]: value },
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
between: (attr, low, high) => {
|
|
67
|
+
const lowName = getUniqueValueName(`${attr.name}_low`);
|
|
68
|
+
const highName = getUniqueValueName(`${attr.name}_high`);
|
|
69
|
+
return {
|
|
70
|
+
expression: `#${attr.name} BETWEEN :${lowName} AND :${highName}`,
|
|
71
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
72
|
+
values: {
|
|
73
|
+
[`:${lowName}`]: low,
|
|
74
|
+
[`:${highName}`]: high,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
beginsWith: (attr, value) => {
|
|
79
|
+
const valueName = getUniqueValueName(attr.name);
|
|
80
|
+
return {
|
|
81
|
+
expression: `begins_with(#${attr.name}, :${valueName})`,
|
|
82
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
83
|
+
values: { [`:${valueName}`]: value },
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
contains: (attr, value) => {
|
|
87
|
+
const valueName = getUniqueValueName(attr.name);
|
|
88
|
+
return {
|
|
89
|
+
expression: `contains(#${attr.name}, :${valueName})`,
|
|
90
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
91
|
+
values: { [`:${valueName}`]: value },
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
exists: (attr) => ({
|
|
95
|
+
expression: `attribute_exists(#${attr.name})`,
|
|
96
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
97
|
+
}),
|
|
98
|
+
notExists: (attr) => ({
|
|
99
|
+
expression: `attribute_not_exists(#${attr.name})`,
|
|
100
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
101
|
+
}),
|
|
102
|
+
attributeType: (attr, type) => {
|
|
103
|
+
const valueName = getUniqueValueName(`${attr.name}_type`);
|
|
104
|
+
return {
|
|
105
|
+
expression: `attribute_type(#${attr.name}, :${valueName})`,
|
|
106
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
107
|
+
values: { [`:${valueName}`]: type },
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
in: (attr, values) => {
|
|
111
|
+
const valueNames = values.map((_, i) => getUniqueValueName(`${attr.name}_in${i}`));
|
|
112
|
+
const placeholders = valueNames.map((name) => `:${name}`).join(', ');
|
|
113
|
+
const valuesObj: Record<string, any> = {};
|
|
114
|
+
valueNames.forEach((name, i) => {
|
|
115
|
+
valuesObj[`:${name}`] = values[i];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
expression: `#${attr.name} IN (${placeholders})`,
|
|
120
|
+
names: { [`#${attr.name}`]: attr.name },
|
|
121
|
+
values: valuesObj,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
size: (attr) => {
|
|
125
|
+
const sizeExpr = `size(#${attr.name})`;
|
|
126
|
+
const names = { [`#${attr.name}`]: attr.name };
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
eq: (value: number) => {
|
|
130
|
+
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
131
|
+
return {
|
|
132
|
+
expression: `${sizeExpr} = :${valueName}`,
|
|
133
|
+
names,
|
|
134
|
+
values: { [`:${valueName}`]: value },
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
ne: (value: number) => {
|
|
138
|
+
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
139
|
+
return {
|
|
140
|
+
expression: `${sizeExpr} <> :${valueName}`,
|
|
141
|
+
names,
|
|
142
|
+
values: { [`:${valueName}`]: value },
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
lt: (value: number) => {
|
|
146
|
+
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
147
|
+
return {
|
|
148
|
+
expression: `${sizeExpr} < :${valueName}`,
|
|
149
|
+
names,
|
|
150
|
+
values: { [`:${valueName}`]: value },
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
lte: (value: number) => {
|
|
154
|
+
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
155
|
+
return {
|
|
156
|
+
expression: `${sizeExpr} <= :${valueName}`,
|
|
157
|
+
names,
|
|
158
|
+
values: { [`:${valueName}`]: value },
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
gt: (value: number) => {
|
|
162
|
+
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
163
|
+
return {
|
|
164
|
+
expression: `${sizeExpr} > :${valueName}`,
|
|
165
|
+
names,
|
|
166
|
+
values: { [`:${valueName}`]: value },
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
gte: (value: number) => {
|
|
170
|
+
const valueName = getUniqueValueName(`${attr.name}_size`);
|
|
171
|
+
return {
|
|
172
|
+
expression: `${sizeExpr} >= :${valueName}`,
|
|
173
|
+
names,
|
|
174
|
+
values: { [`:${valueName}`]: value },
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
and: (...conditions) => ({
|
|
180
|
+
expression: '',
|
|
181
|
+
operator: 'AND',
|
|
182
|
+
children: conditions,
|
|
183
|
+
}),
|
|
184
|
+
or: (...conditions) => ({
|
|
185
|
+
expression: '',
|
|
186
|
+
operator: 'OR',
|
|
187
|
+
children: conditions,
|
|
188
|
+
}),
|
|
189
|
+
not: (condition) => ({
|
|
190
|
+
...condition,
|
|
191
|
+
isNegated: true,
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated Use createOpBuilder() instead to avoid global state.
|
|
198
|
+
* This export is maintained for backward compatibility.
|
|
199
|
+
*/
|
|
200
|
+
export const opBuilder: OpBuilder = createOpBuilder();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for DynamoDB builders
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Condition object for DynamoDB expressions
|
|
8
|
+
*/
|
|
9
|
+
export type Condition = {
|
|
10
|
+
expression: string;
|
|
11
|
+
names?: Record<string, string>;
|
|
12
|
+
values?: Record<string, any>;
|
|
13
|
+
operator?: 'AND' | 'OR';
|
|
14
|
+
isNegated?: boolean;
|
|
15
|
+
children?: Condition[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Attribute reference for building expressions
|
|
20
|
+
*/
|
|
21
|
+
export type AttrRef = {
|
|
22
|
+
name: string;
|
|
23
|
+
path?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Type-safe attribute builder for models
|
|
28
|
+
* Note: All properties are required (non-optional) to ensure type safety in expressions
|
|
29
|
+
*/
|
|
30
|
+
export type AttrBuilder<Model> = {
|
|
31
|
+
[K in keyof Model]-?: AttrRef;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Available operators for building conditions
|
|
36
|
+
*/
|
|
37
|
+
export type OpBuilder = {
|
|
38
|
+
// Comparison operators
|
|
39
|
+
eq: (attr: AttrRef, value: any) => Condition;
|
|
40
|
+
ne: (attr: AttrRef, value: any) => Condition;
|
|
41
|
+
lt: (attr: AttrRef, value: any) => Condition;
|
|
42
|
+
lte: (attr: AttrRef, value: any) => Condition;
|
|
43
|
+
gt: (attr: AttrRef, value: any) => Condition;
|
|
44
|
+
gte: (attr: AttrRef, value: any) => Condition;
|
|
45
|
+
between: (attr: AttrRef, low: any, high: any) => Condition;
|
|
46
|
+
|
|
47
|
+
// String/Set operators
|
|
48
|
+
beginsWith: (attr: AttrRef, value: string) => Condition;
|
|
49
|
+
contains: (attr: AttrRef, value: any) => Condition;
|
|
50
|
+
|
|
51
|
+
// Attribute existence operators
|
|
52
|
+
exists: (attr: AttrRef) => Condition;
|
|
53
|
+
notExists: (attr: AttrRef) => Condition;
|
|
54
|
+
|
|
55
|
+
// Type checking
|
|
56
|
+
attributeType: (
|
|
57
|
+
attr: AttrRef,
|
|
58
|
+
type: 'S' | 'N' | 'B' | 'SS' | 'NS' | 'BS' | 'M' | 'L' | 'NULL' | 'BOOL'
|
|
59
|
+
) => Condition;
|
|
60
|
+
|
|
61
|
+
// IN operator
|
|
62
|
+
in: (attr: AttrRef, values: any[]) => Condition;
|
|
63
|
+
|
|
64
|
+
// Size function
|
|
65
|
+
size: (attr: AttrRef) => SizeRef;
|
|
66
|
+
|
|
67
|
+
// Logical operators
|
|
68
|
+
and: (...conditions: Condition[]) => Condition;
|
|
69
|
+
or: (...conditions: Condition[]) => Condition;
|
|
70
|
+
not: (condition: Condition) => Condition;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reference to the size() function result for comparisons
|
|
75
|
+
*/
|
|
76
|
+
export type SizeRef = {
|
|
77
|
+
eq: (value: number) => Condition;
|
|
78
|
+
ne: (value: number) => Condition;
|
|
79
|
+
lt: (value: number) => Condition;
|
|
80
|
+
lte: (value: number) => Condition;
|
|
81
|
+
gt: (value: number) => Condition;
|
|
82
|
+
gte: (value: number) => Condition;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Base interface for executable builders
|
|
87
|
+
*/
|
|
88
|
+
export interface ExecutableBuilder<Result> {
|
|
89
|
+
dbParams(): any;
|
|
90
|
+
execute(): Promise<Result>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Base interface for operation builders with conditions
|
|
95
|
+
*/
|
|
96
|
+
export interface OperationBuilder<Model> {
|
|
97
|
+
where(fn: (attr: AttrBuilder<Model>, op: OpBuilder) => Condition): this;
|
|
98
|
+
dbParams(): any;
|
|
99
|
+
execute(): Promise<Model>;
|
|
100
|
+
}
|