@cosmicdrift/kumiko-framework 1.0.0 → 2.0.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 (424) hide show
  1. package/README.md +9 -38
  2. package/package.json +21 -2
  3. package/src/__tests__/anonymous-access.integration.test.ts +185 -0
  4. package/src/__tests__/consumer-cli.integration.test.ts +172 -0
  5. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  6. package/src/__tests__/full-stack.integration.test.ts +1 -1
  7. package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
  8. package/src/__tests__/schema-cli.integration.test.ts +50 -0
  9. package/src/__tests__/store-table.integration.test.ts +93 -0
  10. package/src/api/__tests__/api.test.ts +373 -0
  11. package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  13. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
  14. package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
  15. package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
  16. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
  17. package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
  18. package/src/api/__tests__/batch.integration.test.ts +101 -11
  19. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  20. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  21. package/src/api/__tests__/jwt.test.ts +200 -1
  22. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
  23. package/src/api/__tests__/pat-scope.test.ts +36 -0
  24. package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
  25. package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
  26. package/src/api/__tests__/request-id-middleware.test.ts +51 -0
  27. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  28. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  29. package/src/api/__tests__/sse-broker.test.ts +57 -0
  30. package/src/api/__tests__/sse-route.test.ts +4 -0
  31. package/src/api/api-constants.ts +11 -0
  32. package/src/api/auth-middleware.ts +248 -40
  33. package/src/api/auth-routes.ts +576 -95
  34. package/src/api/index.ts +13 -4
  35. package/src/api/jwt.ts +170 -11
  36. package/src/api/pat-scope.ts +14 -0
  37. package/src/api/pii-leak-guard.ts +47 -0
  38. package/src/api/request-context.ts +3 -0
  39. package/src/api/request-id-middleware.ts +2 -0
  40. package/src/api/routes.ts +169 -2
  41. package/src/api/server.ts +112 -16
  42. package/src/api/sse-broker.ts +39 -0
  43. package/src/bun-db/__tests__/PATTERN.md +0 -1
  44. package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
  45. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  46. package/src/bun-db/__tests__/write-brand.test.ts +21 -0
  47. package/src/bun-db/connection.ts +3 -3
  48. package/src/bun-db/index.ts +2 -0
  49. package/src/bun-db/query.ts +105 -32
  50. package/src/consumer-cli.ts +134 -0
  51. package/src/crypto/__tests__/blind-index.test.ts +130 -0
  52. package/src/crypto/__tests__/event-pii.test.ts +161 -0
  53. package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
  54. package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
  55. package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
  56. package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
  57. package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
  58. package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
  59. package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
  60. package/src/crypto/blind-index.ts +118 -0
  61. package/src/crypto/event-pii.ts +70 -0
  62. package/src/crypto/in-memory-kms-adapter.ts +50 -0
  63. package/src/crypto/index.ts +67 -0
  64. package/src/crypto/kms-adapter.ts +2 -0
  65. package/src/crypto/pg-kms-adapter.ts +295 -0
  66. package/src/crypto/pii-field-encryption.ts +248 -0
  67. package/src/crypto/request-kms-cache.ts +38 -0
  68. package/src/crypto/subject-resolver.ts +91 -0
  69. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
  70. package/src/db/__tests__/blind-index.integration.test.ts +248 -0
  71. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  72. package/src/db/__tests__/collect-table-metas.test.ts +10 -10
  73. package/src/db/__tests__/config-seed.integration.test.ts +13 -5
  74. package/src/db/__tests__/dialect-instant.test.ts +1 -4
  75. package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
  76. package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
  77. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  78. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
  79. package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
  80. package/src/db/__tests__/feature-table-sources.test.ts +34 -0
  81. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
  82. package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
  83. package/src/db/__tests__/located-timestamp.test.ts +19 -0
  84. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  85. package/src/db/__tests__/migrate-runner.test.ts +61 -0
  86. package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
  87. package/src/db/__tests__/rebuild-marker.test.ts +13 -3
  88. package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
  89. package/src/db/__tests__/schema-inspection.test.ts +7 -0
  90. package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
  91. package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
  92. package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
  93. package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
  94. package/src/db/api.ts +2 -2
  95. package/src/db/apply-entity-event.ts +18 -14
  96. package/src/db/blind-index-cleanup.ts +55 -0
  97. package/src/db/bun-provider.ts +2 -2
  98. package/src/db/collect-table-metas.ts +6 -7
  99. package/src/db/config-seed.ts +9 -9
  100. package/src/db/connection.ts +7 -12
  101. package/src/db/cursor.ts +1 -18
  102. package/src/db/dialect.ts +20 -26
  103. package/src/db/entity-field-encryption.ts +81 -24
  104. package/src/db/entity-table-meta-types.ts +2 -0
  105. package/src/db/entity-table-meta.ts +63 -90
  106. package/src/db/event-store-executor-context.ts +404 -0
  107. package/src/db/event-store-executor-read.ts +275 -0
  108. package/src/db/event-store-executor-write.ts +644 -0
  109. package/src/db/event-store-executor.ts +28 -1155
  110. package/src/db/feature-table-sources.ts +6 -5
  111. package/src/db/index.ts +20 -3
  112. package/src/db/located-timestamp.ts +4 -0
  113. package/src/db/migrate-generator.ts +39 -6
  114. package/src/db/migrate-runner.ts +107 -11
  115. package/src/db/pg-error.ts +9 -1
  116. package/src/db/postgres-provider.ts +2 -2
  117. package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
  118. package/src/db/queries/backfill-pii.ts +277 -0
  119. package/src/db/queries/ddl.ts +45 -0
  120. package/src/db/queries/event-consumer.ts +35 -2
  121. package/src/db/queries/event-store.ts +126 -19
  122. package/src/db/queries/projection-rebuild.ts +22 -8
  123. package/src/db/queries/shadow-swap.ts +183 -0
  124. package/src/db/queries/test-stack.ts +4 -30
  125. package/src/db/query-api.ts +1 -0
  126. package/src/db/query.ts +1 -0
  127. package/src/db/rebuild-marker.ts +18 -2
  128. package/src/db/reference-data.ts +2 -3
  129. package/src/db/replay-migration-sql.ts +257 -0
  130. package/src/db/schema-inspection.ts +1 -1
  131. package/src/db/table-builder.ts +102 -71
  132. package/src/db/tenant-db.ts +15 -53
  133. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  134. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  135. package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
  136. package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
  137. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
  138. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
  139. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
  140. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
  141. package/src/engine/__tests__/boot-validator.test.ts +219 -15
  142. package/src/engine/__tests__/build-app-schema.test.ts +82 -0
  143. package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
  144. package/src/engine/__tests__/build-target.test.ts +3 -11
  145. package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
  146. package/src/engine/__tests__/config-helpers.test.ts +16 -0
  147. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
  148. package/src/engine/__tests__/define-roles.test.ts +21 -0
  149. package/src/engine/__tests__/engine.test.ts +163 -1
  150. package/src/engine/__tests__/entity-handlers.test.ts +80 -0
  151. package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
  152. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  153. package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
  154. package/src/engine/__tests__/factories-time.test.ts +2 -66
  155. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  156. package/src/engine/__tests__/feature-manifest.test.ts +31 -1
  157. package/src/engine/__tests__/field-access.test.ts +23 -1
  158. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  159. package/src/engine/__tests__/membership-roles.test.ts +4 -10
  160. package/src/engine/__tests__/nav.test.ts +86 -1
  161. package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
  162. package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
  163. package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
  164. package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
  165. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
  166. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  167. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  168. package/src/engine/__tests__/registry.test.ts +144 -0
  169. package/src/engine/__tests__/schema-builder.test.ts +18 -0
  170. package/src/engine/__tests__/screen.test.ts +147 -1
  171. package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
  172. package/src/engine/__tests__/store-table.test.ts +229 -0
  173. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  174. package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
  175. package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
  176. package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
  177. package/src/engine/boot-validator/action-wiring.ts +149 -0
  178. package/src/engine/boot-validator/boot-check.ts +21 -0
  179. package/src/engine/boot-validator/config-deps.ts +40 -4
  180. package/src/engine/boot-validator/entity-handler.ts +20 -21
  181. package/src/engine/boot-validator/entity-list-screens.ts +88 -0
  182. package/src/engine/boot-validator/gdpr-storage.ts +0 -20
  183. package/src/engine/boot-validator/i18n-keys.ts +58 -4
  184. package/src/engine/boot-validator/index.ts +33 -12
  185. package/src/engine/boot-validator/nav.ts +125 -0
  186. package/src/engine/boot-validator/pii-retention.ts +95 -10
  187. package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
  188. package/src/engine/boot-validator/workspaces.ts +68 -0
  189. package/src/engine/build-app-schema.ts +16 -0
  190. package/src/engine/build-config-feature-schema.ts +44 -7
  191. package/src/engine/codemod/pipeline-codemod.ts +5 -5
  192. package/src/engine/config-helpers.ts +15 -0
  193. package/src/engine/constants.ts +32 -6
  194. package/src/engine/create-app.ts +11 -0
  195. package/src/engine/define-feature.ts +95 -947
  196. package/src/engine/define-handler.ts +28 -94
  197. package/src/engine/define-workflow.ts +1 -1
  198. package/src/engine/effective-features.ts +12 -2
  199. package/src/engine/entity-handlers.ts +76 -11
  200. package/src/engine/extensions/tenant-data.ts +19 -0
  201. package/src/engine/extensions/user-data.ts +29 -2
  202. package/src/engine/factories.ts +8 -49
  203. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
  204. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
  205. package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
  206. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  207. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
  208. package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
  209. package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
  210. package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
  211. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
  212. package/src/engine/feature-ast/extractors/events.ts +322 -0
  213. package/src/engine/feature-ast/extractors/handlers.ts +234 -0
  214. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  215. package/src/engine/feature-ast/extractors/index.ts +34 -31
  216. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  217. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  218. package/src/engine/feature-ast/extractors/round1.ts +3 -3
  219. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  220. package/src/engine/feature-ast/extractors/round6.ts +2 -36
  221. package/src/engine/feature-ast/extractors/shared.ts +64 -6
  222. package/src/engine/feature-ast/index.ts +2 -4
  223. package/src/engine/feature-ast/parse.ts +130 -23
  224. package/src/engine/feature-ast/patch.ts +39 -50
  225. package/src/engine/feature-ast/patcher.ts +37 -38
  226. package/src/engine/feature-ast/patterns.ts +50 -62
  227. package/src/engine/feature-ast/render.ts +67 -44
  228. package/src/engine/feature-builder-state.ts +168 -0
  229. package/src/engine/feature-config-events-jobs.ts +403 -0
  230. package/src/engine/feature-entity-handlers.ts +208 -0
  231. package/src/engine/feature-manifest.ts +2 -1
  232. package/src/engine/feature-ui-extensions.ts +500 -0
  233. package/src/engine/field-access.ts +13 -2
  234. package/src/engine/field-helpers.ts +31 -0
  235. package/src/engine/handler-helpers.ts +26 -0
  236. package/src/engine/hook-helpers.ts +16 -0
  237. package/src/engine/index.ts +23 -7
  238. package/src/engine/membership-roles.ts +13 -0
  239. package/src/engine/object-form.ts +27 -0
  240. package/src/engine/ownership.ts +26 -79
  241. package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
  242. package/src/engine/pattern-library/library.ts +44 -1152
  243. package/src/engine/pattern-library/mixed-schemas.ts +450 -0
  244. package/src/engine/pattern-library/opaque-schemas.ts +124 -0
  245. package/src/engine/pattern-library/shared-fields.ts +75 -0
  246. package/src/engine/pattern-library/static-schemas.ts +456 -0
  247. package/src/engine/pipeline.ts +6 -11
  248. package/src/engine/registry-facade.ts +362 -0
  249. package/src/engine/registry-ingest.ts +478 -0
  250. package/src/engine/registry-state.ts +388 -0
  251. package/src/engine/registry-validate.ts +642 -0
  252. package/src/engine/registry.ts +78 -1658
  253. package/src/engine/run-pipeline.ts +1 -1
  254. package/src/engine/schema-builder.ts +1 -0
  255. package/src/engine/screen-helpers.ts +54 -0
  256. package/src/engine/soft-delete-cleanup.ts +6 -2
  257. package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
  258. package/src/engine/steps/_duration-utils.ts +2 -0
  259. package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
  260. package/src/engine/tier-resolver-extension.ts +3 -2
  261. package/src/engine/types/config.ts +2 -482
  262. package/src/engine/types/define-handler.ts +2 -0
  263. package/src/engine/types/entity-handlers.ts +2 -0
  264. package/src/engine/types/event-type-map.ts +1 -37
  265. package/src/engine/types/feature.ts +2 -972
  266. package/src/engine/types/fields.ts +2 -675
  267. package/src/engine/types/handlers.ts +2 -774
  268. package/src/engine/types/hooks.ts +2 -184
  269. package/src/engine/types/http-route.ts +1 -54
  270. package/src/engine/types/identifiers.ts +1 -47
  271. package/src/engine/types/index.ts +86 -40
  272. package/src/engine/types/nav.ts +2 -63
  273. package/src/engine/types/ownership.ts +2 -0
  274. package/src/engine/types/projection.ts +2 -138
  275. package/src/engine/types/relations.ts +1 -51
  276. package/src/engine/types/screen.ts +2 -574
  277. package/src/engine/types/step.ts +2 -334
  278. package/src/engine/types/target-ref.ts +1 -21
  279. package/src/engine/types/tree-node.ts +1 -132
  280. package/src/engine/types/workspace.ts +2 -49
  281. package/src/engine/validate-projection-allowlist.ts +6 -6
  282. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
  283. package/src/entrypoint/index.ts +49 -6
  284. package/src/errors/classes.ts +22 -1
  285. package/src/errors/field-issue.ts +0 -3
  286. package/src/errors/index.ts +1 -1
  287. package/src/errors/write-error-info.ts +10 -22
  288. package/src/es-ops/README.md +1 -1
  289. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  290. package/src/es-ops/context.ts +4 -2
  291. package/src/es-ops/types.ts +8 -1
  292. package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
  293. package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
  294. package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
  295. package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
  296. package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
  297. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
  298. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  299. package/src/event-store/admin-api.ts +11 -4
  300. package/src/event-store/errors.ts +2 -35
  301. package/src/event-store/event-store.ts +64 -78
  302. package/src/event-store/events-schema.ts +11 -13
  303. package/src/event-store/index.ts +18 -3
  304. package/src/event-store/rebuild-dead-letter.ts +111 -0
  305. package/src/event-store/snapshot.ts +63 -40
  306. package/src/event-store/types.ts +2 -0
  307. package/src/files/__tests__/build-storage-key.test.ts +28 -0
  308. package/src/files/__tests__/file-ref-entity.test.ts +8 -0
  309. package/src/files/__tests__/files.integration.test.ts +24 -0
  310. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  311. package/src/files/__tests__/local-provider.test.ts +31 -0
  312. package/src/files/__tests__/provider-resolver.test.ts +70 -0
  313. package/src/files/__tests__/write-stream.test.ts +13 -0
  314. package/src/files/file-handle.ts +2 -19
  315. package/src/files/file-ref-entity.ts +2 -2
  316. package/src/files/file-routes.ts +13 -0
  317. package/src/files/index.ts +1 -1
  318. package/src/files/local-provider.ts +27 -8
  319. package/src/files/provider-resolver.ts +31 -20
  320. package/src/files/types.ts +13 -55
  321. package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
  322. package/src/i18n/required-surface-keys.ts +120 -1
  323. package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
  324. package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
  325. package/src/jobs/job-runner.ts +82 -12
  326. package/src/logging/types.ts +1 -7
  327. package/src/migrations/pending-rebuilds.ts +1 -1
  328. package/src/observability/__tests__/observability.integration.test.ts +4 -1
  329. package/src/observability/__tests__/recording-tracer.test.ts +6 -4
  330. package/src/observability/index.ts +2 -0
  331. package/src/observability/noop-provider.ts +0 -9
  332. package/src/observability/recording-tracer.ts +0 -12
  333. package/src/observability/standard-metrics.ts +64 -2
  334. package/src/observability/types/index.ts +1 -29
  335. package/src/observability/types/metric.ts +1 -56
  336. package/src/observability/types/provider.ts +1 -32
  337. package/src/observability/types/span.ts +1 -64
  338. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  339. package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
  340. package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
  341. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  342. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
  343. package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
  344. package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
  345. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
  346. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  347. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  348. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
  349. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
  350. package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
  351. package/src/pipeline/dispatch-batch.ts +187 -0
  352. package/src/pipeline/dispatch-query.ts +165 -0
  353. package/src/pipeline/dispatch-shared.ts +826 -0
  354. package/src/pipeline/dispatch-stream.ts +90 -0
  355. package/src/pipeline/dispatch-write.ts +451 -0
  356. package/src/pipeline/dispatcher-utils.ts +1 -1
  357. package/src/pipeline/dispatcher.ts +44 -1372
  358. package/src/pipeline/entity-cache.ts +2 -33
  359. package/src/pipeline/event-consumer-state.ts +30 -2
  360. package/src/pipeline/event-dispatcher-admin.ts +293 -0
  361. package/src/pipeline/event-dispatcher-delivery.ts +305 -0
  362. package/src/pipeline/event-dispatcher.ts +85 -542
  363. package/src/pipeline/index.ts +2 -0
  364. package/src/pipeline/msp-rebuild.ts +42 -3
  365. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  366. package/src/pipeline/projection-rebuild.ts +105 -3
  367. package/src/pipeline/system-hooks.ts +144 -1
  368. package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
  369. package/src/rate-limit/resolver.ts +16 -32
  370. package/src/schema-cli.ts +76 -16
  371. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
  372. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  373. package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
  374. package/src/search/index.ts +6 -0
  375. package/src/search/meilisearch-adapter.ts +13 -12
  376. package/src/search/reindex-entity.ts +177 -0
  377. package/src/search/types.ts +1 -39
  378. package/src/secrets/__tests__/contains-secret.test.ts +34 -0
  379. package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
  380. package/src/secrets/__tests__/envelope.test.ts +1 -1
  381. package/src/secrets/dek-cache.ts +26 -5
  382. package/src/secrets/envelope-cipher.ts +53 -0
  383. package/src/secrets/envelope.ts +5 -3
  384. package/src/secrets/index.ts +13 -1
  385. package/src/secrets/stored-envelope.ts +46 -0
  386. package/src/secrets/types.ts +2 -162
  387. package/src/stack/__tests__/event-collector.test.ts +42 -0
  388. package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
  389. package/src/stack/db.ts +2 -1
  390. package/src/stack/push-entity-projection-tables.ts +4 -3
  391. package/src/stack/redis.ts +8 -0
  392. package/src/stack/request-helper.ts +38 -2
  393. package/src/stack/table-helpers.ts +6 -4
  394. package/src/stack/test-stack.ts +249 -131
  395. package/src/testing/__tests__/late-bound.test.ts +32 -0
  396. package/src/testing/__tests__/wait-for.test.ts +59 -0
  397. package/src/testing/boot-validator-fixture.ts +121 -0
  398. package/src/testing/e2e-generator.ts +8 -0
  399. package/src/testing/file-provider-contract.ts +104 -0
  400. package/src/testing/handler-context.ts +3 -1
  401. package/src/testing/index.ts +5 -0
  402. package/src/testing/late-bound.ts +5 -3
  403. package/src/testing/mutable-master-key-provider.ts +29 -3
  404. package/src/testing/wait-for.ts +3 -0
  405. package/src/testing/without-ambient-temporal.ts +14 -0
  406. package/src/time/__tests__/tz-dateline.test.ts +8 -10
  407. package/src/time/geo-tz.ts +1 -32
  408. package/src/time/index.ts +1 -0
  409. package/src/time/legacy-date.ts +18 -0
  410. package/src/time/polyfill.ts +21 -38
  411. package/src/time/tz-context.ts +44 -85
  412. package/src/ui-types/app-schema.ts +12 -0
  413. package/src/ui-types/index.ts +20 -7
  414. package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
  415. package/src/utils/safe-json.ts +13 -1
  416. package/src/__tests__/raw-table.integration.test.ts +0 -116
  417. package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
  418. package/src/db/__tests__/encryption.test.ts +0 -39
  419. package/src/db/encryption.ts +0 -39
  420. package/src/db/row-helpers.ts +0 -4
  421. package/src/engine/__tests__/raw-table.test.ts +0 -150
  422. package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
  423. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
  424. package/src/engine/feature-ast/extractors/round4.ts +0 -1366
