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.
- package/LICENSE +21 -0
- package/README.md +722 -0
- package/bin/gazan.js +17 -0
- package/package.json +54 -0
- package/src/cli/commands/init.js +126 -0
- package/src/cli/index.js +22 -0
- package/src/config/aliases.js +130 -0
- package/src/config/normalize.js +65 -0
- package/src/generators/aliasResolver.js +68 -0
- package/src/generators/aliasRuntime.js +79 -0
- package/src/generators/database/mongoNative.js +45 -0
- package/src/generators/database/mongoose.js +32 -0
- package/src/generators/database/prisma.js +61 -0
- package/src/generators/engine.js +88 -0
- package/src/generators/entity/sensitiveFields.js +12 -0
- package/src/generators/entity/toApp.js +325 -0
- package/src/generators/entity/toMongoose.js +180 -0
- package/src/generators/entity/toPrisma.js +233 -0
- package/src/generators/entity/toZod.js +70 -0
- package/src/generators/env.js +123 -0
- package/src/generators/errors.js +67 -0
- package/src/generators/features/auth.js +163 -0
- package/src/generators/features/bullmq.js +96 -0
- package/src/generators/features/redis.js +34 -0
- package/src/generators/features/socket.js +45 -0
- package/src/generators/gitignore.js +19 -0
- package/src/generators/index.js +317 -0
- package/src/generators/middlewares.js +167 -0
- package/src/generators/packageJson.js +107 -0
- package/src/generators/paths.js +45 -0
- package/src/generators/project.js +189 -0
- package/src/generators/readme.js +177 -0
- package/src/generators/shutdown.js +113 -0
- package/src/generators/stripSensitiveFieldsHelper.js +50 -0
- package/src/generators/syntax.js +65 -0
- package/src/generators/tsconfig.js +32 -0
- package/src/parser/entity/errors.js +21 -0
- package/src/parser/entity/parse.js +417 -0
- package/src/parser/entity/schema.js +104 -0
- package/src/prompts/index.js +186 -0
- package/src/utils/fsSafety.js +17 -0
- package/src/utils/strings.js +110 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isEsm } = require("../syntax");
|
|
4
|
+
const { sharedDir } = require("../paths");
|
|
5
|
+
const { resolveImportPath } = require("../aliasResolver");
|
|
6
|
+
|
|
7
|
+
/** workers/queue.js — thin factory around BullMQ Queue, reusing the shared Redis connection. */
|
|
8
|
+
function generateQueueFactory(config, aliasConfig) {
|
|
9
|
+
const esm = isEsm(config);
|
|
10
|
+
const isTs = config.language === "ts";
|
|
11
|
+
const fromDir = sharedDir(config, "workers");
|
|
12
|
+
const redisPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "configs")}/redis/index`);
|
|
13
|
+
|
|
14
|
+
const imports = esm ? `import { Queue } from "bullmq";` : `const { Queue } = require("bullmq");`;
|
|
15
|
+
const redisImport = esm
|
|
16
|
+
? `import { redisClient } from "${redisPath}";`
|
|
17
|
+
: `const { redisClient } = require("${redisPath}");`;
|
|
18
|
+
|
|
19
|
+
const sig = isTs ? "createQueue(name: string)" : "createQueue(name)";
|
|
20
|
+
|
|
21
|
+
return `${imports}
|
|
22
|
+
${redisImport}
|
|
23
|
+
|
|
24
|
+
function ${sig} {
|
|
25
|
+
return new Queue(name, { connection: redisClient });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
${esm ? "export { createQueue };" : "module.exports = { createQueue };"}
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** workers/example.worker.js — a starter BullMQ worker demonstrating the queue/worker split. */
|
|
33
|
+
function generateExampleWorker(config, aliasConfig) {
|
|
34
|
+
const esm = isEsm(config);
|
|
35
|
+
const isTs = config.language === "ts";
|
|
36
|
+
const fromDir = sharedDir(config, "workers");
|
|
37
|
+
const redisPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "configs")}/redis/index`);
|
|
38
|
+
|
|
39
|
+
const imports = esm ? `import { Worker } from "bullmq";` : `const { Worker } = require("bullmq");`;
|
|
40
|
+
const redisImport = esm
|
|
41
|
+
? `import { redisClient } from "${redisPath}";`
|
|
42
|
+
: `const { redisClient } = require("${redisPath}");`;
|
|
43
|
+
|
|
44
|
+
const jobType = isTs ? ": import(\"bullmq\").Job" : "";
|
|
45
|
+
|
|
46
|
+
return `${imports}
|
|
47
|
+
${redisImport}
|
|
48
|
+
|
|
49
|
+
const exampleWorker = new Worker(
|
|
50
|
+
"example",
|
|
51
|
+
async (job${jobType}) => {
|
|
52
|
+
console.log(\`[worker:example] processing job \${job.id}\`, job.data);
|
|
53
|
+
},
|
|
54
|
+
{ connection: redisClient }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
exampleWorker.on("completed", (job) => {
|
|
58
|
+
console.log(\`[worker:example] job \${job.id} completed\`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
exampleWorker.on("failed", (job, error) => {
|
|
62
|
+
console.error(\`[worker:example] job \${job?.id} failed:\`, error);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
${esm ? "export { exampleWorker };" : "module.exports = { exampleWorker };"}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** workers/index.js — aggregates queues/workers so server.js can wire them into graceful shutdown in one place. */
|
|
70
|
+
function generateWorkersIndex(config, aliasConfig) {
|
|
71
|
+
const esm = isEsm(config);
|
|
72
|
+
const fromDir = sharedDir(config, "workers");
|
|
73
|
+
// Same-directory siblings — always relative, alias or not.
|
|
74
|
+
const workerPath = resolveImportPath(config, aliasConfig, fromDir, `${fromDir}/example.worker`);
|
|
75
|
+
const queuePath = resolveImportPath(config, aliasConfig, fromDir, `${fromDir}/queue`);
|
|
76
|
+
|
|
77
|
+
const workerImport = esm
|
|
78
|
+
? `import { exampleWorker } from "${workerPath}";`
|
|
79
|
+
: `const { exampleWorker } = require("${workerPath}");`;
|
|
80
|
+
const queueImport = esm
|
|
81
|
+
? `import { createQueue } from "${queuePath}";`
|
|
82
|
+
: `const { createQueue } = require("${queuePath}");`;
|
|
83
|
+
|
|
84
|
+
return `${queueImport}
|
|
85
|
+
${workerImport}
|
|
86
|
+
|
|
87
|
+
const exampleQueue = createQueue("example");
|
|
88
|
+
|
|
89
|
+
const queues = [exampleQueue];
|
|
90
|
+
const workers = [exampleWorker];
|
|
91
|
+
|
|
92
|
+
${esm ? "export { queues, workers, exampleQueue };" : "module.exports = { queues, workers, exampleQueue };"}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { generateQueueFactory, generateExampleWorker, generateWorkersIndex };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isEsm } = require("../syntax");
|
|
4
|
+
const { sharedDir } = require("../paths");
|
|
5
|
+
const { resolveImportPath } = require("../aliasResolver");
|
|
6
|
+
|
|
7
|
+
/** configs/redis/index.{js,ts} — the single shared ioredis connection. Nothing else opens its own client. */
|
|
8
|
+
function generateRedisConfig(config, aliasConfig) {
|
|
9
|
+
const esm = isEsm(config);
|
|
10
|
+
const isTs = config.language === "ts";
|
|
11
|
+
const errorParam = isTs ? "error: Error" : "error";
|
|
12
|
+
const fromDir = `${sharedDir(config, "configs")}/redis`;
|
|
13
|
+
const envPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/env`);
|
|
14
|
+
|
|
15
|
+
// Named import avoids default-export interop issues with ioredis's dual CJS/ESM types under NodeNext.
|
|
16
|
+
const imports = esm ? `import { Redis } from "ioredis";` : `const { Redis } = require("ioredis");`;
|
|
17
|
+
const envImport = esm ? `import { env } from "${envPath}";` : `const { env } = require("${envPath}");`;
|
|
18
|
+
|
|
19
|
+
return `${imports}
|
|
20
|
+
${envImport}
|
|
21
|
+
|
|
22
|
+
const redisClient = new Redis(env.REDIS_URL, {
|
|
23
|
+
maxRetriesPerRequest: null,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
redisClient.on("error", (${errorParam}) => {
|
|
27
|
+
console.error("[redis] connection error:", error);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
${esm ? "export { redisClient };" : "module.exports = { redisClient };"}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { generateRedisConfig };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isEsm } = require("../syntax");
|
|
4
|
+
const { sharedDir } = require("../paths");
|
|
5
|
+
const { resolveImportPath } = require("../aliasResolver");
|
|
6
|
+
|
|
7
|
+
/** socket/index.{js,ts} — Socket.IO is initialized against the raw HTTP server, kept separate from the Express app. */
|
|
8
|
+
function generateSocketIndex(config, aliasConfig) {
|
|
9
|
+
const esm = isEsm(config);
|
|
10
|
+
const isTs = config.language === "ts";
|
|
11
|
+
const fromDir = sharedDir(config, "socket");
|
|
12
|
+
const envPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/env`);
|
|
13
|
+
|
|
14
|
+
const imports = esm ? `import { Server } from "socket.io";` : `const { Server } = require("socket.io");`;
|
|
15
|
+
const envImport = esm ? `import { env } from "${envPath}";` : `const { env } = require("${envPath}");`;
|
|
16
|
+
const httpImport = isTs ? `import type { Server as HttpServer } from "http";\n` : "";
|
|
17
|
+
|
|
18
|
+
const sig = isTs ? "createSocketServer(httpServer: HttpServer)" : "createSocketServer(httpServer)";
|
|
19
|
+
|
|
20
|
+
return `${httpImport}${imports}
|
|
21
|
+
${envImport}
|
|
22
|
+
|
|
23
|
+
function ${sig} {
|
|
24
|
+
const io = new Server(httpServer, {
|
|
25
|
+
cors: {
|
|
26
|
+
origin: env.CORS_ORIGIN,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
io.on("connection", (socket) => {
|
|
31
|
+
console.log(\`[socket] client connected: \${socket.id}\`);
|
|
32
|
+
|
|
33
|
+
socket.on("disconnect", () => {
|
|
34
|
+
console.log(\`[socket] client disconnected: \${socket.id}\`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return io;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
${esm ? "export { createSocketServer };" : "module.exports = { createSocketServer };"}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { generateSocketIndex };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function buildGitignore(config) {
|
|
4
|
+
const lines = [
|
|
5
|
+
"node_modules/",
|
|
6
|
+
".env",
|
|
7
|
+
"*.log",
|
|
8
|
+
"npm-debug.log*",
|
|
9
|
+
".DS_Store",
|
|
10
|
+
"coverage/",
|
|
11
|
+
];
|
|
12
|
+
if (config.language === "ts") lines.push("dist/");
|
|
13
|
+
if (config.database.type === "postgresql" && config.database.orm === "prisma") {
|
|
14
|
+
lines.push("prisma/*.db");
|
|
15
|
+
}
|
|
16
|
+
return lines.join("\n") + "\n";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { buildGitignore };
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { GeneratorEngine } = require("./engine");
|
|
5
|
+
const { baseDir, sharedDir, archDirs } = require("./paths");
|
|
6
|
+
const { buildAliasConfig } = require("../config/aliases");
|
|
7
|
+
const { resolveImportPath } = require("./aliasResolver");
|
|
8
|
+
const {
|
|
9
|
+
buildModuleAliasPackageFragment,
|
|
10
|
+
generateAliasLoaderFile,
|
|
11
|
+
buildTsconfigAliasFragment,
|
|
12
|
+
buildJsconfigJson,
|
|
13
|
+
} = require("./aliasRuntime");
|
|
14
|
+
const { generateAppFile, generateServerFile, generateRoutesIndex } = require("./project");
|
|
15
|
+
const { buildPackageJson, entryPath } = require("./packageJson");
|
|
16
|
+
const { buildTsconfig } = require("./tsconfig");
|
|
17
|
+
const { buildGitignore } = require("./gitignore");
|
|
18
|
+
const { buildReadme } = require("./readme");
|
|
19
|
+
const { generateErrorsFile } = require("./errors");
|
|
20
|
+
const { generateValidateEnvFile, generateEnvHelperFile, buildEnvFileContents } = require("./env");
|
|
21
|
+
const { generateShutdownFile, generateProcessEventsFile } = require("./shutdown");
|
|
22
|
+
const {
|
|
23
|
+
generateNotFoundMiddleware,
|
|
24
|
+
generateErrorHandlerMiddleware,
|
|
25
|
+
generateValidateMiddleware,
|
|
26
|
+
generateRateLimitMiddleware,
|
|
27
|
+
} = require("./middlewares");
|
|
28
|
+
const { generateBcryptHelper, generateJwtHelper, generateAuthMiddleware, generateOAuthStub } = require("./features/auth");
|
|
29
|
+
const { generateRedisConfig } = require("./features/redis");
|
|
30
|
+
const { generateSocketIndex } = require("./features/socket");
|
|
31
|
+
const { generateQueueFactory, generateExampleWorker, generateWorkersIndex } = require("./features/bullmq");
|
|
32
|
+
const { generateSchemaPrisma, generatePrismaDbConfig } = require("./database/prisma");
|
|
33
|
+
const { generateMongooseDbConfig } = require("./database/mongoose");
|
|
34
|
+
const { generateMongoNativeDbConfig } = require("./database/mongoNative");
|
|
35
|
+
const { generateMongooseModel } = require("./entity/toMongoose");
|
|
36
|
+
const { generateEntityValidator } = require("./entity/toZod");
|
|
37
|
+
const { generateService, generateController, generateRoutes } = require("./entity/toApp");
|
|
38
|
+
const { getSensitiveFields } = require("./entity/sensitiveFields");
|
|
39
|
+
const { generateStripSensitiveFieldsHelper } = require("./stripSensitiveFieldsHelper");
|
|
40
|
+
|
|
41
|
+
function ext(config) {
|
|
42
|
+
return config.language === "ts" ? "ts" : "js";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function file(dir, name, config) {
|
|
46
|
+
return path.join(dir, `${name}.${ext(config)}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Phase: base folder skeleton + app/server entrypoints. */
|
|
50
|
+
function generateProject(engine, config) {
|
|
51
|
+
const base = baseDir(config);
|
|
52
|
+
|
|
53
|
+
for (const dir of ["controllers", "routes", "services", "validators", "middlewares", "helpers", "utils", "configs"]) {
|
|
54
|
+
if (config.architecture === "mvc" || !["controllers", "routes", "services", "validators"].includes(dir)) {
|
|
55
|
+
engine.createDirectory(sharedDir(config, dir));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (config.architecture === "hmvc") {
|
|
59
|
+
engine.createDirectory(`${base}/modules`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
engine.writeJson("package.json", buildPackageJson(config));
|
|
63
|
+
engine.createFile(".gitignore", buildGitignore(config));
|
|
64
|
+
|
|
65
|
+
if (config.language === "ts") {
|
|
66
|
+
engine.writeJson("tsconfig.json", buildTsconfig(config));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Phase: helpers/errors, helpers/env + utils/validate-env, process-events, shutdown. */
|
|
71
|
+
function generateEnvironment(engine, config, aliasConfig) {
|
|
72
|
+
const helpersDir = sharedDir(config, "helpers");
|
|
73
|
+
const utilsDir = sharedDir(config, "utils");
|
|
74
|
+
|
|
75
|
+
engine.createFile(file(helpersDir, "errors", config), generateErrorsFile(config));
|
|
76
|
+
|
|
77
|
+
engine.createFile(file(utilsDir, "validate-env", config), generateValidateEnvFile(config));
|
|
78
|
+
engine.createFile(
|
|
79
|
+
file(helpersDir, "env", config),
|
|
80
|
+
generateEnvHelperFile(config, resolveImportPath(config, aliasConfig, helpersDir, `${utilsDir}/validate-env`))
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
engine.createFile(file(utilsDir, "shutdown", config), generateShutdownFile(config));
|
|
84
|
+
engine.createFile(
|
|
85
|
+
file(helpersDir, "process-events", config),
|
|
86
|
+
generateProcessEventsFile(config, resolveImportPath(config, aliasConfig, helpersDir, `${utilsDir}/shutdown`))
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
engine.createFile(".env", buildEnvFileContents(config, { forExample: false }));
|
|
90
|
+
engine.createFile(".env.example", buildEnvFileContents(config, { forExample: true }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Phase: cors/helmet (wired into app.js directly) + rate-limit, error-handler, not-found, validate middlewares. */
|
|
94
|
+
function generateSecurity(engine, config, aliasConfig) {
|
|
95
|
+
const dir = sharedDir(config, "middlewares");
|
|
96
|
+
engine.createFile(file(dir, "not-found", config), generateNotFoundMiddleware(config, aliasConfig));
|
|
97
|
+
engine.createFile(file(dir, "error-handler", config), generateErrorHandlerMiddleware(config, aliasConfig));
|
|
98
|
+
engine.createFile(file(dir, "validate", config), generateValidateMiddleware(config, aliasConfig));
|
|
99
|
+
engine.createFile(file(dir, "rate-limit", config), generateRateLimitMiddleware(config, aliasConfig));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Phase: database config + lifecycle (+ prisma schema from entity when applicable). */
|
|
103
|
+
function generateDatabase(engine, config, aliasConfig, entityModel) {
|
|
104
|
+
if (config.database.type === "none") return;
|
|
105
|
+
|
|
106
|
+
const dbDir = `${sharedDir(config, "configs")}/db`;
|
|
107
|
+
|
|
108
|
+
if (config.database.type === "postgresql" && config.database.orm === "prisma") {
|
|
109
|
+
engine.createFile(file(dbDir, "index", config), generatePrismaDbConfig(config));
|
|
110
|
+
engine.createFile("prisma/schema.prisma", generateSchemaPrisma(config, entityModel));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (config.database.type === "mongodb" && config.database.orm === "mongoose") {
|
|
115
|
+
engine.createFile(file(dbDir, "index", config), generateMongooseDbConfig(config, aliasConfig));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (config.database.type === "mongodb" && config.database.orm === "native") {
|
|
120
|
+
engine.createFile(file(dbDir, "index", config), generateMongoNativeDbConfig(config, aliasConfig));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Phase: shared Redis client. */
|
|
125
|
+
function generateRedis(engine, config, aliasConfig) {
|
|
126
|
+
if (!config.redis) return;
|
|
127
|
+
const dir = `${sharedDir(config, "configs")}/redis`;
|
|
128
|
+
engine.createFile(file(dir, "index", config), generateRedisConfig(config, aliasConfig));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Phase: Socket.IO server, kept separate from Express app + HTTP server. */
|
|
132
|
+
function generateSocket(engine, config, aliasConfig) {
|
|
133
|
+
if (!config.socketIO) return;
|
|
134
|
+
const dir = sharedDir(config, "socket");
|
|
135
|
+
engine.createFile(file(dir, "index", config), generateSocketIndex(config, aliasConfig));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Phase: BullMQ queue/worker split, built on the shared Redis connection. */
|
|
139
|
+
function generateBullMQ(engine, config, aliasConfig) {
|
|
140
|
+
if (!config.bullMQ) return;
|
|
141
|
+
const dir = sharedDir(config, "workers");
|
|
142
|
+
engine.createFile(file(dir, "queue", config), generateQueueFactory(config, aliasConfig));
|
|
143
|
+
engine.createFile(file(dir, "example.worker", config), generateExampleWorker(config, aliasConfig));
|
|
144
|
+
engine.createFile(file(dir, "index", config), generateWorkersIndex(config, aliasConfig));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Phase: bcrypt/jwt helpers + auth middleware, only for the selected methods. */
|
|
148
|
+
function generateAuth(engine, config, aliasConfig) {
|
|
149
|
+
if (!config.authentication.enabled) return;
|
|
150
|
+
const { methods } = config.authentication;
|
|
151
|
+
const helpersDir = sharedDir(config, "helpers");
|
|
152
|
+
|
|
153
|
+
if (methods.includes("email-password")) {
|
|
154
|
+
engine.createFile(file(helpersDir, "bcrypt", config), generateBcryptHelper(config));
|
|
155
|
+
}
|
|
156
|
+
if (methods.includes("jwt") || methods.includes("refresh-token")) {
|
|
157
|
+
engine.createFile(file(helpersDir, "jwt", config), generateJwtHelper(config, aliasConfig));
|
|
158
|
+
engine.createFile(file(sharedDir(config, "middlewares"), "auth", config), generateAuthMiddleware(config, aliasConfig));
|
|
159
|
+
}
|
|
160
|
+
if (methods.includes("oauth")) {
|
|
161
|
+
engine.createFile(file(helpersDir, "oauth.stub", config), generateOAuthStub(config));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (config.language === "ts") {
|
|
165
|
+
const usesJwt = methods.includes("jwt") || methods.includes("refresh-token");
|
|
166
|
+
const userType = usesJwt ? "string | JwtPayload" : "Record<string, unknown>";
|
|
167
|
+
const jwtImport = usesJwt ? `import { JwtPayload } from "jsonwebtoken";\n` : "";
|
|
168
|
+
engine.createFile(
|
|
169
|
+
`${baseDir(config)}/types/express.d.ts`,
|
|
170
|
+
`import "express";\n${jwtImport}\ndeclare module "express-serve-static-core" {\n interface Request {\n user?: ${userType};\n }\n}\n`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Phase: entity.json -> controllers/services/routes/validators (+ mongoose models). Skipped entirely without entity.json. */
|
|
176
|
+
function generateEntityModels(engine, config, aliasConfig, entityModel) {
|
|
177
|
+
const base = baseDir(config);
|
|
178
|
+
const models = entityModel ? entityModel.models : [];
|
|
179
|
+
|
|
180
|
+
if (config.database.type === "mongodb" && config.database.orm === "mongoose") {
|
|
181
|
+
const modelsDir = sharedDir(config, "models");
|
|
182
|
+
for (const model of models) {
|
|
183
|
+
engine.createFile(
|
|
184
|
+
file(modelsDir, `${model.kebabName}.model`, config),
|
|
185
|
+
generateMongooseModel(model, models, config)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const anySensitiveFields = models.some((m) => m.crud !== false && getSensitiveFields(m).length > 0);
|
|
191
|
+
if (anySensitiveFields) {
|
|
192
|
+
engine.createFile(
|
|
193
|
+
file(sharedDir(config, "helpers"), "strip-sensitive-fields", config),
|
|
194
|
+
generateStripSensitiveFieldsHelper(config)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const model of models) {
|
|
199
|
+
if (model.crud === false) continue;
|
|
200
|
+
const dirs = archDirs(config, model.camelName);
|
|
201
|
+
|
|
202
|
+
engine.createFile(file(dirs.validators, `${model.kebabName}.validator`, config), generateEntityValidator(model, config));
|
|
203
|
+
engine.createFile(file(dirs.services, `${model.kebabName}.service`, config), generateService(model, config, aliasConfig, dirs));
|
|
204
|
+
engine.createFile(
|
|
205
|
+
file(dirs.controllers, `${model.kebabName}.controller`, config),
|
|
206
|
+
generateController(model, config, aliasConfig, dirs)
|
|
207
|
+
);
|
|
208
|
+
engine.createFile(file(dirs.routes, `${model.kebabName}.routes`, config), generateRoutes(model, config, aliasConfig, dirs));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const routableModels = models.filter((m) => m.crud !== false);
|
|
212
|
+
engine.createFile(file(sharedDir(config, "routes"), "index", config), generateRoutesIndex(config, aliasConfig, routableModels));
|
|
213
|
+
engine.createFile(file(base, "app", config), generateAppFile(config, aliasConfig));
|
|
214
|
+
engine.createFile(file(base, "server", config), generateServerFile(config, aliasConfig));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Phase: alias runtime support — only runs when aliases are enabled. Merges tsconfig `paths` (TS)
|
|
219
|
+
* or generates jsconfig.json (JS, editor-only) from the SAME aliasConfig every other phase used,
|
|
220
|
+
* plus whatever makes the aliases actually resolve at runtime for the selected module system:
|
|
221
|
+
* tsc-alias (TS build), module-alias (CJS+JS), or a generated ESM loader (MJS+JS).
|
|
222
|
+
*/
|
|
223
|
+
function generateAliasRuntime(engine, config, aliasConfig) {
|
|
224
|
+
if (!aliasConfig.enabled) return;
|
|
225
|
+
|
|
226
|
+
if (config.language === "ts") {
|
|
227
|
+
const { baseUrl, paths } = buildTsconfigAliasFragment(aliasConfig);
|
|
228
|
+
engine.mergeJson("tsconfig.json", { compilerOptions: { baseUrl, paths } });
|
|
229
|
+
engine.updatePackageJson({
|
|
230
|
+
devDependencies: { "tsc-alias": "^1.8.10" },
|
|
231
|
+
scripts: { build: "tsc && tsc-alias" },
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// JS: jsconfig.json is editor-only DX (VS Code path-alias intellisense) — it has no effect on
|
|
237
|
+
// how the project actually runs; module-alias / the generated loader below handle that.
|
|
238
|
+
engine.writeJson("jsconfig.json", buildJsconfigJson(aliasConfig));
|
|
239
|
+
|
|
240
|
+
const entry = entryPath(config);
|
|
241
|
+
if (config.moduleSystem === "mjs") {
|
|
242
|
+
engine.createFile("alias-loader.mjs", generateAliasLoaderFile(aliasConfig));
|
|
243
|
+
engine.updatePackageJson({
|
|
244
|
+
scripts: {
|
|
245
|
+
dev: `node --watch --experimental-loader=./alias-loader.mjs ${entry}`,
|
|
246
|
+
start: `node --experimental-loader=./alias-loader.mjs ${entry}`,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
engine.updatePackageJson({
|
|
251
|
+
dependencies: { "module-alias": "^2.2.3" },
|
|
252
|
+
...buildModuleAliasPackageFragment(aliasConfig),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function generateReadmeFile(engine, config, aliasConfig, entityModel) {
|
|
258
|
+
engine.createFile("README.md", buildReadme(config, aliasConfig, entityModel));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function generate(targetDir, config, entityModel) {
|
|
262
|
+
const engine = new GeneratorEngine(targetDir);
|
|
263
|
+
|
|
264
|
+
// Computed once, up front — every phase below reads this same object, and nothing is written
|
|
265
|
+
// yet if it throws (an HMVC module alias colliding with a reserved name), keeping the atomic
|
|
266
|
+
// generate-then-copy guarantee intact.
|
|
267
|
+
const aliasConfig = buildAliasConfig(config, entityModel);
|
|
268
|
+
|
|
269
|
+
const steps = [
|
|
270
|
+
["Project structure created", () => generateProject(engine, config)],
|
|
271
|
+
["Environment & error handling configured", () => generateEnvironment(engine, config, aliasConfig)],
|
|
272
|
+
["Security middleware configured", () => generateSecurity(engine, config, aliasConfig)],
|
|
273
|
+
["Database configured", () => generateDatabase(engine, config, aliasConfig, entityModel)],
|
|
274
|
+
["Redis configured", () => generateRedis(engine, config, aliasConfig)],
|
|
275
|
+
["BullMQ configured", () => generateBullMQ(engine, config, aliasConfig)],
|
|
276
|
+
["Socket.IO configured", () => generateSocket(engine, config, aliasConfig)],
|
|
277
|
+
["Authentication configured", () => generateAuth(engine, config, aliasConfig)],
|
|
278
|
+
["Entity models generated", () => generateEntityModels(engine, config, aliasConfig, entityModel)],
|
|
279
|
+
["Module aliases configured", () => generateAliasRuntime(engine, config, aliasConfig)],
|
|
280
|
+
["README generated", () => generateReadmeFile(engine, config, aliasConfig, entityModel)],
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const results = [];
|
|
284
|
+
for (const [label, fn] of steps) {
|
|
285
|
+
fn();
|
|
286
|
+
results.push(label);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const warnings = [];
|
|
290
|
+
if (config.authentication.enabled && config.authentication.methods.includes("oauth")) {
|
|
291
|
+
warnings.push(
|
|
292
|
+
"OAuth is scaffolded as a stub only (env vars + helpers/oauth.stub — no provider is wired up). See that file and the README's Authentication section before relying on it."
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (aliasConfig.enabled && config.language === "js" && config.moduleSystem === "mjs") {
|
|
296
|
+
warnings.push(
|
|
297
|
+
"Module aliases on JS+MJS use a custom --experimental-loader — Node will print a one-line ExperimentalWarning on startup. This is expected; see the README's Import Aliases section."
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { engine, results, warnings };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
generate,
|
|
306
|
+
generateProject,
|
|
307
|
+
generateEnvironment,
|
|
308
|
+
generateSecurity,
|
|
309
|
+
generateDatabase,
|
|
310
|
+
generateRedis,
|
|
311
|
+
generateSocket,
|
|
312
|
+
generateBullMQ,
|
|
313
|
+
generateAuth,
|
|
314
|
+
generateEntityModels,
|
|
315
|
+
generateAliasRuntime,
|
|
316
|
+
generateReadmeFile,
|
|
317
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { isEsm } = require("./syntax");
|
|
4
|
+
const { sharedDir } = require("./paths");
|
|
5
|
+
const { resolveImportPath } = require("./aliasResolver");
|
|
6
|
+
|
|
7
|
+
function generateNotFoundMiddleware(config, aliasConfig) {
|
|
8
|
+
const esm = isEsm(config);
|
|
9
|
+
const isTs = config.language === "ts";
|
|
10
|
+
const fromDir = sharedDir(config, "middlewares");
|
|
11
|
+
const errorsPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/errors`);
|
|
12
|
+
const errorsImport = esm
|
|
13
|
+
? `import { NotFoundException } from "${errorsPath}";`
|
|
14
|
+
: `const { NotFoundException } = require("${errorsPath}");`;
|
|
15
|
+
const sig = isTs ? "(req: Request, res: Response, next: NextFunction)" : "(req, res, next)";
|
|
16
|
+
const reqType = isTs ? `import { Request, Response, NextFunction } from "express";\n\n` : "";
|
|
17
|
+
|
|
18
|
+
return `${reqType}${errorsImport}
|
|
19
|
+
|
|
20
|
+
function notFound${sig} {
|
|
21
|
+
next(new NotFoundException(\`Route \${req.method} \${req.originalUrl} not found\`));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
${esm ? "export { notFound };" : "module.exports = { notFound };"}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateErrorHandlerMiddleware(config, aliasConfig) {
|
|
29
|
+
const esm = isEsm(config);
|
|
30
|
+
const isTs = config.language === "ts";
|
|
31
|
+
const fromDir = sharedDir(config, "middlewares");
|
|
32
|
+
const errorsPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/errors`);
|
|
33
|
+
const errorsImport = esm
|
|
34
|
+
? `import { HTTPException } from "${errorsPath}";`
|
|
35
|
+
: `const { HTTPException } = require("${errorsPath}");`;
|
|
36
|
+
const envPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/env`);
|
|
37
|
+
const envImport = esm ? `import { env } from "${envPath}";` : `const { env } = require("${envPath}");`;
|
|
38
|
+
const reqType = isTs ? `import { Request, Response, NextFunction } from "express";\n\n` : "";
|
|
39
|
+
const sig = isTs ? "(err: unknown, req: Request, res: Response, next: NextFunction)" : "(err, req, res, next)";
|
|
40
|
+
|
|
41
|
+
return `${reqType}${errorsImport}
|
|
42
|
+
${envImport}
|
|
43
|
+
|
|
44
|
+
function errorHandler${sig} {
|
|
45
|
+
if (err instanceof HTTPException) {
|
|
46
|
+
return res.status(err.statusCode).json({
|
|
47
|
+
success: false,
|
|
48
|
+
message: err.message,
|
|
49
|
+
details: err.details,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.error("[error]", err);
|
|
54
|
+
|
|
55
|
+
// Never leak internals (stack traces, driver error text) to clients in production — only in
|
|
56
|
+
// development, and only for genuinely unexpected (non-HTTPException) errors.
|
|
57
|
+
const isDev = env.NODE_ENV === "development";
|
|
58
|
+
const devDetails = isDev && err instanceof Error ? err.stack : undefined;
|
|
59
|
+
|
|
60
|
+
return res.status(500).json({
|
|
61
|
+
success: false,
|
|
62
|
+
message: "Internal Server Error",
|
|
63
|
+
...(devDetails ? { details: devDetails } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
${esm ? "export { errorHandler };" : "module.exports = { errorHandler };"}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function generateValidateMiddleware(config, aliasConfig) {
|
|
72
|
+
const esm = isEsm(config);
|
|
73
|
+
const isTs = config.language === "ts";
|
|
74
|
+
const fromDir = sharedDir(config, "middlewares");
|
|
75
|
+
const zodImport = esm ? `import { ZodType } from "zod";` : "";
|
|
76
|
+
const errorsPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/errors`);
|
|
77
|
+
const errorsImport = esm
|
|
78
|
+
? `import { BadRequestException } from "${errorsPath}";`
|
|
79
|
+
: `const { BadRequestException } = require("${errorsPath}");`;
|
|
80
|
+
const reqType = isTs ? `import { Request, Response, NextFunction } from "express";\n${zodImport}\n\n` : "";
|
|
81
|
+
|
|
82
|
+
const sig = isTs
|
|
83
|
+
? "validate(schema: ZodType, source: \"body\" | \"query\" | \"params\" = \"body\")"
|
|
84
|
+
: "validate(schema, source = \"body\")";
|
|
85
|
+
const handlerSig = isTs ? "(req: Request, res: Response, next: NextFunction)" : "(req, res, next)";
|
|
86
|
+
|
|
87
|
+
return `${reqType}${errorsImport}
|
|
88
|
+
|
|
89
|
+
function ${sig} {
|
|
90
|
+
return function ${handlerSig} {
|
|
91
|
+
const result = schema.safeParse(req[source]);
|
|
92
|
+
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
const details = result.error.issues.map((issue) => ({
|
|
95
|
+
path: issue.path.join("."),
|
|
96
|
+
message: issue.message,
|
|
97
|
+
}));
|
|
98
|
+
return next(new BadRequestException("Validation failed", details));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
req[source] = result.data;
|
|
102
|
+
return next();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
${esm ? "export { validate };" : "module.exports = { validate };"}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function generateRateLimitMiddleware(config, aliasConfig) {
|
|
111
|
+
const esm = isEsm(config);
|
|
112
|
+
const isTs = config.language === "ts";
|
|
113
|
+
const fromDir = sharedDir(config, "middlewares");
|
|
114
|
+
|
|
115
|
+
if (config.rateLimitStrategy === "redis") {
|
|
116
|
+
const redisPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "configs")}/redis/index`);
|
|
117
|
+
const imports = esm
|
|
118
|
+
? [
|
|
119
|
+
`import rateLimit from "express-rate-limit";`,
|
|
120
|
+
`import { RedisStore } from "rate-limit-redis";`,
|
|
121
|
+
`import { redisClient } from "${redisPath}";`,
|
|
122
|
+
]
|
|
123
|
+
: [
|
|
124
|
+
`const rateLimit = require("express-rate-limit");`,
|
|
125
|
+
`const { RedisStore } = require("rate-limit-redis");`,
|
|
126
|
+
`const { redisClient } = require("${redisPath}");`,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
return `${imports.join("\n")}
|
|
130
|
+
|
|
131
|
+
const rateLimiter = rateLimit({
|
|
132
|
+
windowMs: 15 * 60 * 1000,
|
|
133
|
+
limit: 100,
|
|
134
|
+
standardHeaders: true,
|
|
135
|
+
legacyHeaders: false,
|
|
136
|
+
store: new RedisStore({
|
|
137
|
+
// Using the array-form call(command, args) overload (rather than spreading) sidesteps a
|
|
138
|
+
// tuple-typing mismatch between ioredis's overloads and rate-limit-redis's SendCommandFn type.
|
|
139
|
+
sendCommand: (...args${isTs ? ": string[]" : ""}) => redisClient.call(args[0], args.slice(1))${isTs ? " as any" : ""},
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
${esm ? "export { rateLimiter };" : "module.exports = { rateLimiter };"}
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const imports = esm ? `import rateLimit from "express-rate-limit";` : `const rateLimit = require("express-rate-limit");`;
|
|
148
|
+
|
|
149
|
+
return `${imports}
|
|
150
|
+
|
|
151
|
+
const rateLimiter = rateLimit({
|
|
152
|
+
windowMs: 15 * 60 * 1000,
|
|
153
|
+
limit: 100,
|
|
154
|
+
standardHeaders: true,
|
|
155
|
+
legacyHeaders: false,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
${esm ? "export { rateLimiter };" : "module.exports = { rateLimiter };"}
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
generateNotFoundMiddleware,
|
|
164
|
+
generateErrorHandlerMiddleware,
|
|
165
|
+
generateValidateMiddleware,
|
|
166
|
+
generateRateLimitMiddleware,
|
|
167
|
+
};
|