bunsane 0.3.1 → 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 (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. 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
- });