@@ -7,6 +7,7 @@
7
7
  // web), Runtime-Helpers (toAppSchema, isAppSchema) bleiben renderer-side
8
8
  // weil das die Layer ist die mit den AppSchemas zur Laufzeit arbeitet.
9
9
 
10
+ import type { TranslationKeys } from "../engine/types/config";
10
11
  import type { EntityDefinition } from "../engine/types/fields";
11
12
  import type { NavDefinition } from "../engine/types/nav";
12
13
  import type { ScreenDefinition } from "../engine/types/screen";
@@ -19,6 +20,17 @@ export type FeatureSchema = {
19
20
  // Flat list; resolveNavigation builds the tree at render-time from
20
21
  // the registry's indexes. Omitted when the app has no top-level nav.
21
22
  readonly navs?: readonly NavDefinition[];
23
+ // Server-authored `r.translations({ keys })`, projected verbatim — byte-
24
+ // identical keys, NOT re-prefixed with featureName (unlike the registry's
25
+ // internal mergedTranslations, which double-prefixes features that
26
+ // already qualify their own keys — see registry-ingest.ts populate
27
+ // Translations). Nav/screen labels resolve these keys directly via
28
+ // `t(label)`, so shipping anything other than the raw authored string
29
+ // would break the lookup. Omitted when a feature declares none. #1059:
30
+ // without this, nav labels only resolved when an app ALSO duplicated the
31
+ // key in `web/i18n.ts` — most bundled features never did, so labels
32
+ // rendered as raw i18n keys in the shell.
33
+ readonly translations?: TranslationKeys;
22
34
  // Workspaces — Legacy-Slot für single-feature-Apps. Bevorzugt liegt
23
35
  // workspaces auf der AppSchema-Ebene weil ihre navMembers regelmäßig
24
36
  // Cross-Feature-Navs referenzieren (siehe AppSchema-Doc). Hier als
@@ -22,6 +22,13 @@
22
22
 
23
23
  export type { ParsedRefTarget } from "../engine/parse-ref-target";
24
24
  export { parseRefTarget } from "../engine/parse-ref-target";
25
+ export {
26
+ evalFieldCondition,
27
+ isExtensionEditSection,
28
+ isFormatSpec,
29
+ normalizeEditField,
30
+ normalizeListColumn,
31
+ } from "../engine/screen-helpers";
25
32
  // Entity + field types. EntityDefinition is the canonical shape that
26
33
  // view-model builders iterate; FieldDefinition is the per-field union
27
34
  // (text, number, boolean, ...) they branch on. AccessRule is used by
@@ -46,6 +53,16 @@ export type {
46
53
  ConfigEditScreenDefinition,
47
54
  CustomScreenDefinition,
48
55
  CustomScreenRoute,
56
+ DashboardChartPanel,
57
+ DashboardCustomPanel,
58
+ DashboardFeedPanel,
59
+ DashboardFilterDefinition,
60
+ DashboardListPanel,
61
+ DashboardPanelDefinition,
62
+ DashboardProgressListPanel,
63
+ DashboardScreenDefinition,
64
+ DashboardStatGroupPanel,
65
+ DashboardStatPanel,
49
66
  EditExtensionSection,
50
67
  EditFieldSpec,
51
68
  EditFieldsSection,
@@ -57,6 +74,8 @@ export type {
57
74
  FieldRenderer,
58
75
  ListColumnSpec,
59
76
  PlatformComponent,
77
+ ProjectionDetailScreenDefinition,
78
+ ProjectionListScreenDefinition,
60
79
  RowAction,
61
80
  RowActionNavigate,
62
81
  RowActionWriteHandler,
@@ -67,14 +86,8 @@ export type {
67
86
  ScreenSlots,
68
87
  ToolbarAction,
69
88
  } from "../engine/types/screen";
70
- export {
71
- evalFieldCondition,
72
- isExtensionEditSection,
73
- isFormatSpec,
74
- normalizeEditField,
75
- normalizeListColumn,
76
- } from "../engine/types/screen";
77
89
  export type { TargetRef } from "../engine/types/target-ref";
78
90
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node";
79
91
  export type { WorkspaceDefinition } from "../engine/types/workspace";
92
+ export { PROJECTION_DETAIL_ENTITY } from "../i18n/required-surface-keys";
80
93
  export type { AppSchema, FeatureSchema, WorkspaceSchema } from "./app-schema";
@@ -0,0 +1,18 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Temporal as PolyfillTemporal } from "temporal-polyfill";
3
+ import { ensureTemporalPolyfill } from "../../time/polyfill";
4
+ import { stringifyJson } from "../safe-json";
5
+
6
+ describe("stringifyJson — Temporal.Instant (native + polyfill)", () => {
7
+ test("serializes polyfill Instant", () => {
8
+ const instant = PolyfillTemporal.Instant.from("2026-01-01T00:00:00Z");
9
+ expect(stringifyJson({ at: instant })).toBe('{"at":"2026-01-01T00:00:00Z"}');
10
+ });
11
+
12
+ test("serializes ambient/native Instant when present", async () => {
13
+ await ensureTemporalPolyfill();
14
+ const T = (globalThis as unknown as { Temporal: typeof PolyfillTemporal }).Temporal;
15
+ const instant = T.Instant.from("2026-06-15T12:00:00Z");
16
+ expect(stringifyJson({ at: instant })).toBe('{"at":"2026-06-15T12:00:00Z"}');
17
+ });
18
+ });
@@ -29,6 +29,15 @@ export function parseJsonOrThrow<T>(raw: string, context: string): T {
29
29
  }
30
30
  }
31
31
 
32
+ /** True for native or polyfill Temporal.Instant (distinct constructors). */
33
+ function isTemporalInstant(v: unknown): v is { toString(): string } {
34
+ return (
35
+ typeof v === "object" &&
36
+ v !== null &&
37
+ Object.prototype.toString.call(v) === "[object Temporal.Instant]"
38
+ );
39
+ }
40
+
32
41
  /** JSON.stringify that survives BigInt / Temporal values from DB rows. */
33
42
  export function stringifyJson(value: unknown): string {
34
43
  return JSON.stringify(value, (_key, v) => {
@@ -43,7 +52,10 @@ export function stringifyJson(value: unknown): string {
43
52
  }
44
53
  return v.toString();
45
54
  }
46
- if (v instanceof Temporal.Instant) return v.toString();
55
+ // Duck-tag, not instanceof: ensureTemporalPolyfill keeps native
56
+ // globalThis.Temporal when present, so DB Instants may be native while
57
+ // other code paths use temporal-polyfill's Instant (fw#1550 / Bugbot).
58
+ if (isTemporalInstant(v)) return v.toString();
47
59
  return v;
48
60
  });
49
61
  }
