@starci/hfs 4.3.0 → 4.4.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 +14 -0
- package/README.md +1 -1
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/contracts.mjs +2 -2
- package/emit/db-types.mjs +12 -3
- package/lint/run.mjs +3 -3
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/report/sonar.mjs +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +2855 -163
- package/runtime/knowledge/hfs/slots.yaml +333 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3091 -43
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +5 -7
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +13769 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +4 -1
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +42 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/architecture.mjs +1 -1
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract.mjs +67 -6
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +9 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +9 -8
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/language.mjs +1 -1
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +2 -4
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +1 -0
- package/scaffold/add-fe-page.mjs +1 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/scaffold/supabase-ports.mjs +1 -1
- package/src/main.mjs +229 -116
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +110 -54
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +3 -0
- package/templates/app/gitignore-lite +3 -0
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/README.md +5 -3
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/index.mjs +54 -41
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { injector } from "@modules/platform/composition"
|
|
2
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
|
+
import type { Inbox } from "./inbox.port"
|
|
4
|
+
|
|
5
|
+
/** Token of the shared, transaction-owned inbox claims. */
|
|
6
|
+
export const INBOX: unique symbol = Symbol("platform.inbox")
|
|
7
|
+
|
|
8
|
+
/** Injects the Inbox port. Parameter type: Inbox. */
|
|
9
|
+
export const InjectInbox = (): TypedParameterDecorator<Inbox> => injector<Inbox>(INBOX)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { InboxOptions } from "./inbox.options"
|
|
3
|
+
|
|
4
|
+
export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<InboxOptions>()
|
|
5
|
+
.setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
|
|
6
|
+
.build()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common"
|
|
2
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
3
|
+
import { INBOX } from "./inbox.decorators"
|
|
4
|
+
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./inbox.module-definition"
|
|
5
|
+
import { PostgresInbox } from "./inbox.service"
|
|
6
|
+
|
|
7
|
+
@Module({})
|
|
8
|
+
/** Provides the lite Inbox port; all claims use the domain service's own transaction. */
|
|
9
|
+
export class InboxModule extends ConfigurableModuleClass {
|
|
10
|
+
/** Registers the shared inbox once per app. */
|
|
11
|
+
static register(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
12
|
+
const base = super.register(options)
|
|
13
|
+
return {
|
|
14
|
+
...base,
|
|
15
|
+
providers: [...(base.providers ?? []), { provide: INBOX, useClass: PostgresInbox }],
|
|
16
|
+
exports: [INBOX],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EntityManager } from "typeorm"
|
|
2
|
+
|
|
3
|
+
/** The signed provider header may omit its delivery identity; the adapter refuses that delivery. */
|
|
4
|
+
export type InboxEventId = string | undefined
|
|
5
|
+
|
|
6
|
+
/** Deduplicates a delivery inside the transaction that records its receipt or applies its effect. */
|
|
7
|
+
export interface Inbox {
|
|
8
|
+
/** Claims once per source and event id; a rolled-back transaction gives the claim back automatically. */
|
|
9
|
+
claim(source: string, eventId: InboxEventId, tx: EntityManager): Promise<boolean>
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
|
|
3
|
+
import { DatabaseErrorCode } from "@modules/platform/database"
|
|
4
|
+
import { PostgresInbox } from "./inbox.service"
|
|
5
|
+
import { CLAIM_EVENT } from "./persistence/inbox.sql"
|
|
6
|
+
|
|
7
|
+
describe("PostgresInbox", () => {
|
|
8
|
+
const build = async () => {
|
|
9
|
+
const module = await Test.createTestingModule({ providers: [PostgresInbox] }).compile()
|
|
10
|
+
return module.get(PostgresInbox)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
it("claims a normalized delivery id through the caller's transaction", async () => {
|
|
14
|
+
const subject = await build()
|
|
15
|
+
const transaction = fakeTransaction(mockEntityManager({ query: [CLAIM_EVENT, [{ event_id: "delivery-1" }]] }))
|
|
16
|
+
|
|
17
|
+
const fresh = await transaction.em.transaction((manager) => subject.claim("webhook:spec", " delivery-1 ", manager))
|
|
18
|
+
|
|
19
|
+
expect(fresh).toBe(true)
|
|
20
|
+
expect(transaction.em.query).toHaveBeenCalledWith(CLAIM_EVENT, ["webhook:spec", "delivery-1"])
|
|
21
|
+
expect(transaction.commits).toBe(1)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("returns false when the source and event id have already been claimed", async () => {
|
|
25
|
+
const subject = await build()
|
|
26
|
+
const manager = mockEntityManager({ query: [CLAIM_EVENT, []] })
|
|
27
|
+
|
|
28
|
+
await expect(subject.claim("webhook:spec", "delivery-1", manager)).resolves.toBe(false)
|
|
29
|
+
|
|
30
|
+
expect(manager.query).toHaveBeenCalledTimes(1)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it.each([["no", undefined], ["a blank", " "]])("refuses %s delivery id before writing a claim", async (_label, eventId) => {
|
|
34
|
+
const subject = await build()
|
|
35
|
+
const manager = mockEntityManager()
|
|
36
|
+
|
|
37
|
+
await expect(subject.claim("webhook:spec", eventId, manager)).rejects.toMatchObject({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
|
|
38
|
+
|
|
39
|
+
expect(manager.query).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common"
|
|
2
|
+
import type { EntityManager } from "typeorm"
|
|
3
|
+
import { DatabaseError, DatabaseErrorCode } from "@modules/platform/database"
|
|
4
|
+
import type { Inbox, InboxEventId } from "./inbox.port"
|
|
5
|
+
import { CLAIM_EVENT } from "./persistence/inbox.sql"
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
/** Claims a delivery with the receipt transaction, so a failure can be retried without losing the delivery. */
|
|
9
|
+
export class PostgresInbox implements Inbox {
|
|
10
|
+
/** The first claim wins; the same source and delivery id becomes a no-op on redelivery. */
|
|
11
|
+
async claim(source: string, eventId: InboxEventId, tx: EntityManager): Promise<boolean> {
|
|
12
|
+
const normalizedEventId = eventId?.trim()
|
|
13
|
+
if (!normalizedEventId) {
|
|
14
|
+
throw new DatabaseError({ code: DatabaseErrorCode.WebhookDeliveryIdRequired })
|
|
15
|
+
}
|
|
16
|
+
const rows: Array<object> = await tx.query(CLAIM_EVENT, [source, normalizedEventId])
|
|
17
|
+
return rows.length > 0
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { sql } from "@modules/platform/database"
|
|
2
|
+
|
|
3
|
+
/** The unique source and delivery id let only one transaction record or process each delivery. */
|
|
4
|
+
export const CLAIM_EVENT = sql`INSERT INTO public.inbox (source, event_id)
|
|
5
|
+
VALUES ($1, $2)
|
|
6
|
+
ON CONFLICT (source, event_id) DO NOTHING
|
|
7
|
+
RETURNING event_id`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- Both the claim and receipt event use the intake transaction; after-commit processing reads the outbox separately.
|
|
2
|
+
create table public.inbox (
|
|
3
|
+
source text not null check (source <> ''),
|
|
4
|
+
event_id text not null check (event_id <> ''),
|
|
5
|
+
claimed_at timestamptz not null default now(),
|
|
6
|
+
primary key (source, event_id)
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
alter table public.inbox enable row level security;
|
|
10
|
+
alter table public.inbox force row level security;
|
|
11
|
+
|
|
12
|
+
create policy "inbox_app_be_select"
|
|
13
|
+
on public.inbox for select to app_be
|
|
14
|
+
using (source <> '');
|
|
15
|
+
|
|
16
|
+
create policy "inbox_app_be_insert"
|
|
17
|
+
on public.inbox for insert to app_be
|
|
18
|
+
with check (source <> '');
|
|
19
|
+
|
|
20
|
+
revoke all on table public.inbox from public, anon, authenticated;
|
|
21
|
+
grant select, insert on table public.inbox to app_be;
|
|
22
|
+
|
|
23
|
+
create table public.event_bus (
|
|
24
|
+
event_id text primary key check (event_id <> ''),
|
|
25
|
+
event_name text not null check (event_name <> ''),
|
|
26
|
+
payload jsonb not null,
|
|
27
|
+
recorded_at timestamptz not null default now(),
|
|
28
|
+
delivered_at timestamptz
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
alter table public.event_bus enable row level security;
|
|
32
|
+
alter table public.event_bus force row level security;
|
|
33
|
+
|
|
34
|
+
create policy "event_bus_app_be_select"
|
|
35
|
+
on public.event_bus for select to app_be
|
|
36
|
+
using (event_name <> '');
|
|
37
|
+
|
|
38
|
+
create policy "event_bus_app_be_insert"
|
|
39
|
+
on public.event_bus for insert to app_be
|
|
40
|
+
with check (event_name <> '');
|
|
41
|
+
|
|
42
|
+
create policy "event_bus_app_be_update"
|
|
43
|
+
on public.event_bus for update to app_be
|
|
44
|
+
using (event_name <> '')
|
|
45
|
+
with check (event_name <> '');
|
|
46
|
+
|
|
47
|
+
revoke all on table public.event_bus from public, anon, authenticated;
|
|
48
|
+
grant select, insert, update on table public.event_bus to app_be;
|
|
@@ -1,42 +1,75 @@
|
|
|
1
|
-
import { fakeTransaction, mockEntityManager } from "@starci/jest-preset"
|
|
1
|
+
import { fakeInbox, fakeTransaction, mockEntityManager, recordingEventBus } from "@starci/jest-preset"
|
|
2
2
|
import { Test } from "@nestjs/testing"
|
|
3
3
|
import { PRIMARY_ENTITY_MANAGER } from "@modules/platform/database"
|
|
4
|
+
import { @@Provider@@DeliveryRecordedEvent } from "@modules/events/@@provider@@"
|
|
5
|
+
import { EVENT_BUS } from "@modules/platform/event-bus"
|
|
6
|
+
import { INBOX } from "@modules/platform/inbox"
|
|
4
7
|
import { INSERT_@@providerUpper@@_INBOX } from "./persistence/@@provider@@-inbox.sql"
|
|
5
8
|
import { @@Provider@@InboxService } from "./@@provider@@-inbox.service"
|
|
6
9
|
|
|
7
10
|
const DELIVERY = { id: "delivery-1" }
|
|
8
11
|
|
|
9
|
-
const build = async (recorded: ReadonlyArray<{ readonly id: string }>) => {
|
|
12
|
+
const build = async (fresh: boolean, recorded: ReadonlyArray<{ readonly id: string }> = [{ id: "inbox-1" }]) => {
|
|
10
13
|
const entityManager = mockEntityManager({ query: [INSERT_@@providerUpper@@_INBOX, recorded] })
|
|
11
14
|
const transaction = fakeTransaction(entityManager)
|
|
15
|
+
const claims = fakeInbox()
|
|
16
|
+
if (!fresh) claims.seen("webhook.@@provider@@", "delivery-1")
|
|
17
|
+
const events = recordingEventBus<@@Provider@@DeliveryRecordedEvent>()
|
|
12
18
|
const moduleRef = await Test.createTestingModule({
|
|
13
19
|
providers: [
|
|
14
20
|
@@Provider@@InboxService,
|
|
15
21
|
{ provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager },
|
|
22
|
+
{ provide: INBOX, useValue: claims },
|
|
23
|
+
{ provide: EVENT_BUS, useValue: events },
|
|
16
24
|
],
|
|
17
25
|
}).compile()
|
|
18
|
-
return { inbox: moduleRef.get(@@Provider@@InboxService), entityManager, transaction }
|
|
26
|
+
return { inbox: moduleRef.get(@@Provider@@InboxService), claims, events, entityManager, transaction }
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
describe("@@Provider@@InboxService", () => {
|
|
22
30
|
describe("record@@Provider@@Delivery", () => {
|
|
23
|
-
it("records
|
|
24
|
-
const { inbox, entityManager, transaction } = await build(
|
|
31
|
+
it("claims, records and publishes a typed event in the same transaction", async () => {
|
|
32
|
+
const { inbox, claims, events, entityManager, transaction } = await build(true)
|
|
25
33
|
|
|
26
|
-
await inbox.record@@Provider@@Delivery("delivery-1", DELIVERY)
|
|
34
|
+
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })
|
|
27
35
|
|
|
28
36
|
expect(entityManager.query).toHaveBeenCalledTimes(1)
|
|
29
37
|
expect(entityManager.query).toHaveBeenCalledWith(INSERT_@@providerUpper@@_INBOX, ["@@provider@@", "delivery-1", JSON.stringify(DELIVERY)])
|
|
38
|
+
expect(claims.claims).toEqual([{ source: "webhook.@@provider@@", eventId: "delivery-1" }])
|
|
39
|
+
expect(events.events).toEqual([@@Provider@@DeliveryRecordedEvent.create({ receiptId: "inbox-1" })])
|
|
40
|
+
expect(events.allInTransaction).toBe(true)
|
|
41
|
+
expect(events.entries[0]?.tx).not.toBe(entityManager)
|
|
30
42
|
expect(transaction.commits).toBe(1)
|
|
31
43
|
})
|
|
32
44
|
|
|
33
45
|
it("acknowledges a duplicate @@provider@@ delivery without further work", async () => {
|
|
34
|
-
const { inbox, entityManager, transaction } = await build(
|
|
46
|
+
const { inbox, events, entityManager, transaction } = await build(false)
|
|
35
47
|
|
|
36
|
-
await inbox.record@@Provider@@Delivery("delivery-1", DELIVERY)
|
|
48
|
+
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })
|
|
37
49
|
|
|
38
|
-
expect(entityManager.query).
|
|
50
|
+
expect(entityManager.query).not.toHaveBeenCalled()
|
|
51
|
+
expect(events.events).toEqual([])
|
|
52
|
+
expect(transaction.commits).toBe(1)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("does not publish for an already recorded receipt", async () => {
|
|
56
|
+
const { inbox, events, transaction } = await build(true, [])
|
|
57
|
+
|
|
58
|
+
await inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })
|
|
59
|
+
|
|
60
|
+
expect(events.events).toEqual([])
|
|
39
61
|
expect(transaction.commits).toBe(1)
|
|
40
62
|
})
|
|
63
|
+
|
|
64
|
+
it("rolls back the receipt when publishing fails", async () => {
|
|
65
|
+
const { inbox, events, transaction } = await build(true)
|
|
66
|
+
const failure = new Error("outbox unavailable")
|
|
67
|
+
events.failNext("publish", failure)
|
|
68
|
+
|
|
69
|
+
await expect(inbox.record@@Provider@@Delivery({ eventId: "delivery-1", delivery: DELIVERY })).rejects.toBe(failure)
|
|
70
|
+
|
|
71
|
+
expect(transaction.rollbacks).toBe(1)
|
|
72
|
+
expect(transaction.commits).toBe(0)
|
|
73
|
+
})
|
|
41
74
|
})
|
|
42
75
|
})
|
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
import { Injectable } from "@nestjs/common"
|
|
2
2
|
import type { EntityManager } from "typeorm"
|
|
3
3
|
import { InjectPrimaryEntityManager, recordWebhookDelivery } from "@modules/platform/database"
|
|
4
|
+
import { @@Provider@@DeliveryRecordedEvent } from "@modules/events/@@provider@@"
|
|
5
|
+
import { InjectEventBus } from "@modules/platform/event-bus"
|
|
6
|
+
import type { EventBus } from "@modules/platform/event-bus"
|
|
7
|
+
import { InjectInbox } from "@modules/platform/inbox"
|
|
8
|
+
import type { Inbox } from "@modules/platform/inbox"
|
|
4
9
|
import { INSERT_@@providerUpper@@_INBOX } from "./persistence/@@provider@@-inbox.sql"
|
|
5
10
|
|
|
6
11
|
interface @@Provider@@Delivery {
|
|
7
12
|
readonly id: string
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
interface @@Provider@@DeliveryRequest {
|
|
16
|
+
readonly eventId: string | undefined
|
|
17
|
+
readonly delivery: @@Provider@@Delivery
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
@Injectable()
|
|
13
21
|
/** Records each @@provider@@ delivery once. Pending rows are consumed by a separate reactor or job after commit. */
|
|
14
22
|
export class @@Provider@@InboxService {
|
|
15
23
|
constructor(
|
|
16
24
|
@InjectPrimaryEntityManager() private readonly entityManager: EntityManager,
|
|
25
|
+
@InjectInbox() private readonly inbox: Inbox,
|
|
26
|
+
@InjectEventBus() private readonly eventBus: EventBus,
|
|
17
27
|
) {}
|
|
18
28
|
|
|
19
29
|
/** A duplicate is acknowledged without creating another pending row; this intake never dispatches a command. */
|
|
20
|
-
record@@Provider@@Delivery(
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
record@@Provider@@Delivery(request: @@Provider@@DeliveryRequest): Promise<void> {
|
|
31
|
+
return this.entityManager.transaction(async (manager) => {
|
|
32
|
+
if (!(await this.inbox.claim("webhook.@@provider@@", request.eventId, manager))) return
|
|
33
|
+
const receiptId = await recordWebhookDelivery({
|
|
34
|
+
manager,
|
|
35
|
+
insert: INSERT_@@providerUpper@@_INBOX,
|
|
36
|
+
provider: "@@provider@@",
|
|
37
|
+
deliveryId: request.eventId,
|
|
38
|
+
payload: request.delivery,
|
|
39
|
+
})
|
|
40
|
+
if (receiptId === undefined) return
|
|
41
|
+
await this.eventBus.publish(@@Provider@@DeliveryRecordedEvent.create({ receiptId }), manager)
|
|
27
42
|
})
|
|
28
43
|
}
|
|
29
44
|
}
|
|
@@ -58,7 +58,7 @@ describe("@@Provider@@Webhook", () => {
|
|
|
58
58
|
await door.receive(REQUEST, sign(RAW_BODY, SIGNED_AT), SIGNED_AT, DELIVERY_ID, DELIVERY)
|
|
59
59
|
|
|
60
60
|
expect(deliveries.record@@Provider@@Delivery).toHaveBeenCalledTimes(1)
|
|
61
|
-
expect(deliveries.record@@Provider@@Delivery).toHaveBeenCalledWith(DELIVERY_ID, DELIVERY)
|
|
61
|
+
expect(deliveries.record@@Provider@@Delivery).toHaveBeenCalledWith({ eventId: DELIVERY_ID, delivery: DELIVERY })
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it("never reaches the inbox when the signature is not the provider's", async () => {
|
|
@@ -10,7 +10,7 @@ import { @@Event@@Request } from "./dto/@@event@@.request"
|
|
|
10
10
|
@Controller("webhooks/@@provider@@")
|
|
11
11
|
/**
|
|
12
12
|
* POST /webhooks/@@provider@@, the signed intake door of the @@provider@@ notifier. The signature and replay window are proven
|
|
13
|
-
* first; the verified delivery goes to one domain intake that records a
|
|
13
|
+
* first; the verified delivery goes to one domain intake that records a receipt and typed outbox event in its transaction. A separate
|
|
14
14
|
* reactor or job consumes the committed row and dispatches the command.
|
|
15
15
|
*/
|
|
16
16
|
export class @@Provider@@Webhook {
|
|
@@ -32,6 +32,6 @@ export class @@Provider@@Webhook {
|
|
|
32
32
|
@Body() delivery: @@Event@@Request,
|
|
33
33
|
): Promise<void> {
|
|
34
34
|
this.signature.verify({ provider: "@@provider@@", rawBody: request.rawBody, signature, timestamp })
|
|
35
|
-
await this.deliveries.record@@Provider@@Delivery(deliveryId, delivery)
|
|
35
|
+
await this.deliveries.record@@Provider@@Delivery({ eventId: deliveryId, delivery })
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { @@Feature@@ApiOptions } from "./@@feature@@.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the @@feature@@ module. */
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
new ConfigurableModuleBuilder<@@Feature@@ApiOptions>().setClassMethodName("register").build()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common"
|
|
2
|
+
import { ConfigurableModuleClass } from "./@@feature@@.module-definition"
|
|
2
3
|
import { @@Action@@Handler } from "./application/@@action@@.handler"
|
|
3
4
|
|
|
4
5
|
@Module({ providers: [@@Action@@Handler] })
|
|
5
6
|
/** The @@feature@@ feature: the handlers of its application. */
|
|
6
|
-
export class @@Feature@@ApiModule {}
|
|
7
|
+
export class @@Feature@@ApiModule extends ConfigurableModuleClass {}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { @@Feature@@GraphqlOptions } from "./@@feature@@-graphql.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the @@feature@@-graphql module. */
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
new ConfigurableModuleBuilder<@@Feature@@GraphqlOptions>().setClassMethodName("register").build()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common"
|
|
2
2
|
import { @@Feature@@ApiModule } from "../../@@feature@@.module"
|
|
3
3
|
import { @@Action@@Resolver } from "./@@action@@.resolver"
|
|
4
|
+
import { ConfigurableModuleClass } from "./@@feature@@-graphql.module-definition"
|
|
4
5
|
|
|
5
|
-
@Module({ imports: [@@Feature@@ApiModule], providers: [@@Action@@Resolver] })
|
|
6
|
+
@Module({ imports: [@@Feature@@ApiModule.register({})], providers: [@@Action@@Resolver] })
|
|
6
7
|
/** The GraphQL transport of the @@feature@@ feature. */
|
|
7
|
-
export class @@Feature@@GraphqlModule {}
|
|
8
|
+
export class @@Feature@@GraphqlModule extends ConfigurableModuleClass {}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { @@Group@@Options } from "./@@group@@.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the @@group@@ module. */
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
new ConfigurableModuleBuilder<@@Group@@Options>().setClassMethodName("register").build()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common"
|
|
2
2
|
import { @@Group@@Cli } from "./@@group@@.cli"
|
|
3
|
+
import { ConfigurableModuleClass } from "./@@group@@.module-definition"
|
|
3
4
|
import { RunCli } from "./subs/run.cli"
|
|
4
5
|
|
|
5
6
|
@Module({ providers: [@@Group@@Cli, RunCli] })
|
|
6
7
|
/** The @@group@@ group: its command and its sub-commands; the work is the runner service the sub-command delegates to. */
|
|
7
|
-
export class @@Group@@Module {}
|
|
8
|
+
export class @@Group@@Module extends ConfigurableModuleClass {}
|
|
@@ -3,8 +3,6 @@ import type { ErrorKind } from "@modules/platform/errors"
|
|
|
3
3
|
|
|
4
4
|
/** Codes of the event-bus capability. */
|
|
5
5
|
export enum EventBusErrorCode {
|
|
6
|
-
/** A delivered envelope does not have the shape its consumer's event class reads. */
|
|
7
|
-
EnvelopeInvalid = "EVENT_BUS_ENVELOPE_INVALID",
|
|
8
6
|
/** The broker did not answer a call within its deadline, or refused it. */
|
|
9
7
|
BrokerUnavailable = "EVENT_BUS_BROKER_UNAVAILABLE",
|
|
10
8
|
/** A dead letter id does not name a message the dead-letter topic holds. */
|
|
@@ -13,7 +11,6 @@ export enum EventBusErrorCode {
|
|
|
13
11
|
|
|
14
12
|
/** How each event-bus code travels. */
|
|
15
13
|
export const EVENT_BUS_ERROR_KINDS: Record<EventBusErrorCode, ErrorKind> = {
|
|
16
|
-
[EventBusErrorCode.EnvelopeInvalid]: "internal",
|
|
17
14
|
[EventBusErrorCode.BrokerUnavailable]: "unavailable",
|
|
18
15
|
[EventBusErrorCode.DeadLetterUnknown]: "not-found",
|
|
19
16
|
}
|
|
@@ -3,12 +3,10 @@ import type { MessageBundle } from "@modules/platform/i18n"
|
|
|
3
3
|
/** Display text of the event-bus codes, Vietnamese and English. */
|
|
4
4
|
export const EVENT_BUS_MESSAGES: MessageBundle = {
|
|
5
5
|
vi: {
|
|
6
|
-
"errors.EVENT_BUS_ENVELOPE_INVALID": "Sự kiện {{event}} nhận được có nội dung không đúng dạng.",
|
|
7
6
|
"errors.EVENT_BUS_BROKER_UNAVAILABLE": "Kênh sự kiện {{operation}} không phản hồi kịp.",
|
|
8
7
|
"errors.EVENT_BUS_DEAD_LETTER_UNKNOWN": "Không có sự kiện lỗi {{id}} để đưa lại.",
|
|
9
8
|
},
|
|
10
9
|
en: {
|
|
11
|
-
"errors.EVENT_BUS_ENVELOPE_INVALID": "The received {{event}} event has a payload of the wrong shape.",
|
|
12
10
|
"errors.EVENT_BUS_BROKER_UNAVAILABLE": "The event broker did not answer {{operation}} in time.",
|
|
13
11
|
"errors.EVENT_BUS_DEAD_LETTER_UNKNOWN": "There is no dead letter {{id}} to requeue.",
|
|
14
12
|
},
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { @@Job@@Options } from "./@@job@@.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the @@job@@ module. */
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
new ConfigurableModuleBuilder<@@Job@@Options>().setClassMethodName("register").build()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Module } from "@nestjs/common"
|
|
2
|
+
import { ConfigurableModuleClass } from "./@@job@@.module-definition"
|
|
2
3
|
import { @@Step@@Step } from "./steps/@@step@@.step"
|
|
3
4
|
|
|
4
5
|
@Module({ providers: [@@Step@@Step], exports: [@@Step@@Step] })
|
|
5
6
|
/** The application of the @@job@@ job: its steps; the queue transport module imports it. */
|
|
6
|
-
export class @@Job@@Module {}
|
|
7
|
+
export class @@Job@@Module extends ConfigurableModuleClass {}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { @@Job@@QueueOptions } from "./@@job@@-queue.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the @@job@@-queue module. */
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
new ConfigurableModuleBuilder<@@Job@@QueueOptions>().setClassMethodName("register").build()
|
|
@@ -3,15 +3,18 @@ import type { OnModuleInit } from "@nestjs/common"
|
|
|
3
3
|
import { InjectJobProcessorRegistry } from "@modules/platform/jobs"
|
|
4
4
|
import type { JobProcessorRegistry } from "@modules/platform/jobs"
|
|
5
5
|
import { @@Job@@Module } from "../../@@job@@.module"
|
|
6
|
+
import { ConfigurableModuleClass } from "./@@job@@-queue.module-definition"
|
|
6
7
|
import { @@Job@@Processor } from "./@@job@@.processor"
|
|
7
8
|
|
|
8
|
-
@Module({ imports: [@@Job@@Module], providers: [@@Job@@Processor] })
|
|
9
|
+
@Module({ imports: [@@Job@@Module.register({})], providers: [@@Job@@Processor] })
|
|
9
10
|
/** The queue transport of the @@job@@ job: its processor, which a worker or api app composes and which subscribes to its queue when the module starts. */
|
|
10
|
-
export class @@Job@@QueueModule implements OnModuleInit {
|
|
11
|
+
export class @@Job@@QueueModule extends ConfigurableModuleClass implements OnModuleInit {
|
|
11
12
|
constructor(
|
|
12
13
|
@InjectJobProcessorRegistry() private readonly registry: JobProcessorRegistry,
|
|
13
14
|
private readonly @@jobCamel@@Processor: @@Job@@Processor,
|
|
14
|
-
) {
|
|
15
|
+
) {
|
|
16
|
+
super()
|
|
17
|
+
}
|
|
15
18
|
|
|
16
19
|
/** Hands the processor to the registry. */
|
|
17
20
|
onModuleInit(): void {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
1
2
|
import type { ClaimedJob } from "./jobs.contracts"
|
|
2
3
|
import { FencedProcessor } from "./fenced.processor"
|
|
3
4
|
|
|
@@ -21,7 +22,8 @@ class MailProcessor extends FencedProcessor {
|
|
|
21
22
|
|
|
22
23
|
describe("FencedProcessor", () => {
|
|
23
24
|
it("lets a processor declare its queue and process a claimed job", async () => {
|
|
24
|
-
const
|
|
25
|
+
const moduleRef = await Test.createTestingModule({ providers: [MailProcessor] }).compile()
|
|
26
|
+
const processor = moduleRef.get(MailProcessor)
|
|
25
27
|
|
|
26
28
|
await processor.process(claimedJob)
|
|
27
29
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
1
2
|
import { fakeTransaction, mock, mockEntityManager } from "@starci/jest-preset"
|
|
2
3
|
import type { MockEntityManager } from "@starci/jest-preset"
|
|
3
4
|
import type { EntityManager } from "typeorm"
|
|
5
|
+
import { injector } from "@modules/platform/composition"
|
|
6
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
7
|
+
import { InjectLogger, LOGGER } from "@modules/platform/logging"
|
|
4
8
|
import type { Logger } from "@modules/platform/logging"
|
|
5
9
|
import { OutboxLogEvent } from "./outbox.log-events"
|
|
6
10
|
import { OutboxRelayPolicy } from "./outbox-relay.policy"
|
|
@@ -9,25 +13,37 @@ interface Row {
|
|
|
9
13
|
readonly id: string
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
const OUTBOX_MANAGERS: unique symbol = Symbol("spec.outbox.managers")
|
|
17
|
+
|
|
18
|
+
/** Injects the entity managers of the test relay. Parameter type: ReadonlyArray<EntityManager>. */
|
|
19
|
+
const InjectOutboxManagers = (): TypedParameterDecorator<ReadonlyArray<EntityManager>> =>
|
|
20
|
+
injector<ReadonlyArray<EntityManager>>(OUTBOX_MANAGERS)
|
|
21
|
+
|
|
12
22
|
class ProbeRelay extends OutboxRelayPolicy<Row> {
|
|
13
23
|
readonly select = jest.fn<Promise<Array<Row>>, [EntityManager]>()
|
|
14
24
|
readonly deliver = jest.fn<Promise<void>, [ReadonlyArray<Row>]>()
|
|
15
25
|
readonly mark = jest.fn<Promise<void>, [EntityManager, ReadonlyArray<Row>]>()
|
|
16
26
|
readonly idle = jest.fn<Promise<void>, []>()
|
|
27
|
+
protected readonly outbox = "probe"
|
|
17
28
|
|
|
18
29
|
constructor(
|
|
19
|
-
protected readonly managers: ReadonlyArray<EntityManager>,
|
|
20
|
-
protected readonly logger: Logger,
|
|
21
|
-
protected readonly outbox = "probe",
|
|
30
|
+
@InjectOutboxManagers() protected readonly managers: ReadonlyArray<EntityManager>,
|
|
31
|
+
@InjectLogger() protected readonly logger: Logger,
|
|
22
32
|
) {
|
|
23
33
|
super()
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
const build = (...managers: Array<MockEntityManager>) => {
|
|
37
|
+
const build = async (...managers: Array<MockEntityManager>) => {
|
|
28
38
|
const logger = mock<Logger>()
|
|
29
|
-
const
|
|
30
|
-
|
|
39
|
+
const moduleRef = await Test.createTestingModule({
|
|
40
|
+
providers: [
|
|
41
|
+
ProbeRelay,
|
|
42
|
+
{ provide: OUTBOX_MANAGERS, useValue: [...managers] },
|
|
43
|
+
{ provide: LOGGER, useValue: logger },
|
|
44
|
+
],
|
|
45
|
+
}).compile()
|
|
46
|
+
return { relay: moduleRef.get(ProbeRelay), logger }
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
describe("OutboxRelayPolicy", () => {
|
|
@@ -35,7 +51,7 @@ describe("OutboxRelayPolicy", () => {
|
|
|
35
51
|
it("delivers and marks every connection's waiting rows in its own transaction", async () => {
|
|
36
52
|
const first = fakeTransaction(mockEntityManager())
|
|
37
53
|
const second = fakeTransaction(mockEntityManager())
|
|
38
|
-
const { relay } = build(first.em, second.em)
|
|
54
|
+
const { relay } = await build(first.em, second.em)
|
|
39
55
|
const rows = [{ id: "row-1" }, { id: "row-2" }]
|
|
40
56
|
relay.select.mockResolvedValueOnce(rows).mockResolvedValueOnce([])
|
|
41
57
|
relay.deliver.mockResolvedValue(undefined)
|
|
@@ -52,7 +68,7 @@ describe("OutboxRelayPolicy", () => {
|
|
|
52
68
|
|
|
53
69
|
it("leaves the rows unmarked when delivery fails", async () => {
|
|
54
70
|
const transaction = fakeTransaction(mockEntityManager())
|
|
55
|
-
const { relay } = build(transaction.em)
|
|
71
|
+
const { relay } = await build(transaction.em)
|
|
56
72
|
const failure = new Error("transport down")
|
|
57
73
|
relay.select.mockResolvedValue([{ id: "row-1" }])
|
|
58
74
|
relay.deliver.mockRejectedValue(failure)
|
|
@@ -66,7 +82,7 @@ describe("OutboxRelayPolicy", () => {
|
|
|
66
82
|
|
|
67
83
|
describe("the loop", () => {
|
|
68
84
|
it("repeats immediately after work, idles after an empty or failed pass, logs the failure and stops on shutdown", async () => {
|
|
69
|
-
const { relay, logger } = build()
|
|
85
|
+
const { relay, logger } = await build()
|
|
70
86
|
const failure = new Error("outbox unreadable")
|
|
71
87
|
jest.spyOn(relay, "relay").mockResolvedValueOnce(1).mockResolvedValueOnce(0).mockRejectedValueOnce(failure)
|
|
72
88
|
let waits = 0
|
|
@@ -2,7 +2,7 @@ import type { EnvSource } from "@modules/platform/config"
|
|
|
2
2
|
import type { QueueOptions } from "./queue.options"
|
|
3
3
|
|
|
4
4
|
/** What the app's `main.ts` reads for the queues; the connections and the schedulers are the app root's choice. */
|
|
5
|
-
export type QueueConfig = Omit<QueueOptions, "connections" | "schedulers">
|
|
5
|
+
export type QueueConfig = Omit<QueueOptions, "connections" | "schedulers" | "outboxOnly">
|
|
6
6
|
|
|
7
7
|
/** Reads the queue options: the Redis address is required, the rest are tunables with literal defaults. */
|
|
8
8
|
export const parseQueueConfig = (env: EnvSource): QueueConfig => ({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { parseQueueConfig } from "./queue.config"
|
|
2
2
|
export type { QueueConfig } from "./queue.config"
|
|
3
3
|
export type { QueueDelivery, QueueSchedulerDefinition } from "./queue.contracts"
|
|
4
|
-
export { InjectQueueOutbox, InjectQueueWorkerRegistry, QUEUE_WORKER_REGISTRY } from "./queue.decorators"
|
|
4
|
+
export { InjectQueueOutbox, InjectQueueWorkerRegistry, QUEUE_OUTBOX, QUEUE_WORKER_REGISTRY } from "./queue.decorators"
|
|
5
5
|
export { QueueModule } from "./queue.module"
|
|
6
6
|
export type { QueueOutbox, QueueWorkerRegistry } from "./queue.port"
|
|
7
7
|
export { queueEntities, queueMigrations } from "./persistence/connection"
|