@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,190 @@
|
|
|
1
|
+
import type { SseBroker } from "../api/sse-broker";
|
|
2
|
+
import type { DbRow } from "../db/connection";
|
|
3
|
+
import { tenantChannel } from "../engine/constants";
|
|
4
|
+
import type { EntityId, Registry } from "../engine/types";
|
|
5
|
+
import type { SearchAdapter, SearchDocument } from "../search/types";
|
|
6
|
+
import type { EventConsumer } from "./event-dispatcher";
|
|
7
|
+
|
|
8
|
+
// --- Search Index Consumer (async, via event-dispatcher) ---
|
|
9
|
+
//
|
|
10
|
+
// Search-Indexierung läuft seit D.4 als async EventConsumer über den event-
|
|
11
|
+
// dispatcher, nicht mehr als synchroner postSave/postDelete-hook. Das
|
|
12
|
+
// spiegelt Marten's ISubscription-Pattern: ein einziger async Pfad für alle
|
|
13
|
+
// non-inline side-effects.
|
|
14
|
+
//
|
|
15
|
+
// Event → Search-Op Mapping:
|
|
16
|
+
//
|
|
17
|
+
// <entity>.created → index(tenantId, doc)
|
|
18
|
+
// <entity>.updated → index(tenantId, doc) // re-index mit neuem state
|
|
19
|
+
// <entity>.restored → index(tenantId, doc) // wiederbeleben
|
|
20
|
+
// <entity>.deleted → remove(tenantId, type, id)
|
|
21
|
+
//
|
|
22
|
+
// Der Document-State wird aus dem Event rekonstruiert (kein SaveContext
|
|
23
|
+
// mehr available). Regel:
|
|
24
|
+
//
|
|
25
|
+
// created: state = event.payload // ganze entity ist im payload
|
|
26
|
+
// updated: state = { ...previous, ...changes } // rekonstruiert neuen state
|
|
27
|
+
// restored: state = event.payload.previous // restored field-set
|
|
28
|
+
//
|
|
29
|
+
// Sensitive fields sind aus dem event log bereits gestrippt (event-store-
|
|
30
|
+
// executor.ts), also kriegt der Search-Index sie ebenfalls nicht — das ist
|
|
31
|
+
// die gleiche Garantie wie vorher beim postSave-hook.
|
|
32
|
+
//
|
|
33
|
+
// Batch-Variante gibt's aktuell nicht mehr — jeder Event triggert einen
|
|
34
|
+
// eigenen index()-call. Wenn Performance nach Scale-Messung das erfordert,
|
|
35
|
+
// kann der event-dispatcher später eine Batch-Handler-Variante bekommen.
|
|
36
|
+
export const SEARCH_CONSUMER_NAME = "system:consumer:search";
|
|
37
|
+
|
|
38
|
+
export function createSearchEventConsumer(
|
|
39
|
+
searchAdapter: SearchAdapter,
|
|
40
|
+
registry: Registry,
|
|
41
|
+
): EventConsumer {
|
|
42
|
+
return {
|
|
43
|
+
name: SEARCH_CONSUMER_NAME,
|
|
44
|
+
handler: async (event) => {
|
|
45
|
+
const entityName = event.aggregateType;
|
|
46
|
+
const verb = event.type.split(".").pop();
|
|
47
|
+
const tenantId = event.tenantId;
|
|
48
|
+
|
|
49
|
+
// skip: delete takes an early-return after removing the index entry —
|
|
50
|
+
// the "reconstruct state" path below only makes sense for created/
|
|
51
|
+
// updated/restored, which carry field data in the payload.
|
|
52
|
+
if (verb === "deleted") {
|
|
53
|
+
await searchAdapter.remove(tenantId, entityName, event.aggregateId);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (verb !== "created" && verb !== "updated" && verb !== "restored") {
|
|
58
|
+
// skip: other event types (custom domain events, future verbs) don't
|
|
59
|
+
// carry a search-indexable payload shape. If a future feature needs
|
|
60
|
+
// them indexed, it registers its own multiStreamProjection.
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const state = reconstructStateForSearch(event.payload, verb);
|
|
65
|
+
const doc = buildSearchDocument(entityName, event.aggregateId, state, registry);
|
|
66
|
+
if (!doc) {
|
|
67
|
+
// skip: entity isn't searchable (no searchable fields declared)
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await searchAdapter.index(tenantId, doc);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Rebuild the entity-state a search index needs from the event-payload alone.
|
|
76
|
+
// Three shapes to handle — see event-store-executor.ts for the emitter side.
|
|
77
|
+
function reconstructStateForSearch(
|
|
78
|
+
payload: Record<string, unknown>,
|
|
79
|
+
verb: "created" | "updated" | "restored",
|
|
80
|
+
): Record<string, unknown> {
|
|
81
|
+
if (verb === "created") {
|
|
82
|
+
// create: payload IS the entity (minus sensitive fields, already
|
|
83
|
+
// stripped by event-store-executor)
|
|
84
|
+
return payload;
|
|
85
|
+
}
|
|
86
|
+
if (verb === "updated") {
|
|
87
|
+
// update: payload = { changes, previous }. Merge to get the new state
|
|
88
|
+
// the index should reflect. Sensitive fields already filtered out.
|
|
89
|
+
const previous = (payload["previous"] as Record<string, unknown> | undefined) ?? {}; // @cast-boundary engine-payload
|
|
90
|
+
const changes = (payload["changes"] as Record<string, unknown> | undefined) ?? {}; // @cast-boundary engine-payload
|
|
91
|
+
return { ...previous, ...changes };
|
|
92
|
+
}
|
|
93
|
+
// restored: payload = { previous }. The restored entity is whatever the
|
|
94
|
+
// field-values were at delete time — restore copies them back verbatim.
|
|
95
|
+
return (payload["previous"] as Record<string, unknown> | undefined) ?? {}; // @cast-boundary engine-payload
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build a SearchDocument from raw field-state. Parallel to the old
|
|
99
|
+
// buildSearchDocument that took a SaveContext — same selector logic, just
|
|
100
|
+
// a different input shape.
|
|
101
|
+
function buildSearchDocument(
|
|
102
|
+
entityName: string,
|
|
103
|
+
entityId: EntityId,
|
|
104
|
+
state: Record<string, unknown>,
|
|
105
|
+
registry: Registry,
|
|
106
|
+
): SearchDocument | null {
|
|
107
|
+
const entity = registry.getEntity(entityName);
|
|
108
|
+
if (!entity) return null;
|
|
109
|
+
|
|
110
|
+
const searchableFields = registry.getSearchableFields(entityName);
|
|
111
|
+
if (searchableFields.length === 0) return null;
|
|
112
|
+
|
|
113
|
+
const embeddedFields = new Set<string>();
|
|
114
|
+
for (const [fname, fdef] of Object.entries(entity.fields)) {
|
|
115
|
+
if (fdef.type === "embedded") embeddedFields.add(fname);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fields: Record<string, unknown> = {};
|
|
119
|
+
for (const f of searchableFields) {
|
|
120
|
+
const underscoreIdx = f.indexOf("_");
|
|
121
|
+
if (underscoreIdx > 0) {
|
|
122
|
+
const parentKey = f.slice(0, underscoreIdx);
|
|
123
|
+
if (embeddedFields.has(parentKey)) {
|
|
124
|
+
const subKey = f.slice(underscoreIdx + 1);
|
|
125
|
+
const parent = state[parentKey];
|
|
126
|
+
if (parent && typeof parent === "object") {
|
|
127
|
+
const value = (parent as DbRow)[subKey];
|
|
128
|
+
if (value !== undefined) fields[f] = value;
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (state[f] !== undefined) {
|
|
134
|
+
fields[f] = state[f];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
entityType: entityName,
|
|
140
|
+
entityId,
|
|
141
|
+
weight: entity.searchWeight ?? 1,
|
|
142
|
+
fields,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- SSE Broadcast (async, via event-dispatcher) ---
|
|
147
|
+
//
|
|
148
|
+
// SSE-Broadcast läuft seit D.3 als async EventConsumer über den event-
|
|
149
|
+
// dispatcher, nicht mehr als synchroner postSave/postDelete-hook. Das hat
|
|
150
|
+
// zwei Konsequenzen:
|
|
151
|
+
//
|
|
152
|
+
// 1. **Event-native Payload-Shape.** Der SSE-event spiegelt den StoredEvent:
|
|
153
|
+
// `type` ist event.type ("user.created", "unit.updated"), `data` enthält
|
|
154
|
+
// id, aggregateType, version und die event-payload — keine künstliche
|
|
155
|
+
// "system:event:<entity>:<verb>" Hülle mehr. Clients haben direkten
|
|
156
|
+
// Zugriff auf `payload.changes` + `payload.previous` (wie im event-log).
|
|
157
|
+
// 2. **Eventual consistency statt Read-after-Write.** Ein SSE-Event kommt
|
|
158
|
+
// ~10–100ms nach dem HTTP-200 (abhängig von pollIntervalMs). UI-Clients
|
|
159
|
+
// die auf optimistic-update setzen merken das nicht; strictly-waiting
|
|
160
|
+
// Clients müssten poll-after-write.
|
|
161
|
+
//
|
|
162
|
+
// Tests drain deterministisch via `await stack.eventDispatcher.runOnce()`.
|
|
163
|
+
export const SSE_BROADCAST_CONSUMER_NAME = "system:consumer:sse-broadcast";
|
|
164
|
+
|
|
165
|
+
export function createSseBroadcastEventConsumer(sseBroker: SseBroker): EventConsumer {
|
|
166
|
+
return {
|
|
167
|
+
name: SSE_BROADCAST_CONSUMER_NAME,
|
|
168
|
+
// Per-instance delivery: each API process has its own pool of SSE
|
|
169
|
+
// clients and its own cursor. Every instance reads every event
|
|
170
|
+
// independently and pushes to its local clients. Without this, in a
|
|
171
|
+
// split-deploy (API-1/API-2 + Worker, Welle 2.5), only ONE API
|
|
172
|
+
// instance would pick up each event (shared-cursor SKIP LOCKED) and
|
|
173
|
+
// the other instance's clients would never see updates. SSE clients
|
|
174
|
+
// pin to a specific API process via the HTTP long-poll; cross-process
|
|
175
|
+
// delivery isn't solvable by sharing a cursor.
|
|
176
|
+
delivery: "per-instance",
|
|
177
|
+
handler: async (event) => {
|
|
178
|
+
sseBroker.pushToChannel(tenantChannel(event.tenantId), {
|
|
179
|
+
type: event.type,
|
|
180
|
+
data: {
|
|
181
|
+
id: event.aggregateId,
|
|
182
|
+
aggregateType: event.aggregateType,
|
|
183
|
+
version: event.version,
|
|
184
|
+
payload: event.payload,
|
|
185
|
+
createdAt: event.createdAt,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
ADJECTIVES,
|
|
4
|
+
generateAdjNounName,
|
|
5
|
+
generateNoConfusableId,
|
|
6
|
+
generateUniqueName,
|
|
7
|
+
NOUNS,
|
|
8
|
+
} from "../index";
|
|
9
|
+
|
|
10
|
+
describe("generateAdjNounName", () => {
|
|
11
|
+
test("default: <adj>-<noun> aus den Standard-Listen", () => {
|
|
12
|
+
const name = generateAdjNounName();
|
|
13
|
+
const [adj, noun, ...rest] = name.split("-");
|
|
14
|
+
expect(rest).toEqual([]);
|
|
15
|
+
expect(ADJECTIVES).toContain(adj);
|
|
16
|
+
expect(NOUNS).toContain(noun);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("custom separator", () => {
|
|
20
|
+
const name = generateAdjNounName({ separator: "_" });
|
|
21
|
+
expect(name).toMatch(/^[a-z]+_[a-z]+$/);
|
|
22
|
+
const [adj, noun] = name.split("_");
|
|
23
|
+
expect(ADJECTIVES).toContain(adj);
|
|
24
|
+
expect(NOUNS).toContain(noun);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("mit suffix: <adj>-<noun>-<suffix>", () => {
|
|
28
|
+
const name = generateAdjNounName({ suffix: { length: 3 } });
|
|
29
|
+
const parts = name.split("-");
|
|
30
|
+
expect(parts).toHaveLength(3);
|
|
31
|
+
const [adj, noun, suffix] = parts;
|
|
32
|
+
expect(ADJECTIVES).toContain(adj);
|
|
33
|
+
expect(NOUNS).toContain(noun);
|
|
34
|
+
expect(suffix).toMatch(/^[a-z2-9]{3}$/);
|
|
35
|
+
// No-confusable: keine 0/1/o/l/i
|
|
36
|
+
expect(suffix).not.toMatch(/[01oli]/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("custom adjectives + nouns Wahl", () => {
|
|
40
|
+
const name = generateAdjNounName({
|
|
41
|
+
adjectives: ["rapid"],
|
|
42
|
+
nouns: ["receiver"],
|
|
43
|
+
});
|
|
44
|
+
expect(name).toBe("rapid-receiver");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("Statistical: 100 Generierungen → mindestens 5 verschiedene", () => {
|
|
48
|
+
const names = new Set<string>();
|
|
49
|
+
for (let i = 0; i < 100; i++) {
|
|
50
|
+
names.add(generateAdjNounName());
|
|
51
|
+
}
|
|
52
|
+
// 22500 combos, 100 picks → erwartete Diversität ist hoch.
|
|
53
|
+
// Lower bound 5 fängt nur einen kompletten RNG-Defekt.
|
|
54
|
+
expect(names.size).toBeGreaterThan(5);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("generateNoConfusableId", () => {
|
|
59
|
+
test("returns string der gewünschten Länge", () => {
|
|
60
|
+
expect(generateNoConfusableId(8)).toHaveLength(8);
|
|
61
|
+
expect(generateNoConfusableId(1)).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("nur Zeichen aus dem no-confusable-Alphabet", () => {
|
|
65
|
+
for (let i = 0; i < 50; i++) {
|
|
66
|
+
const id = generateNoConfusableId(10);
|
|
67
|
+
expect(id).toMatch(/^[a-z2-9]+$/);
|
|
68
|
+
expect(id).not.toMatch(/[01oli]/);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("length < 1 wirft", () => {
|
|
73
|
+
expect(() => generateNoConfusableId(0)).toThrow(/length must be ≥ 1/);
|
|
74
|
+
expect(() => generateNoConfusableId(-3)).toThrow(/length must be ≥ 1/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("Statistical: 200 IDs der Länge 8 → alle unique", () => {
|
|
78
|
+
// 32^8 = 1 Trillion combos → 200 picks haben praktisch 0 % Kollision.
|
|
79
|
+
const ids = new Set<string>();
|
|
80
|
+
for (let i = 0; i < 200; i++) {
|
|
81
|
+
ids.add(generateNoConfusableId(8));
|
|
82
|
+
}
|
|
83
|
+
expect(ids.size).toBe(200);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("generateUniqueName", () => {
|
|
88
|
+
test("isAvailable=true beim ersten Wurf → returnt clean (kein Suffix)", async () => {
|
|
89
|
+
const seen: string[] = [];
|
|
90
|
+
const name = await generateUniqueName({
|
|
91
|
+
isAvailable: async (n) => {
|
|
92
|
+
seen.push(n);
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
expect(seen).toHaveLength(1);
|
|
97
|
+
expect(name).toBe(seen[0]);
|
|
98
|
+
// Clean = nur 2 Teile (adj + noun), kein suffix
|
|
99
|
+
expect(name.split("-")).toHaveLength(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("nach 3 Kollisionen wechselt zu Suffix-Mode", async () => {
|
|
103
|
+
const tried: string[] = [];
|
|
104
|
+
const name = await generateUniqueName({
|
|
105
|
+
maxCleanAttempts: 3,
|
|
106
|
+
isAvailable: async (n) => {
|
|
107
|
+
tried.push(n);
|
|
108
|
+
// Erste 3 sind belegt, der 4. Versuch (suffix-mode) wins.
|
|
109
|
+
return tried.length === 4;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
expect(tried).toHaveLength(4);
|
|
113
|
+
// Erste 3 ohne suffix
|
|
114
|
+
for (let i = 0; i < 3; i++) {
|
|
115
|
+
expect(tried[i]?.split("-")).toHaveLength(2);
|
|
116
|
+
}
|
|
117
|
+
// 4. Versuch hat suffix
|
|
118
|
+
expect(tried[3]?.split("-")).toHaveLength(3);
|
|
119
|
+
expect(name).toBe(tried[3]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("wirft wenn maxTotalAttempts erschöpft", async () => {
|
|
123
|
+
await expect(
|
|
124
|
+
generateUniqueName({
|
|
125
|
+
isAvailable: async () => false,
|
|
126
|
+
maxTotalAttempts: 5,
|
|
127
|
+
}),
|
|
128
|
+
).rejects.toThrow(/failed to find available name after 5 attempts/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("respektiert custom Wortlisten", async () => {
|
|
132
|
+
const name = await generateUniqueName({
|
|
133
|
+
isAvailable: async () => true,
|
|
134
|
+
adjectives: ["bold"],
|
|
135
|
+
nouns: ["receiver"],
|
|
136
|
+
});
|
|
137
|
+
expect(name).toBe("bold-receiver");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("config-Validierung: maxClean > maxTotal wirft", async () => {
|
|
141
|
+
await expect(
|
|
142
|
+
generateUniqueName({
|
|
143
|
+
isAvailable: async () => true,
|
|
144
|
+
maxCleanAttempts: 10,
|
|
145
|
+
maxTotalAttempts: 5,
|
|
146
|
+
}),
|
|
147
|
+
).rejects.toThrow(/maxCleanAttempts.*must not exceed maxTotalAttempts/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Random-Generator für human-readable Resource-Identifier:
|
|
2
|
+
// - Tenant-Keys ("happy-cloud", "swift-river-k8x")
|
|
3
|
+
// - Webhook-Subscriber-Slugs ("bold-falcon-receiver" mit custom nouns)
|
|
4
|
+
// - API-Key-Display-Names (3 Vorschläge anbieten beim Create)
|
|
5
|
+
// - Tenant-Subdomain-Vorschläge im Subdomain-Setup-Screen
|
|
6
|
+
// - Test-Fixtures mit lesbaren Identifiern
|
|
7
|
+
//
|
|
8
|
+
// NICHT für Security-Token (CSRF, Session, API-Keys, OAuth-State,
|
|
9
|
+
// Reset-Token). Math.random() ist nicht kryptografisch unvorhersagbar.
|
|
10
|
+
// Authority-binding für Tenant-Keys läuft über JWT + DB-Lookup, nicht
|
|
11
|
+
// über Slug-Geheimhaltung — der Slug darf erratbar sein.
|
|
12
|
+
//
|
|
13
|
+
// Universal-safe: Math.random() läuft in Bun, Node, Metro/RN, Expo-Web.
|
|
14
|
+
// Keine node:crypto-Imports.
|
|
15
|
+
|
|
16
|
+
import { ADJECTIVES, NOUNS } from "./words";
|
|
17
|
+
|
|
18
|
+
// Alphabet ohne handgetippt verwechselbare Zeichen:
|
|
19
|
+
// - keine 0/O (Null vs Großbuchstabe O)
|
|
20
|
+
// - keine 1/l/I (Eins vs Kleinbuchstabe L vs Großbuchstabe I)
|
|
21
|
+
// Resultat: 32 Zeichen, sicher beim Telefon-Buchstabieren UND beim
|
|
22
|
+
// Mailtext-Copy-Paste in fremde Schriftarten.
|
|
23
|
+
const NO_CONFUSABLE_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz";
|
|
24
|
+
const NO_CONFUSABLE_CHARS: readonly string[] = Object.freeze(NO_CONFUSABLE_ALPHABET.split(""));
|
|
25
|
+
|
|
26
|
+
function pickRandom<T>(arr: readonly T[]): T {
|
|
27
|
+
if (arr.length === 0) {
|
|
28
|
+
throw new Error("pickRandom: cannot pick from empty array");
|
|
29
|
+
}
|
|
30
|
+
const value = arr[Math.floor(Math.random() * arr.length)];
|
|
31
|
+
// Length-Check oben garantiert dass index in-range ist.
|
|
32
|
+
if (value === undefined) {
|
|
33
|
+
throw new Error("pickRandom: indexed value undefined (sparse array?)");
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AdjNounNameOptions = {
|
|
39
|
+
/** Trennzeichen zwischen adj/noun (und ggf. suffix). Default "-". */
|
|
40
|
+
readonly separator?: string;
|
|
41
|
+
/** Wenn gesetzt, wird ein random-suffix der Länge .length angehängt
|
|
42
|
+
* (no-confusable-Alphabet). Empfohlen 3 Zeichen = 32^3 = 32.768
|
|
43
|
+
* zusätzliche Combinations pro Wortpaar. */
|
|
44
|
+
readonly suffix?: { readonly length: number };
|
|
45
|
+
/** Custom Adjective-Liste — default ADJECTIVES (150 generic). */
|
|
46
|
+
readonly adjectives?: readonly string[];
|
|
47
|
+
/** Custom Noun-Liste — default NOUNS (150 generic). Apps die Domain-
|
|
48
|
+
* spezifische Slugs wollen (z.B. webhook-feature mit eigenen
|
|
49
|
+
* -receiver/-listener-Substantiven) reichen ihre eigene Liste. */
|
|
50
|
+
readonly nouns?: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Sync Generator: produziert "happy-cloud" oder "happy-cloud-k8x"
|
|
54
|
+
* (mit suffix). Kein Conflict-Check — Caller verantwortlich für
|
|
55
|
+
* Uniqueness, oder generateUniqueName() für die async Variante. */
|
|
56
|
+
export function generateAdjNounName(options: AdjNounNameOptions = {}): string {
|
|
57
|
+
const sep = options.separator ?? "-";
|
|
58
|
+
const adjs = options.adjectives ?? ADJECTIVES;
|
|
59
|
+
const nouns = options.nouns ?? NOUNS;
|
|
60
|
+
let name = `${pickRandom(adjs)}${sep}${pickRandom(nouns)}`;
|
|
61
|
+
if (options.suffix) {
|
|
62
|
+
name = `${name}${sep}${generateNoConfusableId(options.suffix.length)}`;
|
|
63
|
+
}
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Random-String aus dem no-confusable-Alphabet. Für Suffix-bei-Kollision
|
|
68
|
+
* oder als standalone short-IDs (z.B. Webhook-Verifizierungs-Codes
|
|
69
|
+
* zum Vorlesen am Telefon). length ≥ 1. */
|
|
70
|
+
export function generateNoConfusableId(length: number): string {
|
|
71
|
+
if (length < 1) {
|
|
72
|
+
throw new Error(`generateNoConfusableId: length must be ≥ 1 (got ${length})`);
|
|
73
|
+
}
|
|
74
|
+
let id = "";
|
|
75
|
+
for (let i = 0; i < length; i++) {
|
|
76
|
+
id += pickRandom(NO_CONFUSABLE_CHARS);
|
|
77
|
+
}
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type GenerateUniqueNameOptions = {
|
|
82
|
+
/** Caller-Predicate. Returns true wenn der Name noch nicht vergeben
|
|
83
|
+
* ist (typisch: DB-Query "select where slug=$1" → row count === 0). */
|
|
84
|
+
readonly isAvailable: (name: string) => Promise<boolean>;
|
|
85
|
+
/** Max Versuche OHNE Suffix bevor wir auf suffix-mode wechseln.
|
|
86
|
+
* Default 3. Bei 22.500 Default-Combos und ~150 existierenden
|
|
87
|
+
* Tenants liegt p(Kollision) < 1% — 3 Versuche reichen weit. */
|
|
88
|
+
readonly maxCleanAttempts?: number;
|
|
89
|
+
/** Suffix-Länge bei Kollision-Mode. Default 3 (= 32.768 Combinations
|
|
90
|
+
* pro Wortpaar). */
|
|
91
|
+
readonly suffixLength?: number;
|
|
92
|
+
/** Hard-Cap an Total-Versuchen bevor wir aufgeben. Default 20.
|
|
93
|
+
* Praktisch nie erreicht — wenn doch, ist die Wortliste leer oder
|
|
94
|
+
* isAvailable() returnt durchgängig false (DB-Bug). */
|
|
95
|
+
readonly maxTotalAttempts?: number;
|
|
96
|
+
readonly separator?: string;
|
|
97
|
+
readonly adjectives?: readonly string[];
|
|
98
|
+
readonly nouns?: readonly string[];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Generiert einen unique Adj-Noun-Slug indem es bis zu maxCleanAttempts
|
|
102
|
+
* saubere Wortpaare versucht, danach mit random Suffix bis maxTotal-
|
|
103
|
+
* Attempts. Wirft wenn auch das nicht hilft (= caller hat ein Bug
|
|
104
|
+
* mit isAvailable, oder die Wortliste ist defekt).
|
|
105
|
+
*
|
|
106
|
+
* Beispiel:
|
|
107
|
+
* const slug = await generateUniqueName({
|
|
108
|
+
* isAvailable: async (s) =>
|
|
109
|
+
* !(await db.select().from(tenants).where(eq(tenants.tenantKey, s)).then(r => r.length > 0)),
|
|
110
|
+
* });
|
|
111
|
+
* // → "happy-cloud" oder "happy-cloud-k8x" bei Kollision
|
|
112
|
+
*/
|
|
113
|
+
export async function generateUniqueName(options: GenerateUniqueNameOptions): Promise<string> {
|
|
114
|
+
const maxClean = options.maxCleanAttempts ?? 3;
|
|
115
|
+
const suffixLength = options.suffixLength ?? 3;
|
|
116
|
+
const maxTotal = options.maxTotalAttempts ?? 20;
|
|
117
|
+
if (maxClean > maxTotal) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`generateUniqueName: maxCleanAttempts (${maxClean}) must not exceed maxTotalAttempts (${maxTotal})`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const baseOptions: AdjNounNameOptions = {
|
|
124
|
+
...(options.separator !== undefined && { separator: options.separator }),
|
|
125
|
+
...(options.adjectives !== undefined && { adjectives: options.adjectives }),
|
|
126
|
+
...(options.nouns !== undefined && { nouns: options.nouns }),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < maxTotal; i++) {
|
|
130
|
+
const useSuffix = i >= maxClean;
|
|
131
|
+
const name = generateAdjNounName(
|
|
132
|
+
useSuffix ? { ...baseOptions, suffix: { length: suffixLength } } : baseOptions,
|
|
133
|
+
);
|
|
134
|
+
if (await options.isAvailable(name)) return name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw new Error(
|
|
138
|
+
`generateUniqueName: failed to find available name after ${maxTotal} attempts. ` +
|
|
139
|
+
`Wordlists may be exhausted, or isAvailable() returns false unconditionally.`,
|
|
140
|
+
);
|
|
141
|
+
}
|