bunsane 0.3.2 → 0.4.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 (214) hide show
  1. package/CHANGELOG.md +445 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +85 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +4 -4
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +16 -8
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +364 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/pendingOps.ts +72 -0
  30. package/core/entity/saveEntity.ts +377 -0
  31. package/core/hooks/dispatcher.ts +439 -0
  32. package/core/hooks/guards.ts +155 -0
  33. package/core/hooks/registry.ts +247 -0
  34. package/core/metadata/definitions/Component.ts +1 -1
  35. package/core/metadata/index.ts +15 -4
  36. package/core/middleware/RateLimit.ts +102 -105
  37. package/core/middleware/RequestId.ts +2 -9
  38. package/core/middleware/SecurityHeaders.ts +2 -11
  39. package/core/middleware/headers.ts +28 -0
  40. package/core/remote/OutboxWorker.ts +213 -183
  41. package/core/remote/RemoteManager.ts +401 -400
  42. package/core/remote/types.ts +153 -151
  43. package/core/requestScope.ts +34 -0
  44. package/core/scheduler/cronEvaluator.ts +174 -0
  45. package/core/scheduler/lifecycleHooks.ts +21 -0
  46. package/core/scheduler/lockCoordinator.ts +27 -0
  47. package/core/scheduler/metrics.ts +14 -0
  48. package/core/scheduler/taskRunner.ts +420 -0
  49. package/database/DatabaseHelper.ts +128 -101
  50. package/database/IndexingStrategy.ts +72 -2
  51. package/database/PreparedStatementCache.ts +8 -2
  52. package/database/cancellable.ts +35 -22
  53. package/database/index.ts +15 -3
  54. package/database/instrumentedDb.ts +141 -141
  55. package/endpoints/archetypes.ts +2 -8
  56. package/endpoints/tables.ts +6 -1
  57. package/gql/index.ts +1 -1
  58. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  59. package/package.json +22 -1
  60. package/query/CTENode.ts +5 -3
  61. package/query/ComponentInclusionNode.ts +240 -13
  62. package/query/OrNode.ts +6 -5
  63. package/query/Query.ts +157 -46
  64. package/query/QueryContext.ts +6 -0
  65. package/query/QueryDAG.ts +7 -2
  66. package/query/membershipSource.ts +66 -0
  67. package/storage/LocalStorageProvider.ts +8 -3
  68. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  69. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  70. package/studio/{index.html → dist/index.html} +3 -2
  71. package/swagger/generator.ts +11 -1
  72. package/upload/UploadManager.ts +8 -6
  73. package/utils/uuid.ts +40 -10
  74. package/.claude/scheduled_tasks.lock +0 -1
  75. package/.claude/settings.local.json +0 -47
  76. package/.prettierrc +0 -4
  77. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  78. package/.serena/memories/architecture.md +0 -154
  79. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  80. package/.serena/memories/code_style_and_conventions.md +0 -76
  81. package/.serena/memories/project_overview.md +0 -43
  82. package/.serena/memories/schema-dsl-plan.md +0 -107
  83. package/.serena/memories/suggested_commands.md +0 -80
  84. package/.serena/memories/typescript-compilation-status.md +0 -54
  85. package/.serena/project.yml +0 -114
  86. package/BunSane.jpg +0 -0
  87. package/CLAUDE.md +0 -198
  88. package/TODO.md +0 -2
  89. package/bun.lock +0 -302
  90. package/bunfig.toml +0 -10
  91. package/docs/RFC_APP_REFACTOR.md +0 -248
  92. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  93. package/docs/SCALABILITY_PLAN.md +0 -175
  94. package/studio/bun.lock +0 -482
  95. package/studio/package.json +0 -39
  96. package/studio/postcss.config.js +0 -6
  97. package/studio/src/components/DataTable.tsx +0 -211
  98. package/studio/src/components/Layout.tsx +0 -13
  99. package/studio/src/components/PageContainer.tsx +0 -9
  100. package/studio/src/components/PageHeader.tsx +0 -13
  101. package/studio/src/components/SearchBar.tsx +0 -57
  102. package/studio/src/components/Sidebar.tsx +0 -294
  103. package/studio/src/components/ui/button.tsx +0 -56
  104. package/studio/src/components/ui/checkbox.tsx +0 -26
  105. package/studio/src/components/ui/input.tsx +0 -25
  106. package/studio/src/hooks/useDataTable.ts +0 -131
  107. package/studio/src/index.css +0 -36
  108. package/studio/src/lib/api.ts +0 -186
  109. package/studio/src/lib/utils.ts +0 -13
  110. package/studio/src/main.tsx +0 -17
  111. package/studio/src/pages/ArcheType.tsx +0 -239
  112. package/studio/src/pages/Components.tsx +0 -124
  113. package/studio/src/pages/EntityInspector.tsx +0 -302
  114. package/studio/src/pages/QueryRunner.tsx +0 -246
  115. package/studio/src/pages/Table.tsx +0 -94
  116. package/studio/src/pages/Welcome.tsx +0 -241
  117. package/studio/src/routes.tsx +0 -45
  118. package/studio/src/store/archeTypeSettings.ts +0 -30
  119. package/studio/src/store/studio.ts +0 -65
  120. package/studio/src/utils/columnHelpers.tsx +0 -114
  121. package/studio/studio-instructions.md +0 -81
  122. package/studio/tailwind.config.js +0 -77
  123. package/studio/utils.ts +0 -54
  124. package/studio/vite.config.js +0 -19
  125. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  126. package/tests/benchmark/bunfig.toml +0 -9
  127. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  128. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  129. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  130. package/tests/benchmark/fixtures/index.ts +0 -6
  131. package/tests/benchmark/index.ts +0 -22
  132. package/tests/benchmark/noop-preload.ts +0 -3
  133. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  134. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  135. package/tests/benchmark/runners/index.ts +0 -4
  136. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  137. package/tests/benchmark/scripts/generate-db.ts +0 -344
  138. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  139. package/tests/e2e/http.test.ts +0 -130
  140. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  141. package/tests/fixtures/components/TestOrder.ts +0 -23
  142. package/tests/fixtures/components/TestProduct.ts +0 -23
  143. package/tests/fixtures/components/TestUser.ts +0 -20
  144. package/tests/fixtures/components/index.ts +0 -6
  145. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  146. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  147. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  148. package/tests/helpers/MockRedisClient.ts +0 -113
  149. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  150. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  151. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  152. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  153. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  154. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  155. package/tests/integration/query/Query.abort.test.ts +0 -66
  156. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  157. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  158. package/tests/integration/query/Query.exec.test.ts +0 -576
  159. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  160. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  161. package/tests/integration/remote/dlq.test.ts +0 -175
  162. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  163. package/tests/integration/remote/outbox.test.ts +0 -130
  164. package/tests/integration/remote/rpc.test.ts +0 -177
  165. package/tests/pglite-setup.ts +0 -62
  166. package/tests/setup.ts +0 -164
  167. package/tests/stress/BenchmarkRunner.ts +0 -203
  168. package/tests/stress/DataSeeder.ts +0 -190
  169. package/tests/stress/StressTestReporter.ts +0 -229
  170. package/tests/stress/cursor-perf-test.ts +0 -171
  171. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  172. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  173. package/tests/stress/index.ts +0 -7
  174. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  175. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  176. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  177. package/tests/unit/BatchLoader.test.ts +0 -196
  178. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  179. package/tests/unit/cache/CacheManager.test.ts +0 -498
  180. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  181. package/tests/unit/cache/RedisCache.test.ts +0 -411
  182. package/tests/unit/database/cancellable.test.ts +0 -81
  183. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  184. package/tests/unit/entity/Entity.components.test.ts +0 -317
  185. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  186. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  187. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  188. package/tests/unit/entity/Entity.test.ts +0 -345
  189. package/tests/unit/gql/depthLimit.test.ts +0 -203
  190. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  191. package/tests/unit/health/Health.test.ts +0 -129
  192. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  193. package/tests/unit/middleware/Middleware.test.ts +0 -98
  194. package/tests/unit/middleware/RequestId.test.ts +0 -54
  195. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  196. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  197. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  198. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  199. package/tests/unit/query/Query.test.ts +0 -310
  200. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  201. package/tests/unit/remote/RemoteError.test.ts +0 -55
  202. package/tests/unit/remote/decorators.test.ts +0 -195
  203. package/tests/unit/remote/metrics.test.ts +0 -115
  204. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  205. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  206. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  207. package/tests/unit/schema/schema-integration.test.ts +0 -426
  208. package/tests/unit/schema/schema.test.ts +0 -580
  209. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  210. package/tests/unit/upload/RestUpload.test.ts +0 -267
  211. package/tests/unit/validateEnv.test.ts +0 -82
  212. package/tests/utils/entity-tracker.ts +0 -57
  213. package/tests/utils/index.ts +0 -13
  214. package/tests/utils/test-context.ts +0 -149
