@starci/hfs 4.7.32 → 4.7.33
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 +9 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The HTTP methods the client sends. */
|
|
2
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
|
|
3
|
+
|
|
4
|
+
/** One outbound request. Callers supply the deadline and correlation id explicitly. */
|
|
5
|
+
export interface HttpRequest {
|
|
6
|
+
/** The correlation id carried explicitly from the inbound request or delivery; sent as `x-request-id`. */
|
|
7
|
+
readonly correlationId: string
|
|
8
|
+
/** The HTTP method. */
|
|
9
|
+
readonly method: HttpMethod
|
|
10
|
+
/** The absolute URL. */
|
|
11
|
+
readonly url: string
|
|
12
|
+
/** Extra request headers; correlation headers come from `correlationId` instead. */
|
|
13
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
14
|
+
/** A JSON-serializable body, sent as `application/json`. */
|
|
15
|
+
readonly body?: unknown
|
|
16
|
+
/** Form fields, sent as `application/x-www-form-urlencoded`; use either `body` or `form`, not both. */
|
|
17
|
+
readonly form?: Readonly<Record<string, string>>
|
|
18
|
+
/** Raw bytes sent as they are (the caller names their `content-type`); use instead of `body` and `form`. */
|
|
19
|
+
readonly bytes?: Buffer
|
|
20
|
+
/** How the answer body is read: parsed as JSON (default) or kept as raw bytes (a Buffer, empty when the answer had none). */
|
|
21
|
+
readonly read?: "json" | "bytes"
|
|
22
|
+
/** How long to wait for the answer before the call fails as a timeout. */
|
|
23
|
+
readonly timeoutMs: number
|
|
24
|
+
/** Cancels the call when aborted. */
|
|
25
|
+
readonly signal?: AbortSignal
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The answer to one request: the status and the body parsed as JSON (undefined when the body is empty), or its raw bytes. */
|
|
29
|
+
export interface HttpResponse {
|
|
30
|
+
/** The HTTP status code. */
|
|
31
|
+
readonly status: number
|
|
32
|
+
/** The parsed JSON body (undefined when the answer had none), or a Buffer when the request asked for `read: "bytes"`. */
|
|
33
|
+
readonly body: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The outbound HTTP port: the only way product code calls another service. */
|
|
37
|
+
export interface HttpClient {
|
|
38
|
+
/** Sends `request`; rejects with an HttpError on a network failure, a timeout or an unreadable body. */
|
|
39
|
+
request(request: HttpRequest): Promise<HttpResponse>
|
|
40
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { HTTP_ERROR_KINDS, HttpError, HttpErrorCode } from "./errors/http.error"
|
|
2
|
+
export { HTTP_MESSAGES } from "./messages/http.messages"
|
|
3
|
+
export { callGraphql } from "./graphql-call.mapper"
|
|
4
|
+
export type { GraphqlAnswer } from "./graphql-call.mapper"
|
|
5
|
+
export { HTTP_CLIENT, InjectHttpClient } from "./http.decorators"
|
|
6
|
+
export { HttpModule } from "./http.module"
|
|
7
|
+
export type { HttpClient } from "./http.port"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MessageBundle } from "@modules/platform/i18n"
|
|
2
|
+
|
|
3
|
+
/** Display text of the http capability codes, Vietnamese and English. */
|
|
4
|
+
export const HTTP_MESSAGES: MessageBundle = {
|
|
5
|
+
vi: {
|
|
6
|
+
"errors.HTTP_TIMEOUT": "Dịch vụ phụ thuộc không phản hồi kịp thời.",
|
|
7
|
+
"errors.HTTP_NETWORK": "Không thể kết nối tới dịch vụ phụ thuộc.",
|
|
8
|
+
"errors.HTTP_BODY_UNREADABLE": "Dịch vụ phụ thuộc trả về dữ liệu không đọc được.",
|
|
9
|
+
},
|
|
10
|
+
en: {
|
|
11
|
+
"errors.HTTP_TIMEOUT": "A dependent service did not answer in time.",
|
|
12
|
+
"errors.HTTP_NETWORK": "A dependent service could not be reached.",
|
|
13
|
+
"errors.HTTP_BODY_UNREADABLE": "A dependent service answered with an unreadable body.",
|
|
14
|
+
},
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { injector } from "@modules/platform/composition"
|
|
2
|
+
import type { TypedParameterDecorator } from "@modules/platform/composition"
|
|
3
|
+
import type { Ids } from "./ids.port"
|
|
4
|
+
|
|
5
|
+
/** Token of the Ids port. */
|
|
6
|
+
export const IDS: unique symbol = Symbol("platform.ids")
|
|
7
|
+
|
|
8
|
+
/** Injects the Ids port. Parameter type: Ids. */
|
|
9
|
+
export const InjectIds = (): TypedParameterDecorator<Ids> => injector<Ids>(IDS)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ConfigurableModuleBuilder } from "@nestjs/common"
|
|
2
|
+
import type { IdsOptions } from "./ids.options"
|
|
3
|
+
|
|
4
|
+
/** The configurable-module base of the ids capability; `isGlobal` is decided by the app root. */
|
|
5
|
+
export const { ConfigurableModuleClass, OPTIONS_TYPE } = new ConfigurableModuleBuilder<IdsOptions>()
|
|
6
|
+
.setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
|
|
7
|
+
.build()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common"
|
|
2
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
3
|
+
import { IDS } from "./ids.decorators"
|
|
4
|
+
import { ConfigurableModuleClass, OPTIONS_TYPE } from "./ids.module-definition"
|
|
5
|
+
import { SystemIds } from "./system-ids.service"
|
|
6
|
+
|
|
7
|
+
@Module({})
|
|
8
|
+
/** Provides the Ids port backed by the system random source. */
|
|
9
|
+
export class IdsModule extends ConfigurableModuleClass {
|
|
10
|
+
/** Registers the capability 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: IDS, useClass: SystemIds }],
|
|
16
|
+
exports: [IDS],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { SystemIds } from "./system-ids.service"
|
|
3
|
+
|
|
4
|
+
describe("SystemIds", () => {
|
|
5
|
+
it("answers a UUID, and a different one each time", async () => {
|
|
6
|
+
const moduleRef = await Test.createTestingModule({ providers: [SystemIds] }).compile()
|
|
7
|
+
const ids = moduleRef.get(SystemIds)
|
|
8
|
+
|
|
9
|
+
const first = ids.next()
|
|
10
|
+
const second = ids.next()
|
|
11
|
+
|
|
12
|
+
expect(first).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
|
13
|
+
expect(second).not.toBe(first)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import type { Ids } from "./ids.port"
|
|
3
|
+
|
|
4
|
+
/** The production ids: the one place that reads the ambient random source. It has no dependencies, so the DI container builds it without a decorator. */
|
|
5
|
+
export class SystemIds implements Ids {
|
|
6
|
+
/** A new random UUID. */
|
|
7
|
+
next(): string {
|
|
8
|
+
return randomUUID()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { INestApplication } from "@nestjs/common"
|
|
2
|
+
import { Test } from "@nestjs/testing"
|
|
3
|
+
import { mock } from "@starci/jest-preset"
|
|
4
|
+
import type { NextFunction, Request, RequestHandler, Response } from "express"
|
|
5
|
+
import type { CorrelationIds, CorrelationRunner } from "./correlation.port"
|
|
6
|
+
import { applyCorrelation, correlationMiddleware } from "./correlation-request.policy"
|
|
7
|
+
import { CORRELATION_IDS, CORRELATION_RUNNER } from "./logging.decorators"
|
|
8
|
+
import { runWithCorrelation, stampLogFields } from "./correlation.policy"
|
|
9
|
+
|
|
10
|
+
const SUBJECT = Symbol("request-correlation-policy")
|
|
11
|
+
const build = async () => {
|
|
12
|
+
const moduleRef = await Test.createTestingModule({
|
|
13
|
+
providers: [{ provide: SUBJECT, useValue: { applyCorrelation, correlationMiddleware } }],
|
|
14
|
+
}).compile()
|
|
15
|
+
return moduleRef.get<typeof import("./correlation-request.policy")>(SUBJECT)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("request correlation", () => {
|
|
19
|
+
it("copies the explicit identity to the request and response and scopes downstream work", async () => {
|
|
20
|
+
const subject = await build()
|
|
21
|
+
const ids = mock<CorrelationIds>({ fromHeaders: () => "request-id" })
|
|
22
|
+
const runner = mock<CorrelationRunner>({ run: runWithCorrelation })
|
|
23
|
+
const request = mock<Request>({ headers: { "x-request-id": "request-id" } })
|
|
24
|
+
const setHeader = jest.fn()
|
|
25
|
+
const response = mock<Response>({ setHeader })
|
|
26
|
+
const next: NextFunction = jest.fn(() => {
|
|
27
|
+
expect(stampLogFields({}, "root-id")).toEqual({ correlationId: "request-id" })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
subject.correlationMiddleware(ids, runner)(request, response, next)
|
|
31
|
+
|
|
32
|
+
expect(request).toHaveProperty("correlationId", "request-id")
|
|
33
|
+
expect(setHeader).toHaveBeenCalledWith("x-request-id", "request-id")
|
|
34
|
+
expect(next).toHaveBeenCalledTimes(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("installs the middleware using the registered logging ports", async () => {
|
|
38
|
+
const subject = await build()
|
|
39
|
+
const ids = mock<CorrelationIds>({ fromHeaders: () => "installed-id" })
|
|
40
|
+
const runner = mock<CorrelationRunner>({ run: runWithCorrelation })
|
|
41
|
+
const installed: Array<RequestHandler> = []
|
|
42
|
+
const app: INestApplication = mock<INestApplication>({
|
|
43
|
+
get: jest.fn().mockImplementation((key: unknown) => (key === CORRELATION_IDS ? ids : runner)),
|
|
44
|
+
use: (handler: RequestHandler): INestApplication => {
|
|
45
|
+
installed.push(handler)
|
|
46
|
+
return app
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
subject.applyCorrelation(app)
|
|
50
|
+
|
|
51
|
+
expect(app.get).toHaveBeenCalledWith(CORRELATION_IDS)
|
|
52
|
+
expect(app.get).toHaveBeenCalledWith(CORRELATION_RUNNER)
|
|
53
|
+
|
|
54
|
+
expect(installed).toHaveLength(1)
|
|
55
|
+
const setHeader = jest.fn()
|
|
56
|
+
installed[0]?.(mock<Request>({ headers: {} }), mock<Response>({ setHeader }), jest.fn())
|
|
57
|
+
expect(setHeader).toHaveBeenCalledWith("x-request-id", "installed-id")
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { INestApplication } from "@nestjs/common"
|
|
2
|
+
import type { RequestHandler } from "express"
|
|
3
|
+
import type { CorrelationIds, CorrelationRunner } from "./correlation.port"
|
|
4
|
+
import { CORRELATION_IDS, CORRELATION_RUNNER } from "./logging.decorators"
|
|
5
|
+
|
|
6
|
+
/** Copies the identity onto the request and response, then scopes all downstream request logging. */
|
|
7
|
+
export const correlationMiddleware =
|
|
8
|
+
(ids: CorrelationIds, runner: CorrelationRunner): RequestHandler =>
|
|
9
|
+
(request, response, next) => {
|
|
10
|
+
const correlationId = ids.fromHeaders(request.headers)
|
|
11
|
+
Object.assign(request, { correlationId })
|
|
12
|
+
response.setHeader("x-request-id", correlationId)
|
|
13
|
+
runner.run(correlationId, next)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Installs correlation before request handling; transports read the stamped request field explicitly. */
|
|
17
|
+
export const applyCorrelation = (app: INestApplication): void => {
|
|
18
|
+
app.use(
|
|
19
|
+
correlationMiddleware(app.get<CorrelationIds>(CORRELATION_IDS), app.get<CorrelationRunner>(CORRELATION_RUNNER)),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Headers accepted at the logging boundary; repeated values are not correlation identities. */
|
|
2
|
+
export interface CorrelationHeaders {
|
|
3
|
+
/** An inbound HTTP header value. */
|
|
4
|
+
readonly [name: string]: string | ReadonlyArray<string> | undefined
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The request identity copied explicitly into transport commands and downstream envelopes. */
|
|
8
|
+
export interface CorrelationRequest {
|
|
9
|
+
/** The inbound headers read by logging. */
|
|
10
|
+
readonly headers: CorrelationHeaders
|
|
11
|
+
/** The validated or minted correlation identity. */
|
|
12
|
+
readonly correlationId: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Work entered by infrastructure with an explicit identity; its result and failure pass through unchanged. */
|
|
16
|
+
export type CorrelationWork<T> = () => T
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { fakeIds } from "@starci/jest-preset"
|
|
3
|
+
import { DomainError } from "@modules/platform/errors"
|
|
4
|
+
import { correlationFromHeaders, isCorrelationId, runWithCorrelation, stampLogFields } from "./correlation.policy"
|
|
5
|
+
|
|
6
|
+
type LoggingScopeErrorCode = "LOGGING_SCOPE_FAILED"
|
|
7
|
+
class LoggingScopeError extends DomainError<LoggingScopeErrorCode> {}
|
|
8
|
+
|
|
9
|
+
const SUBJECT = Symbol("correlation-policy")
|
|
10
|
+
const FIRST_ID = "00000000-0000-4000-8000-000000000001"
|
|
11
|
+
const traceId = "4bf92f3577b34da6a3ce929d0e0e4736"
|
|
12
|
+
const spanId = "00f067aa0ba902b7"
|
|
13
|
+
const build = async () => {
|
|
14
|
+
const ids = fakeIds()
|
|
15
|
+
const moduleRef = await Test.createTestingModule({
|
|
16
|
+
providers: [
|
|
17
|
+
{
|
|
18
|
+
provide: SUBJECT,
|
|
19
|
+
useValue: { correlationFromHeaders, isCorrelationId, runWithCorrelation, stampLogFields },
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
}).compile()
|
|
23
|
+
return { subject: moduleRef.get<typeof import("./correlation.policy")>(SUBJECT), ids }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("correlationFromHeaders", () => {
|
|
27
|
+
it("uses the validated trace id before the request id", async () => {
|
|
28
|
+
const { subject, ids } = await build()
|
|
29
|
+
expect(
|
|
30
|
+
subject.correlationFromHeaders(
|
|
31
|
+
{ traceparent: `00-${traceId}-${spanId}-01`, "x-request-id": "request-1" },
|
|
32
|
+
ids,
|
|
33
|
+
),
|
|
34
|
+
).toBe(traceId)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it.each([`01-${traceId}-${spanId}-01`, `01-${traceId}-${spanId}-01-future-field`])(
|
|
38
|
+
"reads the known prefix of a higher traceparent version: %s",
|
|
39
|
+
async (traceparent) => {
|
|
40
|
+
const { subject, ids } = await build()
|
|
41
|
+
expect(subject.correlationFromHeaders({ traceparent, "x-request-id": "request-1" }, ids)).toBe(traceId)
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
"malformed",
|
|
47
|
+
`ff-${traceId}-${spanId}-01`,
|
|
48
|
+
`ff-${traceId}-${spanId}-01-future-field`,
|
|
49
|
+
`00-${traceId}-${spanId}-01-extra`,
|
|
50
|
+
`01-${traceId}-${spanId}-01extra`,
|
|
51
|
+
`01-${traceId}-${spanId}-01_extra`,
|
|
52
|
+
`01-${traceId}-${spanId}-0`,
|
|
53
|
+
`00-${traceId}-${spanId}-01\n`,
|
|
54
|
+
`01-${traceId}-${spanId}-01\n`,
|
|
55
|
+
`00-${"0".repeat(32)}-${spanId}-01`,
|
|
56
|
+
`00-${traceId}-${"0".repeat(16)}-01`,
|
|
57
|
+
`00-${traceId.toUpperCase()}-${spanId}-01`,
|
|
58
|
+
])("falls back to a request id when traceparent is invalid: %s", async (traceparent) => {
|
|
59
|
+
const { subject, ids } = await build()
|
|
60
|
+
expect(subject.correlationFromHeaders({ traceparent, "x-request-id": "request-1" }, ids)).toBe("request-1")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("does not choose among repeated traceparent values", async () => {
|
|
64
|
+
const { subject, ids } = await build()
|
|
65
|
+
expect(
|
|
66
|
+
subject.correlationFromHeaders(
|
|
67
|
+
{ traceparent: [`00-${traceId}-${spanId}-01`], "x-request-id": "request-1" },
|
|
68
|
+
ids,
|
|
69
|
+
),
|
|
70
|
+
).toBe("request-1")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("accepts the bounded safe request id alphabet", async () => {
|
|
74
|
+
const { subject, ids } = await build()
|
|
75
|
+
expect(subject.correlationFromHeaders({ "x-request-id": "A-1._:z" }, ids)).toBe("A-1._:z")
|
|
76
|
+
expect(subject.correlationFromHeaders({ "x-request-id": "a".repeat(128) }, ids)).toHaveLength(128)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it.each([
|
|
80
|
+
{ requestId: undefined },
|
|
81
|
+
{ requestId: "" },
|
|
82
|
+
{ requestId: "a".repeat(129) },
|
|
83
|
+
{ requestId: "line\nbreak" },
|
|
84
|
+
{ requestId: "request-id\n" },
|
|
85
|
+
{ requestId: "request-id\r" },
|
|
86
|
+
{ requestId: "unsafe value" },
|
|
87
|
+
{ requestId: ["a", "b"] },
|
|
88
|
+
])("mints through Ids for absent or unsafe request ids: %p", async ({ requestId }) => {
|
|
89
|
+
const { subject, ids } = await build()
|
|
90
|
+
expect(subject.correlationFromHeaders({ "x-request-id": requestId }, ids)).toBe(FIRST_ID)
|
|
91
|
+
expect(ids.issued).toEqual([FIRST_ID])
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe("logging correlation scopes", () => {
|
|
96
|
+
it("stamps the logger root identity outside a scope", async () => {
|
|
97
|
+
const { subject } = await build()
|
|
98
|
+
expect(subject.stampLogFields(undefined, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("returns the work's result and restores a nested scope", async () => {
|
|
102
|
+
const { subject } = await build()
|
|
103
|
+
const result = subject.runWithCorrelation("outer", () => {
|
|
104
|
+
expect(subject.stampLogFields({ correlationId: "override", owner: "logging" }, FIRST_ID)).toEqual({
|
|
105
|
+
correlationId: "outer",
|
|
106
|
+
owner: "logging",
|
|
107
|
+
})
|
|
108
|
+
subject.runWithCorrelation("inner", () =>
|
|
109
|
+
expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: "inner" }),
|
|
110
|
+
)
|
|
111
|
+
expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: "outer" })
|
|
112
|
+
return "result"
|
|
113
|
+
})
|
|
114
|
+
expect(result).toBe("result")
|
|
115
|
+
expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("restores the surrounding scope after a throw", async () => {
|
|
119
|
+
const { subject } = await build()
|
|
120
|
+
const failure = new LoggingScopeError({ code: "LOGGING_SCOPE_FAILED" })
|
|
121
|
+
expect(() =>
|
|
122
|
+
subject.runWithCorrelation("failed", () => {
|
|
123
|
+
throw failure
|
|
124
|
+
}),
|
|
125
|
+
).toThrow(failure)
|
|
126
|
+
expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("isolates interleaved asynchronous requests", async () => {
|
|
130
|
+
const { subject } = await build()
|
|
131
|
+
let releaseFirst: (() => void) | undefined
|
|
132
|
+
const firstReady = new Promise<void>((resolve) => {
|
|
133
|
+
releaseFirst = resolve
|
|
134
|
+
})
|
|
135
|
+
const first = subject.runWithCorrelation("first", async () => {
|
|
136
|
+
await firstReady
|
|
137
|
+
return subject.stampLogFields({}, FIRST_ID)
|
|
138
|
+
})
|
|
139
|
+
const second = subject.runWithCorrelation("second", async () => {
|
|
140
|
+
await Promise.resolve()
|
|
141
|
+
releaseFirst?.()
|
|
142
|
+
return subject.stampLogFields({}, FIRST_ID)
|
|
143
|
+
})
|
|
144
|
+
await expect(Promise.all([first, second])).resolves.toEqual([
|
|
145
|
+
{ correlationId: "first" },
|
|
146
|
+
{ correlationId: "second" },
|
|
147
|
+
])
|
|
148
|
+
expect(subject.stampLogFields({}, FIRST_ID)).toEqual({ correlationId: FIRST_ID })
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks"
|
|
2
|
+
import type { Ids } from "@modules/platform/ids"
|
|
3
|
+
import type { CorrelationHeaders, CorrelationWork } from "./correlation.contracts"
|
|
4
|
+
import type { LogFields } from "./logging.port"
|
|
5
|
+
|
|
6
|
+
// Logging owns the process's sole ambient correlation store; envelopes carry identities across every other boundary.
|
|
7
|
+
const correlation = new AsyncLocalStorage<string>()
|
|
8
|
+
|
|
9
|
+
/** Enters a logging scope without making ambient reads part of the public logging port. */
|
|
10
|
+
export const runWithCorrelation = <T>(correlationId: string, work: CorrelationWork<T>): T =>
|
|
11
|
+
correlation.run(correlationId, work)
|
|
12
|
+
|
|
13
|
+
/** Stamps the logger's fields last so caller-supplied fields cannot replace the active identity; outside any scope the logger's own root identity stands. */
|
|
14
|
+
export const stampLogFields = (fields: LogFields | undefined, rootId: string): LogFields => ({
|
|
15
|
+
...fields,
|
|
16
|
+
correlationId: correlation.getStore() ?? rootId,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/** Parses the known W3C prefix; newer versions keep opaque additions, while version 00 permits none. */
|
|
20
|
+
const traceIdOf = (traceparent: string): string | undefined => {
|
|
21
|
+
const match = /^(?!ff)([0-9a-f]{2})-((?!0{32})[0-9a-f]{32})-(?!0{16})[0-9a-f]{16}-[0-9a-f]{2}((?![\s\S])|-)/.exec(
|
|
22
|
+
traceparent,
|
|
23
|
+
)
|
|
24
|
+
if (!match) return undefined
|
|
25
|
+
if (match[1] === "00" && match[3] === "-") return undefined
|
|
26
|
+
return match[2]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Reads a valid W3C trace identity or a safe request id; malformed headers cannot enter logs. */
|
|
30
|
+
export const correlationFromHeaders = (headers: CorrelationHeaders, ids: Ids): string => {
|
|
31
|
+
const traceparent = headers["traceparent"]
|
|
32
|
+
if (typeof traceparent === "string") {
|
|
33
|
+
const traceId = traceIdOf(traceparent)
|
|
34
|
+
if (traceId) return traceId
|
|
35
|
+
}
|
|
36
|
+
const requestId = headers["x-request-id"]
|
|
37
|
+
return isCorrelationId(requestId) ? requestId : ids.next()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Accepts only bounded, header-safe identities on serialized async envelopes. */
|
|
41
|
+
export const isCorrelationId = (value: unknown): value is string =>
|
|
42
|
+
typeof value === "string" && value.length > 0 && value.length <= 128 && !/[^A-Za-z0-9._:-]/.test(value)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CorrelationHeaders, CorrelationWork } from "./correlation.contracts"
|
|
2
|
+
|
|
3
|
+
/** Runs infrastructure work under a supplied identity so logging stamps its async descendants. */
|
|
4
|
+
export interface CorrelationRunner {
|
|
5
|
+
/** Restores the surrounding logging scope when the work returns or throws. */
|
|
6
|
+
run<T>(correlationId: string, work: CorrelationWork<T>): T
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Resolves explicit request identities without exposing the logger's ambient state. */
|
|
10
|
+
export interface CorrelationIds {
|
|
11
|
+
/** Uses a valid W3C trace id, then a bounded request id, otherwise asks the Ids port. */
|
|
12
|
+
fromHeaders(headers: CorrelationHeaders): string
|
|
13
|
+
/** Mints an identity for work that starts without an inbound request. */
|
|
14
|
+
create(): string
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Test } from "@nestjs/testing"
|
|
2
|
+
import { fakeIds } from "@starci/jest-preset"
|
|
3
|
+
import { IDS } from "@modules/platform/ids"
|
|
4
|
+
import { CorrelationService, createCorrelationRunner } from "./correlation.service"
|
|
5
|
+
import { stampLogFields } from "./correlation.policy"
|
|
6
|
+
|
|
7
|
+
describe("CorrelationService", () => {
|
|
8
|
+
it("resolves headers, creates identities, and runs work with an explicit identity", async () => {
|
|
9
|
+
const ids = fakeIds()
|
|
10
|
+
const moduleRef = await Test.createTestingModule({
|
|
11
|
+
providers: [CorrelationService, { provide: IDS, useValue: ids }],
|
|
12
|
+
}).compile()
|
|
13
|
+
const service = moduleRef.get<CorrelationService>(CorrelationService)
|
|
14
|
+
|
|
15
|
+
expect(service.fromHeaders({ "x-request-id": "request-1" })).toBe("request-1")
|
|
16
|
+
expect(service.fromHeaders({})).toBe("00000000-0000-4000-8000-000000000001")
|
|
17
|
+
expect(service.create()).toBe("00000000-0000-4000-8000-000000000002")
|
|
18
|
+
expect(service.run("explicit-id", () => stampLogFields({}, "root-id"))).toEqual({ correlationId: "explicit-id" })
|
|
19
|
+
expect(createCorrelationRunner(ids).run("bootstrap-id", () => stampLogFields({}, "root-id"))).toEqual({
|
|
20
|
+
correlationId: "bootstrap-id",
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { InjectIds } from "@modules/platform/ids"
|
|
2
|
+
import type { Ids } from "@modules/platform/ids"
|
|
3
|
+
import type { CorrelationHeaders, CorrelationWork } from "./correlation.contracts"
|
|
4
|
+
import { correlationFromHeaders, runWithCorrelation } from "./correlation.policy"
|
|
5
|
+
import type { CorrelationIds, CorrelationRunner } from "./correlation.port"
|
|
6
|
+
|
|
7
|
+
/** Owns inbound identity resolution and logging scopes; business code receives explicit identities. */
|
|
8
|
+
export class CorrelationService implements CorrelationIds, CorrelationRunner {
|
|
9
|
+
constructor(@InjectIds() private readonly ids: Ids) {}
|
|
10
|
+
|
|
11
|
+
/** Chooses an inbound identity, minting through Ids when both headers are absent or invalid. */
|
|
12
|
+
fromHeaders(headers: CorrelationHeaders): string {
|
|
13
|
+
return correlationFromHeaders(headers, this.ids)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Starts work that has no inbound identity through the deterministic Ids port. */
|
|
17
|
+
create(): string {
|
|
18
|
+
return this.ids.next()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Applies only to logging; callers still pass the identity to outgoing envelopes explicitly. */
|
|
22
|
+
run<T>(correlationId: string, work: CorrelationWork<T>): T {
|
|
23
|
+
return runWithCorrelation(correlationId, work)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Builds an infrastructure logging runner for bootstrap and test worlds without a Nest container. */
|
|
28
|
+
export const createCorrelationRunner = (ids: Ids): CorrelationRunner => new CorrelationService(ids)
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
export { createJsonLogger } from "./json-logger.service"
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
InjectLogger,
|
|
4
|
+
LOGGER,
|
|
5
|
+
CORRELATION_IDS,
|
|
6
|
+
CORRELATION_RUNNER,
|
|
7
|
+
InjectCorrelationIds,
|
|
8
|
+
InjectCorrelationRunner,
|
|
9
|
+
} from "./logging.decorators"
|
|
10
|
+
export { applyCorrelation, correlationMiddleware } from "./correlation-request.policy"
|
|
11
|
+
export type { CorrelationIds, CorrelationRunner } from "./correlation.port"
|
|
12
|
+
export type { CorrelationHeaders, CorrelationRequest } from "./correlation.contracts"
|
|
13
|
+
export { createCorrelationRunner } from "./correlation.service"
|
|
14
|
+
export { isCorrelationId } from "./correlation.policy"
|
|
3
15
|
export { LoggingLogEvent } from "./logging.log-events"
|
|
4
16
|
export { LoggingModule } from "./logging.module"
|
|
5
17
|
export type { Logger } from "./logging.port"
|
|
18
|
+
export { createNestLogger } from "./nest-logger.service"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Writable } from "node:stream"
|
|
2
|
-
import { FakeClock, mock } from "@starci/jest-preset"
|
|
2
|
+
import { FakeClock, fakeIds, mock } from "@starci/jest-preset"
|
|
3
3
|
import { CLOCK } from "@modules/platform/clock"
|
|
4
|
+
import { IDS } from "@modules/platform/ids"
|
|
4
5
|
import { Test } from "@nestjs/testing"
|
|
5
6
|
import { createJsonLogger, JsonLoggerService } from "./json-logger.service"
|
|
6
7
|
import { LOG_ERR, LOG_OUT } from "./logging.decorators"
|
|
7
8
|
import { LoggingLogEvent } from "./logging.log-events"
|
|
9
|
+
import { runWithCorrelation } from "./correlation.policy"
|
|
8
10
|
|
|
9
11
|
type WrittenLine = { stream: "out" | "err"; line: string }
|
|
10
12
|
|
|
@@ -29,9 +31,10 @@ const build = async () => {
|
|
|
29
31
|
{ provide: CLOCK, useValue: clock },
|
|
30
32
|
{ provide: LOG_OUT, useValue: out },
|
|
31
33
|
{ provide: LOG_ERR, useValue: err },
|
|
34
|
+
{ provide: IDS, useValue: fakeIds() },
|
|
32
35
|
],
|
|
33
36
|
}).compile()
|
|
34
|
-
return { logger: moduleRef.get(JsonLoggerService), clock, written }
|
|
37
|
+
return { logger: moduleRef.get<JsonLoggerService>(JsonLoggerService), clock, written }
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
describe("JsonLoggerService", () => {
|
|
@@ -41,7 +44,7 @@ describe("JsonLoggerService", () => {
|
|
|
41
44
|
expect(written).toEqual([
|
|
42
45
|
{
|
|
43
46
|
stream: "out",
|
|
44
|
-
line: `${JSON.stringify({ personId: "p-1", level: "info", event: "cart.opened", time: "2026-05-06T07:08:09.000Z" })}\n`,
|
|
47
|
+
line: `${JSON.stringify({ personId: "p-1", correlationId: "00000000-0000-4000-8000-000000000001", level: "info", event: "cart.opened", time: "2026-05-06T07:08:09.000Z" })}\n`,
|
|
45
48
|
},
|
|
46
49
|
])
|
|
47
50
|
})
|
|
@@ -54,6 +57,7 @@ describe("JsonLoggerService", () => {
|
|
|
54
57
|
level: "warn",
|
|
55
58
|
event: LoggingLogEvent.StartupFailed,
|
|
56
59
|
time: "2026-05-06T07:08:10.000Z",
|
|
60
|
+
correlationId: "00000000-0000-4000-8000-000000000001",
|
|
57
61
|
})
|
|
58
62
|
expect(written[0]?.stream).toBe("err")
|
|
59
63
|
})
|
|
@@ -72,6 +76,7 @@ describe("JsonLoggerService error details", () => {
|
|
|
72
76
|
level: "error",
|
|
73
77
|
event: "db.failed",
|
|
74
78
|
time: "2026-05-06T07:08:09.000Z",
|
|
79
|
+
correlationId: "00000000-0000-4000-8000-000000000001",
|
|
75
80
|
})
|
|
76
81
|
expect(written[0]?.stream).toBe("err")
|
|
77
82
|
})
|
|
@@ -88,6 +93,7 @@ describe("JsonLoggerService error details", () => {
|
|
|
88
93
|
level: "error",
|
|
89
94
|
event: "db.failed",
|
|
90
95
|
time: "2026-05-06T07:08:09.000Z",
|
|
96
|
+
correlationId: "00000000-0000-4000-8000-000000000001",
|
|
91
97
|
})
|
|
92
98
|
})
|
|
93
99
|
|
|
@@ -130,12 +136,39 @@ describe("createJsonLogger", () => {
|
|
|
130
136
|
const lines: Array<string> = []
|
|
131
137
|
jest.spyOn(process.stdout, "write").mockImplementation((chunk) => lines.push(`out:${String(chunk)}`) > 0)
|
|
132
138
|
jest.spyOn(process.stderr, "write").mockImplementation((chunk) => lines.push(`err:${String(chunk)}`) > 0)
|
|
133
|
-
const logger = createJsonLogger(new FakeClock("2026-05-06T07:08:09.000Z"))
|
|
139
|
+
const logger = createJsonLogger(new FakeClock("2026-05-06T07:08:09.000Z"), fakeIds())
|
|
134
140
|
logger.info(LoggingLogEvent.ServerStarted)
|
|
135
141
|
logger.warn(LoggingLogEvent.StartupFailed)
|
|
136
142
|
expect(lines).toEqual([
|
|
137
|
-
`out:${JSON.stringify({ level: "info", event: LoggingLogEvent.ServerStarted, time: "2026-05-06T07:08:09.000Z" })}\n`,
|
|
138
|
-
`err:${JSON.stringify({ level: "warn", event: LoggingLogEvent.StartupFailed, time: "2026-05-06T07:08:09.000Z" })}\n`,
|
|
143
|
+
`out:${JSON.stringify({ correlationId: "00000000-0000-4000-8000-000000000001", level: "info", event: LoggingLogEvent.ServerStarted, time: "2026-05-06T07:08:09.000Z" })}\n`,
|
|
144
|
+
`err:${JSON.stringify({ correlationId: "00000000-0000-4000-8000-000000000001", level: "warn", event: LoggingLogEvent.StartupFailed, time: "2026-05-06T07:08:09.000Z" })}\n`,
|
|
145
|
+
])
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe("JsonLoggerService correlation", () => {
|
|
150
|
+
it("stamps every level with the active identity and refuses caller overrides", async () => {
|
|
151
|
+
const { logger, written } = await build()
|
|
152
|
+
runWithCorrelation("request-id", () => {
|
|
153
|
+
logger.info(LoggingLogEvent.ServerStarted, { correlationId: "override" })
|
|
154
|
+
logger.warn(LoggingLogEvent.StartupFailed)
|
|
155
|
+
logger.error(LoggingLogEvent.StartupFailed, new Error("failed"))
|
|
156
|
+
})
|
|
157
|
+
expect(written).toHaveLength(3)
|
|
158
|
+
expect(written.every(({ line }) => readJsonLine(line).correlationId === "request-id")).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("JsonLogger outside a correlation scope", () => {
|
|
163
|
+
it("stamps every line with one root identity drawn once", async () => {
|
|
164
|
+
const { logger, written } = await build()
|
|
165
|
+
|
|
166
|
+
logger.info("cart.opened")
|
|
167
|
+
logger.warn("cart.slow")
|
|
168
|
+
|
|
169
|
+
expect(written.map((entry) => entry.line)).toEqual([
|
|
170
|
+
`${JSON.stringify({ level: "info", event: "cart.opened", time: "2026-05-06T07:08:09.000Z", correlationId: "00000000-0000-4000-8000-000000000001" })}\n`,
|
|
171
|
+
`${JSON.stringify({ level: "warn", event: "cart.slow", time: "2026-05-06T07:08:09.000Z", correlationId: "00000000-0000-4000-8000-000000000001" })}\n`,
|
|
139
172
|
])
|
|
140
173
|
})
|
|
141
174
|
})
|