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,233 @@
1
+ "use strict";
2
+
3
+ const { pluralize } = require("../../utils/strings");
4
+
5
+ /**
6
+ * Normalized entity model -> Prisma schema string.
7
+ * entity.json -> Entity Parser -> Normalized Entity Model -> (this) Database Generator
8
+ */
9
+
10
+ const PRISMA_TYPE_MAP = {
11
+ string: "String",
12
+ text: "String",
13
+ number: "Float",
14
+ integer: "Int",
15
+ float: "Float",
16
+ boolean: "Boolean",
17
+ date: "DateTime",
18
+ datetime: "DateTime",
19
+ uuid: "String",
20
+ json: "Json",
21
+ enum: null, // resolved to the generated enum name
22
+ decimal: "Decimal",
23
+ bigint: "BigInt",
24
+ };
25
+
26
+ function fieldEnumName(modelName, fieldName) {
27
+ return `${modelName}${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
28
+ }
29
+
30
+ function buildPrismaField(model, field) {
31
+ const parts = [field.name];
32
+ const prismaType = field.type === "enum" ? fieldEnumName(model.pascalName, field.name) : PRISMA_TYPE_MAP[field.type];
33
+ let typeStr = prismaType;
34
+ // Primary keys are always non-optional in Prisma regardless of the entity.json 'required' flag.
35
+ if (!field.primaryKey && (!field.required || field.nullable)) {
36
+ typeStr += "?";
37
+ }
38
+ parts.push(typeStr);
39
+
40
+ const attrs = [];
41
+ if (field.primaryKey) {
42
+ attrs.push("@id");
43
+ }
44
+ if (field.type === "uuid" && field.default === "uuid") {
45
+ attrs.push("@default(uuid())");
46
+ } else if (field.autoIncrement) {
47
+ attrs.push("@default(autoincrement())");
48
+ } else if ((field.type === "date" || field.type === "datetime") && field.default === "now") {
49
+ attrs.push("@default(now())");
50
+ } else if (field.default !== undefined && field.default !== null && field.default !== "uuid") {
51
+ attrs.push(`@default(${formatDefault(field)})`);
52
+ } else if (field.type === "datetime" && field.name.toLowerCase() === "createdat" && !field.default) {
53
+ attrs.push("@default(now())");
54
+ }
55
+ if (field.unique) attrs.push("@unique");
56
+ if (field.length && (field.type === "string" || field.type === "text")) {
57
+ attrs.push(`@db.VarChar(${field.length})`);
58
+ }
59
+
60
+ if (attrs.length > 0) parts.push(attrs.join(" "));
61
+ return parts.join(" ");
62
+ }
63
+
64
+ function formatDefault(field) {
65
+ if (field.type === "boolean") return String(Boolean(field.default));
66
+ if (["number", "integer", "float", "decimal", "bigint"].includes(field.type)) return String(field.default);
67
+ if (field.type === "enum") return String(field.default);
68
+ return JSON.stringify(field.default);
69
+ }
70
+
71
+ function buildEnums(model) {
72
+ return model.fields
73
+ .filter((f) => f.type === "enum")
74
+ .map((f) => {
75
+ const enumName = fieldEnumName(model.pascalName, f.name);
76
+ const values = f.values.join("\n ");
77
+ return `enum ${enumName} {\n ${values}\n}`;
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Deterministic Prisma @relation name for the FK identified by (modelHoldingForeignKey, foreignKey).
83
+ * Both the owning (belongsTo) side and the reverse (hasMany/hasOne/auto) side derive the same
84
+ * string independently from these two inputs, so they always pair up — including self-relations
85
+ * and multiple distinct relations between the same two models, which Prisma would otherwise
86
+ * reject as ambiguous.
87
+ */
88
+ function relationKey(modelHoldingForeignKey, foreignKey) {
89
+ return `${modelHoldingForeignKey.pascalName}_${foreignKey}`;
90
+ }
91
+
92
+ /**
93
+ * Builds a lookup of every hasMany/hasOne relation explicitly declared anywhere in the schema,
94
+ * keyed by the (owner model holding the FK, foreignKey) pair it answers. Used so we never
95
+ * auto-generate a reverse field that duplicates one the user already wrote by hand.
96
+ */
97
+ function indexExplicitReverses(allModels) {
98
+ const map = new Map();
99
+ for (const declaringModel of allModels) {
100
+ for (const relation of declaringModel.relations) {
101
+ if (relation.type !== "hasMany" && relation.type !== "hasOne") continue;
102
+ const owner = allModels.find((m) => m.name === relation.model);
103
+ if (!owner || !relation.foreignKey) continue;
104
+ map.set(relationKey(owner, relation.foreignKey), {
105
+ declaringModelName: declaringModel.name,
106
+ relationName: relation.name,
107
+ });
108
+ }
109
+ }
110
+ return map;
111
+ }
112
+
113
+ /**
114
+ * Counts, per (owner model, target model) pair, how many auto-derived (non-explicit) belongsTo
115
+ * relations need a reverse field — so that when there's more than one (e.g. Post.author and
116
+ * Post.editor both pointing at User), every reverse field is disambiguated, not just the 2nd+.
117
+ */
118
+ function countAutoReversePairs(allModels, explicitReverses) {
119
+ const counts = new Map();
120
+ for (const owner of allModels) {
121
+ for (const relation of owner.relations) {
122
+ if (relation.type !== "belongsTo") continue;
123
+ const target = allModels.find((m) => m.name === relation.model);
124
+ if (!target) continue;
125
+ const key = relationKey(owner, relation.foreignKey);
126
+ if (explicitReverses.has(key)) continue;
127
+ const pairId = `${owner.name}->${target.name}`;
128
+ counts.set(pairId, (counts.get(pairId) || 0) + 1);
129
+ }
130
+ }
131
+ return counts;
132
+ }
133
+
134
+ /**
135
+ * Fields owned by `model` for its own declared relations (belongsTo/hasMany/hasOne/belongsToMany),
136
+ * plus any auto-derived reverse array fields this model needs to receive because some other model
137
+ * declared a belongsTo pointing at it without an explicit matching hasMany/hasOne.
138
+ */
139
+ function buildRelationFields(model, allModels, explicitReverses, pairTotalCounts) {
140
+ const lines = [];
141
+
142
+ for (const relation of model.relations) {
143
+ const target = allModels.find((m) => m.name === relation.model);
144
+ if (!target) continue;
145
+
146
+ if (relation.type === "belongsTo") {
147
+ const key = relationKey(model, relation.foreignKey);
148
+ // The relation field's optionality must mirror the FK scalar's — Prisma rejects a required
149
+ // relation object backed by a nullable foreign key (and vice versa).
150
+ const fkField = model.fields.find((f) => f.name === relation.foreignKey);
151
+ const optional = !fkField || !fkField.required || fkField.nullable ? "?" : "";
152
+ lines.push(
153
+ ` ${relation.name} ${target.pascalName}${optional} @relation("${key}", fields: [${relation.foreignKey}], references: [${target.primaryKey.name}])`
154
+ );
155
+ } else if (relation.type === "hasMany") {
156
+ const key = relationKey(target, relation.foreignKey);
157
+ lines.push(` ${relation.name} ${target.pascalName}[] @relation("${key}")`);
158
+ } else if (relation.type === "hasOne") {
159
+ const key = relationKey(target, relation.foreignKey);
160
+ lines.push(` ${relation.name} ${target.pascalName}? @relation("${key}")`);
161
+ } else if (relation.type === "belongsToMany") {
162
+ lines.push(` ${relation.name} ${target.pascalName}[] @relation("${relation.through}")`);
163
+ }
164
+ }
165
+
166
+ // Auto-derive the reverse side for every belongsTo (anywhere in the schema, including on this
167
+ // model itself for self-relations) that points at `model` and has no explicit hasMany/hasOne.
168
+ for (const owner of allModels) {
169
+ for (const relation of owner.relations) {
170
+ if (relation.type !== "belongsTo" || relation.model !== model.name) continue;
171
+
172
+ const key = relationKey(owner, relation.foreignKey);
173
+ if (explicitReverses.has(key)) continue; // user already declared this side by hand
174
+
175
+ const pairId = `${owner.name}->${model.name}`;
176
+ const base = pluralize(owner.camelName);
177
+ const needsDisambiguation = (pairTotalCounts.get(pairId) || 0) > 1;
178
+ const fieldName = needsDisambiguation ? `${base}As${relation.pascalName}` : base;
179
+
180
+ lines.push(` ${fieldName} ${owner.pascalName}[] @relation("${key}")`);
181
+ }
182
+ }
183
+
184
+ return lines;
185
+ }
186
+
187
+ function generatePrismaSchema(entityModel, config) {
188
+ const provider = "postgresql";
189
+ const header = [
190
+ `generator client {`,
191
+ ` provider = "prisma-client-js"`,
192
+ `}`,
193
+ ``,
194
+ `datasource db {`,
195
+ ` provider = "${provider}"`,
196
+ ` url = env("DATABASE_URL")`,
197
+ `}`,
198
+ ].join("\n");
199
+
200
+ const blocks = [];
201
+ const enumBlocks = [];
202
+ const explicitReverses = indexExplicitReverses(entityModel.models);
203
+ const pairTotalCounts = countAutoReversePairs(entityModel.models, explicitReverses);
204
+
205
+ for (const model of entityModel.models) {
206
+ const lines = [`model ${model.pascalName} {`];
207
+ for (const field of model.fields) {
208
+ lines.push(` ${buildPrismaField(model, field)}`);
209
+ }
210
+ const relationLines = buildRelationFields(model, entityModel.models, explicitReverses, pairTotalCounts);
211
+ for (const line of relationLines) lines.push(line);
212
+
213
+ if (model.timestamps) {
214
+ lines.push(` createdAt DateTime @default(now())`);
215
+ lines.push(` updatedAt DateTime @updatedAt`);
216
+ }
217
+
218
+ const indexedFields = model.fields.filter((f) => f.index && !f.unique && !f.primaryKey);
219
+ for (const field of indexedFields) {
220
+ lines.push(`\n @@index([${field.name}])`);
221
+ }
222
+
223
+ lines.push(`\n @@map("${model.tableName}")`);
224
+ lines.push(`}`);
225
+ blocks.push(lines.join("\n"));
226
+
227
+ enumBlocks.push(...buildEnums(model));
228
+ }
229
+
230
+ return [header, ...enumBlocks, ...blocks].join("\n\n") + "\n";
231
+ }
232
+
233
+ module.exports = { generatePrismaSchema };
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("../syntax");
4
+
5
+ const ZOD_BASE = {
6
+ string: "z.string()",
7
+ text: "z.string()",
8
+ number: "z.number()",
9
+ integer: "z.number().int()",
10
+ float: "z.number()",
11
+ boolean: "z.boolean()",
12
+ date: "z.coerce.date()",
13
+ datetime: "z.coerce.date()",
14
+ uuid: "z.string().uuid()",
15
+ json: "z.record(z.string(), z.any())",
16
+ decimal: "z.number()",
17
+ bigint: "z.number().int()",
18
+ };
19
+
20
+ function buildFieldZod(field) {
21
+ if (field.type === "enum") {
22
+ let expr = `z.enum(${JSON.stringify(field.values)})`;
23
+ if (!field.required) expr += ".optional()";
24
+ if (field.nullable) expr += ".nullable()";
25
+ return expr;
26
+ }
27
+
28
+ let expr = ZOD_BASE[field.type];
29
+ if (field.type === "string" || field.type === "text") {
30
+ if (field.length) expr += `.max(${field.length})`;
31
+ if (field.min !== undefined) expr += `.min(${field.min})`;
32
+ } else if (["number", "integer", "float", "decimal", "bigint"].includes(field.type)) {
33
+ if (field.min !== undefined) expr += `.min(${field.min})`;
34
+ if (field.max !== undefined) expr += `.max(${field.max})`;
35
+ }
36
+
37
+ if (!field.required) expr += ".optional()";
38
+ if (field.nullable) expr += ".nullable()";
39
+ return expr;
40
+ }
41
+
42
+ /** entity model -> Zod create/update validators. Skips the primary key and timestamp fields on write payloads. */
43
+ function generateEntityValidator(model, config) {
44
+ const esm = isEsm(config);
45
+ const zodImport = esm ? `import { z } from "zod";` : `const { z } = require("zod");`;
46
+
47
+ const writableFields = model.fields.filter((f) => !(f.primaryKey && f.synthetic));
48
+
49
+ const createLines = writableFields.map((f) => ` ${f.camelName}: ${buildFieldZod(f)},`).join("\n");
50
+
51
+ const createSchemaName = `create${model.pascalName}Schema`;
52
+ const updateSchemaName = `update${model.pascalName}Schema`;
53
+
54
+ const body = `${zodImport}
55
+
56
+ const ${createSchemaName} = z.object({
57
+ ${createLines}
58
+ });
59
+
60
+ const ${updateSchemaName} = ${createSchemaName}.partial();
61
+ `;
62
+
63
+ const footer = esm
64
+ ? `\nexport { ${createSchemaName}, ${updateSchemaName} };\n`
65
+ : `\nmodule.exports = { ${createSchemaName}, ${updateSchemaName} };\n`;
66
+
67
+ return body + footer;
68
+ }
69
+
70
+ module.exports = { generateEntityValidator };
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("./syntax");
4
+
5
+ /**
6
+ * Computes which env vars this project actually needs, based on the
7
+ * normalized config. Nothing here is generated for disabled features.
8
+ */
9
+ function buildEnvSpec(config) {
10
+ const vars = [
11
+ { name: "NODE_ENV", zod: `z.enum(["development", "test", "production"]).default("development")`, example: "development" },
12
+ { name: "PORT", zod: `z.coerce.number().int().positive().default(3000)`, example: "3000" },
13
+ { name: "CORS_ORIGIN", zod: `z.string().default("*")`, example: "*" },
14
+ ];
15
+
16
+ if (config.database.type === "postgresql") {
17
+ vars.push({
18
+ name: "DATABASE_URL",
19
+ zod: `z.string().url()`,
20
+ example: "postgresql://user:password@localhost:5432/mydb",
21
+ });
22
+ }
23
+
24
+ if (config.database.type === "mongodb") {
25
+ vars.push({
26
+ name: "MONGODB_URI",
27
+ zod: `z.string().min(1)`,
28
+ example: "mongodb://localhost:27017/mydb",
29
+ });
30
+ }
31
+
32
+ if (config.redis) {
33
+ vars.push({
34
+ name: "REDIS_URL",
35
+ zod: `z.string().min(1).default("redis://localhost:6379")`,
36
+ example: "redis://localhost:6379",
37
+ });
38
+ }
39
+
40
+ // helpers/jwt.js always emits sign/verifyAccessToken (using JWT_SECRET/JWT_EXPIRES_IN) whenever
41
+ // it's generated at all — which happens for EITHER 'jwt' or 'refresh-token' — so these must be
42
+ // required in both cases, not just when 'jwt' itself is selected.
43
+ if (
44
+ config.authentication.enabled &&
45
+ (config.authentication.methods.includes("jwt") || config.authentication.methods.includes("refresh-token"))
46
+ ) {
47
+ vars.push({ name: "JWT_SECRET", zod: `z.string().min(10)`, example: "replace-with-a-long-random-secret" });
48
+ vars.push({ name: "JWT_EXPIRES_IN", zod: `z.string().default("15m")`, example: "15m" });
49
+ }
50
+
51
+ if (config.authentication.enabled && config.authentication.methods.includes("refresh-token")) {
52
+ vars.push({ name: "JWT_REFRESH_SECRET", zod: `z.string().min(10)`, example: "replace-with-a-long-random-secret" });
53
+ vars.push({ name: "JWT_REFRESH_EXPIRES_IN", zod: `z.string().default("7d")`, example: "7d" });
54
+ }
55
+
56
+ if (config.authentication.enabled && config.authentication.methods.includes("oauth")) {
57
+ vars.push({ name: "OAUTH_CLIENT_ID", zod: `z.string().min(1)`, example: "your-oauth-client-id" });
58
+ vars.push({ name: "OAUTH_CLIENT_SECRET", zod: `z.string().min(1)`, example: "your-oauth-client-secret" });
59
+ vars.push({ name: "OAUTH_CALLBACK_URL", zod: `z.string().url()`, example: "http://localhost:3000/auth/oauth/callback" });
60
+ }
61
+
62
+ return vars;
63
+ }
64
+
65
+ function generateValidateEnvFile(config) {
66
+ const vars = buildEnvSpec(config);
67
+ const esm = isEsm(config);
68
+ const isTs = config.language === "ts";
69
+
70
+ const zodImport = esm ? `import { z } from "zod";` : `const { z } = require("zod");`;
71
+
72
+ const schemaFields = vars.map((v) => ` ${v.name}: ${v.zod},`).join("\n");
73
+
74
+ const fnBody = `${zodImport}
75
+
76
+ const envSchema = z.object({
77
+ ${schemaFields}
78
+ });
79
+
80
+ ${isTs ? "export type Env = z.infer<typeof envSchema>;\n\n" : ""}function validateEnv(source${isTs ? ": NodeJS.ProcessEnv" : ""} = process.env)${isTs ? ": Env" : ""} {
81
+ const result = envSchema.safeParse(source);
82
+
83
+ if (!result.success) {
84
+ const messages = result.error.issues
85
+ .map((issue) => \` \${issue.path.join(".")}: \${issue.message}\`)
86
+ .join("\\n");
87
+ throw new Error(\`Invalid environment configuration:\\n\${messages}\`);
88
+ }
89
+
90
+ return result.data;
91
+ }
92
+
93
+ ${esm ? "export { validateEnv };" : "module.exports = { validateEnv };"}
94
+ `;
95
+
96
+ return fnBody;
97
+ }
98
+
99
+ function generateEnvHelperFile(config, validateEnvImportPath) {
100
+ const esm = isEsm(config);
101
+ const dotenvImport = esm ? `import dotenv from "dotenv";` : `const dotenv = require("dotenv");`;
102
+ const validateImport = esm
103
+ ? `import { validateEnv } from "${validateEnvImportPath}";`
104
+ : `const { validateEnv } = require("${validateEnvImportPath}");`;
105
+
106
+ return `${dotenvImport}
107
+ ${validateImport}
108
+
109
+ dotenv.config();
110
+
111
+ const env = validateEnv(process.env);
112
+
113
+ ${esm ? "export { env };\nexport default env;" : "module.exports = { env };\nmodule.exports.default = env;"}
114
+ `;
115
+ }
116
+
117
+ function buildEnvFileContents(config, { forExample }) {
118
+ const vars = buildEnvSpec(config);
119
+ const lines = vars.map((v) => `${v.name}=${forExample ? v.example : v.example}`);
120
+ return lines.join("\n") + "\n";
121
+ }
122
+
123
+ module.exports = { buildEnvSpec, generateValidateEnvFile, generateEnvHelperFile, buildEnvFileContents };
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("./syntax");
4
+
5
+ const NAMES = [
6
+ "HTTPException",
7
+ "BadRequestException",
8
+ "UnauthorizedException",
9
+ "ForbiddenException",
10
+ "NotFoundException",
11
+ "ConflictException",
12
+ "UnprocessableEntityException",
13
+ "InternalServerErrorException",
14
+ ];
15
+
16
+ function generateErrorsFile(config) {
17
+ const isTs = config.language === "ts";
18
+ const esm = isEsm(config);
19
+ const kw = esm ? "export class" : "class";
20
+
21
+ const typedCtorArgs = isTs
22
+ ? {
23
+ base: "statusCode: number, message: string, details: unknown = null",
24
+ sub: "message = \"__DEFAULT__\", details: unknown = null",
25
+ }
26
+ : {
27
+ base: "statusCode, message, details = null",
28
+ sub: "message = \"__DEFAULT__\", details = null",
29
+ };
30
+
31
+ const fields = isTs ? " public statusCode: number;\n public details: unknown;\n\n" : "";
32
+
33
+ const base = `${kw} HTTPException extends Error {
34
+ ${fields} constructor(${typedCtorArgs.base}) {
35
+ super(message);
36
+ this.name = "HTTPException";
37
+ this.statusCode = statusCode;
38
+ this.details = details;
39
+ Error.captureStackTrace(this, this.constructor);
40
+ }
41
+ }`;
42
+
43
+ const subclasses = [
44
+ ["BadRequestException", 400, "Bad Request"],
45
+ ["UnauthorizedException", 401, "Unauthorized"],
46
+ ["ForbiddenException", 403, "Forbidden"],
47
+ ["NotFoundException", 404, "Not Found"],
48
+ ["ConflictException", 409, "Conflict"],
49
+ ["UnprocessableEntityException", 422, "Unprocessable Entity"],
50
+ ["InternalServerErrorException", 500, "Internal Server Error"],
51
+ ].map(([name, status, message]) => {
52
+ const ctorArgs = typedCtorArgs.sub.replace("__DEFAULT__", message);
53
+ return `${kw} ${name} extends HTTPException {
54
+ constructor(${ctorArgs}) {
55
+ super(${status}, message, details);
56
+ this.name = "${name}";
57
+ }
58
+ }`;
59
+ });
60
+
61
+ const body = [base, ...subclasses].join("\n\n");
62
+ const footer = esm ? "" : `\n\nmodule.exports = {\n ${NAMES.join(",\n ")},\n};\n`;
63
+
64
+ return `${body}${footer}\n`;
65
+ }
66
+
67
+ module.exports = { generateErrorsFile };
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+
3
+ const { isEsm } = require("../syntax");
4
+ const { sharedDir } = require("../paths");
5
+ const { resolveImportPath } = require("../aliasResolver");
6
+
7
+ /** helpers/bcrypt.{js,ts} — the only place password hashing logic lives. */
8
+ function generateBcryptHelper(config) {
9
+ const esm = isEsm(config);
10
+ const isTs = config.language === "ts";
11
+ const imports = esm ? `import bcrypt from "bcrypt";` : `const bcrypt = require("bcrypt");`;
12
+ const SALT_ROUNDS = 10;
13
+
14
+ const hashSig = isTs ? "hashPassword(password: string)" : "hashPassword(password)";
15
+ const compareSig = isTs
16
+ ? "comparePassword(password: string, hash: string)"
17
+ : "comparePassword(password, hash)";
18
+
19
+ return `${imports}
20
+
21
+ const SALT_ROUNDS = ${SALT_ROUNDS};
22
+
23
+ async function ${hashSig} {
24
+ return bcrypt.hash(password, SALT_ROUNDS);
25
+ }
26
+
27
+ async function ${compareSig} {
28
+ return bcrypt.compare(password, hash);
29
+ }
30
+
31
+ ${esm ? "export { hashPassword, comparePassword };" : "module.exports = { hashPassword, comparePassword };"}
32
+ `;
33
+ }
34
+
35
+ /** helpers/jwt.{js,ts} — access/refresh token signing and verification, centralized. */
36
+ function generateJwtHelper(config, aliasConfig) {
37
+ const esm = isEsm(config);
38
+ const isTs = config.language === "ts";
39
+ const hasRefresh = config.authentication.methods.includes("refresh-token");
40
+ const fromDir = sharedDir(config, "helpers");
41
+ // helpers/env is a same-directory sibling of helpers/jwt, so this resolves to "./env" either way.
42
+ const envPath = resolveImportPath(config, aliasConfig, fromDir, `${fromDir}/env`);
43
+
44
+ const imports = isTs
45
+ ? `import jwt, { SignOptions } from "jsonwebtoken";`
46
+ : esm
47
+ ? `import jwt from "jsonwebtoken";`
48
+ : `const jwt = require("jsonwebtoken");`;
49
+ const envImport = esm ? `import { env } from "${envPath}";` : `const { env } = require("${envPath}");`;
50
+
51
+ const expiresInCast = isTs ? " as SignOptions[\"expiresIn\"]" : "";
52
+ const signAccessSig = isTs ? `signAccessToken(payload: object)` : "signAccessToken(payload)";
53
+ const verifyAccessSig = isTs ? "verifyAccessToken(token: string)" : "verifyAccessToken(token)";
54
+
55
+ let refreshFns = "";
56
+ if (hasRefresh) {
57
+ const signRefreshSig = isTs ? "signRefreshToken(payload: object)" : "signRefreshToken(payload)";
58
+ const verifyRefreshSig = isTs ? "verifyRefreshToken(token: string)" : "verifyRefreshToken(token)";
59
+ refreshFns = `
60
+ function ${signRefreshSig} {
61
+ return jwt.sign(payload, env.JWT_REFRESH_SECRET, { expiresIn: env.JWT_REFRESH_EXPIRES_IN${expiresInCast} });
62
+ }
63
+
64
+ function ${verifyRefreshSig} {
65
+ return jwt.verify(token, env.JWT_REFRESH_SECRET);
66
+ }
67
+ `;
68
+ }
69
+
70
+ const exportsList = ["signAccessToken", "verifyAccessToken"];
71
+ if (hasRefresh) exportsList.push("signRefreshToken", "verifyRefreshToken");
72
+
73
+ return `${imports}
74
+ ${envImport}
75
+
76
+ function ${signAccessSig} {
77
+ return jwt.sign(payload, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN${expiresInCast} });
78
+ }
79
+
80
+ function ${verifyAccessSig} {
81
+ return jwt.verify(token, env.JWT_SECRET);
82
+ }
83
+ ${refreshFns}
84
+ ${esm ? `export { ${exportsList.join(", ")} };` : `module.exports = { ${exportsList.join(", ")} };`}
85
+ `;
86
+ }
87
+
88
+ /** middlewares/auth.{js,ts} — verifies the Bearer access token and attaches req.user. */
89
+ function generateAuthMiddleware(config, aliasConfig) {
90
+ const esm = isEsm(config);
91
+ const isTs = config.language === "ts";
92
+ const fromDir = sharedDir(config, "middlewares");
93
+ const jwtPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/jwt`);
94
+ const errorsPath = resolveImportPath(config, aliasConfig, fromDir, `${sharedDir(config, "helpers")}/errors`);
95
+
96
+ const jwtImport = esm
97
+ ? `import { verifyAccessToken } from "${jwtPath}";`
98
+ : `const { verifyAccessToken } = require("${jwtPath}");`;
99
+ const errorsImport = esm
100
+ ? `import { UnauthorizedException } from "${errorsPath}";`
101
+ : `const { UnauthorizedException } = require("${errorsPath}");`;
102
+ const reqType = isTs ? `import { Request, Response, NextFunction } from "express";\n\n` : "";
103
+ const sig = isTs ? "(req: Request, res: Response, next: NextFunction)" : "(req, res, next)";
104
+
105
+ return `${reqType}${jwtImport}
106
+ ${errorsImport}
107
+
108
+ function authenticate${sig} {
109
+ const header = req.headers.authorization;
110
+
111
+ if (!header || !header.startsWith("Bearer ")) {
112
+ return next(new UnauthorizedException("Missing or malformed Authorization header"));
113
+ }
114
+
115
+ const token = header.slice("Bearer ".length);
116
+
117
+ try {
118
+ req.user = verifyAccessToken(token);
119
+ return next();
120
+ } catch (error) {
121
+ return next(new UnauthorizedException("Invalid or expired token"));
122
+ }
123
+ }
124
+
125
+ ${esm ? "export { authenticate };" : "module.exports = { authenticate };"}
126
+ `;
127
+ }
128
+
129
+ /**
130
+ * helpers/oauth.stub.{js,ts} — GAZAN does NOT generate a working OAuth integration. "OAuth" isn't
131
+ * one thing — Google, GitHub, generic OIDC, etc. all need different scopes, callback handling, and
132
+ * token exchange, so wiring a specific provider is left to the project. This file exists so the
133
+ * gap is obvious and documented rather than a silently missing piece; see README's Authentication
134
+ * section for the same note.
135
+ */
136
+ function generateOAuthStub(config) {
137
+ const esm = isEsm(config);
138
+ const isTs = config.language === "ts";
139
+ const sig = isTs ? "notConfigured(): never" : "notConfigured()";
140
+
141
+ return `/**
142
+ * OAuth is scaffolded as env vars only (OAUTH_CLIENT_ID / OAUTH_CLIENT_SECRET /
143
+ * OAUTH_CALLBACK_URL) — GAZAN does not generate a provider integration. To finish this:
144
+ *
145
+ * 1. Pick a provider (Google, GitHub, a generic OIDC server, ...).
146
+ * 2. Add its client library (e.g. \`openid-client\`, or a provider-specific SDK) yourself —
147
+ * none is installed by default.
148
+ * 3. Implement the redirect + callback routes (e.g. GET /auth/oauth, GET /auth/oauth/callback)
149
+ * using env.OAUTH_CLIENT_ID / env.OAUTH_CLIENT_SECRET / env.OAUTH_CALLBACK_URL below.
150
+ * 4. On success, mint your own access token the same way helpers/jwt.${isTs ? "ts" : "js"} does,
151
+ * so the rest of the app (middlewares/auth) keeps working unchanged.
152
+ */
153
+ function ${sig} {
154
+ throw new Error(
155
+ "OAuth is not implemented — this is a stub. See helpers/oauth.stub.${isTs ? "ts" : "js"} for what to build."
156
+ );
157
+ }
158
+
159
+ ${esm ? "export { notConfigured };" : "module.exports = { notConfigured };"}
160
+ `;
161
+ }
162
+
163
+ module.exports = { generateBcryptHelper, generateJwtHelper, generateAuthMiddleware, generateOAuthStub };