@reldens/storage 0.116.0 → 0.118.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.
Files changed (86) hide show
  1. package/.claude/test-architecture.md +27 -17
  2. package/CLAUDE.md +67 -11
  3. package/README.md +204 -22
  4. package/bin/reldens-storage.js +43 -10
  5. package/index.js +53 -12
  6. package/lib/drizzle/drizzle-data-server.js +123 -0
  7. package/lib/drizzle/drizzle-driver.js +228 -0
  8. package/lib/drizzle/drizzle-models-generation.js +49 -0
  9. package/lib/drizzle/drizzle-modules-loader.js +32 -0
  10. package/lib/drizzle/drizzle-modules-validator.js +74 -0
  11. package/lib/entities-generator.js +36 -7
  12. package/lib/entity-templates/drizzle-model.template +28 -0
  13. package/lib/entity-templates/query-builder-model.template +17 -0
  14. package/lib/generators/base-generator.js +13 -0
  15. package/lib/generators/entities-generation.js +0 -13
  16. package/lib/generators/models-generation.js +179 -693
  17. package/lib/generators/relations-detection.js +171 -0
  18. package/lib/knex/knex-data-server.js +128 -0
  19. package/lib/knex/knex-driver.js +157 -0
  20. package/lib/knex/knex-modules-loader.js +28 -0
  21. package/lib/knex/knex-modules-validator.js +45 -0
  22. package/lib/kysely/kysely-data-server.js +128 -0
  23. package/lib/kysely/kysely-driver.js +176 -0
  24. package/lib/kysely/kysely-modules-loader.js +28 -0
  25. package/lib/kysely/kysely-modules-validator.js +53 -0
  26. package/lib/mikro-orm/mikro-orm-data-server.js +48 -29
  27. package/lib/mikro-orm/mikro-orm-driver.js +385 -121
  28. package/lib/mikro-orm/mikro-orm-models-generation.js +178 -0
  29. package/lib/mikro-orm/mikro-orm-modules-loader.js +50 -0
  30. package/lib/mikro-orm/mikro-orm-modules-validator.js +46 -0
  31. package/lib/mysql2-connection-config.js +38 -0
  32. package/lib/objection-js/objection-js-data-server.js +71 -125
  33. package/lib/objection-js/objection-js-driver.js +4 -1
  34. package/lib/objection-js/objection-js-models-generation.js +98 -0
  35. package/lib/objection-js/objection-modules-loader.js +28 -0
  36. package/lib/objection-js/objection-modules-validator.js +31 -0
  37. package/lib/package-resolver.js +45 -0
  38. package/lib/prisma/prisma-client-loader.js +11 -1
  39. package/lib/prisma/prisma-driver.js +16 -2
  40. package/lib/prisma/prisma-filter-processor.js +5 -6
  41. package/lib/prisma/prisma-models-generation.js +236 -0
  42. package/lib/prisma/prisma-schema-generator.js +5 -1
  43. package/lib/prisma/prisma-type-caster.js +14 -0
  44. package/lib/query-builder-driver.js +210 -0
  45. package/lib/query-builder-models-generation.js +80 -0
  46. package/lib/relations-loader.js +192 -0
  47. package/lib/type-mapper.js +38 -0
  48. package/package.json +7 -7
  49. package/tests/.env.test.example +6 -0
  50. package/tests/fixtures/categories-fixtures.js +8 -0
  51. package/tests/fixtures/expected-entities/entities/test-product-details-entity.js +72 -0
  52. package/tests/fixtures/expected-entities/entities-config.js +6 -4
  53. package/tests/fixtures/expected-entities/entities-translations.js +25 -14
  54. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/registered-models-mikro-orm.js +2 -0
  55. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-categories-model.js +13 -8
  56. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-product-details-model.js +80 -0
  57. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-products-model.js +20 -14
  58. package/tests/fixtures/expected-entities-mikro-orm/models/mikro-orm/test-reviews-model.js +13 -12
  59. package/tests/fixtures/expected-entities-objection-js/models/objection-js/registered-models-objection-js.js +2 -0
  60. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-product-details-model.js +33 -0
  61. package/tests/fixtures/expected-entities-objection-js/models/objection-js/test-products-model.js +9 -0
  62. package/tests/fixtures/expected-entities-prisma/models/prisma/registered-models-prisma.js +2 -0
  63. package/tests/fixtures/expected-entities-prisma/models/prisma/test-product-details-model.js +43 -0
  64. package/tests/fixtures/expected-entities-prisma/models/prisma/test-products-model.js +2 -0
  65. package/tests/fixtures/product-details-fixtures.js +22 -0
  66. package/tests/fixtures/reviews-fixtures.js +2 -1
  67. package/tests/fixtures/sql/test-schema.sql +23 -1
  68. package/tests/fixtures/table-columns-fixtures.js +97 -0
  69. package/tests/integration/test-cross-driver-equivalence.js +286 -0
  70. package/tests/integration/test-nested-filters.js +265 -408
  71. package/tests/integration/test-relations.js +199 -0
  72. package/tests/integration/test-reldens-sample-data.js +255 -0
  73. package/tests/integration/test-reldens-shape-relations.js +292 -0
  74. package/tests/run-tests.js +112 -60
  75. package/tests/unit/test-drivers.js +7 -1
  76. package/tests/unit/test-models-generation.js +174 -0
  77. package/tests/utils/column-value-assert.js +139 -0
  78. package/tests/utils/driver-registry.js +1 -1
  79. package/tests/utils/entity-projection.js +111 -0
  80. package/tests/utils/observed-value.js +41 -0
  81. package/tests/utils/projection-difference.js +151 -0
  82. package/tests/utils/relations-shape-support.js +104 -0
  83. package/tests/utils/reldens-schema-loader.js +274 -0
  84. package/tests/utils/test-helpers.js +152 -19
  85. package/tests/utils/test-runner.js +26 -5
  86. package/tests/utils/whole-result-assert.js +243 -0
