@aws-blocks/bb-distributed-table 0.1.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/LICENSE +174 -0
- package/README.md +292 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/gsi-manager-lambda/index.js +221 -0
- package/dist/gsi-manager-lambda.d.ts +26 -0
- package/dist/gsi-manager-lambda.d.ts.map +1 -0
- package/dist/gsi-manager-lambda.js +244 -0
- package/dist/index.aws.d.ts +59 -0
- package/dist/index.aws.d.ts.map +1 -0
- package/dist/index.aws.js +310 -0
- package/dist/index.browser.d.ts +5 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +7 -0
- package/dist/index.cdk.d.ts +27 -0
- package/dist/index.cdk.d.ts.map +1 -0
- package/dist/index.cdk.js +180 -0
- package/dist/index.cdk.test.d.ts +2 -0
- package/dist/index.cdk.test.d.ts.map +1 -0
- package/dist/index.cdk.test.js +93 -0
- package/dist/index.mock.d.ts +101 -0
- package/dist/index.mock.d.ts.map +1 -0
- package/dist/index.mock.js +301 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +555 -0
- package/dist/parity.test.d.ts +2 -0
- package/dist/parity.test.d.ts.map +1 -0
- package/dist/parity.test.js +557 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +49 -0
- package/src/errors.ts +145 -0
- package/src/gsi-manager-lambda.ts +305 -0
- package/src/index.aws.ts +400 -0
- package/src/index.browser.ts +8 -0
- package/src/index.cdk.test.ts +107 -0
- package/src/index.cdk.ts +220 -0
- package/src/index.mock.ts +363 -0
- package/src/index.test.ts +657 -0
- package/src/parity.test.ts +763 -0
- package/src/types.ts +163 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { test, describe } from 'node:test';
|
|
4
|
+
import { strict as assert } from 'node:assert';
|
|
5
|
+
import { DistributedTable, DistributedTableErrors } from './index.mock.js';
|
|
6
|
+
import { Scope } from '@aws-blocks/core';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
// ── Schemas ─────────────────────────────────────────────────────────────────
|
|
9
|
+
const userSchema = z.object({
|
|
10
|
+
userId: z.string(),
|
|
11
|
+
email: z.string().email(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
createdAt: z.number(),
|
|
14
|
+
});
|
|
15
|
+
const fileSchema = z.object({
|
|
16
|
+
userId: z.string(),
|
|
17
|
+
path: z.string(),
|
|
18
|
+
data: z.string(),
|
|
19
|
+
});
|
|
20
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
21
|
+
let scopeCounter = 0;
|
|
22
|
+
function testScope() {
|
|
23
|
+
return new Scope(`dt-test-${++scopeCounter}-${Date.now()}`);
|
|
24
|
+
}
|
|
25
|
+
async function collect(iter) {
|
|
26
|
+
const items = [];
|
|
27
|
+
for await (const item of iter)
|
|
28
|
+
items.push(item);
|
|
29
|
+
return items;
|
|
30
|
+
}
|
|
31
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
32
|
+
describe('DistributedTable', () => {
|
|
33
|
+
// ── CRUD ────────────────────────────────────────────────────────────────
|
|
34
|
+
describe('CRUD', () => {
|
|
35
|
+
test('put and get', async () => {
|
|
36
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
37
|
+
schema: userSchema,
|
|
38
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
39
|
+
});
|
|
40
|
+
const user = { userId: 'user1', email: 'test@example.com', name: 'Test', createdAt: 1000 };
|
|
41
|
+
await table.put(user);
|
|
42
|
+
assert.deepEqual(await table.get({ userId: 'user1', createdAt: 1000 }), user);
|
|
43
|
+
});
|
|
44
|
+
test('get returns null for missing item', async () => {
|
|
45
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
46
|
+
schema: userSchema,
|
|
47
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
48
|
+
});
|
|
49
|
+
assert.equal(await table.get({ userId: 'nope', createdAt: 0 }), null);
|
|
50
|
+
});
|
|
51
|
+
test('put overwrites existing item', async () => {
|
|
52
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
53
|
+
schema: userSchema,
|
|
54
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
55
|
+
});
|
|
56
|
+
const user = { userId: 'user1', email: 'test@example.com', name: 'Original', createdAt: 1000 };
|
|
57
|
+
await table.put(user);
|
|
58
|
+
await table.put({ ...user, name: 'Updated' });
|
|
59
|
+
assert.equal((await table.get({ userId: 'user1', createdAt: 1000 }))?.name, 'Updated');
|
|
60
|
+
});
|
|
61
|
+
test('delete removes item', async () => {
|
|
62
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
63
|
+
schema: userSchema,
|
|
64
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
65
|
+
});
|
|
66
|
+
await table.put({ userId: 'user1', email: 'test@example.com', name: 'Test', createdAt: 1000 });
|
|
67
|
+
await table.delete({ userId: 'user1', createdAt: 1000 });
|
|
68
|
+
assert.equal(await table.get({ userId: 'user1', createdAt: 1000 }), null);
|
|
69
|
+
});
|
|
70
|
+
test('partition key only (no sort key)', async () => {
|
|
71
|
+
const schema = z.object({ id: z.string(), value: z.string() });
|
|
72
|
+
const table = new DistributedTable(testScope(), 'simple', {
|
|
73
|
+
schema,
|
|
74
|
+
key: { partitionKey: 'id' },
|
|
75
|
+
});
|
|
76
|
+
await table.put({ id: 'item1', value: 'test' });
|
|
77
|
+
assert.equal((await table.get({ id: 'item1' }))?.value, 'test');
|
|
78
|
+
await table.delete({ id: 'item1' });
|
|
79
|
+
assert.equal(await table.get({ id: 'item1' }), null);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// ── Conditional put ─────────────────────────────────────────────────────
|
|
83
|
+
describe('conditional put', () => {
|
|
84
|
+
test('ifNotExists succeeds on new item', async () => {
|
|
85
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
86
|
+
schema: userSchema,
|
|
87
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
88
|
+
});
|
|
89
|
+
const user = { userId: 'user1', email: 'test@example.com', name: 'Test', createdAt: 1000 };
|
|
90
|
+
await table.put(user, { ifNotExists: true });
|
|
91
|
+
assert.deepEqual(await table.get({ userId: 'user1', createdAt: 1000 }), user);
|
|
92
|
+
});
|
|
93
|
+
test('ifNotExists fails on existing item', async () => {
|
|
94
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
95
|
+
schema: userSchema,
|
|
96
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
97
|
+
});
|
|
98
|
+
const user = { userId: 'user1', email: 'test@example.com', name: 'Test', createdAt: 1000 };
|
|
99
|
+
await table.put(user);
|
|
100
|
+
await assert.rejects(() => table.put(user, { ifNotExists: true }), (err) => err.name === DistributedTableErrors.ConditionalCheckFailed);
|
|
101
|
+
});
|
|
102
|
+
test('ifFieldEquals succeeds when field matches', async () => {
|
|
103
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
104
|
+
schema: userSchema,
|
|
105
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
106
|
+
});
|
|
107
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'Test', createdAt: 1000 });
|
|
108
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'Updated', createdAt: 1000 }, { ifFieldEquals: { name: 'Test' } });
|
|
109
|
+
assert.equal((await table.get({ userId: 'u1', createdAt: 1000 }))?.name, 'Updated');
|
|
110
|
+
});
|
|
111
|
+
test('ifFieldEquals fails when field does not match', async () => {
|
|
112
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
113
|
+
schema: userSchema,
|
|
114
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
115
|
+
});
|
|
116
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'Test', createdAt: 1000 });
|
|
117
|
+
await assert.rejects(() => table.put({ userId: 'u1', email: 'a@b.com', name: 'Fail', createdAt: 1000 }, { ifFieldEquals: { name: 'Wrong' } }), (err) => err.name === DistributedTableErrors.ConditionalCheckFailed);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// ── Conditional delete ──────────────────────────────────────────────────
|
|
121
|
+
describe('conditional delete', () => {
|
|
122
|
+
test('ifExists succeeds when item exists', async () => {
|
|
123
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
124
|
+
schema: userSchema,
|
|
125
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
126
|
+
});
|
|
127
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'Test', createdAt: 1000 });
|
|
128
|
+
await table.delete({ userId: 'u1', createdAt: 1000 }, { ifExists: true });
|
|
129
|
+
assert.equal(await table.get({ userId: 'u1', createdAt: 1000 }), null);
|
|
130
|
+
});
|
|
131
|
+
test('ifExists fails when item does not exist', async () => {
|
|
132
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
133
|
+
schema: userSchema,
|
|
134
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
135
|
+
});
|
|
136
|
+
await assert.rejects(() => table.delete({ userId: 'u1', createdAt: 1000 }, { ifExists: true }), (err) => err.name === DistributedTableErrors.ConditionalCheckFailed);
|
|
137
|
+
});
|
|
138
|
+
test('ifFieldEquals succeeds when field matches', async () => {
|
|
139
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
140
|
+
schema: userSchema,
|
|
141
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
142
|
+
});
|
|
143
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'Test', createdAt: 1000 });
|
|
144
|
+
await table.delete({ userId: 'u1', createdAt: 1000 }, { ifFieldEquals: { name: 'Test' } });
|
|
145
|
+
assert.equal(await table.get({ userId: 'u1', createdAt: 1000 }), null);
|
|
146
|
+
});
|
|
147
|
+
test('ifFieldEquals fails when field does not match', async () => {
|
|
148
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
149
|
+
schema: userSchema,
|
|
150
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
151
|
+
});
|
|
152
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'Test', createdAt: 1000 });
|
|
153
|
+
await assert.rejects(() => table.delete({ userId: 'u1', createdAt: 1000 }, { ifFieldEquals: { name: 'Wrong' } }), (err) => err.name === DistributedTableErrors.ConditionalCheckFailed);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
// ── Schema validation ───────────────────────────────────────────────────
|
|
157
|
+
describe('schema validation', () => {
|
|
158
|
+
test('rejects invalid item on put', async () => {
|
|
159
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
160
|
+
schema: userSchema,
|
|
161
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
162
|
+
});
|
|
163
|
+
await assert.rejects(() => table.put({ userId: 'u1', email: 'invalid-email', name: 'Test', createdAt: 1000 }), (err) => err.name === 'ValidationFailedException');
|
|
164
|
+
});
|
|
165
|
+
test('rejects invalid item on putBatch', async () => {
|
|
166
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
167
|
+
schema: userSchema,
|
|
168
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
169
|
+
});
|
|
170
|
+
await assert.rejects(() => table.putBatch([
|
|
171
|
+
{ userId: 'u1', email: 'ok@example.com', name: 'Good', createdAt: 1000 },
|
|
172
|
+
{ userId: 'u2', email: 'bad-email', name: 'Bad', createdAt: 2000 },
|
|
173
|
+
]), (err) => err.name === 'ValidationFailedException');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
// ── Query: numeric sort key ─────────────────────────────────────────────
|
|
177
|
+
describe('query (numeric sort key)', () => {
|
|
178
|
+
function numTable() {
|
|
179
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
180
|
+
schema: userSchema,
|
|
181
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
182
|
+
indexes: { byUser: { partitionKey: 'userId', sortKey: 'createdAt' } },
|
|
183
|
+
});
|
|
184
|
+
return table;
|
|
185
|
+
}
|
|
186
|
+
async function seedNumeric(table) {
|
|
187
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'A', createdAt: 1000 });
|
|
188
|
+
await table.put({ userId: 'u1', email: 'b@b.com', name: 'B', createdAt: 2000 });
|
|
189
|
+
await table.put({ userId: 'u1', email: 'c@b.com', name: 'C', createdAt: 3000 });
|
|
190
|
+
await table.put({ userId: 'u1', email: 'd@b.com', name: 'D', createdAt: 4000 });
|
|
191
|
+
await table.put({ userId: 'u1', email: 'e@b.com', name: 'E', createdAt: 5000 });
|
|
192
|
+
}
|
|
193
|
+
test('no filter — returns all items sorted', async () => {
|
|
194
|
+
const table = numTable();
|
|
195
|
+
await seedNumeric(table);
|
|
196
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' } } }));
|
|
197
|
+
assert.equal(items.length, 5);
|
|
198
|
+
assert.deepEqual(items.map(i => i.createdAt), [1000, 2000, 3000, 4000, 5000]);
|
|
199
|
+
});
|
|
200
|
+
test('equals', async () => {
|
|
201
|
+
const table = numTable();
|
|
202
|
+
await seedNumeric(table);
|
|
203
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { equals: 3000 } } }));
|
|
204
|
+
assert.equal(items.length, 1);
|
|
205
|
+
assert.equal(items[0].name, 'C');
|
|
206
|
+
});
|
|
207
|
+
test('greaterThan', async () => {
|
|
208
|
+
const table = numTable();
|
|
209
|
+
await seedNumeric(table);
|
|
210
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { greaterThan: 3000 } } }));
|
|
211
|
+
assert.equal(items.length, 2);
|
|
212
|
+
assert.deepEqual(items.map(i => i.createdAt), [4000, 5000]);
|
|
213
|
+
});
|
|
214
|
+
test('greaterThanOrEqual', async () => {
|
|
215
|
+
const table = numTable();
|
|
216
|
+
await seedNumeric(table);
|
|
217
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { greaterThanOrEqual: 3000 } } }));
|
|
218
|
+
assert.equal(items.length, 3);
|
|
219
|
+
assert.deepEqual(items.map(i => i.createdAt), [3000, 4000, 5000]);
|
|
220
|
+
});
|
|
221
|
+
test('lessThan', async () => {
|
|
222
|
+
const table = numTable();
|
|
223
|
+
await seedNumeric(table);
|
|
224
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { lessThan: 3000 } } }));
|
|
225
|
+
assert.equal(items.length, 2);
|
|
226
|
+
assert.deepEqual(items.map(i => i.createdAt), [1000, 2000]);
|
|
227
|
+
});
|
|
228
|
+
test('lessThanOrEqual', async () => {
|
|
229
|
+
const table = numTable();
|
|
230
|
+
await seedNumeric(table);
|
|
231
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { lessThanOrEqual: 3000 } } }));
|
|
232
|
+
assert.equal(items.length, 3);
|
|
233
|
+
assert.deepEqual(items.map(i => i.createdAt), [1000, 2000, 3000]);
|
|
234
|
+
});
|
|
235
|
+
test('between (inclusive)', async () => {
|
|
236
|
+
const table = numTable();
|
|
237
|
+
await seedNumeric(table);
|
|
238
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { between: [2000, 4000] } } }));
|
|
239
|
+
assert.equal(items.length, 3);
|
|
240
|
+
assert.deepEqual(items.map(i => i.createdAt), [2000, 3000, 4000]);
|
|
241
|
+
});
|
|
242
|
+
test('between — single match', async () => {
|
|
243
|
+
const table = numTable();
|
|
244
|
+
await seedNumeric(table);
|
|
245
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { between: [2500, 3500] } } }));
|
|
246
|
+
assert.equal(items.length, 1);
|
|
247
|
+
assert.equal(items[0].createdAt, 3000);
|
|
248
|
+
});
|
|
249
|
+
test('between — no match', async () => {
|
|
250
|
+
const table = numTable();
|
|
251
|
+
await seedNumeric(table);
|
|
252
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { between: [5500, 6000] } } }));
|
|
253
|
+
assert.equal(items.length, 0);
|
|
254
|
+
});
|
|
255
|
+
test('limit', async () => {
|
|
256
|
+
const table = numTable();
|
|
257
|
+
await seedNumeric(table);
|
|
258
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' } }, limit: 2 }));
|
|
259
|
+
assert.equal(items.length, 2);
|
|
260
|
+
assert.deepEqual(items.map(i => i.createdAt), [1000, 2000]);
|
|
261
|
+
});
|
|
262
|
+
test('filter + limit combined', async () => {
|
|
263
|
+
const table = numTable();
|
|
264
|
+
await seedNumeric(table);
|
|
265
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, createdAt: { greaterThan: 1000 } }, limit: 2 }));
|
|
266
|
+
assert.equal(items.length, 2);
|
|
267
|
+
assert.deepEqual(items.map(i => i.createdAt), [2000, 3000]);
|
|
268
|
+
});
|
|
269
|
+
test('partition isolation — different pk returns nothing', async () => {
|
|
270
|
+
const table = numTable();
|
|
271
|
+
await seedNumeric(table);
|
|
272
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'other' } } }));
|
|
273
|
+
assert.equal(items.length, 0);
|
|
274
|
+
});
|
|
275
|
+
test('nonexistent index throws', async () => {
|
|
276
|
+
const table = numTable();
|
|
277
|
+
await assert.rejects(
|
|
278
|
+
// @ts-expect-error — 'nonexistent' is not a defined index name
|
|
279
|
+
async () => { for await (const _ of table.query({ index: 'nonexistent', where: { userId: { equals: 'u1' } } })) { } }, /Index 'nonexistent' not found/);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
// ── Query: string sort key ──────────────────────────────────────────────
|
|
283
|
+
describe('query (string sort key)', () => {
|
|
284
|
+
function strTable() {
|
|
285
|
+
return new DistributedTable(testScope(), 'files', {
|
|
286
|
+
schema: fileSchema,
|
|
287
|
+
key: { partitionKey: 'userId', sortKey: 'path' },
|
|
288
|
+
indexes: { byUser: { partitionKey: 'userId', sortKey: 'path' } },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function seedStrings(table) {
|
|
292
|
+
await table.put({ userId: 'u1', path: '/docs/a.txt', data: 'a' });
|
|
293
|
+
await table.put({ userId: 'u1', path: '/docs/b.txt', data: 'b' });
|
|
294
|
+
await table.put({ userId: 'u1', path: '/images/cat.jpg', data: 'c' });
|
|
295
|
+
await table.put({ userId: 'u1', path: '/images/dog.jpg', data: 'd' });
|
|
296
|
+
await table.put({ userId: 'u1', path: '/videos/clip.mp4', data: 'e' });
|
|
297
|
+
}
|
|
298
|
+
test('no filter — returns all sorted lexicographically', async () => {
|
|
299
|
+
const table = strTable();
|
|
300
|
+
await seedStrings(table);
|
|
301
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' } } }));
|
|
302
|
+
assert.equal(items.length, 5);
|
|
303
|
+
// Lexicographic: /docs/a < /docs/b < /images/cat < /images/dog < /videos/clip
|
|
304
|
+
assert.deepEqual(items.map(i => i.path), [
|
|
305
|
+
'/docs/a.txt', '/docs/b.txt', '/images/cat.jpg', '/images/dog.jpg', '/videos/clip.mp4',
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
test('equals', async () => {
|
|
309
|
+
const table = strTable();
|
|
310
|
+
await seedStrings(table);
|
|
311
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { equals: '/docs/a.txt' } } }));
|
|
312
|
+
assert.equal(items.length, 1);
|
|
313
|
+
assert.equal(items[0].data, 'a');
|
|
314
|
+
});
|
|
315
|
+
test('beginsWith', async () => {
|
|
316
|
+
const table = strTable();
|
|
317
|
+
await seedStrings(table);
|
|
318
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { beginsWith: '/docs/' } } }));
|
|
319
|
+
assert.equal(items.length, 2);
|
|
320
|
+
assert.ok(items.every(i => i.path.startsWith('/docs/')));
|
|
321
|
+
});
|
|
322
|
+
test('beginsWith — no match', async () => {
|
|
323
|
+
const table = strTable();
|
|
324
|
+
await seedStrings(table);
|
|
325
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { beginsWith: '/music/' } } }));
|
|
326
|
+
assert.equal(items.length, 0);
|
|
327
|
+
});
|
|
328
|
+
test('greaterThan (lexicographic)', async () => {
|
|
329
|
+
const table = strTable();
|
|
330
|
+
await seedStrings(table);
|
|
331
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { greaterThan: '/images/' } } }));
|
|
332
|
+
assert.equal(items.length, 3);
|
|
333
|
+
});
|
|
334
|
+
test('lessThan (lexicographic)', async () => {
|
|
335
|
+
const table = strTable();
|
|
336
|
+
await seedStrings(table);
|
|
337
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { lessThan: '/images/' } } }));
|
|
338
|
+
assert.equal(items.length, 2);
|
|
339
|
+
});
|
|
340
|
+
test('between (lexicographic, inclusive)', async () => {
|
|
341
|
+
const table = strTable();
|
|
342
|
+
await seedStrings(table);
|
|
343
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { between: ['/docs/', '/images/d'] } } }));
|
|
344
|
+
assert.equal(items.length, 3);
|
|
345
|
+
});
|
|
346
|
+
test('limit', async () => {
|
|
347
|
+
const table = strTable();
|
|
348
|
+
await seedStrings(table);
|
|
349
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' } }, limit: 3 }));
|
|
350
|
+
assert.equal(items.length, 3);
|
|
351
|
+
});
|
|
352
|
+
test('beginsWith + limit combined', async () => {
|
|
353
|
+
const table = strTable();
|
|
354
|
+
await seedStrings(table);
|
|
355
|
+
const items = await collect(table.query({ index: 'byUser', where: { userId: { equals: 'u1' }, path: { beginsWith: '/images/' } }, limit: 1 }));
|
|
356
|
+
assert.equal(items.length, 1);
|
|
357
|
+
assert.ok(items[0].path.startsWith('/images/'));
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
// ── Primary key query ───────────────────────────────────────────────────
|
|
361
|
+
describe('query (primary key)', () => {
|
|
362
|
+
function pkTable() {
|
|
363
|
+
return new DistributedTable(testScope(), 'pk-query', {
|
|
364
|
+
schema: fileSchema,
|
|
365
|
+
key: { partitionKey: 'userId', sortKey: 'path' },
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function seed(table) {
|
|
369
|
+
await table.put({ userId: 'u1', path: '/docs/a.txt', data: 'a' });
|
|
370
|
+
await table.put({ userId: 'u1', path: '/docs/b.txt', data: 'b' });
|
|
371
|
+
await table.put({ userId: 'u1', path: '/images/c.png', data: 'c' });
|
|
372
|
+
await table.put({ userId: 'u2', path: '/docs/d.txt', data: 'd' });
|
|
373
|
+
}
|
|
374
|
+
test('returns all items for a partition key', async () => {
|
|
375
|
+
const table = pkTable();
|
|
376
|
+
await seed(table);
|
|
377
|
+
const items = await collect(table.query({ where: { userId: { equals: 'u1' } } }));
|
|
378
|
+
assert.equal(items.length, 3);
|
|
379
|
+
for (const item of items)
|
|
380
|
+
assert.equal(item.userId, 'u1');
|
|
381
|
+
});
|
|
382
|
+
test('supports sort key conditions', async () => {
|
|
383
|
+
const table = pkTable();
|
|
384
|
+
await seed(table);
|
|
385
|
+
const items = await collect(table.query({ where: { userId: { equals: 'u1' }, path: { beginsWith: '/docs/' } } }));
|
|
386
|
+
assert.equal(items.length, 2);
|
|
387
|
+
for (const item of items)
|
|
388
|
+
assert.ok(item.path.startsWith('/docs/'));
|
|
389
|
+
});
|
|
390
|
+
test('returns empty for non-existent partition key', async () => {
|
|
391
|
+
const table = pkTable();
|
|
392
|
+
await seed(table);
|
|
393
|
+
const items = await collect(table.query({ where: { userId: { equals: 'nobody' } } }));
|
|
394
|
+
assert.equal(items.length, 0);
|
|
395
|
+
});
|
|
396
|
+
test('order desc reverses sort key order', async () => {
|
|
397
|
+
const table = pkTable();
|
|
398
|
+
await seed(table);
|
|
399
|
+
const items = await collect(table.query({ where: { userId: { equals: 'u1' } }, order: 'desc' }));
|
|
400
|
+
assert.equal(items.length, 3);
|
|
401
|
+
assert.ok(items[0].path > items[1].path);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
// ── Scan ────────────────────────────────────────────────────────────────
|
|
405
|
+
describe('scan', () => {
|
|
406
|
+
test('returns all items', async () => {
|
|
407
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
408
|
+
schema: userSchema,
|
|
409
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
410
|
+
});
|
|
411
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'A', createdAt: 1000 });
|
|
412
|
+
await table.put({ userId: 'u2', email: 'b@b.com', name: 'B', createdAt: 2000 });
|
|
413
|
+
await table.put({ userId: 'u3', email: 'c@b.com', name: 'C', createdAt: 3000 });
|
|
414
|
+
assert.equal((await collect(table.scan())).length, 3);
|
|
415
|
+
});
|
|
416
|
+
test('respects limit', async () => {
|
|
417
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
418
|
+
schema: userSchema,
|
|
419
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
420
|
+
});
|
|
421
|
+
await table.put({ userId: 'u1', email: 'a@b.com', name: 'A', createdAt: 1000 });
|
|
422
|
+
await table.put({ userId: 'u2', email: 'b@b.com', name: 'B', createdAt: 2000 });
|
|
423
|
+
await table.put({ userId: 'u3', email: 'c@b.com', name: 'C', createdAt: 3000 });
|
|
424
|
+
assert.equal((await collect(table.scan({ limit: 2 }))).length, 2);
|
|
425
|
+
});
|
|
426
|
+
test('empty table returns nothing', async () => {
|
|
427
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
428
|
+
schema: userSchema,
|
|
429
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
430
|
+
});
|
|
431
|
+
assert.equal((await collect(table.scan())).length, 0);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
// ── Batch operations ────────────────────────────────────────────────────
|
|
435
|
+
describe('batch operations', () => {
|
|
436
|
+
test('putBatch and getBatch', async () => {
|
|
437
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
438
|
+
schema: userSchema,
|
|
439
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
440
|
+
});
|
|
441
|
+
const items = [
|
|
442
|
+
{ userId: 'u1', email: 'a@b.com', name: 'A', createdAt: 1000 },
|
|
443
|
+
{ userId: 'u2', email: 'b@b.com', name: 'B', createdAt: 2000 },
|
|
444
|
+
{ userId: 'u3', email: 'c@b.com', name: 'C', createdAt: 3000 },
|
|
445
|
+
];
|
|
446
|
+
await table.putBatch(items);
|
|
447
|
+
const results = await table.getBatch([
|
|
448
|
+
{ userId: 'u1', createdAt: 1000 },
|
|
449
|
+
{ userId: 'u2', createdAt: 2000 },
|
|
450
|
+
{ userId: 'missing', createdAt: 9999 },
|
|
451
|
+
]);
|
|
452
|
+
assert.equal(results.length, 3);
|
|
453
|
+
assert.deepEqual(results[0], items[0]);
|
|
454
|
+
assert.deepEqual(results[1], items[1]);
|
|
455
|
+
assert.equal(results[2], null);
|
|
456
|
+
});
|
|
457
|
+
test('deleteBatch', async () => {
|
|
458
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
459
|
+
schema: userSchema,
|
|
460
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
461
|
+
});
|
|
462
|
+
await table.putBatch([
|
|
463
|
+
{ userId: 'u1', email: 'a@b.com', name: 'A', createdAt: 1000 },
|
|
464
|
+
{ userId: 'u2', email: 'b@b.com', name: 'B', createdAt: 2000 },
|
|
465
|
+
]);
|
|
466
|
+
await table.deleteBatch([
|
|
467
|
+
{ userId: 'u1', createdAt: 1000 },
|
|
468
|
+
{ userId: 'u2', createdAt: 2000 },
|
|
469
|
+
]);
|
|
470
|
+
assert.equal(await table.get({ userId: 'u1', createdAt: 1000 }), null);
|
|
471
|
+
assert.equal(await table.get({ userId: 'u2', createdAt: 2000 }), null);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
// ── Error constants ─────────────────────────────────────────────────────
|
|
475
|
+
describe('error constants', () => {
|
|
476
|
+
test('DistributedTableErrors has expected values', () => {
|
|
477
|
+
assert.equal(DistributedTableErrors.ConditionalCheckFailed, 'ConditionalCheckFailedException');
|
|
478
|
+
assert.equal(DistributedTableErrors.ValidationFailed, 'ValidationFailedException');
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
// ── TypeScript type safety ──────────────────────────────────────────────
|
|
482
|
+
describe('type safety', () => {
|
|
483
|
+
test('key config rejects non-existent field names', () => {
|
|
484
|
+
new DistributedTable(testScope(), 'bad', {
|
|
485
|
+
schema: userSchema,
|
|
486
|
+
// @ts-expect-error — 'nonExistent' is not a field in the schema
|
|
487
|
+
key: { partitionKey: 'nonExistent' },
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
test('key config rejects non-existent sort key field', () => {
|
|
491
|
+
new DistributedTable(testScope(), 'bad', {
|
|
492
|
+
schema: userSchema,
|
|
493
|
+
// @ts-expect-error — 'badField' is not a field in the schema
|
|
494
|
+
key: { partitionKey: 'userId', sortKey: 'badField' },
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
test('index config rejects non-existent field names', () => {
|
|
498
|
+
new DistributedTable(testScope(), 'bad', {
|
|
499
|
+
schema: userSchema,
|
|
500
|
+
key: { partitionKey: 'userId' },
|
|
501
|
+
// @ts-expect-error — 'fake' is not a field in the schema
|
|
502
|
+
indexes: { byFake: { partitionKey: 'fake' } },
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
test('put rejects items missing required fields', () => {
|
|
506
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
507
|
+
schema: userSchema,
|
|
508
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
509
|
+
});
|
|
510
|
+
// @ts-expect-error — missing 'name' and 'createdAt'
|
|
511
|
+
const _badItem = { userId: 'u1', email: 'a@b.com' };
|
|
512
|
+
});
|
|
513
|
+
test('ifFieldEquals rejects non-schema fields', () => {
|
|
514
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
515
|
+
schema: userSchema,
|
|
516
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
517
|
+
});
|
|
518
|
+
// @ts-expect-error — 'nonField' is not in the schema
|
|
519
|
+
const _badOpts = { ifFieldEquals: { nonField: 'value' } };
|
|
520
|
+
});
|
|
521
|
+
test('get rejects empty key object', () => {
|
|
522
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
523
|
+
schema: userSchema,
|
|
524
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
525
|
+
});
|
|
526
|
+
// @ts-expect-error — empty object is missing required key fields
|
|
527
|
+
const _badKey = {};
|
|
528
|
+
});
|
|
529
|
+
test('get rejects key missing sort key', () => {
|
|
530
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
531
|
+
schema: userSchema,
|
|
532
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
533
|
+
});
|
|
534
|
+
// @ts-expect-error — missing 'createdAt' sort key
|
|
535
|
+
const _badKey = { userId: 'u1' };
|
|
536
|
+
});
|
|
537
|
+
test('delete rejects empty key object', () => {
|
|
538
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
539
|
+
schema: userSchema,
|
|
540
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
541
|
+
});
|
|
542
|
+
// @ts-expect-error — empty object is missing required key fields
|
|
543
|
+
const _badKey = {};
|
|
544
|
+
});
|
|
545
|
+
test('query rejects at runtime for nonexistent index', async () => {
|
|
546
|
+
const table = new DistributedTable(testScope(), 'users', {
|
|
547
|
+
schema: userSchema,
|
|
548
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
549
|
+
});
|
|
550
|
+
await assert.rejects(
|
|
551
|
+
// @ts-expect-error — table has no indexes, testing runtime rejection
|
|
552
|
+
async () => { for await (const _ of table.query({ index: 'doesNotExist', where: { userId: { equals: 'u1' } } })) { } }, /Index 'doesNotExist' not found/);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parity.test.d.ts","sourceRoot":"","sources":["../src/parity.test.ts"],"names":[],"mappings":""}
|