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,141 +1,141 @@
1
- import type { SQL } from "bun";
2
- import { logger as MainLogger } from "../core/Logger";
3
- import { runWithSignal } from "./cancellable";
4
-
5
- const logger = MainLogger.child({ scope: "db" });
6
-
7
- const SLOW_MS = parseInt(process.env.BUNSANE_DB_SLOW_MS ?? '500', 10);
8
-
9
- export type DataLoaderKind = 'entity' | 'component' | 'relation';
10
-
11
- interface DbStatsInternal {
12
- totalCount: number;
13
- totalMs: number;
14
- maxMs: number;
15
- slowCount: number;
16
- abortedCount: number;
17
- inFlight: number;
18
- inFlightMax: number;
19
- dataLoaderCalls: { entity: number; component: number; relation: number };
20
- }
21
-
22
- const stats: DbStatsInternal = {
23
- totalCount: 0,
24
- totalMs: 0,
25
- maxMs: 0,
26
- slowCount: 0,
27
- abortedCount: 0,
28
- inFlight: 0,
29
- inFlightMax: 0,
30
- dataLoaderCalls: { entity: 0, component: 0, relation: 0 },
31
- };
32
-
33
- /**
34
- * Per-request counter incremented when current request context is reachable
35
- * via the (request as any).__bunsaneStats pointer. We accept that as a
36
- * parameter from the call site so this module stays free of GraphQL imports.
37
- */
38
- export interface PerRequestCounters {
39
- dbQueryCount: number;
40
- }
41
-
42
- /**
43
- * Execute `db.unsafe(sql, params)` with optional AbortSignal cancellation
44
- * and roundtrip telemetry. On abort the in-flight query is cancelled via
45
- * `Query.cancel()`. Total ms is recorded into module-level stats; calls
46
- * over `BUNSANE_DB_SLOW_MS` increment slowCount and emit a warn log.
47
- */
48
- export async function timedUnsafe<T = any>(
49
- db: SQL,
50
- sql: string,
51
- params: any[],
52
- signal?: AbortSignal,
53
- perRequest?: PerRequestCounters,
54
- ): Promise<T> {
55
- const t0 = performance.now();
56
- stats.inFlight++;
57
- if (stats.inFlight > stats.inFlightMax) stats.inFlightMax = stats.inFlight;
58
- if (perRequest) perRequest.dbQueryCount++;
59
- let aborted = false;
60
- try {
61
- const q = (db as any).unsafe(sql, params);
62
- return await runWithSignal<T>(q, signal);
63
- } catch (err) {
64
- if ((err as Error)?.name === 'AbortError' || signal?.aborted) {
65
- aborted = true;
66
- stats.abortedCount++;
67
- }
68
- throw err;
69
- } finally {
70
- const dt = performance.now() - t0;
71
- stats.inFlight--;
72
- stats.totalCount++;
73
- stats.totalMs += dt;
74
- if (dt > stats.maxMs) stats.maxMs = dt;
75
- if (SLOW_MS > 0 && dt > SLOW_MS && !aborted) {
76
- stats.slowCount++;
77
- logger.warn(
78
- {
79
- durationMs: Math.round(dt),
80
- thresholdMs: SLOW_MS,
81
- sqlSnippet: sql.length > 200 ? sql.slice(0, 200) + '…' : sql,
82
- msg: 'Slow DB call',
83
- },
84
- 'Slow DB call',
85
- );
86
- }
87
- }
88
- }
89
-
90
- /**
91
- * Increment the per-kind DataLoader counter. Called from inside DataLoader
92
- * batch functions so /metrics + access log can attribute load patterns.
93
- *
94
- * `perRequest` is loosely typed because RequestContext's `RequestStats`
95
- * (defined in core/RequestContext.ts) extends `PerRequestCounters` with
96
- * extra fields like `dataLoaderCalls`. We accept either shape here without
97
- * importing the higher-level type (which would create a cycle).
98
- */
99
- export function incrementDataLoaderCall(
100
- kind: DataLoaderKind,
101
- perRequest?: PerRequestCounters | { dataLoaderCalls?: { entity: number; component: number; relation: number } },
102
- ): void {
103
- stats.dataLoaderCalls[kind]++;
104
- const dlc = (perRequest as any)?.dataLoaderCalls;
105
- if (dlc) dlc[kind]++;
106
- }
107
-
108
- /**
109
- * Snapshot of accumulated DB stats for the /metrics endpoint.
110
- */
111
- export function getDbStats() {
112
- const avgMs = stats.totalCount > 0 ? stats.totalMs / stats.totalCount : 0;
113
- return {
114
- totalCount: stats.totalCount,
115
- totalMs: Math.round(stats.totalMs),
116
- maxMs: Math.round(stats.maxMs),
117
- avgMs: Number(avgMs.toFixed(2)),
118
- slowCount: stats.slowCount,
119
- abortedCount: stats.abortedCount,
120
- inFlight: stats.inFlight,
121
- inFlightMax: stats.inFlightMax,
122
- slowThresholdMs: SLOW_MS,
123
- dataLoaderCalls: { ...stats.dataLoaderCalls },
124
- };
125
- }
126
-
127
- /**
128
- * Reset counters. Intended for tests only.
129
- */
130
- export function resetDbStats(): void {
131
- stats.totalCount = 0;
132
- stats.totalMs = 0;
133
- stats.maxMs = 0;
134
- stats.slowCount = 0;
135
- stats.abortedCount = 0;
136
- stats.inFlight = 0;
137
- stats.inFlightMax = 0;
138
- stats.dataLoaderCalls.entity = 0;
139
- stats.dataLoaderCalls.component = 0;
140
- stats.dataLoaderCalls.relation = 0;
141
- }
1
+ import type { SQL } from "bun";
2
+ import { logger as MainLogger } from "../core/Logger";
3
+ import { runWithSignal } from "./cancellable";
4
+
5
+ const logger = MainLogger.child({ scope: "db" });
6
+
7
+ const SLOW_MS = parseInt(process.env.BUNSANE_DB_SLOW_MS ?? '500', 10);
8
+
9
+ export type DataLoaderKind = 'entity' | 'component' | 'relation';
10
+
11
+ interface DbStatsInternal {
12
+ totalCount: number;
13
+ totalMs: number;
14
+ maxMs: number;
15
+ slowCount: number;
16
+ abortedCount: number;
17
+ inFlight: number;
18
+ inFlightMax: number;
19
+ dataLoaderCalls: { entity: number; component: number; relation: number };
20
+ }
21
+
22
+ const stats: DbStatsInternal = {
23
+ totalCount: 0,
24
+ totalMs: 0,
25
+ maxMs: 0,
26
+ slowCount: 0,
27
+ abortedCount: 0,
28
+ inFlight: 0,
29
+ inFlightMax: 0,
30
+ dataLoaderCalls: { entity: 0, component: 0, relation: 0 },
31
+ };
32
+
33
+ /**
34
+ * Per-request counter incremented when current request context is reachable
35
+ * via the (request as any).__bunsaneStats pointer. We accept that as a
36
+ * parameter from the call site so this module stays free of GraphQL imports.
37
+ */
38
+ export interface PerRequestCounters {
39
+ dbQueryCount: number;
40
+ }
41
+
42
+ /**
43
+ * Execute `db.unsafe(sql, params)` with optional AbortSignal cancellation
44
+ * and roundtrip telemetry. On abort the in-flight query is cancelled via
45
+ * `Query.cancel()`. Total ms is recorded into module-level stats; calls
46
+ * over `BUNSANE_DB_SLOW_MS` increment slowCount and emit a warn log.
47
+ */
48
+ export async function timedUnsafe<T = any>(
49
+ db: SQL,
50
+ sql: string,
51
+ params: any[],
52
+ signal?: AbortSignal,
53
+ perRequest?: PerRequestCounters,
54
+ ): Promise<T> {
55
+ const t0 = performance.now();
56
+ stats.inFlight++;
57
+ if (stats.inFlight > stats.inFlightMax) stats.inFlightMax = stats.inFlight;
58
+ if (perRequest) perRequest.dbQueryCount++;
59
+ let aborted = false;
60
+ try {
61
+ const q = (db as any).unsafe(sql, params);
62
+ return await runWithSignal<T>(q, signal);
63
+ } catch (err) {
64
+ if ((err as Error)?.name === 'AbortError' || signal?.aborted) {
65
+ aborted = true;
66
+ stats.abortedCount++;
67
+ }
68
+ throw err;
69
+ } finally {
70
+ const dt = performance.now() - t0;
71
+ stats.inFlight--;
72
+ stats.totalCount++;
73
+ stats.totalMs += dt;
74
+ if (dt > stats.maxMs) stats.maxMs = dt;
75
+ if (SLOW_MS > 0 && dt > SLOW_MS && !aborted) {
76
+ stats.slowCount++;
77
+ logger.warn(
78
+ {
79
+ durationMs: Math.round(dt),
80
+ thresholdMs: SLOW_MS,
81
+ sqlSnippet: sql.length > 200 ? sql.slice(0, 200) + '…' : sql,
82
+ msg: 'Slow DB call',
83
+ },
84
+ 'Slow DB call',
85
+ );
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Increment the per-kind DataLoader counter. Called from inside DataLoader
92
+ * batch functions so /metrics + access log can attribute load patterns.
93
+ *
94
+ * `perRequest` is loosely typed because RequestContext's `RequestStats`
95
+ * (defined in core/RequestContext.ts) extends `PerRequestCounters` with
96
+ * extra fields like `dataLoaderCalls`. We accept either shape here without
97
+ * importing the higher-level type (which would create a cycle).
98
+ */
99
+ export function incrementDataLoaderCall(
100
+ kind: DataLoaderKind,
101
+ perRequest?: PerRequestCounters | { dataLoaderCalls?: { entity: number; component: number; relation: number } },
102
+ ): void {
103
+ stats.dataLoaderCalls[kind]++;
104
+ const dlc = (perRequest as any)?.dataLoaderCalls;
105
+ if (dlc) dlc[kind]++;
106
+ }
107
+
108
+ /**
109
+ * Snapshot of accumulated DB stats for the /metrics endpoint.
110
+ */
111
+ export function getDbStats() {
112
+ const avgMs = stats.totalCount > 0 ? stats.totalMs / stats.totalCount : 0;
113
+ return {
114
+ totalCount: stats.totalCount,
115
+ totalMs: Math.round(stats.totalMs),
116
+ maxMs: Math.round(stats.maxMs),
117
+ avgMs: Number(avgMs.toFixed(2)),
118
+ slowCount: stats.slowCount,
119
+ abortedCount: stats.abortedCount,
120
+ inFlight: stats.inFlight,
121
+ inFlightMax: stats.inFlightMax,
122
+ slowThresholdMs: SLOW_MS,
123
+ dataLoaderCalls: { ...stats.dataLoaderCalls },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Reset counters. Intended for tests only.
129
+ */
130
+ export function resetDbStats(): void {
131
+ stats.totalCount = 0;
132
+ stats.totalMs = 0;
133
+ stats.maxMs = 0;
134
+ stats.slowCount = 0;
135
+ stats.abortedCount = 0;
136
+ stats.inFlight = 0;
137
+ stats.inFlightMax = 0;
138
+ stats.dataLoaderCalls.entity = 0;
139
+ stats.dataLoaderCalls.component = 0;
140
+ stats.dataLoaderCalls.relation = 0;
141
+ }
@@ -319,19 +319,13 @@ export async function handleStudioArcheTypeDeleteRequest(
319
319
  .join(", ");
320
320
 
321
321
  // Delete in correct order to avoid foreign key constraint violations
322
- // 1. Delete from entity_components (junction table)
323
- await db.unsafe(
324
- `DELETE FROM entity_components WHERE entity_id IN (${idPlaceholders})`,
325
- entityIds
326
- );
327
-
328
- // 2. Delete from components
322
+ // 1. Delete from components (membership source of truth)
329
323
  await db.unsafe(
330
324
  `DELETE FROM components WHERE entity_id IN (${idPlaceholders})`,
331
325
  entityIds
332
326
  );
333
327
 
334
- // 3. Delete from entities
328
+ // 2. Delete from entities
335
329
  await db.unsafe(
336
330
  `DELETE FROM entities WHERE id IN (${idPlaceholders})`,
337
331
  entityIds
@@ -179,7 +179,12 @@ export async function handleStudioTableDeleteRequest(
179
179
 
180
180
  export async function handleGetTables(): Promise<Response> {
181
181
  try {
182
- // Fetch all tables except ECS tables
182
+ // Exclude framework-internal tables and the legacy entity_components table.
183
+ // entity_components is no longer written by the framework (Phase 3 of
184
+ // docs/ENTITY_COMPONENTS_REMOVAL_PLAN.md) but may still exist as an orphan
185
+ // in upgraded databases. Keeping it out of the Studio listing avoids
186
+ // exposing a confusingly schema'd legacy table with no ECS UI support.
187
+ // Users are directed to drop it via the startup orphan-notice log.
183
188
  const ecsTables = ['components', 'entities', 'entity_components', 'spatial_ref_sys'];
184
189
  const ecsTablePlaceholders = ecsTables.map((_, index) => `$${index + 1}`).join(", ");
185
190
 
package/gql/index.ts CHANGED
@@ -116,7 +116,7 @@ const maskError = (error: any, message: string): GraphQLError => {
116
116
  }
117
117
 
118
118
  // Pass through known application-level GraphQL error codes
119
- const isGQLError = (e: any): e is GraphQLError =>
119
+ const isGQLError = (e: any): e is { message: string; extensions?: Record<string, unknown> } =>
120
120
  e instanceof GraphQLError ||
121
121
  (e !== null && typeof e === 'object' && 'extensions' in e && 'message' in e && typeof e.message === 'string');
122
122
  const knownCodes = ['FORBIDDEN', 'NOT_FOUND', 'BAD_USER_INPUT', 'BAD_REQUEST'];
@@ -18,24 +18,45 @@ export class ResolverGeneratorVisitor extends GraphVisitor {
18
18
  this.services = services;
19
19
  this.resolverBuilder = new ResolverBuilder();
20
20
 
21
- // Add Date scalar resolver
21
+ // Add Date scalar resolver.
22
+ // Safety net: gqloom's `z.date()` currently maps to GraphQLString
23
+ // (see @gqloom/zod isZodDate → GraphQLString), so this custom Date
24
+ // scalar is rarely wired by the auto-generated archetype schema.
25
+ // Component-prop leaf resolvers normalize Date → ISO string upstream
26
+ // (core/archetype/fieldResolvers.ts) so GraphQLString coercion does
27
+ // not call Date.valueOf() and emit epoch ms. We still harden this
28
+ // serializer to accept Date, number, and numeric-string inputs in
29
+ // case a downstream user types a field as the `Date` scalar
30
+ // directly.
22
31
  this.resolverBuilder.addScalarResolver('Date', {
23
32
  serialize: (value: any) => {
24
- if (value instanceof Date) {
25
- return value.toISOString();
33
+ if (value === null || value === undefined) return value;
34
+ if (value instanceof Date) return value.toISOString();
35
+ if (typeof value === 'number') return new Date(value).toISOString();
36
+ if (typeof value === 'string') {
37
+ if (/^\d+$/.test(value)) {
38
+ return new Date(Number(value)).toISOString();
39
+ }
40
+ return value;
26
41
  }
27
- return value;
42
+ throw new Error(`Date scalar cannot serialize ${typeof value}`);
28
43
  },
29
44
  parseValue: (value: any) => {
30
45
  if (typeof value === 'string') {
31
46
  return new Date(value);
32
47
  }
48
+ if (typeof value === 'number') {
49
+ return new Date(value);
50
+ }
33
51
  return value;
34
52
  },
35
53
  parseLiteral: (ast: any) => {
36
54
  if (ast.kind === 'StringValue') {
37
55
  return new Date(ast.value);
38
56
  }
57
+ if (ast.kind === 'IntValue') {
58
+ return new Date(Number(ast.value));
59
+ }
39
60
  return null;
40
61
  }
41
62
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunsane",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "author": {
5
5
  "name": "yaaruu"
6
6
  },
@@ -14,6 +14,27 @@
14
14
  ],
15
15
  "module": "index.ts",
16
16
  "type": "module",
17
+ "files": [
18
+ "index.ts",
19
+ "config",
20
+ "core",
21
+ "database",
22
+ "endpoints",
23
+ "gql",
24
+ "plugins",
25
+ "query",
26
+ "rest",
27
+ "scheduler",
28
+ "service",
29
+ "storage",
30
+ "studio/dist",
31
+ "swagger",
32
+ "types",
33
+ "upload",
34
+ "utils",
35
+ "tsconfig.json",
36
+ "CHANGELOG.md"
37
+ ],
17
38
  "scripts": {
18
39
  "build": "bun run build:studio && tsc",
19
40
  "build:studio": "cd studio && bun install && bun run build",
package/query/CTENode.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { QueryNode } from "./QueryNode";
2
2
  import type { QueryResult } from "./QueryNode";
3
3
  import { QueryContext } from "./QueryContext";
4
+ import { getMembershipTable } from "./membershipSource";
4
5
 
5
6
  export class CTENode extends QueryNode {
6
7
  public execute(context: QueryContext): QueryResult {
7
8
  // Generate CTE for base entity filtering
8
9
  const componentIds = Array.from(context.componentIds);
9
10
  const excludedIds = Array.from(context.excludedComponentIds);
11
+ const membershipTable = getMembershipTable();
10
12
 
11
13
  if (componentIds.length === 0) {
12
14
  throw new Error("CTENode requires at least one component type to filter on");
@@ -26,7 +28,7 @@ export class CTENode extends QueryNode {
26
28
  if (excludedIds.length > 0) {
27
29
  const excludedPlaceholders = excludedIds.map((id) => `$${context.addParam(id)}`).join(', ');
28
30
  exclusionCondition = ` AND NOT EXISTS (
29
- SELECT 1 FROM entity_components ec_ex
31
+ SELECT 1 FROM ${membershipTable} ec_ex
30
32
  WHERE ec_ex.entity_id = ec.entity_id
31
33
  AND ec_ex.type_id IN (${excludedPlaceholders})
32
34
  AND ec_ex.deleted_at IS NULL
@@ -45,7 +47,7 @@ export class CTENode extends QueryNode {
45
47
  // Single component - simple query, no INTERSECT needed
46
48
  const paramIdx = context.addParam(componentIds[0]);
47
49
  cteSql += ` SELECT DISTINCT ec.entity_id\n`;
48
- cteSql += ` FROM entity_components ec\n`;
50
+ cteSql += ` FROM ${membershipTable} ec\n`;
49
51
  cteSql += ` WHERE ec.type_id = $${paramIdx}::text\n`;
50
52
  cteSql += ` AND ec.deleted_at IS NULL\n`;
51
53
  if (cursorCondition) cteSql += ` ${cursorCondition.trim()}\n`;
@@ -57,7 +59,7 @@ export class CTENode extends QueryNode {
57
59
  // then efficiently merge results, avoiding Cartesian product explosion
58
60
  const intersectQueries = componentIds.map((compId) => {
59
61
  const paramIdx = context.addParam(compId);
60
- let subquery = `SELECT ec.entity_id FROM entity_components ec WHERE ec.type_id = $${paramIdx}::text AND ec.deleted_at IS NULL`;
62
+ let subquery = `SELECT ec.entity_id FROM ${membershipTable} ec WHERE ec.type_id = $${paramIdx}::text AND ec.deleted_at IS NULL`;
61
63
  // Add cursor/exclusion conditions to each subquery for efficiency
62
64
  if (cursorCondition) subquery += cursorCondition;
63
65
  if (exclusionCondition) subquery += exclusionCondition;