create-forgeon 0.1.34 → 0.1.36

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 (38) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/modules/db-prisma.mjs +401 -0
  4. package/src/modules/executor.mjs +4 -0
  5. package/src/modules/executor.test.mjs +585 -13
  6. package/src/modules/i18n.mjs +244 -22
  7. package/src/modules/jwt-auth.mjs +612 -0
  8. package/src/modules/logger.mjs +76 -27
  9. package/src/modules/registry.mjs +15 -7
  10. package/src/modules/swagger.mjs +12 -2
  11. package/templates/module-fragments/db-prisma/00_title.md +6 -0
  12. package/templates/module-fragments/db-prisma/10_overview.md +10 -0
  13. package/templates/module-fragments/db-prisma/20_scope.md +14 -0
  14. package/templates/module-fragments/db-prisma/90_status_implemented.md +4 -0
  15. package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
  16. package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
  17. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
  18. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
  19. package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
  20. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
  21. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
  22. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
  23. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
  24. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
  25. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
  26. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
  27. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
  28. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
  29. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
  30. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
  31. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
  32. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
  33. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
  34. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
  35. package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
  36. package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
  37. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
  38. package/templates/module-presets/jwt-auth/packages/auth-contracts/tsconfig.json +9 -0
@@ -34,6 +34,31 @@ function ensureScript(packageJson, name, command) {
34
34
  packageJson.scripts[name] = command;
35
35
  }
36
36
 
