@cosmicdrift/kumiko-framework 0.1.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 +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { type ErrorOpts, KumikoError } from "./kumiko-error";
|
|
2
|
+
|
|
3
|
+
// Per-field validation issue. Shared shape between Zod-derived and
|
|
4
|
+
// hook-derived validation errors so the client sees one list.
|
|
5
|
+
export type ValidationFieldIssue = {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly i18nKey: string;
|
|
9
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ValidationDetails = {
|
|
13
|
+
readonly fields: readonly ValidationFieldIssue[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class ValidationError extends KumikoError {
|
|
17
|
+
readonly code = "validation_error";
|
|
18
|
+
readonly httpStatus = 400;
|
|
19
|
+
|
|
20
|
+
constructor(details: ValidationDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
|
|
21
|
+
// The wire `message` stays a short human-readable fallback — Zod's own
|
|
22
|
+
// `error.message` is a multi-line JSON blob that would look awful in any
|
|
23
|
+
// UI. Per-field details belong in `details.fields` (structured, i18nable);
|
|
24
|
+
// the full ZodError is preserved in `cause` for forensics in the log.
|
|
25
|
+
super({
|
|
26
|
+
message: "Validation failed",
|
|
27
|
+
i18nKey: opts?.i18nKey ?? "errors.validation.failed",
|
|
28
|
+
details,
|
|
29
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Raised by the dispatcher when a handler belongs to a feature that is
|
|
35
|
+
// globally disabled. Separate from AccessDenied so clients can distinguish
|
|
36
|
+
// "this feature is off" (retry pointless until ops flips it on) from
|
|
37
|
+
// "you don't have permission" (potentially retryable with a different user).
|
|
38
|
+
export type FeatureDisabledDetails = {
|
|
39
|
+
readonly reason: "feature_disabled";
|
|
40
|
+
readonly feature: string;
|
|
41
|
+
readonly handler: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export class FeatureDisabledError extends KumikoError {
|
|
45
|
+
readonly code = "feature_disabled";
|
|
46
|
+
readonly httpStatus = 403;
|
|
47
|
+
|
|
48
|
+
constructor(feature: string, handler: string, opts?: Pick<ErrorOpts, "cause">) {
|
|
49
|
+
super({
|
|
50
|
+
message: `feature ${feature} is disabled`,
|
|
51
|
+
i18nKey: "errors.feature.disabled",
|
|
52
|
+
i18nParams: { feature },
|
|
53
|
+
details: { reason: "feature_disabled", feature, handler } satisfies FeatureDisabledDetails,
|
|
54
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class AccessDeniedError extends KumikoError {
|
|
60
|
+
readonly code = "access_denied";
|
|
61
|
+
readonly httpStatus = 403;
|
|
62
|
+
|
|
63
|
+
constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">) {
|
|
64
|
+
super({
|
|
65
|
+
message: opts?.message ?? "access denied",
|
|
66
|
+
i18nKey: opts?.i18nKey ?? "errors.access.denied",
|
|
67
|
+
...(opts?.i18nParams && { i18nParams: opts.i18nParams }),
|
|
68
|
+
...(opts?.details !== undefined && { details: opts.details }),
|
|
69
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type NotFoundDetails = {
|
|
75
|
+
readonly entity: string;
|
|
76
|
+
readonly id?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export class NotFoundError extends KumikoError {
|
|
80
|
+
readonly code = "not_found";
|
|
81
|
+
readonly httpStatus = 404;
|
|
82
|
+
|
|
83
|
+
constructor(
|
|
84
|
+
entity: string,
|
|
85
|
+
id?: string | number,
|
|
86
|
+
opts?: Pick<ErrorOpts, "i18nKey" | "i18nParams" | "cause">,
|
|
87
|
+
) {
|
|
88
|
+
const idStr = id !== undefined ? String(id) : undefined;
|
|
89
|
+
// The reason string follows `<snake_entity>_not_found` — keeps a stable,
|
|
90
|
+
// client-friendly tag that survives wire serialization even if the entity
|
|
91
|
+
// name is later renamed for display purposes.
|
|
92
|
+
const reason = `${toSnake(entity)}_not_found`;
|
|
93
|
+
const details: NotFoundDetails & { reason: string } = { reason, entity, id: idStr };
|
|
94
|
+
super({
|
|
95
|
+
message: idStr !== undefined ? `${entity} ${idStr} not found` : `${entity} not found`,
|
|
96
|
+
i18nKey: opts?.i18nKey ?? "errors.notFound",
|
|
97
|
+
i18nParams: { entity, id: idStr, ...opts?.i18nParams },
|
|
98
|
+
details,
|
|
99
|
+
cause: opts?.cause,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Accepts camelCase OR kebab-case entity names and produces snake_case for
|
|
105
|
+
// the reason tag. New code uses kebab; legacy camelCase still flows through.
|
|
106
|
+
function toSnake(s: string): string {
|
|
107
|
+
return s
|
|
108
|
+
.replace(/-/g, "_")
|
|
109
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
110
|
+
.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Generic 409. Features that need a narrower shape should subclass (see
|
|
114
|
+
// VersionConflictError) — this way the HTTP layer stays uniform while callers
|
|
115
|
+
// can still instanceof on the concrete subtype in handlers.
|
|
116
|
+
export class ConflictError extends KumikoError {
|
|
117
|
+
// Widened to `string` so subclasses (VersionConflictError) can refine the
|
|
118
|
+
// value without TS blocking the override. The base class still enforces
|
|
119
|
+
// "some code is set"; concrete classes pick their own literal.
|
|
120
|
+
readonly code: string = "conflict";
|
|
121
|
+
readonly httpStatus = 409;
|
|
122
|
+
|
|
123
|
+
constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">) {
|
|
124
|
+
super({
|
|
125
|
+
message: opts?.message ?? "conflict",
|
|
126
|
+
i18nKey: opts?.i18nKey ?? "errors.conflict",
|
|
127
|
+
...(opts?.i18nParams && { i18nParams: opts.i18nParams }),
|
|
128
|
+
...(opts?.details !== undefined && { details: opts.details }),
|
|
129
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type VersionConflictDetails = {
|
|
135
|
+
readonly entityId: number | string;
|
|
136
|
+
readonly expectedVersion: number;
|
|
137
|
+
readonly currentVersion: number;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export class VersionConflictError extends ConflictError {
|
|
141
|
+
override readonly code: string = "version_conflict";
|
|
142
|
+
|
|
143
|
+
constructor(details: VersionConflictDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
|
|
144
|
+
super({
|
|
145
|
+
message: `version conflict for entity ${details.entityId}`,
|
|
146
|
+
i18nKey: opts?.i18nKey ?? "errors.versionConflict",
|
|
147
|
+
details,
|
|
148
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Entity-level unique-index violation. Distinct from VersionConflictError:
|
|
154
|
+
// version_conflict means "two writers raced on the events_aggregate_version
|
|
155
|
+
// _uq index" (optimistic-concurrency); unique_violation means "you tried to
|
|
156
|
+
// insert a row that violates an app-declared unique-index" (e.g. duplicate
|
|
157
|
+
// email on a User entity, duplicate (tenantId, slug) on Article). Same 409
|
|
158
|
+
// because both are conflicts the client can resolve by retrying with
|
|
159
|
+
// different data, but distinct codes so UI can show the right message.
|
|
160
|
+
//
|
|
161
|
+
// constraintName comes from the PG error and is the *physical* DB constraint
|
|
162
|
+
// (e.g. "users_tenant_email_uniq"). Apps that want to map it to a friendly
|
|
163
|
+
// field name should do so in their own error-handler, not here.
|
|
164
|
+
export type UniqueViolationDetails = {
|
|
165
|
+
readonly entityName: string;
|
|
166
|
+
readonly constraintName?: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export class UniqueViolationError extends ConflictError {
|
|
170
|
+
override readonly code: string = "unique_violation";
|
|
171
|
+
|
|
172
|
+
constructor(details: UniqueViolationDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
|
|
173
|
+
super({
|
|
174
|
+
message: `unique constraint violated on entity ${details.entityName}${
|
|
175
|
+
details.constraintName ? ` (${details.constraintName})` : ""
|
|
176
|
+
}`,
|
|
177
|
+
i18nKey: opts?.i18nKey ?? "errors.uniqueViolation",
|
|
178
|
+
details,
|
|
179
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Business-rule violation. The human-readable reason lives in details.reason
|
|
185
|
+
// so the client can key off it without overloading the top-level code.
|
|
186
|
+
export type UnprocessableOpts = Pick<ErrorOpts, "i18nKey" | "i18nParams" | "cause"> & {
|
|
187
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export class UnprocessableError extends KumikoError {
|
|
191
|
+
readonly code = "unprocessable";
|
|
192
|
+
readonly httpStatus = 422;
|
|
193
|
+
|
|
194
|
+
constructor(reason: string, opts?: UnprocessableOpts) {
|
|
195
|
+
super({
|
|
196
|
+
message: `unprocessable: ${reason}`,
|
|
197
|
+
i18nKey: opts?.i18nKey ?? "errors.unprocessable",
|
|
198
|
+
...(opts?.i18nParams && { i18nParams: opts.i18nParams }),
|
|
199
|
+
details: { reason, ...opts?.details },
|
|
200
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Auto-wrap target for unexpected throws. Never exposes .details to the client
|
|
206
|
+
// — the serializer drops it. Stack + cause live in the log only.
|
|
207
|
+
export class InternalError extends KumikoError {
|
|
208
|
+
readonly code = "internal_error";
|
|
209
|
+
readonly httpStatus = 500;
|
|
210
|
+
|
|
211
|
+
constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">) {
|
|
212
|
+
super({
|
|
213
|
+
message: opts?.message ?? "internal error",
|
|
214
|
+
i18nKey: opts?.i18nKey ?? "errors.internal",
|
|
215
|
+
...(opts?.i18nParams !== undefined && { i18nParams: opts.i18nParams }),
|
|
216
|
+
...(opts?.details !== undefined && { details: opts.details }),
|
|
217
|
+
...(opts?.cause !== undefined && { cause: opts.cause }),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Rate-limit hit. The bucket details (limit, window, current state) live
|
|
223
|
+
// in `details` so a client can show "try again in N seconds" without a
|
|
224
|
+
// second request. Headers `Retry-After`, `X-RateLimit-*` are filled in
|
|
225
|
+
// by the HTTP layer from these same fields.
|
|
226
|
+
export type RateLimitDetails = {
|
|
227
|
+
readonly bucket: string;
|
|
228
|
+
readonly limit: number;
|
|
229
|
+
readonly windowSeconds: number;
|
|
230
|
+
readonly remaining: number;
|
|
231
|
+
readonly retryAfterSeconds: number;
|
|
232
|
+
readonly resetAt: string;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export class RateLimitError extends KumikoError {
|
|
236
|
+
readonly code = "rate_limited";
|
|
237
|
+
readonly httpStatus = 429;
|
|
238
|
+
readonly details: RateLimitDetails;
|
|
239
|
+
|
|
240
|
+
constructor(details: RateLimitDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">) {
|
|
241
|
+
super({
|
|
242
|
+
message: `rate limited: ${details.bucket} (limit ${details.limit} per ${details.windowSeconds}s)`,
|
|
243
|
+
i18nKey: opts?.i18nKey ?? "errors.rate_limited",
|
|
244
|
+
details,
|
|
245
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
246
|
+
});
|
|
247
|
+
this.details = details;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# i18n-Texte für FrameworkReasons (deutsch).
|
|
2
|
+
#
|
|
3
|
+
# Schema pro Reason-Key (snake_case, identisch mit Wert in reasons.ts
|
|
4
|
+
# FrameworkReasons):
|
|
5
|
+
#
|
|
6
|
+
# <reason_key>:
|
|
7
|
+
# endUser: |
|
|
8
|
+
# Was der End-User in der App-UI sieht.
|
|
9
|
+
# Verständliche Sprache, Handlungs-Aufforderung wenn anwendbar.
|
|
10
|
+
# developer: |
|
|
11
|
+
# Technischer Hintergrund + Hinweise zum Konfigurieren / Verhindern.
|
|
12
|
+
# Für App-Owner und Designer-Nutzer.
|
|
13
|
+
|
|
14
|
+
stale_state:
|
|
15
|
+
endUser: |
|
|
16
|
+
Jemand anderes hat dieses Element zwischenzeitlich geändert.
|
|
17
|
+
Bitte Seite neu laden und nochmal speichern.
|
|
18
|
+
developer: |
|
|
19
|
+
`ConflictError` tritt auf wenn ein atomic UPDATE den Optimistic-Locking-
|
|
20
|
+
Race verloren hat — ein anderer Writer hat die Row zwischen Snapshot
|
|
21
|
+
und WHERE-Clause bewegt.
|
|
22
|
+
|
|
23
|
+
Standard-Verhalten im Client-SDK: Toast mit "Bitte neu laden" anzeigen
|
|
24
|
+
und Entity neu fetchen.
|
|
25
|
+
|
|
26
|
+
Wenn du Conflict-Handling pro Entity anpassen willst, siehe
|
|
27
|
+
[Optimistic-Locking-Konfiguration](/de/architecture/optimistic-locking/).
|
|
28
|
+
|
|
29
|
+
invalid_transition:
|
|
30
|
+
endUser: |
|
|
31
|
+
Diese Aktion ist im aktuellen Status nicht möglich.
|
|
32
|
+
Der Datensatz muss sich in einem bestimmten Zustand befinden,
|
|
33
|
+
damit dieser Schritt erlaubt ist.
|
|
34
|
+
developer: |
|
|
35
|
+
`UnprocessableError` beim `guardTransition`: ein State-Machine-Übergang
|
|
36
|
+
wurde abgelehnt weil der aktuelle Zustand nicht zu den erlaubten
|
|
37
|
+
Quell-Zuständen gehört.
|
|
38
|
+
|
|
39
|
+
`details.from`, `details.to` und `details.validTargets` enthalten die
|
|
40
|
+
Diagnose. Definiere erlaubte Übergänge in
|
|
41
|
+
[`r.entity({ stateMachine: ... })`](/de/architecture/state-machine/),
|
|
42
|
+
oder prüfe vor dem Aufruf den aktuellen Zustand.
|
|
43
|
+
|
|
44
|
+
field_access_denied:
|
|
45
|
+
endUser: |
|
|
46
|
+
Du darfst dieses Feld nicht ändern.
|
|
47
|
+
Wende dich an einen Administrator wenn du diese Berechtigung brauchst.
|
|
48
|
+
developer: |
|
|
49
|
+
`AccessDeniedError` beim Field-Level-Write-Check des Dispatchers: ein
|
|
50
|
+
Schreibversuch auf ein Feld wurde abgelehnt weil die aktuelle Rolle
|
|
51
|
+
keinen Zugriff hat.
|
|
52
|
+
|
|
53
|
+
`details.field` und `details.handler` enthalten die Diagnose.
|
|
54
|
+
Konfiguriere Field-Access in der Entity-Definition
|
|
55
|
+
(siehe [Permissions](/de/architecture/permissions/)) oder fordere die
|
|
56
|
+
nötige Rolle an.
|
|
57
|
+
|
|
58
|
+
delete_restricted:
|
|
59
|
+
endUser: |
|
|
60
|
+
Dieser Eintrag kann nicht gelöscht werden weil andere Datensätze noch
|
|
61
|
+
darauf verweisen.
|
|
62
|
+
Lösche zuerst die abhängigen Einträge.
|
|
63
|
+
developer: |
|
|
64
|
+
`ConflictError` beim Cascade-Delete-Guard: das Löschen wurde verhindert
|
|
65
|
+
weil dependent Rows existieren.
|
|
66
|
+
|
|
67
|
+
`details.blockingEntity`, `details.entity` und `details.entityId` zeigen
|
|
68
|
+
welche Relation blockiert. Setze `relation.onDelete` auf `"cascade"`
|
|
69
|
+
oder `"setNull"` wenn automatisches Aufräumen gewünscht ist, oder lösche
|
|
70
|
+
die abhängigen Rows zuerst.
|
|
71
|
+
|
|
72
|
+
feature_disabled:
|
|
73
|
+
endUser: |
|
|
74
|
+
Diese Funktion ist aktuell nicht verfügbar.
|
|
75
|
+
Versuche es später noch einmal oder kontaktiere den Administrator.
|
|
76
|
+
developer: |
|
|
77
|
+
`FeatureDisabledError`: das Feature des aufgerufenen Handlers wurde
|
|
78
|
+
global deaktiviert. Distinct von `access_denied` — Clients sollten
|
|
79
|
+
"Funktion gerade nicht verfügbar" zeigen, nicht "keine Berechtigung".
|
|
80
|
+
|
|
81
|
+
`details.feature` und `details.handler` zeigen welches Feature/Handler.
|
|
82
|
+
Aktiviere das Feature via Feature-Toggle oder Routing-Config (siehe
|
|
83
|
+
[Feature-Toggles](/de/features/feature-toggles/)).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# i18n texts for FrameworkReasons (english).
|
|
2
|
+
#
|
|
3
|
+
# Schema per reason key (snake_case, matches the value in
|
|
4
|
+
# reasons.ts FrameworkReasons):
|
|
5
|
+
#
|
|
6
|
+
# <reason_key>:
|
|
7
|
+
# endUser: |
|
|
8
|
+
# What the end-user sees in the app UI.
|
|
9
|
+
# Plain language, call-to-action where applicable.
|
|
10
|
+
# developer: |
|
|
11
|
+
# Technical background + hints on how to configure / prevent.
|
|
12
|
+
# For app owners and designer users.
|
|
13
|
+
|
|
14
|
+
stale_state:
|
|
15
|
+
endUser: |
|
|
16
|
+
Someone else modified this item in the meantime.
|
|
17
|
+
Please reload the page and try saving again.
|
|
18
|
+
developer: |
|
|
19
|
+
`ConflictError` fires when an atomic UPDATE lost the optimistic-
|
|
20
|
+
locking race — another writer moved the row between our snapshot and
|
|
21
|
+
our WHERE clause.
|
|
22
|
+
|
|
23
|
+
Default client SDK behavior: toast a "please reload" message and
|
|
24
|
+
re-fetch the entity.
|
|
25
|
+
|
|
26
|
+
To customize conflict handling per entity, see
|
|
27
|
+
[optimistic locking configuration](/en/architecture/optimistic-locking/).
|
|
28
|
+
|
|
29
|
+
invalid_transition:
|
|
30
|
+
endUser: |
|
|
31
|
+
This action isn't allowed in the current state.
|
|
32
|
+
The record needs to be in a specific status before this step can happen.
|
|
33
|
+
developer: |
|
|
34
|
+
`UnprocessableError` from `guardTransition`: a state-machine transition
|
|
35
|
+
was rejected because the current state isn't in the allowed source
|
|
36
|
+
states.
|
|
37
|
+
|
|
38
|
+
`details.from`, `details.to` and `details.validTargets` carry the
|
|
39
|
+
diagnostics. Define allowed transitions in
|
|
40
|
+
[`r.entity({ stateMachine: ... })`](/en/architecture/state-machine/),
|
|
41
|
+
or check the current state before calling.
|
|
42
|
+
|
|
43
|
+
field_access_denied:
|
|
44
|
+
endUser: |
|
|
45
|
+
You don't have permission to edit this field.
|
|
46
|
+
Please contact an administrator if you need this access.
|
|
47
|
+
developer: |
|
|
48
|
+
`AccessDeniedError` from the dispatcher's field-level write check: a
|
|
49
|
+
write to a field was rejected because the current role lacks access.
|
|
50
|
+
|
|
51
|
+
`details.field` and `details.handler` contain the diagnostics.
|
|
52
|
+
Configure field-level access in the entity definition (see
|
|
53
|
+
[Permissions](/en/architecture/permissions/)) or request the required
|
|
54
|
+
role.
|
|
55
|
+
|
|
56
|
+
delete_restricted:
|
|
57
|
+
endUser: |
|
|
58
|
+
This entry can't be deleted because other records still reference it.
|
|
59
|
+
Delete the dependent entries first.
|
|
60
|
+
developer: |
|
|
61
|
+
`ConflictError` from the cascade-delete guard: the delete was prevented
|
|
62
|
+
because dependent rows exist.
|
|
63
|
+
|
|
64
|
+
`details.blockingEntity`, `details.entity` and `details.entityId` show
|
|
65
|
+
which relation is blocking. Set `relation.onDelete` to `"cascade"` or
|
|
66
|
+
`"setNull"` if automatic cleanup is desired, or delete the dependent
|
|
67
|
+
rows first.
|
|
68
|
+
|
|
69
|
+
feature_disabled:
|
|
70
|
+
endUser: |
|
|
71
|
+
This feature is currently unavailable.
|
|
72
|
+
Please try again later or contact an administrator.
|
|
73
|
+
developer: |
|
|
74
|
+
`FeatureDisabledError`: the feature owning the called handler is
|
|
75
|
+
globally disabled. Distinct from `access_denied` — clients should show
|
|
76
|
+
"feature currently unavailable" rather than "no permission".
|
|
77
|
+
|
|
78
|
+
`details.feature` and `details.handler` indicate which feature and
|
|
79
|
+
handler. Enable the feature via feature toggle or routing config (see
|
|
80
|
+
[Feature Toggles](/en/features/feature-toggles/)).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
FeatureDisabledDetails,
|
|
3
|
+
NotFoundDetails,
|
|
4
|
+
RateLimitDetails,
|
|
5
|
+
UniqueViolationDetails,
|
|
6
|
+
UnprocessableOpts,
|
|
7
|
+
ValidationDetails,
|
|
8
|
+
ValidationFieldIssue,
|
|
9
|
+
VersionConflictDetails,
|
|
10
|
+
} from "./classes";
|
|
11
|
+
export {
|
|
12
|
+
AccessDeniedError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
FeatureDisabledError,
|
|
15
|
+
InternalError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
UniqueViolationError,
|
|
19
|
+
UnprocessableError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
VersionConflictError,
|
|
22
|
+
} from "./classes";
|
|
23
|
+
export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error";
|
|
24
|
+
export { isKumikoError, KumikoError } from "./kumiko-error";
|
|
25
|
+
export type { FrameworkReason } from "./reasons";
|
|
26
|
+
export { FrameworkReasons } from "./reasons";
|
|
27
|
+
export type { ErrorLogEntry, ErrorResponseBody } from "./serialize";
|
|
28
|
+
export { buildErrorLog, serializeError } from "./serialize";
|
|
29
|
+
export type { InvalidTransitionDetails } from "./transition-details";
|
|
30
|
+
export { buildInvalidTransitionDetails } from "./transition-details";
|
|
31
|
+
export type { WriteErrorInfo, WriteFailure } from "./write-error-info";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
failNotFound,
|
|
35
|
+
failTransition,
|
|
36
|
+
failUnprocessable,
|
|
37
|
+
reraiseAsKumikoError,
|
|
38
|
+
toWriteErrorInfo,
|
|
39
|
+
writeFailure,
|
|
40
|
+
} from "./write-error-info";
|
|
41
|
+
export { validationErrorFromZod } from "./zod-bridge";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Base class for every error the framework recognizes as a contract.
|
|
2
|
+
// Anything that isn't a KumikoError is treated as an unexpected crash and
|
|
3
|
+
// auto-wrapped into InternalError by the dispatcher — so the HTTP layer never
|
|
4
|
+
// has to guess the status or the client-facing shape.
|
|
5
|
+
|
|
6
|
+
export type ErrorOpts = {
|
|
7
|
+
readonly message?: string;
|
|
8
|
+
readonly i18nKey?: string;
|
|
9
|
+
readonly i18nParams?: Readonly<Record<string, unknown>>;
|
|
10
|
+
readonly details?: unknown;
|
|
11
|
+
readonly cause?: Error;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ErrorCtorInput = {
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly i18nKey: string;
|
|
17
|
+
readonly i18nParams?: Readonly<Record<string, unknown>>;
|
|
18
|
+
readonly details?: unknown;
|
|
19
|
+
readonly cause?: Error;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Default-Doku-URL für Self-Service-Errors. Kann via env-var
|
|
23
|
+
// `KUMIKO_DOCS_URL` überschrieben werden — z.B. für Self-Hosted-Kunden
|
|
24
|
+
// die ihre eigene Doku-Instanz hosten.
|
|
25
|
+
const DEFAULT_DOCS_BASE_URL = "https://docs.kumiko.so";
|
|
26
|
+
|
|
27
|
+
function docsBaseUrl(): string {
|
|
28
|
+
return process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export abstract class KumikoError extends Error {
|
|
32
|
+
abstract readonly code: string;
|
|
33
|
+
abstract readonly httpStatus: number;
|
|
34
|
+
readonly i18nKey: string;
|
|
35
|
+
readonly i18nParams: Readonly<Record<string, unknown>> | undefined;
|
|
36
|
+
readonly details: unknown;
|
|
37
|
+
|
|
38
|
+
constructor(input: ErrorCtorInput) {
|
|
39
|
+
super(input.message, input.cause ? { cause: input.cause } : undefined);
|
|
40
|
+
this.name = new.target.name;
|
|
41
|
+
this.i18nKey = input.i18nKey;
|
|
42
|
+
this.i18nParams = input.i18nParams;
|
|
43
|
+
this.details = input.details;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Doku-URL für Self-Service. Pro-Reason-Slug aus `details.reason` wenn
|
|
47
|
+
// vorhanden (z.B. ConflictError → "stale_state"), sonst Fallback auf
|
|
48
|
+
// den Error-Code (z.B. "not_found", "validation_error"). Default-Renderer
|
|
49
|
+
// im Client zeigt "Mehr erfahren →" Link auf diese URL.
|
|
50
|
+
get docsUrl(): string {
|
|
51
|
+
return `${docsBaseUrl()}/errors/${this.reasonSlug}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private get reasonSlug(): string {
|
|
55
|
+
if (this.details && typeof this.details === "object") {
|
|
56
|
+
// @cast-boundary error-details — KumikoError.details ist per-error
|
|
57
|
+
// typed, hier reines reflection-shape für reasonSlug-Lookup.
|
|
58
|
+
const r = (this.details as Record<string, unknown>)["reason"];
|
|
59
|
+
if (typeof r === "string") return r;
|
|
60
|
+
}
|
|
61
|
+
return this.code;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isKumikoError(e: unknown): e is KumikoError {
|
|
66
|
+
return e instanceof KumikoError;
|
|
67
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Centralized registry of the reason codes the framework itself surfaces via
|
|
2
|
+
// `details.reason`. Declaring them here keeps them typed + greppable, and
|
|
3
|
+
// gives features a single source of truth when they need to branch on the
|
|
4
|
+
// framework-level reason (e.g. "is this a stale-state race? retry once").
|
|
5
|
+
//
|
|
6
|
+
// Features add their own local Reasons objects (see `samples/errors/` or
|
|
7
|
+
// `packages/bundled-features/src/tenant/constants.ts` → TenantErrors). The
|
|
8
|
+
// framework deliberately does NOT enforce uniqueness across features — two
|
|
9
|
+
// features may use the same reason string if the semantics match. The
|
|
10
|
+
// convention: snake_case, no spaces, no feature prefix for framework reasons.
|
|
11
|
+
|
|
12
|
+
export const FrameworkReasons = {
|
|
13
|
+
// ConflictError: atomic UPDATE lost the race (another writer moved the row
|
|
14
|
+
// between our snapshot and our WHERE clause). Client SDK default: toast +
|
|
15
|
+
// re-fetch.
|
|
16
|
+
staleState: "stale_state",
|
|
17
|
+
|
|
18
|
+
// UnprocessableError: guardTransition rejected a state-machine transition.
|
|
19
|
+
// Details carry `from`, `to`, and `validTargets` for debugging.
|
|
20
|
+
invalidTransition: "invalid_transition",
|
|
21
|
+
|
|
22
|
+
// AccessDeniedError: dispatcher's field-level write check blocked a field.
|
|
23
|
+
// Details carry `field` and `handler`.
|
|
24
|
+
fieldAccessDenied: "field_access_denied",
|
|
25
|
+
|
|
26
|
+
// ConflictError: cascade-delete guard refused because dependent rows exist.
|
|
27
|
+
// Details carry `blockingEntity`, `entity`, `entityId`.
|
|
28
|
+
deleteRestricted: "delete_restricted",
|
|
29
|
+
|
|
30
|
+
// FeatureDisabledError: handler's owning feature is globally disabled.
|
|
31
|
+
// Distinct from access-denied — clients should surface "feature X is
|
|
32
|
+
// currently unavailable" not "you don't have permission".
|
|
33
|
+
featureDisabled: "feature_disabled",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type FrameworkReason = (typeof FrameworkReasons)[keyof typeof FrameworkReasons];
|