create-backlist 6.1.6 → 6.1.7

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/analyzer.js +410 -99
  3. package/src/generators/dotnet.js +1 -1
  4. package/src/generators/java.js +154 -97
  5. package/src/generators/node.js +213 -211
  6. package/src/templates/java-spring/partials/ApplicationSeeder.java.ejs +29 -7
  7. package/src/templates/java-spring/partials/AuthController.java.ejs +45 -14
  8. package/src/templates/java-spring/partials/Controller.java.ejs +25 -11
  9. package/src/templates/java-spring/partials/Dockerfile.ejs +25 -3
  10. package/src/templates/java-spring/partials/Entity.java.ejs +28 -3
  11. package/src/templates/java-spring/partials/JwtAuthFilter.java.ejs +41 -7
  12. package/src/templates/java-spring/partials/JwtService.java.ejs +47 -12
  13. package/src/templates/java-spring/partials/Repository.java.ejs +8 -1
  14. package/src/templates/java-spring/partials/Service.java.ejs +30 -6
  15. package/src/templates/java-spring/partials/User.java.ejs +26 -3
  16. package/src/templates/java-spring/partials/UserDetailsServiceImpl.java.ejs +10 -4
  17. package/src/templates/java-spring/partials/UserRepository.java.ejs +6 -0
  18. package/src/templates/java-spring/partials/docker-compose.yml.ejs +27 -5
  19. package/src/templates/node-ts-express/base/server.ts +63 -9
  20. package/src/templates/node-ts-express/base/tsconfig.json +19 -4
  21. package/src/templates/node-ts-express/partials/ApiDocs.ts.ejs +24 -9
  22. package/src/templates/node-ts-express/partials/App.test.ts.ejs +47 -27
  23. package/src/templates/node-ts-express/partials/Auth.controller.ts.ejs +68 -45
  24. package/src/templates/node-ts-express/partials/Auth.middleware.ts.ejs +45 -14
  25. package/src/templates/node-ts-express/partials/Auth.routes.ts.ejs +44 -5
  26. package/src/templates/node-ts-express/partials/Controller.ts.ejs +30 -16
  27. package/src/templates/node-ts-express/partials/Dockerfile.ejs +33 -11
  28. package/src/templates/node-ts-express/partials/Model.cs.ejs +38 -5
  29. package/src/templates/node-ts-express/partials/Model.ts.ejs +42 -12
  30. package/src/templates/node-ts-express/partials/PrismaController.ts.ejs +57 -23
  31. package/src/templates/node-ts-express/partials/PrismaSchema.prisma.ejs +33 -10
  32. package/src/templates/node-ts-express/partials/README.md.ejs +8 -10
  33. package/src/templates/node-ts-express/partials/Seeder.ts.ejs +99 -56
  34. package/src/templates/node-ts-express/partials/docker-compose.yml.ejs +30 -3
  35. package/src/templates/node-ts-express/partials/package.json.ejs +12 -7
  36. package/src/templates/node-ts-express/partials/routes.ts.ejs +31 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backlist",
3
- "version": "6.1.6",
3
+ "version": "6.1.7",
4
4
  "description": "An advanced, multi-language backend generator based on frontend analysis.",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/src/analyzer.js CHANGED
