dynamo-query-engine 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -7
- package/package.json +9 -2
- package/src/core/expandResolver.js +1 -1
- package/src/core/gridQueryBuilder.js +3 -3
- package/src/utils/validation.js +50 -7
- package/tests/README.md +279 -0
- package/tests/mocks/dynamoose.mock.js +124 -0
- package/tests/unit/cursor.test.js +167 -0
- package/tests/unit/gridQueryBuilder.test.js +474 -0
- package/tests/unit/validation.test.js +411 -0
- package/vitest.config.js +20 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
validateSingleSort,
|
|
8
|
+
validateFilterField,
|
|
9
|
+
validateSortField,
|
|
10
|
+
validateFilterOperator,
|
|
11
|
+
validateExpandField,
|
|
12
|
+
validatePaginationModel,
|
|
13
|
+
} from "../../src/utils/validation.js";
|
|
14
|
+
|
|
15
|
+
describe("Validation Utils", () => {
|
|
16
|
+
describe("validateSingleSort", () => {
|
|
17
|
+
it("should allow empty sort model", () => {
|
|
18
|
+
expect(() => {
|
|
19
|
+
validateSingleSort([]);
|
|
20
|
+
}).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should allow null sort model", () => {
|
|
24
|
+
expect(() => {
|
|
25
|
+
validateSingleSort(null);
|
|
26
|
+
}).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should allow undefined sort model", () => {
|
|
30
|
+
expect(() => {
|
|
31
|
+
validateSingleSort(undefined);
|
|
32
|
+
}).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should allow single sort field", () => {
|
|
36
|
+
const sortModel = [{ field: "createdAt", sort: "desc" }];
|
|
37
|
+
|
|
38
|
+
expect(() => {
|
|
39
|
+
validateSingleSort(sortModel);
|
|
40
|
+
}).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should throw error for multiple sort fields", () => {
|
|
44
|
+
const sortModel = [
|
|
45
|
+
{ field: "createdAt", sort: "desc" },
|
|
46
|
+
{ field: "status", sort: "asc" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
expect(() => {
|
|
50
|
+
validateSingleSort(sortModel);
|
|
51
|
+
}).toThrow("Multiple sort fields are not allowed");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should mention DynamoDB limitation in error", () => {
|
|
55
|
+
const sortModel = [
|
|
56
|
+
{ field: "field1", sort: "asc" },
|
|
57
|
+
{ field: "field2", sort: "desc" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
expect(() => {
|
|
61
|
+
validateSingleSort(sortModel);
|
|
62
|
+
}).toThrow(/DynamoDB/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("validateFilterField", () => {
|
|
67
|
+
const gridConfig = {
|
|
68
|
+
status: {
|
|
69
|
+
filter: {
|
|
70
|
+
type: "key",
|
|
71
|
+
operators: ["eq"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
createdAt: {
|
|
75
|
+
filter: {
|
|
76
|
+
type: "key",
|
|
77
|
+
operators: ["gte", "lte"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
it("should allow filtering on configured field", () => {
|
|
83
|
+
expect(() => {
|
|
84
|
+
validateFilterField("status", gridConfig);
|
|
85
|
+
}).not.toThrow();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should throw error for non-configured field", () => {
|
|
89
|
+
expect(() => {
|
|
90
|
+
validateFilterField("name", gridConfig);
|
|
91
|
+
}).toThrow("Filtering not allowed on field 'name'");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should throw error for field without filter config", () => {
|
|
95
|
+
const configWithoutFilter = {
|
|
96
|
+
name: {
|
|
97
|
+
sort: { type: "key" },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
expect(() => {
|
|
102
|
+
validateFilterField("name", configWithoutFilter);
|
|
103
|
+
}).toThrow("Filtering not allowed");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should provide helpful error message", () => {
|
|
107
|
+
expect(() => {
|
|
108
|
+
validateFilterField("invalidField", gridConfig);
|
|
109
|
+
}).toThrow(/grid\.filter config/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("validateSortField", () => {
|
|
114
|
+
const gridConfig = {
|
|
115
|
+
createdAt: {
|
|
116
|
+
sort: { type: "key" },
|
|
117
|
+
},
|
|
118
|
+
updatedAt: {
|
|
119
|
+
sort: { type: "key" },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
it("should allow sorting on configured field", () => {
|
|
124
|
+
expect(() => {
|
|
125
|
+
validateSortField("createdAt", gridConfig);
|
|
126
|
+
}).not.toThrow();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should throw error for non-configured field", () => {
|
|
130
|
+
expect(() => {
|
|
131
|
+
validateSortField("name", gridConfig);
|
|
132
|
+
}).toThrow("Sorting not allowed on field 'name'");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should throw error for field without sort config", () => {
|
|
136
|
+
const configWithoutSort = {
|
|
137
|
+
name: {
|
|
138
|
+
filter: { type: "key", operators: ["eq"] },
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
expect(() => {
|
|
143
|
+
validateSortField("name", configWithoutSort);
|
|
144
|
+
}).toThrow("Sorting not allowed");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should provide helpful error message", () => {
|
|
148
|
+
expect(() => {
|
|
149
|
+
validateSortField("invalidField", gridConfig);
|
|
150
|
+
}).toThrow(/grid\.sort config/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("validateFilterOperator", () => {
|
|
155
|
+
const gridConfig = {
|
|
156
|
+
status: {
|
|
157
|
+
filter: {
|
|
158
|
+
type: "key",
|
|
159
|
+
operators: ["eq"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
createdAt: {
|
|
163
|
+
filter: {
|
|
164
|
+
type: "key",
|
|
165
|
+
operators: ["gte", "lte", "between"],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
it("should allow configured operator", () => {
|
|
171
|
+
expect(() => {
|
|
172
|
+
validateFilterOperator("status", "eq", gridConfig);
|
|
173
|
+
}).not.toThrow();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should allow multiple configured operators", () => {
|
|
177
|
+
expect(() => {
|
|
178
|
+
validateFilterOperator("createdAt", "gte", gridConfig);
|
|
179
|
+
}).not.toThrow();
|
|
180
|
+
|
|
181
|
+
expect(() => {
|
|
182
|
+
validateFilterOperator("createdAt", "lte", gridConfig);
|
|
183
|
+
}).not.toThrow();
|
|
184
|
+
|
|
185
|
+
expect(() => {
|
|
186
|
+
validateFilterOperator("createdAt", "between", gridConfig);
|
|
187
|
+
}).not.toThrow();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should throw error for non-allowed operator", () => {
|
|
191
|
+
expect(() => {
|
|
192
|
+
validateFilterOperator("status", "gte", gridConfig);
|
|
193
|
+
}).toThrow("Operator 'gte' not allowed for field 'status'");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should throw error for field without filter config", () => {
|
|
197
|
+
expect(() => {
|
|
198
|
+
validateFilterOperator("nonexistent", "eq", gridConfig);
|
|
199
|
+
}).toThrow("does not have filter configuration");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should list allowed operators in error message", () => {
|
|
203
|
+
expect(() => {
|
|
204
|
+
validateFilterOperator("status", "contains", gridConfig);
|
|
205
|
+
}).toThrow(/Allowed operators: eq/);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("validateExpandField", () => {
|
|
210
|
+
const gridConfig = {
|
|
211
|
+
teamIds: {
|
|
212
|
+
expand: {
|
|
213
|
+
type: "Team",
|
|
214
|
+
relation: "MANY",
|
|
215
|
+
defaultLimit: 5,
|
|
216
|
+
maxLimit: 20,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
ownerId: {
|
|
220
|
+
expand: {
|
|
221
|
+
type: "User",
|
|
222
|
+
relation: "ONE",
|
|
223
|
+
defaultLimit: 1,
|
|
224
|
+
maxLimit: 1,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
it("should allow expanding configured field", () => {
|
|
230
|
+
expect(() => {
|
|
231
|
+
validateExpandField("teamIds", gridConfig);
|
|
232
|
+
}).not.toThrow();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should throw error for non-configured field", () => {
|
|
236
|
+
expect(() => {
|
|
237
|
+
validateExpandField("status", gridConfig);
|
|
238
|
+
}).toThrow("Expand not allowed on field 'status'");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should throw error for field without expand config", () => {
|
|
242
|
+
const configWithoutExpand = {
|
|
243
|
+
name: {
|
|
244
|
+
filter: { type: "key", operators: ["eq"] },
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
expect(() => {
|
|
249
|
+
validateExpandField("name", configWithoutExpand);
|
|
250
|
+
}).toThrow("Expand not allowed");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should provide helpful error message", () => {
|
|
254
|
+
expect(() => {
|
|
255
|
+
validateExpandField("invalidField", gridConfig);
|
|
256
|
+
}).toThrow(/grid\.expand config/);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("validatePaginationModel", () => {
|
|
261
|
+
it("should allow valid pagination model", () => {
|
|
262
|
+
expect(() => {
|
|
263
|
+
validatePaginationModel({ pageSize: 20 });
|
|
264
|
+
}).not.toThrow();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should allow pageSize of 1", () => {
|
|
268
|
+
expect(() => {
|
|
269
|
+
validatePaginationModel({ pageSize: 1 });
|
|
270
|
+
}).not.toThrow();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("should allow pageSize up to 1000", () => {
|
|
274
|
+
expect(() => {
|
|
275
|
+
validatePaginationModel({ pageSize: 1000 });
|
|
276
|
+
}).not.toThrow();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should throw error when paginationModel is missing", () => {
|
|
280
|
+
expect(() => {
|
|
281
|
+
validatePaginationModel(null);
|
|
282
|
+
}).toThrow("paginationModel is required");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should throw error when pageSize is missing", () => {
|
|
286
|
+
expect(() => {
|
|
287
|
+
validatePaginationModel({});
|
|
288
|
+
}).toThrow("pageSize must be a positive number");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should throw error when pageSize is zero", () => {
|
|
292
|
+
expect(() => {
|
|
293
|
+
validatePaginationModel({ pageSize: 0 });
|
|
294
|
+
}).toThrow("pageSize must be a positive number");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("should throw error when pageSize is negative", () => {
|
|
298
|
+
expect(() => {
|
|
299
|
+
validatePaginationModel({ pageSize: -5 });
|
|
300
|
+
}).toThrow("pageSize must be a positive number");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should throw error when pageSize exceeds 1000", () => {
|
|
304
|
+
expect(() => {
|
|
305
|
+
validatePaginationModel({ pageSize: 1001 });
|
|
306
|
+
}).toThrow("pageSize cannot exceed 1000");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("should mention DynamoDB limit in error", () => {
|
|
310
|
+
expect(() => {
|
|
311
|
+
validatePaginationModel({ pageSize: 2000 });
|
|
312
|
+
}).toThrow(/DynamoDB limit/);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("String to Number Conversion", () => {
|
|
316
|
+
it("should convert pageSize string to number", () => {
|
|
317
|
+
const paginationModel = { pageSize: "25" };
|
|
318
|
+
validatePaginationModel(paginationModel);
|
|
319
|
+
expect(paginationModel.pageSize).toBe(25);
|
|
320
|
+
expect(typeof paginationModel.pageSize).toBe("number");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should convert page string to number", () => {
|
|
324
|
+
const paginationModel = { pageSize: 25, page: "1" };
|
|
325
|
+
validatePaginationModel(paginationModel);
|
|
326
|
+
expect(paginationModel.page).toBe(1);
|
|
327
|
+
expect(typeof paginationModel.page).toBe("number");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("should convert both pageSize and page strings to numbers", () => {
|
|
331
|
+
const paginationModel = { pageSize: "50", page: "2" };
|
|
332
|
+
validatePaginationModel(paginationModel);
|
|
333
|
+
expect(paginationModel.pageSize).toBe(50);
|
|
334
|
+
expect(paginationModel.page).toBe(2);
|
|
335
|
+
expect(typeof paginationModel.pageSize).toBe("number");
|
|
336
|
+
expect(typeof paginationModel.page).toBe("number");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should throw error for invalid pageSize string", () => {
|
|
340
|
+
expect(() => {
|
|
341
|
+
validatePaginationModel({ pageSize: "abc" });
|
|
342
|
+
}).toThrow("must be a valid number");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should throw error for invalid page string", () => {
|
|
346
|
+
expect(() => {
|
|
347
|
+
validatePaginationModel({ pageSize: 25, page: "invalid" });
|
|
348
|
+
}).toThrow("must be a valid number");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("should throw error for non-string, non-number pageSize", () => {
|
|
352
|
+
expect(() => {
|
|
353
|
+
validatePaginationModel({ pageSize: true });
|
|
354
|
+
}).toThrow("must be a number");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should throw error for object pageSize", () => {
|
|
358
|
+
expect(() => {
|
|
359
|
+
validatePaginationModel({ pageSize: { value: 25 } });
|
|
360
|
+
}).toThrow("must be a number");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should validate converted string pageSize against limits", () => {
|
|
364
|
+
expect(() => {
|
|
365
|
+
validatePaginationModel({ pageSize: "1001" });
|
|
366
|
+
}).toThrow("cannot exceed 1000");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("should validate converted string pageSize is positive", () => {
|
|
370
|
+
expect(() => {
|
|
371
|
+
validatePaginationModel({ pageSize: "0" });
|
|
372
|
+
}).toThrow("must be a positive number");
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("Edge Cases", () => {
|
|
378
|
+
it("should handle empty grid config", () => {
|
|
379
|
+
const emptyConfig = {};
|
|
380
|
+
|
|
381
|
+
expect(() => {
|
|
382
|
+
validateFilterField("anyField", emptyConfig);
|
|
383
|
+
}).toThrow();
|
|
384
|
+
|
|
385
|
+
expect(() => {
|
|
386
|
+
validateSortField("anyField", emptyConfig);
|
|
387
|
+
}).toThrow();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should handle config with mixed field types", () => {
|
|
391
|
+
const mixedConfig = {
|
|
392
|
+
field1: {
|
|
393
|
+
filter: { type: "key", operators: ["eq"] },
|
|
394
|
+
},
|
|
395
|
+
field2: {
|
|
396
|
+
sort: { type: "key" },
|
|
397
|
+
},
|
|
398
|
+
field3: {
|
|
399
|
+
expand: { type: "Model", relation: "ONE", defaultLimit: 1, maxLimit: 1 },
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
expect(() => validateFilterField("field1", mixedConfig)).not.toThrow();
|
|
404
|
+
expect(() => validateSortField("field2", mixedConfig)).not.toThrow();
|
|
405
|
+
expect(() => validateExpandField("field3", mixedConfig)).not.toThrow();
|
|
406
|
+
|
|
407
|
+
expect(() => validateFilterField("field2", mixedConfig)).toThrow();
|
|
408
|
+
expect(() => validateSortField("field1", mixedConfig)).toThrow();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "node",
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: "v8",
|
|
9
|
+
reporter: ["text", "json", "html"],
|
|
10
|
+
include: ["src/**/*.js"],
|
|
11
|
+
exclude: ["src/types/**", "examples/**", "tests/**"],
|
|
12
|
+
thresholds: {
|
|
13
|
+
lines: 50,
|
|
14
|
+
functions: 50,
|
|
15
|
+
branches: 50,
|
|
16
|
+
statements: 50,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|