37
+ function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
38
+ if (!packageJson.scripts) {
39
+ packageJson.scripts = {};
40
+ }
41
+
42
+ const current = packageJson.scripts[scriptName];
43
+ const steps =
44
+ typeof current === 'string' && current.trim().length > 0
45
+ ? current
46
+ .split('&&')
47
+ .map((item) => item.trim())
48
+ .filter(Boolean)
49
+ : [];
50
+
51
+ for (const command of requiredCommands) {
52
+ if (!steps.includes(command)) {
53
+ steps.push(command);
54
+ }
55
+ }
56
+
57
+ if (steps.length > 0) {
58
+ packageJson.scripts[scriptName] = steps.join(' && ');
59
+ }
60
+ }
61
+
37
62
  function upsertEnvLines(filePath, lines) {
38
63
  let content = '';
39
64
  if (fs.existsSync(filePath)) {
@@ -87,6 +112,48 @@ function ensureLineBefore(content, anchorLine, lineToInsert) {
87
112
  return `${content.slice(0, index)}${lineToInsert}\n${content.slice(index)}`;
88
113
  }
89
114
 
115
+ function ensureLoadItem(content, itemName) {
116
+ const pattern = /load:\s*\[([^\]]*)\]/m;
117
+ const match = content.match(pattern);
118
+ if (!match) {
119
+ return content;
120
+ }
121
+
122
+ const rawList = match[1];
123
+ const items = rawList
124
+ .split(',')
125
+ .map((item) => item.trim())
126
+ .filter(Boolean);
127
+
128
+ if (!items.includes(itemName)) {
129
+ items.push(itemName);
130
+ }
131
+
132
+ const next = `load: [${items.join(', ')}]`;
133
+ return content.replace(pattern, next);
134
+ }
135
+
136
+ function ensureValidatorSchema(content, schemaName) {
137
+ const pattern = /validate:\s*createEnvValidator\(\[([^\]]*)\]\)/m;
138
+ const match = content.match(pattern);
139
+ if (!match) {
140
+ return content;
141
+ }
142
+
143
+ const rawList = match[1];
144
+ const items = rawList
145
+ .split(',')
146
+ .map((item) => item.trim())
147
+ .filter(Boolean);
148
+
149
+ if (!items.includes(schemaName)) {
150
+ items.push(schemaName);
151
+ }
152
+
153
+ const next = `validate: createEnvValidator([${items.join(', ')}])`;
154
+ return content.replace(pattern, next);
155
+ }
156
+
90
157
  function patchApiDockerfile(targetRoot) {
91
158
  const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
92
159
  if (!fs.existsSync(dockerfilePath)) {
@@ -242,11 +309,12 @@ function patchApiPackage(targetRoot) {
242
309
  }
243
310
 
244
311
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
245
- ensureScript(
246
- packageJson,
247
- 'predev',
248
- 'pnpm --filter @forgeon/core build && pnpm --filter @forgeon/db-prisma build && pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n build',
249
- );
312
+ ensureBuildSteps(packageJson, 'predev', [
313
+ 'pnpm --filter @forgeon/core build',
314
+ 'pnpm --filter @forgeon/db-prisma build',
315
+ 'pnpm --filter @forgeon/i18n-contracts build',
316
+ 'pnpm --filter @forgeon/i18n build',
317
+ ]);
250
318
  ensureDependency(packageJson, '@forgeon/i18n', 'workspace:*');
251
319
  ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
252
320
  ensureDependency(packageJson, '@forgeon/db-prisma', 'workspace:*');
@@ -261,16 +329,14 @@ function patchWebPackage(targetRoot) {
261
329
  }
262
330
 
263
331
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
264
- ensureScript(
265
- packageJson,
266
- 'predev',
267
- 'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n-web build',
268
- );
269
- ensureScript(
270
- packageJson,
271
- 'prebuild',
272
- 'pnpm --filter @forgeon/i18n-contracts build && pnpm --filter @forgeon/i18n-web build',
273
- );
332
+ ensureBuildSteps(packageJson, 'predev', [
333
+ 'pnpm --filter @forgeon/i18n-contracts build',
334
+ 'pnpm --filter @forgeon/i18n-web build',
335
+ ]);
336
+ ensureBuildSteps(packageJson, 'prebuild', [
337
+ 'pnpm --filter @forgeon/i18n-contracts build',
338
+ 'pnpm --filter @forgeon/i18n-web build',
339
+ ]);
274
340
  ensureDependency(packageJson, '@forgeon/i18n-contracts', 'workspace:*');
275
341
  ensureDependency(packageJson, '@forgeon/i18n-web', 'workspace:*');
276
342
  ensureDependency(packageJson, 'i18next', '^23.16.8');
@@ -278,6 +344,167 @@ function patchWebPackage(targetRoot) {
278
344
  writeJson(packagePath, packageJson);
279
345
  }
280
346
 
347
+ function patchAppModule(targetRoot) {
348
+ const appModulePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
349
+ if (!fs.existsSync(appModulePath)) {
350
+ return;
351
+ }
352
+
353
+ let content = fs.readFileSync(appModulePath, 'utf8').replace(/\r\n/g, '\n');
354
+ if (!content.includes("from '@forgeon/i18n';")) {
355
+ if (content.includes("import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';")) {
356
+ content = ensureLineAfter(
357
+ content,
358
+ "import { ForgeonLoggerModule, loggerConfig, loggerEnvSchema } from '@forgeon/logger';",
359
+ "import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
360
+ );
361
+ } else if (
362
+ content.includes("import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';")
363
+ ) {
364
+ content = ensureLineAfter(
365
+ content,
366
+ "import { ForgeonSwaggerModule, swaggerConfig, swaggerEnvSchema } from '@forgeon/swagger';",
367
+ "import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
368
+ );
369
+ } else if (
370
+ content.includes("import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';")
371
+ ) {
372
+ content = ensureLineAfter(
373
+ content,
374
+ "import { dbPrismaConfig, dbPrismaEnvSchema, DbPrismaModule } from '@forgeon/db-prisma';",
375
+ "import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
376
+ );
377
+ } else {
378
+ content = ensureLineAfter(
379
+ content,
380
+ "import { ConfigModule } from '@nestjs/config';",
381
+ "import { ForgeonI18nModule, i18nConfig, i18nEnvSchema } from '@forgeon/i18n';",
382
+ );
383
+ }
384
+ }
385
+
386
+ if (!content.includes("import { join } from 'path';")) {
387
+ content = ensureLineBefore(
388
+ content,
389
+ "import { HealthController } from './health/health.controller';",
390
+ "import { join } from 'path';",
391
+ );
392
+ }
393
+
394
+ if (!content.includes('const i18nPath =')) {
395
+ content = ensureLineBefore(
396
+ content,
397
+ '@Module({',
398
+ "const i18nPath = join(__dirname, '..', '..', '..', 'resources', 'i18n');",
399
+ );
400
+ }
401
+
402
+ content = ensureLoadItem(content, 'i18nConfig');
403
+ content = ensureValidatorSchema(content, 'i18nEnvSchema');
404
+
405
+ const i18nModuleBlock = ` ForgeonI18nModule.register({
406
+ path: i18nPath,
407
+ }),`;
408
+ if (!content.includes('ForgeonI18nModule.register({')) {
409
+ if (content.includes(' DbPrismaModule,')) {
410
+ content = ensureLineAfter(content, ' DbPrismaModule,', i18nModuleBlock);
411
+ } else if (content.includes(' ForgeonLoggerModule,')) {
412
+ content = ensureLineAfter(content, ' ForgeonLoggerModule,', i18nModuleBlock);
413
+ } else if (content.includes(' ForgeonSwaggerModule,')) {
414
+ content = ensureLineAfter(content, ' ForgeonSwaggerModule,', i18nModuleBlock);
415
+ } else {
416
+ content = ensureLineAfter(content, ' CoreErrorsModule,', i18nModuleBlock);
417
+ }
418
+ }
419
+
420
+ fs.writeFileSync(appModulePath, `${content.trimEnd()}\n`, 'utf8');
421
+ }
422
+
423
+ function patchHealthController(targetRoot) {
424
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
425
+ if (!fs.existsSync(filePath)) {
426
+ return;
427
+ }
428
+
429
+ let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
430
+ if (!content.includes("from 'nestjs-i18n';")) {
431
+ if (content.includes("import { PrismaService } from '@forgeon/db-prisma';")) {
432
+ content = ensureLineAfter(
433
+ content,
434
+ "import { PrismaService } from '@forgeon/db-prisma';",
435
+ "import { I18nService } from 'nestjs-i18n';",
436
+ );
437
+ } else {
438
+ content = ensureLineAfter(
439
+ content,
440
+ "import { BadRequestException, ConflictException, Controller, Get, Post, Query } from '@nestjs/common';",
441
+ "import { I18nService } from 'nestjs-i18n';",
442
+ );
443
+ }
444
+ }
445
+
446
+ if (!content.includes('private readonly i18n: I18nService')) {
447
+ const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
448
+ if (constructorMatch) {
449
+ const original = constructorMatch[0];
450
+ const inner = constructorMatch[1].trimEnd();
451
+ const separator = inner.length > 0 ? ',' : '';
452
+ const next = `constructor(${inner}${separator}
453
+ private readonly i18n: I18nService,
454
+ ) {`;
455
+ content = content.replace(original, next);
456
+ }
457
+ }
458
+
459
+ if (!content.includes('private translate(')) {
460
+ const translateMethod = `
461
+ private translate(key: string, lang?: string): string {
462
+ const value = this.i18n.t(key, { lang, defaultValue: key });
463
+ return typeof value === 'string' ? value : key;
464
+ }
465
+ `;
466
+ content = `${content.trimEnd()}\n${translateMethod}\n`;
467
+ }
468
+
469
+ content = content.replace(
470
+ /getHealth\(@Query\('lang'\)\s*_lang\?:\s*string\)/g,
471
+ "getHealth(@Query('lang') lang?: string)",
472
+ );
473
+ content = content.replace(
474
+ /getErrorProbe\(\)/g,
475
+ "getErrorProbe(@Query('lang') lang?: string)",
476
+ );
477
+ content = content.replace(
478
+ /getValidationProbe\(@Query\('value'\)\s*value\?:\s*string\)/g,
479
+ "getValidationProbe(@Query('value') value?: string, @Query('lang') lang?: string)",
480
+ );
481
+ content = content.replace(/message:\s*'OK',/g, "message: this.translate('common.actions.ok', lang),");
482
+ content = content.replace(/i18n:\s*'English',/g, "i18n: 'en',");
483
+
484
+ content = content.replace(
485
+ /message:\s*'Email already exists',/g,
486
+ "message: this.translate('errors.http.CONFLICT', lang),",
487
+ );
488
+
489
+ if (
490
+ content.includes("const translatedMessage = this.translate('validation.generic.required', lang);") === false &&
491
+ content.includes("if (!value || value.trim().length === 0) {")
492
+ ) {
493
+ content = content.replace(
494
+ /if \(!value \|\| value\.trim\(\)\.length === 0\) \{\s*throw new BadRequestException\(\{\s*message:\s*'Field is required',\s*details:\s*\[\{ field: 'value', message: 'Field is required' \}\],\s*\}\);\s*\}/m,
495
+ `if (!value || value.trim().length === 0) {
496
+ const translatedMessage = this.translate('validation.generic.required', lang);
497
+ throw new BadRequestException({
498
+ message: translatedMessage,
499
+ details: [{ field: 'value', message: translatedMessage }],
500
+ });
501
+ }`,
502
+ );
503
+ }
504
+
505
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
506
+ }
507
+
281
508
  function patchI18nPackage(targetRoot) {
282
509
  const packagePath = path.join(targetRoot, 'packages', 'i18n', 'package.json');
283
510
  if (!fs.existsSync(packagePath)) {
@@ -327,17 +554,12 @@ export function applyI18nModule({ packageRoot, targetRoot }) {
327
554
  copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'i18n.ts'));
328
555
  copyFromPreset(packageRoot, targetRoot, path.join('apps', 'web', 'src', 'main.tsx'));
329
556
 
330
- copyFromBase(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'app.module.ts'));
331
- copyFromBase(
332
- packageRoot,
333
- targetRoot,
334
- path.join('apps', 'api', 'src', 'health', 'health.controller.ts'),
335
- );
336
-
337
557
  patchI18nPackage(targetRoot);
338
558
  patchApiPackage(targetRoot);
339
559
  patchWebPackage(targetRoot);
340
560
  patchRootPackage(targetRoot);
561
+ patchAppModule(targetRoot);
562
+ patchHealthController(targetRoot);
341
563
  patchApiDockerfile(targetRoot);
342
564
  patchProxyDockerfiles(targetRoot);
343
565