pipework 0.252.0 → 0.253.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/CHANGELOG.md +10 -0
- package/dist/infra/support/handle.d.ts +8 -1
- package/dist/infra/support/handle.d.ts.map +1 -1
- package/dist/infra/support/handle.js +9 -2
- package/dist/infra/support/handle.js.map +1 -1
- package/dist/infra/support/namespace.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/async/jobs/abortable-delay.ts +18 -0
- package/src/async/jobs/claims.ts +52 -0
- package/src/async/jobs/cron.ts +179 -0
- package/src/async/jobs/errors.ts +56 -0
- package/src/async/jobs/exclusion.ts +117 -0
- package/src/async/jobs/execute.ts +219 -0
- package/src/async/jobs/index.ts +10 -0
- package/src/async/jobs/listener.ts +135 -0
- package/src/async/jobs/namespace.ts +20 -0
- package/src/async/jobs/queue-door.ts +44 -0
- package/src/async/jobs/queue.ts +1012 -0
- package/src/async/jobs/run-script.ts +67 -0
- package/src/async/jobs/scheduler.ts +218 -0
- package/src/async/jobs/schedules-table.ts +37 -0
- package/src/async/jobs/serialize-error.ts +48 -0
- package/src/async/jobs/table.ts +191 -0
- package/src/async/pipeline/define.ts +53 -0
- package/src/async/pipeline/errors.ts +35 -0
- package/src/async/pipeline/execute.ts +53 -0
- package/src/async/pipeline/index.ts +13 -0
- package/src/async/pipeline/namespace.ts +12 -0
- package/src/async/pipeline/resume.ts +67 -0
- package/src/async/pipeline/run-steps.ts +98 -0
- package/src/async/pipeline/types.ts +57 -0
- package/src/async/state-machine/define.ts +64 -0
- package/src/async/state-machine/errors.ts +29 -0
- package/src/async/state-machine/index.ts +3 -0
- package/src/async/state-machine/namespace.ts +12 -0
- package/src/async/state-machine/transition.ts +90 -0
- package/src/async/state-machine/types.ts +37 -0
- package/src/async/steps/errors.ts +31 -0
- package/src/async/steps/graph.ts +1214 -0
- package/src/async/steps/hash.ts +40 -0
- package/src/async/steps/index.ts +7 -0
- package/src/async/steps/namespace.ts +7 -0
- package/src/async/steps/table.ts +97 -0
- package/src/async/steps/types.ts +318 -0
- package/src/audit/chain/index.ts +9 -0
- package/src/audit/chain/install.ts +113 -0
- package/src/audit/chain/retire.ts +52 -0
- package/src/audit/chain/verify.ts +257 -0
- package/src/audit/query/cursor.ts +37 -0
- package/src/audit/query/index.ts +9 -0
- package/src/audit/query/query.ts +223 -0
- package/src/audit/schema/audit-record-fields.ts +68 -0
- package/src/audit/schema/audit-record.ts +23 -0
- package/src/auth/auth/audit.ts +31 -0
- package/src/auth/auth/chain.ts +84 -0
- package/src/auth/auth/cookie.ts +136 -0
- package/src/auth/auth/email-links.ts +311 -0
- package/src/auth/auth/guard.ts +128 -0
- package/src/auth/auth/index.ts +18 -0
- package/src/auth/auth/multi-org.ts +337 -0
- package/src/auth/auth/namespace.ts +34 -0
- package/src/auth/auth/parse-expiry.ts +13 -0
- package/src/auth/auth/passwords.ts +110 -0
- package/src/auth/auth/schema.ts +110 -0
- package/src/auth/auth/sessions.ts +565 -0
- package/src/auth/auth/sign-in.ts +156 -0
- package/src/auth/auth/signing-keys.ts +69 -0
- package/src/auth/auth/types.ts +26 -0
- package/src/auth/auth/uuid.ts +10 -0
- package/src/auth/rbac/cache.ts +55 -0
- package/src/auth/rbac/check.ts +30 -0
- package/src/auth/rbac/create.ts +35 -0
- package/src/auth/rbac/index.ts +5 -0
- package/src/auth/rbac/middleware.ts +20 -0
- package/src/auth/rbac/namespace.ts +15 -0
- package/src/auth/rbac/resolve.ts +31 -0
- package/src/auth/rbac/schema.sql +31 -0
- package/src/auth/rbac/seed.ts +67 -0
- package/src/auth/rbac/tables.ts +32 -0
- package/src/auth/rbac/types.ts +65 -0
- package/src/auth/tenant/extract.ts +33 -0
- package/src/auth/tenant/in-tenant.ts +49 -0
- package/src/auth/tenant/index.ts +7 -0
- package/src/auth/tenant/namespace.ts +23 -0
- package/src/auth/tenant/propagate.ts +20 -0
- package/src/auth/tenant/rls.ts +120 -0
- package/src/auth/tenant/scope.ts +33 -0
- package/src/auth/tenant/scoped-db.ts +581 -0
- package/src/auth/tenant/types.ts +14 -0
- package/src/auth/tenant/validate.ts +27 -0
- package/src/cli/commands/add.ts +158 -0
- package/src/cli/commands/alerts.ts +98 -0
- package/src/cli/commands/check.ts +1418 -0
- package/src/cli/commands/codemod.ts +78 -0
- package/src/cli/commands/db.ts +356 -0
- package/src/cli/commands/dev.ts +101 -0
- package/src/cli/commands/generate.ts +75 -0
- package/src/cli/commands/init.ts +36 -0
- package/src/cli/commands/install.ts +28 -0
- package/src/cli/commands/lint.ts +62 -0
- package/src/cli/commands/migrate.ts +132 -0
- package/src/cli/commands/openapi.ts +63 -0
- package/src/cli/commands/remove.ts +72 -0
- package/src/cli/commands/run.ts +50 -0
- package/src/cli/commands/scope.ts +54 -0
- package/src/cli/commands/serve.ts +68 -0
- package/src/cli/commands/shard.ts +31 -0
- package/src/cli/commands/shared/config-pipework-version.ts +28 -0
- package/src/cli/commands/shared/find-config.ts +22 -0
- package/src/cli/commands/shared/load-config.ts +35 -0
- package/src/cli/commands/shared/pnpm-install.ts +95 -0
- package/src/cli/commands/shared/report-removed.ts +8 -0
- package/src/cli/commands/sync.ts +32 -0
- package/src/cli/commands/test.ts +307 -0
- package/src/cli/commands/upgrade.ts +48 -0
- package/src/cli/dev-bootstrap.ts +24 -0
- package/src/cli/errors.ts +10 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/output.ts +17 -0
- package/src/cli/port.ts +15 -0
- package/src/cli/surface-kinds.ts +20 -0
- package/src/client/index.ts +4 -0
- package/src/client/poll.ts +130 -0
- package/src/client/report-fault.ts +119 -0
- package/src/core/config/app-pool.ts +19 -0
- package/src/core/config/aws-env.ts +29 -0
- package/src/core/config/base-dir.ts +24 -0
- package/src/core/config/discover.ts +64 -0
- package/src/core/config/env-resolve.ts +87 -0
- package/src/core/config/env-types.ts +116 -0
- package/src/core/config/env.ts +55 -0
- package/src/core/config/errors.ts +9 -0
- package/src/core/config/import-config.ts +40 -0
- package/src/core/config/index.ts +8 -0
- package/src/core/config/load.ts +417 -0
- package/src/core/config/namespace.ts +14 -0
- package/src/core/config/resolve.ts +195 -0
- package/src/core/config/schema.ts +488 -0
- package/src/core/config/statement-observer.ts +48 -0
- package/src/core/config/ts-inline-hooks.ts +75 -0
- package/src/core/config/ts-register.ts +4 -0
- package/src/core/config/ts-resolve-hooks.ts +101 -0
- package/src/core/errors/base.ts +49 -0
- package/src/core/errors/http.ts +146 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/namespace.ts +31 -0
- package/src/core/lifecycle/drain.ts +33 -0
- package/src/core/lifecycle/health.ts +72 -0
- package/src/core/lifecycle/index.ts +4 -0
- package/src/core/lifecycle/namespace.ts +7 -0
- package/src/core/lifecycle/startup.ts +102 -0
- package/src/core/lifecycle/types.ts +36 -0
- package/src/core/logging/context.ts +60 -0
- package/src/core/logging/error-chain.ts +68 -0
- package/src/core/logging/index.ts +5 -0
- package/src/core/logging/logger.ts +65 -0
- package/src/core/logging/namespace.ts +16 -0
- package/src/core/logging/proxy.ts +22 -0
- package/src/core/logging/redact-match.ts +24 -0
- package/src/core/logging/types.ts +47 -0
- package/src/core/pipework.ts +566 -0
- package/src/core/runtime/broker.ts +69 -0
- package/src/core/surface/http.ts +133 -0
- package/src/core/surface/index.ts +11 -0
- package/src/core/surface/job-timeout.ts +40 -0
- package/src/core/surface/namespace.ts +13 -0
- package/src/core/surface/script.ts +33 -0
- package/src/core/surface/start.ts +142 -0
- package/src/core/surface/types.ts +225 -0
- package/src/core/surface/validate.ts +88 -0
- package/src/core/surface/worker.ts +528 -0
- package/src/core/validation/connectivity.ts +49 -0
- package/src/core/validation/domain.ts +99 -0
- package/src/core/validation/index.ts +5 -0
- package/src/core/validation/queue-tables.ts +312 -0
- package/src/core/validation/rls-role.ts +138 -0
- package/src/core/validation/startup.ts +51 -0
- package/src/core/validation/surfaces.ts +65 -0
- package/src/core/validation/types.ts +25 -0
- package/src/data/db/advisory-lock.ts +55 -0
- package/src/data/db/aggregates.ts +172 -0
- package/src/data/db/array-elements.ts +111 -0
- package/src/data/db/bulk-set.ts +53 -0
- package/src/data/db/col/index.ts +14 -0
- package/src/data/db/composed.ts +53 -0
- package/src/data/db/db.ts +276 -0
- package/src/data/db/excluded.ts +16 -0
- package/src/data/db/expressions.ts +473 -0
- package/src/data/db/filter.ts +90 -0
- package/src/data/db/function-source.ts +114 -0
- package/src/data/db/identifiers.ts +16 -0
- package/src/data/db/idx/index.ts +9 -0
- package/src/data/db/in-list.ts +49 -0
- package/src/data/db/index.ts +9 -0
- package/src/data/db/insert-split.ts +137 -0
- package/src/data/db/instrumentation.ts +82 -0
- package/src/data/db/maintenance.ts +28 -0
- package/src/data/db/namespace.ts +107 -0
- package/src/data/db/owner-labels.ts +9 -0
- package/src/data/db/pool.ts +187 -0
- package/src/data/db/query-label.ts +13 -0
- package/src/data/db/query-watch.ts +116 -0
- package/src/data/db/raw-client-warning.ts +37 -0
- package/src/data/db/readonly.ts +23 -0
- package/src/data/db/reservation.ts +151 -0
- package/src/data/db/schema-features.ts +14 -0
- package/src/data/db/serializable.ts +83 -0
- package/src/data/db/sql.ts +1 -0
- package/src/data/db/timeouts.ts +76 -0
- package/src/data/db/types.ts +18 -0
- package/src/data/db/unbound.ts +33 -0
- package/src/data/db/values.ts +29 -0
- package/src/data/domain/brand.ts +15 -0
- package/src/data/domain/build-factory.ts +193 -0
- package/src/data/domain/build-schema.ts +161 -0
- package/src/data/domain/build-table.ts +355 -0
- package/src/data/domain/define.ts +239 -0
- package/src/data/domain/external-registry.ts +34 -0
- package/src/data/domain/external.ts +77 -0
- package/src/data/domain/field.ts +279 -0
- package/src/data/domain/index.ts +13 -0
- package/src/data/domain/naming.ts +6 -0
- package/src/data/domain/projections.ts +57 -0
- package/src/data/domain/trace-safe-projection.ts +31 -0
- package/src/data/domain/traits.ts +108 -0
- package/src/data/domain/types.ts +289 -0
- package/src/data/jsonb/composed.ts +18 -0
- package/src/data/jsonb/index.ts +1 -0
- package/src/data/jsonb/namespace.ts +30 -0
- package/src/data/jsonb/query.ts +86 -0
- package/src/data/migrate/append-only-generate.ts +47 -0
- package/src/data/migrate/applied.ts +47 -0
- package/src/data/migrate/apply.ts +309 -0
- package/src/data/migrate/check.ts +119 -0
- package/src/data/migrate/diff.ts +357 -0
- package/src/data/migrate/door-generate.ts +130 -0
- package/src/data/migrate/framework-rls.ts +58 -0
- package/src/data/migrate/generate.ts +201 -0
- package/src/data/migrate/history.ts +215 -0
- package/src/data/migrate/index.ts +9 -0
- package/src/data/migrate/internal-definitions.ts +175 -0
- package/src/data/migrate/post-process.ts +73 -0
- package/src/data/migrate/pragma.ts +73 -0
- package/src/data/migrate/provision-framework.ts +147 -0
- package/src/data/migrate/rls-generate.ts +61 -0
- package/src/data/migrate/run.ts +124 -0
- package/src/data/migrate/safety.ts +167 -0
- package/src/data/migrate/snapshot.ts +368 -0
- package/src/data/migrate/sql-emit.ts +392 -0
- package/src/data/migrate/statements.ts +312 -0
- package/src/data/query/alias.ts +126 -0
- package/src/data/query/batch.ts +8 -0
- package/src/data/query/cache/core/cache.ts +78 -0
- package/src/data/query/cache/core/index.ts +1 -0
- package/src/data/query/cache/core/types.ts +29 -0
- package/src/data/query/cache/readme.md +230 -0
- package/src/data/query/casing.ts +76 -0
- package/src/data/query/collation.ts +11 -0
- package/src/data/query/column-builder.ts +370 -0
- package/src/data/query/column.ts +150 -0
- package/src/data/query/entity.ts +42 -0
- package/src/data/query/errors.ts +33 -0
- package/src/data/query/index.ts +14 -0
- package/src/data/query/logger.ts +47 -0
- package/src/data/query/operations.ts +45 -0
- package/src/data/query/pg-core/alias.ts +12 -0
- package/src/data/query/pg-core/checks.ts +32 -0
- package/src/data/query/pg-core/columns/all.ts +71 -0
- package/src/data/query/pg-core/columns/bigint.ts +110 -0
- package/src/data/query/pg-core/columns/bigserial.ts +128 -0
- package/src/data/query/pg-core/columns/boolean.ts +43 -0
- package/src/data/query/pg-core/columns/char.ts +85 -0
- package/src/data/query/pg-core/columns/cidr.ts +43 -0
- package/src/data/query/pg-core/columns/common.ts +366 -0
- package/src/data/query/pg-core/columns/custom.ts +232 -0
- package/src/data/query/pg-core/columns/date.common.ts +15 -0
- package/src/data/query/pg-core/columns/date.ts +112 -0
- package/src/data/query/pg-core/columns/double-precision.ts +55 -0
- package/src/data/query/pg-core/columns/enum.ts +201 -0
- package/src/data/query/pg-core/columns/index.ts +35 -0
- package/src/data/query/pg-core/columns/inet.ts +43 -0
- package/src/data/query/pg-core/columns/int.common.ts +57 -0
- package/src/data/query/pg-core/columns/integer.ts +53 -0
- package/src/data/query/pg-core/columns/interval.ts +83 -0
- package/src/data/query/pg-core/columns/json.ts +58 -0
- package/src/data/query/pg-core/columns/jsonb.ts +56 -0
- package/src/data/query/pg-core/columns/line.ts +117 -0
- package/src/data/query/pg-core/columns/macaddr.ts +43 -0
- package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
- package/src/data/query/pg-core/columns/numeric.ts +227 -0
- package/src/data/query/pg-core/columns/point.ts +127 -0
- package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
- package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
- package/src/data/query/pg-core/columns/real.ts +58 -0
- package/src/data/query/pg-core/columns/serial.ts +55 -0
- package/src/data/query/pg-core/columns/smallint.ts +53 -0
- package/src/data/query/pg-core/columns/smallserial.ts +60 -0
- package/src/data/query/pg-core/columns/text.ts +67 -0
- package/src/data/query/pg-core/columns/time.ts +72 -0
- package/src/data/query/pg-core/columns/timestamp.ts +163 -0
- package/src/data/query/pg-core/columns/uuid.ts +51 -0
- package/src/data/query/pg-core/columns/varchar.ts +89 -0
- package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
- package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
- package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
- package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
- package/src/data/query/pg-core/db.ts +674 -0
- package/src/data/query/pg-core/dialect.ts +1404 -0
- package/src/data/query/pg-core/expressions.ts +25 -0
- package/src/data/query/pg-core/foreign-keys.ts +130 -0
- package/src/data/query/pg-core/index.ts +21 -0
- package/src/data/query/pg-core/indexes.ts +276 -0
- package/src/data/query/pg-core/policies.ts +56 -0
- package/src/data/query/pg-core/primary-keys.ts +62 -0
- package/src/data/query/pg-core/query-builders/count.ts +86 -0
- package/src/data/query/pg-core/query-builders/delete.ts +293 -0
- package/src/data/query/pg-core/query-builders/index.ts +7 -0
- package/src/data/query/pg-core/query-builders/insert.ts +452 -0
- package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
- package/src/data/query/pg-core/query-builders/query.ts +157 -0
- package/src/data/query/pg-core/query-builders/raw.ts +49 -0
- package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
- package/src/data/query/pg-core/query-builders/select.ts +1346 -0
- package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
- package/src/data/query/pg-core/query-builders/update.ts +626 -0
- package/src/data/query/pg-core/roles.ts +41 -0
- package/src/data/query/pg-core/schema.ts +72 -0
- package/src/data/query/pg-core/sequence.ts +41 -0
- package/src/data/query/pg-core/session.ts +292 -0
- package/src/data/query/pg-core/subquery.ts +46 -0
- package/src/data/query/pg-core/table.ts +252 -0
- package/src/data/query/pg-core/unique-constraint.ts +73 -0
- package/src/data/query/pg-core/utils/array.ts +95 -0
- package/src/data/query/pg-core/utils/index.ts +1 -0
- package/src/data/query/pg-core/utils.ts +104 -0
- package/src/data/query/pg-core/view-base.ts +14 -0
- package/src/data/query/pg-core/view-common.ts +1 -0
- package/src/data/query/pg-core/view.ts +435 -0
- package/src/data/query/pg-core/with-builder.ts +41 -0
- package/src/data/query/postgres-js/README.md +46 -0
- package/src/data/query/postgres-js/array-parser.ts +45 -0
- package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
- package/src/data/query/postgres-js/driver.ts +163 -0
- package/src/data/query/postgres-js/index.ts +2 -0
- package/src/data/query/postgres-js/session.ts +234 -0
- package/src/data/query/primary-key.ts +11 -0
- package/src/data/query/query-builders/query-builder.ts +19 -0
- package/src/data/query/query-builders/select.types.ts +175 -0
- package/src/data/query/query-promise.ts +35 -0
- package/src/data/query/relations.ts +725 -0
- package/src/data/query/runnable-query.ts +12 -0
- package/src/data/query/selection-proxy.ts +121 -0
- package/src/data/query/session.ts +8 -0
- package/src/data/query/sql/expressions/conditions.ts +741 -0
- package/src/data/query/sql/expressions/index.ts +2 -0
- package/src/data/query/sql/expressions/select.ts +45 -0
- package/src/data/query/sql/functions/aggregate.ts +129 -0
- package/src/data/query/sql/functions/index.ts +2 -0
- package/src/data/query/sql/functions/vector.ts +178 -0
- package/src/data/query/sql/index.ts +3 -0
- package/src/data/query/sql/sql.ts +794 -0
- package/src/data/query/subquery.ts +53 -0
- package/src/data/query/table.ts +208 -0
- package/src/data/query/table.utils.ts +2 -0
- package/src/data/query/tracing-utils.ts +3 -0
- package/src/data/query/tracing.ts +11 -0
- package/src/data/query/utils.ts +357 -0
- package/src/data/query/version.ts +4 -0
- package/src/data/query/view-common.ts +1 -0
- package/src/data/schema/branded.ts +13 -0
- package/src/data/schema/generated/coerce.ts +5 -0
- package/src/data/schema/generated/combinator.ts +6 -0
- package/src/data/schema/generated/encoding.ts +5 -0
- package/src/data/schema/generated/error.ts +6 -0
- package/src/data/schema/generated/format.ts +5 -0
- package/src/data/schema/generated/type-constructor.ts +10 -0
- package/src/data/schema/generated/type.ts +6 -0
- package/src/data/schema/index.ts +6 -0
- package/src/data/schema/json-schema.ts +33 -0
- package/src/data/schema/namespace.ts +55 -0
- package/src/data/schema/parse.ts +39 -0
- package/src/data/schema/types.ts +39 -0
- package/src/data/temporal/append-only.ts +134 -0
- package/src/data/temporal/close.ts +27 -0
- package/src/data/temporal/columns.ts +10 -0
- package/src/data/temporal/definition-queries.ts +70 -0
- package/src/data/temporal/errors.ts +10 -0
- package/src/data/temporal/get-current.ts +22 -0
- package/src/data/temporal/index.ts +9 -0
- package/src/data/temporal/namespace.ts +36 -0
- package/src/data/temporal/query.ts +25 -0
- package/src/data/temporal/resolve.ts +17 -0
- package/src/data/temporal/revise.ts +67 -0
- package/src/data/temporal/types.ts +22 -0
- package/src/data/valve/artifact.ts +33 -0
- package/src/data/valve/contract-diff.ts +21 -0
- package/src/data/valve/cross.ts +57 -0
- package/src/data/valve/crossing-ledger.ts +50 -0
- package/src/data/valve/decision.ts +44 -0
- package/src/data/valve/durable-crossing.ts +52 -0
- package/src/data/valve/execute-crossing.ts +77 -0
- package/src/data/valve/faucet.ts +56 -0
- package/src/data/valve/gate.ts +114 -0
- package/src/data/valve/graph.ts +77 -0
- package/src/data/valve/index.ts +53 -0
- package/src/data/valve/namespace.ts +57 -0
- package/src/data/valve/registry.ts +50 -0
- package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
- package/src/data/valve/schema/valve-crossing.ts +9 -0
- package/src/data/valve/supply.ts +154 -0
- package/src/data/valve/types.ts +143 -0
- package/src/data/vector/columns.ts +27 -0
- package/src/data/vector/index.ts +4 -0
- package/src/data/vector/namespace.ts +33 -0
- package/src/data/vector/query.ts +85 -0
- package/src/data/vector/validate.ts +16 -0
- package/src/data/write/audit/actor.ts +15 -0
- package/src/data/write/audit/audit-columns.ts +18 -0
- package/src/data/write/audit/audit-select.ts +111 -0
- package/src/data/write/audit/bound-transaction.ts +14 -0
- package/src/data/write/audit/cte-compile.ts +32 -0
- package/src/data/write/audit/decode-returning.ts +47 -0
- package/src/data/write/audit/final-select.ts +43 -0
- package/src/data/write/audit/infra-audit.ts +36 -0
- package/src/data/write/audit/primary-key.ts +65 -0
- package/src/data/write/audit/projection.ts +41 -0
- package/src/data/write/audit/read-touches.ts +86 -0
- package/src/data/write/audit/touch-match.ts +78 -0
- package/src/data/write/audit/verb-delete.ts +62 -0
- package/src/data/write/audit/verb-insert.ts +36 -0
- package/src/data/write/audit/verb-select.ts +47 -0
- package/src/data/write/audit/verb-update.ts +60 -0
- package/src/data/write/audit/verb-upsert.ts +104 -0
- package/src/data/write/conflict.ts +43 -0
- package/src/data/write/delete.ts +38 -0
- package/src/data/write/execute.ts +243 -0
- package/src/data/write/index.ts +11 -0
- package/src/data/write/insert.ts +63 -0
- package/src/data/write/state.ts +43 -0
- package/src/data/write/update.ts +54 -0
- package/src/data/write/upsert.ts +58 -0
- package/src/data/write/values.ts +19 -0
- package/src/index.ts +230 -0
- package/src/infra/access-links/code.ts +70 -0
- package/src/infra/access-links/handle.ts +892 -0
- package/src/infra/access-links/index.ts +9 -0
- package/src/infra/access-links/namespace.ts +30 -0
- package/src/infra/access-links/person-check.ts +177 -0
- package/src/infra/access-links/schema.ts +71 -0
- package/src/infra/alerts/async-sources.ts +231 -0
- package/src/infra/alerts/audit.ts +36 -0
- package/src/infra/alerts/beat.ts +123 -0
- package/src/infra/alerts/client-faults.ts +111 -0
- package/src/infra/alerts/faults.ts +156 -0
- package/src/infra/alerts/handle.ts +778 -0
- package/src/infra/alerts/http.ts +192 -0
- package/src/infra/alerts/index.ts +12 -0
- package/src/infra/alerts/infra-sources.ts +180 -0
- package/src/infra/alerts/namespace.ts +81 -0
- package/src/infra/alerts/redact.ts +21 -0
- package/src/infra/alerts/request-faults.ts +41 -0
- package/src/infra/alerts/schema.ts +124 -0
- package/src/infra/alerts/sinks.ts +125 -0
- package/src/infra/alerts/source-options.ts +26 -0
- package/src/infra/alerts/sources.ts +503 -0
- package/src/infra/alerts/subsystem-sources.ts +137 -0
- package/src/infra/audit/emit.ts +87 -0
- package/src/infra/audit/index.ts +2 -0
- package/src/infra/audit/namespace.ts +22 -0
- package/src/infra/audit/schema.sql +35 -0
- package/src/infra/audit/types.ts +18 -0
- package/src/infra/aws/credentials.ts +147 -0
- package/src/infra/aws/sigv4.ts +140 -0
- package/src/infra/cache/create.ts +58 -0
- package/src/infra/cache/entry.ts +81 -0
- package/src/infra/cache/index.ts +4 -0
- package/src/infra/cache/namespace.ts +10 -0
- package/src/infra/cache/tenant.ts +93 -0
- package/src/infra/cache/types.ts +24 -0
- package/src/infra/crawl/audit.ts +35 -0
- package/src/infra/crawl/errors.ts +25 -0
- package/src/infra/crawl/handle.ts +693 -0
- package/src/infra/crawl/index.ts +9 -0
- package/src/infra/crawl/links.ts +160 -0
- package/src/infra/crawl/namespace.ts +36 -0
- package/src/infra/crawl/robots.ts +100 -0
- package/src/infra/crawl/schema.ts +89 -0
- package/src/infra/credit-line/handle.ts +883 -0
- package/src/infra/credit-line/index.ts +6 -0
- package/src/infra/credit-line/namespace.ts +7 -0
- package/src/infra/credit-line/schema.ts +96 -0
- package/src/infra/credits/handle.ts +706 -0
- package/src/infra/credits/index.ts +5 -0
- package/src/infra/credits/namespace.ts +7 -0
- package/src/infra/credits/schema.ts +83 -0
- package/src/infra/email/audit.ts +36 -0
- package/src/infra/email/inbound.ts +622 -0
- package/src/infra/email/index.ts +11 -0
- package/src/infra/email/mail.ts +517 -0
- package/src/infra/email/mime.ts +447 -0
- package/src/infra/email/namespace.ts +22 -0
- package/src/infra/email/providers.ts +111 -0
- package/src/infra/email/schema.ts +96 -0
- package/src/infra/email/sns.ts +200 -0
- package/src/infra/email/unsubscribe.ts +44 -0
- package/src/infra/fetch/address.ts +88 -0
- package/src/infra/fetch/connect-retry.ts +87 -0
- package/src/infra/fetch/handle.ts +329 -0
- package/src/infra/fetch/index.ts +7 -0
- package/src/infra/fetch/namespace.ts +70 -0
- package/src/infra/idempotency/index.ts +3 -0
- package/src/infra/idempotency/middleware.ts +87 -0
- package/src/infra/idempotency/store.ts +92 -0
- package/src/infra/idempotency/types.ts +30 -0
- package/src/infra/join-requests/handle.ts +312 -0
- package/src/infra/join-requests/index.ts +2 -0
- package/src/infra/join-requests/namespace.ts +7 -0
- package/src/infra/join-requests/schema.ts +33 -0
- package/src/infra/outbox/core.ts +165 -0
- package/src/infra/quota/errors.ts +32 -0
- package/src/infra/quota/handle.ts +215 -0
- package/src/infra/quota/index.ts +5 -0
- package/src/infra/quota/namespace.ts +7 -0
- package/src/infra/quota/schema.ts +15 -0
- package/src/infra/storage/backend.ts +34 -0
- package/src/infra/storage/handle.ts +513 -0
- package/src/infra/storage/index.ts +9 -0
- package/src/infra/storage/local.ts +104 -0
- package/src/infra/storage/namespace.ts +13 -0
- package/src/infra/storage/s3.ts +257 -0
- package/src/infra/storage/schema.ts +24 -0
- package/src/infra/support/handle.ts +869 -0
- package/src/infra/support/http.ts +229 -0
- package/src/infra/support/index.ts +7 -0
- package/src/infra/support/namespace.ts +12 -0
- package/src/infra/support/schema.ts +96 -0
- package/src/infra/webhook/inbound.ts +45 -0
- package/src/infra/webhook/index.ts +5 -0
- package/src/infra/webhook/namespace.ts +24 -0
- package/src/infra/webhook/outbound.ts +315 -0
- package/src/infra/webhook/sign.ts +32 -0
- package/src/infra/webhook/verify.ts +82 -0
- package/src/invariants/assert.ts +26 -0
- package/src/invariants/index.ts +1 -0
- package/src/lint/config.ts +56 -0
- package/src/lint/doctrine.ts +163 -0
- package/src/lint/include.ts +97 -0
- package/src/lint/index.ts +3 -0
- package/src/lint/preset.ts +10 -0
- package/src/lint/resolver.ts +218 -0
- package/src/lint/rules/no-reexports.ts +46 -0
- package/src/request/behavior/audited.ts +85 -0
- package/src/request/behavior/cached.ts +62 -0
- package/src/request/behavior/compose.ts +28 -0
- package/src/request/behavior/index.ts +5 -0
- package/src/request/behavior/namespace.ts +16 -0
- package/src/request/behavior/types.ts +26 -0
- package/src/request/behavior/versioned.ts +54 -0
- package/src/request/context/create.ts +103 -0
- package/src/request/context/guard.ts +49 -0
- package/src/request/context/index.ts +4 -0
- package/src/request/context/run-in-transaction.ts +200 -0
- package/src/request/context/store.ts +28 -0
- package/src/request/context/types.ts +50 -0
- package/src/request/di/builder.ts +535 -0
- package/src/request/di/index.ts +8 -0
- package/src/request/di/resolve.ts +271 -0
- package/src/request/di/types.ts +262 -0
- package/src/request/fitting/namespace.ts +7 -0
- package/src/request/fixture/batch.ts +71 -0
- package/src/request/fixture/builder.ts +26 -0
- package/src/request/fixture/errors.ts +10 -0
- package/src/request/fixture/index.ts +8 -0
- package/src/request/fixture/namespace.ts +23 -0
- package/src/request/fixture/paginate.ts +44 -0
- package/src/request/fixture/to-handlers.ts +260 -0
- package/src/request/fixture/types.ts +79 -0
- package/src/request/flow/act-as.ts +78 -0
- package/src/request/flow/namespace.ts +17 -0
- package/src/request/http/error-step.ts +42 -0
- package/src/request/http/file.ts +141 -0
- package/src/request/http/index.ts +4 -0
- package/src/request/http/middleware.ts +141 -0
- package/src/request/http/namespace.ts +5 -0
- package/src/request/http/rate-limit-classes.ts +170 -0
- package/src/request/http/response.ts +82 -0
- package/src/request/http/server.ts +916 -0
- package/src/request/http/sniff.ts +261 -0
- package/src/request/http/types.ts +250 -0
- package/src/request/http/upload.ts +284 -0
- package/src/request/internal/text.ts +6 -0
- package/src/request/openapi/generate.ts +205 -0
- package/src/request/openapi/index.ts +2 -0
- package/src/request/openapi/namespace.ts +7 -0
- package/src/test/auth.ts +13 -0
- package/src/test/auto-setup.ts +45 -0
- package/src/test/context.ts +91 -0
- package/src/test/crawl-fixture.ts +157 -0
- package/src/test/discover-fork.ts +17 -0
- package/src/test/fingerprint.ts +135 -0
- package/src/test/globals.d.ts +7 -0
- package/src/test/index.ts +19 -0
- package/src/test/namespace.ts +87 -0
- package/src/test/plugin.ts +189 -0
- package/src/test/progress.ts +185 -0
- package/src/test/reap-after-run.ts +28 -0
- package/src/test/reap.ts +286 -0
- package/src/test/reset.ts +253 -0
- package/src/test/savepoints.ts +172 -0
- package/src/test/setup.ts +1422 -0
- package/src/test/system-channel.ts +50 -0
- package/src/test/vitest-plugin.ts +142 -0
- package/src/test/vitest-workspace.ts +507 -0
- package/src/test/vitest.ts +333 -0
- package/src/test/watch.ts +105 -0
- package/src/trace/capture/bounded-serializer.ts +146 -0
- package/src/trace/cardinality/analyze.ts +173 -0
- package/src/trace/cardinality/branch-context.ts +214 -0
- package/src/trace/cardinality/check.ts +33 -0
- package/src/trace/cardinality/classify-edge.ts +64 -0
- package/src/trace/cardinality/structural-max.ts +107 -0
- package/src/trace/check/coverage-check.ts +188 -0
- package/src/trace/check/flow-check.ts +103 -0
- package/src/trace/check/flow-step-site.ts +234 -0
- package/src/trace/check/io-pattern.ts +116 -0
- package/src/trace/codemod/add-cardinality.ts +152 -0
- package/src/trace/codemod/run.ts +80 -0
- package/src/trace/entry/child-trace.ts +21 -0
- package/src/trace/entry/close-trace.ts +87 -0
- package/src/trace/entry/finalize-trace.ts +28 -0
- package/src/trace/entry/open-trace.ts +138 -0
- package/src/trace/entry/parent-trace.ts +20 -0
- package/src/trace/entry/parse-traceparent.ts +39 -0
- package/src/trace/entry/process-start.ts +38 -0
- package/src/trace/entry/trace-context.ts +26 -0
- package/src/trace/entry/tracing-enabled.ts +10 -0
- package/src/trace/external/record.ts +195 -0
- package/src/trace/external/report.ts +66 -0
- package/src/trace/namespace.ts +19 -0
- package/src/trace/partition/day.ts +47 -0
- package/src/trace/partition/index.ts +14 -0
- package/src/trace/partition/partition-name.ts +23 -0
- package/src/trace/partition/plan.ts +82 -0
- package/src/trace/partition/schedule.ts +26 -0
- package/src/trace/partition/startup-hook.ts +39 -0
- package/src/trace/partition/sync.ts +154 -0
- package/src/trace/partition/tables.ts +53 -0
- package/src/trace/read/tree.ts +83 -0
- package/src/trace/retain/copy-on-error.ts +76 -0
- package/src/trace/runtime/expire-stale-traces.ts +54 -0
- package/src/trace/runtime/flow-step.ts +147 -0
- package/src/trace/runtime/flush-steps.ts +31 -0
- package/src/trace/runtime/pending-writes.ts +54 -0
- package/src/trace/runtime/step-buffer.ts +298 -0
- package/src/trace/runtime/step-error.ts +21 -0
- package/src/trace/runtime/step-meta.ts +55 -0
- package/src/trace/runtime/thenable.ts +12 -0
- package/src/trace/runtime/truncate-trace.ts +62 -0
- package/src/trace/schema/trace-fields.ts +52 -0
- package/src/trace/schema/trace-retained.ts +22 -0
- package/src/trace/schema/trace-step-fields.ts +44 -0
- package/src/trace/schema/trace-step-retained.ts +25 -0
- package/src/trace/schema/trace-step.ts +18 -0
- package/src/trace/schema/trace.ts +12 -0
- package/src/trace/transformer/apply.ts +79 -0
- package/src/trace/transformer/build-meta-literal.ts +30 -0
- package/src/trace/transformer/find-flow-step-sites.ts +85 -0
- package/src/trace/transformer/inject-flow-step-import.ts +39 -0
- package/src/trace/transformer/transform-error.ts +14 -0
- package/src/trace/transformer/transform-flow-steps.ts +123 -0
- package/src/trace/transformer/wrap-function-declaration.ts +62 -0
- package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
- package/src/workspace/affected.ts +110 -0
- package/src/workspace/changed-files.ts +163 -0
- package/src/workspace/check-deps.ts +213 -0
- package/src/workspace/checksum.ts +20 -0
- package/src/workspace/dependency-graph.ts +67 -0
- package/src/workspace/detect-pipework-copies.ts +93 -0
- package/src/workspace/discover.ts +206 -0
- package/src/workspace/generate.ts +401 -0
- package/src/workspace/index.ts +7 -0
- package/src/workspace/module-imports-pipework.ts +39 -0
- package/src/workspace/module-package-name.ts +12 -0
- package/src/workspace/scrub-git-env.ts +23 -0
- package/src/workspace/source-paths.ts +36 -0
- package/src/workspace/sync.ts +182 -0
- package/dist/REFERENCE.md +0 -1477
- package/dist/infra/access-links/rate-limit.d.ts +0 -10
- package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
- package/dist/infra/access-links/rate-limit.js +0 -28
- package/dist/infra/access-links/rate-limit.js.map +0 -1
- package/dist/infra/email/sigv4.d.ts +0 -24
- package/dist/infra/email/sigv4.d.ts.map +0 -1
- package/dist/infra/email/sigv4.js +0 -55
- package/dist/infra/email/sigv4.js.map +0 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { DefinedTable } from '../../data/domain/types.js'
|
|
2
|
+
import { resolveDefinitionRef } from '../../data/domain/types.js'
|
|
3
|
+
import type { ValidationIssue } from './types.js'
|
|
4
|
+
|
|
5
|
+
function inheritsBrandFromCompositeFK(def: DefinedTable, fieldName: string, brand: string): boolean {
|
|
6
|
+
for (const fk of def.$options.foreignKeys ?? []) {
|
|
7
|
+
const i = fk.columns.indexOf(fieldName)
|
|
8
|
+
if (i === -1) continue
|
|
9
|
+
const foreignKey = fk.references.columns[i]
|
|
10
|
+
if (foreignKey === undefined) continue
|
|
11
|
+
if (resolveDefinitionRef(fk.references.definition).$fields[foreignKey]?._meta.brand === brand) return true
|
|
12
|
+
}
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateDomainIntegrity(
|
|
17
|
+
definitions: ReadonlyMap<string, DefinedTable>,
|
|
18
|
+
configuredDatabases: readonly string[],
|
|
19
|
+
): ValidationIssue[] {
|
|
20
|
+
const issues: ValidationIssue[] = []
|
|
21
|
+
|
|
22
|
+
const tableNames = new Map<string, string>()
|
|
23
|
+
const brandNames = new Map<string, string>()
|
|
24
|
+
|
|
25
|
+
for (const [name, def] of definitions) {
|
|
26
|
+
if (def.$options.virtual === true) continue
|
|
27
|
+
|
|
28
|
+
if (tableNames.has(name)) {
|
|
29
|
+
issues.push({
|
|
30
|
+
phase: 'domain',
|
|
31
|
+
message: `"${name}" table name conflicts with "${tableNames.get(name)!}"`,
|
|
32
|
+
})
|
|
33
|
+
} else {
|
|
34
|
+
tableNames.set(name, name)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (def.$options.database !== undefined) {
|
|
38
|
+
if (!configuredDatabases.includes(def.$options.database)) {
|
|
39
|
+
issues.push({
|
|
40
|
+
phase: 'domain',
|
|
41
|
+
message: `${name}.database "${def.$options.database}" is not a configured database`,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let tenantFieldCount = 0
|
|
47
|
+
|
|
48
|
+
for (const [fieldName, descriptor] of Object.entries(def.$fields)) {
|
|
49
|
+
const meta = descriptor._meta
|
|
50
|
+
|
|
51
|
+
if (meta.tenant) tenantFieldCount++
|
|
52
|
+
|
|
53
|
+
if (meta.reference !== undefined) {
|
|
54
|
+
const refDef = resolveDefinitionRef(meta.reference.definition)
|
|
55
|
+
if (!definitions.has(refDef.$name)) {
|
|
56
|
+
issues.push({
|
|
57
|
+
phase: 'domain',
|
|
58
|
+
message: `${name}.${fieldName} references "${refDef.$name}" — no matching domain definition`,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const refField = refDef.$fields[meta.reference.field]
|
|
63
|
+
if (refField === undefined) {
|
|
64
|
+
issues.push({
|
|
65
|
+
phase: 'domain',
|
|
66
|
+
message: `${name}.${fieldName} references "${refDef.$name}.${meta.reference.field}" — field does not exist`,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (meta.brand !== undefined) {
|
|
72
|
+
const isInheritedFromFK = (meta.reference !== undefined
|
|
73
|
+
&& resolveDefinitionRef(meta.reference.definition).$fields[meta.reference.field]?._meta.brand === meta.brand)
|
|
74
|
+
|| inheritsBrandFromCompositeFK(def, fieldName, meta.brand)
|
|
75
|
+
|
|
76
|
+
if (!isInheritedFromFK) {
|
|
77
|
+
const existing = brandNames.get(meta.brand)
|
|
78
|
+
if (existing !== undefined && existing !== `${name}.${fieldName}`) {
|
|
79
|
+
issues.push({
|
|
80
|
+
phase: 'domain',
|
|
81
|
+
message: `brand "${meta.brand}" used by both ${existing} and ${name}.${fieldName}`,
|
|
82
|
+
})
|
|
83
|
+
} else {
|
|
84
|
+
brandNames.set(meta.brand, `${name}.${fieldName}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (tenantFieldCount > 1) {
|
|
91
|
+
issues.push({
|
|
92
|
+
phase: 'domain',
|
|
93
|
+
message: `${name} has ${String(tenantFieldCount)} tenant fields — exactly one expected`,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return issues
|
|
99
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { validateStartup, type ValidationContext } from './startup.js'
|
|
2
|
+
export { validateDomainIntegrity } from './domain.js'
|
|
3
|
+
export { collectSurfaceIssues } from './surfaces.js'
|
|
4
|
+
export { validateConnectivity } from './connectivity.js'
|
|
5
|
+
export { StartupValidationError, type ValidationIssue, type ValidationPhase } from './types.js'
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { ValidationIssue } from './types.js'
|
|
2
|
+
import type { ResolvedDatabase } from '../config/load.js'
|
|
3
|
+
import { probeConnectTimeout } from './connectivity.js'
|
|
4
|
+
import { queueDefinitions, queueProgressColumns } from '../../async/jobs/table.js'
|
|
5
|
+
import { getTableColumns } from '../../data/query/index.js'
|
|
6
|
+
|
|
7
|
+
// D2 residual risk (subseam audit 2026-07-16): a queue whose module is in no
|
|
8
|
+
// schema glob and no import chain reaches the registry at runtime (createQueue
|
|
9
|
+
// runs when the worker imports it) but never reached a migration — so the
|
|
10
|
+
// first enqueue in production failed on a missing relation. This check makes
|
|
11
|
+
// that state loud at startup and at test bootstrap instead.
|
|
12
|
+
//
|
|
13
|
+
// The same story one level down: a table that exists but predates the current
|
|
14
|
+
// queue definition (0.26.0 added claim_token) fails at the first claim() with
|
|
15
|
+
// a raw 42703. So the shape is compared too. Extra columns are ignored on
|
|
16
|
+
// purpose — a consumer may add their own columns to a queue table; only
|
|
17
|
+
// columns the definition requires and the table lacks are reported.
|
|
18
|
+
//
|
|
19
|
+
// One level down again: a column can be present under the right name and
|
|
20
|
+
// still be the wrong column. `ALTER COLUMN ... SET DATA TYPE` / `DROP NOT
|
|
21
|
+
// NULL` are migration steps a consumer can skip exactly as they can skip an
|
|
22
|
+
// ADD COLUMN, and the result is quieter: claim_token as text compares wrong
|
|
23
|
+
// against a uuid rather than erroring, so a fence built on it silently never
|
|
24
|
+
// matches. See columnDrift() for what counts as compatible.
|
|
25
|
+
|
|
26
|
+
/** A required column whose type or nullability in the database contradicts the definition. */
|
|
27
|
+
export interface QueueColumnDrift {
|
|
28
|
+
readonly column: string
|
|
29
|
+
/** Present when the type differs; the definition's spelling and the database's. */
|
|
30
|
+
readonly requiredType?: string
|
|
31
|
+
readonly foundType?: string
|
|
32
|
+
/** True when the definition requires NOT NULL and the column is nullable. */
|
|
33
|
+
readonly nullableButRequired?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A registered queue table that is absent, or present with columns missing or drifted. */
|
|
37
|
+
export interface QueueTableProblem {
|
|
38
|
+
readonly table: string
|
|
39
|
+
/** Empty when the table itself is absent. */
|
|
40
|
+
readonly missingColumns: readonly string[]
|
|
41
|
+
/** Omitted entirely when no present column has drifted. */
|
|
42
|
+
readonly driftedColumns?: readonly QueueColumnDrift[]
|
|
43
|
+
/** The configured `progressColumn` when the table lacks it. */
|
|
44
|
+
readonly missingProgressColumn?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function queueProgressColumnMessage(table: string, database: string, column: string): string {
|
|
48
|
+
return (
|
|
49
|
+
`queue table "${table}" in database "${database}" is missing column "${column}", which ` +
|
|
50
|
+
`createQueue({ progressColumn: '${column}' }) records as each claim's progressMark. ` +
|
|
51
|
+
`It is a column of your own: add it with a migration of your schema, then run \`pipework migrate\`.`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function queueTableMessage(table: string, database: string): string {
|
|
56
|
+
return (
|
|
57
|
+
`queue table "${table}" is registered (createQueue) but does not exist in database "${database}". ` +
|
|
58
|
+
`The module calling createQueue({ table: '${table}' }) must be reachable from the database's ` +
|
|
59
|
+
`schema glob (or imported by a schema file) so \`pipework generate\` emits its migration. ` +
|
|
60
|
+
`Add it to the schema patterns, run \`pipework generate\`, then \`pipework migrate\`.`
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function queueColumnsMessage(table: string, database: string, missing: readonly string[]): string {
|
|
65
|
+
const columns = missing.map(c => `"${c}"`).join(', ')
|
|
66
|
+
const plural = missing.length === 1 ? 'column' : 'columns'
|
|
67
|
+
return (
|
|
68
|
+
`queue table "${table}" in database "${database}" is missing ${plural} ${columns}, which the queue ` +
|
|
69
|
+
`definition requires. The table predates the current definition — its migration has not been ` +
|
|
70
|
+
`regenerated or applied. Run \`pipework generate\`, then \`pipework migrate\`. ` +
|
|
71
|
+
`(Extra columns of your own are fine; only missing ones are reported.)`
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function driftClause(drift: QueueColumnDrift): string {
|
|
76
|
+
const parts: string[] = []
|
|
77
|
+
if (drift.requiredType !== undefined && drift.foundType !== undefined) {
|
|
78
|
+
parts.push(`requires ${drift.requiredType} but the database has ${drift.foundType}`)
|
|
79
|
+
}
|
|
80
|
+
if (drift.nullableButRequired === true) {
|
|
81
|
+
parts.push('requires NOT NULL but the database allows nulls')
|
|
82
|
+
}
|
|
83
|
+
return `"${drift.column}" ${parts.join(', and ')}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function queueColumnDriftMessage(
|
|
87
|
+
table: string,
|
|
88
|
+
database: string,
|
|
89
|
+
drifted: readonly QueueColumnDrift[],
|
|
90
|
+
): string {
|
|
91
|
+
const plural = drifted.length === 1 ? 'column' : 'columns'
|
|
92
|
+
return (
|
|
93
|
+
`queue table "${table}" in database "${database}" has ${plural} the queue definition disagrees with: ` +
|
|
94
|
+
`${drifted.map(driftClause).join('; ')}. The table predates the current definition — the migration ` +
|
|
95
|
+
`that alters ${drifted.length === 1 ? 'it' : 'them'} has not been regenerated or applied. ` +
|
|
96
|
+
`Run \`pipework generate\`, then \`pipework migrate\`. ` +
|
|
97
|
+
`(Length and precision are not compared, and a column stricter than the definition asks is fine.)`
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The message for one problem, whichever kinds it carries. */
|
|
102
|
+
export function queueTableProblemMessage(problem: QueueTableProblem, database: string): string {
|
|
103
|
+
if (problem.missingColumns.length === 0 && (problem.driftedColumns ?? []).length === 0 && problem.missingProgressColumn === undefined) {
|
|
104
|
+
return queueTableMessage(problem.table, database)
|
|
105
|
+
}
|
|
106
|
+
const parts: string[] = []
|
|
107
|
+
if (problem.missingColumns.length > 0) {
|
|
108
|
+
parts.push(queueColumnsMessage(problem.table, database, problem.missingColumns))
|
|
109
|
+
}
|
|
110
|
+
if (problem.driftedColumns !== undefined && problem.driftedColumns.length > 0) {
|
|
111
|
+
parts.push(queueColumnDriftMessage(problem.table, database, problem.driftedColumns))
|
|
112
|
+
}
|
|
113
|
+
if (problem.missingProgressColumn !== undefined) {
|
|
114
|
+
parts.push(queueProgressColumnMessage(problem.table, database, problem.missingProgressColumn))
|
|
115
|
+
}
|
|
116
|
+
return parts.join(' ')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface RequiredColumn {
|
|
120
|
+
readonly name: string
|
|
121
|
+
/** The definition's own spelling, e.g. "timestamp with time zone"; resolved to an OID by Postgres. */
|
|
122
|
+
readonly sqlType: string
|
|
123
|
+
readonly notNull: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Columns the definition requires — read off the table it builds, never a second list. */
|
|
127
|
+
function requiredColumns(): Map<string, readonly RequiredColumn[]> {
|
|
128
|
+
const required = new Map<string, readonly RequiredColumn[]>()
|
|
129
|
+
for (const [table, definition] of queueDefinitions()) {
|
|
130
|
+
const columns = Object.values(getTableColumns(definition.table())).map(c => ({
|
|
131
|
+
name: c.name,
|
|
132
|
+
sqlType: c.getSQLType(),
|
|
133
|
+
notNull: c.notNull,
|
|
134
|
+
}))
|
|
135
|
+
required.set(table, columns)
|
|
136
|
+
}
|
|
137
|
+
return required
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface ActualColumn {
|
|
141
|
+
readonly typeOid: number
|
|
142
|
+
/** format_type without a typmod: the found type spelled as the definition spells it. */
|
|
143
|
+
readonly typeName: string
|
|
144
|
+
readonly notNull: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* What counts as compatible, and why:
|
|
149
|
+
*
|
|
150
|
+
* - Type is compared as canonical type identity, not spelling: the
|
|
151
|
+
* definition's declared type is resolved through Postgres's own
|
|
152
|
+
* `to_regtype` and the resulting OID compared against the column's
|
|
153
|
+
* `atttypid`. `timestamptz` and `timestamp with time zone` are then the
|
|
154
|
+
* same thing without a hand-maintained alias table that can rot. A type
|
|
155
|
+
* Postgres cannot resolve (to_regtype returns NULL — an enum or domain the
|
|
156
|
+
* database has not got) is not reported as drift: the missing type would
|
|
157
|
+
* have shown up as a missing table or column, and guessing here would
|
|
158
|
+
* report false drift. Note the arm swallows a pseudo-type too: `serial` is
|
|
159
|
+
* not a type Postgres resolves, so a column ever declared that way would be
|
|
160
|
+
* skipped silently, with no missing column to reveal it.
|
|
161
|
+
* - Type modifiers (length, precision) are ignored. varchar(64) vs
|
|
162
|
+
* varchar(128) is not reported: comparing typmod invites false drift on
|
|
163
|
+
* healthy tables, and a check that cries wolf gets switched off, which
|
|
164
|
+
* leaves things worse than not checking at all.
|
|
165
|
+
* - Nullability is reported in one direction only. A column the definition
|
|
166
|
+
* requires NOT NULL that is nullable in the database breaks a promise the
|
|
167
|
+
* definition makes, so it is reported. Stricter in the database than the
|
|
168
|
+
* definition asks cannot violate anything the definition promises, and a
|
|
169
|
+
* consumer may have tightened it deliberately, so it is not.
|
|
170
|
+
* - Defaults are not compared at all (out of scope for 0.28.0).
|
|
171
|
+
*/
|
|
172
|
+
function columnDrift(
|
|
173
|
+
required: RequiredColumn,
|
|
174
|
+
actual: ActualColumn,
|
|
175
|
+
expectedOid: number | undefined,
|
|
176
|
+
): QueueColumnDrift | undefined {
|
|
177
|
+
const typeDiffers = expectedOid !== undefined && expectedOid !== actual.typeOid
|
|
178
|
+
const nullableButRequired = required.notNull && !actual.notNull
|
|
179
|
+
if (!typeDiffers && !nullableButRequired) return undefined
|
|
180
|
+
return {
|
|
181
|
+
column: required.name,
|
|
182
|
+
...(typeDiffers ? { requiredType: required.sqlType, foundType: actual.typeName } : {}),
|
|
183
|
+
...(nullableButRequired ? { nullableButRequired: true } : {}),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Registered queue tables that are absent from the database at `url`, or
|
|
189
|
+
* present without every column their definition requires, or present with a
|
|
190
|
+
* required column whose type or nullability contradicts the definition.
|
|
191
|
+
* Opens a short-lived probe connection, mirroring validateConnectivity. Two
|
|
192
|
+
* queries total, whatever the number of queues. `timeoutMs: null` leaves the
|
|
193
|
+
* connect timeout to the driver (PGCONNECT_TIMEOUT, else 30 s).
|
|
194
|
+
*/
|
|
195
|
+
export async function queueTableProblems(url: string, timeoutMs: number | null = 5000): Promise<QueueTableProblem[]> {
|
|
196
|
+
const required = requiredColumns()
|
|
197
|
+
const tables = [...required.keys()]
|
|
198
|
+
if (tables.length === 0) return []
|
|
199
|
+
|
|
200
|
+
const { default: postgres } = await import('postgres')
|
|
201
|
+
const sql = postgres(url, {
|
|
202
|
+
max: 1,
|
|
203
|
+
idle_timeout: 1,
|
|
204
|
+
...probeConnectTimeout(timeoutMs),
|
|
205
|
+
onnotice: () => {},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const presence = await sql<{ name: string; present: boolean }[]>`
|
|
210
|
+
SELECT t.name, to_regclass('public.' || quote_ident(t.name)) IS NOT NULL AS present
|
|
211
|
+
FROM unnest(${tables}::text[]) AS t(name)
|
|
212
|
+
`
|
|
213
|
+
const present = new Set(presence.filter(r => r.present).map(r => r.name))
|
|
214
|
+
|
|
215
|
+
const actual = new Map<string, Map<string, ActualColumn>>()
|
|
216
|
+
const expectedOids = new Map<string, number>()
|
|
217
|
+
const typeNames = [...new Set(
|
|
218
|
+
[...present].flatMap(t => (required.get(t) ?? []).map(c => c.sqlType)),
|
|
219
|
+
)]
|
|
220
|
+
if (present.size > 0) {
|
|
221
|
+
// pg_attribute, not information_schema.columns: that view is filtered to
|
|
222
|
+
// columns the current role holds a privilege on, so a role with CONNECT
|
|
223
|
+
// and USAGE but no table grants would read zero columns and be told to
|
|
224
|
+
// migrate when the cure is a GRANT. attnum > 0 AND NOT attisdropped
|
|
225
|
+
// excludes system and dropped columns. The definition's declared types
|
|
226
|
+
// are canonicalised by the same round trip (to_regtype), so this stays
|
|
227
|
+
// two queries whatever the number of queues; format_type without a
|
|
228
|
+
// typmod prints the found type the way the definition spells it.
|
|
229
|
+
const [row] = await sql<{
|
|
230
|
+
columns: { table_name: string; column_name: string; type_oid: number; not_null: boolean; type_name: string }[]
|
|
231
|
+
types: { name: string; oid: number | null }[]
|
|
232
|
+
}[]>`
|
|
233
|
+
SELECT
|
|
234
|
+
(
|
|
235
|
+
SELECT coalesce(json_agg(json_build_object(
|
|
236
|
+
'table_name', c.relname, 'column_name', a.attname,
|
|
237
|
+
'type_oid', a.atttypid::int, 'not_null', a.attnotnull,
|
|
238
|
+
'type_name', format_type(a.atttypid, NULL)
|
|
239
|
+
)), '[]'::json)
|
|
240
|
+
FROM pg_attribute a
|
|
241
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
242
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
243
|
+
WHERE n.nspname = 'public' AND c.relname = ANY(${[...present]}::text[])
|
|
244
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
|
245
|
+
) AS columns,
|
|
246
|
+
(
|
|
247
|
+
SELECT coalesce(json_agg(json_build_object('name', t, 'oid', to_regtype(t)::oid::int)), '[]'::json)
|
|
248
|
+
FROM unnest(${typeNames}::text[]) AS t
|
|
249
|
+
) AS types
|
|
250
|
+
`
|
|
251
|
+
for (const column of row?.columns ?? []) {
|
|
252
|
+
let columns = actual.get(column.table_name)
|
|
253
|
+
if (columns === undefined) { columns = new Map(); actual.set(column.table_name, columns) }
|
|
254
|
+
columns.set(column.column_name, {
|
|
255
|
+
typeOid: column.type_oid,
|
|
256
|
+
typeName: column.type_name,
|
|
257
|
+
notNull: column.not_null,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
for (const type of row?.types ?? []) {
|
|
261
|
+
if (type.oid !== null && type.oid !== 0) expectedOids.set(type.name, type.oid)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const problems: QueueTableProblem[] = []
|
|
266
|
+
for (const table of tables) {
|
|
267
|
+
if (!present.has(table)) {
|
|
268
|
+
problems.push({ table, missingColumns: [] })
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
const have = actual.get(table) ?? new Map<string, ActualColumn>()
|
|
272
|
+
const missing: string[] = []
|
|
273
|
+
const drifted: QueueColumnDrift[] = []
|
|
274
|
+
for (const column of required.get(table) ?? []) {
|
|
275
|
+
const found = have.get(column.name)
|
|
276
|
+
if (found === undefined) { missing.push(column.name); continue }
|
|
277
|
+
// The unprivileged-role case: pg_attribute is readable by anyone, so a
|
|
278
|
+
// present column always carries its real type here.
|
|
279
|
+
const drift = columnDrift(column, found, expectedOids.get(column.sqlType))
|
|
280
|
+
if (drift !== undefined) drifted.push(drift)
|
|
281
|
+
}
|
|
282
|
+
const progress = queueProgressColumns().get(table)
|
|
283
|
+
const missingProgress = progress !== undefined && !have.has(progress) ? { missingProgressColumn: progress } : {}
|
|
284
|
+
if (missing.length > 0 || drifted.length > 0 || progress !== undefined && !have.has(progress)) {
|
|
285
|
+
problems.push({
|
|
286
|
+
table, missingColumns: missing, ...(drifted.length > 0 ? { driftedColumns: drifted } : {}), ...missingProgress,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return problems
|
|
291
|
+
} finally {
|
|
292
|
+
await sql.end({ timeout: 1 })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Startup-validation leg: every registered queue's table must exist, in shape, in the default database (where queue migrations are emitted). */
|
|
297
|
+
export async function validateQueueTables(
|
|
298
|
+
defaultDb: ResolvedDatabase,
|
|
299
|
+
timeoutMs = 5000,
|
|
300
|
+
): Promise<ValidationIssue[]> {
|
|
301
|
+
try {
|
|
302
|
+
const problems = await queueTableProblems(defaultDb.url, timeoutMs)
|
|
303
|
+
return problems.map(problem => ({
|
|
304
|
+
phase: 'database' as const,
|
|
305
|
+
message: queueTableProblemMessage(problem, defaultDb.name),
|
|
306
|
+
}))
|
|
307
|
+
} catch {
|
|
308
|
+
// An unreachable database is validateConnectivity's finding — reporting it
|
|
309
|
+
// here too would double up the same root cause.
|
|
310
|
+
return []
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ValidationIssue } from './types.js'
|
|
2
|
+
import type { ResolvedDatabase } from '../config/load.js'
|
|
3
|
+
import { probeConnectTimeout } from './connectivity.js'
|
|
4
|
+
import type { IsolationStrategy } from '../config/schema.js'
|
|
5
|
+
import { appPoolCreated } from '../config/app-pool.js'
|
|
6
|
+
|
|
7
|
+
// D4b (subseam audit 2026-07-16, finding F3): a database can carry a full RLS
|
|
8
|
+
// policy set while its runtime role is superuser or BYPASSRLS — every policy
|
|
9
|
+
// inert, with zero signal from the framework. Probed at startup and at test
|
|
10
|
+
// bootstrap; refusal is an error, not a warning.
|
|
11
|
+
|
|
12
|
+
export interface RlsRoleProbe {
|
|
13
|
+
readonly role: string
|
|
14
|
+
readonly superuser: boolean
|
|
15
|
+
readonly bypassRls: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function rlsRoleMessage(database: string, probe: RlsRoleProbe): string {
|
|
19
|
+
const flag = probe.superuser ? 'SUPERUSER' : 'BYPASSRLS'
|
|
20
|
+
return (
|
|
21
|
+
`database "${database}" has rls configured, but its runtime role "${probe.role}" is ${flag} — ` +
|
|
22
|
+
`row-level security policies never apply to it, so tenant isolation is inert. ` +
|
|
23
|
+
`Provision a non-BYPASSRLS, non-superuser application role and point the runtime connection at it ` +
|
|
24
|
+
`(appUrl/appTestUrl, or the url/testUrl var this connection resolved from).`
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The connection whose role actually executes queries under the harness: transaction isolation collapses every role onto the resolved `url` connection; database/container isolation resolves roles normally (`appUrl ?? url`). */
|
|
29
|
+
export function rlsProbeTarget(db: ResolvedDatabase, strategy: IsolationStrategy): string {
|
|
30
|
+
return appPoolCreated(strategy) ? (db.appUrl ?? db.url) : db.url
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Role flags for `current_user` at `url`. Short-lived probe connection, mirroring
|
|
35
|
+
* validateConnectivity. `timeoutMs: null` leaves the connect timeout to the
|
|
36
|
+
* driver (PGCONNECT_TIMEOUT, else 30 s).
|
|
37
|
+
*/
|
|
38
|
+
export async function probeRlsRole(url: string, timeoutMs: number | null = 5000): Promise<RlsRoleProbe> {
|
|
39
|
+
const { default: postgres } = await import('postgres')
|
|
40
|
+
const sql = postgres(url, {
|
|
41
|
+
max: 1,
|
|
42
|
+
idle_timeout: 1,
|
|
43
|
+
...probeConnectTimeout(timeoutMs),
|
|
44
|
+
onnotice: () => {},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const rows = await sql`SELECT current_user AS role, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`
|
|
49
|
+
const row = rows[0] as { role: string; rolsuper: boolean; rolbypassrls: boolean } | undefined
|
|
50
|
+
if (row === undefined) {
|
|
51
|
+
throw new Error('current_user not found in pg_roles')
|
|
52
|
+
}
|
|
53
|
+
return { role: row.role, superuser: row.rolsuper, bypassRls: row.rolbypassrls }
|
|
54
|
+
} finally {
|
|
55
|
+
await sql.end({ timeout: 1 })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Startup-validation leg: for each database with `rls` configured, the runtime connection's role (`appUrl ?? url`) must not bypass RLS. */
|
|
60
|
+
export async function validateRlsRoles(
|
|
61
|
+
databases: ReadonlyMap<string, ResolvedDatabase>,
|
|
62
|
+
timeoutMs = 5000,
|
|
63
|
+
): Promise<ValidationIssue[]> {
|
|
64
|
+
const issues: ValidationIssue[] = []
|
|
65
|
+
|
|
66
|
+
const checks = [...databases.entries()]
|
|
67
|
+
.filter(([, db]) => db.rls !== undefined)
|
|
68
|
+
.map(async ([name, db]) => {
|
|
69
|
+
let probe: RlsRoleProbe
|
|
70
|
+
try {
|
|
71
|
+
probe = await probeRlsRole(db.appUrl ?? db.url, timeoutMs)
|
|
72
|
+
} catch {
|
|
73
|
+
// An unreachable database is validateConnectivity's finding.
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (probe.superuser || probe.bypassRls) {
|
|
77
|
+
issues.push({ phase: 'database', message: rlsRoleMessage(name, probe) })
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await Promise.all(checks)
|
|
82
|
+
return issues
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function systemRoleMessage(database: string, platformRole: string, probe: RlsRoleProbe): string[] {
|
|
86
|
+
const out: string[] = []
|
|
87
|
+
if (probe.superuser || probe.bypassRls) {
|
|
88
|
+
const flag = probe.superuser ? 'SUPERUSER' : 'BYPASSRLS'
|
|
89
|
+
out.push(
|
|
90
|
+
`database "${database}": the systemUrl role "${probe.role}" is ${flag} — row-level security never applies to it, ` +
|
|
91
|
+
`so the system channel reaches every tenant row and no door means anything. ` +
|
|
92
|
+
`Point systemUrl/systemTestUrl at a role with neither flag, named rls.platformRole.`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
if (probe.role !== platformRole) {
|
|
96
|
+
out.push(
|
|
97
|
+
`database "${database}": the systemUrl connection authenticates as "${probe.role}", but rls.platformRole is "${platformRole}" — ` +
|
|
98
|
+
`door policies are granted to "${platformRole}" only. Point systemUrl/systemTestUrl at a connection as "${platformRole}", ` +
|
|
99
|
+
`or change rls.platformRole.`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Startup-validation leg: for each database in platform mode, the `systemUrl` role must be the named platform role and must not bypass RLS. */
|
|
106
|
+
export async function validateSystemRoles(
|
|
107
|
+
databases: ReadonlyMap<string, ResolvedDatabase>,
|
|
108
|
+
timeoutMs = 5000,
|
|
109
|
+
): Promise<ValidationIssue[]> {
|
|
110
|
+
const issues: ValidationIssue[] = []
|
|
111
|
+
|
|
112
|
+
const checks = [...databases.entries()]
|
|
113
|
+
.filter(([, db]) => db.rls?.platformRole !== undefined && db.systemUrl !== undefined)
|
|
114
|
+
.map(async ([name, db]) => {
|
|
115
|
+
let probe: RlsRoleProbe
|
|
116
|
+
try {
|
|
117
|
+
probe = await probeRlsRole(db.systemUrl!, timeoutMs)
|
|
118
|
+
} catch {
|
|
119
|
+
// An unreachable database is validateConnectivity's finding.
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
for (const message of systemRoleMessage(name, db.rls!.platformRole!, probe)) {
|
|
123
|
+
issues.push({ phase: 'database', message })
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await Promise.all(checks)
|
|
128
|
+
return issues
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Databases with `rls` and no platform role: their system channel runs on the owner role, which bypasses RLS. */
|
|
132
|
+
export function ownerSystemChannelWarnings(databases: ReadonlyMap<string, ResolvedDatabase>): string[] {
|
|
133
|
+
return [...databases.entries()]
|
|
134
|
+
.filter(([, db]) => db.rls !== undefined && db.rls.platformRole === undefined)
|
|
135
|
+
.map(([name]) =>
|
|
136
|
+
`database "${name}" has rls configured and no platform role: pipe.system() runs on the owner role, which bypasses RLS. ` +
|
|
137
|
+
`Set systemUrl and rls.platformRole to run it on a role with no BYPASSRLS and read tenant tables through declared doors.`)
|
|
138
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ValidationIssue } from './types.js'
|
|
2
|
+
import { StartupValidationError } from './types.js'
|
|
3
|
+
import { validateDomainIntegrity } from './domain.js'
|
|
4
|
+
import { collectSurfaceIssues } from './surfaces.js'
|
|
5
|
+
import { validateConnectivity } from './connectivity.js'
|
|
6
|
+
import { validateQueueTables } from './queue-tables.js'
|
|
7
|
+
import { validateRlsRoles, validateSystemRoles } from './rls-role.js'
|
|
8
|
+
import { definitions } from '../../data/domain/define.js'
|
|
9
|
+
import type { SurfaceMap } from '../surface/types.js'
|
|
10
|
+
import type { ResolvedDatabase } from '../config/load.js'
|
|
11
|
+
|
|
12
|
+
export interface ValidationContext {
|
|
13
|
+
readonly configuredDatabases: readonly string[]
|
|
14
|
+
readonly environment: string
|
|
15
|
+
readonly surfaces?: SurfaceMap | undefined
|
|
16
|
+
readonly defaultDbAvailable?: boolean | undefined
|
|
17
|
+
readonly databases?: ReadonlyMap<string, ResolvedDatabase> | undefined
|
|
18
|
+
/** The database queue migrations are emitted into — queue-table existence is checked there. Undefined skips the check (genuinely ambiguous multi-DB configs fail earlier, at generate). */
|
|
19
|
+
readonly defaultDatabase?: ResolvedDatabase | undefined
|
|
20
|
+
readonly connectivityCheck?: boolean | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function validateStartup(ctx: ValidationContext): Promise<void> {
|
|
24
|
+
const issues: ValidationIssue[] = []
|
|
25
|
+
|
|
26
|
+
issues.push(...validateDomainIntegrity(definitions(), ctx.configuredDatabases))
|
|
27
|
+
|
|
28
|
+
if (ctx.surfaces !== undefined) {
|
|
29
|
+
issues.push(
|
|
30
|
+
...collectSurfaceIssues(
|
|
31
|
+
ctx.surfaces,
|
|
32
|
+
ctx.configuredDatabases,
|
|
33
|
+
ctx.defaultDbAvailable ?? ctx.configuredDatabases.length === 1,
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (ctx.connectivityCheck !== false && ctx.databases !== undefined) {
|
|
39
|
+
issues.push(...await validateConnectivity(ctx.databases))
|
|
40
|
+
issues.push(...await validateRlsRoles(ctx.databases))
|
|
41
|
+
issues.push(...await validateSystemRoles(ctx.databases))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (ctx.connectivityCheck !== false && ctx.defaultDatabase !== undefined) {
|
|
45
|
+
issues.push(...await validateQueueTables(ctx.defaultDatabase))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (issues.length > 0) {
|
|
49
|
+
throw new StartupValidationError(issues)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SurfaceMap } from '../surface/types.js'
|
|
2
|
+
import { DEFAULT_DB } from '../../request/di/types.js'
|
|
3
|
+
import type { ValidationIssue } from './types.js'
|
|
4
|
+
|
|
5
|
+
export function collectSurfaceIssues(
|
|
6
|
+
surfaces: SurfaceMap,
|
|
7
|
+
configuredDatabases: readonly string[],
|
|
8
|
+
defaultDbAvailable: boolean,
|
|
9
|
+
): ValidationIssue[] {
|
|
10
|
+
const issues: ValidationIssue[] = []
|
|
11
|
+
const dbSet = new Set(configuredDatabases)
|
|
12
|
+
|
|
13
|
+
for (const [name, surface] of Object.entries(surfaces)) {
|
|
14
|
+
for (const db of surface.manifest.databases) {
|
|
15
|
+
if (db === DEFAULT_DB) {
|
|
16
|
+
if (!defaultDbAvailable) {
|
|
17
|
+
issues.push({
|
|
18
|
+
phase: 'surface',
|
|
19
|
+
message: `"${name}" uses the default database, but multiple databases are configured: ${configuredDatabases.join(', ')}`,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
} else if (!dbSet.has(db)) {
|
|
23
|
+
issues.push({
|
|
24
|
+
phase: 'surface',
|
|
25
|
+
message: `"${name}" requires database "${db}" which is not configured (available: ${configuredDatabases.join(', ')})`,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (surface.kind === 'worker' && surface.manifest.jobTypes.length === 0) {
|
|
31
|
+
issues.push({
|
|
32
|
+
phase: 'surface',
|
|
33
|
+
message: `"${name}" is a worker with no job handlers`,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const seen = new Set<string>()
|
|
38
|
+
for (const jobType of surface.manifest.jobTypes) {
|
|
39
|
+
if (seen.has(jobType)) {
|
|
40
|
+
issues.push({
|
|
41
|
+
phase: 'surface',
|
|
42
|
+
message: `"${name}" has duplicate job type "${jobType}"`,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
seen.add(jobType)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const jobOwners = new Map<string, string>()
|
|
50
|
+
for (const [name, surface] of Object.entries(surfaces)) {
|
|
51
|
+
for (const jobType of surface.manifest.jobTypes) {
|
|
52
|
+
const existing = jobOwners.get(jobType)
|
|
53
|
+
if (existing !== undefined) {
|
|
54
|
+
issues.push({
|
|
55
|
+
phase: 'surface',
|
|
56
|
+
message: `job type "${jobType}" handled by both "${existing}" and "${name}"`,
|
|
57
|
+
})
|
|
58
|
+
} else {
|
|
59
|
+
jobOwners.set(jobType, name)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return issues
|
|
65
|
+
}
|