@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.
- package/README.md +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- 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
|
package/src/ui-types/index.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/utils/safe-json.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
});
|
package/src/db/encryption.ts
DELETED
|
@@ -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
|
-
}
|
package/src/db/row-helpers.ts
DELETED
|
@@ -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
|
-
});
|