@@ -1,125 +1,436 @@
1
- const fs = require('fs-extra');
2
- const { glob } = require('glob');
3
- const parser = require('@babel/parser');
4
- const traverse = require('@babel/traverse').default;
5
-
6
- /**
7
- * Converts a string to TitleCase, which is suitable for model and controller names.
8
- * e.g., 'user-orders' -> 'UserOrders'
9
- * @param {string} str The input string.
10
- * @returns {string} The TitleCased string.
11
- */
1
+ /* eslint-disable @typescript-eslint/no-var-requires */
2
+ const fs = require("fs-extra");
3
+ const path = require("path");
4
+ const { glob } = require("glob");
5
+
6
+ const parser = require("@babel/parser");
7
+ const traverse = require("@babel/traverse").default;
8
+
9
+ const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete"]);
10
+
11
+ // -------------------------
12
+ // Utils
13
+ // -------------------------
14
+ function normalizeSlashes(p) {
15
+ return String(p || "").replace(/\\/g, "/");
16
+ }
17
+
12
18
  function toTitleCase(str) {
13
- if (!str) return 'Default';
14
- return str.replace(/-_(\w)/g, g => g[1].toUpperCase()) // handle snake_case and kebab-case
15
- .replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
16
- .replace(/[^a-zA-Z0-9]/g, '');
19
+ if (!str) return "Default";
20
+ return String(str)
21
+ .replace(/[-_]+(\w)/g, (_, c) => c.toUpperCase())
22
+ .replace(/^\w/, (c) => c.toUpperCase())
23
+ .replace(/[^a-zA-Z0-9]/g, "");
24
+ }
25
+
26
+ // Convert `/api/users/{id}` -> `/api/users/:id`
27
+ function normalizeRouteForBackend(urlValue) {
28
+ return String(urlValue || "").replace(/\{(\w+)\}/g, ":$1");
29
+ }
30
+
31
+ function extractApiPath(urlValue) {
32
+ // supports:
33
+ // - /api/...
34
+ // - http://localhost:5000/api/...
35
+ if (!urlValue) return null;
36
+ const idx = urlValue.indexOf("/api/");
37
+ if (idx === -1) return null;
38
+ return urlValue.slice(idx); // => /api/...
39
+ }
40
+
41
+ function extractPathParams(route) {
42
+ const params = [];
43
+ const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
44
+ let m;
45
+ while ((m = re.exec(route))) params.push(m[1]);
46
+ return Array.from(new Set(params));
47
+ }
48
+
49
+ function extractQueryParamsFromUrl(urlValue) {
50
+ try {
51
+ const qIndex = urlValue.indexOf("?");
52
+ if (qIndex === -1) return [];
53
+ const qs = urlValue.slice(qIndex + 1);
54
+ return qs
55
+ .split("&")
56
+ .map((p) => p.split("=")[0])
57
+ .filter(Boolean);
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
63
+ function deriveControllerNameFromUrl(urlValue) {
64
+ const apiPath = extractApiPath(urlValue) || urlValue;
65
+ const parts = String(apiPath).split("/").filter(Boolean); // ["api","v1","products"]
66
+ const apiIndex = parts.indexOf("api");
67
+
68
+ let seg = null;
69
+ if (apiIndex >= 0) {
70
+ seg = parts[apiIndex + 1] || null;
71
+
72
+ // skip version segment (v1, v2, v10...)
73
+ if (seg && /^v\d+$/i.test(seg)) {
74
+ seg = parts[apiIndex + 2] || seg;
75
+ }
76
+ } else {
77
+ seg = parts[0] || null;
78
+ }
79
+
80
+ return toTitleCase(seg);
81
+ }
82
+
83
+ function deriveActionName(method, route) {
84
+ const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
85
+ const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
86
+ return `${String(method).toLowerCase()}${toTitleCase(last)}`;
87
+ }
88
+
89
+ // -------------------------
90
+ // URL extraction
91
+ // -------------------------
92
+ function getUrlValue(urlNode) {
93
+ if (!urlNode) return null;
94
+
95
+ if (urlNode.type === "StringLiteral") return urlNode.value;
96
+
97
+ if (urlNode.type === "TemplateLiteral") {
98
+ // `/api/users/${id}` -> `/api/users/{id}` or `{param1}`
99
+ const quasis = urlNode.quasis || [];
100
+ const exprs = urlNode.expressions || [];
101
+ let out = "";
102
+ for (let i = 0; i < quasis.length; i++) {
103
+ out += quasis[i].value.raw;
104
+ if (exprs[i]) {
105
+ if (exprs[i].type === "Identifier") out += `{${exprs[i].name}}`;
106
+ else out += `{param${i + 1}}`;
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ // -------------------------
116
+ // axios-like detection
117
+ // -------------------------
118
+ function detectAxiosLikeMethod(node) {
119
+ // axios.get(...) / api.get(...) / httpClient.post(...) etc
120
+ if (!node.callee || node.callee.type !== "MemberExpression") return null;
121
+
122
+ const prop = node.callee.property;
123
+ if (!prop || prop.type !== "Identifier") return null;
124
+
125
+ const name = prop.name.toLowerCase();
126
+ if (!HTTP_METHODS.has(name)) return null;
127
+
128
+ return name.toUpperCase();
129
+ }
130
+
131
+ // -------------------------
132
+ // Request body schema (simple + identifier tracing)
133
+ // -------------------------
134
+ function inferTypeFromNode(node) {
135
+ if (!node) return "String";
136
+ switch (node.type) {
137
+ case "StringLiteral":
138
+ return "String";
139
+ case "NumericLiteral":
140
+ return "Number";
141
+ case "BooleanLiteral":
142
+ return "Boolean";
143
+ case "NullLiteral":
144
+ return "String";
145
+ default:
146
+ return "String";
147
+ }
148
+ }
149
+
150
+ function extractObjectSchema(objExpr) {
151
+ const schemaFields = {};
152
+ if (!objExpr || objExpr.type !== "ObjectExpression") return null;
153
+
154
+ for (const prop of objExpr.properties) {
155
+ if (prop.type !== "ObjectProperty") continue;
156
+
157
+ const key =
158
+ prop.key.type === "Identifier"
159
+ ? prop.key.name
160
+ : prop.key.type === "StringLiteral"
161
+ ? prop.key.value
162
+ : null;
163
+
164
+ if (!key) continue;
165
+ schemaFields[key] = inferTypeFromNode(prop.value);
166
+ }
167
+ return schemaFields;
168
+ }
169
+
170
+ function resolveIdentifierToInit(callPath, identifierName) {
171
+ try {
172
+ const binding = callPath.scope.getBinding(identifierName);
173
+ if (!binding) return null;
174
+ const declPath = binding.path;
175
+ if (!declPath || !declPath.node) return null;
176
+
177
+ if (declPath.node.type === "VariableDeclarator") return declPath.node.init || null;
178
+ return null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ function isJSONStringifyCall(node) {
185
+ // JSON.stringify(x)
186
+ return (
187
+ node &&
188
+ node.type === "CallExpression" &&
189
+ node.callee &&
190
+ node.callee.type === "MemberExpression" &&
191
+ node.callee.object &&
192
+ node.callee.object.type === "Identifier" &&
193
+ node.callee.object.name === "JSON" &&
194
+ node.callee.property &&
195
+ node.callee.property.type === "Identifier" &&
196
+ node.callee.property.name === "stringify"
197
+ );
17
198
  }
18
199
 
19
- /**
20
- * Analyzes frontend source files to find API endpoints and their details.
21
- * @param {string} srcPath The path to the frontend source directory.
22
- * @returns {Promise<Array<object>>} A promise that resolves to an array of endpoint objects.
23
- */
200
+ // -------------------------
201
+ // DB insights: guess db + infer models + seeds
202
+ // -------------------------
203
+ function guessDbTypeFromRepo(rootDir) {
204
+ // Best-effort; if it's only frontend repo, usually null.
205
+ try {
206
+ const pkgPath = path.join(rootDir, "package.json");
207
+ if (!fs.existsSync(pkgPath)) return null;
208
+
209
+ const pkg = fs.readJsonSync(pkgPath);
210
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
211
+
212
+ if (deps.mongoose || deps.mongodb) return "mongodb-mongoose";
213
+ if (deps.prisma || deps["@prisma/client"]) return "sql-prisma";
214
+ if (deps.sequelize) return "sql-sequelize";
215
+ if (deps.typeorm) return "sql-typeorm";
216
+
217
+ return null;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ function inferModelsFromEndpoints(endpoints) {
224
+ const models = new Map();
225
+
226
+ for (const ep of endpoints) {
227
+ const modelName = ep.controllerName || "Default";
228
+
229
+ if (!models.has(modelName)) {
230
+ models.set(modelName, {
231
+ name: modelName,
232
+ fields: {}, // merged fields from bodies
233
+ sources: new Set(),
234
+ endpoints: [],
235
+ });
236
+ }
237
+
238
+ const m = models.get(modelName);
239
+ m.endpoints.push({ method: ep.method, route: ep.route });
240
+ if (ep.sourceFile) m.sources.add(ep.sourceFile);
241
+
242
+ const fields = ep.schemaFields || (ep.requestBody && ep.requestBody.fields) || null;
243
+ if (fields) {
244
+ for (const [k, t] of Object.entries(fields)) {
245
+ if (!m.fields[k]) m.fields[k] = t || "String";
246
+ }
247
+ }
248
+ }
249
+
250
+ return Array.from(models.values()).map((m) => ({
251
+ name: m.name,
252
+ fields: m.fields,
253
+ sources: Array.from(m.sources),
254
+ endpoints: m.endpoints,
255
+ }));
256
+ }
257
+
258
+ function seedValueForType(t) {
259
+ if (t === "Number") return 1;
260
+ if (t === "Boolean") return true;
261
+ return "test"; // String default
262
+ }
263
+
264
+ function generateSeedsFromModels(models, perModel = 3) {
265
+ return models.map((m) => {
266
+ const rows = [];
267
+ for (let i = 0; i < perModel; i++) {
268
+ const obj = {};
269
+ for (const [k, t] of Object.entries(m.fields || {})) {
270
+ obj[k] = seedValueForType(t);
271
+ }
272
+ rows.push(obj);
273
+ }
274
+ return { model: m.name, rows };
275
+ });
276
+ }
277
+
278
+ // -------------------------
279
+ // MAIN frontend analyzer
280
+ // -------------------------
24
281
  async function analyzeFrontend(srcPath) {
282
+ if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
25
283
  if (!fs.existsSync(srcPath)) {
26
284
  throw new Error(`The source directory '${srcPath}' does not exist.`);
27
285
  }
28
286
 
29
- const files = await glob(`${srcPath}/**/*.{js,ts,jsx,tsx}`, { ignore: 'node_modules/**' });
287
+ const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
288
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
289
+ });
290
+
30
291
  const endpoints = new Map();
31
292
 
32
293
  for (const file of files) {
33
- const code = await fs.readFile(file, 'utf-8');
294
+ let code;
34
295
  try {
35
- const ast = parser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
36
-
37
- traverse(ast, {
38
- CallExpression(path) {
39
- // We are only interested in 'fetch' calls
40
- if (path.node.callee.name !== 'fetch') return;
41
-
42
- const urlNode = path.node.arguments[0];
43
-
44
- let urlValue;
45
- if (urlNode.type === 'StringLiteral') {
46
- urlValue = urlNode.value;
47
- } else if (urlNode.type === 'TemplateLiteral' && urlNode.quasis.length > 0) {
48
- // Reconstruct path for dynamic URLs like `/api/users/${id}` -> `/api/users/{id}`
49
- urlValue = urlNode.quasis.map((q, i) => {
50
- return q.value.raw + (urlNode.expressions[i] ? `{${urlNode.expressions[i].name || 'id'}}` : '');
51
- }).join('');
52
- }
53
-
54
- // Only process API calls that start with '/api/'
55
- if (!urlValue || !urlValue.startsWith('/api/')) return;
56
-
57
- let method = 'GET';
58
- let schemaFields = null;
59
-
60
- const optionsNode = path.node.arguments[1];
61
- if (optionsNode && optionsNode.type === 'ObjectExpression') {
62
- // Find the HTTP method
63
- const methodProp = optionsNode.properties.find(p => p.key.name === 'method');
64
- if (methodProp && methodProp.value.type === 'StringLiteral') {
296
+ code = await fs.readFile(file, "utf-8");
297
+ } catch {
298
+ continue;
299
+ }
300
+
301
+ let ast;
302
+ try {
303
+ ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
304
+ } catch {
305
+ continue;
306
+ }
307
+
308
+ traverse(ast, {
309
+ CallExpression(callPath) {
310
+ const node = callPath.node;
311
+
312
+ const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
313
+ const axiosMethod = detectAxiosLikeMethod(node);
314
+
315
+ if (!isFetch && !axiosMethod) return;
316
+
317
+ let urlValue = null;
318
+ let method = "GET";
319
+ let schemaFields = null;
320
+
321
+ // ---- fetch(url, options) ----
322
+ if (isFetch) {
323
+ urlValue = getUrlValue(node.arguments[0]);
324
+ const optionsNode = node.arguments[1];
325
+
326
+ if (optionsNode && optionsNode.type === "ObjectExpression") {
327
+ const methodProp = optionsNode.properties.find(
328
+ (p) =>
329
+ p.type === "ObjectProperty" &&
330
+ p.key.type === "Identifier" &&
331
+ p.key.name === "method"
332
+ );
333
+ if (methodProp && methodProp.value.type === "StringLiteral") {
65
334
  method = methodProp.value.value.toUpperCase();
66
335
  }
67
336
 
68
- // --- NEW LOGIC: Analyze the 'body' for POST/PUT requests ---
69
- if (method === 'POST' || method === 'PUT') {
70
- const bodyProp = optionsNode.properties.find(p => p.key.name === 'body');
71
-
72
- // Check if body is wrapped in JSON.stringify
73
- if (bodyProp && bodyProp.value.callee && bodyProp.value.callee.name === 'JSON.stringify') {
74
- const dataObjectNode = bodyProp.value.arguments[0];
75
-
76
- // This is a simplified analysis assuming the object is defined inline.
77
- // A more robust solution would trace variables back to their definition.
78
- if (dataObjectNode.type === 'ObjectExpression') {
79
- schemaFields = {};
80
- dataObjectNode.properties.forEach(prop => {
81
- const key = prop.key.name;
82
- const valueNode = prop.value;
83
-
84
- // Infer Mongoose schema type based on the value's literal type
85
- if (valueNode.type === 'StringLiteral') {
86
- schemaFields[key] = 'String';
87
- } else if (valueNode.type === 'NumericLiteral') {
88
- schemaFields[key] = 'Number';
89
- } else if (valueNode.type === 'BooleanLiteral') {
90
- schemaFields[key] = 'Boolean';
91
- } else {
92
- // Default to String if the type is complex or a variable
93
- schemaFields[key] = 'String';
94
- }
95
- });
337
+ if (["POST", "PUT", "PATCH"].includes(method)) {
338
+ const bodyProp = optionsNode.properties.find(
339
+ (p) =>
340
+ p.type === "ObjectProperty" &&
341
+ p.key.type === "Identifier" &&
342
+ p.key.name === "body"
343
+ );
344
+
345
+ if (bodyProp) {
346
+ const v = bodyProp.value;
347
+
348
+ if (isJSONStringifyCall(v)) {
349
+ const arg0 = v.arguments[0];
350
+
351
+ if (arg0?.type === "ObjectExpression") {
352
+ schemaFields = extractObjectSchema(arg0);
353
+ } else if (arg0?.type === "Identifier") {
354
+ const init = resolveIdentifierToInit(callPath, arg0.name);
355
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
356
+ }
96
357
  }
97
358
  }
98
359
  }
99
360
  }
361
+ }
362
+
363
+ // ---- axios-like client ----
364
+ if (axiosMethod) {
365
+ method = axiosMethod;
366
+ urlValue = getUrlValue(node.arguments[0]);
100
367
 
101
- // Generate a clean controller name (e.g., /api/user-orders -> UserOrders)
102
- const controllerName = toTitleCase(urlValue.split('/')[2]);
103
- const key = `${method}:${urlValue}`;
104
-
105
- // Avoid adding duplicate endpoints
106
- if (!endpoints.has(key)) {
107
- endpoints.set(key, {
108
- path: urlValue,
109
- method,
110
- controllerName,
111
- schemaFields // This will be null for GET/DELETE, and an object for POST/PUT
112
- });
368
+ if (["POST", "PUT", "PATCH"].includes(method)) {
369
+ const dataArg = node.arguments[1];
370
+ if (dataArg?.type === "ObjectExpression") {
371
+ schemaFields = extractObjectSchema(dataArg);
372
+ } else if (dataArg?.type === "Identifier") {
373
+ const init = resolveIdentifierToInit(callPath, dataArg.name);
374
+ if (init?.type === "ObjectExpression") schemaFields = extractObjectSchema(init);
375
+ }
113
376
  }
114
- },
115
- });
116
- } catch (e) {
117
- // Ignore files that babel can't parse (e.g., CSS-in-JS files)
118
- }
377
+ }
378
+
379
+ // accept only URLs that contain /api/ anywhere
380
+ const apiPath = extractApiPath(urlValue);
381
+ if (!apiPath) return;
382
+
383
+ const route = normalizeRouteForBackend(apiPath.split("?")[0]);
384
+ const controllerName = deriveControllerNameFromUrl(apiPath);
385
+ const actionName = deriveActionName(method, route);
386
+
387
+ const key = `${method}:${route}`;
388
+ if (!endpoints.has(key)) {
389
+ endpoints.set(key, {
390
+ path: apiPath,
391
+ route,
392
+ method,
393
+ controllerName,
394
+ actionName,
395
+ pathParams: extractPathParams(route),
396
+ queryParams: extractQueryParamsFromUrl(apiPath),
397
+ schemaFields,
398
+ requestBody: schemaFields ? { fields: schemaFields } : null,
399
+ sourceFile: normalizeSlashes(file),
400
+ });
401
+ }
402
+ },
403
+ });
119
404
  }
120
405
 
121
- // Return all found endpoints as an array
122
406
  return Array.from(endpoints.values());
123
407
  }
124
408
 
125
- module.exports = { analyzeFrontend };
409
+ // -------------------------
410
+ // Optional: full project analyze (endpoints + db insights)
411
+ // -------------------------
412
+ async function analyze(projectRoot = process.cwd()) {
413
+ const rootDir = path.resolve(projectRoot);
414
+
415
+ const frontendSrc = ["src", "app", "pages"]
416
+ .map((d) => path.join(rootDir, d))
417
+ .find((d) => fs.existsSync(d));
418
+
419
+ const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
420
+
421
+ const models = inferModelsFromEndpoints(endpoints);
422
+ const seeds = generateSeedsFromModels(models, 3);
423
+ const guessedDb = guessDbTypeFromRepo(rootDir);
424
+
425
+ return {
426
+ rootDir: normalizeSlashes(rootDir),
427
+ endpoints,
428
+ dbInsights: {
429
+ guessedDb, // null | mongodb-mongoose | sql-prisma | ...
430
+ models, // inferred entities + fields
431
+ seeds, // dummy seed rows
432
+ },
433
+ };
434
+ }
435
+
436
+ module.exports = { analyzeFrontend, analyze };
@@ -77,7 +77,7 @@ async function generateDotnetProject(options) {
77
77
  let usingStatements = 'using Microsoft.EntityFrameworkCore;\nusing '+projectName+'.Data;\n';
78
78
  programCsContent = usingStatements + programCsContent;
79
79
 
80
- let dbContextService = `// Configure the database context\nbuilder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseInMemoryDatabase("MyDb"));`;
80
+ let dbContextService = '// Configure the database context\nbuilder.Services.AddDbContext<ApplicationDbContext>(opt => opt.UseInMemoryDatabase("MyDb"));';
81
81
  programCsContent = programCsContent.replace('builder.Services.AddControllers();', `builder.Services.AddControllers();\n\n${dbContextService}`);
82
82
 
83
83
  // Enable CORS to allow frontend communication