@warlock.js/core 5.22.1 → 5.23.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 +14 -0
- package/esm/cli/commands/generate/generate.command.mjs +17 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/use-case.generator.mjs +43 -0
- package/esm/cli/commands/generate/generators/use-case.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/stubs.mjs +47 -1
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs +2 -1
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +2 -2
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/stubs.mjs +2 -2
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts +13 -2
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +16 -3
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +5 -0
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/updater/package-manager.mjs +28 -6
- package/esm/updater/package-manager.mjs.map +1 -1
- package/esm/utils/framework-vesion.mjs.map +1 -1
- package/esm/validation/types.d.mts +12 -4
- package/esm/validation/types.d.mts.map +1 -1
- package/package.json +11 -11
- package/skills/build-restful/SKILL.md +2 -2
- package/skills/write-use-case/SKILL.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.23.0 - 2026-09-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `warlock generate.use-case <module>/<verb-noun>` scaffolds `use-cases/<verb-noun>.use-case.ts` (transport-agnostic `(input, actor)` returning a result union) plus a vitest spec, and refuses to overwrite existing files without `--force`.
|
|
14
|
+
- `rateLimit` accepts `key: "user"` to bucket by the signed-in user id (routes and page actions), with `guests: "ip" | "skip"` (default `"ip"`) for unauthenticated requests. `keyGenerator` and the 429 shape are unchanged.
|
|
15
|
+
- Each `rateLimit()` call now keeps its own counters, so two limits on the same route (two page actions on one page, say) no longer share a bucket. `errorMessage` also accepts `(request) => string` for per-request (translated) messages.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `startHttpTestServer()` now loads `.env.test` itself before configuration is read, so a Vitest global setup cannot boot against the development database.
|
|
20
|
+
- `warlock add` installs with the project's package manager: the nearest `packageManager` field, then a `pnpm-workspace.yaml`, then the nearest lockfile, searched upward so an app inside a workspace uses the workspace's manager.
|
|
21
|
+
- `uniqueExceptCurrentUser` / `uniqueExceptCurrentId` / `existsExceptCurrentUser` / `existsExceptCurrentId` on `StringValidator` now return `this`, so `Infer<>` keeps the concrete output type.
|
|
22
|
+
|
|
9
23
|
## 5.22.1 - 2026-09-25
|
|
10
24
|
|
|
11
25
|
### Changed
|
|
@@ -6,6 +6,7 @@ import { generateModule } from "./generators/module.generator.mjs";
|
|
|
6
6
|
import { generateRepository } from "./generators/repository.generator.mjs";
|
|
7
7
|
import { generateResource } from "./generators/resource.generator.mjs";
|
|
8
8
|
import { generateService } from "./generators/service.generator.mjs";
|
|
9
|
+
import { generateUseCase } from "./generators/use-case.generator.mjs";
|
|
9
10
|
import { colors } from "@mongez/copper";
|
|
10
11
|
|
|
11
12
|
//#region ../core/src/cli/commands/generate/generate.command.ts
|
|
@@ -13,6 +14,7 @@ const generators = {
|
|
|
13
14
|
module: generateModule,
|
|
14
15
|
controller: generateController,
|
|
15
16
|
service: generateService,
|
|
17
|
+
"use-case": generateUseCase,
|
|
16
18
|
model: generateModel,
|
|
17
19
|
migration: generateMigration,
|
|
18
20
|
repository: generateRepository,
|
|
@@ -124,6 +126,20 @@ const generateServiceCommand = command({
|
|
|
124
126
|
type: "boolean"
|
|
125
127
|
}]
|
|
126
128
|
});
|
|
129
|
+
const generateUseCaseCommand = command({
|
|
130
|
+
name: "generate.use-case <module>/<verb-noun>",
|
|
131
|
+
description: "Generate a transport-agnostic use-case and its spec",
|
|
132
|
+
action: generateUseCase,
|
|
133
|
+
options: [{
|
|
134
|
+
text: "--force, -f",
|
|
135
|
+
description: "Overwrite existing files",
|
|
136
|
+
type: "boolean"
|
|
137
|
+
}, {
|
|
138
|
+
text: "--dry-run",
|
|
139
|
+
description: "Preview the files that would be generated without writing anything",
|
|
140
|
+
type: "boolean"
|
|
141
|
+
}]
|
|
142
|
+
});
|
|
127
143
|
const generateModelCommand = command({
|
|
128
144
|
name: "generate.model <module>/<name>",
|
|
129
145
|
alias: "gen.md",
|
|
@@ -238,5 +254,5 @@ const generateMigrationCommand = command({
|
|
|
238
254
|
});
|
|
239
255
|
|
|
240
256
|
//#endregion
|
|
241
|
-
export { generateCommand, generateControllerCommand, generateMigrationCommand, generateModelCommand, generateModuleCommand, generateRepositoryCommand, generateResourceCommand, generateServiceCommand };
|
|
257
|
+
export { generateCommand, generateControllerCommand, generateMigrationCommand, generateModelCommand, generateModuleCommand, generateRepositoryCommand, generateResourceCommand, generateServiceCommand, generateUseCaseCommand };
|
|
242
258
|
//# sourceMappingURL=generate.command.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.command.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/generate/generate.command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"../../../commands/cli-command\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { generateController } from \"./generators/controller.generator\";\nimport { generateMigration } from \"./generators/migration.generator\";\nimport { generateModel } from \"./generators/model.generator\";\nimport { generateModule } from \"./generators/module.generator\";\nimport { generateRepository } from \"./generators/repository.generator\";\nimport { generateResource } from \"./generators/resource.generator\";\nimport { generateService } from \"./generators/service.generator\";\n\nconst generators: Record<string, (data: CommandActionData) => Promise<void>> = {\n module: generateModule,\n controller: generateController,\n service: generateService,\n model: generateModel,\n migration: generateMigration,\n repository: generateRepository,\n resource: generateResource,\n};\n\nasync function generateCommandAction(data: CommandActionData) {\n const commandName = data.args[0];\n\n if (!commandName) {\n console.log(colors.red(\"Error: Generator name is required\"));\n console.log(colors.yellow(\"\\nAvailable generators:\"));\n Object.keys(generators).forEach((cmd) => {\n console.log(colors.cyan(` - ${cmd}`));\n });\n console.log(colors.yellow(\"\\nUsage: warlock generate <generator> <name> [options]\"));\n process.exit(1);\n }\n\n const generator = generators[commandName];\n\n if (!generator) {\n console.log(colors.red(`Error: Unknown generator \"${commandName}\"`));\n console.log(colors.yellow(\"\\nAvailable generators:\"));\n Object.keys(generators).forEach((cmd) => {\n console.log(colors.cyan(` - ${cmd}`));\n });\n process.exit(1);\n }\n\n // Remove the command name from args for the generator\n const generatorData: CommandActionData = {\n ...data,\n args: data.args.slice(1),\n };\n\n await generator(generatorData);\n}\n\n// Master generate command\nexport const generateCommand = command({\n name: \"generate <generator> [args...]\",\n alias: \"g\", // Allows 'warlock g module ...'\n description: \"Scaffold a new component\",\n action: generateCommandAction,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\n// Individual commands\nexport const generateModuleCommand = command({\n name: \"generate.module <name>\",\n alias: \"gen.m\",\n description: \"Generate a new module\",\n action: generateModule,\n options: [\n {\n text: \"--minimal, -m\",\n description:\n \"Generate a minimal skeleton (routes, main, locales, empty subfolders) instead of the full CRUD scaffold\",\n type: \"boolean\",\n },\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateControllerCommand = command({\n name: \"generate.controller <module>/<name>\",\n alias: \"gen.c\",\n description: \"Generate a new controller\",\n action: generateController,\n options: [\n {\n text: \"--with-validation, -v\",\n description: \"Generate a validation schema and bind it to the controller\",\n type: \"boolean\",\n },\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateServiceCommand = command({\n name: \"generate.service <module>/<name>\",\n alias: \"gen.s\",\n description: \"Generate a new service\",\n action: generateService,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateModelCommand = command({\n name: \"generate.model <module>/<name>\",\n alias: \"gen.md\",\n description: \"Generate a new model with migration\",\n action: generateModel,\n options: [\n {\n text: \"--with-resource, -rs\",\n description: \"Generate resource\",\n type: \"boolean\",\n },\n {\n text: \"--table <name>\",\n description: \"Specify table name\",\n },\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps in migration (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateRepositoryCommand = command({\n name: \"generate.repository <module>/<name>\",\n alias: \"gen.r\",\n description: \"Generate a new repository\",\n action: generateRepository,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateResourceCommand = command({\n name: \"generate.resource <module>/<name>\",\n alias: \"gen.rs\",\n description: \"Generate a new resource\",\n action: generateResource,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateMigrationCommand = command({\n name: \"generate.migration <model-path>\",\n alias: \"gen.mig\",\n description: \"Generate a new migration for a model\",\n action: generateMigration,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n {\n text: \"--add <columns>\",\n description: \"Columns to add (DSL: name:type:modifier,...)\",\n },\n {\n text: \"--drop <columns>\",\n description: \"Columns to drop (comma-separated names)\",\n },\n {\n text: \"--rename <columns>\",\n description: \"Columns to rename (DSL: old:new,...)\",\n },\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,aAAyE;CAC7E,QAAQ;CACR,YAAY;CACZ,SAAS;CACT,OAAO;CACP,WAAW;CACX,YAAY;CACZ,UAAU;AACZ;AAEA,eAAe,sBAAsB,MAAyB;CAC5D,MAAM,cAAc,KAAK,KAAK;CAE9B,IAAI,CAAC,aAAa;EAChB,QAAQ,IAAI,OAAO,IAAI,mCAAmC,CAAC;EAC3D,QAAQ,IAAI,OAAO,OAAO,yBAAyB,CAAC;EACpD,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,QAAQ;GACvC,QAAQ,IAAI,OAAO,KAAK,OAAO,KAAK,CAAC;EACvC,CAAC;EACD,QAAQ,IAAI,OAAO,OAAO,wDAAwD,CAAC;EACnF,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,YAAY,WAAW;CAE7B,IAAI,CAAC,WAAW;EACd,QAAQ,IAAI,OAAO,IAAI,6BAA6B,YAAY,EAAE,CAAC;EACnE,QAAQ,IAAI,OAAO,OAAO,yBAAyB,CAAC;EACpD,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,QAAQ;GACvC,QAAQ,IAAI,OAAO,KAAK,OAAO,KAAK,CAAC;EACvC,CAAC;EACD,QAAQ,KAAK,CAAC;CAChB;CAQA,MAAM,UAAU;EAJd,GAAG;EACH,MAAM,KAAK,KAAK,MAAM,CAAC;CAGG,CAAC;AAC/B;AAGA,MAAa,kBAAkB,QAAQ;CACrC,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAGD,MAAa,wBAAwB,QAAQ;CAC3C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC;AAED,MAAa,4BAA4B,QAAQ;CAC/C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC;AAED,MAAa,yBAAyB,QAAQ;CAC5C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,uBAAuB,QAAQ;CAC1C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC;AAED,MAAa,4BAA4B,QAAQ;CAC/C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,0BAA0B,QAAQ;CAC7C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,2BAA2B,QAAQ;CAC9C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"generate.command.mjs","names":[],"sources":["../../../../../../../../../core/src/cli/commands/generate/generate.command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"../../../commands/cli-command\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { generateController } from \"./generators/controller.generator\";\nimport { generateMigration } from \"./generators/migration.generator\";\nimport { generateModel } from \"./generators/model.generator\";\nimport { generateModule } from \"./generators/module.generator\";\nimport { generateRepository } from \"./generators/repository.generator\";\nimport { generateResource } from \"./generators/resource.generator\";\nimport { generateService } from \"./generators/service.generator\";\nimport { generateUseCase } from \"./generators/use-case.generator\";\n\nconst generators: Record<string, (data: CommandActionData) => Promise<void>> = {\n module: generateModule,\n controller: generateController,\n service: generateService,\n \"use-case\": generateUseCase,\n model: generateModel,\n migration: generateMigration,\n repository: generateRepository,\n resource: generateResource,\n};\n\nasync function generateCommandAction(data: CommandActionData) {\n const commandName = data.args[0];\n\n if (!commandName) {\n console.log(colors.red(\"Error: Generator name is required\"));\n console.log(colors.yellow(\"\\nAvailable generators:\"));\n Object.keys(generators).forEach((cmd) => {\n console.log(colors.cyan(` - ${cmd}`));\n });\n console.log(colors.yellow(\"\\nUsage: warlock generate <generator> <name> [options]\"));\n process.exit(1);\n }\n\n const generator = generators[commandName];\n\n if (!generator) {\n console.log(colors.red(`Error: Unknown generator \"${commandName}\"`));\n console.log(colors.yellow(\"\\nAvailable generators:\"));\n Object.keys(generators).forEach((cmd) => {\n console.log(colors.cyan(` - ${cmd}`));\n });\n process.exit(1);\n }\n\n // Remove the command name from args for the generator\n const generatorData: CommandActionData = {\n ...data,\n args: data.args.slice(1),\n };\n\n await generator(generatorData);\n}\n\n// Master generate command\nexport const generateCommand = command({\n name: \"generate <generator> [args...]\",\n alias: \"g\", // Allows 'warlock g module ...'\n description: \"Scaffold a new component\",\n action: generateCommandAction,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\n// Individual commands\nexport const generateModuleCommand = command({\n name: \"generate.module <name>\",\n alias: \"gen.m\",\n description: \"Generate a new module\",\n action: generateModule,\n options: [\n {\n text: \"--minimal, -m\",\n description:\n \"Generate a minimal skeleton (routes, main, locales, empty subfolders) instead of the full CRUD scaffold\",\n type: \"boolean\",\n },\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateControllerCommand = command({\n name: \"generate.controller <module>/<name>\",\n alias: \"gen.c\",\n description: \"Generate a new controller\",\n action: generateController,\n options: [\n {\n text: \"--with-validation, -v\",\n description: \"Generate a validation schema and bind it to the controller\",\n type: \"boolean\",\n },\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateServiceCommand = command({\n name: \"generate.service <module>/<name>\",\n alias: \"gen.s\",\n description: \"Generate a new service\",\n action: generateService,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateUseCaseCommand = command({\n name: \"generate.use-case <module>/<verb-noun>\",\n description: \"Generate a transport-agnostic use-case and its spec\",\n action: generateUseCase,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateModelCommand = command({\n name: \"generate.model <module>/<name>\",\n alias: \"gen.md\",\n description: \"Generate a new model with migration\",\n action: generateModel,\n options: [\n {\n text: \"--with-resource, -rs\",\n description: \"Generate resource\",\n type: \"boolean\",\n },\n {\n text: \"--table <name>\",\n description: \"Specify table name\",\n },\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--overwrite\",\n description:\n \"With --force, also replace existing files that differ from the generated output (default: skip and list them)\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps in migration (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateRepositoryCommand = command({\n name: \"generate.repository <module>/<name>\",\n alias: \"gen.r\",\n description: \"Generate a new repository\",\n action: generateRepository,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateResourceCommand = command({\n name: \"generate.resource <module>/<name>\",\n alias: \"gen.rs\",\n description: \"Generate a new resource\",\n action: generateResource,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n ],\n});\n\nexport const generateMigrationCommand = command({\n name: \"generate.migration <model-path>\",\n alias: \"gen.mig\",\n description: \"Generate a new migration for a model\",\n action: generateMigration,\n options: [\n {\n text: \"--force, -f\",\n description: \"Overwrite existing files\",\n type: \"boolean\",\n },\n {\n text: \"--dry-run\",\n description: \"Preview the files that would be generated without writing anything\",\n type: \"boolean\",\n },\n {\n text: \"--add <columns>\",\n description: \"Columns to add (DSL: name:type:modifier,...)\",\n },\n {\n text: \"--drop <columns>\",\n description: \"Columns to drop (comma-separated names)\",\n },\n {\n text: \"--rename <columns>\",\n description: \"Columns to rename (DSL: old:new,...)\",\n },\n {\n text: \"--timestamps [bool]\",\n description: \"Include timestamps (default: true)\",\n type: \"boolean\",\n },\n {\n text: \"--no-timestamps\",\n description: \"Disable timestamps (same as --timestamps false)\",\n type: \"boolean\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,aAAyE;CAC7E,QAAQ;CACR,YAAY;CACZ,SAAS;CACT,YAAY;CACZ,OAAO;CACP,WAAW;CACX,YAAY;CACZ,UAAU;AACZ;AAEA,eAAe,sBAAsB,MAAyB;CAC5D,MAAM,cAAc,KAAK,KAAK;CAE9B,IAAI,CAAC,aAAa;EAChB,QAAQ,IAAI,OAAO,IAAI,mCAAmC,CAAC;EAC3D,QAAQ,IAAI,OAAO,OAAO,yBAAyB,CAAC;EACpD,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,QAAQ;GACvC,QAAQ,IAAI,OAAO,KAAK,OAAO,KAAK,CAAC;EACvC,CAAC;EACD,QAAQ,IAAI,OAAO,OAAO,wDAAwD,CAAC;EACnF,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,YAAY,WAAW;CAE7B,IAAI,CAAC,WAAW;EACd,QAAQ,IAAI,OAAO,IAAI,6BAA6B,YAAY,EAAE,CAAC;EACnE,QAAQ,IAAI,OAAO,OAAO,yBAAyB,CAAC;EACpD,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,QAAQ;GACvC,QAAQ,IAAI,OAAO,KAAK,OAAO,KAAK,CAAC;EACvC,CAAC;EACD,QAAQ,KAAK,CAAC;CAChB;CAQA,MAAM,UAAU;EAJd,GAAG;EACH,MAAM,KAAK,KAAK,MAAM,CAAC;CAGG,CAAC;AAC/B;AAGA,MAAa,kBAAkB,QAAQ;CACrC,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAGD,MAAa,wBAAwB,QAAQ;CAC3C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC;AAED,MAAa,4BAA4B,QAAQ;CAC/C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC;AAED,MAAa,yBAAyB,QAAQ;CAC5C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,yBAAyB,QAAQ;CAC5C,MAAM;CACN,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,uBAAuB,QAAQ;CAC1C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aACE;GACF,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC;AAED,MAAa,4BAA4B,QAAQ;CAC/C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,0BAA0B,QAAQ;CAC7C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,CACF;AACF,CAAC;AAED,MAAa,2BAA2B,QAAQ;CAC9C,MAAM;CACN,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;EACP;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;EACA;GACE,MAAM;GACN,aAAa;GACb,MAAM;EACR;CACF;AACF,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCaseSpecStub, useCaseStub } from "../templates/stubs.mjs";
|
|
2
|
+
import { parseModulePath, parseName } from "../utils/name-parser.mjs";
|
|
3
|
+
import { putFileAsync, setDryRun } from "../utils/writer.mjs";
|
|
4
|
+
import { componentExists, ensureComponentDirectory, moduleExists, resolveComponentPath } from "../utils/path-resolver.mjs";
|
|
5
|
+
import { colors } from "@mongez/copper";
|
|
6
|
+
|
|
7
|
+
//#region ../core/src/cli/commands/generate/generators/use-case.generator.ts
|
|
8
|
+
async function generateUseCase(data) {
|
|
9
|
+
const input = data.args[0];
|
|
10
|
+
if (!input) {
|
|
11
|
+
console.log(colors.red("Error: Use-case name is required"));
|
|
12
|
+
console.log(colors.yellow("Usage: warlock generate.use-case <module>/<verb-noun>"));
|
|
13
|
+
console.log(colors.yellow("Example: warlock generate.use-case orders/place-order"));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const { module, name: componentName } = parseModulePath(input);
|
|
17
|
+
if (!module) {
|
|
18
|
+
console.log(colors.red("Error: Module name is required"));
|
|
19
|
+
console.log(colors.yellow("Usage: warlock generate.use-case <module>/<verb-noun>"));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (!await moduleExists(module)) {
|
|
23
|
+
console.log(colors.red(`Error: Module "${module}" does not exist`));
|
|
24
|
+
console.log(colors.yellow(`Run: warlock generate.module ${module}`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const name = parseName(componentName);
|
|
28
|
+
const force = data.options.force || data.options.f;
|
|
29
|
+
setDryRun(Boolean(data.options.dryRun));
|
|
30
|
+
for (const file of [`${name.kebab}.use-case`, `${name.kebab}.use-case.spec`]) if (await componentExists(module, "use-cases", file) && !force) {
|
|
31
|
+
console.log(colors.red(`Error: Use-case file "${file}.ts" already exists`));
|
|
32
|
+
console.log(colors.yellow("Use --force to overwrite"));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
await ensureComponentDirectory(module, "use-cases");
|
|
36
|
+
await putFileAsync(resolveComponentPath(module, "use-cases", `${name.kebab}.use-case`), useCaseStub(name));
|
|
37
|
+
await putFileAsync(resolveComponentPath(module, "use-cases", `${name.kebab}.use-case.spec`), useCaseSpecStub(name));
|
|
38
|
+
console.log(colors.cyan(`\n✨ Use-case "${name.camel}UseCase" generated successfully!`));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { generateUseCase };
|
|
43
|
+
//# sourceMappingURL=use-case.generator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-case.generator.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/generators/use-case.generator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../../../commands/types\";\nimport { useCaseSpecStub, useCaseStub } from \"../templates/stubs\";\nimport { parseModulePath, parseName } from \"../utils/name-parser\";\nimport {\n componentExists,\n ensureComponentDirectory,\n moduleExists,\n resolveComponentPath,\n} from \"../utils/path-resolver\";\nimport { putFileAsync, setDryRun } from \"../utils/writer\";\n\nexport async function generateUseCase(data: CommandActionData): Promise<void> {\n const input = data.args[0];\n\n if (!input) {\n console.log(colors.red(\"Error: Use-case name is required\"));\n console.log(colors.yellow(\"Usage: warlock generate.use-case <module>/<verb-noun>\"));\n console.log(colors.yellow(\"Example: warlock generate.use-case orders/place-order\"));\n process.exit(1);\n }\n\n const { module, name: componentName } = parseModulePath(input);\n\n if (!module) {\n console.log(colors.red(\"Error: Module name is required\"));\n console.log(colors.yellow(\"Usage: warlock generate.use-case <module>/<verb-noun>\"));\n process.exit(1);\n }\n\n if (!(await moduleExists(module))) {\n console.log(colors.red(`Error: Module \"${module}\" does not exist`));\n console.log(colors.yellow(`Run: warlock generate.module ${module}`));\n process.exit(1);\n }\n\n const name = parseName(componentName);\n const force = data.options.force || data.options.f;\n setDryRun(Boolean(data.options.dryRun));\n\n // Refuse to overwrite either file unless --force\n for (const file of [`${name.kebab}.use-case`, `${name.kebab}.use-case.spec`]) {\n if ((await componentExists(module, \"use-cases\", file)) && !force) {\n console.log(colors.red(`Error: Use-case file \"${file}.ts\" already exists`));\n console.log(colors.yellow(\"Use --force to overwrite\"));\n process.exit(1);\n }\n }\n\n await ensureComponentDirectory(module, \"use-cases\");\n\n await putFileAsync(\n resolveComponentPath(module, \"use-cases\", `${name.kebab}.use-case`),\n useCaseStub(name),\n );\n await putFileAsync(\n resolveComponentPath(module, \"use-cases\", `${name.kebab}.use-case.spec`),\n useCaseSpecStub(name),\n );\n\n console.log(colors.cyan(`\\n✨ Use-case \"${name.camel}UseCase\" generated successfully!`));\n}\n"],"mappings":";;;;;;;AAYA,eAAsB,gBAAgB,MAAwC;CAC5E,MAAM,QAAQ,KAAK,KAAK;CAExB,IAAI,CAAC,OAAO;EACV,QAAQ,IAAI,OAAO,IAAI,kCAAkC,CAAC;EAC1D,QAAQ,IAAI,OAAO,OAAO,uDAAuD,CAAC;EAClF,QAAQ,IAAI,OAAO,OAAO,uDAAuD,CAAC;EAClF,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,EAAE,QAAQ,MAAM,kBAAkB,gBAAgB,KAAK;CAE7D,IAAI,CAAC,QAAQ;EACX,QAAQ,IAAI,OAAO,IAAI,gCAAgC,CAAC;EACxD,QAAQ,IAAI,OAAO,OAAO,uDAAuD,CAAC;EAClF,QAAQ,KAAK,CAAC;CAChB;CAEA,IAAI,CAAE,MAAM,aAAa,MAAM,GAAI;EACjC,QAAQ,IAAI,OAAO,IAAI,kBAAkB,OAAO,iBAAiB,CAAC;EAClE,QAAQ,IAAI,OAAO,OAAO,gCAAgC,QAAQ,CAAC;EACnE,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,OAAO,UAAU,aAAa;CACpC,MAAM,QAAQ,KAAK,QAAQ,SAAS,KAAK,QAAQ;CACjD,UAAU,QAAQ,KAAK,QAAQ,MAAM,CAAC;CAGtC,KAAK,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,YAAY,GAAG,KAAK,MAAM,eAAe,GACzE,IAAK,MAAM,gBAAgB,QAAQ,aAAa,IAAI,KAAM,CAAC,OAAO;EAChE,QAAQ,IAAI,OAAO,IAAI,yBAAyB,KAAK,oBAAoB,CAAC;EAC1E,QAAQ,IAAI,OAAO,OAAO,0BAA0B,CAAC;EACrD,QAAQ,KAAK,CAAC;CAChB;CAGF,MAAM,yBAAyB,QAAQ,WAAW;CAElD,MAAM,aACJ,qBAAqB,QAAQ,aAAa,GAAG,KAAK,MAAM,UAAU,GAClE,YAAY,IAAI,CAClB;CACA,MAAM,aACJ,qBAAqB,QAAQ,aAAa,GAAG,KAAK,MAAM,eAAe,GACvE,gBAAgB,IAAI,CACtB;CAEA,QAAQ,IAAI,OAAO,KAAK,iBAAiB,KAAK,MAAM,iCAAiC,CAAC;AACxF"}
|
|
@@ -507,7 +507,53 @@ export class ${name.singular.pascal}Resource extends Resource {
|
|
|
507
507
|
}
|
|
508
508
|
`;
|
|
509
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Use-case template stub
|
|
512
|
+
* Outputs to: use-cases/{name}.use-case.ts
|
|
513
|
+
*
|
|
514
|
+
* A use-case takes validated input and an actor, never a request or response,
|
|
515
|
+
* so a page action and a controller can both call it.
|
|
516
|
+
*/
|
|
517
|
+
function useCaseStub(name) {
|
|
518
|
+
return `import type { ${name.pascal}Schema } from "../schema/${name.kebab}.schema";
|
|
519
|
+
|
|
520
|
+
export type ${name.pascal}Actor = { id: number | string };
|
|
521
|
+
|
|
522
|
+
export type ${name.pascal}Result =
|
|
523
|
+
| { code: "OK" }
|
|
524
|
+
| { code: "FORBIDDEN" }
|
|
525
|
+
| { code: "NOT_FOUND" };
|
|
526
|
+
|
|
527
|
+
export async function ${name.camel}UseCase(
|
|
528
|
+
input: ${name.pascal}Schema,
|
|
529
|
+
actor: ${name.pascal}Actor,
|
|
530
|
+
): Promise<${name.pascal}Result> {
|
|
531
|
+
// TODO: Implement use-case logic
|
|
532
|
+
void input;
|
|
533
|
+
void actor;
|
|
534
|
+
|
|
535
|
+
return { code: "OK" };
|
|
536
|
+
}
|
|
537
|
+
`;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Use-case spec template stub
|
|
541
|
+
* Outputs to: use-cases/{name}.use-case.spec.ts
|
|
542
|
+
*/
|
|
543
|
+
function useCaseSpecStub(name) {
|
|
544
|
+
return `import { describe, expect, it } from "vitest";
|
|
545
|
+
import { ${name.camel}UseCase } from "./${name.kebab}.use-case";
|
|
546
|
+
|
|
547
|
+
describe("${name.camel}UseCase", () => {
|
|
548
|
+
it("returns OK for a valid input", async () => {
|
|
549
|
+
const result = await ${name.camel}UseCase({} as never, { id: 1 });
|
|
550
|
+
|
|
551
|
+
expect(result.code).toBe("OK");
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
510
556
|
|
|
511
557
|
//#endregion
|
|
512
|
-
export { controllerStub, crudCreateControllerStub, crudCreateSchemaStub, crudCreateServiceStub, crudDeleteControllerStub, crudDeleteServiceStub, crudGetServiceStub, crudListControllerStub, crudListServiceStub, crudModelStub, crudRepositoryStub, crudResourceStub, crudRoutesStub, crudSeedStub, crudShowControllerStub, crudUpdateControllerStub, crudUpdateSchemaStub, crudUpdateServiceStub, migrationAlterStub, migrationStub, modelStub, repositoryStub, resourceStub, schemaStub, serviceStub };
|
|
558
|
+
export { controllerStub, crudCreateControllerStub, crudCreateSchemaStub, crudCreateServiceStub, crudDeleteControllerStub, crudDeleteServiceStub, crudGetServiceStub, crudListControllerStub, crudListServiceStub, crudModelStub, crudRepositoryStub, crudResourceStub, crudRoutesStub, crudSeedStub, crudShowControllerStub, crudUpdateControllerStub, crudUpdateSchemaStub, crudUpdateServiceStub, migrationAlterStub, migrationStub, modelStub, repositoryStub, resourceStub, schemaStub, serviceStub, useCaseSpecStub, useCaseStub };
|
|
513
559
|
//# sourceMappingURL=stubs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/templates/stubs.ts"],"sourcesContent":["import type { ParsedName } from \"../types\";\nimport { Name } from \"../utils/name-parser\";\n\n/**\n * Controller template stub\n *\n * Controllers default to the guarded handler type since application\n * routes run behind `guarded()`. When `withValidation` is set, the\n * schema's exported type + value are imported directly from the\n * `schema/` folder and bound to the handler — no `requests/` alias.\n */\nexport function controllerStub(\n name: ParsedName,\n options: { withValidation?: boolean } = {},\n): string {\n const { withValidation } = options;\n\n if (!withValidation) {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\n\nexport const ${name.camel}Controller: GuardedRequestHandler = async ({ response }) => {\n // TODO: Implement controller logic\n return response.success({});\n};\n`;\n }\n\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { type ${name.pascal}Schema, ${name.camel}Schema } from \"../schema/${name.kebab}.schema\";\n\nexport const ${name.camel}Controller: GuardedRequestHandler<${name.pascal}Schema> = async ({\n response,\n}) => {\n // TODO: Implement controller logic\n return response.success({});\n};\n\n${name.camel}Controller.validation = {\n schema: ${name.camel}Schema,\n};\n`;\n}\n\n/**\n * CRUD Create Controller template\n * Note: moduleName is the module (plural), we need singular entity name\n */\nexport function crudCreateControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { type Create${moduleName.pascal}Schema, create${moduleName.pascal}Schema } from \"../schema/create-${moduleName.kebab}.schema\";\nimport { create${moduleName.pascal}Service } from \"../services/create-${moduleName.kebab}.service\";\n\nexport const create${moduleName.pascal}Controller: GuardedRequestHandler<Create${moduleName.pascal}Schema> = async ({\n request,\n response,\n}) => {\n const ${moduleName.camel} = await create${moduleName.pascal}Service(request.validated());\n\n return response.successCreate({\n ${moduleName.camel},\n });\n};\n\ncreate${moduleName.pascal}Controller.validation = {\n schema: create${moduleName.pascal}Schema,\n};\n`;\n}\n\n/**\n * CRUD Update Controller template\n */\nexport function crudUpdateControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { type Update${moduleName.pascal}Schema, update${moduleName.pascal}Schema } from \"../schema/update-${moduleName.kebab}.schema\";\nimport { update${moduleName.pascal}Service } from \"../services/update-${moduleName.kebab}.service\";\n\nexport const update${moduleName.pascal}Controller: GuardedRequestHandler<Update${moduleName.pascal}Schema> = async ({\n request,\n response,\n}) => {\n const ${moduleName.camel} = await update${moduleName.pascal}Service(request.input(\"id\"), request.validated());\n\n return response.success({\n ${moduleName.camel},\n });\n};\n\nupdate${moduleName.pascal}Controller.validation = {\n schema: update${moduleName.pascal}Schema,\n};\n`;\n}\n\n/**\n * CRUD List Controller template\n */\nexport function crudListControllerStub(moduleName: Name): string {\n const plural = moduleName.plural;\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { list${plural.pascal}Service } from \"../services/list-${plural.kebab}.service\";\n\nexport const list${plural.pascal}Controller: GuardedRequestHandler = async ({\n request,\n response,\n}) => {\n const { data, pagination } = await list${plural.pascal}Service(request.all());\n\n return response.success({\n data,\n pagination,\n });\n};\n`;\n}\n\n/**\n * CRUD Show/Get Controller template\n */\nexport function crudShowControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { get${moduleName.pascal}Service } from \"../services/get-${moduleName.kebab}.service\";\n\nexport const get${moduleName.pascal}Controller: GuardedRequestHandler = async ({\n request,\n response,\n}) => {\n const ${moduleName.camel} = await get${moduleName.pascal}Service(request.input(\"id\"));\n\n if (!${moduleName.camel}) {\n return response.notFound();\n }\n\n return response.success({\n ${moduleName.camel},\n });\n};\n`;\n}\n\n/**\n * CRUD Delete Controller template\n */\nexport function crudDeleteControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { delete${moduleName.pascal}Service } from \"../services/delete-${moduleName.kebab}.service\";\n\nexport const delete${moduleName.pascal}Controller: GuardedRequestHandler = async ({\n request,\n response,\n}) => {\n await delete${moduleName.pascal}Service(request.input(\"id\"));\n\n return response.noContent();\n};\n`;\n}\n\n/**\n * CRUD Routes template\n */\nexport function crudRoutesStub(moduleName: Name): string {\n const singular = moduleName.singular;\n const plural = moduleName.plural;\n\n return `import { router } from \"@warlock.js/core\";\nimport { guarded } from \"app/shared/utils/router\";\nimport { create${singular.pascal}Controller } from \"./controllers/create-${singular.kebab}.controller\";\nimport { delete${singular.pascal}Controller } from \"./controllers/delete-${singular.kebab}.controller\";\nimport { get${singular.pascal}Controller } from \"./controllers/get-${singular.kebab}.controller\";\nimport { list${plural.pascal}Controller } from \"./controllers/list-${plural.kebab}.controller\";\nimport { update${singular.pascal}Controller } from \"./controllers/update-${singular.kebab}.controller\";\n\nguarded(() => {\n router\n .route(\"/${plural.kebab}\")\n .list(list${plural.pascal}Controller)\n .show(get${singular.pascal}Controller)\n .create(create${singular.pascal}Controller)\n .update(update${singular.pascal}Controller)\n .destroy(delete${singular.pascal}Controller);\n});\n`;\n}\n\n/**\n * CRUD Model template\n */\nexport function crudModelStub(moduleName: Name): string {\n const singular = moduleName.singular;\n const plural = moduleName.plural;\n\n return `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\nimport { ${singular.pascal}Resource } from \"app/${plural.kebab}/resources/${singular.kebab}.resource\";\n\nexport const ${singular.camel}Schema = v.object({\n // TODO: Add more fields\n});\n\nexport type ${singular.pascal}Schema = Infer.Output<typeof ${singular.camel}Schema>;\n\n@RegisterModel()\nexport class ${singular.pascal} extends Model<${singular.pascal}Schema> {\n public static table = \"${plural.snake}\";\n\n public static schema = ${singular.camel}Schema;\n\n public static relations = {};\n\n public static resource = ${singular.pascal}Resource;\n}\n`;\n}\n\n/**\n * CRUD Resource template\n */\nexport function crudResourceStub(moduleName: Name): string {\n // Get singular entity name\n const entity = moduleName.singular;\n\n return `import { defineResource } from \"@warlock.js/core\";\n\nexport const ${entity.pascal}Resource = defineResource({\n schema: {\n id: \"number\",\n // TODO: Add more resource fields\n },\n});\n`;\n}\n\n/**\n * CRUD Repository template\n */\nexport function crudRepositoryStub(entity: Name): string {\n const moduleSingularName = entity.singular;\n const modulePluralName = entity.plural;\n return `import type { FilterRules, RepositoryOptions, TypedRepositoryOptions } from \"@warlock.js/core\";\nimport { RepositoryManager } from \"@warlock.js/core\";\nimport { ${moduleSingularName.pascal} } from \"../models/${moduleSingularName.kebab}\";\n\ntype ${moduleSingularName.pascal}ListFilter = {\n // Repository list filters\n};\n\nexport type ${moduleSingularName.pascal}ListOptions = TypedRepositoryOptions<${moduleSingularName.pascal}ListFilter>;\n\nclass ${modulePluralName.pascal}Repository extends RepositoryManager<${moduleSingularName.pascal}, ${moduleSingularName.pascal}ListOptions> {\n public source = ${moduleSingularName.pascal};\n\n public simpleSelectColumns: string[] = [\"id\"];\n\n public filterBy: FilterRules = {\n id: \"=\",\n };\n\n public defaultOptions: RepositoryOptions = {\n orderBy: {\n id: \"desc\",\n },\n };\n}\n\nexport const ${modulePluralName.camel}Repository = new ${modulePluralName.pascal}Repository();\n`;\n}\n\n/**\n * CRUD Create Service template\n */\nexport function crudCreateServiceStub(entity: Name): string {\n const moduleSingularName = entity.singular;\n return `import { ${moduleSingularName.pascal} } from \"../models/${moduleSingularName.kebab}\";\nimport type { Create${moduleSingularName.pascal}Schema } from \"../schema/create-${moduleSingularName.kebab}.schema\";\n\nexport async function create${moduleSingularName.pascal}Service(data: Create${moduleSingularName.pascal}Schema) {\n const ${moduleSingularName.camel} = await ${moduleSingularName.pascal}.create(data);\n return ${moduleSingularName.camel};\n}\n`;\n}\n\n/**\n * CRUD Update Service template\n */\nexport function crudUpdateServiceStub(entity: Name): string {\n const moduleSingularName = entity.singular;\n return `import { ResourceNotFoundError } from \"@warlock.js/core\";\nimport { get${moduleSingularName.pascal}Service } from \"./get-${moduleSingularName.kebab}.service\";\nimport type { Update${moduleSingularName.pascal}Schema } from \"../schema/update-${moduleSingularName.kebab}.schema\";\n\nexport async function update${moduleSingularName.pascal}Service(id: number | string, data: Update${moduleSingularName.pascal}Schema) {\n const ${moduleSingularName.camel} = await get${moduleSingularName.pascal}Service(id);\n\n await ${moduleSingularName.camel}.save({ merge: data });\n return ${moduleSingularName.camel};\n}\n`;\n}\n\n/**\n * CRUD List Service template\n */\nexport function crudListServiceStub(entity: Name): string {\n const modulePluralName = entity.plural;\n return `import { ${modulePluralName.camel}Repository } from \"../repositories/${modulePluralName.kebab}.repository\";\n\nexport async function list${modulePluralName.pascal}Service(filters: any) {\n return ${modulePluralName.camel}Repository.listCached(filters);\n}\n`;\n}\n\n/**\n * CRUD Get Service template\n */\nexport function crudGetServiceStub(entity: Name): string {\n return `import { ${entity.plural.camel}Repository } from \"../repositories/${entity.plural.kebab}.repository\";\nimport { ResourceNotFoundError } from \"@warlock.js/core\";\n\nexport async function get${entity.singular.pascal}Service(id: number | string) {\n const ${entity.singular.camel} = await ${entity.plural.camel}Repository.getCached(id);\n\n if (!${entity.singular.camel}) {\n throw new ResourceNotFoundError(\"${entity.singular.pascal} resource not found!\");\n }\n\n return ${entity.singular.camel};\n}\n`;\n}\n\n/**\n * CRUD Delete Service template\n */\nexport function crudDeleteServiceStub(entity: Name): string {\n const singular = entity.singular;\n return `import { ResourceNotFoundError } from \"@warlock.js/core\";\nimport { get${singular.pascal}Service } from \"./get-${singular.kebab}.service\";\n\nexport async function delete${singular.pascal}Service(id: number | string) {\n const ${singular.camel} = await get${singular.pascal}Service(id);\n if (!${singular.camel}) {\n throw new ResourceNotFoundError(\"${singular.pascal} not found\");\n }\n await ${singular.camel}.destroy();\n}\n`;\n}\n\n/**\n * CRUD Seed template\n */\nexport function crudSeedStub(entity: Name): string {\n return `import { seeder } from \"@warlock.js/core\";\nimport { ${entity.singular.pascal} } from \"../models/${entity.singular.kebab}\";\n\nexport default seeder({\n name: \"Seed ${entity.plural.pascal}\",\n once: true,\n // Disabled until you fill in the fields below. A generated stub calls\n // create({}) with no data, which fails required-field validation and would\n // abort the whole \\`warlock seed\\` run. Add the fields, then set this to true.\n enabled: false,\n run: async ({ track }) => {\n const total = 10;\n for (let i = 0; i < total; i++) {\n track(\n await ${entity.singular.pascal}.create({\n // TODO: Add the model's required fields here, then set enabled: true above.\n }),\n );\n }\n },\n});\n`;\n}\n\n/**\n * Migration template\n */\n/**\n * Migration Create template\n */\nexport function migrationStub(\n entityName: ParsedName,\n options: {\n columns?: string;\n imports?: string[];\n timestamps?: boolean;\n tableName?: string;\n } = {},\n): string {\n const { columns = \"\", imports = [], timestamps = true } = options;\n\n const allImports = [\"Migration\", ...imports].join(\", \");\n\n let optionsString = \"\";\n if (timestamps === false) {\n optionsString = `, { timestamps: false }`;\n }\n\n return `import { ${allImports} } from \"@warlock.js/cascade\";\nimport { ${entityName.pascal} } from \"../${entityName.kebab}.model\";\n\nexport default Migration.create(${entityName.pascal}, {\n${columns ? columns : \" // add your columns here, id is auto added to the list\"}\n}${optionsString});\n`;\n}\n\n/**\n * Migration Alter template\n */\nexport function migrationAlterStub(\n entityName: ParsedName,\n options: {\n add?: string;\n drop?: string; // stringified array like `\"[\\\"col1\\\", \\\"col2\\\"]\"`\n rename?: string; // stringified object like `{ old: \"new\" }`\n imports?: string[];\n } = {},\n): string {\n const { add = \"\", drop, rename, imports = [] } = options;\n const allImports = [\"Migration\", ...imports].join(\", \");\n\n // Build the schema object dynamically\n const schemaParts: string[] = [];\n\n if (add) {\n schemaParts.push(` add: {\\n${add}\\n },`);\n }\n\n if (drop) {\n schemaParts.push(` drop: ${drop},`);\n }\n\n if (rename) {\n schemaParts.push(` rename: ${rename},`);\n }\n\n return `import { ${allImports} } from \"@warlock.js/cascade\";\nimport { ${entityName.pascal} } from \"../${entityName.kebab}.model\";\n\nexport default Migration.alter(${entityName.pascal}, {\n${schemaParts.join(\"\\n\")}\n});\n`;\n}\n\n/**\n * CRUD Create Schema template\n * Outputs to: schema/create-{entity}.schema.ts\n */\nexport function crudCreateSchemaStub(moduleName: Name): string {\n return `import { type Infer, v } from \"@warlock.js/seal\";\n\nexport const create${moduleName.pascal}Schema = v.object({\n // TODO: Add validation rules\n});\n\nexport type Create${moduleName.pascal}Schema = Infer<typeof create${moduleName.pascal}Schema>;\n`;\n}\n\n/**\n * CRUD Update Schema template\n * Outputs to: schema/update-{entity}.schema.ts\n */\nexport function crudUpdateSchemaStub(moduleName: Name): string {\n return `import { type Infer, v } from \"@warlock.js/seal\";\n\nexport const update${moduleName.pascal}Schema = v.object({\n // TODO: Add validation rules\n});\n\nexport type Update${moduleName.pascal}Schema = Infer<typeof update${moduleName.pascal}Schema>;\n`;\n}\n\n/**\n * Service template stub\n */\nexport function serviceStub(name: Name): string {\n return `export async function ${name.camel}Service(data: any): Promise<any> {\n // TODO: Implement service logic\n throw new Error(\"${name.camel}Service not implemented\");\n}\n`;\n}\n\n/**\n * Schema template stub\n * Outputs to: schema/{name}.schema.ts\n */\nexport function schemaStub(name: Name): string {\n return `import { type Infer, v } from \"@warlock.js/seal\";\n\nexport const ${name.camel}Schema = v.object({\n // TODO: Define validation schema\n});\n\nexport type ${name.pascal}Schema = Infer<typeof ${name.camel}Schema>;\n`;\n}\n\n/**\n * Model template stub\n */\nexport function modelStub(\n name: Name,\n options: { tableName?: string; withResource?: boolean } = {},\n): string {\n const { tableName = `${name.plural.snake}`, withResource } = options;\n\n return `import { Model, type StrictMode } from \"@warlock.js/cascade\";\nimport { v, type Infer } from \"@warlock.js/seal\";\n${withResource ? `import { ${name.singular.pascal}Resource } from \"../../resources/${name.singular.kebab}.resource\";` : \"\"}\n\nconst ${name.singular.camel}Schema = v.object({\n // TODO: Define model schema\n});\n\nexport type ${name.singular.pascal}Type = Infer.Output<typeof ${name.singular.camel}Schema>;\n\nexport class ${name.singular.pascal} extends Model<${name.singular.pascal}Type> {\n public static table = \"${tableName}\";\n public static strictMode: StrictMode = \"fail\";\n${withResource ? ` public static resource = ${name.singular.pascal}Resource;` : \"\"}\n\n public static schema = ${name.singular.camel}Schema;\n\n public static relations = {\n // TODO: Define relations\n };\n}\n`;\n}\n\n/**\n * Repository template stub\n */\nexport function repositoryStub(name: Name): string {\n return `import type { FilterRules, RepositoryOptions, TypedRepositoryOptions } from \"@warlock.js/core\";\nimport { RepositoryManager } from \"@warlock.js/core\";\nimport { ${name.singular.pascal} } from \"../models/${name.singular.kebab}\";\n\ntype ${name.singular.pascal}ListFilter = {\n // Repository list filters\n};\n\nexport type ${name.singular.pascal}ListOptions = TypedRepositoryOptions<${name.singular.pascal}ListFilter>;\n\nexport class ${name.plural.pascal}Repository extends RepositoryManager<${name.singular.pascal}, ${name.singular.pascal}ListFilter> {\n public source = ${name.singular.pascal};\n\n protected defaultOptions: Partial<RepositoryOptions> = {};\n\n protected filterBy: FilterRules = {\n name: \"like\",\n };\n}\n\nexport const ${name.plural.camel}Repository = new ${name.plural.pascal}Repository();\n`;\n}\n\n/**\n * Resource template stub\n */\nexport function resourceStub(name: Name): string {\n return `import { Resource, type ResourceSchema } from \"@warlock.js/core\";\n\nexport class ${name.singular.pascal}Resource extends Resource {\n public static schema: ResourceSchema = {\n id: \"int\",\n name: \"string\",\n // TODO: Define resource schema\n };\n}\n`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../../../../core/src/cli/commands/generate/templates/stubs.ts"],"sourcesContent":["import type { ParsedName } from \"../types\";\nimport { Name } from \"../utils/name-parser\";\n\n/**\n * Controller template stub\n *\n * Controllers default to the guarded handler type since application\n * routes run behind `guarded()`. When `withValidation` is set, the\n * schema's exported type + value are imported directly from the\n * `schema/` folder and bound to the handler — no `requests/` alias.\n */\nexport function controllerStub(\n name: ParsedName,\n options: { withValidation?: boolean } = {},\n): string {\n const { withValidation } = options;\n\n if (!withValidation) {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\n\nexport const ${name.camel}Controller: GuardedRequestHandler = async ({ response }) => {\n // TODO: Implement controller logic\n return response.success({});\n};\n`;\n }\n\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { type ${name.pascal}Schema, ${name.camel}Schema } from \"../schema/${name.kebab}.schema\";\n\nexport const ${name.camel}Controller: GuardedRequestHandler<${name.pascal}Schema> = async ({\n response,\n}) => {\n // TODO: Implement controller logic\n return response.success({});\n};\n\n${name.camel}Controller.validation = {\n schema: ${name.camel}Schema,\n};\n`;\n}\n\n/**\n * CRUD Create Controller template\n * Note: moduleName is the module (plural), we need singular entity name\n */\nexport function crudCreateControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { type Create${moduleName.pascal}Schema, create${moduleName.pascal}Schema } from \"../schema/create-${moduleName.kebab}.schema\";\nimport { create${moduleName.pascal}Service } from \"../services/create-${moduleName.kebab}.service\";\n\nexport const create${moduleName.pascal}Controller: GuardedRequestHandler<Create${moduleName.pascal}Schema> = async ({\n request,\n response,\n}) => {\n const ${moduleName.camel} = await create${moduleName.pascal}Service(request.validated());\n\n return response.successCreate({\n ${moduleName.camel},\n });\n};\n\ncreate${moduleName.pascal}Controller.validation = {\n schema: create${moduleName.pascal}Schema,\n};\n`;\n}\n\n/**\n * CRUD Update Controller template\n */\nexport function crudUpdateControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { type Update${moduleName.pascal}Schema, update${moduleName.pascal}Schema } from \"../schema/update-${moduleName.kebab}.schema\";\nimport { update${moduleName.pascal}Service } from \"../services/update-${moduleName.kebab}.service\";\n\nexport const update${moduleName.pascal}Controller: GuardedRequestHandler<Update${moduleName.pascal}Schema> = async ({\n request,\n response,\n}) => {\n const ${moduleName.camel} = await update${moduleName.pascal}Service(request.input(\"id\"), request.validated());\n\n return response.success({\n ${moduleName.camel},\n });\n};\n\nupdate${moduleName.pascal}Controller.validation = {\n schema: update${moduleName.pascal}Schema,\n};\n`;\n}\n\n/**\n * CRUD List Controller template\n */\nexport function crudListControllerStub(moduleName: Name): string {\n const plural = moduleName.plural;\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { list${plural.pascal}Service } from \"../services/list-${plural.kebab}.service\";\n\nexport const list${plural.pascal}Controller: GuardedRequestHandler = async ({\n request,\n response,\n}) => {\n const { data, pagination } = await list${plural.pascal}Service(request.all());\n\n return response.success({\n data,\n pagination,\n });\n};\n`;\n}\n\n/**\n * CRUD Show/Get Controller template\n */\nexport function crudShowControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { get${moduleName.pascal}Service } from \"../services/get-${moduleName.kebab}.service\";\n\nexport const get${moduleName.pascal}Controller: GuardedRequestHandler = async ({\n request,\n response,\n}) => {\n const ${moduleName.camel} = await get${moduleName.pascal}Service(request.input(\"id\"));\n\n if (!${moduleName.camel}) {\n return response.notFound();\n }\n\n return response.success({\n ${moduleName.camel},\n });\n};\n`;\n}\n\n/**\n * CRUD Delete Controller template\n */\nexport function crudDeleteControllerStub(moduleName: Name): string {\n return `import { type GuardedRequestHandler } from \"app/auth/requests/guarded.request\";\nimport { delete${moduleName.pascal}Service } from \"../services/delete-${moduleName.kebab}.service\";\n\nexport const delete${moduleName.pascal}Controller: GuardedRequestHandler = async ({\n request,\n response,\n}) => {\n await delete${moduleName.pascal}Service(request.input(\"id\"));\n\n return response.noContent();\n};\n`;\n}\n\n/**\n * CRUD Routes template\n */\nexport function crudRoutesStub(moduleName: Name): string {\n const singular = moduleName.singular;\n const plural = moduleName.plural;\n\n return `import { router } from \"@warlock.js/core\";\nimport { guarded } from \"app/shared/utils/router\";\nimport { create${singular.pascal}Controller } from \"./controllers/create-${singular.kebab}.controller\";\nimport { delete${singular.pascal}Controller } from \"./controllers/delete-${singular.kebab}.controller\";\nimport { get${singular.pascal}Controller } from \"./controllers/get-${singular.kebab}.controller\";\nimport { list${plural.pascal}Controller } from \"./controllers/list-${plural.kebab}.controller\";\nimport { update${singular.pascal}Controller } from \"./controllers/update-${singular.kebab}.controller\";\n\nguarded(() => {\n router\n .route(\"/${plural.kebab}\")\n .list(list${plural.pascal}Controller)\n .show(get${singular.pascal}Controller)\n .create(create${singular.pascal}Controller)\n .update(update${singular.pascal}Controller)\n .destroy(delete${singular.pascal}Controller);\n});\n`;\n}\n\n/**\n * CRUD Model template\n */\nexport function crudModelStub(moduleName: Name): string {\n const singular = moduleName.singular;\n const plural = moduleName.plural;\n\n return `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\nimport { ${singular.pascal}Resource } from \"app/${plural.kebab}/resources/${singular.kebab}.resource\";\n\nexport const ${singular.camel}Schema = v.object({\n // TODO: Add more fields\n});\n\nexport type ${singular.pascal}Schema = Infer.Output<typeof ${singular.camel}Schema>;\n\n@RegisterModel()\nexport class ${singular.pascal} extends Model<${singular.pascal}Schema> {\n public static table = \"${plural.snake}\";\n\n public static schema = ${singular.camel}Schema;\n\n public static relations = {};\n\n public static resource = ${singular.pascal}Resource;\n}\n`;\n}\n\n/**\n * CRUD Resource template\n */\nexport function crudResourceStub(moduleName: Name): string {\n // Get singular entity name\n const entity = moduleName.singular;\n\n return `import { defineResource } from \"@warlock.js/core\";\n\nexport const ${entity.pascal}Resource = defineResource({\n schema: {\n id: \"number\",\n // TODO: Add more resource fields\n },\n});\n`;\n}\n\n/**\n * CRUD Repository template\n */\nexport function crudRepositoryStub(entity: Name): string {\n const moduleSingularName = entity.singular;\n const modulePluralName = entity.plural;\n return `import type { FilterRules, RepositoryOptions, TypedRepositoryOptions } from \"@warlock.js/core\";\nimport { RepositoryManager } from \"@warlock.js/core\";\nimport { ${moduleSingularName.pascal} } from \"../models/${moduleSingularName.kebab}\";\n\ntype ${moduleSingularName.pascal}ListFilter = {\n // Repository list filters\n};\n\nexport type ${moduleSingularName.pascal}ListOptions = TypedRepositoryOptions<${moduleSingularName.pascal}ListFilter>;\n\nclass ${modulePluralName.pascal}Repository extends RepositoryManager<${moduleSingularName.pascal}, ${moduleSingularName.pascal}ListOptions> {\n public source = ${moduleSingularName.pascal};\n\n public simpleSelectColumns: string[] = [\"id\"];\n\n public filterBy: FilterRules = {\n id: \"=\",\n };\n\n public defaultOptions: RepositoryOptions = {\n orderBy: {\n id: \"desc\",\n },\n };\n}\n\nexport const ${modulePluralName.camel}Repository = new ${modulePluralName.pascal}Repository();\n`;\n}\n\n/**\n * CRUD Create Service template\n */\nexport function crudCreateServiceStub(entity: Name): string {\n const moduleSingularName = entity.singular;\n return `import { ${moduleSingularName.pascal} } from \"../models/${moduleSingularName.kebab}\";\nimport type { Create${moduleSingularName.pascal}Schema } from \"../schema/create-${moduleSingularName.kebab}.schema\";\n\nexport async function create${moduleSingularName.pascal}Service(data: Create${moduleSingularName.pascal}Schema) {\n const ${moduleSingularName.camel} = await ${moduleSingularName.pascal}.create(data);\n return ${moduleSingularName.camel};\n}\n`;\n}\n\n/**\n * CRUD Update Service template\n */\nexport function crudUpdateServiceStub(entity: Name): string {\n const moduleSingularName = entity.singular;\n return `import { ResourceNotFoundError } from \"@warlock.js/core\";\nimport { get${moduleSingularName.pascal}Service } from \"./get-${moduleSingularName.kebab}.service\";\nimport type { Update${moduleSingularName.pascal}Schema } from \"../schema/update-${moduleSingularName.kebab}.schema\";\n\nexport async function update${moduleSingularName.pascal}Service(id: number | string, data: Update${moduleSingularName.pascal}Schema) {\n const ${moduleSingularName.camel} = await get${moduleSingularName.pascal}Service(id);\n\n await ${moduleSingularName.camel}.save({ merge: data });\n return ${moduleSingularName.camel};\n}\n`;\n}\n\n/**\n * CRUD List Service template\n */\nexport function crudListServiceStub(entity: Name): string {\n const modulePluralName = entity.plural;\n return `import { ${modulePluralName.camel}Repository } from \"../repositories/${modulePluralName.kebab}.repository\";\n\nexport async function list${modulePluralName.pascal}Service(filters: any) {\n return ${modulePluralName.camel}Repository.listCached(filters);\n}\n`;\n}\n\n/**\n * CRUD Get Service template\n */\nexport function crudGetServiceStub(entity: Name): string {\n return `import { ${entity.plural.camel}Repository } from \"../repositories/${entity.plural.kebab}.repository\";\nimport { ResourceNotFoundError } from \"@warlock.js/core\";\n\nexport async function get${entity.singular.pascal}Service(id: number | string) {\n const ${entity.singular.camel} = await ${entity.plural.camel}Repository.getCached(id);\n\n if (!${entity.singular.camel}) {\n throw new ResourceNotFoundError(\"${entity.singular.pascal} resource not found!\");\n }\n\n return ${entity.singular.camel};\n}\n`;\n}\n\n/**\n * CRUD Delete Service template\n */\nexport function crudDeleteServiceStub(entity: Name): string {\n const singular = entity.singular;\n return `import { ResourceNotFoundError } from \"@warlock.js/core\";\nimport { get${singular.pascal}Service } from \"./get-${singular.kebab}.service\";\n\nexport async function delete${singular.pascal}Service(id: number | string) {\n const ${singular.camel} = await get${singular.pascal}Service(id);\n if (!${singular.camel}) {\n throw new ResourceNotFoundError(\"${singular.pascal} not found\");\n }\n await ${singular.camel}.destroy();\n}\n`;\n}\n\n/**\n * CRUD Seed template\n */\nexport function crudSeedStub(entity: Name): string {\n return `import { seeder } from \"@warlock.js/core\";\nimport { ${entity.singular.pascal} } from \"../models/${entity.singular.kebab}\";\n\nexport default seeder({\n name: \"Seed ${entity.plural.pascal}\",\n once: true,\n // Disabled until you fill in the fields below. A generated stub calls\n // create({}) with no data, which fails required-field validation and would\n // abort the whole \\`warlock seed\\` run. Add the fields, then set this to true.\n enabled: false,\n run: async ({ track }) => {\n const total = 10;\n for (let i = 0; i < total; i++) {\n track(\n await ${entity.singular.pascal}.create({\n // TODO: Add the model's required fields here, then set enabled: true above.\n }),\n );\n }\n },\n});\n`;\n}\n\n/**\n * Migration template\n */\n/**\n * Migration Create template\n */\nexport function migrationStub(\n entityName: ParsedName,\n options: {\n columns?: string;\n imports?: string[];\n timestamps?: boolean;\n tableName?: string;\n } = {},\n): string {\n const { columns = \"\", imports = [], timestamps = true } = options;\n\n const allImports = [\"Migration\", ...imports].join(\", \");\n\n let optionsString = \"\";\n if (timestamps === false) {\n optionsString = `, { timestamps: false }`;\n }\n\n return `import { ${allImports} } from \"@warlock.js/cascade\";\nimport { ${entityName.pascal} } from \"../${entityName.kebab}.model\";\n\nexport default Migration.create(${entityName.pascal}, {\n${columns ? columns : \" // add your columns here, id is auto added to the list\"}\n}${optionsString});\n`;\n}\n\n/**\n * Migration Alter template\n */\nexport function migrationAlterStub(\n entityName: ParsedName,\n options: {\n add?: string;\n drop?: string; // stringified array like `\"[\\\"col1\\\", \\\"col2\\\"]\"`\n rename?: string; // stringified object like `{ old: \"new\" }`\n imports?: string[];\n } = {},\n): string {\n const { add = \"\", drop, rename, imports = [] } = options;\n const allImports = [\"Migration\", ...imports].join(\", \");\n\n // Build the schema object dynamically\n const schemaParts: string[] = [];\n\n if (add) {\n schemaParts.push(` add: {\\n${add}\\n },`);\n }\n\n if (drop) {\n schemaParts.push(` drop: ${drop},`);\n }\n\n if (rename) {\n schemaParts.push(` rename: ${rename},`);\n }\n\n return `import { ${allImports} } from \"@warlock.js/cascade\";\nimport { ${entityName.pascal} } from \"../${entityName.kebab}.model\";\n\nexport default Migration.alter(${entityName.pascal}, {\n${schemaParts.join(\"\\n\")}\n});\n`;\n}\n\n/**\n * CRUD Create Schema template\n * Outputs to: schema/create-{entity}.schema.ts\n */\nexport function crudCreateSchemaStub(moduleName: Name): string {\n return `import { type Infer, v } from \"@warlock.js/seal\";\n\nexport const create${moduleName.pascal}Schema = v.object({\n // TODO: Add validation rules\n});\n\nexport type Create${moduleName.pascal}Schema = Infer<typeof create${moduleName.pascal}Schema>;\n`;\n}\n\n/**\n * CRUD Update Schema template\n * Outputs to: schema/update-{entity}.schema.ts\n */\nexport function crudUpdateSchemaStub(moduleName: Name): string {\n return `import { type Infer, v } from \"@warlock.js/seal\";\n\nexport const update${moduleName.pascal}Schema = v.object({\n // TODO: Add validation rules\n});\n\nexport type Update${moduleName.pascal}Schema = Infer<typeof update${moduleName.pascal}Schema>;\n`;\n}\n\n/**\n * Service template stub\n */\nexport function serviceStub(name: Name): string {\n return `export async function ${name.camel}Service(data: any): Promise<any> {\n // TODO: Implement service logic\n throw new Error(\"${name.camel}Service not implemented\");\n}\n`;\n}\n\n/**\n * Schema template stub\n * Outputs to: schema/{name}.schema.ts\n */\nexport function schemaStub(name: Name): string {\n return `import { type Infer, v } from \"@warlock.js/seal\";\n\nexport const ${name.camel}Schema = v.object({\n // TODO: Define validation schema\n});\n\nexport type ${name.pascal}Schema = Infer<typeof ${name.camel}Schema>;\n`;\n}\n\n/**\n * Model template stub\n */\nexport function modelStub(\n name: Name,\n options: { tableName?: string; withResource?: boolean } = {},\n): string {\n const { tableName = `${name.plural.snake}`, withResource } = options;\n\n return `import { Model, type StrictMode } from \"@warlock.js/cascade\";\nimport { v, type Infer } from \"@warlock.js/seal\";\n${withResource ? `import { ${name.singular.pascal}Resource } from \"../../resources/${name.singular.kebab}.resource\";` : \"\"}\n\nconst ${name.singular.camel}Schema = v.object({\n // TODO: Define model schema\n});\n\nexport type ${name.singular.pascal}Type = Infer.Output<typeof ${name.singular.camel}Schema>;\n\nexport class ${name.singular.pascal} extends Model<${name.singular.pascal}Type> {\n public static table = \"${tableName}\";\n public static strictMode: StrictMode = \"fail\";\n${withResource ? ` public static resource = ${name.singular.pascal}Resource;` : \"\"}\n\n public static schema = ${name.singular.camel}Schema;\n\n public static relations = {\n // TODO: Define relations\n };\n}\n`;\n}\n\n/**\n * Repository template stub\n */\nexport function repositoryStub(name: Name): string {\n return `import type { FilterRules, RepositoryOptions, TypedRepositoryOptions } from \"@warlock.js/core\";\nimport { RepositoryManager } from \"@warlock.js/core\";\nimport { ${name.singular.pascal} } from \"../models/${name.singular.kebab}\";\n\ntype ${name.singular.pascal}ListFilter = {\n // Repository list filters\n};\n\nexport type ${name.singular.pascal}ListOptions = TypedRepositoryOptions<${name.singular.pascal}ListFilter>;\n\nexport class ${name.plural.pascal}Repository extends RepositoryManager<${name.singular.pascal}, ${name.singular.pascal}ListFilter> {\n public source = ${name.singular.pascal};\n\n protected defaultOptions: Partial<RepositoryOptions> = {};\n\n protected filterBy: FilterRules = {\n name: \"like\",\n };\n}\n\nexport const ${name.plural.camel}Repository = new ${name.plural.pascal}Repository();\n`;\n}\n\n/**\n * Resource template stub\n */\nexport function resourceStub(name: Name): string {\n return `import { Resource, type ResourceSchema } from \"@warlock.js/core\";\n\nexport class ${name.singular.pascal}Resource extends Resource {\n public static schema: ResourceSchema = {\n id: \"int\",\n name: \"string\",\n // TODO: Define resource schema\n };\n}\n`;\n}\n\n/**\n * Use-case template stub\n * Outputs to: use-cases/{name}.use-case.ts\n *\n * A use-case takes validated input and an actor, never a request or response,\n * so a page action and a controller can both call it.\n */\nexport function useCaseStub(name: Name): string {\n return `import type { ${name.pascal}Schema } from \"../schema/${name.kebab}.schema\";\n\nexport type ${name.pascal}Actor = { id: number | string };\n\nexport type ${name.pascal}Result =\n | { code: \"OK\" }\n | { code: \"FORBIDDEN\" }\n | { code: \"NOT_FOUND\" };\n\nexport async function ${name.camel}UseCase(\n input: ${name.pascal}Schema,\n actor: ${name.pascal}Actor,\n): Promise<${name.pascal}Result> {\n // TODO: Implement use-case logic\n void input;\n void actor;\n\n return { code: \"OK\" };\n}\n`;\n}\n\n/**\n * Use-case spec template stub\n * Outputs to: use-cases/{name}.use-case.spec.ts\n */\nexport function useCaseSpecStub(name: Name): string {\n return `import { describe, expect, it } from \"vitest\";\nimport { ${name.camel}UseCase } from \"./${name.kebab}.use-case\";\n\ndescribe(\"${name.camel}UseCase\", () => {\n it(\"returns OK for a valid input\", async () => {\n const result = await ${name.camel}UseCase({} as never, { id: 1 });\n\n expect(result.code).toBe(\"OK\");\n });\n});\n`;\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { createDatabaseCommand } from "./commands/create-database.command.mjs";
|
|
|
4
4
|
import { devServerCommand } from "./commands/dev-server.command.mjs";
|
|
5
5
|
import { doctorCommand } from "./commands/doctor.command.mjs";
|
|
6
6
|
import { dropTablesCommand } from "./commands/drop-tables.command.mjs";
|
|
7
|
-
import { generateCommand, generateControllerCommand, generateMigrationCommand, generateModelCommand, generateModuleCommand, generateRepositoryCommand, generateResourceCommand, generateServiceCommand } from "./commands/generate/generate.command.mjs";
|
|
7
|
+
import { generateCommand, generateControllerCommand, generateMigrationCommand, generateModelCommand, generateModuleCommand, generateRepositoryCommand, generateResourceCommand, generateServiceCommand, generateUseCaseCommand } from "./commands/generate/generate.command.mjs";
|
|
8
8
|
import { migrateCommand } from "./commands/migrate.command.mjs";
|
|
9
9
|
import { routesCommand, routesDiffCommand } from "./commands/routes.command.mjs";
|
|
10
10
|
import { seedCommand } from "./commands/seed.command.mjs";
|
|
@@ -32,6 +32,7 @@ const frameworkCommands = [
|
|
|
32
32
|
generateModuleCommand,
|
|
33
33
|
generateControllerCommand,
|
|
34
34
|
generateServiceCommand,
|
|
35
|
+
generateUseCaseCommand,
|
|
35
36
|
generateModelCommand,
|
|
36
37
|
generateRepositoryCommand,
|
|
37
38
|
generateResourceCommand,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-cli-commands.mjs","names":[],"sources":["../../../../../../../core/src/cli/framework-cli-commands.ts"],"sourcesContent":["import { addCommand } from \"./commands/add.command\";\
|
|
1
|
+
{"version":3,"file":"framework-cli-commands.mjs","names":[],"sources":["../../../../../../../core/src/cli/framework-cli-commands.ts"],"sourcesContent":["import { addCommand } from \"./commands/add.command\";\nimport { buildCommand } from \"./commands/build.command\";\nimport { createDatabaseCommand } from \"./commands/create-database.command\";\nimport { devServerCommand } from \"./commands/dev-server.command\";\nimport { doctorCommand } from \"./commands/doctor.command\";\nimport { dropTablesCommand } from \"./commands/drop-tables.command\";\nimport {\n generateCommand,\n generateControllerCommand,\n generateMigrationCommand,\n generateModelCommand,\n generateModuleCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n} from \"./commands/generate/generate.command\";\nimport { migrateCommand } from \"./commands/migrate.command\";\nimport { routesCommand, routesDiffCommand } from \"./commands/routes.command\";\nimport { seedCommand } from \"./commands/seed.command\";\nimport { startProductionCommand } from \"./commands/start-production.command\";\nimport { storagePutCommand } from \"./commands/storage-put.command\";\nimport { typingsGeneratorCommand } from \"./commands/typings-generator.command\";\nimport { updateCommand } from \"./commands/update.command\";\n\nexport const frameworkCommands = [\n // development commands\n devServerCommand,\n typingsGeneratorCommand,\n\n // diagnostics commands\n doctorCommand,\n routesCommand,\n routesDiffCommand,\n\n // production commands\n buildCommand,\n startProductionCommand,\n\n // database commands\n migrateCommand,\n seedCommand,\n createDatabaseCommand,\n dropTablesCommand,\n\n // generation/installation commands\n addCommand,\n updateCommand,\n\n // scaffolding commands\n generateCommand,\n generateModuleCommand,\n generateControllerCommand,\n generateServiceCommand,\n generateUseCaseCommand,\n generateModelCommand,\n generateRepositoryCommand,\n generateResourceCommand,\n generateMigrationCommand,\n\n // storage commands\n storagePutCommand,\n];\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,MAAa,oBAAoB;CAE/B;CACA;CAGA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CAGA;CACA;CAGA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAGA;AACF"}
|
|
@@ -108,7 +108,7 @@ async function addCommandAction(options) {
|
|
|
108
108
|
* Install the resolved dependency sets through the project's package manager.
|
|
109
109
|
* Runs two passes (prod then dev) so each lands in the correct section.
|
|
110
110
|
*/
|
|
111
|
-
async function installDependencies(packageManager, dependencies, devDependencies) {
|
|
111
|
+
async function installDependencies(packageManager, dependencies, devDependencies, exec = execSync) {
|
|
112
112
|
const resolvedPackageManager = packageManager ?? await detectPackageManager();
|
|
113
113
|
const packageManagerCommand = getAddCommand(resolvedPackageManager);
|
|
114
114
|
const exactPackageManagerCommand = getExactAddCommand(resolvedPackageManager);
|
|
@@ -122,7 +122,7 @@ async function installDependencies(packageManager, dependencies, devDependencies
|
|
|
122
122
|
const developmentFlag = development ? " -D" : "";
|
|
123
123
|
const runInstall = (command, specs) => {
|
|
124
124
|
if (specs.length === 0) return;
|
|
125
|
-
|
|
125
|
+
exec(`${command} ${specs.join(" ")}${developmentFlag}`, {
|
|
126
126
|
cwd: process.cwd(),
|
|
127
127
|
stdio: "inherit"
|
|
128
128
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport {\r\n detectPackageManager,\r\n getAddCommand,\r\n getExactAddCommand,\r\n type PackageManager,\r\n} from \"../updater/package-manager\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport { featuresMap } from \"./features\";\r\nimport { mergePnpmSharpApproval } from \"./pnpm-build-approvals\";\r\n\r\nexport { featuresMap };\r\nexport type { FeatureDefinition } from \"./features\";\r\n\r\n/**\r\n * The parts of a project `package.json` this action reads or writes. Deliberately\r\n * partial — it describes what we touch, not the whole manifest.\r\n */\r\ntype ProjectPackageJson = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n scripts?: Record<string, string>;\r\n};\r\n\r\nexport const allowedFeatures = Object.keys(featuresMap);\r\n\r\nfunction isWarlockPackage(packageName: string): boolean {\r\n return packageName.startsWith(\"@warlock.js/\");\r\n}\r\n\r\n/**\r\n * Resolve the internal feature-map placeholder to the version of Core that is\r\n * actually executing the command. Every @warlock.js package is lockstep, so\r\n * Core is the single source of truth; non-Warlock dependencies are untouched.\r\n */\r\nexport function resolveWarlockDependencyVersions(\r\n dependencies: Record<string, string>,\r\n frameworkVersion: string,\r\n): void {\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (isWarlockPackage(dependency)) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n}\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n // `addCommandAction` runs `validateFeatures()` before calling this, and the\r\n // recursive calls below pass `requires` entries that come out of the map\r\n // itself — so every lookup resolves. Skipping an unresolvable one keeps a\r\n // typo from crashing the command with `Cannot read properties of\r\n // undefined`, which names neither the feature nor the fact that it was the\r\n // ARGUMENT that was wrong.\r\n if (def === undefined) continue;\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n const definition = featuresMap[feature];\r\n\r\n // `allowedFeatures` is derived from this same map, so this resolves.\r\n if (definition === undefined) continue;\r\n\r\n console.log(`- ${colors.yellowBright(feature)}: ${colors.green(definition.description)}`);\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n\r\n // Validated upstream; see the note in resolveFeatures above.\r\n if (featurePackages === undefined) continue;\r\n\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n resolveWarlockDependencyVersions(dependencies, frameworkVersion);\r\n\r\n const currentPackageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n const resolvedPackageManager =\r\n (packageManager as PackageManager | undefined) ?? (await detectPackageManager());\r\n if (resolvedPackageManager === \"pnpm\" && dependencies.sharp !== undefined) {\r\n const workspacePath = rootPath(\"pnpm-workspace.yaml\");\r\n const current = (await fileExistsAsync(workspacePath))\r\n ? await getFileAsync(workspacePath)\r\n : \"\";\r\n await putFileAsync(workspacePath, mergePnpmSharpApproval(current));\r\n }\r\n await installDependencies(\r\n packageManager as PackageManager | undefined,\r\n dependencies,\r\n devDependencies,\r\n );\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n\r\n if (featurePackages?.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nexport async function installDependencies(\r\n packageManager: PackageManager | undefined,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n // `--package-manager` is optional; without it, fall back to the lockfile.\r\n const resolvedPackageManager = packageManager ?? (await detectPackageManager());\r\n const packageManagerCommand = getAddCommand(resolvedPackageManager);\r\n const exactPackageManagerCommand = getExactAddCommand(resolvedPackageManager);\r\n\r\n const installDependencySet = (dependencySet: Record<string, string>, development: boolean) => {\r\n const regularSpecs: string[] = [];\r\n const exactWarlockSpecs: string[] = [];\r\n\r\n for (const [name, version] of Object.entries(dependencySet)) {\r\n const spec = `${name}@${version}`;\r\n (isWarlockPackage(name) ? exactWarlockSpecs : regularSpecs).push(spec);\r\n }\r\n\r\n const developmentFlag = development ? \" -D\" : \"\";\r\n const runInstall = (command: string, specs: string[]) => {\r\n if (specs.length === 0) return;\r\n\r\n execSync(`${command} ${specs.join(\" \")}${developmentFlag}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n };\r\n\r\n runInstall(packageManagerCommand, regularSpecs);\r\n runInstall(exactPackageManagerCommand, exactWarlockSpecs);\r\n };\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n installDependencySet(dependencies, false);\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n installDependencySet(devDependencies, true);\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport type { CommandActionData } from \"../commands/types\";\r\nimport {\r\n detectPackageManager,\r\n getAddCommand,\r\n getExactAddCommand,\r\n type PackageManager,\r\n} from \"../updater/package-manager\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport { featuresMap } from \"./features\";\r\nimport { mergePnpmSharpApproval } from \"./pnpm-build-approvals\";\r\n\r\nexport { featuresMap };\r\nexport type { FeatureDefinition } from \"./features\";\r\n\r\n/**\r\n * The parts of a project `package.json` this action reads or writes. Deliberately\r\n * partial — it describes what we touch, not the whole manifest.\r\n */\r\ntype ProjectPackageJson = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n scripts?: Record<string, string>;\r\n};\r\n\r\nexport const allowedFeatures = Object.keys(featuresMap);\r\n\r\nfunction isWarlockPackage(packageName: string): boolean {\r\n return packageName.startsWith(\"@warlock.js/\");\r\n}\r\n\r\n/**\r\n * Resolve the internal feature-map placeholder to the version of Core that is\r\n * actually executing the command. Every @warlock.js package is lockstep, so\r\n * Core is the single source of truth; non-Warlock dependencies are untouched.\r\n */\r\nexport function resolveWarlockDependencyVersions(\r\n dependencies: Record<string, string>,\r\n frameworkVersion: string,\r\n): void {\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (isWarlockPackage(dependency)) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n}\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n // `addCommandAction` runs `validateFeatures()` before calling this, and the\r\n // recursive calls below pass `requires` entries that come out of the map\r\n // itself — so every lookup resolves. Skipping an unresolvable one keeps a\r\n // typo from crashing the command with `Cannot read properties of\r\n // undefined`, which names neither the feature nor the fact that it was the\r\n // ARGUMENT that was wrong.\r\n if (def === undefined) continue;\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n const definition = featuresMap[feature];\r\n\r\n // `allowedFeatures` is derived from this same map, so this resolves.\r\n if (definition === undefined) continue;\r\n\r\n console.log(`- ${colors.yellowBright(feature)}: ${colors.green(definition.description)}`);\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n\r\n // Validated upstream; see the note in resolveFeatures above.\r\n if (featurePackages === undefined) continue;\r\n\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n resolveWarlockDependencyVersions(dependencies, frameworkVersion);\r\n\r\n const currentPackageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n const resolvedPackageManager =\r\n (packageManager as PackageManager | undefined) ?? (await detectPackageManager());\r\n if (resolvedPackageManager === \"pnpm\" && dependencies.sharp !== undefined) {\r\n const workspacePath = rootPath(\"pnpm-workspace.yaml\");\r\n const current = (await fileExistsAsync(workspacePath))\r\n ? await getFileAsync(workspacePath)\r\n : \"\";\r\n await putFileAsync(workspacePath, mergePnpmSharpApproval(current));\r\n }\r\n await installDependencies(\r\n packageManager as PackageManager | undefined,\r\n dependencies,\r\n devDependencies,\r\n );\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n\r\n if (featurePackages?.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nexport async function installDependencies(\r\n packageManager: PackageManager | undefined,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n exec: (command: string, options: { cwd: string; stdio: \"inherit\" }) => unknown = execSync,\r\n) {\r\n // `--package-manager` is optional; without it, fall back to the lockfile.\r\n const resolvedPackageManager = packageManager ?? (await detectPackageManager());\r\n const packageManagerCommand = getAddCommand(resolvedPackageManager);\r\n const exactPackageManagerCommand = getExactAddCommand(resolvedPackageManager);\r\n\r\n const installDependencySet = (dependencySet: Record<string, string>, development: boolean) => {\r\n const regularSpecs: string[] = [];\r\n const exactWarlockSpecs: string[] = [];\r\n\r\n for (const [name, version] of Object.entries(dependencySet)) {\r\n const spec = `${name}@${version}`;\r\n (isWarlockPackage(name) ? exactWarlockSpecs : regularSpecs).push(spec);\r\n }\r\n\r\n const developmentFlag = development ? \" -D\" : \"\";\r\n const runInstall = (command: string, specs: string[]) => {\r\n if (specs.length === 0) return;\r\n\r\n exec(`${command} ${specs.join(\" \")}${developmentFlag}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n };\r\n\r\n runInstall(packageManagerCommand, regularSpecs);\r\n runInstall(exactPackageManagerCommand, exactWarlockSpecs);\r\n };\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n installDependencySet(dependencies, false);\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n installDependencySet(devDependencies, true);\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -738,10 +738,10 @@ export { register } from "./index.register";
|
|
|
738
738
|
* where the file lives. A page file with
|
|
739
739
|
* no \`config.route\` declaration derives its URL from the filesystem.
|
|
740
740
|
*/
|
|
741
|
-
export const config = {
|
|
741
|
+
export const config: PageConfig = {
|
|
742
742
|
route: { path: "/", name: "index" },
|
|
743
743
|
metadata: { title: "Home" },
|
|
744
|
-
}
|
|
744
|
+
};
|
|
745
745
|
|
|
746
746
|
const contactSchema = v.object({
|
|
747
747
|
name: v.string().min(2),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../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/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\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\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 */\nconst notificationSchema = v.object({\n user_id: v.string(),\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 /**\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 + accessors all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\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. Spread it to add your own columns\n * (remember to mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\nimport { inApp, type Id } from \"@warlock.js/notifications\";\n\n/**\n * The authenticated user's notification HTTP surface — thin wrappers over the\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\n * rows). Notifications are produced by domain events, never over HTTP, so there\n * is no create. Trim or split these as your app grows.\n */\n\n/**\n * Read \\`id\\` off \\`request.locals.user\\` without assuming this app's\n * \\`RequestUser\\` augmentation declares it — \\`RequestUser\\` (declared by\n * \\`@warlock.js/auth\\`) is empty by default, so a narrow runtime read survives\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\n * \\`request.locals.user\\` itself — also skips a needless \\`Notifiable\\` cast.\n */\nfunction recipientId(user: unknown): Id {\n if (user && typeof user === \"object\" && \"id\" in user) {\n const id = (user as { id?: unknown }).id;\n\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n\n throw new Error(\"Authenticated request is missing a usable user id\");\n}\n\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(recipientId(request.locals.user), request.all());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\n const id = request.input(\"id\");\n const userId = recipientId(request.locals.user);\n\n await inApp.markAsRead(userId, id);\n const notification = await inApp.find(userId, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n\n/** DELETE /notifications — dismiss all for the user. */\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user));\n\n return response.noContent();\n};\n\nclearNotificationsController.description = \"Clear notifications\";\n\n/** DELETE /notifications/:id — dismiss one. */\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user), request.input(\"id\"));\n\n return response.noContent();\n};\n\ndeleteNotificationController.description = \"Delete notification\";\n`;\n\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport {\n clearNotificationsController,\n deleteNotificationController,\n listNotificationsController,\n markAllNotificationsReadController,\n markNotificationReadController,\n unreadNotificationsCountController,\n} from \"./controllers/notifications.controller\";\n\n/**\n * Notification routes — the authenticated user's read + dismiss surface.\n *\n * Notifications are produced by domain events (never created over HTTP), so\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\n * don't need; if your app reads notifications over sockets/GraphQL instead,\n * delete this file + the controllers entirely.\n */\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\n router.get(\"/\", listNotificationsController);\n router.get(\"/unread-count\", unreadNotificationsCountController);\n router.patch(\"/read-all\", markAllNotificationsReadController);\n router.patch(\"/:id/read\", markNotificationReadController);\n router.delete(\"/\", clearNotificationsController);\n router.delete(\"/:id\", deleteNotificationController);\n});\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 * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\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 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 { type Infer, v } 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);\n`;\n\n/**\n * `src/web/index.register.ts` — universal static setup for the starter page.\n *\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\n * still sees it in both realms without making React Fast Refresh treat every\n * JSX edit as an incompatible function-export replacement.\n */\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\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 PageConfig, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nexport { register } from \"./index.register\";\n\n/**\n * A page route is an ordinary Warlock route whose handler renders React\n * instead of returning JSON.\n *\n * The URL and stable hydration name are the ones this file DECLARES below.\n * This page answers \\`GET \"/\"\\` because \\`config.route.path = \"/\"\\`, not because of\n * where the file lives. A page file with\n * no \\`config.route\\` declaration derives its URL from the filesystem.\n */\nexport const config = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n} as const satisfies PageConfig;\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/**\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\n * \\`data\\`, typed:\n *\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\n */\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../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/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\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\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 */\nconst notificationSchema = v.object({\n user_id: v.string(),\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 /**\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 + accessors all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\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. Spread it to add your own columns\n * (remember to mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\nimport { inApp, type Id } from \"@warlock.js/notifications\";\n\n/**\n * The authenticated user's notification HTTP surface — thin wrappers over the\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\n * rows). Notifications are produced by domain events, never over HTTP, so there\n * is no create. Trim or split these as your app grows.\n */\n\n/**\n * Read \\`id\\` off \\`request.locals.user\\` without assuming this app's\n * \\`RequestUser\\` augmentation declares it — \\`RequestUser\\` (declared by\n * \\`@warlock.js/auth\\`) is empty by default, so a narrow runtime read survives\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\n * \\`request.locals.user\\` itself — also skips a needless \\`Notifiable\\` cast.\n */\nfunction recipientId(user: unknown): Id {\n if (user && typeof user === \"object\" && \"id\" in user) {\n const id = (user as { id?: unknown }).id;\n\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n\n throw new Error(\"Authenticated request is missing a usable user id\");\n}\n\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(recipientId(request.locals.user), request.all());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\n const id = request.input(\"id\");\n const userId = recipientId(request.locals.user);\n\n await inApp.markAsRead(userId, id);\n const notification = await inApp.find(userId, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n\n/** DELETE /notifications — dismiss all for the user. */\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user));\n\n return response.noContent();\n};\n\nclearNotificationsController.description = \"Clear notifications\";\n\n/** DELETE /notifications/:id — dismiss one. */\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user), request.input(\"id\"));\n\n return response.noContent();\n};\n\ndeleteNotificationController.description = \"Delete notification\";\n`;\n\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport {\n clearNotificationsController,\n deleteNotificationController,\n listNotificationsController,\n markAllNotificationsReadController,\n markNotificationReadController,\n unreadNotificationsCountController,\n} from \"./controllers/notifications.controller\";\n\n/**\n * Notification routes — the authenticated user's read + dismiss surface.\n *\n * Notifications are produced by domain events (never created over HTTP), so\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\n * don't need; if your app reads notifications over sockets/GraphQL instead,\n * delete this file + the controllers entirely.\n */\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\n router.get(\"/\", listNotificationsController);\n router.get(\"/unread-count\", unreadNotificationsCountController);\n router.patch(\"/read-all\", markAllNotificationsReadController);\n router.patch(\"/:id/read\", markNotificationReadController);\n router.delete(\"/\", clearNotificationsController);\n router.delete(\"/:id\", deleteNotificationController);\n});\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 * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\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 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 { type Infer, v } 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);\n`;\n\n/**\n * `src/web/index.register.ts` — universal static setup for the starter page.\n *\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\n * still sees it in both realms without making React Fast Refresh treat every\n * JSX edit as an incompatible function-export replacement.\n */\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\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 PageConfig, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nexport { register } from \"./index.register\";\n\n/**\n * A page route is an ordinary Warlock route whose handler renders React\n * instead of returning JSON.\n *\n * The URL and stable hydration name are the ones this file DECLARES below.\n * This page answers \\`GET \"/\"\\` because \\`config.route.path = \"/\"\\`, not because of\n * where the file lives. A page file with\n * no \\`config.route\\` declaration derives its URL from the filesystem.\n */\nexport const config: PageConfig = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n};\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/**\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\n * \\`data\\`, typed:\n *\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\n */\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
@@ -22,9 +22,20 @@ type RateLimitOptions = {
|
|
|
22
22
|
*/
|
|
23
23
|
keyGenerator?: (request: Request) => string;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Key the bucket to the signed-in user (`request.locals.user.id`). Works on
|
|
26
|
+
* routes and page actions alike. `keyGenerator` wins when both are set.
|
|
26
27
|
*/
|
|
27
|
-
|
|
28
|
+
key?: "user";
|
|
29
|
+
/**
|
|
30
|
+
* With `key: "user"`, what to do for unauthenticated requests: bucket them
|
|
31
|
+
* by IP (default) or skip limiting entirely.
|
|
32
|
+
*/
|
|
33
|
+
guests?: "ip" | "skip";
|
|
34
|
+
/**
|
|
35
|
+
* Override the default error message. Pass a function to build it per
|
|
36
|
+
* request, e.g. to translate it into the visitor's locale.
|
|
37
|
+
*/
|
|
38
|
+
errorMessage?: string | ((request: Request) => string);
|
|
28
39
|
};
|
|
29
40
|
/**
|
|
30
41
|
* Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"mappings":";;;;;;KAQY,gBAAA;EAAgB;;;EAI1B,GAAA;EAIA;;;EAAA,QAAA;
|
|
1
|
+
{"version":3,"file":"rate-limit.middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"mappings":";;;;;;KAQY,gBAAA;EAAgB;;;EAI1B,GAAA;EAIA;;;EAAA,QAAA;EAaA;;;;;;AAU0C;EAf1C,YAAA,IAAgB,OAAA,EAAS,OAAA;EAoEQ;;;;EA/DjC,GAAA;EA+D8D;;AAAU;;EA1DxE,MAAA;;;;;EAKA,YAAA,cAA0B,OAAA,EAAS,OAAO;AAAA;;;;;;;;;;;;;;;;;;;;;iBAqD5B,mBAAA,CAAoB,OAAA,EAAS,gBAAA,GAAmB,UAAU"}
|
|
@@ -4,6 +4,12 @@ import { t } from "./inject-request-context.mjs";
|
|
|
4
4
|
//#region ../core/src/http/middleware/rate-limit.middleware.ts
|
|
5
5
|
const buckets = /* @__PURE__ */ new Map();
|
|
6
6
|
/**
|
|
7
|
+
* Each `rateLimitMiddleware()` call is its own limiter. Page actions share one
|
|
8
|
+
* route, so without this two limits on the same page (say `like` and
|
|
9
|
+
* `comment`) would count into one bucket.
|
|
10
|
+
*/
|
|
11
|
+
let nextLimiterId = 0;
|
|
12
|
+
/**
|
|
7
13
|
* Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys
|
|
8
14
|
* (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).
|
|
9
15
|
* Called opportunistically, at most once per minute, not on a timer — keeps
|
|
@@ -34,14 +40,21 @@ function pruneExpired(now) {
|
|
|
34
40
|
* });
|
|
35
41
|
*/
|
|
36
42
|
function rateLimitMiddleware(options) {
|
|
43
|
+
const limiterId = nextLimiterId++;
|
|
37
44
|
return ({ request, response }) => {
|
|
38
45
|
const now = Date.now();
|
|
39
46
|
if (now - lastPruneAt > 6e4) {
|
|
40
47
|
pruneExpired(now);
|
|
41
48
|
lastPruneAt = now;
|
|
42
49
|
}
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
let userKey;
|
|
51
|
+
if (options.key === "user" && !options.keyGenerator) {
|
|
52
|
+
const id = request.locals?.user?.id;
|
|
53
|
+
if (id !== void 0 && id !== null && id !== "") userKey = `user:${String(id)}`;
|
|
54
|
+
else if (options.guests === "skip") return;
|
|
55
|
+
}
|
|
56
|
+
const groupKey = options.keyGenerator?.(request) || userKey || request.detectIp() || "unknown";
|
|
57
|
+
const cacheKey = `${limiterId}:${request.route.path}:${groupKey}`;
|
|
45
58
|
let bucket = buckets.get(cacheKey);
|
|
46
59
|
if (!bucket || bucket.resetAt <= now) {
|
|
47
60
|
bucket = {
|
|
@@ -59,7 +72,7 @@ function rateLimitMiddleware(options) {
|
|
|
59
72
|
if (bucket.count > options.max) {
|
|
60
73
|
response.header("Retry-After", retryAfter);
|
|
61
74
|
return response.tooManyRequests({
|
|
62
|
-
error: options.errorMessage || t("http.rateLimitExceeded"),
|
|
75
|
+
error: (typeof options.errorMessage === "function" ? options.errorMessage(request) : options.errorMessage) || t("http.rateLimitExceeded"),
|
|
63
76
|
errorCode: "EC102"
|
|
64
77
|
});
|
|
65
78
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the per-route rate limit middleware.\n */\nexport type RateLimitOptions = {\n /**\n * Max requests allowed within the time window.\n */\n max: number;\n /**\n * Time window in milliseconds.\n */\n duration: number;\n /**\n * Group key generator. Defaults to the client IP. Override to scope per-user,\n * per-organization, per-tenant, etc.\n *\n * @example\n * keyGenerator: (request) => request.locals.user?.id ?? request.ip,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Override the default error message.\n */\n errorMessage?: string;\n};\n\ntype Bucket = {\n count: number;\n resetAt: number;\n};\n\nconst buckets = new Map<string, Bucket>();\n\n/**\n * Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys\n * (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).\n * Called opportunistically, at most once per minute, not on a timer — keeps\n * the module side-effect-free for testing.\n */\nlet lastPruneAt = 0;\n\nfunction pruneExpired(now: number) {\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {\n buckets.delete(key);\n }\n }\n}\n\n/**\n * Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s\n * global cap (configured via `http.rateLimit`) — both run, and either can\n * 429. Use this for endpoints that need a tighter cap than the global default:\n * login, OTP request, password reset, expensive search, AI completions.\n *\n * The counter of THIS middleware always lives in-process. With `N` replicas\n * its effective cap is `N × max`. The global limit can be shared across\n * replicas by setting `http.rateLimit.redis` (an ioredis instance); that\n * does not affect this per-route middleware.\n *\n * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on\n * every response; adds `Retry-After` (seconds) on a 429.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n * router.post(\"/auth/login\", loginController, {\n * middleware: [middleware.rateLimit({ max: 5, duration: 60_000 })],\n * });\n */\nexport function rateLimitMiddleware(options: RateLimitOptions): Middleware {\n return ({ request, response }) => {\n const now = Date.now();\n\n if (now - lastPruneAt > 60_000) {\n pruneExpired(now);\n lastPruneAt = now;\n }\n\n const groupKey = options.keyGenerator?.(request) || request.detectIp() || \"unknown\";\n const cacheKey = `${request.route.path}:${groupKey}`;\n\n let bucket = buckets.get(cacheKey);\n\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + options.duration };\n buckets.set(cacheKey, bucket);\n }\n\n bucket.count++;\n\n const remaining = Math.max(0, options.max - bucket.count);\n const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));\n\n response.header(\"X-RateLimit-Limit\", options.max);\n response.header(\"X-RateLimit-Remaining\", remaining);\n response.header(\"X-RateLimit-Reset\", Math.ceil(bucket.resetAt / 1000));\n\n if (bucket.count > options.max) {\n response.header(\"Retry-After\", retryAfter);\n\n return response.tooManyRequests({\n error: options.errorMessage || t(\"http.rateLimitExceeded\"),\n errorCode: HttpErrorCodes.RateLimitExceeded,\n });\n }\n };\n}\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"rate-limit.middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/rate-limit.middleware.ts"],"sourcesContent":["import type { Middleware } from \"../../router\";\nimport { HttpErrorCodes } from \"../error-codes\";\nimport type { Request } from \"../request\";\nimport { t } from \"./inject-request-context\";\n\n/**\n * Options for the per-route rate limit middleware.\n */\nexport type RateLimitOptions = {\n /**\n * Max requests allowed within the time window.\n */\n max: number;\n /**\n * Time window in milliseconds.\n */\n duration: number;\n /**\n * Group key generator. Defaults to the client IP. Override to scope per-user,\n * per-organization, per-tenant, etc.\n *\n * @example\n * keyGenerator: (request) => request.locals.user?.id ?? request.ip,\n */\n keyGenerator?: (request: Request) => string;\n /**\n * Key the bucket to the signed-in user (`request.locals.user.id`). Works on\n * routes and page actions alike. `keyGenerator` wins when both are set.\n */\n key?: \"user\";\n /**\n * With `key: \"user\"`, what to do for unauthenticated requests: bucket them\n * by IP (default) or skip limiting entirely.\n */\n guests?: \"ip\" | \"skip\";\n /**\n * Override the default error message. Pass a function to build it per\n * request, e.g. to translate it into the visitor's locale.\n */\n errorMessage?: string | ((request: Request) => string);\n};\n\ntype Bucket = {\n count: number;\n resetAt: number;\n};\n\nconst buckets = new Map<string, Bucket>();\n\n/**\n * Each `rateLimitMiddleware()` call is its own limiter. Page actions share one\n * route, so without this two limits on the same page (say `like` and\n * `comment`) would count into one bucket.\n */\nlet nextLimiterId = 0;\n\n/**\n * Sweep expired buckets so the Map doesn't grow unbounded with one-shot keys\n * (rare per-user keys, dynamic path parameters in `keyGenerator`, etc.).\n * Called opportunistically, at most once per minute, not on a timer — keeps\n * the module side-effect-free for testing.\n */\nlet lastPruneAt = 0;\n\nfunction pruneExpired(now: number) {\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {\n buckets.delete(key);\n }\n }\n}\n\n/**\n * Per-route / per-group rate limit. Layers on top of `@fastify/rate-limit`'s\n * global cap (configured via `http.rateLimit`) — both run, and either can\n * 429. Use this for endpoints that need a tighter cap than the global default:\n * login, OTP request, password reset, expensive search, AI completions.\n *\n * The counter of THIS middleware always lives in-process. With `N` replicas\n * its effective cap is `N × max`. The global limit can be shared across\n * replicas by setting `http.rateLimit.redis` (an ioredis instance); that\n * does not affect this per-route middleware.\n *\n * Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on\n * every response; adds `Retry-After` (seconds) on a 429.\n *\n * @example\n * import { middleware } from \"@warlock.js/core\";\n * router.post(\"/auth/login\", loginController, {\n * middleware: [middleware.rateLimit({ max: 5, duration: 60_000 })],\n * });\n */\nexport function rateLimitMiddleware(options: RateLimitOptions): Middleware {\n const limiterId = nextLimiterId++;\n\n return ({ request, response }) => {\n const now = Date.now();\n\n if (now - lastPruneAt > 60_000) {\n pruneExpired(now);\n lastPruneAt = now;\n }\n\n let userKey: string | undefined;\n\n if (options.key === \"user\" && !options.keyGenerator) {\n const id = (request.locals as { user?: { id?: unknown } } | undefined)?.user?.id;\n\n if (id !== undefined && id !== null && id !== \"\") {\n userKey = `user:${String(id)}`;\n } else if (options.guests === \"skip\") {\n return;\n }\n }\n\n const groupKey = options.keyGenerator?.(request) || userKey || request.detectIp() || \"unknown\";\n const cacheKey = `${limiterId}:${request.route.path}:${groupKey}`;\n\n let bucket = buckets.get(cacheKey);\n\n if (!bucket || bucket.resetAt <= now) {\n bucket = { count: 0, resetAt: now + options.duration };\n buckets.set(cacheKey, bucket);\n }\n\n bucket.count++;\n\n const remaining = Math.max(0, options.max - bucket.count);\n const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));\n\n response.header(\"X-RateLimit-Limit\", options.max);\n response.header(\"X-RateLimit-Remaining\", remaining);\n response.header(\"X-RateLimit-Reset\", Math.ceil(bucket.resetAt / 1000));\n\n if (bucket.count > options.max) {\n response.header(\"Retry-After\", retryAfter);\n\n return response.tooManyRequests({\n error:\n (typeof options.errorMessage === \"function\"\n ? options.errorMessage(request)\n : options.errorMessage) || t(\"http.rateLimitExceeded\"),\n errorCode: HttpErrorCodes.RateLimitExceeded,\n });\n }\n };\n}\n"],"mappings":";;;;AA+CA,MAAM,0BAAU,IAAI,IAAoB;;;;;;AAOxC,IAAI,gBAAgB;;;;;;;AAQpB,IAAI,cAAc;AAElB,SAAS,aAAa,KAAa;CACjC,KAAK,MAAM,CAAC,KAAK,WAAW,SAC1B,IAAI,OAAO,WAAW,KACpB,QAAQ,OAAO,GAAG;AAGxB;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,oBAAoB,SAAuC;CACzE,MAAM,YAAY;CAElB,QAAQ,EAAE,SAAS,eAAe;EAChC,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,MAAM,cAAc,KAAQ;GAC9B,aAAa,GAAG;GAChB,cAAc;EAChB;EAEA,IAAI;EAEJ,IAAI,QAAQ,QAAQ,UAAU,CAAC,QAAQ,cAAc;GACnD,MAAM,KAAM,QAAQ,QAAoD,MAAM;GAE9E,IAAI,OAAO,UAAa,OAAO,QAAQ,OAAO,IAC5C,UAAU,QAAQ,OAAO,EAAE;QACtB,IAAI,QAAQ,WAAW,QAC5B;EAEJ;EAEA,MAAM,WAAW,QAAQ,eAAe,OAAO,KAAK,WAAW,QAAQ,SAAS,KAAK;EACrF,MAAM,WAAW,GAAG,UAAU,GAAG,QAAQ,MAAM,KAAK,GAAG;EAEvD,IAAI,SAAS,QAAQ,IAAI,QAAQ;EAEjC,IAAI,CAAC,UAAU,OAAO,WAAW,KAAK;GACpC,SAAS;IAAE,OAAO;IAAG,SAAS,MAAM,QAAQ;GAAS;GACrD,QAAQ,IAAI,UAAU,MAAM;EAC9B;EAEA,OAAO;EAEP,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,MAAM,OAAO,KAAK;EACxD,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,UAAU,OAAO,GAAI,CAAC;EAEvE,SAAS,OAAO,qBAAqB,QAAQ,GAAG;EAChD,SAAS,OAAO,yBAAyB,SAAS;EAClD,SAAS,OAAO,qBAAqB,KAAK,KAAK,OAAO,UAAU,GAAI,CAAC;EAErE,IAAI,OAAO,QAAQ,QAAQ,KAAK;GAC9B,SAAS,OAAO,eAAe,UAAU;GAEzC,OAAO,SAAS,gBAAgB;IAC9B,QACG,OAAO,QAAQ,iBAAiB,aAC7B,QAAQ,aAAa,OAAO,IAC5B,QAAQ,iBAAiB,EAAE,wBAAwB;IACzD;GACF,CAAC;EACH;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";KAyBY,0BAAA;EAAA;;;;AAON;AA4EN;EA5EE,IAAI;AAAA;;;;;;AA4EsF;AAkG5F;;iBAlGsB,mBAAA,CAAoB,OAAA,GAAS,0BAAA,GAAkC,OAAO;;AAkGzC;AA6BnD;;iBA7BsB,kBAAA,IAAsB,OAAO;;AA6BhB;;iBAAnB,mBAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Application } from "../application/application.mjs";
|
|
2
2
|
import "../application/index.mjs";
|
|
3
3
|
import { assertPortIsAvailable } from "../http/port-preflight.mjs";
|
|
4
|
+
import { loadEnvironmentFiles } from "../utils/load-environment.mjs";
|
|
4
5
|
import { bootstrap } from "../bootstrap.mjs";
|
|
5
6
|
import { ConnectorLifecyclePhase } from "../connectors/types.mjs";
|
|
6
7
|
import { connectorsManager } from "../connectors/connectors-manager.mjs";
|
|
@@ -55,12 +56,16 @@ async function applyTestServerPort(port) {
|
|
|
55
56
|
* await startHttpTestServer({ port: 3999 });
|
|
56
57
|
*/
|
|
57
58
|
async function startHttpTestServer(options = {}) {
|
|
59
|
+
const nodeEnvironment = process.env.NODE_ENV;
|
|
60
|
+
if (nodeEnvironment === void 0) process.env.NODE_ENV = "test";
|
|
61
|
+
else if (nodeEnvironment !== "test") throw new Error(`startHttpTestServer() requires NODE_ENV="test", but received NODE_ENV=${JSON.stringify(nodeEnvironment)}. Set NODE_ENV=test before starting the test server.`);
|
|
58
62
|
if (isServerRunning) {
|
|
59
63
|
console.log("[test-server] Server already running, skipping start");
|
|
60
64
|
return;
|
|
61
65
|
}
|
|
62
66
|
console.log("[test-server] Starting HTTP test server...");
|
|
63
67
|
try {
|
|
68
|
+
await loadEnvironmentFiles();
|
|
64
69
|
Application.setRuntimeStrategy("development");
|
|
65
70
|
Application.setEnvironment("test");
|
|
66
71
|
await warlockConfigManager.load();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.mjs","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\n * HTTP Test Server\n *\n * Starts a minimal HTTP server for testing.\n * Unlike the full DevelopmentServer, this:\n * - Does NOT watch files\n * - Does NOT do HMR\n * - Only starts connectors needed for HTTP requests\n *\n * Used in Vitest's globalSetup to start server once for all test workers.\n */\nimport config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\n\nlet isServerRunning = false;\n\nexport type StartHttpTestServerOptions = {\n /**\n * Port the test server binds to, overriding `http.port`.\n *\n * Needed because the server cannot be moved from outside: this function\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\n */\n port?: number;\n};\n\n/**\n * Apply the caller's port and preflight the bind.\n *\n * Called at the last moment before the HTTP connector binds, and that timing is\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\n * by default), and `src/config/http.ts` then resolves its port from that store,\n * so anything the caller set earlier is gone by the time the port is read.\n * Writing into `config` here is also the only channel that works — assigning\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\n * and never falls back to `process.env`.\n */\nasync function applyTestServerPort(port?: number): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n if (port !== undefined) {\n throw new Error(\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\n );\n }\n\n return;\n }\n\n if (port !== undefined) {\n config.set(\"http.port\", port);\n }\n\n const resolvedPort = config.get(\"http.port\");\n\n // No configured port at all is fine: Fastify falls back to its own default and\n // the workers resolve the same one from their own config, so both ends agree\n // without the channel.\n //\n // A non-numeric port is NOT fine, and must fail loudly rather than skip the\n // preflight silently. `env()` coerces a value only when it round-trips exactly\n // (`String(Number(v)) === v`), so `HTTP_PORT=3999` and `HTTP_PORT=0` arrive as\n // numbers, but `03999`, `+3999`, `1e3` and anything with stray whitespace stay\n // strings. Letting those through used to reach `HttpConnector.start()`'s\n // `listen({ port })` unchanged — no preflight, no published port, and every\n // test worker resolving a URL from a channel nothing ever wrote to.\n if (typeof resolvedPort !== \"number\") {\n throw new Error(\n `startHttpTestServer() cannot use \\`http.port\\` value ${JSON.stringify(resolvedPort)} — it did not round-trip through Number() (env() only coerces a value when \\`String(Number(v)) === v\\`, so \"03999\", \"+3999\", \"1e3\", and values with stray whitespace stay strings). Fix \\`HTTP_PORT\\` in .env, or pass an explicit numeric port: startHttpTestServer({ port: 3999 }).`,\n );\n }\n\n // `0` is the OS's \"pick a free one for me\" idiom, and the test server cannot\n // support it. Preflighting 0 would bind some unrelated ephemeral port and pass\n // without proving anything, and there is nothing meaningful to publish: the\n // port Fastify ends up binding is never recorded — `HttpConnector.start()`\n // stores the port it ASKED for, not the one it got. Accepting 0 silently is\n // what left `getTestServerUrl()` resolving to `http://host:0` through its own\n // config fallback, so every worker request went to a port nothing listens on.\n if (resolvedPort <= 0) {\n throw new Error(\n `startHttpTestServer() cannot run on port ${resolvedPort}. Pass an explicit port — e.g. startHttpTestServer({ port: 3999 }) — or set \\`http.port\\` in your config. Test workers are separate processes that resolve the server's URL from the port published at startup, and an OS-assigned port is not knowable to them.`,\n );\n }\n\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\n\n publishTestServerPort(resolvedPort);\n}\n\n/**\n * Start the HTTP test server (minimal - no file watching)\n * Call this in Vitest's globalSetup\n *\n * @example\n * // run this suite on its own port, whatever `.env` says\n * await startHttpTestServer({ port: 3999 });\n */\nexport async function startHttpTestServer(options: StartHttpTestServerOptions = {}): Promise<void> {\n if (isServerRunning) {\n console.log(\"[test-server] Server already running, skipping start\");\n return;\n }\n\n console.log(\"[test-server] Starting HTTP test server...\");\n\n try {\n // Set environment\n Application.setRuntimeStrategy(\"development\");\n Application.setEnvironment(\"test\");\n\n // Bootstrap (env, etc.)\n await warlockConfigManager.load();\n await bootstrap();\n\n // Initialize file orchestrator (but don't watch)\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n\n // Load config files\n await loadConfigFiles(true);\n\n // Early-phase connectors (database, cache, logger, storage, mailer,\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\n // side-effect can query the DB at import time, so the data source has to\n // be registered first. This mirrors the dev/prod boot order (see\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n\n // Load application modules (their boot side-effects now see a live DB).\n await filesOrchestrator.moduleLoader.loadAll();\n\n await applyTestServerPort(options.port);\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — same slot as\n // `DevelopmentServer.start()` and the generated production `app.ts`.\n await Application.runStartupValidators();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered its routes and listeners.\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n\n isServerRunning = true;\n } catch (error) {\n // This function owns the connectors it starts, so a failure part-way\n // through has to leave nothing behind. Without this, `isServerRunning` was\n // still `false` — it is only set on the last line — so `globalTeardown`'s\n // `stopHttpTestServer()` reported \"No server to stop\" and walked away from\n // live early-phase connectors and a published port that outlived them.\n await unwindPartialStartup();\n\n // Rethrown unchanged: the startup failure is the one the caller needs to\n // read. A cleanup failure that replaced it would hide the actual cause.\n throw error;\n }\n}\n\n/**\n * Best-effort teardown of whatever a failed {@link startHttpTestServer} managed\n * to start.\n *\n * Every step is independent and none may throw: this runs while an error is\n * already in flight, and the caller is about to rethrow it.\n */\nasync function unwindPartialStartup(): Promise<void> {\n try {\n await connectorsManager.shutdown();\n } catch (shutdownError) {\n // Reported, not thrown — it is secondary to the startup error being\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(\"[test-server] Cleanup after a failed start did not complete:\", shutdownError);\n }\n\n withdrawTestServerPort();\n isServerRunning = false;\n}\n\n/**\n * Stop the HTTP test server\n * Call this in Vitest's globalTeardown\n */\nexport async function stopHttpTestServer(): Promise<void> {\n if (!isServerRunning) {\n console.log(\"[test-server] No server to stop\");\n return;\n }\n\n try {\n await connectorsManager.shutdown();\n\n console.log(\"[test-server] HTTP test server stopped\");\n } catch (error) {\n // Surfaced, never swallowed — cleaning up is not the same as the shutdown\n // having succeeded, and a suite whose teardown failed should say so.\n console.error(\"[test-server] Error stopping HTTP server:\", error);\n\n throw error;\n } finally {\n // In a `finally` rather than after the `await`: when `shutdown()` threw,\n // the published port survived the server it pointed at, so the next run in\n // the same process inherited a stale one and sent every request to it.\n withdrawTestServerPort();\n\n isServerRunning = false;\n }\n}\n\n/**\n * Check if test server is running\n */\nexport function isTestServerRunning(): boolean {\n return isServerRunning;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAI,kBAAkB;;;;;;;;;;;;AAuBtB,eAAe,oBAAoB,MAA8B;CAG/D,IAAI,CAFe,OAAO,IAAI,MAEhB,GAAG;EACf,IAAI,SAAS,QACX,MAAM,IAAI,MACR,+BAA+B,KAAK,2HACtC;EAGF;CACF;CAEA,IAAI,SAAS,QACX,OAAO,IAAI,aAAa,IAAI;CAG9B,MAAM,eAAe,OAAO,IAAI,WAAW;CAa3C,IAAI,OAAO,iBAAiB,UAC1B,MAAM,IAAI,MACR,wDAAwD,KAAK,UAAU,YAAY,EAAE,sRACvF;CAUF,IAAI,gBAAgB,GAClB,MAAM,IAAI,MACR,4CAA4C,aAAa,iQAC3D;CAGF,MAAM,sBAAsB,cAAc,OAAO,IAAI,WAAW,KAAK,WAAW;CAEhF,sBAAsB,YAAY;AACpC;;;;;;;;;AAUA,eAAsB,oBAAoB,UAAsC,CAAC,GAAkB;CACjG,IAAI,iBAAiB;EACnB,QAAQ,IAAI,sDAAsD;EAClE;CACF;CAEA,QAAQ,IAAI,4CAA4C;CAExD,IAAI;EAEF,YAAY,mBAAmB,aAAa;EAC5C,YAAY,eAAe,MAAM;EAGjC,MAAM,qBAAqB,KAAK;EAChC,MAAM,UAAU;EAGhB,MAAM,kBAAkB,KAAK;EAC7B,MAAM,kBAAkB,cAAc;EAGtC,MAAM,gBAAgB,IAAI;EAO1B,MAAM,kBAAkB,kBAAwC;EAGhE,MAAM,kBAAkB,aAAa,QAAQ;EAE7C,MAAM,oBAAoB,QAAQ,IAAI;EAKtC,MAAM,YAAY,qBAAqB;EAIvC,MAAM,kBAAkB,iBAAuC;EAE/D,kBAAkB;CACpB,SAAS,OAAO;EAMd,MAAM,qBAAqB;EAI3B,MAAM;CACR;AACF;;;;;;;;AASA,eAAe,uBAAsC;CACnD,IAAI;EACF,MAAM,kBAAkB,SAAS;CACnC,SAAS,eAAe;EAGtB,QAAQ,MAAM,gEAAgE,aAAa;CAC7F;CAEA,uBAAuB;CACvB,kBAAkB;AACpB;;;;;AAMA,eAAsB,qBAAoC;CACxD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IAAI,iCAAiC;EAC7C;CACF;CAEA,IAAI;EACF,MAAM,kBAAkB,SAAS;EAEjC,QAAQ,IAAI,wCAAwC;CACtD,SAAS,OAAO;EAGd,QAAQ,MAAM,6CAA6C,KAAK;EAEhE,MAAM;CACR,UAAU;EAIR,uBAAuB;EAEvB,kBAAkB;CACpB;AACF;;;;AAKA,SAAgB,sBAA+B;CAC7C,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"start-http-development-server.mjs","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\n * HTTP Test Server\n *\n * Starts a minimal HTTP server for testing.\n * Unlike the full DevelopmentServer, this:\n * - Does NOT watch files\n * - Does NOT do HMR\n * - Only starts connectors needed for HTTP requests\n *\n * Used in Vitest's globalSetup to start server once for all test workers.\n */\nimport config from \"@mongez/config\";\nimport { Application } from \"../application\";\nimport { bootstrap } from \"../bootstrap\";\nimport { loadConfigFiles } from \"../config/load-config-files\";\nimport { connectorsManager } from \"../connectors/connectors-manager\";\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\nimport { loadEnvironmentFiles } from \"../utils/load-environment\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\n\nlet isServerRunning = false;\n\nexport type StartHttpTestServerOptions = {\n /**\n * Port the test server binds to, overriding `http.port`.\n *\n * Needed because the server cannot be moved from outside: this function\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\n */\n port?: number;\n};\n\n/**\n * Apply the caller's port and preflight the bind.\n *\n * Called at the last moment before the HTTP connector binds, and that timing is\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\n * by default), and `src/config/http.ts` then resolves its port from that store,\n * so anything the caller set earlier is gone by the time the port is read.\n * Writing into `config` here is also the only channel that works — assigning\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\n * and never falls back to `process.env`.\n */\nasync function applyTestServerPort(port?: number): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n if (port !== undefined) {\n throw new Error(\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\n );\n }\n\n return;\n }\n\n if (port !== undefined) {\n config.set(\"http.port\", port);\n }\n\n const resolvedPort = config.get(\"http.port\");\n\n // No configured port at all is fine: Fastify falls back to its own default and\n // the workers resolve the same one from their own config, so both ends agree\n // without the channel.\n //\n // A non-numeric port is NOT fine, and must fail loudly rather than skip the\n // preflight silently. `env()` coerces a value only when it round-trips exactly\n // (`String(Number(v)) === v`), so `HTTP_PORT=3999` and `HTTP_PORT=0` arrive as\n // numbers, but `03999`, `+3999`, `1e3` and anything with stray whitespace stay\n // strings. Letting those through used to reach `HttpConnector.start()`'s\n // `listen({ port })` unchanged — no preflight, no published port, and every\n // test worker resolving a URL from a channel nothing ever wrote to.\n if (typeof resolvedPort !== \"number\") {\n throw new Error(\n `startHttpTestServer() cannot use \\`http.port\\` value ${JSON.stringify(resolvedPort)} — it did not round-trip through Number() (env() only coerces a value when \\`String(Number(v)) === v\\`, so \"03999\", \"+3999\", \"1e3\", and values with stray whitespace stay strings). Fix \\`HTTP_PORT\\` in .env, or pass an explicit numeric port: startHttpTestServer({ port: 3999 }).`,\n );\n }\n\n // `0` is the OS's \"pick a free one for me\" idiom, and the test server cannot\n // support it. Preflighting 0 would bind some unrelated ephemeral port and pass\n // without proving anything, and there is nothing meaningful to publish: the\n // port Fastify ends up binding is never recorded — `HttpConnector.start()`\n // stores the port it ASKED for, not the one it got. Accepting 0 silently is\n // what left `getTestServerUrl()` resolving to `http://host:0` through its own\n // config fallback, so every worker request went to a port nothing listens on.\n if (resolvedPort <= 0) {\n throw new Error(\n `startHttpTestServer() cannot run on port ${resolvedPort}. Pass an explicit port — e.g. startHttpTestServer({ port: 3999 }) — or set \\`http.port\\` in your config. Test workers are separate processes that resolve the server's URL from the port published at startup, and an OS-assigned port is not knowable to them.`,\n );\n }\n\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\n\n publishTestServerPort(resolvedPort);\n}\n\n/**\n * Start the HTTP test server (minimal - no file watching)\n * Call this in Vitest's globalSetup\n *\n * @example\n * // run this suite on its own port, whatever `.env` says\n * await startHttpTestServer({ port: 3999 });\n */\nexport async function startHttpTestServer(options: StartHttpTestServerOptions = {}): Promise<void> {\n const nodeEnvironment = process.env.NODE_ENV;\n\n if (nodeEnvironment === undefined) {\n process.env.NODE_ENV = \"test\";\n } else if (nodeEnvironment !== \"test\") {\n throw new Error(\n `startHttpTestServer() requires NODE_ENV=\"test\", but received NODE_ENV=${JSON.stringify(nodeEnvironment)}. Set NODE_ENV=test before starting the test server.`,\n );\n }\n\n if (isServerRunning) {\n console.log(\"[test-server] Server already running, skipping start\");\n return;\n }\n\n console.log(\"[test-server] Starting HTTP test server...\");\n\n try {\n // `globalSetup` runs in Vitest's main process, where Vitest's worker env\n // setup has not run. Load `.env.test` here, before any config is read.\n await loadEnvironmentFiles();\n\n // Set environment\n Application.setRuntimeStrategy(\"development\");\n Application.setEnvironment(\"test\");\n\n // Bootstrap (env, etc.)\n await warlockConfigManager.load();\n await bootstrap();\n\n // Initialize file orchestrator (but don't watch)\n await filesOrchestrator.init();\n await filesOrchestrator.initializeAll();\n\n // Load config files\n await loadConfigFiles(true);\n\n // Early-phase connectors (database, cache, logger, storage, mailer,\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\n // side-effect can query the DB at import time, so the data source has to\n // be registered first. This mirrors the dev/prod boot order (see\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\n\n // Load application modules (their boot side-effects now see a live DB).\n await filesOrchestrator.moduleLoader.loadAll();\n\n await applyTestServerPort(options.port);\n\n // A rejecting validator (Application.onValidateBoot) must abort boot\n // before late-phase connectors bind a port — same slot as\n // `DevelopmentServer.start()` and the generated production `app.ts`.\n await Application.runStartupValidators();\n\n // Late-phase connectors (http, socket) bind after app code has\n // registered its routes and listeners.\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\n\n isServerRunning = true;\n } catch (error) {\n // This function owns the connectors it starts, so a failure part-way\n // through has to leave nothing behind. Without this, `isServerRunning` was\n // still `false` — it is only set on the last line — so `globalTeardown`'s\n // `stopHttpTestServer()` reported \"No server to stop\" and walked away from\n // live early-phase connectors and a published port that outlived them.\n await unwindPartialStartup();\n\n // Rethrown unchanged: the startup failure is the one the caller needs to\n // read. A cleanup failure that replaced it would hide the actual cause.\n throw error;\n }\n}\n\n/**\n * Best-effort teardown of whatever a failed {@link startHttpTestServer} managed\n * to start.\n *\n * Every step is independent and none may throw: this runs while an error is\n * already in flight, and the caller is about to rethrow it.\n */\nasync function unwindPartialStartup(): Promise<void> {\n try {\n await connectorsManager.shutdown();\n } catch (shutdownError) {\n // Reported, not thrown — it is secondary to the startup error being\n // rethrown, and losing that one to this would be the worse outcome.\n console.error(\"[test-server] Cleanup after a failed start did not complete:\", shutdownError);\n }\n\n withdrawTestServerPort();\n isServerRunning = false;\n}\n\n/**\n * Stop the HTTP test server\n * Call this in Vitest's globalTeardown\n */\nexport async function stopHttpTestServer(): Promise<void> {\n if (!isServerRunning) {\n console.log(\"[test-server] No server to stop\");\n return;\n }\n\n try {\n await connectorsManager.shutdown();\n\n console.log(\"[test-server] HTTP test server stopped\");\n } catch (error) {\n // Surfaced, never swallowed — cleaning up is not the same as the shutdown\n // having succeeded, and a suite whose teardown failed should say so.\n console.error(\"[test-server] Error stopping HTTP server:\", error);\n\n throw error;\n } finally {\n // In a `finally` rather than after the `await`: when `shutdown()` threw,\n // the published port survived the server it pointed at, so the next run in\n // the same process inherited a stale one and sent every request to it.\n withdrawTestServerPort();\n\n isServerRunning = false;\n }\n}\n\n/**\n * Check if test server is running\n */\nexport function isTestServerRunning(): boolean {\n return isServerRunning;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAI,kBAAkB;;;;;;;;;;;;AAuBtB,eAAe,oBAAoB,MAA8B;CAG/D,IAAI,CAFe,OAAO,IAAI,MAEhB,GAAG;EACf,IAAI,SAAS,QACX,MAAM,IAAI,MACR,+BAA+B,KAAK,2HACtC;EAGF;CACF;CAEA,IAAI,SAAS,QACX,OAAO,IAAI,aAAa,IAAI;CAG9B,MAAM,eAAe,OAAO,IAAI,WAAW;CAa3C,IAAI,OAAO,iBAAiB,UAC1B,MAAM,IAAI,MACR,wDAAwD,KAAK,UAAU,YAAY,EAAE,sRACvF;CAUF,IAAI,gBAAgB,GAClB,MAAM,IAAI,MACR,4CAA4C,aAAa,iQAC3D;CAGF,MAAM,sBAAsB,cAAc,OAAO,IAAI,WAAW,KAAK,WAAW;CAEhF,sBAAsB,YAAY;AACpC;;;;;;;;;AAUA,eAAsB,oBAAoB,UAAsC,CAAC,GAAkB;CACjG,MAAM,kBAAkB,QAAQ,IAAI;CAEpC,IAAI,oBAAoB,QACtB,QAAQ,IAAI,WAAW;MAClB,IAAI,oBAAoB,QAC7B,MAAM,IAAI,MACR,yEAAyE,KAAK,UAAU,eAAe,EAAE,qDAC3G;CAGF,IAAI,iBAAiB;EACnB,QAAQ,IAAI,sDAAsD;EAClE;CACF;CAEA,QAAQ,IAAI,4CAA4C;CAExD,IAAI;EAGF,MAAM,qBAAqB;EAG3B,YAAY,mBAAmB,aAAa;EAC5C,YAAY,eAAe,MAAM;EAGjC,MAAM,qBAAqB,KAAK;EAChC,MAAM,UAAU;EAGhB,MAAM,kBAAkB,KAAK;EAC7B,MAAM,kBAAkB,cAAc;EAGtC,MAAM,gBAAgB,IAAI;EAO1B,MAAM,kBAAkB,kBAAwC;EAGhE,MAAM,kBAAkB,aAAa,QAAQ;EAE7C,MAAM,oBAAoB,QAAQ,IAAI;EAKtC,MAAM,YAAY,qBAAqB;EAIvC,MAAM,kBAAkB,iBAAuC;EAE/D,kBAAkB;CACpB,SAAS,OAAO;EAMd,MAAM,qBAAqB;EAI3B,MAAM;CACR;AACF;;;;;;;;AASA,eAAe,uBAAsC;CACnD,IAAI;EACF,MAAM,kBAAkB,SAAS;CACnC,SAAS,eAAe;EAGtB,QAAQ,MAAM,gEAAgE,aAAa;CAC7F;CAEA,uBAAuB;CACvB,kBAAkB;AACpB;;;;;AAMA,eAAsB,qBAAoC;CACxD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IAAI,iCAAiC;EAC7C;CACF;CAEA,IAAI;EACF,MAAM,kBAAkB,SAAS;EAEjC,QAAQ,IAAI,wCAAwC;CACtD,SAAS,OAAO;EAGd,QAAQ,MAAM,6CAA6C,KAAK;EAEhE,MAAM;CACR,UAAU;EAIR,uBAAuB;EAEvB,kBAAkB;CACpB;AACF;;;;AAKA,SAAgB,sBAA+B;CAC7C,OAAO;AACT"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { rootPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { fileExistsAsync } from "@warlock.js/fs";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
4
6
|
|
|
5
7
|
//#region ../core/src/updater/package-manager.ts
|
|
6
8
|
/**
|
|
@@ -19,16 +21,16 @@ const LOCKFILES = [
|
|
|
19
21
|
packageManager: "bun"
|
|
20
22
|
},
|
|
21
23
|
{
|
|
22
|
-
file: "
|
|
23
|
-
packageManager: "
|
|
24
|
+
file: "pnpm-lock.yaml",
|
|
25
|
+
packageManager: "pnpm"
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
file: "yarn.lock",
|
|
27
29
|
packageManager: "yarn"
|
|
28
30
|
},
|
|
29
31
|
{
|
|
30
|
-
file: "
|
|
31
|
-
packageManager: "
|
|
32
|
+
file: "package-lock.json",
|
|
33
|
+
packageManager: "npm"
|
|
32
34
|
}
|
|
33
35
|
];
|
|
34
36
|
/**
|
|
@@ -36,10 +38,30 @@ const LOCKFILES = [
|
|
|
36
38
|
* npm when none is present. Shared by `warlock update` and `warlock add` so
|
|
37
39
|
* both agree on a project that happens to carry more than one lockfile.
|
|
38
40
|
*/
|
|
39
|
-
async function detectPackageManager() {
|
|
40
|
-
|
|
41
|
+
async function detectPackageManager(startDir = rootPath()) {
|
|
42
|
+
const dirs = [];
|
|
43
|
+
for (let dir = path.resolve(startDir);; dir = path.dirname(dir)) {
|
|
44
|
+
dirs.push(dir);
|
|
45
|
+
if (path.dirname(dir) === dir) break;
|
|
46
|
+
}
|
|
47
|
+
for (const dir of dirs) {
|
|
48
|
+
const declared = await readDeclaredPackageManager(dir);
|
|
49
|
+
if (declared) return declared;
|
|
50
|
+
}
|
|
51
|
+
for (const dir of dirs) if (await fileExistsAsync(path.join(dir, "pnpm-workspace.yaml"))) return "pnpm";
|
|
52
|
+
for (const dir of dirs) for (const { file, packageManager } of LOCKFILES) if (await fileExistsAsync(path.join(dir, file))) return packageManager;
|
|
53
|
+
const agent = process.env.npm_config_user_agent?.split("/")[0];
|
|
54
|
+
if (agent === "pnpm" || agent === "yarn" || agent === "bun") return agent;
|
|
41
55
|
return "npm";
|
|
42
56
|
}
|
|
57
|
+
/** The manager named by the app's `packageManager` field, if any. */
|
|
58
|
+
async function readDeclaredPackageManager(dir) {
|
|
59
|
+
try {
|
|
60
|
+
const json = JSON.parse(await readFile(path.join(dir, "package.json"), "utf8"));
|
|
61
|
+
const name = String(json.packageManager ?? "").split("@")[0];
|
|
62
|
+
if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") return name;
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
43
65
|
/**
|
|
44
66
|
* The lockfile-syncing install command for the given manager. No package
|
|
45
67
|
* arguments — `warlock update` rewrites the versions in package.json first,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-manager.mjs","names":[],"sources":["../../../../../../../core/src/updater/package-manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { rootPath } from \"../utils\";\n\n/** Package managers the framework knows how to drive. */\nexport type PackageManager = \"npm\" | \"yarn\" | \"pnpm\" | \"bun\";\n\n/**\n * The lockfile that identifies each package manager, in detection order.\n * Bun is checked first because a Bun project may also carry a `yarn.lock`\n * (Bun writes one for tooling compatibility) — matching yarn there would run\n * the wrong installer against the wrong lockfile.\n */\nconst LOCKFILES: ReadonlyArray<{ file: string; packageManager: PackageManager }> = [\n { file: \"bun.lock\", packageManager: \"bun\" },\n { file: \"bun.lockb\", packageManager: \"bun\" },\n { file: \"
|
|
1
|
+
{"version":3,"file":"package-manager.mjs","names":[],"sources":["../../../../../../../core/src/updater/package-manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { rootPath } from \"../utils\";\n\n/** Package managers the framework knows how to drive. */\nexport type PackageManager = \"npm\" | \"yarn\" | \"pnpm\" | \"bun\";\n\n/**\n * The lockfile that identifies each package manager, in detection order.\n * Bun is checked first because a Bun project may also carry a `yarn.lock`\n * (Bun writes one for tooling compatibility) — matching yarn there would run\n * the wrong installer against the wrong lockfile.\n */\nconst LOCKFILES: ReadonlyArray<{ file: string; packageManager: PackageManager }> = [\n { file: \"bun.lock\", packageManager: \"bun\" },\n { file: \"bun.lockb\", packageManager: \"bun\" },\n { file: \"pnpm-lock.yaml\", packageManager: \"pnpm\" },\n { file: \"yarn.lock\", packageManager: \"yarn\" },\n { file: \"package-lock.json\", packageManager: \"npm\" },\n];\n\n/**\n * Detect the project's package manager from its lockfile, falling back to\n * npm when none is present. Shared by `warlock update` and `warlock add` so\n * both agree on a project that happens to carry more than one lockfile.\n */\nexport async function detectPackageManager(startDir: string = rootPath()): Promise<PackageManager> {\n const dirs: string[] = [];\n\n for (let dir = path.resolve(startDir); ; dir = path.dirname(dir)) {\n dirs.push(dir);\n\n if (path.dirname(dir) === dir) break;\n }\n\n // 1. nearest declared `packageManager` field\n for (const dir of dirs) {\n const declared = await readDeclaredPackageManager(dir);\n\n if (declared) return declared;\n }\n\n // 2. a pnpm workspace marker beats any (possibly stray) lockfile\n for (const dir of dirs) {\n if (await fileExistsAsync(path.join(dir, \"pnpm-workspace.yaml\"))) return \"pnpm\";\n }\n\n // 3. lockfiles, nearest level first\n for (const dir of dirs) {\n for (const { file, packageManager } of LOCKFILES) {\n if (await fileExistsAsync(path.join(dir, file))) return packageManager;\n }\n }\n\n const agent = process.env.npm_config_user_agent?.split(\"/\")[0];\n\n if (agent === \"pnpm\" || agent === \"yarn\" || agent === \"bun\") return agent;\n\n return \"npm\";\n}\n\n/** The manager named by the app's `packageManager` field, if any. */\nasync function readDeclaredPackageManager(dir: string): Promise<PackageManager | undefined> {\n try {\n const json = JSON.parse(await readFile(path.join(dir, \"package.json\"), \"utf8\"));\n const name = String(json.packageManager ?? \"\").split(\"@\")[0];\n\n if (name === \"pnpm\" || name === \"yarn\" || name === \"bun\" || name === \"npm\") return name;\n } catch {\n // no readable package.json — fall through to lockfiles\n }\n\n return undefined;\n}\n\n/**\n * The lockfile-syncing install command for the given manager. No package\n * arguments — `warlock update` rewrites the versions in package.json first,\n * then a plain install reconciles `node_modules` to match.\n */\nexport function getInstallCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn install\";\n\n case \"pnpm\":\n return \"pnpm install\";\n\n case \"bun\":\n return \"bun install\";\n\n default:\n return \"npm install\";\n }\n}\n\n/**\n * The command that installs *specific* packages — what `warlock add` needs.\n * Distinct from {@link getInstallCommand}, which only reconciles what is\n * already written into package.json.\n */\nexport function getAddCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn add\";\n\n case \"pnpm\":\n return \"pnpm add\";\n\n case \"bun\":\n return \"bun add\";\n\n default:\n return \"npm install\";\n }\n}\n\n/**\n * The package-manager command that saves an exact version. Warlock family\n * dependencies use this path so the version selected by the executing Core is\n * preserved verbatim in package.json instead of being widened by a manager's\n * default save prefix.\n */\nexport function getExactAddCommand(packageManager: PackageManager): string {\n switch (packageManager) {\n case \"yarn\":\n return \"yarn add --exact\";\n\n case \"pnpm\":\n return \"pnpm add --save-exact\";\n\n case \"bun\":\n return \"bun add --exact\";\n\n default:\n return \"npm install --save-exact\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAM,YAA6E;CACjF;EAAE,MAAM;EAAY,gBAAgB;CAAM;CAC1C;EAAE,MAAM;EAAa,gBAAgB;CAAM;CAC3C;EAAE,MAAM;EAAkB,gBAAgB;CAAO;CACjD;EAAE,MAAM;EAAa,gBAAgB;CAAO;CAC5C;EAAE,MAAM;EAAqB,gBAAgB;CAAM;AACrD;;;;;;AAOA,eAAsB,qBAAqB,WAAmB,SAAS,GAA4B;CACjG,MAAM,OAAiB,CAAC;CAExB,KAAK,IAAI,MAAM,KAAK,QAAQ,QAAQ,IAAK,MAAM,KAAK,QAAQ,GAAG,GAAG;EAChE,KAAK,KAAK,GAAG;EAEb,IAAI,KAAK,QAAQ,GAAG,MAAM,KAAK;CACjC;CAGA,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,WAAW,MAAM,2BAA2B,GAAG;EAErD,IAAI,UAAU,OAAO;CACvB;CAGA,KAAK,MAAM,OAAO,MAChB,IAAI,MAAM,gBAAgB,KAAK,KAAK,KAAK,qBAAqB,CAAC,GAAG,OAAO;CAI3E,KAAK,MAAM,OAAO,MAChB,KAAK,MAAM,EAAE,MAAM,oBAAoB,WACrC,IAAI,MAAM,gBAAgB,KAAK,KAAK,KAAK,IAAI,CAAC,GAAG,OAAO;CAI5D,MAAM,QAAQ,QAAQ,IAAI,uBAAuB,MAAM,GAAG,CAAC,CAAC;CAE5D,IAAI,UAAU,UAAU,UAAU,UAAU,UAAU,OAAO,OAAO;CAEpE,OAAO;AACT;;AAGA,eAAe,2BAA2B,KAAkD;CAC1F,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,KAAK,KAAK,cAAc,GAAG,MAAM,CAAC;EAC9E,MAAM,OAAO,OAAO,KAAK,kBAAkB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;EAE1D,IAAI,SAAS,UAAU,SAAS,UAAU,SAAS,SAAS,SAAS,OAAO,OAAO;CACrF,QAAQ,CAER;AAGF;;;;;;AAOA,SAAgB,kBAAkB,gBAAwC;CACxE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF;;;;;;AAOA,SAAgB,cAAc,gBAAwC;CACpE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF;;;;;;;AAQA,SAAgB,mBAAmB,gBAAwC;CACzE,QAAQ,gBAAR;EACE,KAAK,QACH,OAAO;EAET,KAAK,QACH,OAAO;EAET,KAAK,OACH,OAAO;EAET,SACE,OAAO;CACX;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framework-vesion.mjs","names":[],"sources":["../../../../../../../core/src/utils/framework-vesion.ts"],"sourcesContent":["import { getJsonFileAsync } from \"@warlock.js/fs\";\
|
|
1
|
+
{"version":3,"file":"framework-vesion.mjs","names":[],"sources":["../../../../../../../core/src/utils/framework-vesion.ts"],"sourcesContent":["import { getJsonFileAsync } from \"@warlock.js/fs\";\nimport path from \"node:path\";\n\n/**\n * Cached version string\n */\nlet cachedVersion: string | null = null;\n\n/**\n * Get the framework version from package.json (cached)\n */\nexport async function getWarlockVersion(): Promise<string> {\n if (cachedVersion) return cachedVersion;\n\n const frameworkPackageJson = (await getJsonFileAsync(\n path.join(import.meta.dirname, \"./../../package.json\"),\n )) as { version: string };\n\n const version = frameworkPackageJson.version.replace(/\\^|\\~/g, \"\");\n cachedVersion = version;\n return version;\n}\n\nexport function getFrameworkVersion(): string | null {\n return cachedVersion;\n}\n"],"mappings":";;;;;;;AAMA,IAAI,gBAA+B;;;;AAKnC,eAAsB,oBAAqC;CACzD,IAAI,eAAe,OAAO;CAM1B,MAAM,WAAU,MAJoB,iBAClC,KAAK,KAAK,OAAO,KAAK,SAAS,sBAAsB,CACvD,EAEoC,CAAC,QAAQ,QAAQ,UAAU,EAAE;CACjE,gBAAgB;CAChB,OAAO;AACT;AAEA,SAAgB,sBAAqC;CACnD,OAAO;AACT"}
|
|
@@ -46,10 +46,18 @@ declare module "@warlock.js/seal" {
|
|
|
46
46
|
}): this;
|
|
47
47
|
}
|
|
48
48
|
interface StringValidator {
|
|
49
|
-
uniqueExceptCurrentUser:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
uniqueExceptCurrentUser(model: ChildModel<Model> | string, optionsList?: Partial<UniqueExceptCurrentUserRuleOptions> & {
|
|
50
|
+
errorMessage?: string;
|
|
51
|
+
}): this;
|
|
52
|
+
uniqueExceptCurrentId(model: ChildModel<Model> | string, optionsList?: Partial<UniqueExceptCurrentIdRuleOptions> & {
|
|
53
|
+
errorMessage?: string;
|
|
54
|
+
}): this;
|
|
55
|
+
existsExceptCurrentUser(model: ChildModel<Model> | string, optionsList?: Partial<ExistsExceptCurrentUserRuleOptions> & {
|
|
56
|
+
errorMessage?: string;
|
|
57
|
+
}): this;
|
|
58
|
+
existsExceptCurrentId(model: ChildModel<Model> | string, optionsList?: Partial<ExistsExceptCurrentIdRuleOptions> & {
|
|
59
|
+
errorMessage?: string;
|
|
60
|
+
}): this;
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
type ValidationConfiguration = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/validation/types.ts"],"mappings":";;;;;;;;;;;;KAmCK,uBAAA,WAAkC,aAAA,IAAiB,eAAA;EACtD,UAAA,EAAY,eAAA,GAAkB,gBAAA;EAC9B,KAAA,EAAO,CAAA;AAAA,KAEP,gBAAA;EACE,UAAA;EACA,KAAA,EAAO,KAAA,CAAM,CAAA;AAAA;AAAA;EAAA,UAME,UAAA;IACf,IAAA,GAAO,YAAA,cAA0B,aAAA,GAAgB,gBAAA,CAAiB,YAAA;IAClE,SAAA,aACY,aAAA,GAAgB,eAAA,GAAkB,gBAAA,6BAE5C,cAAA,GAAiB,CAAA,EACjB,YAAA,cACG,cAAA;MACH,SAAA,EAAW,uBAAA,CAAwB,CAAA;IAAA,IACjC,gBAAA,CACA,KAAA;MACE,UAAA;MACA,KAAA,EAAO,KAAA,CAAM,CAAA;IAAA;EAAA;EAAA,UAKX,eAAA;IAvBM;IAyBd,uBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,kCAAA;MACpB,YAAA;IAAA;IA5BU;IAiCd,qBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,gCAAA;MACpB,YAAA;IAAA;IA3BQ;IAgCZ,uBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,kCAAA;MACpB,YAAA;IAAA;IA/BC;IAoCL,qBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,gCAAA;MACpB,YAAA;IAAA;EAAA;EAAA,UAMI,eAAA;IACR,uBAAA,
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/validation/types.ts"],"mappings":";;;;;;;;;;;;KAmCK,uBAAA,WAAkC,aAAA,IAAiB,eAAA;EACtD,UAAA,EAAY,eAAA,GAAkB,gBAAA;EAC9B,KAAA,EAAO,CAAA;AAAA,KAEP,gBAAA;EACE,UAAA;EACA,KAAA,EAAO,KAAA,CAAM,CAAA;AAAA;AAAA;EAAA,UAME,UAAA;IACf,IAAA,GAAO,YAAA,cAA0B,aAAA,GAAgB,gBAAA,CAAiB,YAAA;IAClE,SAAA,aACY,aAAA,GAAgB,eAAA,GAAkB,gBAAA,6BAE5C,cAAA,GAAiB,CAAA,EACjB,YAAA,cACG,cAAA;MACH,SAAA,EAAW,uBAAA,CAAwB,CAAA;IAAA,IACjC,gBAAA,CACA,KAAA;MACE,UAAA;MACA,KAAA,EAAO,KAAA,CAAM,CAAA;IAAA;EAAA;EAAA,UAKX,eAAA;IAvBM;IAyBd,uBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,kCAAA;MACpB,YAAA;IAAA;IA5BU;IAiCd,qBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,gCAAA;MACpB,YAAA;IAAA;IA3BQ;IAgCZ,uBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,kCAAA;MACpB,YAAA;IAAA;IA/BC;IAoCL,qBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,gCAAA;MACpB,YAAA;IAAA;EAAA;EAAA,UAMI,eAAA;IACR,uBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,kCAAA;MACpB,YAAA;IAAA;IAGJ,qBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,gCAAA;MACpB,YAAA;IAAA;IAGJ,uBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,kCAAA;MACpB,YAAA;IAAA;IAGJ,qBAAA,CACE,KAAA,EAAO,UAAA,CAAW,KAAA,YAClB,WAAA,GAAc,OAAA,CAAQ,gCAAA;MACpB,YAAA;IAAA;EAAA;AAAA;AAAA,KAcI,uBAAA;EA3CQ;;;;;;EAmDlB,gBAAA;EAnC0B;;;;;;EA0C1B,cAAA;EA/BW;;;;EAoCX,cAAA;EA5GiB;;;;EAiHjB,aAAA,GAAgB,qBAAA;EAhHoD;;;EAoHpE,kBAAA,GAAqB,0BAA0B;AAAA"}
|
package/package.json
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.5",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/cache": "5.
|
|
29
|
-
"@warlock.js/cascade": "5.
|
|
30
|
-
"@warlock.js/context": "5.
|
|
31
|
-
"@warlock.js/fs": "5.
|
|
32
|
-
"@warlock.js/logger": "5.
|
|
33
|
-
"@warlock.js/seal": "5.
|
|
28
|
+
"@warlock.js/cache": "5.23.0",
|
|
29
|
+
"@warlock.js/cascade": "5.23.0",
|
|
30
|
+
"@warlock.js/context": "5.23.0",
|
|
31
|
+
"@warlock.js/fs": "5.23.0",
|
|
32
|
+
"@warlock.js/logger": "5.23.0",
|
|
33
|
+
"@warlock.js/seal": "5.23.0",
|
|
34
34
|
"bcryptjs": "^3.0.3",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"dayjs": "^1.11.19",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@aws-sdk/lib-storage": "^3.955.0",
|
|
53
53
|
"@aws-sdk/s3-request-presigner": "^3.955.0",
|
|
54
54
|
"@react-email/render": "^2.0.5",
|
|
55
|
-
"@warlock.js/access": "5.
|
|
56
|
-
"@warlock.js/ai": "5.
|
|
57
|
-
"@warlock.js/herald": "5.
|
|
58
|
-
"@warlock.js/notifications": "5.
|
|
55
|
+
"@warlock.js/access": "5.23.0",
|
|
56
|
+
"@warlock.js/ai": "5.23.0",
|
|
57
|
+
"@warlock.js/herald": "5.23.0",
|
|
58
|
+
"@warlock.js/notifications": "5.23.0",
|
|
59
59
|
"nodemailer": "^8.0.5",
|
|
60
60
|
"react": "^19.2.3",
|
|
61
61
|
"react-dom": "^19.2.3",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
],
|
|
124
124
|
"author": "hassanzohdy",
|
|
125
125
|
"license": "MIT",
|
|
126
|
-
"version": "5.
|
|
126
|
+
"version": "5.23.0",
|
|
127
127
|
"type": "module",
|
|
128
128
|
"main": "./esm/index.mjs",
|
|
129
129
|
"module": "./esm/index.mjs",
|
|
@@ -106,8 +106,8 @@ export class UsersRestful extends Restful<User> implements RouteResource {
|
|
|
106
106
|
public validation = {
|
|
107
107
|
create: {
|
|
108
108
|
schema: v.object({
|
|
109
|
-
name: v.string().
|
|
110
|
-
email: v.email()
|
|
109
|
+
name: v.string().min(2),
|
|
110
|
+
email: v.email(),
|
|
111
111
|
}),
|
|
112
112
|
},
|
|
113
113
|
update: {
|
|
@@ -274,6 +274,8 @@ Per-invocation callbacks fire first, then use-case-level, then globals. The retu
|
|
|
274
274
|
|
|
275
275
|
Don't reach for `useCase` for a 5-line service — the ceremony outweighs the benefit.
|
|
276
276
|
|
|
277
|
+
For the plain-function flavour (`warlock generate.use-case <module>/<verb-noun>`) shared by a page action and a controller, see the guide [One use-case, two adapters](https://warlock.js.org/v/latest/core/guides/one-use-case-two-adapters/).
|
|
278
|
+
|
|
277
279
|
## Gotchas
|
|
278
280
|
|
|
279
281
|
- **`name` must be unique.** The registry de-dupes by name; a duplicate in dev logs a warning and one wins. Use namespaced names: `"orders.placeOrder"`.
|