pipework 0.252.0 → 0.252.1
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 +5 -0
- 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 +854 -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,312 @@
|
|
|
1
|
+
import { sql } from '../../data/db/sql.js'
|
|
2
|
+
import { pipe, pipeRaw, pipeSystem } from '../../data/db/db.js'
|
|
3
|
+
import type { DB } from '../../data/db/index.js'
|
|
4
|
+
import { requireContext } from '../../request/context/store.js'
|
|
5
|
+
import type { Flow } from '../../request/context/types.js'
|
|
6
|
+
import { writeInfraAudit } from '../../data/write/audit/infra-audit.js'
|
|
7
|
+
import { ValidationError, ForbiddenError } from '../../core/errors/index.js'
|
|
8
|
+
import { inTenant as runAsTenant } from '../../auth/tenant/in-tenant.js'
|
|
9
|
+
import { JOIN_REQUEST_STATUSES } from './schema.js'
|
|
10
|
+
|
|
11
|
+
export type JoinRequestStatus = typeof JOIN_REQUEST_STATUSES[number]
|
|
12
|
+
|
|
13
|
+
export interface JoinRequest {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly orgId: string
|
|
16
|
+
readonly userId: string
|
|
17
|
+
readonly status: JoinRequestStatus
|
|
18
|
+
readonly reason: string | null
|
|
19
|
+
/** App data given to `request`; may be personal data, so the audit record leaves it out. */
|
|
20
|
+
readonly evidence: unknown
|
|
21
|
+
readonly requestedAt: Date
|
|
22
|
+
readonly expiresAt: Date
|
|
23
|
+
readonly settledAt: Date | null
|
|
24
|
+
/** Who settled it; `null` while open, and for an expiry. */
|
|
25
|
+
readonly settledBy: string | null
|
|
26
|
+
/** The role given on approval. */
|
|
27
|
+
readonly role: string | null
|
|
28
|
+
/** The note given on refusal. */
|
|
29
|
+
readonly note: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface JoinRequestsConfig {
|
|
33
|
+
/** The app's owner check; roles are the app's. Called inside the org's transaction. */
|
|
34
|
+
isOwner(tx: DB, args: { orgId: string; userId: string }): Promise<boolean>
|
|
35
|
+
/** Adds the member; called inside the approve transaction, so a throw rolls the approval back. */
|
|
36
|
+
addMember(tx: DB, args: { orgId: string; userId: string; role: string | undefined; requestId: string; by: string }): Promise<void>
|
|
37
|
+
/** How long a request stays open. Default 30 days. */
|
|
38
|
+
readonly expiresInMs?: number
|
|
39
|
+
/** Awaited inside the transaction that created the request; a throw rolls it back. */
|
|
40
|
+
onRequested?(tx: DB, request: JoinRequest): void | Promise<void>
|
|
41
|
+
/** Awaited inside the transaction that settled the request (approved, refused, withdrawn, expired); a throw rolls it back. */
|
|
42
|
+
onSettled?(tx: DB, request: JoinRequest): void | Promise<void>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type SettleResult =
|
|
46
|
+
| { readonly ok: true; readonly request: JoinRequest }
|
|
47
|
+
| { readonly ok: false; readonly reason: 'not-found' | 'expired' | 'already-settled' }
|
|
48
|
+
|
|
49
|
+
export interface JoinRequests {
|
|
50
|
+
/**
|
|
51
|
+
* Asks to join `orgId` as `userId`. Runs in the named org (the joiner is not in it). One open request
|
|
52
|
+
* per (org, user): while one is open and unexpired it is returned with `created: false`, with no audit
|
|
53
|
+
* record and no hook. An open one past its expiry is marked expired first. Writes a `JoinRequest:request`
|
|
54
|
+
* audit record when the Flow has a trace with an open step.
|
|
55
|
+
*/
|
|
56
|
+
request(orgId: string, userId: string, opts?: { reason?: string; evidence?: unknown }): Promise<{ request: JoinRequest; created: boolean }>
|
|
57
|
+
/** The Flow tenant's owner (`isOwner`) approves: marks it approved and calls `addMember`, in one transaction. A non-owner gets `ForbiddenError`. */
|
|
58
|
+
approve(requestId: string, opts: { role?: string }, by: string): Promise<SettleResult>
|
|
59
|
+
/** The Flow tenant's owner refuses it. A non-owner gets `ForbiddenError`. */
|
|
60
|
+
refuse(requestId: string, opts: { note?: string }, by: string): Promise<SettleResult>
|
|
61
|
+
/** The joiner withdraws their own open request; another user's id is `not-found`. */
|
|
62
|
+
withdraw(requestId: string, userId: string): Promise<SettleResult>
|
|
63
|
+
/** The Flow tenant's requests with `status` (default `open`), newest first. */
|
|
64
|
+
list(opts?: { status?: JoinRequestStatus }): Promise<JoinRequest[]>
|
|
65
|
+
/** One user's requests across orgs, read through the system channel, newest first. */
|
|
66
|
+
mine(userId: string): Promise<JoinRequest[]>
|
|
67
|
+
/** Marks every open request past its expiry as expired; schedule it (a cron job). */
|
|
68
|
+
sweep(): Promise<{ expired: number }>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const DEFAULT_EXPIRES_IN_MS = 30 * 24 * 60 * 60 * 1000
|
|
72
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
73
|
+
const MAX_INSERT_ATTEMPTS = 3
|
|
74
|
+
|
|
75
|
+
type Row = Record<string, unknown>
|
|
76
|
+
|
|
77
|
+
function toDate(value: unknown): Date {
|
|
78
|
+
return value instanceof Date ? value : new Date(String(value))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toDateOrNull(value: unknown): Date | null {
|
|
82
|
+
return value === null || value === undefined ? null : toDate(value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rowToRequest(row: Row): JoinRequest {
|
|
86
|
+
return {
|
|
87
|
+
id: row['id'] as string,
|
|
88
|
+
orgId: row['tenant_id'] as string,
|
|
89
|
+
userId: row['user_id'] as string,
|
|
90
|
+
status: row['status'] as JoinRequestStatus,
|
|
91
|
+
reason: (row['reason'] as string | null) ?? null,
|
|
92
|
+
evidence: row['evidence'] ?? null,
|
|
93
|
+
requestedAt: toDate(row['requested_at']),
|
|
94
|
+
expiresAt: toDate(row['expires_at']),
|
|
95
|
+
settledAt: toDateOrNull(row['settled_at']),
|
|
96
|
+
settledBy: (row['settled_by'] as string | null) ?? null,
|
|
97
|
+
role: (row['role'] as string | null) ?? null,
|
|
98
|
+
note: (row['note'] as string | null) ?? null,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isUniqueViolation(err: unknown): boolean {
|
|
103
|
+
let current: unknown = err
|
|
104
|
+
for (let depth = 0; depth < 4 && typeof current === 'object' && current !== null; depth++) {
|
|
105
|
+
if ((current as { code?: unknown }).code === '23505') return true
|
|
106
|
+
current = (current as { cause?: unknown }).cause
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requireUuid(value: unknown, name: string): string {
|
|
112
|
+
if (typeof value !== 'string' || !UUID.test(value)) {
|
|
113
|
+
throw new ValidationError({ [name]: ['must be a uuid'] }, `joinRequests: ${name} must be a uuid, got ${JSON.stringify(value)}.`)
|
|
114
|
+
}
|
|
115
|
+
return value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requireText(value: unknown, name: string): string {
|
|
119
|
+
if (typeof value !== 'string' || value === '') {
|
|
120
|
+
throw new ValidationError({ [name]: ['must be a non-empty string'] }, `joinRequests: ${name} must be a non-empty string.`)
|
|
121
|
+
}
|
|
122
|
+
return value
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The audit record leaves out `evidence`, which may be personal data.
|
|
126
|
+
function auditProjection(request: JoinRequest): Record<string, unknown> {
|
|
127
|
+
const { id, orgId, userId, status, reason, requestedAt, expiresAt, settledAt, settledBy, role, note } = request
|
|
128
|
+
return { id, orgId, userId, status, reason, requestedAt, expiresAt, settledAt, settledBy, role, note }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createJoinRequests(config: JoinRequestsConfig): JoinRequests {
|
|
132
|
+
if (typeof config.isOwner !== 'function' || typeof config.addMember !== 'function') {
|
|
133
|
+
throw new Error('[pipework] joinRequests.create needs `isOwner` and `addMember`.\n\n Roles are the app\'s: isOwner(tx, { orgId, userId }) says who may settle a request, and\n addMember(tx, { orgId, userId, role, requestId, by }) adds the member inside the approval.\n')
|
|
134
|
+
}
|
|
135
|
+
const expiresInMs = config.expiresInMs ?? DEFAULT_EXPIRES_IN_MS
|
|
136
|
+
if (!Number.isFinite(expiresInMs) || expiresInMs <= 0) {
|
|
137
|
+
throw new Error('[pipework] joinRequests expiresInMs must be a positive number of milliseconds.\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function requireTenant(): { flow: Flow; tenantId: string } {
|
|
141
|
+
const flow = requireContext()
|
|
142
|
+
if (flow.tenant === null) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'[pipework] This joinRequests method needs a tenant in the Flow.\n\n' +
|
|
145
|
+
' approve/refuse/list act for one org. Call them from an authenticated tenant route;\n' +
|
|
146
|
+
' request/withdraw/mine are the ones for the joiner, who is not in the org.\n',
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return { flow, tenantId: flow.tenant }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function inTenant<T>(flow: Flow, tenantId: string, fn: (tx: DB) => Promise<T>): Promise<T> {
|
|
153
|
+
return runAsTenant(flow, tenantId, fn)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function audit(flow: Flow, tx: DB, verb: string, request: JoinRequest): Promise<void> {
|
|
157
|
+
return writeInfraAudit(flow, tx, {
|
|
158
|
+
tenantId: request.orgId, entity: 'JoinRequest', verb, entityId: request.id, after: auditProjection(request),
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function settled(flow: Flow, tx: DB, verb: string, request: JoinRequest): Promise<void> {
|
|
163
|
+
await audit(flow, tx, verb, request)
|
|
164
|
+
await config.onSettled?.(tx, request)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Says why an open row was not there to settle.
|
|
168
|
+
async function whyNot(tx: DB, requestId: string, orgId: string): Promise<'not-found' | 'expired' | 'already-settled'> {
|
|
169
|
+
const rows = await tx.execute(sql`
|
|
170
|
+
SELECT status, (expires_at <= now()) AS past FROM join_request WHERE id = ${requestId} AND tenant_id = ${orgId}`)
|
|
171
|
+
const row = rows[0]
|
|
172
|
+
if (row === undefined) return 'not-found'
|
|
173
|
+
if (row['status'] === 'expired' || (row['status'] === 'open' && row['past'] === true)) return 'expired'
|
|
174
|
+
return 'already-settled'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function settleAsOwner(kind: 'approve' | 'refuse', requestId: string, by: string, opts: { role?: string; note?: string }): Promise<SettleResult> {
|
|
178
|
+
requireUuid(requestId, 'requestId')
|
|
179
|
+
requireText(by, 'by')
|
|
180
|
+
const { flow, tenantId } = requireTenant()
|
|
181
|
+
return pipe.transaction(async (tx: DB): Promise<SettleResult> => {
|
|
182
|
+
if (!(await config.isOwner(tx, { orgId: tenantId, userId: by }))) {
|
|
183
|
+
throw new ForbiddenError(`Only an owner of the organisation can ${kind} a join request.`, { orgId: tenantId, userId: by })
|
|
184
|
+
}
|
|
185
|
+
const rows = kind === 'approve'
|
|
186
|
+
? await tx.execute(sql`
|
|
187
|
+
UPDATE join_request SET status = 'approved', settled_at = now(), settled_by = ${by}, role = ${opts.role ?? null}
|
|
188
|
+
WHERE id = ${requestId} AND tenant_id = ${tenantId} AND status = 'open' AND expires_at > now()
|
|
189
|
+
RETURNING *`)
|
|
190
|
+
: await tx.execute(sql`
|
|
191
|
+
UPDATE join_request SET status = 'refused', settled_at = now(), settled_by = ${by}, note = ${opts.note ?? null}
|
|
192
|
+
WHERE id = ${requestId} AND tenant_id = ${tenantId} AND status = 'open' AND expires_at > now()
|
|
193
|
+
RETURNING *`)
|
|
194
|
+
if (rows.length === 0) return { ok: false, reason: await whyNot(tx, requestId, tenantId) }
|
|
195
|
+
const request = rowToRequest(rows[0]!)
|
|
196
|
+
if (kind === 'approve') {
|
|
197
|
+
await config.addMember(tx, { orgId: tenantId, userId: request.userId, role: opts.role, requestId, by })
|
|
198
|
+
}
|
|
199
|
+
await settled(flow, tx, kind, request)
|
|
200
|
+
return { ok: true, request }
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Marks the open requests of one org that are past expiry (optionally one user's) as expired.
|
|
205
|
+
async function expireOpen(flow: Flow, tx: DB, orgId: string, userId?: string): Promise<number> {
|
|
206
|
+
const rows = await tx.execute(sql`
|
|
207
|
+
UPDATE join_request SET status = 'expired', settled_at = now()
|
|
208
|
+
WHERE tenant_id = ${orgId} AND status = 'open' AND expires_at <= now()
|
|
209
|
+
${userId === undefined ? sql`` : sql`AND user_id = ${userId}`}
|
|
210
|
+
RETURNING *`)
|
|
211
|
+
for (const row of rows) await settled(flow, tx, 'expire', rowToRequest(row))
|
|
212
|
+
return rows.length
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handle: JoinRequests = {
|
|
216
|
+
async request(orgId, userId, opts) {
|
|
217
|
+
requireUuid(orgId, 'orgId')
|
|
218
|
+
requireText(userId, 'userId')
|
|
219
|
+
const flow = requireContext()
|
|
220
|
+
const evidence = opts?.evidence === undefined || opts.evidence === null ? null : JSON.stringify(opts.evidence)
|
|
221
|
+
return inTenant(flow, orgId, async (tx) => {
|
|
222
|
+
for (let attempt = 1; attempt <= MAX_INSERT_ATTEMPTS; attempt++) {
|
|
223
|
+
const open = await tx.execute(sql`
|
|
224
|
+
SELECT *, (expires_at <= now()) AS past FROM join_request
|
|
225
|
+
WHERE tenant_id = ${orgId} AND user_id = ${userId} AND status = 'open'`)
|
|
226
|
+
if (open.length > 0) {
|
|
227
|
+
if (open[0]!['past'] !== true) return { request: rowToRequest(open[0]!), created: false }
|
|
228
|
+
await expireOpen(flow, tx, orgId, userId)
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
// A savepoint: losing the race to another request aborts only this insert.
|
|
232
|
+
const rows = await tx.transaction((sp: DB) => sp.execute(sql`
|
|
233
|
+
INSERT INTO join_request (tenant_id, user_id, status, reason, evidence, expires_at)
|
|
234
|
+
VALUES (${orgId}, ${userId}, 'open', ${opts?.reason ?? null},
|
|
235
|
+
${evidence === null ? sql`NULL` : sql`${evidence}::jsonb`},
|
|
236
|
+
now() + ${expiresInMs}::float8 * interval '1 millisecond')
|
|
237
|
+
RETURNING *`))
|
|
238
|
+
const request = rowToRequest(rows[0]!)
|
|
239
|
+
await audit(flow, tx, 'request', request)
|
|
240
|
+
await config.onRequested?.(tx, request)
|
|
241
|
+
return { request, created: true }
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (!isUniqueViolation(err)) throw err
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
throw new Error('[pipework] joinRequests.request could not settle on an open request after repeated conflicts; retry.\n')
|
|
247
|
+
})
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
approve(requestId, opts, by) {
|
|
251
|
+
return settleAsOwner('approve', requestId, by, { ...(opts.role !== undefined ? { role: opts.role } : {}) })
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
refuse(requestId, opts, by) {
|
|
255
|
+
return settleAsOwner('refuse', requestId, by, { ...(opts.note !== undefined ? { note: opts.note } : {}) })
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async withdraw(requestId, userId) {
|
|
259
|
+
requireUuid(requestId, 'requestId')
|
|
260
|
+
requireText(userId, 'userId')
|
|
261
|
+
const flow = requireContext()
|
|
262
|
+
const found = await pipeSystem().execute(sql`SELECT tenant_id FROM join_request WHERE id = ${requestId}`)
|
|
263
|
+
if (found.length === 0) return { ok: false, reason: 'not-found' }
|
|
264
|
+
const orgId = found[0]!['tenant_id'] as string
|
|
265
|
+
return inTenant(flow, orgId, async (tx): Promise<SettleResult> => {
|
|
266
|
+
const rows = await tx.execute(sql`
|
|
267
|
+
UPDATE join_request SET status = 'withdrawn', settled_at = now(), settled_by = ${userId}
|
|
268
|
+
WHERE id = ${requestId} AND tenant_id = ${orgId} AND user_id = ${userId} AND status = 'open' AND expires_at > now()
|
|
269
|
+
RETURNING *`)
|
|
270
|
+
if (rows.length === 0) {
|
|
271
|
+
const own = await tx.execute(sql`SELECT 1 FROM join_request WHERE id = ${requestId} AND tenant_id = ${orgId} AND user_id = ${userId}`)
|
|
272
|
+
return { ok: false, reason: own.length === 0 ? 'not-found' : await whyNot(tx, requestId, orgId) }
|
|
273
|
+
}
|
|
274
|
+
const request = rowToRequest(rows[0]!)
|
|
275
|
+
await settled(flow, tx, 'withdraw', request)
|
|
276
|
+
return { ok: true, request }
|
|
277
|
+
})
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
async list(opts) {
|
|
281
|
+
const { tenantId } = requireTenant()
|
|
282
|
+
const status = opts?.status ?? 'open'
|
|
283
|
+
if (!(JOIN_REQUEST_STATUSES as readonly string[]).includes(status)) {
|
|
284
|
+
throw new ValidationError({ status: [`must be one of ${JOIN_REQUEST_STATUSES.join(', ')}`] }, 'joinRequests list status is not valid.')
|
|
285
|
+
}
|
|
286
|
+
const rows = await pipeRaw().execute(sql`
|
|
287
|
+
SELECT * FROM join_request WHERE tenant_id = ${tenantId} AND status = ${status}
|
|
288
|
+
ORDER BY requested_at DESC, id DESC`)
|
|
289
|
+
return rows.map(rowToRequest)
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async mine(userId) {
|
|
293
|
+
requireText(userId, 'userId')
|
|
294
|
+
const rows = await pipeSystem().execute(sql`
|
|
295
|
+
SELECT * FROM join_request WHERE user_id = ${userId} ORDER BY requested_at DESC, id DESC`)
|
|
296
|
+
return rows.map(rowToRequest)
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async sweep() {
|
|
300
|
+
const flow = requireContext()
|
|
301
|
+
const orgs = await pipeSystem().execute(sql`
|
|
302
|
+
SELECT DISTINCT tenant_id FROM join_request WHERE status = 'open' AND expires_at <= now()`)
|
|
303
|
+
let expired = 0
|
|
304
|
+
for (const org of orgs) {
|
|
305
|
+
const orgId = org['tenant_id'] as string
|
|
306
|
+
expired += await inTenant(flow, orgId, tx => expireOpen(flow, tx, orgId))
|
|
307
|
+
}
|
|
308
|
+
return { expired }
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
return handle
|
|
312
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createJoinRequests } from './handle.js'
|
|
2
|
+
|
|
3
|
+
/** Join requests namespace — a user asks to join an existing org and one of its owners settles it. */
|
|
4
|
+
export const joinRequests = {
|
|
5
|
+
/** Creates a join-request handle: `request`, `approve`, `refuse`, `withdraw`, `list`, `mine` and `sweep`. Needs blueprint `joinRequests: {}`. */
|
|
6
|
+
create: createJoinRequests,
|
|
7
|
+
} as const
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The table behind join requests. `join_request` carries a `.tenant()` key (the
|
|
2
|
+
// org the user asks to join), so generated migrations put it under forced
|
|
3
|
+
// row-level security. One open request per (org, user) is enforced by a
|
|
4
|
+
// partial unique index.
|
|
5
|
+
|
|
6
|
+
import { define } from '../../data/domain/index.js'
|
|
7
|
+
import { field } from '../../data/domain/field.js'
|
|
8
|
+
import { schema } from '../../data/schema/namespace.js'
|
|
9
|
+
import { sql } from '../../data/db/sql.js'
|
|
10
|
+
import { index, uniqueIndex } from '../../data/db/idx/index.js'
|
|
11
|
+
|
|
12
|
+
export const JOIN_REQUEST_STATUSES = ['open', 'approved', 'refused', 'withdrawn', 'expired'] as const
|
|
13
|
+
|
|
14
|
+
export const joinRequest = define('join_request', {
|
|
15
|
+
id: field.uuid().defaultRandom().primaryKey(),
|
|
16
|
+
tenantId: field.uuid().tenant(),
|
|
17
|
+
userId: field.text(),
|
|
18
|
+
status: field.enum(JOIN_REQUEST_STATUSES),
|
|
19
|
+
reason: field.text().nullable(),
|
|
20
|
+
evidence: field.jsonb(schema.check.json()).nullable(),
|
|
21
|
+
requestedAt: field.timestamp().default('now'),
|
|
22
|
+
expiresAt: field.timestamp(),
|
|
23
|
+
settledAt: field.timestamp().nullable(),
|
|
24
|
+
settledBy: field.text().nullable(),
|
|
25
|
+
role: field.text().nullable(),
|
|
26
|
+
note: field.text().nullable(),
|
|
27
|
+
}, {
|
|
28
|
+
indexes: (t) => ({
|
|
29
|
+
oneOpen: uniqueIndex('uq_join_request_open').on(t['tenantId'], t['userId']).where(sql.raw(`status = 'open'`)),
|
|
30
|
+
byUser: index('idx_join_request_user').on(t['userId']),
|
|
31
|
+
byStatus: index('idx_join_request_status').on(t['tenantId'], t['status']),
|
|
32
|
+
}),
|
|
33
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The delivery core shared by alerts and support events: rows in an outbox table are leased with
|
|
2
|
+
// SKIP LOCKED, sent in order per (group, sink), retried with backoff and given up after maxAttempts.
|
|
3
|
+
// Table and column names are the caller's own constants, never user input.
|
|
4
|
+
|
|
5
|
+
import { sql } from '../../data/db/sql.js'
|
|
6
|
+
import { pipeSystem } from '../../data/db/db.js'
|
|
7
|
+
import { getBaseLogger } from '../../core/logging/index.js'
|
|
8
|
+
import { describeError } from '../../core/logging/error-chain.js'
|
|
9
|
+
import { ValidationError } from '../../core/errors/index.js'
|
|
10
|
+
|
|
11
|
+
const SEND_TIMEOUT_MS = 10_000
|
|
12
|
+
const LEASE_MS = 30_000
|
|
13
|
+
const BATCH = 50
|
|
14
|
+
const MAX_BATCHES = 20
|
|
15
|
+
const MAX_BACKOFF_MS = 3_600_000
|
|
16
|
+
|
|
17
|
+
export interface OutboxTotals {
|
|
18
|
+
delivered: number
|
|
19
|
+
retried: number
|
|
20
|
+
failed: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What a sink receives. `kind` is the payload's own kind; `key` the group the row is ordered in (alert key, ticket id). */
|
|
24
|
+
export interface SinkDelivery<P extends { readonly kind: string }> {
|
|
25
|
+
readonly id: string
|
|
26
|
+
readonly kind: P['kind']
|
|
27
|
+
readonly key: string
|
|
28
|
+
readonly sink: string
|
|
29
|
+
readonly payload: P
|
|
30
|
+
/** 1 on the first try. */
|
|
31
|
+
readonly attempt: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A place events are sent. `send` throwing is a failed attempt; so is running past 10 s, which is all the
|
|
36
|
+
* outbox gives a send (the row is leased for 30 s). `delivery.id` is the same on every attempt, so a receiver can dedupe on it.
|
|
37
|
+
*/
|
|
38
|
+
export interface Sink<P extends { readonly kind: string }> {
|
|
39
|
+
readonly name: string
|
|
40
|
+
send(delivery: SinkDelivery<P>): Promise<void>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OutboxSink<D> {
|
|
44
|
+
readonly name: string
|
|
45
|
+
send(delivery: D): Promise<void>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OutboxOptions<D> {
|
|
49
|
+
/** The outbox table: `id`, the group column, `kind`, `sink`, `payload`, `attempts`, `next_attempt_at`, `delivered_at`, `failed_at`, `last_error`, `created_at`. */
|
|
50
|
+
readonly table: string
|
|
51
|
+
/** The column holding what a row is ordered within, together with its sink. */
|
|
52
|
+
readonly groupColumn: string
|
|
53
|
+
readonly database: string | undefined
|
|
54
|
+
readonly sinks: ReadonlyMap<string, OutboxSink<D>>
|
|
55
|
+
readonly toDelivery: (row: Record<string, unknown>, attempt: number) => D
|
|
56
|
+
readonly maxAttempts: number
|
|
57
|
+
readonly backoffMs: number
|
|
58
|
+
/** Names the log line written when a delivery is given up. */
|
|
59
|
+
readonly label: string
|
|
60
|
+
readonly onDelivered: (row: Record<string, unknown>, attempt: number) => Promise<void>
|
|
61
|
+
readonly onGivenUp: (row: Record<string, unknown>, attempt: number, error: string) => Promise<void>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Outbox {
|
|
65
|
+
/** Delivers what is due now, up to 20 batches; rows that fail wait for the next call. */
|
|
66
|
+
drain(): Promise<OutboxTotals>
|
|
67
|
+
/**
|
|
68
|
+
* Puts given-up rows (`failed_at` set, never delivered) back on the queue: `failed_at` cleared, `attempts` 0, due now.
|
|
69
|
+
* `last_error` stays until the next attempt. Ids are unchanged. Returns each requeued row as it now is, plus
|
|
70
|
+
* `prev_attempts`, the attempts it had when given up.
|
|
71
|
+
*/
|
|
72
|
+
requeue(options: { sink?: string; failedSince?: Date }): Promise<Record<string, unknown>[]>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function withTimeout(promise: Promise<void>, ms: number): Promise<void> {
|
|
76
|
+
let timer: NodeJS.Timeout | undefined
|
|
77
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
78
|
+
timer = setTimeout(() => reject(new Error(`sink did not answer within ${String(ms)} ms`)), ms)
|
|
79
|
+
})
|
|
80
|
+
return Promise.race([promise, timeout]).finally(() => { clearTimeout(timer) })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createOutbox<D>(options: OutboxOptions<D>): Outbox {
|
|
84
|
+
const table = sql.raw(`"${options.table}"`)
|
|
85
|
+
const group = sql.raw(`"${options.groupColumn}"`)
|
|
86
|
+
const log = getBaseLogger()
|
|
87
|
+
const db = () => pipeSystem(options.database)
|
|
88
|
+
|
|
89
|
+
// Leases the due rows by pushing next_attempt_at out, so no row lock is held across a send. A row is
|
|
90
|
+
// due only if no older undelivered row of the same group and sink exists: a later event never overtakes an earlier one.
|
|
91
|
+
async function claimDue(dueBefore: string): Promise<Record<string, unknown>[]> {
|
|
92
|
+
return [...await db().execute(sql`
|
|
93
|
+
UPDATE ${table} SET next_attempt_at = clock_timestamp() + ${LEASE_MS} * interval '1 millisecond'
|
|
94
|
+
WHERE id IN (
|
|
95
|
+
SELECT d.id FROM ${table} d
|
|
96
|
+
WHERE d.delivered_at IS NULL AND d.failed_at IS NULL AND d.next_attempt_at <= ${dueBefore}::timestamptz
|
|
97
|
+
AND NOT EXISTS (
|
|
98
|
+
SELECT 1 FROM ${table} o
|
|
99
|
+
WHERE o.${group} = d.${group} AND o.sink = d.sink AND o.delivered_at IS NULL AND o.failed_at IS NULL
|
|
100
|
+
AND (o.created_at, o.id) < (d.created_at, d.id))
|
|
101
|
+
ORDER BY d.next_attempt_at LIMIT ${BATCH} FOR UPDATE OF d SKIP LOCKED)
|
|
102
|
+
RETURNING *`)]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function deliverBatch(batch: Record<string, unknown>[], totals: OutboxTotals): Promise<void> {
|
|
106
|
+
await Promise.all(batch.map(async (row) => {
|
|
107
|
+
const id = row['id'] as string
|
|
108
|
+
const sinkName = row['sink'] as string
|
|
109
|
+
const attempt = Number(row['attempts']) + 1
|
|
110
|
+
const sink = options.sinks.get(sinkName)
|
|
111
|
+
let error: string | null = null
|
|
112
|
+
if (sink === undefined) {
|
|
113
|
+
error = `sink "${sinkName}" is not configured`
|
|
114
|
+
} else {
|
|
115
|
+
try {
|
|
116
|
+
await withTimeout(sink.send(options.toDelivery(row, attempt)), SEND_TIMEOUT_MS)
|
|
117
|
+
} catch (err) {
|
|
118
|
+
error = describeError(err).message
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (error === null) {
|
|
122
|
+
await db().execute(sql`UPDATE ${table} SET delivered_at = clock_timestamp(), attempts = ${attempt}, last_error = NULL WHERE id = ${id}::uuid`)
|
|
123
|
+
await options.onDelivered(row, attempt)
|
|
124
|
+
totals.delivered++
|
|
125
|
+
} else if (sink === undefined || attempt >= options.maxAttempts) {
|
|
126
|
+
await db().execute(sql`UPDATE ${table} SET failed_at = clock_timestamp(), attempts = ${attempt}, last_error = ${error} WHERE id = ${id}::uuid`)
|
|
127
|
+
log.error({ id, sink: sinkName, kind: row['kind'], attempt, error }, options.label)
|
|
128
|
+
await options.onGivenUp(row, attempt, error)
|
|
129
|
+
totals.failed++
|
|
130
|
+
} else {
|
|
131
|
+
const delay = Math.min(options.backoffMs * 2 ** (attempt - 1), MAX_BACKOFF_MS)
|
|
132
|
+
await db().execute(sql`UPDATE ${table} SET attempts = ${attempt}, last_error = ${error},
|
|
133
|
+
next_attempt_at = clock_timestamp() + ${delay} * interval '1 millisecond' WHERE id = ${id}::uuid`)
|
|
134
|
+
totals.retried++
|
|
135
|
+
}
|
|
136
|
+
}))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
async requeue(opts) {
|
|
141
|
+
if (opts.failedSince !== undefined && !(opts.failedSince instanceof Date && !Number.isNaN(opts.failedSince.getTime()))) {
|
|
142
|
+
throw new ValidationError({ failedSince: ['must be a valid Date'] }, 'Outbox failedSince must be a Date.')
|
|
143
|
+
}
|
|
144
|
+
const sinkFilter = opts.sink === undefined ? sql`` : sql`AND sink = ${opts.sink}`
|
|
145
|
+
const sinceFilter = opts.failedSince === undefined ? sql`` : sql`AND failed_at >= ${opts.failedSince.toISOString()}::timestamptz`
|
|
146
|
+
return [...await db().execute(sql`
|
|
147
|
+
UPDATE ${table} t SET failed_at = NULL, attempts = 0, next_attempt_at = clock_timestamp()
|
|
148
|
+
FROM (SELECT id, attempts AS prev_attempts FROM ${table}
|
|
149
|
+
WHERE failed_at IS NOT NULL AND delivered_at IS NULL ${sinkFilter} ${sinceFilter} FOR UPDATE) old
|
|
150
|
+
WHERE t.id = old.id
|
|
151
|
+
RETURNING t.*, old.prev_attempts`)]
|
|
152
|
+
},
|
|
153
|
+
async drain() {
|
|
154
|
+
const totals: OutboxTotals = { delivered: 0, retried: 0, failed: 0 }
|
|
155
|
+
// Rows that fail during this call wait for the next one, however short their backoff.
|
|
156
|
+
const dueBefore = String((await db().execute(sql`SELECT clock_timestamp()::text AS t`))[0]!['t'])
|
|
157
|
+
for (let i = 0; i < MAX_BATCHES; i++) {
|
|
158
|
+
const batch = await claimDue(dueBefore)
|
|
159
|
+
if (batch.length === 0) break
|
|
160
|
+
await deliverBatch(batch, totals)
|
|
161
|
+
}
|
|
162
|
+
return totals
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PipeworkError } from '../../core/errors/base.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by `quota.call` before sending, when the upstream's recorded budget for `key`
|
|
5
|
+
* has fallen to the reserve kept for other classes. Inside a job it is handed back like
|
|
6
|
+
* `jobs.later` until `resetAt`.
|
|
7
|
+
*/
|
|
8
|
+
export class QuotaExhaustedError extends PipeworkError {
|
|
9
|
+
readonly code = 'QUOTA_EXHAUSTED' as const
|
|
10
|
+
readonly httpStatus = 429 as const
|
|
11
|
+
readonly key: string
|
|
12
|
+
readonly class: string
|
|
13
|
+
readonly remaining: number
|
|
14
|
+
readonly reserve: number
|
|
15
|
+
readonly resetAt: Date
|
|
16
|
+
/** Whole seconds from now to `resetAt`; sent as Retry-After and in the 429 body. */
|
|
17
|
+
readonly retryAfterSeconds: number
|
|
18
|
+
|
|
19
|
+
constructor(info: { key: string; class: string; remaining: number; reserve: number; resetAt: Date }) {
|
|
20
|
+
super(
|
|
21
|
+
`[pipework] Quota for "${info.key}" is exhausted for class "${info.class}": ` +
|
|
22
|
+
`${String(info.remaining)} remaining, ${String(info.reserve)} held in reserve; resets at ${info.resetAt.toISOString()}.`,
|
|
23
|
+
)
|
|
24
|
+
this.name = 'QuotaExhaustedError'
|
|
25
|
+
this.key = info.key
|
|
26
|
+
this.class = info.class
|
|
27
|
+
this.remaining = info.remaining
|
|
28
|
+
this.reserve = info.reserve
|
|
29
|
+
this.resetAt = info.resetAt
|
|
30
|
+
this.retryAfterSeconds = Math.max(0, Math.ceil((info.resetAt.getTime() - Date.now()) / 1000))
|
|
31
|
+
}
|
|
32
|
+
}
|