gazan-init 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +722 -0
  3. package/bin/gazan.js +17 -0
  4. package/package.json +54 -0
  5. package/src/cli/commands/init.js +126 -0
  6. package/src/cli/index.js +22 -0
  7. package/src/config/aliases.js +130 -0
  8. package/src/config/normalize.js +65 -0
  9. package/src/generators/aliasResolver.js +68 -0
  10. package/src/generators/aliasRuntime.js +79 -0
  11. package/src/generators/database/mongoNative.js +45 -0
  12. package/src/generators/database/mongoose.js +32 -0
  13. package/src/generators/database/prisma.js +61 -0
  14. package/src/generators/engine.js +88 -0
  15. package/src/generators/entity/sensitiveFields.js +12 -0
  16. package/src/generators/entity/toApp.js +325 -0
  17. package/src/generators/entity/toMongoose.js +180 -0
  18. package/src/generators/entity/toPrisma.js +233 -0
  19. package/src/generators/entity/toZod.js +70 -0
  20. package/src/generators/env.js +123 -0
  21. package/src/generators/errors.js +67 -0
  22. package/src/generators/features/auth.js +163 -0
  23. package/src/generators/features/bullmq.js +96 -0
  24. package/src/generators/features/redis.js +34 -0
  25. package/src/generators/features/socket.js +45 -0
  26. package/src/generators/gitignore.js +19 -0
  27. package/src/generators/index.js +317 -0
  28. package/src/generators/middlewares.js +167 -0
  29. package/src/generators/packageJson.js +107 -0
  30. package/src/generators/paths.js +45 -0
  31. package/src/generators/project.js +189 -0
  32. package/src/generators/readme.js +177 -0
  33. package/src/generators/shutdown.js +113 -0
  34. package/src/generators/stripSensitiveFieldsHelper.js +50 -0
  35. package/src/generators/syntax.js +65 -0
  36. package/src/generators/tsconfig.js +32 -0
  37. package/src/parser/entity/errors.js +21 -0
  38. package/src/parser/entity/parse.js +417 -0
  39. package/src/parser/entity/schema.js +104 -0
  40. package/src/prompts/index.js +186 -0
  41. package/src/utils/fsSafety.js +17 -0
  42. package/src/utils/strings.js +110 -0
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+
3
+ const { baseDir } = require("./paths");
4
+
5
+ function entryPath(config) {
6
+ const base = baseDir(config);
7
+ const file = `server.${config.language}`;
8
+ return base === "." ? file : `${base}/${file}`;
9
+ }
10
+
11
+ function buildPackageJson(config) {
12
+ const dependencies = {
13
+ express: "^4.21.1",
14
+ dotenv: "^16.4.5",
15
+ zod: "^3.23.8",
16
+ cors: "^2.8.5",
17
+ helmet: "^7.1.0",
18
+ "express-rate-limit": "^7.4.1",
19
+ };
20
+ const devDependencies = {};
21
+
22
+ if (config.redis) {
23
+ dependencies.ioredis = "^5.4.1";
24
+ }
25
+ if (config.rateLimitStrategy === "redis") {
26
+ dependencies["rate-limit-redis"] = "^4.2.0";
27
+ }
28
+ if (config.bullMQ) {
29
+ dependencies.bullmq = "^5.21.2";
30
+ }
31
+ if (config.socketIO) {
32
+ dependencies["socket.io"] = "^4.8.0";
33
+ }
34
+ if (config.database.type === "postgresql" && config.database.orm === "prisma") {
35
+ dependencies["@prisma/client"] = "^5.20.0";
36
+ devDependencies.prisma = "^5.20.0";
37
+ }
38
+ if (config.database.type === "mongodb" && config.database.orm === "mongoose") {
39
+ dependencies.mongoose = "^8.7.0";
40
+ }
41
+ if (config.database.type === "mongodb" && config.database.orm === "native") {
42
+ dependencies.mongodb = "^6.9.0";
43
+ }
44
+ if (config.authentication.enabled && config.authentication.methods.includes("email-password")) {
45
+ dependencies.bcrypt = "^5.1.1";
46
+ }
47
+ if (
48
+ config.authentication.enabled &&
49
+ (config.authentication.methods.includes("jwt") || config.authentication.methods.includes("refresh-token"))
50
+ ) {
51
+ dependencies.jsonwebtoken = "^9.0.2";
52
+ }
53
+ // Deliberately no OAuth dependency here — see helpers/oauth.stub.{js,ts} and the README's
54
+ // Authentication section. GAZAN scaffolds OAuth env vars only; picking and wiring a provider
55
+ // (and its client library) is left to the project, since "OAuth" isn't one integration.
56
+
57
+ if (config.language === "ts") {
58
+ devDependencies.typescript = "^5.6.3";
59
+ devDependencies.tsx = "^4.19.1";
60
+ devDependencies["@types/node"] = "^22.7.5";
61
+ devDependencies["@types/express"] = "^4.17.21";
62
+ devDependencies["@types/cors"] = "^2.8.17";
63
+ if (dependencies.bcrypt) devDependencies["@types/bcrypt"] = "^5.0.2";
64
+ if (dependencies.jsonwebtoken) devDependencies["@types/jsonwebtoken"] = "^9.0.7";
65
+ }
66
+
67
+ const entry = entryPath(config);
68
+ const scripts = {
69
+ start: config.language === "ts" ? "node dist/server.js" : `node ${entry}`,
70
+ dev: config.language === "ts" ? `tsx watch ${entry}` : `node --watch ${entry}`,
71
+ };
72
+
73
+ if (config.language === "ts") {
74
+ scripts.build = "tsc";
75
+ }
76
+
77
+ if (config.database.type === "postgresql" && config.database.orm === "prisma") {
78
+ scripts["db:generate"] = "prisma generate";
79
+ scripts["db:migrate"] = "prisma migrate dev";
80
+ scripts["db:push"] = "prisma db push";
81
+ scripts["db:studio"] = "prisma studio";
82
+ }
83
+
84
+ const pkg = {
85
+ name: config.projectName,
86
+ version: "0.1.0",
87
+ private: true,
88
+ type: config.moduleSystem === "mjs" ? "module" : "commonjs",
89
+ main: entry,
90
+ scripts,
91
+ dependencies: sortKeys(dependencies),
92
+ };
93
+
94
+ if (Object.keys(devDependencies).length > 0) {
95
+ pkg.devDependencies = sortKeys(devDependencies);
96
+ }
97
+
98
+ return pkg;
99
+ }
100
+
101
+ function sortKeys(obj) {
102
+ const sorted = {};
103
+ for (const key of Object.keys(obj).sort()) sorted[key] = obj[key];
104
+ return sorted;
105
+ }
106
+
107
+ module.exports = { buildPackageJson, entryPath };
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+
5
+ function baseDir(config) {
6
+ return config.useSrc ? "src" : ".";
7
+ }
8
+
9
+ function sharedDir(config, name) {
10
+ return path.join(baseDir(config), name);
11
+ }
12
+
13
+ /**
14
+ * Architecture-aware locations for controller/route/service/validator files.
15
+ * MVC: flat, shared folders. HMVC: grouped per entity module.
16
+ */
17
+ function archDirs(config, moduleName) {
18
+ const base = baseDir(config);
19
+ if (config.architecture === "hmvc") {
20
+ const moduleBase = path.join(base, "modules", moduleName);
21
+ return {
22
+ controllers: path.join(moduleBase, "controllers"),
23
+ routes: path.join(moduleBase, "routes"),
24
+ services: path.join(moduleBase, "services"),
25
+ validators: path.join(moduleBase, "validators"),
26
+ moduleBase,
27
+ };
28
+ }
29
+ return {
30
+ controllers: path.join(base, "controllers"),
31
+ routes: path.join(base, "routes"),
32
+ services: path.join(base, "services"),
33
+ validators: path.join(base, "validators"),
34
+ moduleBase: base,
35
+ };
36
+ }
37
+
38
+ /** Relative import path (POSIX, no extension) from `fromDir` to `toFile` (also relative to project root, no extension). */
39
+ function relativeImport(fromDir, toFileNoExt) {
40
+ let rel = path.relative(fromDir, toFileNoExt).split(path.sep).join("/");
41
+ if (!rel.startsWith(".")) rel = `./${rel}`;
42
+ return rel;
43
+ }
44
+
45
+ module.exports = { baseDir, sharedDir, archDirs, relativeImport };
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("./syntax");
4
+ const { baseDir, sharedDir, archDirs } = require("./paths");
5
+ const { resolveImportPath } = require("./aliasResolver");
6
+
7
+ function generateAppFile(config, aliasConfig) {
8
+ const esm = isEsm(config);
9
+ const base = baseDir(config);
10
+ const r = (target) => resolveImportPath(config, aliasConfig, base, target);
11
+
12
+ const imports = [];
13
+ imports.push(esm ? `import express from "express";` : `const express = require("express");`);
14
+ imports.push(esm ? `import cors from "cors";` : `const cors = require("cors");`);
15
+ imports.push(esm ? `import helmet from "helmet";` : `const helmet = require("helmet");`);
16
+ const envPath = r(`${sharedDir(config, "helpers")}/env`);
17
+ imports.push(esm ? `import { env } from "${envPath}";` : `const { env } = require("${envPath}");`);
18
+ const rateLimitPath = r(`${sharedDir(config, "middlewares")}/rate-limit`);
19
+ imports.push(
20
+ esm ? `import { rateLimiter } from "${rateLimitPath}";` : `const { rateLimiter } = require("${rateLimitPath}");`
21
+ );
22
+ const notFoundPath = r(`${sharedDir(config, "middlewares")}/not-found`);
23
+ imports.push(esm ? `import { notFound } from "${notFoundPath}";` : `const { notFound } = require("${notFoundPath}");`);
24
+ const errorHandlerPath = r(`${sharedDir(config, "middlewares")}/error-handler`);
25
+ imports.push(
26
+ esm
27
+ ? `import { errorHandler } from "${errorHandlerPath}";`
28
+ : `const { errorHandler } = require("${errorHandlerPath}");`
29
+ );
30
+ const routesPath = r(`${sharedDir(config, "routes")}/index`);
31
+ imports.push(esm ? `import routes from "${routesPath}";` : `const routes = require("${routesPath}");`);
32
+
33
+ const body = `if (env.NODE_ENV === "production" && env.CORS_ORIGIN === "*") {
34
+ // CORS_ORIGIN="*" is fine for local development but should never ship to production —
35
+ // lock it down to your actual frontend origin(s) via the CORS_ORIGIN env var.
36
+ console.warn("[security] CORS_ORIGIN is \\"*\\" in production — restrict it to your real origin(s).");
37
+ }
38
+
39
+ const app = express();
40
+
41
+ app.use(helmet());
42
+ app.use(cors({ origin: env.CORS_ORIGIN }));
43
+ app.use(express.json({ limit: "1mb" }));
44
+ app.use(express.urlencoded({ extended: true, limit: "1mb" }));
45
+ app.use(rateLimiter);
46
+
47
+ app.get("/health", (req, res) => {
48
+ res.status(200).json({ success: true, data: { status: "ok" } });
49
+ });
50
+
51
+ app.use("/api", routes);
52
+
53
+ app.use(notFound);
54
+ app.use(errorHandler);
55
+ `;
56
+
57
+ const footer = esm ? "\nexport default app;\n" : "\nmodule.exports = app;\n";
58
+
59
+ return `${imports.join("\n")}\n\n${body}${footer}`;
60
+ }
61
+
62
+ function generateServerFile(config, aliasConfig) {
63
+ const esm = isEsm(config);
64
+ const isTs = config.language === "ts";
65
+ const base = baseDir(config);
66
+ const r = (target) => resolveImportPath(config, aliasConfig, base, target);
67
+
68
+ // CJS+JS is the one combination where aliases need an explicit runtime registration call —
69
+ // TS relies on tsc-alias (build) / tsx (dev), and MJS+JS on a --experimental-loader flag, but
70
+ // module-alias must be required before anything else in the process resolves a module.
71
+ const moduleAliasRegister =
72
+ aliasConfig && aliasConfig.enabled && config.language === "js" && config.moduleSystem === "cjs"
73
+ ? `require("module-alias/register");\n\n`
74
+ : "";
75
+
76
+ const imports = [];
77
+ imports.push(esm ? `import http from "http";` : `const http = require("http");`);
78
+ const appPath = r(`${base}/app`);
79
+ imports.push(esm ? `import app from "${appPath}";` : `const app = require("${appPath}");`);
80
+ const envPath = r(`${sharedDir(config, "helpers")}/env`);
81
+ imports.push(esm ? `import { env } from "${envPath}";` : `const { env } = require("${envPath}");`);
82
+ const shutdownPath = r(`${sharedDir(config, "utils")}/shutdown`);
83
+ imports.push(
84
+ esm ? `import { createShutdown } from "${shutdownPath}";` : `const { createShutdown } = require("${shutdownPath}");`
85
+ );
86
+ const processEventsPath = r(`${sharedDir(config, "helpers")}/process-events`);
87
+ imports.push(
88
+ esm
89
+ ? `import { registerProcessEvents } from "${processEventsPath}";`
90
+ : `const { registerProcessEvents } = require("${processEventsPath}");`
91
+ );
92
+
93
+ if (config.database.type !== "none") {
94
+ const dbPath = r(`${sharedDir(config, "configs")}/db/index`);
95
+ imports.push(esm ? `import * as db from "${dbPath}";` : `const db = require("${dbPath}");`);
96
+ }
97
+ if (config.redis) {
98
+ const redisPath = r(`${sharedDir(config, "configs")}/redis/index`);
99
+ imports.push(
100
+ esm ? `import { redisClient } from "${redisPath}";` : `const { redisClient } = require("${redisPath}");`
101
+ );
102
+ }
103
+ if (config.socketIO) {
104
+ const socketPath = r(`${sharedDir(config, "socket")}/index`);
105
+ imports.push(
106
+ esm ? `import { createSocketServer } from "${socketPath}";` : `const { createSocketServer } = require("${socketPath}");`
107
+ );
108
+ }
109
+ if (config.bullMQ) {
110
+ const workersPath = r(`${sharedDir(config, "workers")}/index`);
111
+ imports.push(
112
+ esm ? `import { queues, workers } from "${workersPath}";` : `const { queues, workers } = require("${workersPath}");`
113
+ );
114
+ }
115
+
116
+ const bootLines = [];
117
+ bootLines.push(`const httpServer = http.createServer(app);`);
118
+ if (config.socketIO) {
119
+ bootLines.push(`const io = createSocketServer(httpServer);`);
120
+ }
121
+
122
+ const shutdownResourceLines = [` httpServer,`];
123
+ if (config.socketIO) {
124
+ shutdownResourceLines.push(` io: { close: () => new Promise((resolve) => io.close(() => resolve())) },`);
125
+ }
126
+ if (config.bullMQ) {
127
+ shutdownResourceLines.push(` workers,`);
128
+ shutdownResourceLines.push(` queues,`);
129
+ }
130
+ if (config.redis) {
131
+ shutdownResourceLines.push(` redis: redisClient,`);
132
+ }
133
+ if (config.database.type !== "none") {
134
+ shutdownResourceLines.push(` db,`);
135
+ }
136
+
137
+ const dbConnectCall = config.database.type !== "none" ? ` await db.connect();\n` : "";
138
+
139
+ const body = `async function start()${isTs ? ": Promise<void>" : ""} {
140
+ ${dbConnectCall}
141
+ ${bootLines.map((l) => " " + l).join("\n")}
142
+
143
+ const shutdown = createShutdown({
144
+ ${shutdownResourceLines.join("\n")}
145
+ });
146
+
147
+ registerProcessEvents(shutdown);
148
+
149
+ httpServer.listen(env.PORT, () => {
150
+ console.log(\`[server] listening on port \${env.PORT} (\${env.NODE_ENV})\`);
151
+ });
152
+ }
153
+
154
+ start().catch((error) => {
155
+ console.error("[server] failed to start:", error);
156
+ process.exit(1);
157
+ });
158
+ `;
159
+
160
+ return `${moduleAliasRegister}${imports.join("\n")}\n\n${body}`;
161
+ }
162
+
163
+ function generateRoutesIndex(config, aliasConfig, models) {
164
+ const esm = isEsm(config);
165
+ const fromDir = sharedDir(config, "routes");
166
+ const imports = [esm ? `import { Router } from "express";` : `const { Router } = require("express");`];
167
+
168
+ const useLines = [];
169
+ for (const model of models) {
170
+ const varName = `${model.camelName}Routes`;
171
+ const targetDir = archDirs(config, model.camelName).routes;
172
+ const importPath = resolveImportPath(config, aliasConfig, fromDir, `${targetDir}/${model.kebabName}.routes`);
173
+ imports.push(
174
+ esm ? `import ${varName} from "${importPath}";` : `const ${varName} = require("${importPath}");`
175
+ );
176
+ useLines.push(`router.use("/${model.routePath}", ${varName});`);
177
+ }
178
+
179
+ const body = `const router = Router();
180
+
181
+ ${useLines.join("\n")}
182
+ `;
183
+
184
+ const footer = esm ? "\nexport default router;\n" : "\nmodule.exports = router;\n";
185
+
186
+ return `${imports.join("\n")}\n\n${body}${footer}`;
187
+ }
188
+
189
+ module.exports = { generateAppFile, generateServerFile, generateRoutesIndex };
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ const { buildEnvSpec } = require("./env");
4
+ const { getSensitiveFields } = require("./entity/sensitiveFields");
5
+ const { allAliasEntries } = require("../config/aliases");
6
+
7
+ function buildReadme(config, aliasConfig, entityModel) {
8
+ const lines = [];
9
+ lines.push(`# ${config.projectName}`, "");
10
+ lines.push("Generated with [GAZAN](https://github.com/) — an interactive backend project initializer.", "");
11
+
12
+ lines.push("## Stack", "");
13
+ lines.push(`- Module system: **${config.moduleSystem.toUpperCase()}**`);
14
+ lines.push(`- Language: **${config.language === "ts" ? "TypeScript" : "JavaScript"}**`);
15
+ lines.push(`- Architecture: **${config.architecture.toUpperCase()}**`);
16
+ lines.push(`- Source layout: ${config.useSrc ? "`src/`" : "project root"}`);
17
+ if (config.database.type !== "none") {
18
+ lines.push(`- Database: **${config.database.type}** (${config.database.orm})`);
19
+ } else {
20
+ lines.push(`- Database: none`);
21
+ }
22
+ if (config.redis) lines.push(`- Redis: enabled`);
23
+ if (config.bullMQ) lines.push(`- BullMQ workers: enabled`);
24
+ if (config.socketIO) lines.push(`- Socket.IO: enabled`);
25
+ if (config.authentication.enabled) {
26
+ lines.push(`- Authentication: ${config.authentication.methods.join(", ")}`);
27
+ }
28
+ if (aliasConfig && aliasConfig.enabled) lines.push(`- Import aliases: enabled`);
29
+ lines.push("");
30
+
31
+ if (config.authentication.enabled && config.authentication.methods.includes("oauth")) {
32
+ lines.push("> **OAuth is a stub.** Only env vars and `helpers/oauth.stub.*` (a documented");
33
+ lines.push("> `throw`, not a working integration) are generated — no provider, client");
34
+ lines.push("> library, or routes. See that file for what to build.");
35
+ lines.push("");
36
+ }
37
+
38
+ lines.push("## Project structure", "");
39
+ lines.push("```");
40
+ lines.push(...buildStructureTree(config));
41
+ lines.push("```", "");
42
+
43
+ if (aliasConfig && aliasConfig.enabled) {
44
+ lines.push("## Import Aliases", "");
45
+ lines.push("This project uses module aliases.", "");
46
+ const entries = Object.entries(allAliasEntries(aliasConfig));
47
+ const widest = Math.max(...entries.map(([key]) => key.length));
48
+ for (const [key, target] of entries) {
49
+ lines.push(`${key.padEnd(widest)} → ${target}`);
50
+ }
51
+ lines.push("");
52
+ const runtimeNote =
53
+ config.language === "ts"
54
+ ? "Resolved via tsconfig `paths` at dev time (`tsx`), and rewritten to relative paths after `tsc` by `tsc-alias` at build time."
55
+ : config.moduleSystem === "mjs"
56
+ ? "Resolved at runtime by the generated `alias-loader.mjs`, registered via `node --experimental-loader=./alias-loader.mjs` (already wired into `npm run dev` / `npm start`)."
57
+ : "Resolved at runtime by `module-alias` (registered as the first line of the entry file) from the `_moduleAliases` field in `package.json`.";
58
+ lines.push(`> ${runtimeNote}`, "");
59
+
60
+ const exampleEntry = entries.find(([key]) => key !== aliasConfig.root) || entries[0];
61
+ if (exampleEntry) {
62
+ lines.push("```ts");
63
+ lines.push(`import x from '${exampleEntry[0]}/example';`);
64
+ lines.push("```", "");
65
+ }
66
+ }
67
+
68
+ lines.push("## Environment variables", "");
69
+ lines.push("| Variable | Description |", "|---|---|");
70
+ for (const v of buildEnvSpec(config)) {
71
+ lines.push(`| \`${v.name}\` | example: \`${v.example}\` |`);
72
+ }
73
+ lines.push("");
74
+
75
+ lines.push("## Development", "");
76
+ lines.push("```bash");
77
+ lines.push("npm install");
78
+ lines.push("cp .env.example .env");
79
+ if (config.database.type === "postgresql" && config.database.orm === "prisma") {
80
+ lines.push("npm run db:generate");
81
+ lines.push("npm run db:migrate");
82
+ }
83
+ lines.push("npm run dev");
84
+ lines.push("```", "");
85
+
86
+ lines.push("## Production", "");
87
+ lines.push("```bash");
88
+ if (config.language === "ts") {
89
+ lines.push("npm run build");
90
+ }
91
+ lines.push("npm start");
92
+ lines.push("```", "");
93
+
94
+ if (config.database.type === "postgresql" && config.database.orm === "prisma") {
95
+ lines.push("## Database commands", "");
96
+ lines.push("```bash");
97
+ lines.push("npm run db:generate # regenerate the Prisma client");
98
+ lines.push("npm run db:migrate # run dev migrations");
99
+ lines.push("npm run db:push # push schema without a migration");
100
+ lines.push("npm run db:studio # open Prisma Studio");
101
+ lines.push("```", "");
102
+ }
103
+
104
+ if (entityModel && entityModel.models.length > 0) {
105
+ lines.push("## Generated entities", "");
106
+ for (const model of entityModel.models) {
107
+ lines.push(`- **${model.pascalName}** — \`/api/${model.routePath}\``);
108
+ }
109
+ lines.push("");
110
+
111
+ const sensitiveModels = entityModel.models.filter((m) => getSensitiveFields(m).length > 0);
112
+ if (sensitiveModels.length > 0) {
113
+ lines.push(
114
+ `> Fields matching \`password\`/\`secret\`/\`hash\` (${sensitiveModels
115
+ .map((m) => `${m.pascalName}.${getSensitiveFields(m).join(", ")}`)
116
+ .join("; ")}) are stripped from every generated response automatically.`
117
+ );
118
+ lines.push("");
119
+ }
120
+
121
+ const hasManyToMany = entityModel.models.some((m) => m.relations.some((r) => r.type === "belongsToMany"));
122
+ if (hasManyToMany) {
123
+ lines.push(
124
+ "> One or more entities use a many-to-many (`belongsToMany`) relation. It's represented in the"
125
+ );
126
+ lines.push(
127
+ "> generated schema/model, but the generated CRUD create/update endpoints don't read or write it —"
128
+ );
129
+ lines.push("> manage those joins via your own service code.");
130
+ lines.push("");
131
+ }
132
+ }
133
+
134
+ return lines.join("\n");
135
+ }
136
+
137
+ function buildStructureTree(config) {
138
+ const base = config.useSrc ? "src/" : "";
139
+ const lines = [`${config.projectName}/`];
140
+ if (config.useSrc) lines.push("├── src/");
141
+ const indent = config.useSrc ? "│ " : "";
142
+
143
+ if (config.architecture === "mvc") {
144
+ lines.push(`${indent}├── controllers/`);
145
+ lines.push(`${indent}├── routes/`);
146
+ lines.push(`${indent}├── services/`);
147
+ lines.push(`${indent}├── validators/`);
148
+ } else {
149
+ lines.push(`${indent}├── modules/`);
150
+ lines.push(`${indent}│ └── <entity>/`);
151
+ lines.push(`${indent}│ ├── controllers/`);
152
+ lines.push(`${indent}│ ├── routes/`);
153
+ lines.push(`${indent}│ ├── services/`);
154
+ lines.push(`${indent}│ └── validators/`);
155
+ }
156
+ lines.push(`${indent}├── middlewares/`);
157
+ lines.push(`${indent}├── helpers/`);
158
+ lines.push(`${indent}├── utils/`);
159
+ lines.push(`${indent}├── configs/`);
160
+ if (config.database.type !== "none") lines.push(`${indent}│ └── db/`);
161
+ if (config.redis) lines.push(`${indent}│ └── redis/`);
162
+ if (config.bullMQ) lines.push(`${indent}├── workers/`);
163
+ if (config.socketIO) lines.push(`${indent}├── socket/`);
164
+ lines.push(`${indent}├── app.${config.language}`);
165
+ lines.push(`${indent}└── server.${config.language}`);
166
+ if (config.database.type === "postgresql" && config.database.orm === "prisma") {
167
+ lines.push("├── prisma/");
168
+ lines.push("│ └── schema.prisma");
169
+ }
170
+ lines.push("├── .env.example");
171
+ lines.push("├── package.json");
172
+ lines.push("└── README.md");
173
+
174
+ return lines;
175
+ }
176
+
177
+ module.exports = { buildReadme };
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("./syntax");
4
+
5
+ /**
6
+ * Generic, idempotent graceful-shutdown orchestrator. It knows nothing about
7
+ * which features are enabled — server.js wires in only the resources that
8
+ * actually exist, so a resource that was never enabled is never touched.
9
+ */
10
+ function generateShutdownFile(config) {
11
+ const esm = isEsm(config);
12
+ const isTs = config.language === "ts";
13
+
14
+ const typeBlock = isTs
15
+ ? `export interface ShutdownResources {
16
+ httpServer?: { close: (cb: (err?: Error) => void) => void };
17
+ io?: { close: () => Promise<void> | void };
18
+ workers?: Array<{ close: () => Promise<void> }>;
19
+ queues?: Array<{ close: () => Promise<void> }>;
20
+ redis?: { quit: () => Promise<unknown> };
21
+ db?: { disconnect?: () => Promise<void>; close?: () => Promise<void> };
22
+ }
23
+
24
+ `
25
+ : "";
26
+
27
+ const fnSignature = isTs ? "createShutdown(resources: ShutdownResources = {})" : "createShutdown(resources = {})";
28
+
29
+ const body = `${typeBlock}function ${fnSignature} {
30
+ let shuttingDown = false;
31
+
32
+ return async function shutdown(signal${isTs ? ": string" : ""} = "SIGTERM") {
33
+ if (shuttingDown) return;
34
+ shuttingDown = true;
35
+
36
+ console.log(\`\\n[shutdown] received \${signal}, closing resources...\`);
37
+
38
+ if (resources.httpServer) {
39
+ const server = resources.httpServer;
40
+ await new Promise((resolve) => server.close(() => resolve(undefined)));
41
+ console.log("[shutdown] http server closed");
42
+ }
43
+
44
+ if (resources.io) {
45
+ await resources.io.close();
46
+ console.log("[shutdown] socket.io closed");
47
+ }
48
+
49
+ if (resources.workers && resources.workers.length > 0) {
50
+ await Promise.all(resources.workers.map((worker) => worker.close()));
51
+ console.log("[shutdown] bullmq workers closed");
52
+ }
53
+
54
+ if (resources.queues && resources.queues.length > 0) {
55
+ await Promise.all(resources.queues.map((queue) => queue.close()));
56
+ console.log("[shutdown] bullmq queues closed");
57
+ }
58
+
59
+ if (resources.redis) {
60
+ await resources.redis.quit();
61
+ console.log("[shutdown] redis connection closed");
62
+ }
63
+
64
+ if (resources.db) {
65
+ if (resources.db.disconnect) await resources.db.disconnect();
66
+ else if (resources.db.close) await resources.db.close();
67
+ console.log("[shutdown] database connection closed");
68
+ }
69
+
70
+ console.log("[shutdown] complete");
71
+ process.exit(0);
72
+ };
73
+ }
74
+
75
+ ${esm ? "export { createShutdown };" : "module.exports = { createShutdown };"}
76
+ `;
77
+
78
+ return body;
79
+ }
80
+
81
+ function generateProcessEventsFile(config, shutdownImportPath) {
82
+ const esm = isEsm(config);
83
+ const isTs = config.language === "ts";
84
+ const shutdownImport = esm
85
+ ? `import type { createShutdown } from "${shutdownImportPath}";`
86
+ : "";
87
+
88
+ const fnSignature = isTs
89
+ ? "registerProcessEvents(shutdown: ReturnType<typeof createShutdown>)"
90
+ : "registerProcessEvents(shutdown)";
91
+
92
+ const body = `${isTs && shutdownImport ? shutdownImport + "\n\n" : ""}function ${fnSignature} {
93
+ process.on("SIGINT", () => shutdown("SIGINT"));
94
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
95
+
96
+ process.on("uncaughtException", (error) => {
97
+ console.error("[process] uncaught exception:", error);
98
+ shutdown("uncaughtException");
99
+ });
100
+
101
+ process.on("unhandledRejection", (reason) => {
102
+ console.error("[process] unhandled rejection:", reason);
103
+ shutdown("unhandledRejection");
104
+ });
105
+ }
106
+
107
+ ${esm ? "export { registerProcessEvents };" : "module.exports = { registerProcessEvents };"}
108
+ `;
109
+
110
+ return body;
111
+ }
112
+
113
+ module.exports = { generateShutdownFile, generateProcessEventsFile };
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("./syntax");
4
+
5
+ /**
6
+ * helpers/strip-sensitive-fields.{js,ts} — generated only when at least one entity.json model has
7
+ * a field matching the sensitive-field heuristic (password/secret/hash). Services for those models
8
+ * use this to keep such fields out of API responses without touching every ORM's query layer.
9
+ *
10
+ * `T extends object` (rather than `Record<string, unknown>`) so this also accepts ORM-specific
11
+ * result types without an index signature (e.g. Mongoose Documents, Prisma model types) — the
12
+ * internal cast is the narrow, deliberate escape hatch generic "delete an arbitrary key" needs.
13
+ */
14
+ function generateStripSensitiveFieldsHelper(config) {
15
+ const esm = isEsm(config);
16
+ const isTs = config.language === "ts";
17
+
18
+ const singleSig = isTs
19
+ ? "stripSensitiveFields<T extends object>(record: T | null | undefined, fields: string[]): T | null | undefined"
20
+ : "stripSensitiveFields(record, fields)";
21
+ const listSig = isTs
22
+ ? "stripSensitiveFieldsFromList<T extends object>(records: T[], fields: string[]): T[]"
23
+ : "stripSensitiveFieldsFromList(records, fields)";
24
+ const cast = isTs ? " as Record<string, unknown>" : "";
25
+ const returnCast = isTs ? " as T" : "";
26
+
27
+ return `function ${singleSig} {
28
+ if (!record) return record;
29
+ const clone = { ...record }${cast};
30
+ for (const field of fields) {
31
+ delete clone[field];
32
+ }
33
+ return clone${returnCast};
34
+ }
35
+
36
+ function ${listSig} {
37
+ return records.map((record) => {
38
+ const clone = { ...record }${cast};
39
+ for (const field of fields) {
40
+ delete clone[field];
41
+ }
42
+ return clone${returnCast};
43
+ });
44
+ }
45
+
46
+ ${esm ? "export { stripSensitiveFields, stripSensitiveFieldsFromList };" : "module.exports = { stripSensitiveFields, stripSensitiveFieldsFromList };"}
47
+ `;
48
+ }
49
+
50
+ module.exports = { generateStripSensitiveFieldsHelper };