@@ -1,408 +1,265 @@
1
- /**
2
- *
3
- * Reldens - Nested Filters Integration Test
4
- * Tests complex filter syntax across all three storage drivers with an actual database
5
- *
6
- */
7
-
8
- const { TestRunner, assert } = require('../utils/test-runner');
9
- const { TestHelpers } = require('../utils/test-helpers');
10
- const { CategoriesFixtures } = require('../fixtures/categories-fixtures');
11
- const { ProductsFixtures } = require('../fixtures/products-fixtures');
12
-
13
- class NestedFiltersTest
14
- {
15
-
16
- constructor(dataServer, repos, driverName)
17
- {
18
- this.dataServer = dataServer;
19
- this.categoriesRepo = repos.testCategories;
20
- this.productsRepo = repos.testProducts;
21
- this.driverName = driverName;
22
- this.runner = new TestRunner();
23
- }
24
-
25
- async run()
26
- {
27
- this.runner.suite('Nested Filters - Driver: '+this.driverName);
28
- await this.testAndOperator();
29
- await this.testOrOperator();
30
- await this.testNestedAndOr();
31
- await this.testNotOperator();
32
- await this.testInOperator();
33
- await this.testLikeOperator();
34
- await this.testComparisonOperators();
35
- await this.testComplexNestedScenarios();
36
- await this.testEdgeCases();
37
- return this.runner.getResults();
38
- }
39
-
40
- async testAndOperator()
41
- {
42
- this.runner.group('AND Operator');
43
- await TestHelpers.cleanDatabase(this.dataServer);
44
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
45
- CategoriesFixtures.category_filters_1,
46
- CategoriesFixtures.category_filters_2,
47
- CategoriesFixtures.category_filters_3
48
- ]);
49
- await this.runner.test('should filter with simple AND condition', async () => {
50
- let results = await this.categoriesRepo.load({
51
- AND: [
52
- {is_active: 1},
53
- {display_order: 1}
54
- ]
55
- });
56
- assert.ok(results);
57
- assert.strictEqual(results.length, 1);
58
- assert.strictEqual(results[0].display_order, 1);
59
- assert.strictEqual(results[0].is_active, 1);
60
- });
61
- await this.runner.test('should filter with AND containing multiple conditions', async () => {
62
- let results = await this.categoriesRepo.load({
63
- AND: [
64
- {is_active: 1},
65
- {display_order: {operator: 'gte', value: 1}}
66
- ]
67
- });
68
- assert.ok(results);
69
- assert.strictEqual(results.length, 2);
70
- });
71
- await this.runner.test('should return empty when AND conditions do not match', async () => {
72
- let results = await this.categoriesRepo.load({
73
- AND: [
74
- {is_active: 1},
75
- {slug: 'nonexistent'}
76
- ]
77
- });
78
- assert.ok(results);
79
- assert.strictEqual(results.length, 0);
80
- });
81
- }
82
-
83
- async testOrOperator()
84
- {
85
- this.runner.group('OR Operator');
86
- await TestHelpers.cleanDatabase(this.dataServer);
87
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
88
- CategoriesFixtures.category_filters_1,
89
- CategoriesFixtures.category_filters_2,
90
- CategoriesFixtures.category_filters_3
91
- ]);
92
- await this.runner.test('should filter with simple OR condition', async () => {
93
- let results = await this.categoriesRepo.load({
94
- OR: [
95
- {slug: 'filters-test-1'},
96
- {slug: 'filters-test-2'}
97
- ]
98
- });
99
- assert.ok(results);
100
- assert.strictEqual(results.length, 2);
101
- });
102
- await this.runner.test('should filter with OR containing operator conditions', async () => {
103
- let results = await this.categoriesRepo.load({
104
- OR: [
105
- {display_order: {operator: 'lt', value: 2}},
106
- {is_active: 0}
107
- ]
108
- });
109
- assert.ok(results);
110
- assert.strictEqual(results.length, 2);
111
- });
112
- await this.runner.test('should return all matching records with OR', async () => {
113
- let results = await this.categoriesRepo.load({
114
- OR: [
115
- {is_active: 1},
116
- {is_active: 0}
117
- ]
118
- });
119
- assert.ok(results);
120
- assert.strictEqual(results.length, 3);
121
- });
122
- }
123
-
124
- async testNestedAndOr()
125
- {
126
- this.runner.group('Nested AND/OR');
127
- await TestHelpers.cleanDatabase(this.dataServer);
128
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
129
- CategoriesFixtures.category_filters_1,
130
- CategoriesFixtures.category_filters_2,
131
- CategoriesFixtures.category_filters_3
132
- ]);
133
- await this.runner.test('should filter with OR inside AND', async () => {
134
- let results = await this.categoriesRepo.load({
135
- AND: [
136
- {is_active: 1},
137
- {
138
- OR: [
139
- {display_order: 1},
140
- {display_order: 2}
141
- ]
142
- }
143
- ]
144
- });
145
- assert.ok(results);
146
- assert.strictEqual(results.length, 2);
147
- });
148
- await this.runner.test('should filter with AND inside OR', async () => {
149
- let results = await this.categoriesRepo.load({
150
- OR: [
151
- {
152
- AND: [
153
- {is_active: 1},
154
- {display_order: 1}
155
- ]
156
- },
157
- {is_active: 0}
158
- ]
159
- });
160
- assert.ok(results);
161
- assert.strictEqual(results.length, 2);
162
- });
163
- await this.runner.test('should handle deep nesting of AND/OR', async () => {
164
- let results = await this.categoriesRepo.load({
165
- AND: [
166
- {
167
- OR: [
168
- {display_order: 1},
169
- {display_order: 2}
170
- ]
171
- },
172
- {
173
- OR: [
174
- {is_active: 1},
175
- {is_active: 0}
176
- ]
177
- }
178
- ]
179
- });
180
- assert.ok(results);
181
- assert.strictEqual(results.length, 2);
182
- });
183
- }
184
-
185
- async testNotOperator()
186
- {
187
- this.runner.group('NOT Operator');
188
- await TestHelpers.cleanDatabase(this.dataServer);
189
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
190
- CategoriesFixtures.category_filters_1,
191
- CategoriesFixtures.category_filters_2,
192
- CategoriesFixtures.category_filters_3
193
- ]);
194
- await this.runner.test('should filter with NOT operator', async () => {
195
- let results = await this.categoriesRepo.load({
196
- is_active: {operator: 'not', value: 0}
197
- });
198
- assert.ok(results);
199
- assert.strictEqual(results.length, 2);
200
- for(let category of results){
201
- assert.strictEqual(category.is_active, 1);
202
- }
203
- });
204
- await this.runner.test('should filter with NOT inside AND', async () => {
205
- let results = await this.categoriesRepo.load({
206
- AND: [
207
- {is_active: {operator: 'not', value: 0}},
208
- {display_order: 1}
209
- ]
210
- });
211
- assert.ok(results);
212
- assert.strictEqual(results.length, 1);
213
- });
214
- }
215
-
216
- async testInOperator()
217
- {
218
- this.runner.group('IN Operator');
219
- await TestHelpers.cleanDatabase(this.dataServer);
220
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
221
- CategoriesFixtures.category_filters_1,
222
- CategoriesFixtures.category_filters_2,
223
- CategoriesFixtures.category_filters_3
224
- ]);
225
- await this.runner.test('should filter with IN operator', async () => {
226
- let results = await this.categoriesRepo.load({
227
- display_order: {operator: 'in', value: [1, 2]}
228
- });
229
- assert.ok(results);
230
- assert.strictEqual(results.length, 2);
231
- });
232
- await this.runner.test('should filter with IN operator on string field', async () => {
233
- let results = await this.categoriesRepo.load({
234
- slug: {operator: 'in', value: ['filters-test-1', 'filters-test-2']}
235
- });
236
- assert.ok(results);
237
- assert.strictEqual(results.length, 2);
238
- });
239
- await this.runner.test('should return empty with IN operator when no match', async () => {
240
- let results = await this.categoriesRepo.load({
241
- display_order: {operator: 'in', value: [99, 100]}
242
- });
243
- assert.ok(results);
244
- assert.strictEqual(results.length, 0);
245
- });
246
- }
247
-
248
- async testLikeOperator()
249
- {
250
- this.runner.group('LIKE Operator');
251
- await TestHelpers.cleanDatabase(this.dataServer);
252
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
253
- CategoriesFixtures.category_filters_1,
254
- CategoriesFixtures.category_filters_2
255
- ]);
256
- await this.runner.test('should filter with LIKE operator', async () => {
257
- let results = await this.categoriesRepo.load({
258
- name: {operator: 'like', value: '%Filters%'}
259
- });
260
- assert.ok(results);
261
- assert.ok(results.length > 0);
262
- assert.ok(results[0].name.includes('Filters'));
263
- });
264
- await this.runner.test('should filter with LIKE and wildcards', async () => {
265
- let results = await this.categoriesRepo.load({
266
- slug: {operator: 'like', value: '%filters%'}
267
- });
268
- assert.ok(results);
269
- assert.ok(results.length > 0);
270
- });
271
- await this.runner.test('should work with LIKE inside AND', async () => {
272
- let results = await this.categoriesRepo.load({
273
- AND: [
274
- {is_active: 1},
275
- {name: {operator: 'like', value: '%Filters%'}}
276
- ]
277
- });
278
- assert.ok(results);
279
- assert.ok(results.length > 0);
280
- });
281
- }
282
-
283
- async testComparisonOperators()
284
- {
285
- this.runner.group('Comparison Operators');
286
- await TestHelpers.cleanDatabase(this.dataServer);
287
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
288
- CategoriesFixtures.category_filters_1,
289
- CategoriesFixtures.category_filters_2,
290
- CategoriesFixtures.category_filters_3
291
- ]);
292
- await this.runner.test('should filter with gt operator', async () => {
293
- let results = await this.categoriesRepo.load({
294
- display_order: {operator: 'gt', value: 1}
295
- });
296
- assert.ok(results);
297
- assert.strictEqual(results.length, 2);
298
- });
299
- await this.runner.test('should filter with gte operator', async () => {
300
- let results = await this.categoriesRepo.load({
301
- display_order: {operator: 'gte', value: 2}
302
- });
303
- assert.ok(results);
304
- assert.strictEqual(results.length, 2);
305
- });
306
- await this.runner.test('should filter with lt operator', async () => {
307
- let results = await this.categoriesRepo.load({
308
- display_order: {operator: 'lt', value: 3}
309
- });
310
- assert.ok(results);
311
- assert.strictEqual(results.length, 2);
312
- });
313
- await this.runner.test('should filter with lte operator', async () => {
314
- let results = await this.categoriesRepo.load({
315
- display_order: {operator: 'lte', value: 2}
316
- });
317
- assert.ok(results);
318
- assert.strictEqual(results.length, 2);
319
- });
320
- }
321
-
322
- async testComplexNestedScenarios()
323
- {
324
- this.runner.group('Complex Nested Scenarios');
325
- await TestHelpers.cleanDatabase(this.dataServer);
326
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
327
- CategoriesFixtures.category_filters_1
328
- ]);
329
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_products', [
330
- ProductsFixtures.product_filters_1,
331
- ProductsFixtures.product_filters_2
332
- ]);
333
- await this.runner.test('should handle complex filter with multiple operators', async () => {
334
- let results = await this.productsRepo.load({
335
- AND: [
336
- {status: 'published'},
337
- {
338
- OR: [
339
- {price: {operator: 'lt', value: 1000}},
340
- {stock_quantity: {operator: 'gte', value: 50}}
341
- ]
342
- }
343
- ]
344
- });
345
- assert.ok(results);
346
- assert.ok(results.length > 0);
347
- });
348
- await this.runner.test('should handle filter with IN inside OR', async () => {
349
- let results = await this.productsRepo.load({
350
- OR: [
351
- {status: {operator: 'in', value: ['published', 'draft']}},
352
- {stock_quantity: {operator: 'gt', value: 0}}
353
- ]
354
- });
355
- assert.ok(results);
356
- assert.strictEqual(results.length, 2);
357
- });
358
- await this.runner.test('should handle nested AND/OR with LIKE and comparison operators', async () => {
359
- let results = await this.productsRepo.load({
360
- AND: [
361
- {
362
- OR: [
363
- {name: {operator: 'like', value: '%Phone%'}},
364
- {name: {operator: 'like', value: '%Product%'}}
365
- ]
366
- },
367
- {price: {operator: 'gte', value: 0}}
368
- ]
369
- });
370
- assert.ok(results);
371
- assert.ok(results.length > 0);
372
- });
373
- }
374
-
375
- async testEdgeCases()
376
- {
377
- this.runner.group('Edge Cases');
378
- await TestHelpers.cleanDatabase(this.dataServer);
379
- await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', [
380
- CategoriesFixtures.category_filters_1
381
- ]);
382
- await this.runner.test('should handle empty AND array', async () => {
383
- let results = await this.categoriesRepo.load({AND: []});
384
- assert.ok(results);
385
- });
386
- await this.runner.test('should handle empty OR array', async () => {
387
- let results = await this.categoriesRepo.load({OR: []});
388
- assert.ok(results);
389
- });
390
- await this.runner.test('should handle single condition in AND', async () => {
391
- let results = await this.categoriesRepo.load({
392
- AND: [{is_active: 1}]
393
- });
394
- assert.ok(results);
395
- assert.strictEqual(results.length, 1);
396
- });
397
- await this.runner.test('should handle single condition in OR', async () => {
398
- let results = await this.categoriesRepo.load({
399
- OR: [{is_active: 1}]
400
- });
401
- assert.ok(results);
402
- assert.strictEqual(results.length, 1);
403
- });
404
- }
405
-
406
- }
407
-
408
- module.exports = NestedFiltersTest;
1
+ /**
2
+ *
3
+ * Reldens - Nested Filters Integration Test
4
+ * Tests complex filter syntax across all the storage drivers with an actual database, asserting the whole
5
+ * result of every filter: the exact amount of records and every column of every returned record against the
6
+ * fixture rows that were inserted.
7
+ *
8
+ */
9
+
10
+ const { TestRunner } = require('../utils/test-runner');
11
+ const { TestHelpers } = require('../utils/test-helpers');
12
+ const { WholeResultAssert } = require('../utils/whole-result-assert');
13
+ const { CategoriesFixtures } = require('../fixtures/categories-fixtures');
14
+ const { ProductsFixtures } = require('../fixtures/products-fixtures');
15
+
16
+ class NestedFiltersTest
17
+ {
18
+
19
+ constructor(dataServer, repos, driverName)
20
+ {
21
+ this.dataServer = dataServer;
22
+ this.categoriesRepo = repos.testCategories;
23
+ this.productsRepo = repos.testProducts;
24
+ this.driverName = driverName;
25
+ this.runner = new TestRunner();
26
+ this.categoriesById = {
27
+ 1500: CategoriesFixtures.category_filters_1,
28
+ 1501: CategoriesFixtures.category_filters_2,
29
+ 1502: CategoriesFixtures.category_filters_3
30
+ };
31
+ this.productsById = {
32
+ 2500: ProductsFixtures.product_filters_1,
33
+ 2501: ProductsFixtures.product_filters_2
34
+ };
35
+ }
36
+
37
+ async run()
38
+ {
39
+ this.runner.suite('Nested Filters - Driver: '+this.driverName);
40
+ await this.testAndOperator();
41
+ await this.testOrOperator();
42
+ await this.testNestedAndOr();
43
+ await this.testNotOperator();
44
+ await this.testInOperator();
45
+ await this.testLikeOperator();
46
+ await this.testComparisonOperators();
47
+ await this.testComplexNestedScenarios();
48
+ await this.testEdgeCases();
49
+ return this.runner.getResults();
50
+ }
51
+
52
+ async insertCategories(fixtureRows)
53
+ {
54
+ await TestHelpers.cleanDatabase(this.dataServer);
55
+ await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_categories', fixtureRows);
56
+ return true;
57
+ }
58
+
59
+ allFilterCategories()
60
+ {
61
+ return [
62
+ CategoriesFixtures.category_filters_1,
63
+ CategoriesFixtures.category_filters_2,
64
+ CategoriesFixtures.category_filters_3
65
+ ];
66
+ }
67
+
68
+ assertFilteredRecords(results, tableName, expectedIds, label)
69
+ {
70
+ let fixturesById = 'test_categories' === tableName ? this.categoriesById : this.productsById;
71
+ let expectedFixtures = [];
72
+ for(let expectedId of expectedIds){
73
+ expectedFixtures.push(fixturesById[expectedId]);
74
+ }
75
+ return WholeResultAssert.fixtureList(results, tableName, expectedFixtures, label);
76
+ }
77
+
78
+ async testAndOperator()
79
+ {
80
+ this.runner.group('AND Operator');
81
+ await this.insertCategories(this.allFilterCategories());
82
+ await this.runner.test('should filter with simple AND condition', async () => {
83
+ let results = await this.categoriesRepo.load({AND: [{is_active: 1}, {display_order: 1}]});
84
+ this.assertFilteredRecords(results, 'test_categories', [1500], 'AND is_active 1 and order 1');
85
+ });
86
+ await this.runner.test('should filter with AND containing multiple conditions', async () => {
87
+ let orderFilter = {operator: 'gte', value: 1};
88
+ let results = await this.categoriesRepo.load({AND: [{is_active: 1}, {display_order: orderFilter}]});
89
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'AND is_active 1 and order gte 1');
90
+ });
91
+ await this.runner.test('should return empty when AND conditions do not match', async () => {
92
+ let results = await this.categoriesRepo.load({AND: [{is_active: 1}, {slug: 'nonexistent'}]});
93
+ this.assertFilteredRecords(results, 'test_categories', [], 'AND with a slug that does not exist');
94
+ });
95
+ }
96
+
97
+ async testOrOperator()
98
+ {
99
+ this.runner.group('OR Operator');
100
+ await this.insertCategories(this.allFilterCategories());
101
+ await this.runner.test('should filter with simple OR condition', async () => {
102
+ let filters = {OR: [{slug: 'filters-test-1'}, {slug: 'filters-test-2'}]};
103
+ let results = await this.categoriesRepo.load(filters);
104
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'OR on two slugs');
105
+ });
106
+ await this.runner.test('should filter with OR containing operator conditions', async () => {
107
+ let filters = {OR: [{display_order: {operator: 'lt', value: 2}}, {is_active: 0}]};
108
+ let results = await this.categoriesRepo.load(filters);
109
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1502], 'OR order lt 2 or inactive');
110
+ });
111
+ await this.runner.test('should return all matching records with OR', async () => {
112
+ let results = await this.categoriesRepo.load({OR: [{is_active: 1}, {is_active: 0}]});
113
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501, 1502], 'OR on both is_active');
114
+ });
115
+ }
116
+
117
+ async testNestedAndOr()
118
+ {
119
+ this.runner.group('Nested AND/OR');
120
+ await this.insertCategories(this.allFilterCategories());
121
+ await this.runner.test('should filter with OR inside AND', async () => {
122
+ let orderOptions = {OR: [{display_order: 1}, {display_order: 2}]};
123
+ let results = await this.categoriesRepo.load({AND: [{is_active: 1}, orderOptions]});
124
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'OR inside AND');
125
+ });
126
+ await this.runner.test('should filter with AND inside OR', async () => {
127
+ let activeAndFirst = {AND: [{is_active: 1}, {display_order: 1}]};
128
+ let results = await this.categoriesRepo.load({OR: [activeAndFirst, {is_active: 0}]});
129
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1502], 'AND inside OR');
130
+ });
131
+ await this.runner.test('should handle deep nesting of AND/OR', async () => {
132
+ let orderOptions = {OR: [{display_order: 1}, {display_order: 2}]};
133
+ let activeOptions = {OR: [{is_active: 1}, {is_active: 0}]};
134
+ let results = await this.categoriesRepo.load({AND: [orderOptions, activeOptions]});
135
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'two OR groups inside AND');
136
+ });
137
+ }
138
+
139
+ async testNotOperator()
140
+ {
141
+ this.runner.group('NOT Operator');
142
+ await this.insertCategories(this.allFilterCategories());
143
+ await this.runner.test('should filter with NOT operator', async () => {
144
+ let results = await this.categoriesRepo.load({is_active: {operator: 'not', value: 0}});
145
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'NOT is_active 0');
146
+ });
147
+ await this.runner.test('should filter with NOT inside AND', async () => {
148
+ let activeFilter = {is_active: {operator: 'not', value: 0}};
149
+ let results = await this.categoriesRepo.load({AND: [activeFilter, {display_order: 1}]});
150
+ this.assertFilteredRecords(results, 'test_categories', [1500], 'NOT inside AND');
151
+ });
152
+ }
153
+
154
+ async testInOperator()
155
+ {
156
+ this.runner.group('IN Operator');
157
+ await this.insertCategories(this.allFilterCategories());
158
+ await this.runner.test('should filter with IN operator', async () => {
159
+ let results = await this.categoriesRepo.load({display_order: {operator: 'in', value: [1, 2]}});
160
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'IN display_order 1 and 2');
161
+ });
162
+ await this.runner.test('should filter with IN operator on string field', async () => {
163
+ let slugsFilter = {operator: 'in', value: ['filters-test-1', 'filters-test-2']};
164
+ let results = await this.categoriesRepo.load({slug: slugsFilter});
165
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'IN slug');
166
+ });
167
+ await this.runner.test('should return empty with IN operator when no match', async () => {
168
+ let results = await this.categoriesRepo.load({display_order: {operator: 'in', value: [99, 100]}});
169
+ this.assertFilteredRecords(results, 'test_categories', [], 'IN display_order without match');
170
+ });
171
+ }
172
+
173
+ async testLikeOperator()
174
+ {
175
+ this.runner.group('LIKE Operator');
176
+ await this.insertCategories([CategoriesFixtures.category_filters_1, CategoriesFixtures.category_filters_2]);
177
+ await this.runner.test('should filter with LIKE operator', async () => {
178
+ let results = await this.categoriesRepo.load({name: {operator: 'like', value: '%Filters%'}});
179
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'LIKE on name');
180
+ });
181
+ await this.runner.test('should filter with LIKE and wildcards', async () => {
182
+ let results = await this.categoriesRepo.load({slug: {operator: 'like', value: '%filters%'}});
183
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'LIKE on slug');
184
+ });
185
+ await this.runner.test('should work with LIKE inside AND', async () => {
186
+ let nameFilter = {name: {operator: 'like', value: '%Filters%'}};
187
+ let results = await this.categoriesRepo.load({AND: [{is_active: 1}, nameFilter]});
188
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'LIKE inside AND');
189
+ });
190
+ }
191
+
192
+ async testComparisonOperators()
193
+ {
194
+ this.runner.group('Comparison Operators');
195
+ await this.insertCategories(this.allFilterCategories());
196
+ await this.runner.test('should filter with gt operator', async () => {
197
+ let results = await this.categoriesRepo.load({display_order: {operator: 'gt', value: 1}});
198
+ this.assertFilteredRecords(results, 'test_categories', [1501, 1502], 'display_order gt 1');
199
+ });
200
+ await this.runner.test('should filter with gte operator', async () => {
201
+ let results = await this.categoriesRepo.load({display_order: {operator: 'gte', value: 2}});
202
+ this.assertFilteredRecords(results, 'test_categories', [1501, 1502], 'display_order gte 2');
203
+ });
204
+ await this.runner.test('should filter with lt operator', async () => {
205
+ let results = await this.categoriesRepo.load({display_order: {operator: 'lt', value: 3}});
206
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'display_order lt 3');
207
+ });
208
+ await this.runner.test('should filter with lte operator', async () => {
209
+ let results = await this.categoriesRepo.load({display_order: {operator: 'lte', value: 2}});
210
+ this.assertFilteredRecords(results, 'test_categories', [1500, 1501], 'display_order lte 2');
211
+ });
212
+ }
213
+
214
+ async testComplexNestedScenarios()
215
+ {
216
+ this.runner.group('Complex Nested Scenarios');
217
+ await this.insertCategories([CategoriesFixtures.category_filters_1]);
218
+ await TestHelpers.insertFixturesViaRawSQL(this.dataServer, 'test_products', [
219
+ ProductsFixtures.product_filters_1,
220
+ ProductsFixtures.product_filters_2
221
+ ]);
222
+ await this.runner.test('should handle complex filter with multiple operators', async () => {
223
+ let priceOrStock = {OR: [{price: {operator: 'lt', value: 1000}}, {stock_quantity: {operator: 'gte', value: 50}}]};
224
+ let results = await this.productsRepo.load({AND: [{status: 'published'}, priceOrStock]});
225
+ this.assertFilteredRecords(results, 'test_products', [2500], 'published with price or stock');
226
+ });
227
+ await this.runner.test('should handle filter with IN inside OR', async () => {
228
+ let statusFilter = {status: {operator: 'in', value: ['published', 'draft']}};
229
+ let stockFilter = {stock_quantity: {operator: 'gt', value: 0}};
230
+ let results = await this.productsRepo.load({OR: [statusFilter, stockFilter]});
231
+ this.assertFilteredRecords(results, 'test_products', [2500, 2501], 'IN inside OR');
232
+ });
233
+ await this.runner.test('should handle nested AND/OR with LIKE and comparison operators', async () => {
234
+ let nameOptions = {OR: [{name: {operator: 'like', value: '%Phone%'}}, {name: {operator: 'like', value: '%Product%'}}]};
235
+ let priceFilter = {price: {operator: 'gte', value: 0}};
236
+ let results = await this.productsRepo.load({AND: [nameOptions, priceFilter]});
237
+ this.assertFilteredRecords(results, 'test_products', [2500, 2501], 'LIKE options with price');
238
+ });
239
+ }
240
+
241
+ async testEdgeCases()
242
+ {
243
+ this.runner.group('Edge Cases');
244
+ await this.insertCategories([CategoriesFixtures.category_filters_1]);
245
+ await this.runner.test('should handle empty AND array', async () => {
246
+ let results = await this.categoriesRepo.load({AND: []});
247
+ this.assertFilteredRecords(results, 'test_categories', [1500], 'empty AND returns every record');
248
+ });
249
+ await this.runner.test('should handle empty OR array', async () => {
250
+ let results = await this.categoriesRepo.load({OR: []});
251
+ this.assertFilteredRecords(results, 'test_categories', [1500], 'empty OR returns every record');
252
+ });
253
+ await this.runner.test('should handle single condition in AND', async () => {
254
+ let results = await this.categoriesRepo.load({AND: [{is_active: 1}]});
255
+ this.assertFilteredRecords(results, 'test_categories', [1500], 'single condition in AND');
256
+ });
257
+ await this.runner.test('should handle single condition in OR', async () => {
258
+ let results = await this.categoriesRepo.load({OR: [{is_active: 1}]});
259
+ this.assertFilteredRecords(results, 'test_categories', [1500], 'single condition in OR');
260
+ });
261
+ }
262
+
263
+ }
264
+
265
+ module.exports = NestedFiltersTest;