@@ -1,116 +0,0 @@
1
- // Integration test for r.rawTable() — proves the full boot path:
2
- // defineFeature declares a raw table → setupTestStack auto-pushes it →
3
- // INSERT/SELECT against the real DB roundtrip. Plan reference:
4
- // kumiko-platform/docs/plans/architecture/table-ddl-guard.md (Stufe 3).
5
-
6
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
- import { table, text, timestamp } from "../db/dialect";
8
- import { asRawClient, insertOne, selectMany } from "../db/query";
9
- import { defineFeature } from "../engine";
10
- import { setupTestStack, type TestStack, unsafePushTables } from "../stack";
11
-
12
- // External-system payload cache — the textbook r.rawTable() use case:
13
- // write-only by an integration handler, read-only by a query, never
14
- // event-sourced (the data isn't a domain fact, it's a side-effect
15
- // snapshot).
16
- const stripeWebhookCache = table("rt_int_stripe_webhook_cache", {
17
- eventId: text("event_id").primaryKey(),
18
- payload: text("payload").notNull(),
19
- receivedAt: timestamp("received_at", { withTimezone: true }).notNull().defaultNow(),
20
- });
21
-
22
- // A second physical table reachable through two distinct r.rawTable
23
- // registrations — pins the seenTables-by-reference dedupe at push time.
24
- // Two logical names, one CREATE.
25
- const sharedSyncCache = table("rt_int_shared_sync_cache", {
26
- syncId: text("sync_id").primaryKey(),
27
- payload: text("payload").notNull(),
28
- });
29
-
30
- const webhookCacheFeature = defineFeature("webhook-cache", (r) => {
31
- r.rawTable("stripe", stripeWebhookCache, {
32
- reason: "external Stripe webhook payload cache — write-only by webhook handler",
33
- });
34
- r.rawTable("primary-sync", sharedSyncCache, {
35
- reason: "shared sync-state cache, primary writer",
36
- });
37
- r.rawTable("secondary-sync", sharedSyncCache, {
38
- reason: "same physical table, different logical role for read consumers",
39
- });
40
- });
41
-
42
- let stack: TestStack;
43
-
44
- beforeAll(async () => {
45
- stack = await setupTestStack({ features: [webhookCacheFeature] });
46
- });
47
-
48
- afterAll(async () => {
49
- await stack.cleanup();
50
- });
51
-
52
- describe("r.rawTable — DB roundtrip via setupTestStack", () => {
53
- test("table is auto-pushed and accepts INSERT + SELECT", async () => {
54
- const eventId = "evt_test_123";
55
- const payload = JSON.stringify({ type: "invoice.paid", amount: 4200 });
56
-
57
- await insertOne(stack.db, stripeWebhookCache, { eventId, payload });
58
-
59
- const rows = await selectMany(stack.db, stripeWebhookCache, { eventId: eventId });
60
-
61
- expect(rows).toHaveLength(1);
62
- expect(rows[0]?.payload).toBe(payload);
63
- expect(rows[0]?.receivedAt).toBeInstanceOf(Temporal.Instant);
64
- });
65
-
66
- test("registry exposes the raw table with its reason and featureName", () => {
67
- const all = stack.registry.getAllRawTables();
68
- const entry = all.get("stripe");
69
- expect(entry).toBeDefined();
70
- expect(entry?.featureName).toBe("webhook-cache");
71
- expect(entry?.reason).toContain("Stripe webhook payload cache");
72
- expect(entry?.table).toBe(stripeWebhookCache);
73
- });
74
-
75
- test("INSERT bypasses the event store — no kumiko_events row produced", async () => {
76
- // Proves that the rawTable lives outside the event-sourcing graph:
77
- // a write to it shouldn't append anything to kumiko_events. If a
78
- // future regression accidentally routed raw-table writes through
79
- // the executor, a row would show up here.
80
- const before = await asRawClient(stack.db).unsafe<{ count: string }>(
81
- `SELECT COUNT(*)::text AS count FROM kumiko_events`,
82
- );
83
- const beforeCount = Number(before[0]?.count ?? 0);
84
-
85
- await insertOne(stack.db, stripeWebhookCache, {
86
- eventId: "evt_no_event_emitted",
87
- payload: "{}",
88
- });
89
-
90
- const after = await asRawClient(stack.db).unsafe<{ count: string }>(
91
- `SELECT COUNT(*)::text AS count FROM kumiko_events`,
92
- );
93
- const afterCount = Number(after[0]?.count ?? 0);
94
-
95
- expect(afterCount).toBe(beforeCount);
96
- });
97
-
98
- test("two registrations sharing one PgTable result in one CREATE (dedupe by reference)", async () => {
99
- // primary-sync + secondary-sync both target sharedSyncCache. If the
100
- // setupTestStack dedupe (seenTables-by-table-reference) had silently
101
- // broken, beforeAll's setupTestStack would have raised a 42P07 on
102
- // the second push and never reached this test.
103
- await insertOne(stack.db, sharedSyncCache, { syncId: "sync_1", payload: "{}" });
104
- const rows = await selectMany(stack.db, sharedSyncCache, { syncId: "sync_1" });
105
- expect(rows).toHaveLength(1);
106
- // Both registrations are visible in the registry — same physical
107
- // target, different logical handles.
108
- expect(stack.registry.getAllRawTables().get("primary-sync")?.table).toBe(sharedSyncCache);
109
- expect(stack.registry.getAllRawTables().get("secondary-sync")?.table).toBe(sharedSyncCache);
110
- });
111
-
112
- test("a second push on the same rawTable is idempotent — CREATE IF NOT EXISTS", async () => {
113
- // unsafePushTables uses CREATE TABLE IF NOT EXISTS — idempotent by design.
114
- await expect(unsafePushTables(stack.db, { idem: stripeWebhookCache })).resolves.toBeUndefined();
115
- });
116
- });
@@ -1,6 +0,0 @@
1
- // Provider-agnostic Test-Stack — re-exported from stack/test-stack.ts.
2
- // setupBunTestStack / BunTestStack sind Aliase für setupTestStack / TestStack.
3
- // "Bun" ist historisch, jetzt provider-neutral (DB_PROVIDER env).
4
-
5
- export type { TestStack as BunTestStack } from "../../stack/test-stack";
6
- export { setupTestStack as setupBunTestStack } from "../../stack/test-stack";
@@ -1,39 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createEncryptionProvider } from "../encryption";
3
-
4
- // 32 bytes base64-encoded for AES-256
5
- const TEST_KEY = Buffer.from("a]bJm#kP9xQ2@wN!vL$hR5yT8eU0iO3f").toString("base64");
6
-
7
- describe("EncryptionProvider", () => {
8
- test("encrypt + decrypt roundtrip returns original", () => {
9
- const provider = createEncryptionProvider(TEST_KEY);
10
- const ciphertext = provider.encrypt("hello world");
11
- expect(provider.decrypt(ciphertext)).toBe("hello world");
12
- });
13
-
14
- test("same plaintext produces different ciphertexts (random IV)", () => {
15
- const provider = createEncryptionProvider(TEST_KEY);
16
- const a = provider.encrypt("same");
17
- const b = provider.encrypt("same");
18
- expect(a).not.toBe(b);
19
- });
20
-
21
- test("decrypt with different key throws", () => {
22
- const key2 = Buffer.from("x]bJm#kP9xQ2@wN!vL$hR5yT8eU0iO3f").toString("base64");
23
- const p1 = createEncryptionProvider(TEST_KEY);
24
- const p2 = createEncryptionProvider(key2);
25
- const ciphertext = p1.encrypt("secret");
26
- expect(() => p2.decrypt(ciphertext)).toThrow();
27
- });
28
-
29
- test("handles unicode and emoji", () => {
30
- const provider = createEncryptionProvider(TEST_KEY);
31
- const ciphertext = provider.encrypt("Ünïcödé 🔐");
32
- expect(provider.decrypt(ciphertext)).toBe("Ünïcödé 🔐");
33
- });
34
-
35
- test("throws on invalid key length", () => {
36
- const shortKey = Buffer.from("too-short").toString("base64");
37
- expect(() => createEncryptionProvider(shortKey)).toThrow(/32 bytes/);
38
- });
39
- });
@@ -1,39 +0,0 @@
1
- import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
-
3
- const ALGORITHM = "aes-256-gcm";
4
- const IV_LENGTH = 12;
5
- const TAG_LENGTH = 16;
6
-
7
- export type EncryptionProvider = {
8
- encrypt(plaintext: string): string;
9
- decrypt(ciphertext: string): string;
10
- };
11
-
12
- export function createEncryptionProvider(key: string): EncryptionProvider {
13
- // Key must be 32 bytes for AES-256
14
- const keyBuffer = Buffer.from(key, "base64");
15
- if (keyBuffer.length !== 32) {
16
- throw new Error("ENCRYPTION_KEY must be 32 bytes (base64 encoded)");
17
- }
18
-
19
- return {
20
- encrypt(plaintext: string): string {
21
- const iv = randomBytes(IV_LENGTH);
22
- const cipher = createCipheriv(ALGORITHM, keyBuffer, iv);
23
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
24
- const tag = cipher.getAuthTag();
25
- // Format: base64(iv + tag + ciphertext)
26
- return Buffer.concat([iv, tag, encrypted]).toString("base64");
27
- },
28
-
29
- decrypt(ciphertext: string): string {
30
- const data = Buffer.from(ciphertext, "base64");
31
- const iv = data.subarray(0, IV_LENGTH);
32
- const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
33
- const encrypted = data.subarray(IV_LENGTH + TAG_LENGTH);
34
- const decipher = createDecipheriv(ALGORITHM, keyBuffer, iv);
35
- decipher.setAuthTag(tag);
36
- return decipher.update(encrypted) + decipher.final("utf8");
37
- },
38
- };
39
- }
@@ -1,4 +0,0 @@
1
- // Legacy re-export shim — fetchOne lebt jetzt in src/bun-db/query.ts.
2
-
3
- export type { WhereObject } from "../db/query";
4
- export { fetchOne } from "../db/query";
@@ -1,150 +0,0 @@
1
- // Unit tests for r.rawTable() — declaration-time validation + registry
2
- // aggregation. Full DB roundtrip (setupTestStack pushes the table → INSERT
3
- // / SELECT) lives in src/__tests__/raw-table.integration.ts.
4
-
5
- import { describe, expect, test } from "bun:test";
6
- import type { SchemaTable } from "../../db/dialect";
7
- import { table, text } from "../../db/dialect";
8
- import { defineFeature } from "../define-feature";
9
- import { createRegistry } from "../registry";
10
- import type { ProjectionDefinition } from "../types";
11
-
12
- const probeTable = table("rt_probe_table", {
13
- id: text("id").primaryKey(),
14
- });
15
- const probeTableTwo = table("rt_probe_table_two", {
16
- id: text("id").primaryKey(),
17
- });
18
-
19
- describe("r.rawTable — declaration", () => {
20
- test("rejects non-kebab-case names", () => {
21
- expect(() =>
22
- defineFeature("probe", (r) => {
23
- r.rawTable("BadName", probeTable, { reason: "test" });
24
- }),
25
- ).toThrow(/must be kebab-case/);
26
- });
27
-
28
- test("rejects duplicate names within one feature", () => {
29
- expect(() =>
30
- defineFeature("probe", (r) => {
31
- r.rawTable("cache", probeTable, { reason: "test" });
32
- r.rawTable("cache", probeTableTwo, { reason: "test" });
33
- }),
34
- ).toThrow(/already registered/);
35
- });
36
-
37
- test("rejects empty reason", () => {
38
- expect(() =>
39
- defineFeature("probe", (r) => {
40
- r.rawTable("cache", probeTable, { reason: "" });
41
- }),
42
- ).toThrow(/options\.reason must be a non-empty string/);
43
- });
44
-
45
- test("rejects whitespace-only reason", () => {
46
- expect(() =>
47
- defineFeature("probe", (r) => {
48
- r.rawTable("cache", probeTable, { reason: " " });
49
- }),
50
- ).toThrow(/options\.reason must be a non-empty string/);
51
- });
52
-
53
- test("accepts valid registration and stores reason verbatim", () => {
54
- const feature = defineFeature("probe", (r) => {
55
- r.rawTable("cache", probeTable, {
56
- reason: "external Stripe webhook cache, write-only by webhook handler",
57
- });
58
- });
59
- expect(feature.rawTables).toHaveProperty("cache");
60
- expect(feature.rawTables["cache"]?.reason).toBe(
61
- "external Stripe webhook cache, write-only by webhook handler",
62
- );
63
- expect(feature.rawTables["cache"]?.table).toBe(probeTable);
64
- });
65
- });
66
-
67
- describe("createRegistry — rawTable aggregation", () => {
68
- test("aggregates raw tables across features and tags featureName", () => {
69
- const featA = defineFeature("billing", (r) => {
70
- r.rawTable("stripe-cache", probeTable, { reason: "external API cache" });
71
- });
72
- const featB = defineFeature("inventory", (r) => {
73
- r.rawTable("legacy-import", probeTableTwo, { reason: "imported pre-ES" });
74
- });
75
-
76
- const registry = createRegistry([featA, featB]);
77
- const all = registry.getAllRawTables();
78
-
79
- expect(all.size).toBe(2);
80
- expect(all.get("stripe-cache")?.featureName).toBe("billing");
81
- expect(all.get("stripe-cache")?.reason).toBe("external API cache");
82
- expect(all.get("legacy-import")?.featureName).toBe("inventory");
83
- });
84
-
85
- test("rejects cross-feature name collisions at boot", () => {
86
- const featA = defineFeature("a", (r) => {
87
- r.rawTable("shared", probeTable, { reason: "first" });
88
- });
89
- const featB = defineFeature("b", (r) => {
90
- r.rawTable("shared", probeTableTwo, { reason: "second" });
91
- });
92
-
93
- expect(() => createRegistry([featA, featB])).toThrow(
94
- /Raw-table "shared" registered by both feature "a" and "b"/,
95
- );
96
- });
97
-
98
- test("absent rawTables block on a feature is ok (legacy / hand-built features)", () => {
99
- const featNoRaw = defineFeature("no-raw", () => {
100
- // no r.rawTable calls
101
- });
102
- const registry = createRegistry([featNoRaw]);
103
- expect(registry.getAllRawTables().size).toBe(0);
104
- });
105
-
106
- test("two raw tables on the same PgTable register under both names", () => {
107
- // Different logical names, identical physical target — legitimate
108
- // when one role writes (primary cache) and a second role reads
109
- // (alias view). Push-time dedupe keeps the boot to a single CREATE;
110
- // the registry keeps both entries so callers can resolve either name.
111
- const sharedT = table("rt_shared_dedupe", {
112
- id: text("id").primaryKey(),
113
- }) as unknown as SchemaTable;
114
- const feat = defineFeature("dedupe", (r) => {
115
- r.rawTable("primary", sharedT, { reason: "main writer" });
116
- r.rawTable("alias", sharedT, { reason: "alias for read consumers" });
117
- });
118
- const registry = createRegistry([feat]);
119
-
120
- expect(registry.getAllRawTables().size).toBe(2);
121
- expect(registry.getAllRawTables().get("primary")?.table).toBe(sharedT);
122
- expect(registry.getAllRawTables().get("alias")?.table).toBe(sharedT);
123
- });
124
-
125
- test("rejects rawTable that shares a PgTable with an explicit projection", () => {
126
- // A r.rawTable() declaring the same physical table as a registered
127
- // r.projection() is almost always an authoring bug — two owners on
128
- // the same physical table, one event-sourced, one bypass. Boot
129
- // throws so the failure points at the misconfiguration site.
130
- const sharedT = table("rt_collision_phys", {
131
- id: text("id").primaryKey(),
132
- }) as unknown as SchemaTable;
133
- const projection: ProjectionDefinition = {
134
- name: "shared-summary",
135
- source: "shared",
136
- table: sharedT,
137
- apply: {},
138
- };
139
- const featProj = defineFeature("proj-owner", (r) => {
140
- r.projection(projection);
141
- });
142
- const featRaw = defineFeature("raw-owner", (r) => {
143
- r.rawTable("collision", sharedT, { reason: "intentionally bad" });
144
- });
145
-
146
- expect(() => createRegistry([featProj, featRaw])).toThrow(
147
- /shares a Drizzle PgTable with a registered projection/,
148
- );
149
- });
150
- });
@@ -1,127 +0,0 @@
1
- // Unit tests for r.unmanagedTable() — the EntityTableMeta cousin of
2
- // r.rawTable. Same audit-trail contract, different storage shape (post-
3
- // drizzle migrate-runner). See define-feature.ts / DX-4.
4
-
5
- import { describe, expect, test } from "bun:test";
6
- import { defineUnmanagedTable, resolveTableName } from "../../db/entity-table-meta";
7
- import { defineFeature } from "../define-feature";
8
- import { createEntity, createTextField } from "../index";
9
- import { createRegistry } from "../registry";
10
-
11
- const probeMeta = defineUnmanagedTable({
12
- tableName: "ut_probe",
13
- columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
14
- });
15
- const probeMetaTwo = defineUnmanagedTable({
16
- tableName: "ut_probe_two",
17
- columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
18
- });
19
-
20
- describe("r.unmanagedTable — declaration", () => {
21
- test("rejects duplicate registrations within one feature", () => {
22
- expect(() =>
23
- defineFeature("probe", (r) => {
24
- r.unmanagedTable(probeMeta, { reason: "test" });
25
- r.unmanagedTable(probeMeta, { reason: "test" });
26
- }),
27
- ).toThrow(/already registered/);
28
- });
29
-
30
- test("rejects empty reason", () => {
31
- expect(() =>
32
- defineFeature("probe", (r) => {
33
- r.unmanagedTable(probeMeta, { reason: "" });
34
- }),
35
- ).toThrow(/options\.reason must be a non-empty string/);
36
- });
37
-
38
- test("rejects whitespace-only reason", () => {
39
- expect(() =>
40
- defineFeature("probe", (r) => {
41
- r.unmanagedTable(probeMeta, { reason: " " });
42
- }),
43
- ).toThrow(/options\.reason must be a non-empty string/);
44
- });
45
-
46
- test("accepts valid registration and stores meta + reason", () => {
47
- const feature = defineFeature("probe", (r) => {
48
- r.unmanagedTable(probeMeta, {
49
- reason: "read-side projection of an event-stream",
50
- });
51
- });
52
- expect(feature.unmanagedTables).toHaveProperty("ut_probe");
53
- expect(feature.unmanagedTables["ut_probe"]?.reason).toBe(
54
- "read-side projection of an event-stream",
55
- );
56
- expect(feature.unmanagedTables["ut_probe"]?.meta).toBe(probeMeta);
57
- });
58
-
59
- test("two unmanaged tables on one feature register under their tableName", () => {
60
- const feature = defineFeature("dual", (r) => {
61
- r.unmanagedTable(probeMeta, { reason: "one" });
62
- r.unmanagedTable(probeMetaTwo, { reason: "two" });
63
- });
64
- expect(Object.keys(feature.unmanagedTables).sort()).toEqual(["ut_probe", "ut_probe_two"]);
65
- });
66
-
67
- test("absent unmanagedTables on a feature is ok", () => {
68
- const feat = defineFeature("plain", () => {
69
- // no r.unmanagedTable calls
70
- });
71
- expect(feat.unmanagedTables).toEqual({});
72
- });
73
- });
74
-
75
- describe("createRegistry — unmanagedTable aggregation", () => {
76
- test("rejects cross-feature tableName collisions at boot", () => {
77
- // Two features can't share the same physical tableName — migrate-runner
78
- // would race two CREATE TABLE statements. Boot-validator catches it.
79
- const featA = defineFeature("a", (r) => {
80
- r.unmanagedTable(probeMeta, { reason: "first" });
81
- });
82
- const featB = defineFeature("b", (r) => {
83
- r.unmanagedTable(probeMeta, { reason: "second" });
84
- });
85
- expect(() => createRegistry([featA, featB])).toThrow(
86
- /Unmanaged-table "ut_probe" registered by both feature "a" and "b"/,
87
- );
88
- });
89
-
90
- test("two features with distinct tableNames register cleanly", () => {
91
- const featA = defineFeature("a", (r) => {
92
- r.unmanagedTable(probeMeta, { reason: "first" });
93
- });
94
- const featB = defineFeature("b", (r) => {
95
- r.unmanagedTable(probeMetaTwo, { reason: "second" });
96
- });
97
- expect(() => createRegistry([featA, featB])).not.toThrow();
98
- });
99
-
100
- test("rejects an unmanaged-table that collides with an entity's physical name", () => {
101
- const widget = createEntity({ fields: { name: createTextField() } });
102
- // resolveTableName mirrors the migrate-runner — pin the exact physical name.
103
- const physical = resolveTableName("widget", widget, "shop");
104
- const clashing = defineUnmanagedTable({
105
- tableName: physical,
106
- columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
107
- });
108
- const entityFeature = defineFeature("shop", (r) => {
109
- r.entity("widget", widget);
110
- });
111
- const tableFeature = defineFeature("other", (r) => {
112
- r.unmanagedTable(clashing, { reason: "clash" });
113
- });
114
-
115
- // Entity registered first, then the colliding unmanaged table.
116
- expect(() => createRegistry([entityFeature, tableFeature])).toThrow(
117
- new RegExp(
118
- `Unmanaged-table "${physical}".*collides with the physical table of entity "widget"`,
119
- ),
120
- );
121
-
122
- // Order-independent: unmanaged table registered first, then the entity.
123
- expect(() => createRegistry([tableFeature, entityFeature])).toThrow(
124
- new RegExp(`Entity "widget".*collides with r.unmanagedTable\\("${physical}"\\)`),
125
- );
126
- });
127
- });