@starci/hfs 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3401 -152
- package/runtime/knowledge/hfs/slots.yaml +344 -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 +3113 -43
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -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 +3 -0
- 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 +41 -0
- 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/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/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 +3 -3
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- 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/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 +67 -7
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -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/contract.mjs +66 -5
- 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 +1 -1
- 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 +1 -0
- 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/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/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- 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 +122 -0
- package/scaffold/add-fe-page.mjs +269 -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/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/index.mjs +108 -52
- 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 +53 -40
package/scaffold/add-table.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { ScaffoldError, pascalOf } from "./service.mjs";
|
|
|
10
10
|
import { filesBelow, registerLiteDomainService, registerLiteExports, registrationFiles } from "./lite-exports.mjs";
|
|
11
11
|
import { byCodeUnit } from "../report/order.mjs";
|
|
12
12
|
import { coreFeApps } from "./app.mjs";
|
|
13
|
+
import { addModuleImport, planComposeGroup } from "./add-compose.mjs";
|
|
14
|
+
import { withSqlIdentifierError } from "./add-database-lite.mjs";
|
|
13
15
|
|
|
14
16
|
const NAME = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
15
17
|
const MIGRATION = /^(\d{14})_[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.sql$/;
|
|
@@ -163,29 +165,6 @@ function removeEmptyParents(file, stop) {
|
|
|
163
165
|
/** The app-root relative path of a file under the app (posix). */
|
|
164
166
|
const at = (root, relative) => path.join(root, ...relative.split("/"));
|
|
165
167
|
|
|
166
|
-
/**
|
|
167
|
-
* Registers `entry` in the `imports` array of a Nest module file (one import line after the last import, one array entry right
|
|
168
|
-
* after `imports: [`), unless `symbol` is already imported. Returns the new text.
|
|
169
|
-
*/
|
|
170
|
-
function wireImport(text, relative, { importLine, symbol, entry, kinds }) {
|
|
171
|
-
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
172
|
-
if (lines.some((line) => line.startsWith("import ") && new RegExp(`\\b${symbol}\\b`).test(line))) return text;
|
|
173
|
-
const lastImport = lines.reduce((last, line, index) => (line.startsWith("import ") ? index : last), -1);
|
|
174
|
-
if (lastImport < 0 || lines.findIndex((line) => /^\s*imports: \[$/.test(line)) < 0)
|
|
175
|
-
throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the imports array in ${relative}`);
|
|
176
|
-
lines.splice(lastImport + 1, 0, importLine);
|
|
177
|
-
const imports = lines.findIndex((line) => /^\s*imports: \[$/.test(line));
|
|
178
|
-
const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? "";
|
|
179
|
-
lines.splice(imports + 1, 0, `${indent} ${entry},`);
|
|
180
|
-
if (kinds !== undefined) {
|
|
181
|
-
const kindsAt = lines.findIndex((line) => /^\s*kinds: \[$/.test(line));
|
|
182
|
-
if (kindsAt < 0) throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the error kinds array in ${relative}`);
|
|
183
|
-
const kindsIndent = /^(\s*)/.exec(lines[kindsAt])?.[1] ?? "";
|
|
184
|
-
lines.splice(kindsAt + 1, 0, `${kindsIndent} ${kinds},`);
|
|
185
|
-
}
|
|
186
|
-
return `${lines.join("\n").replace(/\n+$/, "")}\n`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
168
|
/** A line that opens the error kinds array of the errors registration. */
|
|
190
169
|
const KINDS_LINE = /^\s*kinds: \[$/;
|
|
191
170
|
|
|
@@ -206,45 +185,33 @@ function kindsHomeOf(root, appRelative) {
|
|
|
206
185
|
/** A composition group after `kinds` is listed first in its error kinds array and imported from `specifier`. Returns the new text. */
|
|
207
186
|
function wireKinds(text, relative, { kinds, specifier }) {
|
|
208
187
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
209
|
-
if (lines.some((line) => line.startsWith("import ") && new RegExp(
|
|
188
|
+
if (lines.some((line) => line.startsWith("import ") && new RegExp(String.raw`\b${kinds}\b`).test(line))) return text;
|
|
210
189
|
const lastImport = lines.reduce((last, line, index) => (line.startsWith("import ") ? index : last), -1);
|
|
211
190
|
const kindsAt = lines.findIndex((line) => KINDS_LINE.test(line));
|
|
212
191
|
if (lastImport < 0 || kindsAt < 0) throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `add table could not find the error kinds array in ${relative}`);
|
|
213
192
|
const kindsIndent = /^(\s*)/.exec(lines[kindsAt])?.[1] ?? "";
|
|
214
193
|
lines.splice(kindsAt + 1, 0, `${kindsIndent} ${kinds},`);
|
|
215
194
|
lines.splice(lastImport + 1, 0, `import { ${kinds} } from "${specifier}"`);
|
|
216
|
-
return `${lines.join("\n").
|
|
195
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
217
196
|
}
|
|
218
197
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
importLine:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
198
|
+
/** Plans the domain and API area groups of the connection owner, keeping registrations out of app.module.ts. */
|
|
199
|
+
function tableComposition({ root, repo, owner, feature, fe, ownerRelative }) {
|
|
200
|
+
const domain = { importLine: `import { ${pascalOf(feature)}Module } from "@modules/domain/${feature}"`, entry: `${pascalOf(feature)}Module.register({ isGlobal: true })` };
|
|
201
|
+
const groups = [['domain', [domain]]];
|
|
202
|
+
if (fe) groups.push(['api', [
|
|
203
|
+
{ importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })' },
|
|
204
|
+
{ importLine: `import { ${pascalOf(feature)}GraphqlModule } from "@features/api/${feature}"`, entry: `${pascalOf(feature)}GraphqlModule.register({})` },
|
|
205
|
+
]]);
|
|
206
|
+
const lines = fs.readFileSync(at(root, ownerRelative), 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
207
|
+
const patches = groups.map(([area, entries]) => {
|
|
208
|
+
const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
|
|
209
|
+
addModuleImport(lines, { importLine: `import { ${area}Modules } from "./compose/${area}"`, entry: `...${area}Modules(${argument})`, relative: ownerRelative, noun: 'table' });
|
|
210
|
+
return planComposeGroup({ repoRoot: root, repo, app: owner.name, area, noun: 'table', entries });
|
|
231
211
|
});
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
wireImport(
|
|
236
|
-
wireImport(text, relative, {
|
|
237
|
-
importLine: 'import { GraphqlModule } from "@modules/platform/graphql"',
|
|
238
|
-
symbol: "GraphqlModule",
|
|
239
|
-
entry: "GraphqlModule.register({ isGlobal: true })",
|
|
240
|
-
}),
|
|
241
|
-
relative,
|
|
242
|
-
{
|
|
243
|
-
importLine: `import { ${pascalOf(name)}GraphqlModule } from "@features/api/${name}"`,
|
|
244
|
-
symbol: `${pascalOf(name)}GraphqlModule`,
|
|
245
|
-
entry: `${pascalOf(name)}GraphqlModule`,
|
|
246
|
-
},
|
|
247
|
-
);
|
|
212
|
+
patches.push({ relative: ownerRelative, text: lines.join('\n') });
|
|
213
|
+
return patches;
|
|
214
|
+
}
|
|
248
215
|
|
|
249
216
|
/** The platform GraphQL server capability (templates/be/platform/graphql), whole or absent: written once, by the first GraphQL feature. */
|
|
250
217
|
const GRAPHQL_PLATFORM = "be/src/modules/platform/graphql";
|
|
@@ -287,7 +254,7 @@ const GRAPHQL_URL_READER = [
|
|
|
287
254
|
|
|
288
255
|
/** An app's modules/config text with graphqlUrl appended once. */
|
|
289
256
|
const withGraphqlUrl = (text) =>
|
|
290
|
-
/^export const graphqlUrl\b/m.test(text) ? text : `${text.
|
|
257
|
+
/^export const graphqlUrl\b/m.test(text) ? text : `${text.trimEnd()}\n${GRAPHQL_URL_READER}\n`;
|
|
291
258
|
|
|
292
259
|
/** The be api feature of a table (owner ruling 2026-10-13, lite = full): get-<singular> and create-<singular> over GraphQL. */
|
|
293
260
|
const apiFeatureFiles = (values) => {
|
|
@@ -296,6 +263,8 @@ const apiFeatureFiles = (values) => {
|
|
|
296
263
|
const create = `create-${values.singular}`;
|
|
297
264
|
return [
|
|
298
265
|
[`${base}/index.ts`, "feature.index.ts.tpl"],
|
|
266
|
+
[`${base}/${values.name}.options.ts`, "feature.options.ts.tpl"],
|
|
267
|
+
[`${base}/${values.name}.module-definition.ts`, "feature.module-definition.ts.tpl"],
|
|
299
268
|
[`${base}/${values.name}.module.ts`, "feature.module.ts.tpl"],
|
|
300
269
|
[`${base}/application/${get}.query.ts`, "get.query.ts.tpl"],
|
|
301
270
|
[`${base}/application/${get}.handler.ts`, "get.handler.ts.tpl"],
|
|
@@ -310,6 +279,8 @@ const apiFeatureFiles = (values) => {
|
|
|
310
279
|
[`${base}/transport/graphql/${create}.resolver.ts`, "create.resolver.ts.tpl"],
|
|
311
280
|
[`${base}/transport/graphql/${create}.mapper.ts`, "create.mapper.ts.tpl"],
|
|
312
281
|
[`${base}/transport/graphql/dto/${create}.type.ts`, "create.type.ts.tpl"],
|
|
282
|
+
[`${base}/transport/graphql/${values.name}-graphql.options.ts`, "graphql.options.ts.tpl"],
|
|
283
|
+
[`${base}/transport/graphql/${values.name}-graphql.module-definition.ts`, "graphql.module-definition.ts.tpl"],
|
|
313
284
|
[`${base}/transport/graphql/${values.name}-graphql.module.ts`, "graphql.module.ts.tpl"],
|
|
314
285
|
].map(([relative, file]) => ({ relative, body: template(`be/table/api/${file}`, values) }));
|
|
315
286
|
};
|
|
@@ -351,7 +322,7 @@ const feBarrels = (app, values) => {
|
|
|
351
322
|
const withExport = (text, line) => {
|
|
352
323
|
const lines = text.replace(/\r\n/g, "\n").split("\n").filter(Boolean);
|
|
353
324
|
if (lines.includes(line)) return text;
|
|
354
|
-
return `${[...lines, line].sort(
|
|
325
|
+
return `${[...lines, line].sort(byCodeUnit).join("\n")}\n`;
|
|
355
326
|
};
|
|
356
327
|
|
|
357
328
|
/**
|
|
@@ -359,13 +330,7 @@ const withExport = (text, line) => {
|
|
|
359
330
|
* server capability on first use) and, in every product Next app, its operation files and SWR hooks.
|
|
360
331
|
* `emitTypes` returns the generated database.types.ts text; false is the explicit `--no-types` seam.
|
|
361
332
|
*/
|
|
362
|
-
|
|
363
|
-
root,
|
|
364
|
-
name,
|
|
365
|
-
fe = false,
|
|
366
|
-
emitTypes = emitDbTypes,
|
|
367
|
-
now = Date.now,
|
|
368
|
-
}) {
|
|
333
|
+
function tableContext({ root, name }) {
|
|
369
334
|
const requestedName = String(name);
|
|
370
335
|
if (!NAME.test(requestedName))
|
|
371
336
|
throw new ScaffoldError(
|
|
@@ -384,12 +349,16 @@ export function addTable({
|
|
|
384
349
|
);
|
|
385
350
|
}
|
|
386
351
|
|
|
352
|
+
return { root, name, requestedName, feature, table, repo, connection };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function nextMigration({ root, feature, requestedName, now }) {
|
|
387
356
|
const migrationsDir = path.join(root, "supabase", "migrations");
|
|
388
357
|
const migrations = fs.existsSync(migrationsDir)
|
|
389
358
|
? fs
|
|
390
359
|
.readdirSync(migrationsDir)
|
|
391
360
|
.filter((file) => MIGRATION.test(file))
|
|
392
|
-
.sort()
|
|
361
|
+
.sort(byCodeUnit)
|
|
393
362
|
: [];
|
|
394
363
|
if (migrations.some((file) => file.endsWith(`_${feature}.sql`)))
|
|
395
364
|
throw new ScaffoldError(
|
|
@@ -404,6 +373,10 @@ export function addTable({
|
|
|
404
373
|
`the current UTC timestamp ${stamp} is not later than the latest migration ${latest}; fix the clock or wait for the next second`,
|
|
405
374
|
);
|
|
406
375
|
|
|
376
|
+
return stamp;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function tableValues({ feature, table }) {
|
|
407
380
|
const singular = singularOf(feature);
|
|
408
381
|
const values = {
|
|
409
382
|
name: feature,
|
|
@@ -417,12 +390,20 @@ export function addTable({
|
|
|
417
390
|
singularCamel: camelOf(singular),
|
|
418
391
|
singularUpper: snakeOf(singular).toUpperCase(),
|
|
419
392
|
};
|
|
393
|
+
return values;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function productApps(repo, fe) {
|
|
420
397
|
const apps = fe ? coreFeApps(repo.sides?.fe?.apps ?? []) : [];
|
|
421
398
|
if (fe && !apps.length)
|
|
422
399
|
throw new ScaffoldError(
|
|
423
400
|
"HFS_ADD_NO_FRONT_END",
|
|
424
401
|
"add table --fe needs at least one declared product Next app",
|
|
425
402
|
);
|
|
403
|
+
return apps;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function tableFiles({ root, repo, feature, stamp, values, fe, apps }) {
|
|
426
407
|
const api = fe ? ".api" : "";
|
|
427
408
|
const domain = `be/src/modules/domain/${feature}`;
|
|
428
409
|
const planned = [
|
|
@@ -455,6 +436,10 @@ export function addTable({
|
|
|
455
436
|
}
|
|
456
437
|
}
|
|
457
438
|
|
|
439
|
+
return planned;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function tableOwner({ root, repo, connection, name, requestedName, planned }) {
|
|
458
443
|
const clash = planned
|
|
459
444
|
.filter((file) => fs.existsSync(at(root, file.relative)))
|
|
460
445
|
.map((file) => file.relative);
|
|
@@ -470,8 +455,10 @@ export function addTable({
|
|
|
470
455
|
if (!fs.existsSync(at(root, ownerRelative)))
|
|
471
456
|
throw new ScaffoldError("HFS_ADD_WIRE_MISSING", `add table registers ${name} in ${ownerRelative}, which does not exist`);
|
|
472
457
|
|
|
473
|
-
|
|
474
|
-
|
|
458
|
+
return { owner, ownerRelative };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function tableEditor(root, touched) {
|
|
475
462
|
const edit = (relative, change) => {
|
|
476
463
|
const file = at(root, relative);
|
|
477
464
|
if (!touched.has(file)) touched.set(file, fs.existsSync(file) ? fs.readFileSync(file) : null);
|
|
@@ -481,38 +468,51 @@ export function addTable({
|
|
|
481
468
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
482
469
|
fs.writeFileSync(file, after);
|
|
483
470
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
471
|
+
return edit;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function writeTableFiles({ root, planned, emitTypes, edit }, created) {
|
|
475
|
+
for (const file of planned) {
|
|
476
|
+
const target = at(root, file.relative);
|
|
477
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
478
|
+
fs.writeFileSync(target, file.body);
|
|
479
|
+
created.push(target);
|
|
480
|
+
}
|
|
481
|
+
if (typeof emitTypes === "function") {
|
|
482
|
+
const generated = emitTypes({ root });
|
|
483
|
+
if (typeof generated === "string") edit(dbTypesPath, () => generated);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function tableRegistrations({ root, repo, fe, edit }) {
|
|
489
|
+
const errorFile = "be/src/modules/platform/database/errors/database.error.ts";
|
|
490
|
+
if (repo.edition === "lite" && fs.existsSync(at(root, errorFile))) edit(errorFile, withSqlIdentifierError);
|
|
501
491
|
const registrations = [
|
|
502
492
|
...(repo.edition === "lite" ? ["table"] : []),
|
|
503
493
|
...(fe ? ["api", "graphql"] : []),
|
|
504
494
|
];
|
|
505
495
|
for (const generator of registrations) for (const relative of registrationFiles(generator)) edit(relative, (text) => text);
|
|
506
496
|
for (const generator of registrations) registerLiteExports({ root, generator });
|
|
507
|
-
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function wireTableFrontends({ root, feature, values, apps, edit }) {
|
|
508
500
|
registerLiteDomainService({ root, moduleSpecifier: `@modules/domain/${feature}`, service: `${values.Name}Service` });
|
|
509
|
-
edit(ownerRelative, (text) => wireGraphqlFeature(text, ownerRelative, feature));
|
|
510
501
|
for (const app of apps) {
|
|
511
502
|
for (const [relative, line] of feBarrels(app.name, values)) edit(relative, (text) => withExport(text, line));
|
|
512
503
|
edit(`fe/apps/${app.name}/src/modules/config/index.ts`, withGraphqlUrl);
|
|
513
504
|
}
|
|
514
|
-
|
|
515
|
-
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function wireTable({ root, repo, feature, values, ownerRelative, composition, fe, apps, edit }) {
|
|
508
|
+
const kindsHome = kindsHomeOf(root, ownerRelative);
|
|
509
|
+
for (const patch of composition) edit(patch.relative, () => patch.text);
|
|
510
|
+
edit(kindsHome, (text) => wireKinds(text, kindsHome, { kinds: `${snakeOf(feature).toUpperCase()}_ERROR_KINDS`, specifier: `@modules/domain/${feature}` }));
|
|
511
|
+
tableRegistrations({ root, repo, fe, edit });
|
|
512
|
+
if (fe) wireTableFrontends({ root, feature, values, apps, edit });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function rollbackTable(root, touched, created) {
|
|
516
516
|
for (const [file, before] of touched) {
|
|
517
517
|
if (before === null) fs.rmSync(file, { force: true });
|
|
518
518
|
else fs.writeFileSync(file, before);
|
|
@@ -522,10 +522,33 @@ export function addTable({
|
|
|
522
522
|
removeEmptyParents(file, root);
|
|
523
523
|
}
|
|
524
524
|
for (const [file, before] of touched) if (before === null) removeEmptyParents(file, root);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function applyTable(context) {
|
|
528
|
+
const touched = new Map();
|
|
529
|
+
const created = [];
|
|
530
|
+
const edit = tableEditor(context.root, touched);
|
|
531
|
+
try {
|
|
532
|
+
writeTableFiles({ ...context, edit }, created);
|
|
533
|
+
wireTable({ ...context, edit });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
rollbackTable(context.root, touched, created);
|
|
525
536
|
throw error;
|
|
526
537
|
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Adds one table through typed domain/API groups and restores every touched byte on failure. */
|
|
541
|
+
export function addTable({ root, name, fe = false, emitTypes = emitDbTypes, now = Date.now }) {
|
|
542
|
+
const context = tableContext({ root, name });
|
|
543
|
+
const stamp = nextMigration({ ...context, now });
|
|
544
|
+
const values = tableValues(context);
|
|
545
|
+
const apps = productApps(context.repo, fe);
|
|
546
|
+
const planned = tableFiles({ ...context, stamp, values, apps, fe });
|
|
547
|
+
const owner = tableOwner({ ...context, planned });
|
|
548
|
+
const composition = tableComposition({ ...context, ...owner, fe });
|
|
549
|
+
applyTable({ ...context, ...owner, planned, composition, apps, values, fe, emitTypes });
|
|
527
550
|
return {
|
|
528
|
-
created: planned.map((file) => file.relative),
|
|
551
|
+
created: [...planned.map((file) => file.relative), ...composition.filter((patch) => patch.exists === false).map((patch) => patch.relative)],
|
|
529
552
|
types: typeof emitTypes === "function" ? dbTypesPath : "skipped",
|
|
530
553
|
};
|
|
531
554
|
}
|
package/scaffold/add.mjs
CHANGED
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// `@@vCamel@@` its camelCase, `@@vUpper@@` its UPPER_SNAKE and `@@vSnake@@` its snake_case; the option `--service Class=module` gives `@@service@@` (the class) and `@@serviceModule@@`.
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
-
import { TEMPLATES_DIR } from '../sync/index.mjs';
|
|
20
19
|
import { ScaffoldError, pascalOf } from './service.mjs';
|
|
21
20
|
import { addTable, graphqlPlatformFiles } from './add-table.mjs';
|
|
21
|
+
import { addModuleImport, compositionCode, readComposeGroup, writeComposeGroup } from './add-compose.mjs';
|
|
22
22
|
import { addApp } from './add-app.mjs';
|
|
23
23
|
import { ensureLiteCli } from './add-cli-lite.mjs';
|
|
24
24
|
import { addLiteWebhookInbox } from './add-inbox-lite.mjs';
|
|
@@ -59,6 +59,7 @@ function registerFeatureExports({ repoRoot, repo, noun, values }) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declarationFile, values, forms, beRoot, patterns, planned }) {
|
|
62
|
+
if (noun === 'api' || noun === 'webhook') preflightFeatureComposition({ repoRoot, repo, noun, name });
|
|
62
63
|
const bootstrapCreated = noun === 'cli' && repo.edition === 'lite' ? ensureLiteCli({ root: repoRoot, manifest, repo }) : [];
|
|
63
64
|
for (const file of planned) {
|
|
64
65
|
fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
|
|
@@ -66,8 +67,8 @@ function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declaratio
|
|
|
66
67
|
}
|
|
67
68
|
registerFeatureExports({ repoRoot, repo, noun, values });
|
|
68
69
|
if (spec.wire) wire({ beRoot, wire: spec.wire, forms, noun, name });
|
|
69
|
-
const platformCreated = repo.edition === 'lite' && (noun === 'api' || noun === 'webhook') ? wireLiteFeature({ repoRoot, repo, noun, name }) : [];
|
|
70
70
|
const inboxCreated = addLiteWebhookInbox({ root: repoRoot, repo, noun, provider: name, values, now });
|
|
71
|
+
const platformCreated = noun === 'api' || noun === 'webhook' ? wireFeature({ repoRoot, repo, noun, name }) : [];
|
|
71
72
|
const graphqlDoor = noun === 'api' ? ensureGraphqlPeers(repoRoot) : [];
|
|
72
73
|
const registered = register({ declarationFile, spec: { patterns, trigger: spec.trigger, optionalSlots: noun === 'api' ? [GRAPHQL_CONTRACT_SLOT] : [] } });
|
|
73
74
|
return { created: [...bootstrapCreated, ...planned.map((file) => `be/${file.target}`), ...platformCreated, ...inboxCreated, ...graphqlDoor], registered };
|
|
@@ -91,15 +92,59 @@ function ensureGraphqlPeers(repoRoot) {
|
|
|
91
92
|
return ['package.json'];
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
/**
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
/** The skeleton registrations that move into area groups when the first transport is added. */
|
|
96
|
+
function skeletonComposition(lines, edition) {
|
|
97
|
+
const active = compositionCode(lines.join('\n')).split('\n');
|
|
98
|
+
const candidates = [
|
|
99
|
+
{ area: 'domain', symbol: 'IdentityModule', source: '@modules/domain/identity', entry: edition === 'lite' ? 'IdentityModule.register({ isGlobal: true, verifyAccessToken })' : 'IdentityModule.register({ isGlobal: true })' },
|
|
100
|
+
{ area: 'domain', symbol: 'LivenessModule', source: '@modules/domain/liveness', entry: 'LivenessModule.register({ isGlobal: true })' },
|
|
101
|
+
{ area: 'api', symbol: 'SystemHealthHttpModule', source: '@features/api/system-health', entry: 'SystemHealthHttpModule.register({})' },
|
|
102
|
+
{ area: 'api', symbol: 'GraphqlModule', source: '@modules/platform/graphql', entry: 'GraphqlModule.register({ isGlobal: true })' },
|
|
103
|
+
];
|
|
104
|
+
if (edition === 'full') candidates.push({ area: 'domain', symbol: 'NoteModule', source: '@modules/domain/note', entry: 'NoteModule.register({ isGlobal: true })' });
|
|
105
|
+
return candidates.filter(({ entry }) => active.some(line => line.trim() === `${entry},`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Existing groups that the root spreads must survive when another area is generated. */
|
|
109
|
+
function referencedComposeAreas(lines) {
|
|
110
|
+
const code = compositionCode(lines.join('\n'));
|
|
111
|
+
return ['domain', 'system', 'api', 'webhooks'].filter((area) => code.includes(`...${area}Modules(`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function featureCompositionRoot({ repoRoot, repo, noun }) {
|
|
115
|
+
const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
|
|
116
|
+
if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared api app`);
|
|
117
|
+
const relative = `be/apps/${app.name}/src/app.module.ts`;
|
|
118
|
+
const file = path.join(repoRoot, ...relative.split('/'));
|
|
119
|
+
if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
|
|
120
|
+
return { app, relative, file, lines: fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n') };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Refuse an incompatible root or area group before generating the feature or changing its capability exports. */
|
|
124
|
+
function preflightFeatureComposition({ repoRoot, repo, noun, name }) {
|
|
125
|
+
const { app, relative, lines } = featureCompositionRoot({ repoRoot, repo, noun });
|
|
126
|
+
if (noun === 'webhook' && repo.edition === 'lite') {
|
|
127
|
+
if (!compositionCode(lines.join('\n')).split('\n').some(line => /^\s*imports: \[$/.test(line)))
|
|
128
|
+
throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook needs a multiline imports array in ${relative}`);
|
|
129
|
+
planLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
|
|
130
|
+
}
|
|
131
|
+
const areas = new Set([noun === 'api' ? 'api' : 'webhooks', ...referencedComposeAreas(lines), ...skeletonComposition(lines, repo.edition).map(entry => entry.area)]);
|
|
132
|
+
for (const area of areas) {
|
|
133
|
+
readComposeGroup({ repoRoot, repo, app: app.name, area, noun });
|
|
134
|
+
const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
|
|
135
|
+
addModuleImport(lines, { importLine: `import { ${area}Modules } from "./compose/${area}"`, entry: `...${area}Modules(${argument})`, relative, noun });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Remove only a moved skeleton registration and its named import, keeping guards imported from the same barrel. */
|
|
140
|
+
function removeSkeletonRegistration(lines, { symbol, source, entry }) {
|
|
141
|
+
const entryAt = compositionCode(lines.join('\n')).split('\n').findIndex(line => line.trim() === `${entry},`);
|
|
142
|
+
lines.splice(entryAt, 1);
|
|
143
|
+
const importAt = lines.findIndex(line => line.startsWith('import { ') && line.replace(/;\s*$/, '').endsWith(` } from "${source}"`));
|
|
144
|
+
if (importAt < 0) return;
|
|
145
|
+
const symbols = lines[importAt].slice('import { '.length, lines[importAt].indexOf(' } from ')).split(',').map(value => value.trim()).filter(value => value !== symbol);
|
|
146
|
+
if (symbols.length) lines[importAt] = `import { ${symbols.join(', ')} } from "${source}"`;
|
|
147
|
+
else lines.splice(importAt, 1);
|
|
103
148
|
}
|
|
104
149
|
|
|
105
150
|
/** The GraphQL server capability (be/src/modules/platform/graphql), written whole by the first GraphQL feature of an app that has none. */
|
|
@@ -117,41 +162,62 @@ function ensureGraphqlPlatform(repoRoot) {
|
|
|
117
162
|
return written;
|
|
118
163
|
}
|
|
119
164
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
*/
|
|
124
|
-
function wireLiteFeature({ repoRoot, repo, noun, name }) {
|
|
125
|
-
const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
|
|
126
|
-
if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared lite api app`);
|
|
127
|
-
const relative = `be/apps/${app.name}/src/app.module.ts`;
|
|
128
|
-
const file = path.join(repoRoot, ...relative.split('/'));
|
|
129
|
-
if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
|
|
130
|
-
const lines = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n');
|
|
131
|
-
const created = [];
|
|
165
|
+
/** The typed area members of one transport, including lite transactional handoff capabilities. */
|
|
166
|
+
function transportComposition(noun, name, edition) {
|
|
167
|
+
const entries = [];
|
|
132
168
|
if (noun === 'api') {
|
|
133
|
-
|
|
134
|
-
addModuleImport(lines, { importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })', relative, noun });
|
|
169
|
+
entries.push({ importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })' });
|
|
135
170
|
const symbol = `${pascalOf(name)}GraphqlModule`;
|
|
136
|
-
|
|
171
|
+
entries.push({ importLine: `import { ${symbol} } from "@features/api/${name}"`, entry: symbol });
|
|
137
172
|
} else {
|
|
138
173
|
const symbol = `${pascalOf(name)}HttpModule`;
|
|
139
|
-
|
|
174
|
+
entries.push({ importLine: `import { ${symbol} } from "@features/webhooks/${name}"`, entry: symbol });
|
|
175
|
+
if (edition === 'lite') {
|
|
176
|
+
const inbox = `${pascalOf(name)}InboxModule`;
|
|
177
|
+
const importLine = `import { ${inbox} } from "@modules/domain/${name}-inbox"`;
|
|
178
|
+
const entry = `${inbox}.register({ isGlobal: true })`;
|
|
179
|
+
entries.push({ importLine, entry });
|
|
180
|
+
for (const platform of ['InboxModule', 'EventBusModule']) {
|
|
181
|
+
const capability = platform === 'InboxModule' ? 'inbox' : 'event-bus';
|
|
182
|
+
entries.push({ importLine: `import { ${platform} } from "@modules/platform/${capability}"`, entry: `${platform}.register({ isGlobal: true })` });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return entries;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compose each generated transport in its declared API owner through an explicitly typed area group. The root only spreads
|
|
191
|
+
* the group; it never acquires a second feature list. Lite's inbox capability belongs to its webhook group as well.
|
|
192
|
+
*/
|
|
193
|
+
function wireFeature({ repoRoot, repo, noun, name }) {
|
|
194
|
+
const { app, relative, file, lines } = featureCompositionRoot({ repoRoot, repo, noun });
|
|
195
|
+
const created = [];
|
|
196
|
+
const area = noun === 'api' ? 'api' : 'webhooks';
|
|
197
|
+
const groups = new Map([...referencedComposeAreas(lines), area].map((group) => [group, []]));
|
|
198
|
+
for (const migration of skeletonComposition(lines, repo.edition)) {
|
|
199
|
+
const entries = groups.get(migration.area) ?? [];
|
|
200
|
+
entries.push({ importLine: `import { ${migration.symbol} } from "${migration.source}"`, entry: migration.entry });
|
|
201
|
+
groups.set(migration.area, entries);
|
|
202
|
+
removeSkeletonRegistration(lines, migration);
|
|
140
203
|
}
|
|
141
|
-
|
|
142
|
-
if (noun === '
|
|
204
|
+
const entries = groups.get(area);
|
|
205
|
+
if (noun === 'api') created.push(...ensureGraphqlPlatform(repoRoot));
|
|
206
|
+
entries.push(...transportComposition(noun, name, repo.edition));
|
|
207
|
+
for (const [group, members] of groups) {
|
|
208
|
+
const symbol = `${group}Modules`;
|
|
209
|
+
const argument = group === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
|
|
210
|
+
addModuleImport(lines, { importLine: `import { ${symbol} } from "./compose/${group}"`, entry: `...${symbol}(${argument})`, relative, noun });
|
|
211
|
+
created.push(...writeComposeGroup({ repoRoot, repo, app: app.name, area: group, entries: members, noun }));
|
|
212
|
+
}
|
|
213
|
+
fs.writeFileSync(file, `${lines.join('\n').trimEnd()}\n`);
|
|
214
|
+
if (noun === 'webhook' && repo.edition === 'lite') wireLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
|
|
143
215
|
return created;
|
|
144
216
|
}
|
|
145
217
|
|
|
146
|
-
/** Adds
|
|
147
|
-
function
|
|
148
|
-
const
|
|
149
|
-
const relative = fs.existsSync(path.join(repoRoot, ...envRelative.split('/')))
|
|
150
|
-
? envRelative
|
|
151
|
-
: `be/apps/${app}/src/main.ts`;
|
|
152
|
-
const file = path.join(repoRoot, ...relative.split('/'));
|
|
153
|
-
let text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
154
|
-
const httpSecurityImport = /^import \{ ([^}]+) \} from "@modules\/platform\/http-security"$/m;
|
|
218
|
+
/** Adds the provider config reader to the existing typed HTTP security import. */
|
|
219
|
+
function webhookConfigImport(text, relative) {
|
|
220
|
+
const httpSecurityImport = /^import \{ ([^}]+) \} from "@modules\/platform\/http-security";?$/m;
|
|
155
221
|
const importMatch = httpSecurityImport.exec(text);
|
|
156
222
|
if (!importMatch) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook could not find the HTTP security import in ${relative}`);
|
|
157
223
|
const imported = importMatch[1].split(',').map(symbol => symbol.trim()).filter(Boolean);
|
|
@@ -160,6 +226,40 @@ function wireLiteWebhookConfig({ repoRoot, app, provider }) {
|
|
|
160
226
|
imported.splice(configIndex < 0 ? imported.length : configIndex + 1, 0, 'parseWebhookProviderConfig');
|
|
161
227
|
text = text.replace(httpSecurityImport, `import { ${imported.join(', ')} } from "@modules/platform/http-security"`);
|
|
162
228
|
}
|
|
229
|
+
return text;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Declares a signing key by name and leaves custody/provisioning to the environment owner. */
|
|
233
|
+
function webhookRosterPatches(repoRoot, provider) {
|
|
234
|
+
const stacks = path.join(repoRoot, '.starcistacks');
|
|
235
|
+
if (!fs.existsSync(stacks)) return [];
|
|
236
|
+
const key = formsOf({ provider }).providerUpper + '_WEBHOOK_SECRET';
|
|
237
|
+
const patches = [];
|
|
238
|
+
for (const entry of fs.readdirSync(stacks, { withFileTypes: true })) {
|
|
239
|
+
if (!entry.isDirectory()) continue;
|
|
240
|
+
const file = path.join(stacks, entry.name, 'runtime', 'env', 'KEYS.md');
|
|
241
|
+
if (!fs.existsSync(file)) continue;
|
|
242
|
+
const before = fs.readFileSync(file, 'utf8');
|
|
243
|
+
if (before.includes('\x60' + key + '\x60')) continue;
|
|
244
|
+
const lines = before.replace(/\r\n/g, '\n').split('\n');
|
|
245
|
+
const lastRow = lines.reduce((last, line, index) => line.startsWith('|') ? index : last, -1);
|
|
246
|
+
const row = '| \x60' + key + '\x60 | API signed ' + provider + ' webhook; owner must seal this key and provision its versioned external runtime secret before deployment |';
|
|
247
|
+
lines.splice(lastRow + 1, 0, row);
|
|
248
|
+
patches.push({ file, text: lines.join('\n') });
|
|
249
|
+
}
|
|
250
|
+
return patches;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Adds one generated webhook provider to the lite API's typed env reader (or an existing inline bootstrap) and enables exact raw-body verification. */
|
|
254
|
+
function planLiteWebhookConfig({ repoRoot, app, provider }) {
|
|
255
|
+
const envRelative = `be/apps/${app}/src/${app}.env.ts`;
|
|
256
|
+
const relative = fs.existsSync(path.join(repoRoot, ...envRelative.split('/')))
|
|
257
|
+
? envRelative
|
|
258
|
+
: `be/apps/${app}/src/main.ts`;
|
|
259
|
+
const file = path.join(repoRoot, ...relative.split('/'));
|
|
260
|
+
if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add webhook needs ${relative}`);
|
|
261
|
+
let text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
262
|
+
text = webhookConfigImport(text, relative);
|
|
163
263
|
const providerKey = /^[a-z][a-z0-9]*$/.test(provider) ? provider : JSON.stringify(provider);
|
|
164
264
|
const indent = /^([ \t]*)httpSecurity:/m.exec(text)?.[1] ?? ' ';
|
|
165
265
|
const providerLine = `${indent} ${providerKey}: parseWebhookProviderConfig(env, ${JSON.stringify(formsOf({ provider }).providerUpper)}),`;
|
|
@@ -168,24 +268,39 @@ function wireLiteWebhookConfig({ repoRoot, app, provider }) {
|
|
|
168
268
|
if (empty.test(text)) {
|
|
169
269
|
text = text.replace(empty, ['httpSecurity: {', `${indent} ...parseHttpSecurityConfig(env),`, `${indent} webhooks: {`, providerLine, `${indent} },`, `${indent}}`].join('\n'));
|
|
170
270
|
} else {
|
|
171
|
-
const found =
|
|
271
|
+
const found = /^[ \t]*webhooks: \{$/m.exec(text);
|
|
172
272
|
if (!found) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook could not find httpSecurity options in ${relative}`);
|
|
173
273
|
const insertAt = found.index + found[0].length;
|
|
174
274
|
text = `${text.slice(0, insertAt)}\n${providerLine}${text.slice(insertAt)}`;
|
|
175
275
|
}
|
|
176
276
|
}
|
|
177
277
|
text = text.replace('NestFactory.create(AppModule.register(options))', 'NestFactory.create(AppModule.register(options), { rawBody: true })');
|
|
178
|
-
|
|
278
|
+
const planned = [{ file, text }];
|
|
179
279
|
if (relative === envRelative) {
|
|
180
280
|
const main = path.join(repoRoot, 'be', 'apps', app, 'src', 'main.ts');
|
|
281
|
+
if (!fs.existsSync(main)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add webhook needs be/apps/${app}/src/main.ts`);
|
|
181
282
|
const entry = fs.readFileSync(main, 'utf8').replace('NestFactory.create(AppModule.register(options))', 'NestFactory.create(AppModule.register(options), { rawBody: true })');
|
|
182
|
-
|
|
283
|
+
planned.push({ file: main, text: entry });
|
|
284
|
+
}
|
|
285
|
+
planned.push(...webhookRosterPatches(repoRoot, provider));
|
|
286
|
+
return planned;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Applies the preflighted provider configuration and exact raw-body bootstrap option. */
|
|
290
|
+
function wireLiteWebhookConfig(input) {
|
|
291
|
+
const patches = planLiteWebhookConfig(input);
|
|
292
|
+
const originals = new Map(patches.map(({ file }) => [file, fs.readFileSync(file)]));
|
|
293
|
+
try {
|
|
294
|
+
for (const { file, text } of patches) fs.writeFileSync(file, text);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
for (const [file, text] of originals) fs.writeFileSync(file, text);
|
|
297
|
+
throw error;
|
|
183
298
|
}
|
|
184
299
|
}
|
|
185
300
|
|
|
186
301
|
/**
|
|
187
302
|
* Registers the generated member in the static module that lists its siblings (the cli feature root lists each group module): one import
|
|
188
|
-
* line after the last import and one more entry of the `imports` array of its `@Module
|
|
303
|
+
* line after the last import and one more entry of the `imports` array of its `@Module`, written `Symbol.register({})` (every module has a module definition). Nothing happens when the member is already listed.
|
|
189
304
|
*/
|
|
190
305
|
function wire({ beRoot, wire: target, forms, noun, name }) {
|
|
191
306
|
const file = path.join(beRoot, ...target.file.split('/'));
|
|
@@ -193,13 +308,13 @@ function wire({ beRoot, wire: target, forms, noun, name }) {
|
|
|
193
308
|
const fill = (text) => fillKnownForms(text, forms);
|
|
194
309
|
const symbol = fill(target.symbol);
|
|
195
310
|
let text = fs.readFileSync(file, 'utf8');
|
|
196
|
-
if (new RegExp(
|
|
311
|
+
if (new RegExp(String.raw`\b${symbol}\b`).test(text)) return;
|
|
197
312
|
const array = /@Module\(\{ imports: \[([^\]]*)\] \}\)/.exec(text);
|
|
198
313
|
const lines = text.split('\n');
|
|
199
314
|
const lastImport = lines.map((line) => line.startsWith('import ')).lastIndexOf(true);
|
|
200
315
|
if (!array || lastImport < 0) throw new ScaffoldError('HFS_ADD_WIRE_FAILED', `${target.file} has no \`@Module({ imports: [...] })\` to register ${symbol} in`);
|
|
201
316
|
lines.splice(lastImport + 1, 0, `import { ${symbol} } from "${fill(target.import)}"`);
|
|
202
|
-
text = lines.join('\n').replace(array[0], `@Module({ imports: [${[array[1].trim(), symbol].filter(Boolean).join(', ')}] })`);
|
|
317
|
+
text = lines.join('\n').replace(array[0], `@Module({ imports: [${[array[1].trim(), `${symbol}.register({})`].filter(Boolean).join(', ')}] })`);
|
|
203
318
|
fs.writeFileSync(file, text);
|
|
204
319
|
}
|
|
205
320
|
|