@warlock.js/core 5.34.2 → 5.35.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 +62 -0
- package/esm/cli/cli-commands.manager.mjs +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +30 -3
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +1 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +5 -0
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
- package/esm/cli/commands/explain.command.mjs +14 -4
- package/esm/cli/commands/explain.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +19 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
- package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
- package/esm/cli/commands/inspect.command.mjs +13 -8
- package/esm/cli/commands/inspect.command.mjs.map +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -0
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/load-config-files.mjs +4 -0
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +11 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +16 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +13 -4
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +31 -12
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +14 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +4 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/e2e/browser.d.mts.map +1 -1
- package/esm/e2e/browser.mjs +3 -1
- package/esm/e2e/browser.mjs.map +1 -1
- package/esm/e2e/index.d.mts +2 -2
- package/esm/e2e/page.d.mts +6 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +36 -1
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +37 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +48 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +48 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/errors/index.d.mts +2 -2
- package/esm/errors/index.mjs +2 -2
- package/esm/generations/add-command.action.mjs +2 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/design-system-payload.mjs +53 -0
- package/esm/generations/design-system-payload.mjs.map +1 -0
- package/esm/generations/features/design-system.feature.mjs +38 -0
- package/esm/generations/features/design-system.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +3 -1
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +13 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +48 -13
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +9 -5
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/error-mapper.d.mts +1 -1
- package/esm/http/error-mapper.mjs +3 -3
- package/esm/http/error-mapper.mjs.map +1 -1
- package/esm/http/error-response.d.mts +15 -0
- package/esm/http/error-response.d.mts.map +1 -0
- package/esm/http/error-response.mjs +47 -0
- package/esm/http/error-response.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +8 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +13 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +5 -3
- package/esm/http/index.mjs +3 -2
- package/esm/http/list-endpoint.d.mts +35 -0
- package/esm/http/list-endpoint.d.mts.map +1 -0
- package/esm/http/list-endpoint.mjs +28 -0
- package/esm/http/list-endpoint.mjs.map +1 -0
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +12 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/response.d.mts +2 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +2 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs +2 -0
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts +7 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/index.d.mts +10 -7
- package/esm/index.mjs +3 -2
- package/esm/openapi/security.mjs +12 -4
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +2 -0
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/build-budgets.d.mts +46 -0
- package/esm/production/build-budgets.d.mts.map +1 -0
- package/esm/production/build-budgets.mjs +109 -0
- package/esm/production/build-budgets.mjs.map +1 -0
- package/esm/production/production-builder.mjs +13 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.d.mts.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/tests/fakes/storage-fake.d.mts +3 -1
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.mjs +3 -3
- package/esm/tests/fakes/storage-fake.mjs.map +1 -1
- package/esm/tests/fakes/test-fakes.d.mts +3 -1
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +3 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +4 -2
- package/esm/tests/index.mjs +3 -1
- package/esm/tests/load-unit-config.mjs +24 -0
- package/esm/tests/load-unit-config.mjs.map +1 -0
- package/esm/tests/response-contract.d.mts +32 -0
- package/esm/tests/response-contract.d.mts.map +1 -0
- package/esm/tests/response-contract.mjs +116 -0
- package/esm/tests/response-contract.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +10 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-locales.mjs +33 -0
- package/esm/tests/test-locales.mjs.map +1 -0
- package/esm/tests/test-runtime-scope.d.mts +15 -0
- package/esm/tests/test-runtime-scope.d.mts.map +1 -0
- package/esm/tests/test-runtime-scope.mjs +80 -0
- package/esm/tests/test-runtime-scope.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +13 -3
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +146 -18
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/affected-files.d.mts +5 -0
- package/esm/tooling/check/affected-files.d.mts.map +1 -0
- package/esm/tooling/check/affected-files.mjs +56 -0
- package/esm/tooling/check/affected-files.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +2 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -1
- package/esm/tooling/check/changed-files.mjs +2 -2
- package/esm/tooling/check/changed-files.mjs.map +1 -1
- package/esm/tooling/check/conventions-step.d.mts +2 -1
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +48 -6
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/fixes.d.mts +8 -0
- package/esm/tooling/check/fixes.d.mts.map +1 -0
- package/esm/tooling/check/fixes.mjs +31 -0
- package/esm/tooling/check/fixes.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +12 -0
- package/esm/tooling/check/index.mjs +3 -2
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +20 -4
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/run-tests.mjs +1 -1
- package/esm/tooling/check/source-ast.d.mts +1 -0
- package/esm/tooling/check/web-conventions.d.mts +10 -0
- package/esm/tooling/check/web-conventions.d.mts.map +1 -0
- package/esm/tooling/check/web-conventions.mjs +126 -0
- package/esm/tooling/check/web-conventions.mjs.map +1 -0
- package/esm/tooling/index.d.mts +7 -2
- package/esm/tooling/index.mjs +4 -2
- package/esm/tooling/inspect/collect-files.mjs +172 -0
- package/esm/tooling/inspect/collect-files.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +3 -2
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +1 -1
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/tooling/inspect/explain-file.d.mts +12 -0
- package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
- package/esm/tooling/inspect/explain-file.mjs +45 -0
- package/esm/tooling/inspect/explain-file.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +3 -2
- package/esm/tooling/inspect/index.mjs +2 -1
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-app.mjs +9 -3
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
- package/esm/tooling/mcp/mcp-tools.mjs +8 -2
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +4 -0
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs +1 -0
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/tooling/syntax/ast.d.mts +2 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs +3 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -1
- package/esm/vite/index.d.mts +3 -1
- package/esm/vite/index.mjs +3 -1
- package/esm/vite/test-setup-reporter.d.mts +16 -0
- package/esm/vite/test-setup-reporter.d.mts.map +1 -0
- package/esm/vite/test-setup-reporter.mjs +55 -0
- package/esm/vite/test-setup-reporter.mjs.map +1 -0
- package/esm/vite/test-workers.d.mts +5 -0
- package/esm/vite/test-workers.d.mts.map +1 -0
- package/esm/vite/test-workers.mjs +38 -0
- package/esm/vite/test-workers.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +3 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +883 -70
- package/llms.txt +58 -56
- package/package.json +15 -14
- package/schemas/inspect-report.schema.json +47 -1
- package/skills/build-budgets/SKILL.md +51 -0
- package/skills/build-restful/SKILL.md +56 -0
- package/skills/configure-app/SKILL.md +34 -0
- package/skills/create-module/SKILL.md +9 -0
- package/skills/generate-openapi/SKILL.md +1 -1
- package/skills/generate-web-ui/SKILL.md +71 -0
- package/skills/index.md +3 -3
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +25 -0
- package/skills/send-response/SKILL.md +77 -0
- package/skills/test-browser/SKILL.md +54 -0
- package/skills/test-http/SKILL.md +91 -0
- package/skills/test-service/SKILL.md +161 -3
- package/skills/upgrade-from-5-29/SKILL.md +22 -0
- package/skills/use-localization/SKILL.md +21 -0
- package/skills/warlock-check/SKILL.md +72 -1
- package/skills/warlock-inspect/SKILL.md +45 -2
- package/skills/warlock-mcp/SKILL.md +4 -0
- package/skills/warlock-trace/SKILL.md +2 -2
- package/skills/write-cli-command/SKILL.md +20 -2
- package/esm/production/build-app-production.mjs +0 -14
- package/esm/production/build-app-production.mjs.map +0 -1
|
@@ -11,6 +11,8 @@ import { aiFeature } from "./ai.feature.mjs";
|
|
|
11
11
|
import { authGoogleFeature } from "./auth-google.feature.mjs";
|
|
12
12
|
import { authPasskeysFeature } from "./auth-passkeys.feature.mjs";
|
|
13
13
|
import { bullBoardFeature } from "./bull-board.feature.mjs";
|
|
14
|
+
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
15
|
+
import { designSystemFeature } from "./design-system.feature.mjs";
|
|
14
16
|
import { devtoolsFeature } from "./devtools.feature.mjs";
|
|
15
17
|
import { heraldFeature } from "./herald.feature.mjs";
|
|
16
18
|
import { imageFeature } from "./image.feature.mjs";
|
|
@@ -27,7 +29,6 @@ import { redisFeature } from "./redis.feature.mjs";
|
|
|
27
29
|
import { s3Feature } from "./s3.feature.mjs";
|
|
28
30
|
import { schedulerFeature } from "./scheduler.feature.mjs";
|
|
29
31
|
import { sesFeature } from "./ses.feature.mjs";
|
|
30
|
-
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
31
32
|
import { sitemapFeature } from "./sitemap.feature.mjs";
|
|
32
33
|
import { socketFeature } from "./socket.feature.mjs";
|
|
33
34
|
import { tailwindFeature } from "./tailwind.feature.mjs";
|
|
@@ -59,6 +60,7 @@ const featuresMap = {
|
|
|
59
60
|
web: webFeature,
|
|
60
61
|
tailwind: tailwindFeature,
|
|
61
62
|
shadcn: shadcnFeature,
|
|
63
|
+
"design-system": designSystemFeature,
|
|
62
64
|
sitemap: sitemapFeature,
|
|
63
65
|
devtools: devtoolsFeature,
|
|
64
66
|
process: processFeature,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\nimport { aiGoogleFeature } from \"./ai-google.feature\";\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\nimport { aiToolsFeature } from \"./ai-tools.feature\";\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\nimport { aiFeature } from \"./ai.feature\";\nimport { authGoogleFeature } from \"./auth-google.feature\";\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\nimport { bullBoardFeature } from \"./bull-board.feature\";\nimport { devtoolsFeature } from \"./devtools.feature\";\nimport { heraldFeature } from \"./herald.feature\";\nimport { imageFeature } from \"./image.feature\";\nimport { mailFeature } from \"./mail.feature\";\nimport { mongodbFeature } from \"./mongodb.feature\";\nimport { mysqlFeature } from \"./mysql.feature\";\nimport { notificationsFeature } from \"./notifications.feature\";\nimport { postgresFeature } from \"./postgres.feature\";\nimport { processFeature } from \"./process.feature\";\nimport { queueFeature } from \"./queue.feature\";\nimport { reactEmailFeature } from \"./react-email.feature\";\nimport { reactFeature } from \"./react.feature\";\nimport { redisFeature } from \"./redis.feature\";\nimport { s3Feature } from \"./s3.feature\";\nimport { schedulerFeature } from \"./scheduler.feature\";\nimport { sesFeature } from \"./ses.feature\";\nimport { shadcnFeature } from \"./shadcn.feature\";\nimport { sitemapFeature } from \"./sitemap.feature\";\nimport { socketFeature } from \"./socket.feature\";\nimport { tailwindFeature } from \"./tailwind.feature\";\nimport { testFeature } from \"./test.feature\";\nimport type { FeatureDefinition } from \"./types\";\nimport { webFeature } from \"./web.feature\";\n\nexport type { FeatureDefinition } from \"./types\";\n\n/**\n * The feature registry `warlock add` dispatches against.\n *\n * This file is an INDEX, nothing more: every entry lives in its own module\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\n * the order `--list` prints and the order the \"not allowed\" error lists — so\n * add new features in the place they should appear, not alphabetically.\n */\nexport const featuresMap: Record<string, FeatureDefinition> = {\n \"react-email\": reactEmailFeature,\n react: reactFeature,\n image: imageFeature,\n mail: mailFeature,\n ses: sesFeature,\n mongodb: mongodbFeature,\n scheduler: schedulerFeature,\n // swagger / postman intentionally omitted — those packages do not exist yet;\n // they will ship together in the unified @warlock.js/api-docs package.\n postgres: postgresFeature,\n mysql: mysqlFeature,\n redis: redisFeature,\n s3: s3Feature,\n test: testFeature,\n web: webFeature,\n // Directly after `web`, and only there: it `requires` it, it is useless\n // without it, and a reader scanning `--list` for the page stack should meet\n // the two together rather than find styling filed between queues and sockets.\n tailwind: tailwindFeature,\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\n // it `requires` it, it appends to the stylesheet that feature creates, and the\n // three of them are one stack a reader should meet in build order.\n shadcn: shadcnFeature,\n // Directly after the web/tailwind/shadcn stack, for the same reason: it\n // `requires` web (it reads the page registry `listRoutablePages()`\n // exposes) and a reader scanning `--list` for the page stack should meet it\n // there rather than filed between queues and sockets.\n sitemap: sitemapFeature,\n devtools: devtoolsFeature,\n process: processFeature,\n herald: heraldFeature,\n queue: queueFeature,\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\n // needs queue already installed and configured, and a reader scanning\n // `--list` for job-queue features should meet the two together.\n \"bull-board\": bullBoardFeature,\n socket: socketFeature,\n notifications: notificationsFeature,\n access: accessFeature,\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\n \"auth-google\": authGoogleFeature,\n \"auth-passkeys\": authPasskeysFeature,\n ai: aiFeature,\n \"ai-openai\": aiOpenaiFeature,\n \"ai-google\": aiGoogleFeature,\n \"ai-anthropic\": aiAnthropicFeature,\n \"ai-bedrock\": aiBedrockFeature,\n \"ai-ollama\": aiOllamaFeature,\n \"ai-tools\": aiToolsFeature,\n \"ai-panoptic\": aiPanopticFeature,\n \"ai-workspace\": aiWorkspaceFeature,\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\nimport { aiGoogleFeature } from \"./ai-google.feature\";\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\nimport { aiToolsFeature } from \"./ai-tools.feature\";\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\nimport { aiFeature } from \"./ai.feature\";\nimport { authGoogleFeature } from \"./auth-google.feature\";\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\nimport { bullBoardFeature } from \"./bull-board.feature\";\nimport { designSystemFeature } from \"./design-system.feature\";\nimport { devtoolsFeature } from \"./devtools.feature\";\nimport { heraldFeature } from \"./herald.feature\";\nimport { imageFeature } from \"./image.feature\";\nimport { mailFeature } from \"./mail.feature\";\nimport { mongodbFeature } from \"./mongodb.feature\";\nimport { mysqlFeature } from \"./mysql.feature\";\nimport { notificationsFeature } from \"./notifications.feature\";\nimport { postgresFeature } from \"./postgres.feature\";\nimport { processFeature } from \"./process.feature\";\nimport { queueFeature } from \"./queue.feature\";\nimport { reactEmailFeature } from \"./react-email.feature\";\nimport { reactFeature } from \"./react.feature\";\nimport { redisFeature } from \"./redis.feature\";\nimport { s3Feature } from \"./s3.feature\";\nimport { schedulerFeature } from \"./scheduler.feature\";\nimport { sesFeature } from \"./ses.feature\";\nimport { shadcnFeature } from \"./shadcn.feature\";\nimport { sitemapFeature } from \"./sitemap.feature\";\nimport { socketFeature } from \"./socket.feature\";\nimport { tailwindFeature } from \"./tailwind.feature\";\nimport { testFeature } from \"./test.feature\";\nimport type { FeatureDefinition } from \"./types\";\nimport { webFeature } from \"./web.feature\";\n\nexport type { FeatureDefinition } from \"./types\";\n\n/**\n * The feature registry `warlock add` dispatches against.\n *\n * This file is an INDEX, nothing more: every entry lives in its own module\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\n * the order `--list` prints and the order the \"not allowed\" error lists — so\n * add new features in the place they should appear, not alphabetically.\n */\nexport const featuresMap: Record<string, FeatureDefinition> = {\n \"react-email\": reactEmailFeature,\n react: reactFeature,\n image: imageFeature,\n mail: mailFeature,\n ses: sesFeature,\n mongodb: mongodbFeature,\n scheduler: schedulerFeature,\n // swagger / postman intentionally omitted — those packages do not exist yet;\n // they will ship together in the unified @warlock.js/api-docs package.\n postgres: postgresFeature,\n mysql: mysqlFeature,\n redis: redisFeature,\n s3: s3Feature,\n test: testFeature,\n web: webFeature,\n // Directly after `web`, and only there: it `requires` it, it is useless\n // without it, and a reader scanning `--list` for the page stack should meet\n // the two together rather than find styling filed between queues and sockets.\n tailwind: tailwindFeature,\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\n // it `requires` it, it appends to the stylesheet that feature creates, and the\n // three of them are one stack a reader should meet in build order.\n shadcn: shadcnFeature,\n \"design-system\": designSystemFeature,\n // Directly after the web/tailwind/shadcn stack, for the same reason: it\n // `requires` web (it reads the page registry `listRoutablePages()`\n // exposes) and a reader scanning `--list` for the page stack should meet it\n // there rather than filed between queues and sockets.\n sitemap: sitemapFeature,\n devtools: devtoolsFeature,\n process: processFeature,\n herald: heraldFeature,\n queue: queueFeature,\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\n // needs queue already installed and configured, and a reader scanning\n // `--list` for job-queue features should meet the two together.\n \"bull-board\": bullBoardFeature,\n socket: socketFeature,\n notifications: notificationsFeature,\n access: accessFeature,\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\n \"auth-google\": authGoogleFeature,\n \"auth-passkeys\": authPasskeysFeature,\n ai: aiFeature,\n \"ai-openai\": aiOpenaiFeature,\n \"ai-google\": aiGoogleFeature,\n \"ai-anthropic\": aiAnthropicFeature,\n \"ai-bedrock\": aiBedrockFeature,\n \"ai-ollama\": aiOllamaFeature,\n \"ai-tools\": aiToolsFeature,\n \"ai-panoptic\": aiPanopticFeature,\n \"ai-workspace\": aiWorkspaceFeature,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAa,cAAiD;CAC5D,eAAe;CACf,OAAO;CACP,OAAO;CACP,MAAM;CACN,KAAK;CACL,SAAS;CACT,WAAW;CAGX,UAAU;CACV,OAAO;CACP,OAAO;CACP,IAAI;CACJ,MAAM;CACN,KAAK;CAIL,UAAU;CAIV,QAAQ;CACR,iBAAiB;CAKjB,SAAS;CACT,UAAU;CACV,SAAS;CACT,QAAQ;CACR,OAAO;CAIP,cAAc;CACd,QAAQ;CACR,eAAe;CACf,QAAQ;CAER,eAAe;CACf,iBAAiB;CACjB,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
|
|
@@ -2,7 +2,7 @@ import { srcPath } from "../../utils/paths.mjs";
|
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { recordSkippedFile } from "../generation-run.mjs";
|
|
4
4
|
import { directoryExistsAsync, ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "../generation-fs.mjs";
|
|
5
|
-
import { listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub,
|
|
5
|
+
import { createNotificationLocalesStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, unreadNotificationsCountControllerStub } from "../stubs.mjs";
|
|
6
6
|
import { migrationTimestamp } from "./shared/migration-timestamp.mjs";
|
|
7
7
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
8
8
|
import { dirname } from "node:path";
|
|
@@ -63,6 +63,7 @@ async function writeModuleFile(relativePath, content, skipped) {
|
|
|
63
63
|
* that renames its group, is left for the app to update by hand.
|
|
64
64
|
*/
|
|
65
65
|
async function registerNotificationLocales(skipped) {
|
|
66
|
+
const notificationLocalesStub = createNotificationLocalesStub();
|
|
66
67
|
const localesPath = srcPath("app/notifications/utils/locales.json");
|
|
67
68
|
if (!await fileExistsAsync(localesPath)) {
|
|
68
69
|
await writeModuleFile("utils/locales.json", notificationLocalesStub, skipped);
|
|
@@ -83,8 +84,18 @@ async function registerNotificationLocales(skipped) {
|
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
const missing = Object.keys(defaults).filter((key) => dictionary[key] === void 0);
|
|
87
|
+
const existing = dictionary.notFound;
|
|
88
|
+
const translations = defaults.notFound;
|
|
89
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing) && translations && typeof translations === "object" && !Array.isArray(translations)) {
|
|
90
|
+
const current = existing;
|
|
91
|
+
for (const [code, fallback] of Object.entries(translations)) {
|
|
92
|
+
if (current[code] !== void 0) continue;
|
|
93
|
+
current[code] = fallback;
|
|
94
|
+
missing.push(`notFound.${code}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
86
97
|
if (missing.length === 0) return;
|
|
87
|
-
for (const key of
|
|
98
|
+
for (const key of Object.keys(defaults)) if (dictionary[key] === void 0) dictionary[key] = defaults[key];
|
|
88
99
|
await putFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\n`);
|
|
89
100
|
console.log(`${colors.green("✓")} Added ${missing.join(", ")} to src/app/notifications/utils/locales.json`);
|
|
90
101
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { dirname } from \"node:path\";\nimport {\n directoryExistsAsync,\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n recordSkippedFile,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { srcPath } from \"../../utils\";\nimport {\n listNotificationsControllerStub,\n listNotificationsSchemaStub,\n markAllNotificationsReadControllerStub,\n markNotificationReadControllerStub,\n markNotificationReadServiceStub,\n notificationControllerSpecStub,\n notificationTestFixtureStub,\n notificationLocalesStub,\n notificationMigrationStub,\n notificationModelStub,\n notificationResourceStub,\n notificationRoutesStub,\n notificationsConfigStub,\n notificationsRequestTypeStub,\n unreadNotificationsCountControllerStub,\n} from \"../stubs\";\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/**\n * The module's controllers, with the route each one serves. A spec per\n * controller is written next to it when the app has the test feature.\n */\nconst notificationControllers = [\n {\n file: \"list-notifications.controller\",\n stub: listNotificationsControllerStub,\n method: \"GET\",\n path: \"/notifications\",\n },\n {\n file: \"unread-notifications-count.controller\",\n stub: unreadNotificationsCountControllerStub,\n method: \"GET\",\n path: \"/notifications/unread-count\",\n },\n {\n file: \"mark-notification-read.controller\",\n stub: markNotificationReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/1/read\",\n },\n {\n file: \"mark-all-notifications-read.controller\",\n stub: markAllNotificationsReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/read-all\",\n },\n] as const;\n\n/** The model's import path in the ejected config, before and since 5.30. */\nconst MODEL_IMPORT = \"app/notifications/models/notification/notification.model\";\nconst LEGACY_MODEL_IMPORT = \"app/notifications/notification.model\";\n\n/**\n * Write one module file unless it already exists. A file the app already has\n * is the app's own code: it is reported and left alone, never overwritten.\n */\nasync function writeModuleFile(relativePath: string, content: string, skipped: string[]) {\n const path = srcPath(\"app/notifications\", relativePath);\n\n if (await fileExistsAsync(path)) {\n skipped.push(relativePath);\n recordSkippedFile(path, \"exists\");\n return;\n }\n\n await ensureDirectoryAsync(dirname(path));\n await putFileAsync(path, content);\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/${relativePath}`);\n}\n\n/**\n * Add the module's `notFound` message to an existing utils/locales.json, or\n * create the file. Existing keys win; a file that is not a JSON object, or\n * that renames its group, is left for the app to update by hand.\n */\nasync function registerNotificationLocales(skipped: string[]) {\n const localesPath = srcPath(\"app/notifications/utils/locales.json\");\n\n if (!(await fileExistsAsync(localesPath))) {\n await writeModuleFile(\"utils/locales.json\", notificationLocalesStub, skipped);\n return;\n }\n\n const defaults = JSON.parse(notificationLocalesStub) as Record<string, unknown>;\n let dictionary: Record<string, unknown>;\n\n try {\n const parsed: unknown = JSON.parse(await getFileAsync(localesPath));\n\n if (parsed === null || Array.isArray(parsed) || typeof parsed !== \"object\") throw new Error();\n\n dictionary = parsed as Record<string, unknown>;\n } catch {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON is invalid; add notFound to src/app/notifications/utils/locales.json manually.`,\n );\n return;\n }\n\n // A root `$group` renames the file's namespace, so the service's\n // t(\"notifications.notFound\") would not find a key merged here.\n if (dictionary.$group !== undefined && dictionary.$group !== \"notifications\") {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON declares a different $group; add notifications.notFound manually.`,\n );\n return;\n }\n\n const missing = Object.keys(defaults).filter((key) => dictionary[key] === undefined);\n\n if (missing.length === 0) return;\n\n for (const key of missing) dictionary[key] = defaults[key];\n\n await putFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\\n`);\n console.log(\n `${colors.green(\"✓\")} Added ${missing.join(\", \")} to src/app/notifications/utils/locales.json`,\n );\n}\n\n/**\n * The config ejected just before this runs imports the 5.30 model path. In an\n * app scaffolded by 5.29 the model is still at the module root, so point the\n * import there instead of at a file that does not exist.\n */\nasync function pointConfigAtLegacyModel() {\n const configPath = srcPath(\"config/notifications.ts\");\n\n if (!(await fileExistsAsync(configPath))) return;\n\n const config = await getFileAsync(configPath);\n\n if (!config.includes(`\"${MODEL_IMPORT}\"`)) return;\n\n await putFileAsync(configPath, config.replace(`\"${MODEL_IMPORT}\"`, `\"${LEGACY_MODEL_IMPORT}\"`));\n console.log(\n `${colors.green(\"✓\")} src/config/notifications.ts imports the existing model at ${LEGACY_MODEL_IMPORT}`,\n );\n}\n\nasync function completeNotificationsInstallation(_options: CommandActionData) {\n const modelPath = srcPath(\"app/notifications/models/notification/notification.model.ts\");\n // Where 5.29 and earlier wrote the model.\n const legacyModelPath = srcPath(\"app/notifications/notification.model.ts\");\n\n // The model file is the sentinel for \"notifications already scaffolded\" —\n // its presence means the migration was created too (timestamped, so we must\n // not re-emit a duplicate on a second run).\n if (await fileExistsAsync(legacyModelPath)) {\n if (!(await fileExistsAsync(modelPath))) await pointConfigAtLegacyModel();\n\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded (5.29 layout), skipping...`,\n );\n return;\n }\n\n if (await fileExistsAsync(modelPath)) {\n console.log(`${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping...`);\n return;\n }\n\n const skipped: string[] = [];\n // An app that already has its own notifications module may keep its model\n // anywhere in it; a second Notification model would collide with it.\n const moduleExists = await directoryExistsAsync(srcPath(\"app/notifications\"));\n\n // 1. Model + migration under models/notification/, like every other module.\n // The migration's MM-DD-YYYY_HH-MM-SS prefix lets cascade infer its\n // createdAt and order it deterministically.\n if (moduleExists) {\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already exists: not adding a Notification model or migration. config/notifications.ts must import your model.`,\n );\n } else {\n await writeModuleFile(\n \"models/notification/notification.model.ts\",\n notificationModelStub,\n skipped,\n );\n await writeModuleFile(\n `models/notification/migrations/${migrationTimestamp()}-notification.migration.ts`,\n notificationMigrationStub,\n skipped,\n );\n }\n\n // 2. One responsibility per file: output resource, validated list query,\n // guarded request type, service, translations.\n await writeModuleFile(\"resources/notification.resource.ts\", notificationResourceStub, skipped);\n await writeModuleFile(\n \"schema/list-notifications.schema.ts\",\n listNotificationsSchemaStub,\n skipped,\n );\n await writeModuleFile(\n \"types/notifications-request.type.ts\",\n notificationsRequestTypeStub,\n skipped,\n );\n await writeModuleFile(\n \"services/mark-notification-read.service.ts\",\n markNotificationReadServiceStub,\n skipped,\n );\n await registerNotificationLocales(skipped);\n\n // 3. HTTP surface — list, unread count, mark read, mark all read, one\n // controller per file, gated by authMiddleware in routes.ts.\n const withSpecs = await fileExistsAsync(srcPath(\"test-setup.ts\"));\n if (withSpecs) {\n await writeModuleFile(\"tests/notifications.fixture.ts\", notificationTestFixtureStub, skipped);\n }\n\n for (const controller of notificationControllers) {\n await writeModuleFile(`controllers/${controller.file}.ts`, controller.stub, skipped);\n\n if (withSpecs) {\n await writeModuleFile(\n `controllers/${controller.file}.spec.ts`,\n notificationControllerSpecStub(controller.method, controller.path),\n skipped,\n );\n }\n }\n\n await writeModuleFile(\"routes.ts\", notificationRoutesStub, skipped);\n\n if (skipped.length > 0) {\n console.log(\n `${colors.yellowBright(\"Kept your existing files\")} (not overwritten): ${skipped\n .map((file) => `src/app/notifications/${file}`)\n .join(\", \")}`,\n );\n }\n}\n\nexport const notificationsFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds src/app/notifications: the Notification model + migration, a resource, a validated list query, and the recipient-scoped list / unread-count / mark-read / mark-all-read endpoints (one controller per file, a spec each when tests are set up)\",\n // The ejected config wires a `mail` channel by default (needs nodemailer,\n // via the `mail` feature); the scaffolded routes are gated by\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\n requires: [\"mail\"],\n dependencies: {\n \"@warlock.js/notifications\": INSTALLED_WARLOCK_VERSION,\n \"@warlock.js/auth\": INSTALLED_WARLOCK_VERSION,\n },\n ejectConfig: {\n content: notificationsConfigStub,\n name: \"notifications\",\n },\n onExecuting: completeNotificationsInstallation,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"notifications.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/notifications.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { dirname } from \"node:path\";\nimport {\n directoryExistsAsync,\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n recordSkippedFile,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { srcPath } from \"../../utils\";\nimport {\n listNotificationsControllerStub,\n listNotificationsSchemaStub,\n markAllNotificationsReadControllerStub,\n markNotificationReadControllerStub,\n markNotificationReadServiceStub,\n notificationControllerSpecStub,\n notificationTestFixtureStub,\n createNotificationLocalesStub,\n notificationMigrationStub,\n notificationModelStub,\n notificationResourceStub,\n notificationRoutesStub,\n notificationsConfigStub,\n notificationsRequestTypeStub,\n unreadNotificationsCountControllerStub,\n} from \"../stubs\";\nimport { migrationTimestamp } from \"./shared/migration-timestamp\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/**\n * The module's controllers, with the route each one serves. A spec per\n * controller is written next to it when the app has the test feature.\n */\nconst notificationControllers = [\n {\n file: \"list-notifications.controller\",\n stub: listNotificationsControllerStub,\n method: \"GET\",\n path: \"/notifications\",\n },\n {\n file: \"unread-notifications-count.controller\",\n stub: unreadNotificationsCountControllerStub,\n method: \"GET\",\n path: \"/notifications/unread-count\",\n },\n {\n file: \"mark-notification-read.controller\",\n stub: markNotificationReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/1/read\",\n },\n {\n file: \"mark-all-notifications-read.controller\",\n stub: markAllNotificationsReadControllerStub,\n method: \"PATCH\",\n path: \"/notifications/read-all\",\n },\n] as const;\n\n/** The model's import path in the ejected config, before and since 5.30. */\nconst MODEL_IMPORT = \"app/notifications/models/notification/notification.model\";\nconst LEGACY_MODEL_IMPORT = \"app/notifications/notification.model\";\n\n/**\n * Write one module file unless it already exists. A file the app already has\n * is the app's own code: it is reported and left alone, never overwritten.\n */\nasync function writeModuleFile(relativePath: string, content: string, skipped: string[]) {\n const path = srcPath(\"app/notifications\", relativePath);\n\n if (await fileExistsAsync(path)) {\n skipped.push(relativePath);\n recordSkippedFile(path, \"exists\");\n return;\n }\n\n await ensureDirectoryAsync(dirname(path));\n await putFileAsync(path, content);\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/${relativePath}`);\n}\n\n/**\n * Add the module's `notFound` message to an existing utils/locales.json, or\n * create the file. Existing keys win; a file that is not a JSON object, or\n * that renames its group, is left for the app to update by hand.\n */\nasync function registerNotificationLocales(skipped: string[]) {\n const notificationLocalesStub = createNotificationLocalesStub();\n const localesPath = srcPath(\"app/notifications/utils/locales.json\");\n\n if (!(await fileExistsAsync(localesPath))) {\n await writeModuleFile(\"utils/locales.json\", notificationLocalesStub, skipped);\n return;\n }\n\n const defaults = JSON.parse(notificationLocalesStub) as Record<string, unknown>;\n let dictionary: Record<string, unknown>;\n\n try {\n const parsed: unknown = JSON.parse(await getFileAsync(localesPath));\n\n if (parsed === null || Array.isArray(parsed) || typeof parsed !== \"object\") throw new Error();\n\n dictionary = parsed as Record<string, unknown>;\n } catch {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON is invalid; add notFound to src/app/notifications/utils/locales.json manually.`,\n );\n return;\n }\n\n // A root `$group` renames the file's namespace, so the service's\n // t(\"notifications.notFound\") would not find a key merged here.\n if (dictionary.$group !== undefined && dictionary.$group !== \"notifications\") {\n console.log(\n `${colors.yellowBright(\"notifications\")} locale JSON declares a different $group; add notifications.notFound manually.`,\n );\n return;\n }\n\n const missing = Object.keys(defaults).filter((key) => dictionary[key] === undefined);\n const existing = dictionary.notFound;\n const translations = defaults.notFound;\n if (\n existing &&\n typeof existing === \"object\" &&\n !Array.isArray(existing) &&\n translations &&\n typeof translations === \"object\" &&\n !Array.isArray(translations)\n ) {\n const current = existing as Record<string, unknown>;\n for (const [code, fallback] of Object.entries(translations)) {\n if (current[code] !== undefined) continue;\n current[code] = fallback;\n missing.push(`notFound.${code}`);\n }\n }\n\n if (missing.length === 0) return;\n\n for (const key of Object.keys(defaults)) {\n if (dictionary[key] === undefined) dictionary[key] = defaults[key];\n }\n\n await putFileAsync(localesPath, `${JSON.stringify(dictionary, null, 2)}\\n`);\n console.log(\n `${colors.green(\"✓\")} Added ${missing.join(\", \")} to src/app/notifications/utils/locales.json`,\n );\n}\n\n/**\n * The config ejected just before this runs imports the 5.30 model path. In an\n * app scaffolded by 5.29 the model is still at the module root, so point the\n * import there instead of at a file that does not exist.\n */\nasync function pointConfigAtLegacyModel() {\n const configPath = srcPath(\"config/notifications.ts\");\n\n if (!(await fileExistsAsync(configPath))) return;\n\n const config = await getFileAsync(configPath);\n\n if (!config.includes(`\"${MODEL_IMPORT}\"`)) return;\n\n await putFileAsync(configPath, config.replace(`\"${MODEL_IMPORT}\"`, `\"${LEGACY_MODEL_IMPORT}\"`));\n console.log(\n `${colors.green(\"✓\")} src/config/notifications.ts imports the existing model at ${LEGACY_MODEL_IMPORT}`,\n );\n}\n\nasync function completeNotificationsInstallation(_options: CommandActionData) {\n const modelPath = srcPath(\"app/notifications/models/notification/notification.model.ts\");\n // Where 5.29 and earlier wrote the model.\n const legacyModelPath = srcPath(\"app/notifications/notification.model.ts\");\n\n // The model file is the sentinel for \"notifications already scaffolded\" —\n // its presence means the migration was created too (timestamped, so we must\n // not re-emit a duplicate on a second run).\n if (await fileExistsAsync(legacyModelPath)) {\n if (!(await fileExistsAsync(modelPath))) await pointConfigAtLegacyModel();\n\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded (5.29 layout), skipping...`,\n );\n return;\n }\n\n if (await fileExistsAsync(modelPath)) {\n console.log(`${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping...`);\n return;\n }\n\n const skipped: string[] = [];\n // An app that already has its own notifications module may keep its model\n // anywhere in it; a second Notification model would collide with it.\n const moduleExists = await directoryExistsAsync(srcPath(\"app/notifications\"));\n\n // 1. Model + migration under models/notification/, like every other module.\n // The migration's MM-DD-YYYY_HH-MM-SS prefix lets cascade infer its\n // createdAt and order it deterministically.\n if (moduleExists) {\n console.log(\n `${colors.yellowBright(\"src/app/notifications\")} already exists: not adding a Notification model or migration. config/notifications.ts must import your model.`,\n );\n } else {\n await writeModuleFile(\n \"models/notification/notification.model.ts\",\n notificationModelStub,\n skipped,\n );\n await writeModuleFile(\n `models/notification/migrations/${migrationTimestamp()}-notification.migration.ts`,\n notificationMigrationStub,\n skipped,\n );\n }\n\n // 2. One responsibility per file: output resource, validated list query,\n // guarded request type, service, translations.\n await writeModuleFile(\"resources/notification.resource.ts\", notificationResourceStub, skipped);\n await writeModuleFile(\n \"schema/list-notifications.schema.ts\",\n listNotificationsSchemaStub,\n skipped,\n );\n await writeModuleFile(\n \"types/notifications-request.type.ts\",\n notificationsRequestTypeStub,\n skipped,\n );\n await writeModuleFile(\n \"services/mark-notification-read.service.ts\",\n markNotificationReadServiceStub,\n skipped,\n );\n await registerNotificationLocales(skipped);\n\n // 3. HTTP surface — list, unread count, mark read, mark all read, one\n // controller per file, gated by authMiddleware in routes.ts.\n const withSpecs = await fileExistsAsync(srcPath(\"test-setup.ts\"));\n if (withSpecs) {\n await writeModuleFile(\"tests/notifications.fixture.ts\", notificationTestFixtureStub, skipped);\n }\n\n for (const controller of notificationControllers) {\n await writeModuleFile(`controllers/${controller.file}.ts`, controller.stub, skipped);\n\n if (withSpecs) {\n await writeModuleFile(\n `controllers/${controller.file}.spec.ts`,\n notificationControllerSpecStub(controller.method, controller.path),\n skipped,\n );\n }\n }\n\n await writeModuleFile(\"routes.ts\", notificationRoutesStub, skipped);\n\n if (skipped.length > 0) {\n console.log(\n `${colors.yellowBright(\"Kept your existing files\")} (not overwritten): ${skipped\n .map((file) => `src/app/notifications/${file}`)\n .join(\", \")}`,\n );\n }\n}\n\nexport const notificationsFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds src/app/notifications: the Notification model + migration, a resource, a validated list query, and the recipient-scoped list / unread-count / mark-read / mark-all-read endpoints (one controller per file, a spec each when tests are set up)\",\n // The ejected config wires a `mail` channel by default (needs nodemailer,\n // via the `mail` feature); the scaffolded routes are gated by\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\n requires: [\"mail\"],\n dependencies: {\n \"@warlock.js/notifications\": INSTALLED_WARLOCK_VERSION,\n \"@warlock.js/auth\": INSTALLED_WARLOCK_VERSION,\n },\n ejectConfig: {\n content: notificationsConfigStub,\n name: \"notifications\",\n },\n onExecuting: completeNotificationsInstallation,\n};\n"],"mappings":"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"}
|
|
@@ -53,31 +53,66 @@ export async function teardown() {
|
|
|
53
53
|
* setupTest() owns closing it in the same runtime context.
|
|
54
54
|
*/
|
|
55
55
|
import { setupTest, teardownTest } from "@warlock.js/core/tests";
|
|
56
|
-
import { afterAll } from "vitest";
|
|
56
|
+
import { afterAll, expect } from "vitest";
|
|
57
57
|
|
|
58
|
-
await setupTest({ importModule: (file) => import(file) });
|
|
58
|
+
await setupTest({ importModule: (file) => import(file), file: expect.getState().testPath });
|
|
59
59
|
|
|
60
60
|
afterAll(teardownTest);
|
|
61
61
|
`);
|
|
62
62
|
console.log(`${colors.green("✓")} Created src/test-setup.ts`);
|
|
63
63
|
} else recordSkippedFile(testSetupPath, "exists");
|
|
64
|
+
const runnerPath = srcPath("test-runner.ts");
|
|
65
|
+
if (!await fileExistsAsync(runnerPath)) await putFileAsync(runnerPath, `import { configureTestWorker } from "@warlock.js/core/tests";
|
|
66
|
+
import { VitestTestRunner } from "vitest/runners";
|
|
67
|
+
|
|
68
|
+
// Vitest 4.1 awaits onCleanupWorkerContext callbacks before recycling the fork.
|
|
69
|
+
export default class WarlockTestRunner extends VitestTestRunner {
|
|
70
|
+
public constructor(config: ConstructorParameters<typeof VitestTestRunner>[0]) {
|
|
71
|
+
super(config);
|
|
72
|
+
configureTestWorker(
|
|
73
|
+
{ pool: this.pool, watch: config.watch, isolate: config.isolate },
|
|
74
|
+
(cleanup) => this.onCleanupWorkerContext(cleanup),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`);
|
|
79
|
+
else recordSkippedFile(runnerPath, "exists");
|
|
64
80
|
const viteConfigPath = rootPath("vite.config.ts");
|
|
65
81
|
if (!await fileExistsAsync(viteConfigPath)) {
|
|
66
|
-
await putFileAsync(viteConfigPath, `import { lowerStage3Decorators } from "@warlock.js/core/vite";
|
|
82
|
+
await putFileAsync(viteConfigPath, `import { lowerStage3Decorators, recommendedTestWorkers, TestSetupReporter } from "@warlock.js/core/vite";
|
|
67
83
|
import mongezVite from "@mongez/vite";
|
|
68
84
|
import { defineConfig } from "vitest/config";
|
|
69
85
|
|
|
70
86
|
export default defineConfig({
|
|
71
|
-
// lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)
|
|
72
|
-
// decorators with esbuild before oxc / the SSR rewrite can mangle them, so
|
|
73
|
-
// decorated Cascade models load under Vitest.
|
|
74
87
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
75
88
|
test: {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
maxWorkers: recommendedTestWorkers(),
|
|
90
|
+
reporters: ["default", new TestSetupReporter()],
|
|
91
|
+
projects: [
|
|
92
|
+
{
|
|
93
|
+
extends: true,
|
|
94
|
+
test: {
|
|
95
|
+
name: "unit",
|
|
96
|
+
environment: "node",
|
|
97
|
+
include: ["src/**/*.spec.ts"],
|
|
98
|
+
exclude: ["src/**/tests/**"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
extends: true,
|
|
103
|
+
test: {
|
|
104
|
+
name: "integration",
|
|
105
|
+
environment: "node",
|
|
106
|
+
pool: "forks",
|
|
107
|
+
// Required only when opting into tests.lifetime: "worker".
|
|
108
|
+
isolate: true,
|
|
109
|
+
runner: "./src/test-runner.ts",
|
|
110
|
+
globalSetup: "./src/test-global-setup.ts",
|
|
111
|
+
setupFiles: ["./src/test-setup.ts"],
|
|
112
|
+
include: ["src/**/*.test.ts", "src/**/tests/**/*.{test,spec}.ts", "tests/**/*.{test,spec}.ts"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
81
116
|
},
|
|
82
117
|
});
|
|
83
118
|
`);
|
|
@@ -96,8 +131,8 @@ const testFeature = {
|
|
|
96
131
|
devDependencies: {
|
|
97
132
|
"@mongez/vite": "^2.0.4",
|
|
98
133
|
vite: VITE_RANGE,
|
|
99
|
-
vitest: "^4.1.
|
|
100
|
-
"@vitest/coverage-v8": "^4.1.
|
|
134
|
+
vitest: "^4.1.10",
|
|
135
|
+
"@vitest/coverage-v8": "^4.1.10"
|
|
101
136
|
}
|
|
102
137
|
};
|
|
103
138
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/test.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, putFileAsync, recordSkippedFile } from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\nimport { VITE_RANGE } from \"./web.feature\";\n\nasync function completeTestInstallation(options: CommandActionData) {\n // Create test-global-setup.ts (runs once before all tests)\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\n\n if (!testGlobalSetupExists) {\n await putFileAsync(\n testGlobalSetupPath,\n `/**\n * Global Test Setup\n *\n * Runs ONCE before all test workers.\n * Starts the HTTP server for integration tests.\n *\n * port: 0 lets the OS pick a free port, so tests run while \\`warlock dev\\`\n * holds the configured one; the test helpers (testGet, actingAs, ...) are\n * told the bound port automatically.\n */\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\n\nexport async function setup() {\n await startHttpTestServer({ port: 0 });\n}\n\nexport async function teardown() {\n await stopHttpTestServer();\n}\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\n } else {\n recordSkippedFile(testGlobalSetupPath, \"exists\");\n }\n\n // Create test-setup.ts (runs before EVERY test file)\n const testSetupPath = srcPath(\"test-setup.ts\");\n const testSetupExists = await fileExistsAsync(testSetupPath);\n\n if (!testSetupExists) {\n await putFileAsync(\n testSetupPath,\n `/**\n * Test Setup - runs before EVERY test file\n *\n * Vitest runs setupFiles before each test file and rebuilds the module\n * registry with it, so this pair boots and closes the test runtime once per\n * test file.\n *\n * setupTest() gets no connectors option on purpose: an explicit connectors\n * value outranks tests.connectors from src/config/tests.ts, so passing one\n * here would erase your project config. Omitting it leaves the config in\n * charge.\n *\n * importModule makes Warlock load your app modules (config, routes, ...)\n * through THIS file's import(), which Vitest handles. Tests and the framework\n * then share one copy of every class, so \"owner instanceof User\" works.\n *\n * afterAll(teardownTest) is the other half of the pair: whoever calls\n * setupTest() owns closing it in the same runtime context.\n */\nimport { setupTest, teardownTest } from \"@warlock.js/core/tests\";\nimport { afterAll } from \"vitest\";\n\nawait setupTest({ importModule: (file) => import(file) });\n\nafterAll(teardownTest);\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\n } else {\n recordSkippedFile(testSetupPath, \"exists\");\n }\n\n // Create vite.config.ts\n const viteConfigPath = rootPath(\"vite.config.ts\");\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\n\n if (!viteConfigExists) {\n await putFileAsync(\n viteConfigPath,\n `import { lowerStage3Decorators } from \"@warlock.js/core/vite\";\nimport mongezVite from \"@mongez/vite\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n
|
|
1
|
+
{"version":3,"file":"test.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/test.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, putFileAsync, recordSkippedFile } from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\nimport { VITE_RANGE } from \"./web.feature\";\n\nasync function completeTestInstallation(options: CommandActionData) {\n // Create test-global-setup.ts (runs once before all tests)\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\n\n if (!testGlobalSetupExists) {\n await putFileAsync(\n testGlobalSetupPath,\n `/**\n * Global Test Setup\n *\n * Runs ONCE before all test workers.\n * Starts the HTTP server for integration tests.\n *\n * port: 0 lets the OS pick a free port, so tests run while \\`warlock dev\\`\n * holds the configured one; the test helpers (testGet, actingAs, ...) are\n * told the bound port automatically.\n */\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\n\nexport async function setup() {\n await startHttpTestServer({ port: 0 });\n}\n\nexport async function teardown() {\n await stopHttpTestServer();\n}\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\n } else {\n recordSkippedFile(testGlobalSetupPath, \"exists\");\n }\n\n // Create test-setup.ts (runs before EVERY test file)\n const testSetupPath = srcPath(\"test-setup.ts\");\n const testSetupExists = await fileExistsAsync(testSetupPath);\n\n if (!testSetupExists) {\n await putFileAsync(\n testSetupPath,\n `/**\n * Test Setup - runs before EVERY test file\n *\n * Vitest runs setupFiles before each test file and rebuilds the module\n * registry with it, so this pair boots and closes the test runtime once per\n * test file.\n *\n * setupTest() gets no connectors option on purpose: an explicit connectors\n * value outranks tests.connectors from src/config/tests.ts, so passing one\n * here would erase your project config. Omitting it leaves the config in\n * charge.\n *\n * importModule makes Warlock load your app modules (config, routes, ...)\n * through THIS file's import(), which Vitest handles. Tests and the framework\n * then share one copy of every class, so \"owner instanceof User\" works.\n *\n * afterAll(teardownTest) is the other half of the pair: whoever calls\n * setupTest() owns closing it in the same runtime context.\n */\nimport { setupTest, teardownTest } from \"@warlock.js/core/tests\";\nimport { afterAll, expect } from \"vitest\";\n\nawait setupTest({ importModule: (file) => import(file), file: expect.getState().testPath });\n\nafterAll(teardownTest);\n`,\n );\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\n } else {\n recordSkippedFile(testSetupPath, \"exists\");\n }\n\n const runnerPath = srcPath(\"test-runner.ts\");\n if (!(await fileExistsAsync(runnerPath))) {\n await putFileAsync(\n runnerPath,\n `import { configureTestWorker } from \"@warlock.js/core/tests\";\nimport { VitestTestRunner } from \"vitest/runners\";\n\n// Vitest 4.1 awaits onCleanupWorkerContext callbacks before recycling the fork.\nexport default class WarlockTestRunner extends VitestTestRunner {\n public constructor(config: ConstructorParameters<typeof VitestTestRunner>[0]) {\n super(config);\n configureTestWorker(\n { pool: this.pool, watch: config.watch, isolate: config.isolate },\n (cleanup) => this.onCleanupWorkerContext(cleanup),\n );\n }\n}\n`,\n );\n } else {\n recordSkippedFile(runnerPath, \"exists\");\n }\n\n // Create vite.config.ts\n const viteConfigPath = rootPath(\"vite.config.ts\");\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\n\n if (!viteConfigExists) {\n await putFileAsync(\n viteConfigPath,\n `import { lowerStage3Decorators, recommendedTestWorkers, TestSetupReporter } from \"@warlock.js/core/vite\";\nimport mongezVite from \"@mongez/vite\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n plugins: [lowerStage3Decorators(), mongezVite()],\n test: {\n maxWorkers: recommendedTestWorkers(),\n reporters: [\"default\", new TestSetupReporter()],\n projects: [\n {\n extends: true,\n test: {\n name: \"unit\",\n environment: \"node\",\n include: [\"src/**/*.spec.ts\"],\n exclude: [\"src/**/tests/**\"],\n },\n },\n {\n extends: true,\n test: {\n name: \"integration\",\n environment: \"node\",\n pool: \"forks\",\n // Required only when opting into tests.lifetime: \"worker\".\n isolate: true,\n runner: \"./src/test-runner.ts\",\n globalSetup: \"./src/test-global-setup.ts\",\n setupFiles: [\"./src/test-setup.ts\"],\n include: [\"src/**/*.test.ts\", \"src/**/tests/**/*.{test,spec}.ts\", \"tests/**/*.{test,spec}.ts\"],\n },\n },\n ],\n },\n});\n`,\n );\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\n } else {\n recordSkippedFile(viteConfigPath, \"exists\");\n }\n}\n\nexport const testFeature: FeatureDefinition = {\n description: \"Installs warlock test for testing\",\n onExecuting: completeTestInstallation,\n script: {\n test: \"vitest run\",\n \"test:coverage\": \"vitest run --coverage\",\n \"test:ui\": \"vitest --ui\",\n \"test:watch\": \"vitest --watch\",\n },\n devDependencies: {\n \"@mongez/vite\": \"^2.0.4\",\n vite: VITE_RANGE,\n vitest: \"^4.1.10\",\n \"@vitest/coverage-v8\": \"^4.1.10\",\n },\n};\n"],"mappings":";;;;;;;;AAOA,eAAe,yBAAyB,SAA4B;CAElE,MAAM,sBAAsB,QAAQ,sBAAsB;CAG1D,IAAI,CAAC,MAF+B,gBAAgB,mBAAmB,GAE3C;EAC1B,MAAM,aACJ,qBACA;;;;;;;;;;;;;;;;;;;CAoBF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kCAAkC;CACrE,OACE,kBAAkB,qBAAqB,QAAQ;CAIjD,MAAM,gBAAgB,QAAQ,eAAe;CAG7C,IAAI,CAAC,MAFyB,gBAAgB,aAAa,GAErC;EACpB,MAAM,aACJ,eACA;;;;;;;;;;;;;;;;;;;;;;;;;CA0BF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2BAA2B;CAC9D,OACE,kBAAkB,eAAe,QAAQ;CAG3C,MAAM,aAAa,QAAQ,gBAAgB;CAC3C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GACpC,MAAM,aACJ,YACA;;;;;;;;;;;;;CAcF;MAEA,kBAAkB,YAAY,QAAQ;CAIxC,MAAM,iBAAiB,SAAS,gBAAgB;CAGhD,IAAI,CAAC,MAF0B,gBAAgB,cAAc,GAEtC;EACrB,MAAM,aACJ,gBACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,wBAAwB;CAC3D,OACE,kBAAkB,gBAAgB,QAAQ;AAE9C;AAEA,MAAa,cAAiC;CAC5C,aAAa;CACb,aAAa;CACb,QAAQ;EACN,MAAM;EACN,iBAAiB;EACjB,WAAW;EACX,cAAc;CAChB;CACA,iBAAiB;EACf,gBAAgB;EAChB,MAAM;EACN,QAAQ;EACR,uBAAuB;CACzB;AACF"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import baseConfig from "@mongez/config";
|
|
2
|
+
|
|
1
3
|
//#region ../../packages/core/src/generations/stubs.ts
|
|
2
4
|
const accessConfigStub = `import { type AccessConfigurations } from "@warlock.js/access";
|
|
3
5
|
import { DatabaseAccessResolver } from "app/access/services/access-resolver";
|
|
@@ -655,11 +657,13 @@ router.group({ middleware: [authMiddleware([])] }, () => {
|
|
|
655
657
|
notifications.nest("/:id/read").patch(markNotificationReadController);
|
|
656
658
|
});
|
|
657
659
|
`;
|
|
658
|
-
/**
|
|
659
|
-
|
|
660
|
-
|
|
660
|
+
/** Build module locale JSON, with English as the explicit translation fallback. */
|
|
661
|
+
function createNotificationLocalesStub() {
|
|
662
|
+
const configured = baseConfig.get("app.localeCodes");
|
|
663
|
+
const codes = Array.isArray(configured) && configured.length > 0 ? configured.filter((code) => typeof code === "string") : ["en"];
|
|
664
|
+
return JSON.stringify({ notFound: Object.fromEntries(codes.map((code) => [code, "Notification not found"])) }, null, 2) + "\n";
|
|
661
665
|
}
|
|
662
|
-
|
|
666
|
+
const notificationLocalesStub = createNotificationLocalesStub();
|
|
663
667
|
/** Shared fixtures for the generated signed-in notification controller specs. */
|
|
664
668
|
const notificationTestFixtureStub = `import type { Auth } from "@warlock.js/auth";
|
|
665
669
|
import type { ChildModel } from "@warlock.js/cascade";
|
|
@@ -1172,5 +1176,5 @@ function ContactMessage() {
|
|
|
1172
1176
|
`;
|
|
1173
1177
|
|
|
1174
1178
|
//#endregion
|
|
1175
|
-
export { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, aiConfigStub, communicatorsConfigStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub,
|
|
1179
|
+
export { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, aiConfigStub, communicatorsConfigStub, createNotificationLocalesStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, socketConfigStub, unreadNotificationsCountControllerStub, webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeSetupStub, webRootSetupStub, webRootStub };
|
|
1176
1180
|
//# sourceMappingURL=stubs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../../packages/core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/models/notification/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\n/** `src/app/notifications/models/notification/notification.model.ts`. */\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\nimport { NotificationResource } from \"../../resources/notification.resource\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n *\n * user_id accepts a string or an integer so it matches whichever id type your\n * users use (and the idType you pass to notificationColumns in the migration).\n * Narrow it to that one type.\n */\nconst notificationSchema = v.object({\n user_id: v.union([v.string(), v.int()]),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /** Every response that returns a notification is shaped by this resource. */\n public static resource = NotificationResource;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration, the accessors and the resource\n * all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\n/** The notifications migration, under `models/notification/migrations/`. */\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. The recipient column is a UUID unless\n * you pass { idType: \"integer\" } (or \"bigint\" / \"string\") to match your users.\n * Spread it to add your own columns (and mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification, { idType: \"integer\" }),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\n/** `src/app/notifications/resources/notification.resource.ts` — the output shape. */\nexport const notificationResourceStub = `import { defineResource } from \"@warlock.js/core\";\n\n/**\n * What every notification endpoint returns, whatever columns the model holds.\n * readAt reads the default read_at column; change it if you change the model's\n * columnMap.\n */\nexport const NotificationResource = defineResource({\n schema: {\n // Passed through as stored: a UUID string or an integer, per the migration.\n id: (id: string | number) => id,\n type: \"string\",\n title: \"string\",\n body: \"string?\",\n payload: \"object?\",\n readAt: [\"read_at\", \"date?\"],\n createdAt: \"date?\",\n },\n});\n`;\n\n/** `src/app/notifications/schema/list-notifications.schema.ts` — the validated list query. */\nexport const listNotificationsSchemaStub = `import { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Query for GET /notifications. Only these keys reach the store (others, such\n * as a cache-busting \\`_\\`, are dropped rather than rejected), and limit is\n * capped so one request cannot read every row. Query values arrive as strings,\n * so the numbers and the flag are coerced.\n */\nexport const listNotificationsSchema = v.object({\n page: v.int().coerce().min(1).optional(),\n limit: v.int().coerce().min(1).max(50).optional(),\n type: v.string().optional(),\n unread: v.boolean().coerce().optional(),\n}).stripUnknown();\n\nexport type ListNotificationsSchema = Infer<typeof listNotificationsSchema>;\n`;\n\n/** `src/app/notifications/types/notifications-request.type.ts` — the guarded request. */\nexport const notificationsRequestTypeStub = `import type { Request, RequestHandler, RequestLocals } from \"@warlock.js/core\";\nimport type { Id } from \"@warlock.js/notifications\";\n\n/**\n * A request that passed authMiddleware, so locals.user is always set. Only the\n * user's id is declared because that is all the inApp store needs. If your\n * app has its own guarded request type, use it instead.\n */\nexport type NotificationsRequest<Payload = unknown> = Request<Payload> & {\n locals: RequestLocals & { user: { id: Id } };\n};\n\nexport type NotificationsRequestHandler<Payload = unknown> = RequestHandler<\n NotificationsRequest<Payload>\n>;\n`;\n\n/** `src/app/notifications/services/mark-notification-read.service.ts`. */\nexport const markNotificationReadServiceStub = `import { ResourceNotFoundError, t } from \"@warlock.js/core\";\nimport { type Id, inApp } from \"@warlock.js/notifications\";\n\n/**\n * Mark one of the user's notifications read and return it. inApp is scoped to\n * the recipient, so another user's id changes nothing and answers 404 here.\n */\nexport async function markNotificationReadService(userId: Id, notificationId: Id) {\n await inApp.markAsRead(userId, notificationId);\n\n const notification = await inApp.find(userId, notificationId);\n\n if (!notification) {\n throw new ResourceNotFoundError(t(\"notifications.notFound\"));\n }\n\n return notification;\n}\n`;\n\n/** `src/app/notifications/controllers/list-notifications.controller.ts`. */\nexport const listNotificationsControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport {\n type ListNotificationsSchema,\n listNotificationsSchema,\n} from \"../schema/list-notifications.schema\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications — the user's notifications, most recent first. */\nexport const listNotificationsController: NotificationsRequestHandler<\n ListNotificationsSchema\n> = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(request.locals.user.id, request.validated());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\nlistNotificationsController.validation = {\n schema: listNotificationsSchema,\n validating: [\"query\"],\n};\n`;\n\n/** `src/app/notifications/controllers/unread-notifications-count.controller.ts`. */\nexport const unreadNotificationsCountControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(request.locals.user.id);\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n`;\n\n/** `src/app/notifications/controllers/mark-notification-read.controller.ts`. */\nexport const markNotificationReadControllerStub = `import { markNotificationReadService } from \"../services/mark-notification-read.service\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/:id/read — mark one read and return it. */\nexport const markNotificationReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n // Route params are strings. A numeric id is an integer id (the default\n // MongoDB stack); anything else, such as a UUID, stays a string.\n const rawId = request.string(\"id\");\n const id = /^\\\\d+$/.test(rawId) ? Number(rawId) : rawId;\n\n const notification = await markNotificationReadService(request.locals.user.id, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n`;\n\n/** `src/app/notifications/controllers/mark-all-notifications-read.controller.ts`. */\nexport const markAllNotificationsReadControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(request.locals.user.id);\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n`;\n\n/** `src/app/notifications/routes.ts` — the four read endpoints behind `authMiddleware`. */\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport { listNotificationsController } from \"./controllers/list-notifications.controller\";\nimport { markAllNotificationsReadController } from \"./controllers/mark-all-notifications-read.controller\";\nimport { markNotificationReadController } from \"./controllers/mark-notification-read.controller\";\nimport { unreadNotificationsCountController } from \"./controllers/unread-notifications-count.controller\";\n\n/**\n * The signed-in user's own notifications.\n *\n * Notifications come from domain events, so there is no create. Dismissing is\n * left out on purpose; add it when the product needs it (inApp.dismiss).\n *\n * authMiddleware([]) admits any signed-in user. To limit who may read\n * notifications, add a permission gate to the group:\n *\n * import { gate } from \"@warlock.js/access\";\n *\n * router.group({ middleware: [authMiddleware([]), gate(\"notifications.read\")] }, ...);\n */\nrouter.group({ middleware: [authMiddleware([])] }, () => {\n const notifications = router.route(\"/notifications\");\n\n notifications.list(listNotificationsController);\n notifications.nest(\"/unread-count\").get(unreadNotificationsCountController);\n notifications.nest(\"/read-all\").patch(markAllNotificationsReadController);\n notifications.nest(\"/:id/read\").patch(markNotificationReadController);\n});\n`;\n\n/** `src/app/notifications/utils/locales.json` — the module's translations. */\nexport const notificationLocalesStub = `{\n \"notFound\": { \"en\": \"Notification not found\" }\n}\n`;\n\n/** Shared fixtures for the generated signed-in notification controller specs. */\nexport const notificationTestFixtureStub = `import type { Auth } from \"@warlock.js/auth\";\nimport type { ChildModel } from \"@warlock.js/cascade\";\nimport { config } from \"@warlock.js/core\";\nimport { actingAs, type AuthenticatedTestClient } from \"@warlock.js/core/tests\";\nimport { randomUUID } from \"node:crypto\";\nimport { Notification } from \"../models/notification/notification.model\";\n\n/** Isolated recipients and rows; adjust user fields if your auth model requires more. */\nexport async function createNotificationTestFixture() {\n const User = config.key<ChildModel<Auth>>(\"auth.userType.user\");\n if (!User) throw new Error(\"Notification specs need the configured auth.userType.user model.\");\n const users: Auth[] = [];\n const clients: AuthenticatedTestClient[] = [];\n const rows: Notification[] = [];\n\n async function cleanup() {\n const credentials = await Promise.allSettled(\n clients.splice(0).map((client) => client.dispose()),\n );\n const notifications = await Promise.allSettled(\n rows.splice(0).map((row) => row.destroy({ strategy: \"permanent\" })),\n );\n const recipients = await Promise.allSettled(\n users.splice(0).map((user) => user.destroy({ strategy: \"permanent\" })),\n );\n const errors = [...credentials, ...notifications, ...recipients]\n .filter((result): result is PromiseRejectedResult => result.status === \"rejected\")\n .map((result) => result.reason);\n if (errors.length) throw new AggregateError(errors, \"Notification fixture cleanup failed.\");\n }\n\n const createUser = async () => {\n const user = await User.create({\n name: \"Notification test user\",\n email: \"notifications-\" + randomUUID() + \"@example.test\",\n password: \"test-password\",\n });\n users.push(user);\n return user;\n };\n const signIn = async (user: Auth) => {\n const client = await actingAs(user);\n clients.push(client);\n return client;\n };\n const createNotification = async (recipientId: string | number, title: string, readAt?: Date) => {\n const row = await Notification.create({\n user_id: recipientId,\n type: \"test\",\n title,\n read_at: readAt,\n });\n rows.push(row);\n return row;\n };\n\n try {\n const user = await createUser();\n const otherUser = await createUser();\n const client = await signIn(user);\n const otherClient = await signIn(otherUser);\n const own = await createNotification(user.id, \"Own unread notification\");\n const alreadyRead = await createNotification(user.id, \"Own read notification\", new Date());\n const foreign = await createNotification(otherUser.id, \"Other user's notification\");\n await createNotification(otherUser.id, \"Other user's second notification\");\n return { user, otherUser, client, otherClient, own, alreadyRead, foreign, cleanup };\n } catch (error) {\n // Preserve the setup failure after attempting every owned resource's cleanup.\n await cleanup().catch(() => undefined);\n throw error;\n }\n}\n`;\n\n/** A spec for each generated controller, including authenticated recipient isolation. */\nexport function notificationControllerSpecStub(method: \"GET\" | \"PATCH\", path: string): string {\n const helper = method === \"GET\" ? \"testGet\" : \"testPatch\";\n let signedInCases: string;\n if (path === \"/notifications\") {\n signedInCases = ` it(\"lists only the signed-in recipient's rows, ignoring a forged recipient query\", async () => {\n const response = await fixture.client.get(\n \"/notifications?recipientId=\" + fixture.otherUser.id,\n );\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual(\n expect.arrayContaining([fixture.own.id, fixture.alreadyRead.id]),\n );\n expect(body.notifications).toHaveLength(2);\n });\n\n it(\"applies unread and paging query filters\", async () => {\n const response = await fixture.client.get(\"/notifications?unread=true&limit=1\");\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual([fixture.own.id]);\n });\n\n it(\"rejects a list limit above the schema's cap\", async () => {\n expect((await fixture.client.get(\"/notifications?limit=999\")).status).toBe(422);\n });`;\n } else if (path === \"/notifications/unread-count\") {\n signedInCases = ` it(\"counts only this recipient's unread notifications\", async () => {\n const own = await expectJson<{ count: number }>(\n await fixture.client.get(\"/notifications/unread-count\"),\n );\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(own.count).toBe(1);\n expect(other.count).toBe(2);\n });`;\n } else if (path === \"/notifications/read-all\") {\n signedInCases = ` it(\"marks only this recipient's unread rows and leaves another recipient unchanged\", async () => {\n const body = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(body.count).toBe(1);\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(other.count).toBe(2);\n const again = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(again.count).toBe(0);\n });`;\n } else {\n signedInCases = ` it(\"marks its own notification read and returns it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.own.id + \"/read\");\n const body = await expectJson<{\n notification: { id: string | number; readAt?: string | null };\n }>(response);\n expect(body.notification.id).toBe(fixture.own.id);\n expect(body.notification.readAt).toBeTruthy();\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n });\n\n it(\"answers 404 for another recipient's notification without changing it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.foreign.id + \"/read\");\n expect(response.status).toBe(404);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n });`;\n }\n const modelImport =\n method === \"PATCH\"\n ? 'import { Notification } from \"../models/notification/notification.model\";\\n'\n : \"\";\n return `import { expectJson, ${helper} } from \"@warlock.js/core/tests\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n${modelImport}import { createNotificationTestFixture } from \"../tests/notifications.fixture\";\n\ndescribe(\"${method} ${path}\", () => {\n it(\"answers 401 without a signed-in user\", async () => {\n const response = await ${helper}(\"${path}\");\n expect(response.status).toBe(401);\n });\n\n describe(\"signed in\", () => {\n let fixture: Awaited<ReturnType<typeof createNotificationTestFixture>> | undefined;\n beforeEach(async () => {\n fixture = await createNotificationTestFixture();\n });\n afterEach(async () => {\n const current = fixture;\n fixture = undefined;\n await current?.cleanup();\n });\n\n${signedInCases.replaceAll(\"fixture.\", \"fixture!.\")}\n });\n});\n`;\n}\n\n/** `src/web/root.setup.ts` — document policy. */\nexport const webRootSetupStub = `import type { RootConfig } from \"@warlock.js/web\";\n\nexport const config: RootConfig = { strictMode: true };\n`;\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * document component stays here; request policy and an app-level loader\n * belong in `root.setup.ts`. Named ErrorBoundary components stay in the UI.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n <Head /> also renders the viewport tag (width=device-width,\n initial-scale=1). Change it with \\`metadata.viewport\\`,\n or set it to false to leave it out; do not add a second one here.\n */}\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { v, type Infer } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController, { public: true });\n`;\n\n/** `src/web/index.setup.ts` — page policy and universal registration. */\nexport const webHomeSetupStub = `import { extend } from \"@mongez/localization\";\nimport type { PageConfig } from \"@warlock.js/web\";\n\nexport const config: PageConfig = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n};\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/** Fetch data in index.setup.ts; use a type-only loader import for PageProps. */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;AAwBzC,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;AAsBxC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;AAmB3C,MAAa,+BAA+B;;;;;;;;;;;;;;;;;AAkB5C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;AAqB/C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;AAyB/C,MAAa,yCAAyC;;;;;;;;;;;;;;;;AAiBtD,MAAa,qCAAqC;;;;;;;;;;;;;;;;;;;;;AAsBlD,MAAa,yCAAyC;;;;;;;;;;;;;;;;AAiBtD,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BtC,MAAa,0BAA0B;;;;;AAMvC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2E3C,SAAgB,+BAA+B,QAAyB,MAAsB;CAC5F,MAAM,SAAS,WAAW,QAAQ,YAAY;CAC9C,IAAI;CACJ,IAAI,SAAS,kBACX,gBAAgB;;;;;;;;;;;;;;;;;;;;MAoBX,IAAI,SAAS,+BAClB,gBAAgB;;;;;;;;;;MAUX,IAAI,SAAS,2BAClB,gBAAgB;;;;;;;;;;;;;;;;MAiBhB,gBAAgB;;;;;;;;;;;;;;;CAoBlB,OAAO,wBAAwB,OAAO;;EAHpC,WAAW,UACP,kFACA,GAGM;;YAEF,OAAO,GAAG,KAAK;;6BAEE,OAAO,IAAI,KAAK;;;;;;;;;;;;;;;EAe3C,cAAc,WAAW,YAAY,WAAW,EAAE;;;;AAIpD;;AAGA,MAAa,mBAAmB;;;;;;;;;;;;AAahC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4D3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;AAOpC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0ChC,MAAa,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":["config"],"sources":["../../../../../../../../packages/core/src/generations/stubs.ts"],"sourcesContent":["import config from \"@mongez/config\";\n\nexport const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/models/notification/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\n/** `src/app/notifications/models/notification/notification.model.ts`. */\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\nimport { NotificationResource } from \"../../resources/notification.resource\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n *\n * user_id accepts a string or an integer so it matches whichever id type your\n * users use (and the idType you pass to notificationColumns in the migration).\n * Narrow it to that one type.\n */\nconst notificationSchema = v.object({\n user_id: v.union([v.string(), v.int()]),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /** Every response that returns a notification is shaped by this resource. */\n public static resource = NotificationResource;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration, the accessors and the resource\n * all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\n/** The notifications migration, under `models/notification/migrations/`. */\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. The recipient column is a UUID unless\n * you pass { idType: \"integer\" } (or \"bigint\" / \"string\") to match your users.\n * Spread it to add your own columns (and mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification, { idType: \"integer\" }),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\n/** `src/app/notifications/resources/notification.resource.ts` — the output shape. */\nexport const notificationResourceStub = `import { defineResource } from \"@warlock.js/core\";\n\n/**\n * What every notification endpoint returns, whatever columns the model holds.\n * readAt reads the default read_at column; change it if you change the model's\n * columnMap.\n */\nexport const NotificationResource = defineResource({\n schema: {\n // Passed through as stored: a UUID string or an integer, per the migration.\n id: (id: string | number) => id,\n type: \"string\",\n title: \"string\",\n body: \"string?\",\n payload: \"object?\",\n readAt: [\"read_at\", \"date?\"],\n createdAt: \"date?\",\n },\n});\n`;\n\n/** `src/app/notifications/schema/list-notifications.schema.ts` — the validated list query. */\nexport const listNotificationsSchemaStub = `import { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Query for GET /notifications. Only these keys reach the store (others, such\n * as a cache-busting \\`_\\`, are dropped rather than rejected), and limit is\n * capped so one request cannot read every row. Query values arrive as strings,\n * so the numbers and the flag are coerced.\n */\nexport const listNotificationsSchema = v.object({\n page: v.int().coerce().min(1).optional(),\n limit: v.int().coerce().min(1).max(50).optional(),\n type: v.string().optional(),\n unread: v.boolean().coerce().optional(),\n}).stripUnknown();\n\nexport type ListNotificationsSchema = Infer<typeof listNotificationsSchema>;\n`;\n\n/** `src/app/notifications/types/notifications-request.type.ts` — the guarded request. */\nexport const notificationsRequestTypeStub = `import type { Request, RequestHandler, RequestLocals } from \"@warlock.js/core\";\nimport type { Id } from \"@warlock.js/notifications\";\n\n/**\n * A request that passed authMiddleware, so locals.user is always set. Only the\n * user's id is declared because that is all the inApp store needs. If your\n * app has its own guarded request type, use it instead.\n */\nexport type NotificationsRequest<Payload = unknown> = Request<Payload> & {\n locals: RequestLocals & { user: { id: Id } };\n};\n\nexport type NotificationsRequestHandler<Payload = unknown> = RequestHandler<\n NotificationsRequest<Payload>\n>;\n`;\n\n/** `src/app/notifications/services/mark-notification-read.service.ts`. */\nexport const markNotificationReadServiceStub = `import { ResourceNotFoundError, t } from \"@warlock.js/core\";\nimport { type Id, inApp } from \"@warlock.js/notifications\";\n\n/**\n * Mark one of the user's notifications read and return it. inApp is scoped to\n * the recipient, so another user's id changes nothing and answers 404 here.\n */\nexport async function markNotificationReadService(userId: Id, notificationId: Id) {\n await inApp.markAsRead(userId, notificationId);\n\n const notification = await inApp.find(userId, notificationId);\n\n if (!notification) {\n throw new ResourceNotFoundError(t(\"notifications.notFound\"));\n }\n\n return notification;\n}\n`;\n\n/** `src/app/notifications/controllers/list-notifications.controller.ts`. */\nexport const listNotificationsControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport {\n type ListNotificationsSchema,\n listNotificationsSchema,\n} from \"../schema/list-notifications.schema\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications — the user's notifications, most recent first. */\nexport const listNotificationsController: NotificationsRequestHandler<\n ListNotificationsSchema\n> = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(request.locals.user.id, request.validated());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\nlistNotificationsController.validation = {\n schema: listNotificationsSchema,\n validating: [\"query\"],\n};\n`;\n\n/** `src/app/notifications/controllers/unread-notifications-count.controller.ts`. */\nexport const unreadNotificationsCountControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(request.locals.user.id);\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n`;\n\n/** `src/app/notifications/controllers/mark-notification-read.controller.ts`. */\nexport const markNotificationReadControllerStub = `import { markNotificationReadService } from \"../services/mark-notification-read.service\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/:id/read — mark one read and return it. */\nexport const markNotificationReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n // Route params are strings. A numeric id is an integer id (the default\n // MongoDB stack); anything else, such as a UUID, stays a string.\n const rawId = request.string(\"id\");\n const id = /^\\\\d+$/.test(rawId) ? Number(rawId) : rawId;\n\n const notification = await markNotificationReadService(request.locals.user.id, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n`;\n\n/** `src/app/notifications/controllers/mark-all-notifications-read.controller.ts`. */\nexport const markAllNotificationsReadControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(request.locals.user.id);\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n`;\n\n/** `src/app/notifications/routes.ts` — the four read endpoints behind `authMiddleware`. */\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport { listNotificationsController } from \"./controllers/list-notifications.controller\";\nimport { markAllNotificationsReadController } from \"./controllers/mark-all-notifications-read.controller\";\nimport { markNotificationReadController } from \"./controllers/mark-notification-read.controller\";\nimport { unreadNotificationsCountController } from \"./controllers/unread-notifications-count.controller\";\n\n/**\n * The signed-in user's own notifications.\n *\n * Notifications come from domain events, so there is no create. Dismissing is\n * left out on purpose; add it when the product needs it (inApp.dismiss).\n *\n * authMiddleware([]) admits any signed-in user. To limit who may read\n * notifications, add a permission gate to the group:\n *\n * import { gate } from \"@warlock.js/access\";\n *\n * router.group({ middleware: [authMiddleware([]), gate(\"notifications.read\")] }, ...);\n */\nrouter.group({ middleware: [authMiddleware([])] }, () => {\n const notifications = router.route(\"/notifications\");\n\n notifications.list(listNotificationsController);\n notifications.nest(\"/unread-count\").get(unreadNotificationsCountController);\n notifications.nest(\"/read-all\").patch(markAllNotificationsReadController);\n notifications.nest(\"/:id/read\").patch(markNotificationReadController);\n});\n`;\n\n/** Build module locale JSON, with English as the explicit translation fallback. */\nexport function createNotificationLocalesStub(): string {\n const configured: unknown = config.get(\"app.localeCodes\");\n const codes: string[] =\n Array.isArray(configured) && configured.length > 0\n ? configured.filter((code): code is string => typeof code === \"string\")\n : [\"en\"];\n return (\n JSON.stringify(\n { notFound: Object.fromEntries(codes.map((code) => [code, \"Notification not found\"])) },\n null,\n 2,\n ) + \"\\n\"\n );\n}\n\nexport const notificationLocalesStub = createNotificationLocalesStub();\n\n/** Shared fixtures for the generated signed-in notification controller specs. */\nexport const notificationTestFixtureStub = `import type { Auth } from \"@warlock.js/auth\";\nimport type { ChildModel } from \"@warlock.js/cascade\";\nimport { config } from \"@warlock.js/core\";\nimport { actingAs, type AuthenticatedTestClient } from \"@warlock.js/core/tests\";\nimport { randomUUID } from \"node:crypto\";\nimport { Notification } from \"../models/notification/notification.model\";\n\n/** Isolated recipients and rows; adjust user fields if your auth model requires more. */\nexport async function createNotificationTestFixture() {\n const User = config.key<ChildModel<Auth>>(\"auth.userType.user\");\n if (!User) throw new Error(\"Notification specs need the configured auth.userType.user model.\");\n const users: Auth[] = [];\n const clients: AuthenticatedTestClient[] = [];\n const rows: Notification[] = [];\n\n async function cleanup() {\n const credentials = await Promise.allSettled(\n clients.splice(0).map((client) => client.dispose()),\n );\n const notifications = await Promise.allSettled(\n rows.splice(0).map((row) => row.destroy({ strategy: \"permanent\" })),\n );\n const recipients = await Promise.allSettled(\n users.splice(0).map((user) => user.destroy({ strategy: \"permanent\" })),\n );\n const errors = [...credentials, ...notifications, ...recipients]\n .filter((result): result is PromiseRejectedResult => result.status === \"rejected\")\n .map((result) => result.reason);\n if (errors.length) throw new AggregateError(errors, \"Notification fixture cleanup failed.\");\n }\n\n const createUser = async () => {\n const user = await User.create({\n name: \"Notification test user\",\n email: \"notifications-\" + randomUUID() + \"@example.test\",\n password: \"test-password\",\n });\n users.push(user);\n return user;\n };\n const signIn = async (user: Auth) => {\n const client = await actingAs(user);\n clients.push(client);\n return client;\n };\n const createNotification = async (recipientId: string | number, title: string, readAt?: Date) => {\n const row = await Notification.create({\n user_id: recipientId,\n type: \"test\",\n title,\n read_at: readAt,\n });\n rows.push(row);\n return row;\n };\n\n try {\n const user = await createUser();\n const otherUser = await createUser();\n const client = await signIn(user);\n const otherClient = await signIn(otherUser);\n const own = await createNotification(user.id, \"Own unread notification\");\n const alreadyRead = await createNotification(user.id, \"Own read notification\", new Date());\n const foreign = await createNotification(otherUser.id, \"Other user's notification\");\n await createNotification(otherUser.id, \"Other user's second notification\");\n return { user, otherUser, client, otherClient, own, alreadyRead, foreign, cleanup };\n } catch (error) {\n // Preserve the setup failure after attempting every owned resource's cleanup.\n await cleanup().catch(() => undefined);\n throw error;\n }\n}\n`;\n\n/** A spec for each generated controller, including authenticated recipient isolation. */\nexport function notificationControllerSpecStub(method: \"GET\" | \"PATCH\", path: string): string {\n const helper = method === \"GET\" ? \"testGet\" : \"testPatch\";\n let signedInCases: string;\n if (path === \"/notifications\") {\n signedInCases = ` it(\"lists only the signed-in recipient's rows, ignoring a forged recipient query\", async () => {\n const response = await fixture.client.get(\n \"/notifications?recipientId=\" + fixture.otherUser.id,\n );\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual(\n expect.arrayContaining([fixture.own.id, fixture.alreadyRead.id]),\n );\n expect(body.notifications).toHaveLength(2);\n });\n\n it(\"applies unread and paging query filters\", async () => {\n const response = await fixture.client.get(\"/notifications?unread=true&limit=1\");\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual([fixture.own.id]);\n });\n\n it(\"rejects a list limit above the schema's cap\", async () => {\n expect((await fixture.client.get(\"/notifications?limit=999\")).status).toBe(422);\n });`;\n } else if (path === \"/notifications/unread-count\") {\n signedInCases = ` it(\"counts only this recipient's unread notifications\", async () => {\n const own = await expectJson<{ count: number }>(\n await fixture.client.get(\"/notifications/unread-count\"),\n );\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(own.count).toBe(1);\n expect(other.count).toBe(2);\n });`;\n } else if (path === \"/notifications/read-all\") {\n signedInCases = ` it(\"marks only this recipient's unread rows and leaves another recipient unchanged\", async () => {\n const body = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(body.count).toBe(1);\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(other.count).toBe(2);\n const again = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(again.count).toBe(0);\n });`;\n } else {\n signedInCases = ` it(\"marks its own notification read and returns it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.own.id + \"/read\");\n const body = await expectJson<{\n notification: { id: string | number; readAt?: string | null };\n }>(response);\n expect(body.notification.id).toBe(fixture.own.id);\n expect(body.notification.readAt).toBeTruthy();\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n });\n\n it(\"answers 404 for another recipient's notification without changing it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.foreign.id + \"/read\");\n expect(response.status).toBe(404);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n });`;\n }\n const modelImport =\n method === \"PATCH\"\n ? 'import { Notification } from \"../models/notification/notification.model\";\\n'\n : \"\";\n return `import { expectJson, ${helper} } from \"@warlock.js/core/tests\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n${modelImport}import { createNotificationTestFixture } from \"../tests/notifications.fixture\";\n\ndescribe(\"${method} ${path}\", () => {\n it(\"answers 401 without a signed-in user\", async () => {\n const response = await ${helper}(\"${path}\");\n expect(response.status).toBe(401);\n });\n\n describe(\"signed in\", () => {\n let fixture: Awaited<ReturnType<typeof createNotificationTestFixture>> | undefined;\n beforeEach(async () => {\n fixture = await createNotificationTestFixture();\n });\n afterEach(async () => {\n const current = fixture;\n fixture = undefined;\n await current?.cleanup();\n });\n\n${signedInCases.replaceAll(\"fixture.\", \"fixture!.\")}\n });\n});\n`;\n}\n\n/** `src/web/root.setup.ts` — document policy. */\nexport const webRootSetupStub = `import type { RootConfig } from \"@warlock.js/web\";\n\nexport const config: RootConfig = { strictMode: true };\n`;\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * document component stays here; request policy and an app-level loader\n * belong in `root.setup.ts`. Named ErrorBoundary components stay in the UI.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n <Head /> also renders the viewport tag (width=device-width,\n initial-scale=1). Change it with \\`metadata.viewport\\`,\n or set it to false to leave it out; do not add a second one here.\n */}\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { v, type Infer } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController, { public: true });\n`;\n\n/** `src/web/index.setup.ts` — page policy and universal registration. */\nexport const webHomeSetupStub = `import { extend } from \"@mongez/localization\";\nimport type { PageConfig } from \"@warlock.js/web\";\n\nexport const config: PageConfig = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n};\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/** Fetch data in index.setup.ts; use a type-only loader import for PageProps. */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@ export type HttpErrorMapping = {
|
|
|
26
26
|
* (a worker, a CLI, a standalone helper) carry a status without extending
|
|
27
27
|
* Core's `HttpError`.
|
|
28
28
|
*
|
|
29
|
-
* Called only for errors that are not already an `HttpError`. Return a
|
|
29
|
+
* Called only for errors that are not already an `HttpError` or `AppError`. Return a
|
|
30
30
|
* mapping (or an `HttpError`) to answer with it, or `undefined` to keep the
|
|
31
31
|
* default handling (a model validation error keeps its configured status,
|
|
32
32
|
* anything else is an opaque 500).
|