@@ -1,111 +0,0 @@
1
- /**
2
- * Unit tests for FilterBuilder utility functions
3
- */
4
- import { describe, test, expect } from 'bun:test';
5
- import { buildJSONPath, composeFilters, type FilterBuilder, type FilterResult } from '../../../query/FilterBuilder';
6
-
7
- describe('FilterBuilder', () => {
8
- describe('buildJSONPath()', () => {
9
- test('builds simple field path', () => {
10
- const result = buildJSONPath('name', 'c');
11
- expect(result).toBe("c.data->>'name'");
12
- });
13
-
14
- test('builds nested field path', () => {
15
- const result = buildJSONPath('location.latitude', 'c');
16
- expect(result).toBe("c.data->'location'->>'latitude'");
17
- });
18
-
19
- test('builds deeply nested path', () => {
20
- const result = buildJSONPath('device.location.coordinates.latitude', 'c');
21
- expect(result).toBe("c.data->'device'->'location'->'coordinates'->>'latitude'");
22
- });
23
-
24
- test('uses provided alias', () => {
25
- const result = buildJSONPath('field', 'comp');
26
- expect(result).toBe("comp.data->>'field'");
27
- });
28
- });
29
-
30
- describe('composeFilters()', () => {
31
- test('throws for empty array', () => {
32
- expect(() => composeFilters([])).toThrow('Cannot compose empty array of filter builders');
33
- });
34
-
35
- test('composes single builder', () => {
36
- const mockBuilder: FilterBuilder = () => ({
37
- sql: 'field = $1',
38
- addedParams: 1
39
- });
40
-
41
- const composed = composeFilters([mockBuilder]);
42
- const result = composed(
43
- { field: 'test', operator: 'EQ', value: 'value' },
44
- 'c',
45
- {} as any
46
- );
47
-
48
- expect(result.sql).toBe('(field = $1)');
49
- expect(result.addedParams).toBe(1);
50
- });
51
-
52
- test('composes multiple builders with AND', () => {
53
- const builder1: FilterBuilder = () => ({
54
- sql: 'field1 = $1',
55
- addedParams: 1
56
- });
57
- const builder2: FilterBuilder = () => ({
58
- sql: 'field2 > $2',
59
- addedParams: 1
60
- });
61
-
62
- const composed = composeFilters([builder1, builder2]);
63
- const result = composed(
64
- { field: 'test', operator: 'EQ', value: 'value' },
65
- 'c',
66
- {} as any
67
- );
68
-
69
- expect(result.sql).toBe('(field1 = $1) AND (field2 > $2)');
70
- expect(result.addedParams).toBe(2);
71
- });
72
-
73
- test('handles empty SQL from builder', () => {
74
- const builder1: FilterBuilder = () => ({
75
- sql: 'field1 = $1',
76
- addedParams: 1
77
- });
78
- const builder2: FilterBuilder = () => ({
79
- sql: '',
80
- addedParams: 0
81
- });
82
-
83
- const composed = composeFilters([builder1, builder2]);
84
- const result = composed(
85
- { field: 'test', operator: 'EQ', value: 'value' },
86
- 'c',
87
- {} as any
88
- );
89
-
90
- expect(result.sql).toBe('(field1 = $1)');
91
- expect(result.addedParams).toBe(1);
92
- });
93
-
94
- test('handles whitespace-only SQL', () => {
95
- const builder: FilterBuilder = () => ({
96
- sql: ' ',
97
- addedParams: 0
98
- });
99
-
100
- const composed = composeFilters([builder]);
101
- const result = composed(
102
- { field: 'test', operator: 'EQ', value: 'value' },
103
- 'c',
104
- {} as any
105
- );
106
-
107
- expect(result.sql).toBe('');
108
- expect(result.addedParams).toBe(0);
109
- });
110
- });
111
- });
@@ -1,178 +0,0 @@
1
- /**
2
- * Unit tests for JSONB Array Filter Builders
3
- */
4
- import { describe, test, expect } from 'bun:test';
5
- import { buildJSONBPath } from '../../../query/FilterBuilder';
6
- import { QueryContext } from '../../../query/QueryContext';
7
- import {
8
- jsonbContainsBuilder,
9
- jsonbContainedByBuilder,
10
- jsonbHasAnyBuilder,
11
- jsonbHasAllBuilder,
12
- jsonbArrayOptions,
13
- } from '../../../query/builders/JsonbArrayBuilder';
14
-
15
- describe('buildJSONBPath', () => {
16
- test('simple field returns JSONB node path', () => {
17
- expect(buildJSONBPath('tags', 'c')).toBe("c.data->'tags'");
18
- });
19
-
20
- test('nested field returns JSONB node path', () => {
21
- expect(buildJSONBPath('metadata.tags', 'c')).toBe("c.data->'metadata'->'tags'");
22
- });
23
-
24
- test('deeply nested field', () => {
25
- expect(buildJSONBPath('a.b.c', 'c')).toBe("c.data->'a'->'b'->'c'");
26
- });
27
-
28
- test('uses provided alias', () => {
29
- expect(buildJSONBPath('tags', 'comp')).toBe("comp.data->'tags'");
30
- });
31
- });
32
-
33
- describe('jsonbContainsBuilder (@>)', () => {
34
- test('single string value is auto-wrapped in array', () => {
35
- const ctx = new QueryContext();
36
- const result = jsonbContainsBuilder(
37
- { field: 'tags', operator: 'CONTAINS', value: 'urgent' },
38
- 'c', ctx
39
- );
40
- expect(result.sql).toBe("c.data->'tags' @> $1::jsonb");
41
- expect(ctx.params[0]).toEqual(['urgent']);
42
- expect(result.addedParams).toBe(1);
43
- });
44
-
45
- test('array value is passed as raw array', () => {
46
- const ctx = new QueryContext();
47
- const result = jsonbContainsBuilder(
48
- { field: 'tags', operator: 'CONTAINS', value: ['a', 'b'] },
49
- 'c', ctx
50
- );
51
- expect(result.sql).toBe("c.data->'tags' @> $1::jsonb");
52
- expect(ctx.params[0]).toEqual(['a', 'b']);
53
- });
54
-
55
- test('nested field path', () => {
56
- const ctx = new QueryContext();
57
- const result = jsonbContainsBuilder(
58
- { field: 'meta.tags', operator: 'CONTAINS', value: 'x' },
59
- 'c', ctx
60
- );
61
- expect(result.sql).toBe("c.data->'meta'->'tags' @> $1::jsonb");
62
- });
63
-
64
- test('numeric value', () => {
65
- const ctx = new QueryContext();
66
- jsonbContainsBuilder(
67
- { field: 'scores', operator: 'CONTAINS', value: 42 },
68
- 'c', ctx
69
- );
70
- expect(ctx.params[0]).toEqual([42]);
71
- });
72
- });
73
-
74
- describe('jsonbContainedByBuilder (<@)', () => {
75
- test('generates correct SQL', () => {
76
- const ctx = new QueryContext();
77
- const result = jsonbContainedByBuilder(
78
- { field: 'tags', operator: 'CONTAINED_BY', value: ['a', 'b', 'c'] },
79
- 'c', ctx
80
- );
81
- expect(result.sql).toBe("c.data->'tags' <@ $1::jsonb");
82
- expect(ctx.params[0]).toEqual(['a', 'b', 'c']);
83
- expect(result.addedParams).toBe(1);
84
- });
85
-
86
- test('single value is auto-wrapped', () => {
87
- const ctx = new QueryContext();
88
- jsonbContainedByBuilder(
89
- { field: 'tags', operator: 'CONTAINED_BY', value: 'only' },
90
- 'c', ctx
91
- );
92
- expect(ctx.params[0]).toEqual(['only']);
93
- });
94
- });
95
-
96
- describe('jsonbHasAnyBuilder (?|)', () => {
97
- test('generates correct SQL with text[] cast', () => {
98
- const ctx = new QueryContext();
99
- const result = jsonbHasAnyBuilder(
100
- { field: 'tags', operator: 'HAS_ANY', value: ['a', 'b'] },
101
- 'c', ctx
102
- );
103
- expect(result.sql).toBe("c.data->'tags' ?| $1::text[]");
104
- expect(ctx.params[0]).toEqual(['a', 'b']);
105
- expect(result.addedParams).toBe(1);
106
- });
107
-
108
- test('single value is auto-wrapped and stringified', () => {
109
- const ctx = new QueryContext();
110
- jsonbHasAnyBuilder(
111
- { field: 'tags', operator: 'HAS_ANY', value: 'solo' },
112
- 'c', ctx
113
- );
114
- expect(ctx.params[0]).toEqual(['solo']);
115
- });
116
-
117
- test('numeric values are cast to strings', () => {
118
- const ctx = new QueryContext();
119
- jsonbHasAnyBuilder(
120
- { field: 'ids', operator: 'HAS_ANY', value: [1, 2, 3] },
121
- 'c', ctx
122
- );
123
- expect(ctx.params[0]).toEqual(['1', '2', '3']);
124
- });
125
- });
126
-
127
- describe('jsonbHasAllBuilder (?&)', () => {
128
- test('generates correct SQL with text[] cast', () => {
129
- const ctx = new QueryContext();
130
- const result = jsonbHasAllBuilder(
131
- { field: 'tags', operator: 'HAS_ALL', value: ['x', 'y'] },
132
- 'c', ctx
133
- );
134
- expect(result.sql).toBe("c.data->'tags' ?& $1::text[]");
135
- expect(ctx.params[0]).toEqual(['x', 'y']);
136
- expect(result.addedParams).toBe(1);
137
- });
138
- });
139
-
140
- describe('validation', () => {
141
- const validate = jsonbArrayOptions.validate!;
142
-
143
- test('rejects null value', () => {
144
- expect(validate({ field: 'f', operator: 'CONTAINS', value: null })).toBe(false);
145
- });
146
-
147
- test('rejects undefined value', () => {
148
- expect(validate({ field: 'f', operator: 'CONTAINS', value: undefined })).toBe(false);
149
- });
150
-
151
- test('rejects empty array', () => {
152
- expect(validate({ field: 'f', operator: 'CONTAINS', value: [] })).toBe(false);
153
- });
154
-
155
- test('accepts string value', () => {
156
- expect(validate({ field: 'f', operator: 'CONTAINS', value: 'tag' })).toBe(true);
157
- });
158
-
159
- test('accepts number value', () => {
160
- expect(validate({ field: 'f', operator: 'CONTAINS', value: 42 })).toBe(true);
161
- });
162
-
163
- test('accepts boolean value', () => {
164
- expect(validate({ field: 'f', operator: 'CONTAINS', value: true })).toBe(true);
165
- });
166
-
167
- test('accepts array of strings', () => {
168
- expect(validate({ field: 'f', operator: 'CONTAINS', value: ['a', 'b'] })).toBe(true);
169
- });
170
-
171
- test('rejects object value', () => {
172
- expect(validate({ field: 'f', operator: 'CONTAINS', value: { key: 'val' } })).toBe(false);
173
- });
174
-
175
- test('rejects array with non-primitive elements', () => {
176
- expect(validate({ field: 'f', operator: 'CONTAINS', value: [{ a: 1 }] })).toBe(false);
177
- });
178
- });
@@ -1,69 +0,0 @@
1
- /**
2
- * Empty-string filter support. JSONB text extraction (c.data->>'field')
3
- * returns text, so `= ''` / `!= ''` / LIKE against empty string are
4
- * legitimate. UUID-cast path is gated on a regex that empty cannot match.
5
- */
6
- import { describe, test, expect, beforeAll } from 'bun:test';
7
- import { Entity } from '../../../core/Entity';
8
- import { BaseComponent } from '../../../core/components/BaseComponent';
9
- import { Component, CompData } from '../../../core/components/Decorators';
10
- import { Query, FilterOp } from '../../../query/Query';
11
- import { ensureComponentsRegistered } from '../../utils';
12
-
13
- @Component
14
- class EmptyableNote extends BaseComponent {
15
- @CompData()
16
- value: string = '';
17
- }
18
-
19
- describe('Query empty-string filter', () => {
20
- beforeAll(async () => {
21
- await ensureComponentsRegistered(EmptyableNote);
22
- });
23
-
24
- test('Query.filter accepts empty-string value without throwing', () => {
25
- expect(() => Query.filter('value', FilterOp.EQ, '')).not.toThrow();
26
- const f = Query.filter('value', FilterOp.EQ, '');
27
- expect(f.value).toBe('');
28
- });
29
-
30
- test('Query.filter accepts whitespace-only value without throwing', () => {
31
- expect(() => Query.filter('value', FilterOp.EQ, ' ')).not.toThrow();
32
- });
33
-
34
- test('.with(C, filter EQ "") executes and returns matching rows', async () => {
35
- const withEmpty = Entity.Create();
36
- withEmpty.add(EmptyableNote, { value: '' });
37
- await withEmpty.save();
38
-
39
- const withData = Entity.Create();
40
- withData.add(EmptyableNote, { value: 'not empty' });
41
- await withData.save();
42
-
43
- const rows = await new Query()
44
- .with(EmptyableNote, Query.filters(Query.filter('value', FilterOp.EQ, '')))
45
- .exec();
46
-
47
- const ids = rows.map(e => e.id);
48
- expect(ids).toContain(withEmpty.id);
49
- expect(ids).not.toContain(withData.id);
50
- });
51
-
52
- test('.with(C, filter != "") excludes rows with empty value', async () => {
53
- const withEmpty = Entity.Create();
54
- withEmpty.add(EmptyableNote, { value: '' });
55
- await withEmpty.save();
56
-
57
- const withData = Entity.Create();
58
- withData.add(EmptyableNote, { value: 'populated' });
59
- await withData.save();
60
-
61
- const rows = await new Query()
62
- .with(EmptyableNote, Query.filters(Query.filter('value', FilterOp.NEQ, '')))
63
- .exec();
64
-
65
- const ids = rows.map(e => e.id);
66
- expect(ids).toContain(withData.id);
67
- expect(ids).not.toContain(withEmpty.id);
68
- });
69
- });
@@ -1,310 +0,0 @@
1
- /**
2
- * Unit tests for Query class
3
- * Tests query builder methods and structure
4
- */
5
- import { describe, test, expect, beforeAll } from 'bun:test';
6
- import { Query, FilterOp } from '../../../query/Query';
7
- import { TestUser, TestProduct } from '../../fixtures/components';
8
- import { ensureComponentsRegistered } from '../../utils';
9
-
10
- describe('Query', () => {
11
- beforeAll(async () => {
12
- await ensureComponentsRegistered(TestUser, TestProduct);
13
- });
14
-
15
- describe('constructor', () => {
16
- test('creates a new query instance', () => {
17
- const query = new Query();
18
- expect(query).toBeDefined();
19
- });
20
-
21
- test('creates query with transaction', () => {
22
- // Query accepts optional transaction parameter
23
- const query = new Query();
24
- expect(query).toBeDefined();
25
- });
26
- });
27
-
28
- describe('findById()', () => {
29
- test('sets up query to find by ID', () => {
30
- const query = new Query();
31
- const result = query.findById('test-id-123');
32
- expect(result).toBe(query); // Returns this for chaining
33
- });
34
-
35
- test('throws for empty string id', () => {
36
- const query = new Query();
37
- expect(() => query.findById('')).toThrow();
38
- });
39
-
40
- test('throws for whitespace id', () => {
41
- const query = new Query();
42
- expect(() => query.findById(' ')).toThrow();
43
- });
44
- });
45
-
46
- describe('with()', () => {
47
- test('adds component requirement to query', () => {
48
- const query = new Query();
49
- const result = query.with(TestUser);
50
- // with() returns same instance for chaining (type changes for type accumulation)
51
- expect(result as any).toBe(query as any);
52
- });
53
-
54
- test('allows chaining multiple with() calls', () => {
55
- const query = new Query();
56
- const result = query.with(TestUser).with(TestProduct);
57
- expect(result as any).toBe(query as any);
58
- });
59
-
60
- test('accepts component with filters', () => {
61
- const query = new Query();
62
- const result = query.with(TestUser, {
63
- filters: [{ field: 'name', operator: FilterOp.EQ, value: 'John' }]
64
- });
65
- expect(result as any).toBe(query as any);
66
- });
67
-
68
- test('accepts array of components with filters', () => {
69
- const query = new Query();
70
- const result = query.with([
71
- { component: TestUser, filters: [{ field: 'age', operator: FilterOp.GT, value: 18 }] },
72
- { component: TestProduct, filters: [{ field: 'price', operator: FilterOp.LT, value: 100 }] }
73
- ]);
74
- expect(result).toBe(query);
75
- });
76
- });
77
-
78
- describe('without()', () => {
79
- test('excludes entities with specified component', () => {
80
- const query = new Query();
81
- const result = query.with(TestUser).without(TestProduct);
82
- expect(result as any).toBe(query as any);
83
- });
84
- });
85
-
86
- describe('excludeEntityId()', () => {
87
- test('excludes specific entity from results', () => {
88
- const query = new Query();
89
- const result = query.excludeEntityId('entity-to-exclude');
90
- expect(result).toBe(query);
91
- });
92
- });
93
-
94
- describe('populate()', () => {
95
- test('enables component population', () => {
96
- const query = new Query();
97
- const result = query.with(TestUser).populate();
98
- expect(result as any).toBe(query as any);
99
- });
100
- });
101
-
102
- describe('eagerLoadComponents()', () => {
103
- test('sets up eager loading for components', () => {
104
- const query = new Query();
105
- const result = query.with(TestUser).eagerLoadComponents([TestUser, TestProduct]);
106
- expect(result as any).toBe(query as any);
107
- });
108
- });
109
-
110
- describe('eagerLoad()', () => {
111
- test('is an alias for eagerLoadComponents', () => {
112
- const query = new Query();
113
- const result = query.with(TestUser).eagerLoad([TestUser]);
114
- expect(result as any).toBe(query as any);
115
- });
116
- });
117
-
118
- describe('take()', () => {
119
- test('sets limit on query results', () => {
120
- const query = new Query();
121
- const result = query.with(TestUser).take(10);
122
- expect(result as any).toBe(query as any);
123
- });
124
- });
125
-
126
- describe('offset()', () => {
127
- test('sets offset for pagination', () => {
128
- const query = new Query();
129
- const result = query.with(TestUser).offset(20);
130
- expect(result as any).toBe(query as any);
131
- });
132
- });
133
-
134
- describe('sortBy()', () => {
135
- test('sets sort order', () => {
136
- const query = new Query();
137
- const result = query.with(TestUser).sortBy(TestUser, 'name', 'ASC');
138
- expect(result as any).toBe(query as any);
139
- });
140
-
141
- test('accepts DESC direction', () => {
142
- const query = new Query();
143
- const result = query.with(TestUser).sortBy(TestUser, 'age', 'DESC');
144
- expect(result as any).toBe(query as any);
145
- });
146
-
147
- test('accepts nullsFirst option', () => {
148
- const query = new Query();
149
- const result = query.with(TestUser).sortBy(TestUser, 'bio', 'ASC', true);
150
- expect(result as any).toBe(query as any);
151
- });
152
-
153
- test('throws if component not in query', () => {
154
- const query = new Query();
155
- expect(() => query.sortBy(TestUser, 'name')).toThrow();
156
- });
157
- });
158
-
159
- describe('debugMode()', () => {
160
- test('enables debug mode', () => {
161
- const query = new Query();
162
- const result = query.debugMode(true);
163
- expect(result).toBe(query);
164
- });
165
-
166
- test('can disable debug mode', () => {
167
- const query = new Query();
168
- const result = query.debugMode(false);
169
- expect(result).toBe(query);
170
- });
171
- });
172
-
173
- describe('noCache()', () => {
174
- test('bypasses prepared statement cache by default', () => {
175
- const query = new Query();
176
- const result = query.noCache();
177
- expect(result).toBe(query);
178
- });
179
-
180
- test('accepts cache options', () => {
181
- const query = new Query();
182
- const result = query.noCache({ preparedStatement: true, component: true });
183
- expect(result).toBe(query);
184
- });
185
- });
186
-
187
- describe('filter()', () => {
188
- test('creates filter object', () => {
189
- const filter = Query.filter('name', FilterOp.EQ, 'John');
190
- expect(filter.field).toBe('name');
191
- expect(filter.operator).toBe(FilterOp.EQ);
192
- expect(filter.value).toBe('John');
193
- });
194
-
195
- test('accepts empty string value', () => {
196
- const filter = Query.filter('name', FilterOp.EQ, '');
197
- expect(filter.value).toBe('');
198
- });
199
-
200
- test('accepts whitespace value', () => {
201
- const filter = Query.filter('name', FilterOp.EQ, ' ');
202
- expect(filter.value).toBe(' ');
203
- });
204
- });
205
-
206
- describe('typedFilter()', () => {
207
- test('creates typed filter object', () => {
208
- const filter = Query.typedFilter(TestUser, 'name', FilterOp.EQ, 'John');
209
- expect(filter.field).toBe('name');
210
- expect(filter.operator).toBe(FilterOp.EQ);
211
- expect(filter.value).toBe('John');
212
- });
213
- });
214
-
215
- describe('filters()', () => {
216
- test('creates filter options from multiple filters', () => {
217
- const filter1 = Query.filter('name', FilterOp.EQ, 'John');
218
- const filter2 = Query.filter('age', FilterOp.GT, 18);
219
- const options = Query.filters(filter1, filter2);
220
-
221
- expect(options.filters).toBeDefined();
222
- expect(options.filters?.length).toBe(2);
223
- });
224
- });
225
-
226
- describe('FilterOp', () => {
227
- test('has all expected operators', () => {
228
- expect(FilterOp.EQ).toBeDefined();
229
- expect(FilterOp.NEQ).toBeDefined();
230
- expect(FilterOp.GT).toBeDefined();
231
- expect(FilterOp.GTE).toBeDefined();
232
- expect(FilterOp.LT).toBeDefined();
233
- expect(FilterOp.LTE).toBeDefined();
234
- expect(FilterOp.LIKE).toBeDefined();
235
- expect(FilterOp.IN).toBeDefined();
236
- expect(FilterOp.NOT_IN).toBeDefined();
237
- });
238
- });
239
-
240
- describe('chaining', () => {
241
- test('supports full query chain', () => {
242
- const query = new Query()
243
- .with(TestUser, { filters: [Query.filter('age', FilterOp.GTE, 18)] })
244
- .with(TestProduct)
245
- .without(TestProduct)
246
- .take(10)
247
- .offset(0)
248
- .sortBy(TestUser, 'name')
249
- .populate()
250
- .debugMode(false)
251
- .noCache();
252
-
253
- expect(query).toBeDefined();
254
- });
255
- });
256
-
257
- describe('sum()', () => {
258
- test('throws if component not in query', async () => {
259
- const query = new Query();
260
- // TestProduct not added via .with()
261
- await expect(query.sum(TestProduct, 'price')).rejects.toThrow(
262
- /not included in the query/
263
- );
264
- });
265
-
266
- test('throws if component is not registered', async () => {
267
- class UnregisteredComponent {
268
- value!: number;
269
- }
270
- const query = new Query();
271
- // @ts-expect-error - Testing with unregistered component
272
- await expect(query.sum(UnregisteredComponent, 'value')).rejects.toThrow(
273
- /not registered/
274
- );
275
- });
276
-
277
- test('returns a Promise', () => {
278
- const query = new Query().with(TestProduct);
279
- const result = query.sum(TestProduct, 'price');
280
- expect(result).toBeInstanceOf(Promise);
281
- });
282
- });
283
-
284
- describe('average()', () => {
285
- test('throws if component not in query', async () => {
286
- const query = new Query();
287
- // TestProduct not added via .with()
288
- await expect(query.average(TestProduct, 'price')).rejects.toThrow(
289
- /not included in the query/
290
- );
291
- });
292
-
293
- test('throws if component is not registered', async () => {
294
- class UnregisteredComponent {
295
- value!: number;
296
- }
297
- const query = new Query();
298
- // @ts-expect-error - Testing with unregistered component
299
- await expect(query.average(UnregisteredComponent, 'value')).rejects.toThrow(
300
- /not registered/
301
- );
302
- });
303
-
304
- test('returns a Promise', () => {
305
- const query = new Query().with(TestProduct);
306
- const result = query.average(TestProduct, 'price');
307
- expect(result).toBeInstanceOf(Promise);
308
- });
309
- });
310
- });