create-forgeon 0.3.15 → 0.3.17

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 (129) hide show
  1. package/package.json +4 -2
  2. package/src/cli/add-options.test.mjs +5 -2
  3. package/src/cli/options.test.mjs +1 -0
  4. package/src/cli/prompt-select.test.mjs +1 -0
  5. package/src/core/docs.test.mjs +80 -40
  6. package/src/core/scaffold.test.mjs +100 -0
  7. package/src/core/validate.test.mjs +1 -0
  8. package/src/modules/accounts.mjs +416 -0
  9. package/src/modules/db-prisma.mjs +23 -55
  10. package/src/modules/dependencies.test.mjs +71 -29
  11. package/src/modules/executor.mjs +3 -2
  12. package/src/modules/executor.test.mjs +631 -500
  13. package/src/modules/files-access.mjs +36 -105
  14. package/src/modules/files-image.mjs +35 -107
  15. package/src/modules/files-local.mjs +15 -6
  16. package/src/modules/files-quotas.mjs +75 -93
  17. package/src/modules/files-s3.mjs +17 -6
  18. package/src/modules/files.mjs +56 -125
  19. package/src/modules/i18n.mjs +17 -121
  20. package/src/modules/idempotency.test.mjs +180 -0
  21. package/src/modules/logger.mjs +0 -9
  22. package/src/modules/probes.test.mjs +204 -0
  23. package/src/modules/queue.mjs +325 -440
  24. package/src/modules/rate-limit.mjs +36 -76
  25. package/src/modules/rbac.mjs +39 -78
  26. package/src/modules/registry.mjs +22 -35
  27. package/src/modules/scheduler.mjs +51 -171
  28. package/src/modules/shared/files-runtime-wiring.mjs +81 -0
  29. package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
  30. package/src/modules/shared/patch-utils.mjs +29 -1
  31. package/src/modules/shared/probes.mjs +235 -0
  32. package/src/modules/sync-integrations.mjs +109 -396
  33. package/src/modules/sync-integrations.test.mjs +141 -0
  34. package/src/run-add-module.test.mjs +154 -0
  35. package/templates/base/README.md +7 -55
  36. package/templates/base/apps/web/src/App.tsx +70 -42
  37. package/templates/base/apps/web/src/probes.ts +61 -0
  38. package/templates/base/apps/web/src/styles.css +86 -25
  39. package/templates/base/package.json +21 -15
  40. package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
  41. package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
  42. package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
  43. package/templates/module-fragments/accounts/20_scope.md +29 -0
  44. package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
  45. package/templates/module-fragments/accounts/90_status_planned.md +7 -0
  46. package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
  47. package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
  48. package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
  49. package/templates/module-fragments/swagger/20_scope.md +2 -1
  50. package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
  51. package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
  52. package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
  53. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
  54. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
  55. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
  56. package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
  57. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
  58. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
  59. package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
  60. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
  61. package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
  62. package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
  63. package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
  64. package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
  65. package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
  66. package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
  67. package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
  68. package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
  69. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
  70. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
  71. package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
  72. package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
  73. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
  74. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
  75. package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
  76. package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
  77. package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
  78. package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
  79. package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
  80. package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
  81. package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
  82. package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
  83. package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
  84. package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
  85. package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
  86. package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
  87. package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
  88. package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
  89. package/templates/module-presets/files/packages/files/package.json +1 -2
  90. package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
  91. package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
  92. package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
  93. package/templates/module-presets/files/packages/files/src/index.ts +2 -1
  94. package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
  95. package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
  96. package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
  97. package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
  98. package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
  99. package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
  100. package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
  101. package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
  102. package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
  103. package/src/modules/jwt-auth.mjs +0 -390
  104. package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
  105. package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
  106. package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
  107. package/templates/base/docs/AI/PROJECT.md +0 -43
  108. package/templates/base/docs/AI/ROADMAP.md +0 -171
  109. package/templates/base/docs/AI/TASKS.md +0 -60
  110. package/templates/base/docs/AI/VALIDATION.md +0 -31
  111. package/templates/base/docs/README.md +0 -18
  112. package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
  113. package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
  114. package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
  115. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
  116. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
  117. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
  118. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
  119. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
  120. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
  121. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
  122. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
  123. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
  124. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
  125. package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
  126. /package/templates/module-presets/{jwt-auth/packages/auth-api/src/jwt-auth.guard.ts → accounts/packages/accounts-api/src/access-token.guard.ts} +0 -0
  127. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
  128. /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
  129. /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
@@ -1,17 +1,15 @@
1
- import fs from 'node:fs';
1
+ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { copyRecursive, writeJson } from '../utils/fs.mjs';
4
4
  import {
5
5
  ensureBuildSteps,
6
- ensureClassMember,
7
6
  ensureDependency,
8
- ensureImportLine,
9
7
  ensureLineAfter,
10
8
  ensureLineBefore,
11
- ensureLoadItem,
12
- ensureValidatorSchema,
13
9
  upsertEnvLines,
14
10
  } from './shared/patch-utils.mjs';
11
+ import { patchAppModuleRegistration, patchHealthControllerServiceProbe } from './shared/nest-runtime-wiring.mjs';
12
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
15
13
 
16
14
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
17
15
  const source = path.join(packageRoot, 'templates', 'module-presets', 'scheduler', relativePath);
@@ -35,169 +33,47 @@ function patchApiPackage(targetRoot) {
35
33
  }
36
34
 
37
35
  function patchAppModule(targetRoot) {
38
- const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
39
- if (!fs.existsSync(filePath)) {
40
- return;
41
- }
42
-
43
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
44
- content = ensureImportLine(
45
- content,
46
- "import { ForgeonSchedulerModule, schedulerConfig, schedulerEnvSchema } from '@forgeon/scheduler';",
47
- );
48
- content = ensureLoadItem(content, 'schedulerConfig');
49
- content = ensureValidatorSchema(content, 'schedulerEnvSchema');
50
-
51
- if (!content.includes(' ForgeonSchedulerModule,')) {
52
- if (content.includes(' ForgeonQueueModule,')) {
53
- content = ensureLineAfter(content, ' ForgeonQueueModule,', ' ForgeonSchedulerModule,');
54
- } else if (content.includes(' ForgeonI18nModule.register({')) {
55
- content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonSchedulerModule,');
56
- } else if (content.includes(' ForgeonAuthModule.register({')) {
57
- content = ensureLineBefore(content, ' ForgeonAuthModule.register({', ' ForgeonSchedulerModule,');
58
- } else if (content.includes(' ForgeonAuthModule.register(),')) {
59
- content = ensureLineBefore(content, ' ForgeonAuthModule.register(),', ' ForgeonSchedulerModule,');
60
- } else if (content.includes(' DbPrismaModule,')) {
61
- content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonSchedulerModule,');
62
- } else if (content.includes(' ForgeonLoggerModule,')) {
63
- content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonSchedulerModule,');
64
- } else if (content.includes(' ForgeonSwaggerModule,')) {
65
- content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonSchedulerModule,');
66
- } else {
67
- content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonSchedulerModule,');
68
- }
69
- }
70
-
71
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
36
+ patchAppModuleRegistration(targetRoot, {
37
+ importLine: "import { ForgeonSchedulerModule, schedulerConfig, schedulerEnvSchema } from '@forgeon/scheduler';",
38
+ loadItem: 'schedulerConfig',
39
+ envSchema: 'schedulerEnvSchema',
40
+ moduleLine: ' ForgeonSchedulerModule,',
41
+ beforeAnchors: [
42
+ ' ForgeonI18nModule.register({',
43
+ ' ForgeonAccountsModule.register({',
44
+ ' ForgeonAccountsModule.register(),',
45
+ ],
46
+ afterAnchors: [
47
+ ' ForgeonQueueModule,',
48
+ ' DbPrismaModule,',
49
+ ' ForgeonLoggerModule,',
50
+ ' ForgeonSwaggerModule,',
51
+ ],
52
+ });
72
53
  }
73
54
 
74
- function patchHealthController(targetRoot) {
75
- const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
76
- if (!fs.existsSync(filePath)) {
77
- return;
78
- }
79
-
80
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
81
- content = ensureImportLine(content, "import { ForgeonSchedulerService } from '@forgeon/scheduler';");
82
-
83
- if (!content.includes('private readonly schedulerService: ForgeonSchedulerService')) {
84
- const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
85
- if (constructorMatch) {
86
- const original = constructorMatch[0];
87
- const inner = constructorMatch[1].trimEnd();
88
- const normalizedInner = inner.replace(/,\s*$/, '');
89
- const separator = normalizedInner.length > 0 ? ',' : '';
90
- const next = `constructor(${normalizedInner}${separator}
91
- private readonly schedulerService: ForgeonSchedulerService,
92
- ) {`;
93
- content = content.replace(original, next);
94
- } else {
95
- const classAnchor = 'export class HealthController {';
96
- if (content.includes(classAnchor)) {
97
- content = content.replace(
98
- classAnchor,
99
- `${classAnchor}
100
- constructor(private readonly schedulerService: ForgeonSchedulerService) {}
101
- `,
102
- );
103
- }
104
- }
105
- }
106
-
107
- if (!content.includes("@Get('scheduler')")) {
108
- const method = `
109
- @Get('scheduler')
110
- async getSchedulerProbe() {
111
- return this.schedulerService.getProbeStatus();
112
- }
113
- `;
114
- content = ensureClassMember(content, 'HealthController', method, { beforeNeedle: 'private translate(' });
115
- }
116
-
117
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
55
+ function patchHealthController(targetRoot, probeTargets) {
56
+ patchHealthControllerServiceProbe(targetRoot, probeTargets, {
57
+ importLine: "import { ForgeonSchedulerService } from '@forgeon/scheduler';",
58
+ constructorMember: 'private readonly schedulerService: ForgeonSchedulerService',
59
+ routePath: 'scheduler',
60
+ methodName: 'getSchedulerProbe',
61
+ serviceCall: 'this.schedulerService.getProbeStatus()',
62
+ });
118
63
  }
119
64
 
120
- function patchWebApp(targetRoot) {
121
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
122
- if (!fs.existsSync(filePath)) {
123
- return;
124
- }
125
-
126
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
127
- content = content
128
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
129
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
130
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
131
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
132
-
133
- if (!content.includes('schedulerProbeResult')) {
134
- const stateAnchors = [
135
- ' const [queueProbeResult, setQueueProbeResult] = useState<ProbeResult | null>(null);',
136
- ' const [filesImageProbeResult, setFilesImageProbeResult] = useState<ProbeResult | null>(null);',
137
- ' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
138
- ' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
139
- ' const [filesVariantsProbeResult, setFilesVariantsProbeResult] = useState<ProbeResult | null>(null);',
140
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
141
- ' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
142
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
143
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
144
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
145
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
146
- ];
147
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
148
- if (stateAnchor) {
149
- content = ensureLineAfter(
150
- content,
151
- stateAnchor,
152
- ' const [schedulerProbeResult, setSchedulerProbeResult] = useState<ProbeResult | null>(null);',
153
- );
154
- }
155
- }
156
-
157
- if (!content.includes('Check scheduler health')) {
158
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
159
- ? '/health/scheduler'
160
- : '/api/health/scheduler';
161
- const button = ` <button onClick={() => runProbe(setSchedulerProbeResult, '${probePath}')}>
162
- Check scheduler health
163
- </button>`;
164
-
165
- const actionsStart = content.indexOf('<div className="actions">');
166
- if (actionsStart >= 0) {
167
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
168
- if (actionsEnd >= 0) {
169
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
170
- }
171
- }
172
- }
173
-
174
- if (!content.includes("{renderResult('Scheduler probe response', schedulerProbeResult)}")) {
175
- const resultLine = " {renderResult('Scheduler probe response', schedulerProbeResult)}";
176
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
177
- if (content.includes(networkLine)) {
178
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
179
- } else {
180
- const anchors = [
181
- "{renderResult('Queue probe response', queueProbeResult)}",
182
- "{renderResult('Files image probe response', filesImageProbeResult)}",
183
- "{renderResult('Files quotas probe response', filesQuotasProbeResult)}",
184
- "{renderResult('Files access probe response', filesAccessProbeResult)}",
185
- "{renderResult('Files variants probe response', filesVariantsProbeResult)}",
186
- "{renderResult('Files probe response', filesProbeResult)}",
187
- "{renderResult('RBAC probe response', rbacProbeResult)}",
188
- "{renderResult('Rate limit probe response', rateLimitProbeResult)}",
189
- "{renderResult('Auth probe response', authProbeResult)}",
190
- "{renderResult('DB probe response', dbProbeResult)}",
191
- "{renderResult('Validation probe response', validationProbeResult)}",
192
- ];
193
- const anchor = anchors.find((line) => content.includes(line));
194
- if (anchor) {
195
- content = ensureLineAfter(content, anchor, resultLine);
196
- }
197
- }
198
- }
199
-
200
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
65
+ function registerWebProbe(targetRoot, probeTargets) {
66
+ ensureWebProbeDefinition({
67
+ targetRoot,
68
+ probeTargets,
69
+ definition: {
70
+ id: 'scheduler',
71
+ title: 'Scheduler',
72
+ buttonLabel: 'Check scheduler health',
73
+ resultTitle: 'Scheduler probe response',
74
+ path: '/health/scheduler',
75
+ },
76
+ });
201
77
  }
202
78
 
203
79
  function patchApiDockerfile(targetRoot) {
@@ -216,7 +92,7 @@ function patchApiDockerfile(targetRoot) {
216
92
  'COPY packages/files-s3/package.json packages/files-s3/package.json',
217
93
  'COPY packages/files-local/package.json packages/files-local/package.json',
218
94
  'COPY packages/files/package.json packages/files/package.json',
219
- 'COPY packages/auth-api/package.json packages/auth-api/package.json',
95
+ 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
220
96
  'COPY packages/rbac/package.json packages/rbac/package.json',
221
97
  'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
222
98
  'COPY packages/logger/package.json packages/logger/package.json',
@@ -237,7 +113,7 @@ function patchApiDockerfile(targetRoot) {
237
113
  'COPY packages/files-s3 packages/files-s3',
238
114
  'COPY packages/files-local packages/files-local',
239
115
  'COPY packages/files packages/files',
240
- 'COPY packages/auth-api packages/auth-api',
116
+ 'COPY packages/accounts-api packages/accounts-api',
241
117
  'COPY packages/rbac packages/rbac',
242
118
  'COPY packages/rate-limit packages/rate-limit',
243
119
  'COPY packages/logger packages/logger',
@@ -279,7 +155,7 @@ function patchCompose(targetRoot) {
279
155
  /^(\s+FILES_S3_MAX_ATTEMPTS:.*)$/m,
280
156
  /^(\s+FILES_LOCAL_ROOT:.*)$/m,
281
157
  /^(\s+FILES_PUBLIC_BASE_PATH:.*)$/m,
282
- /^(\s+AUTH_DEMO_PASSWORD:.*)$/m,
158
+ /^(\s+AUTH_ARGON2_PARALLELISM:.*)$/m,
283
159
  /^(\s+THROTTLE_TRUST_PROXY:.*)$/m,
284
160
  /^(\s+LOGGER_REQUEST_ID_HEADER:.*)$/m,
285
161
  /^(\s+SWAGGER_DOCS_PATH:.*)$/m,
@@ -346,11 +222,12 @@ Operational notes:
346
222
 
347
223
  export function applySchedulerModule({ packageRoot, targetRoot }) {
348
224
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'scheduler'));
349
-
225
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'scheduler' });
226
+
350
227
  patchApiPackage(targetRoot);
351
228
  patchAppModule(targetRoot);
352
- patchHealthController(targetRoot);
353
- patchWebApp(targetRoot);
229
+ patchHealthController(targetRoot, probeTargets);
230
+ registerWebProbe(targetRoot, probeTargets);
354
231
  patchApiDockerfile(targetRoot);
355
232
  patchCompose(targetRoot);
356
233
  patchReadme(targetRoot);
@@ -365,4 +242,7 @@ export function applySchedulerModule({ packageRoot, targetRoot }) {
365
242
  'SCHEDULER_TIMEZONE=UTC',
366
243
  'SCHEDULER_HEARTBEAT_CRON=*/5 * * * *',
367
244
  ]);
368
- }
245
+ }
246
+
247
+
248
+
@@ -0,0 +1,81 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ensureLineAfter, ensureLineBefore } from './patch-utils.mjs';
4
+
5
+ function normalize(content) {
6
+ return content.replace(/\r\n/g, '\n');
7
+ }
8
+
9
+ export function readPreferredFilesStorageDriver(targetRoot) {
10
+ const envPath = path.join(targetRoot, 'apps', 'api', '.env.example');
11
+ if (!fs.existsSync(envPath)) {
12
+ return 'local';
13
+ }
14
+
15
+ const content = normalize(fs.readFileSync(envPath, 'utf8'));
16
+ const match = content.match(/^FILES_STORAGE_DRIVER=(local|s3)$/m);
17
+ return match?.[1] ?? 'local';
18
+ }
19
+
20
+ export function resolveFilesStorageRuntimeModule(targetRoot) {
21
+ const hasLocal = fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json'));
22
+ const hasS3 = fs.existsSync(path.join(targetRoot, 'packages', 'files-s3', 'package.json'));
23
+ const preferredDriver = readPreferredFilesStorageDriver(targetRoot);
24
+
25
+ if (preferredDriver === 's3' && hasS3) {
26
+ return 'ForgeonFilesS3StorageModule';
27
+ }
28
+ if (preferredDriver === 'local' && hasLocal) {
29
+ return 'ForgeonFilesLocalStorageModule';
30
+ }
31
+ if (hasLocal) {
32
+ return 'ForgeonFilesLocalStorageModule';
33
+ }
34
+ if (hasS3) {
35
+ return 'ForgeonFilesS3StorageModule';
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export function upsertFilesModuleRegistration(content, storageRuntimeModuleName = null) {
41
+ const runtimeImports = ['ForgeonFilesDbPrismaModule'];
42
+ if (storageRuntimeModuleName) {
43
+ runtimeImports.push(storageRuntimeModuleName);
44
+ }
45
+
46
+ const moduleBlock = ` ForgeonFilesModule.register({
47
+ imports: [${runtimeImports.join(', ')}],
48
+ }),`;
49
+
50
+ if (content.includes('ForgeonFilesModule.register({')) {
51
+ return content.replace(
52
+ / {4}ForgeonFilesModule\.register\(\{[\s\S]*? {4}\}\),/m,
53
+ moduleBlock,
54
+ );
55
+ }
56
+
57
+ if (content.includes(' ForgeonFilesModule,')) {
58
+ return content.replace(' ForgeonFilesModule,', moduleBlock);
59
+ }
60
+
61
+ if (content.includes(' ForgeonI18nModule.register({')) {
62
+ return ensureLineBefore(content, ' ForgeonI18nModule.register({', moduleBlock);
63
+ }
64
+ if (content.includes(' ForgeonAccountsModule.register({')) {
65
+ return ensureLineBefore(content, ' ForgeonAccountsModule.register({', moduleBlock);
66
+ }
67
+ if (content.includes(' ForgeonAccountsModule.register(),')) {
68
+ return ensureLineBefore(content, ' ForgeonAccountsModule.register(),', moduleBlock);
69
+ }
70
+ if (content.includes(' DbPrismaModule,')) {
71
+ return ensureLineAfter(content, ' DbPrismaModule,', moduleBlock);
72
+ }
73
+ if (content.includes(' ForgeonLoggerModule,')) {
74
+ return ensureLineAfter(content, ' ForgeonLoggerModule,', moduleBlock);
75
+ }
76
+ if (content.includes(' ForgeonSwaggerModule,')) {
77
+ return ensureLineAfter(content, ' ForgeonSwaggerModule,', moduleBlock);
78
+ }
79
+ return ensureLineAfter(content, ' CoreErrorsModule,', moduleBlock);
80
+ }
81
+
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ ensureClassMember,
5
+ ensureImportLine,
6
+ ensureLineAfter,
7
+ ensureLineBefore,
8
+ ensureLoadItem,
9
+ ensureValidatorSchema,
10
+ } from './patch-utils.mjs';
11
+
12
+ function normalize(content) {
13
+ return content.replace(/\r\n/g, '\n');
14
+ }
15
+
16
+ export function patchAppModuleRegistration(targetRoot, options) {
17
+ const {
18
+ importLine,
19
+ loadItem,
20
+ envSchema,
21
+ moduleLine,
22
+ afterAnchors = [],
23
+ beforeAnchors = [],
24
+ fallbackAnchor = ' CoreErrorsModule,',
25
+ } = options;
26
+
27
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'app.module.ts');
28
+ if (!fs.existsSync(filePath)) {
29
+ return;
30
+ }
31
+
32
+ let content = normalize(fs.readFileSync(filePath, 'utf8'));
33
+ content = ensureImportLine(content, importLine);
34
+ content = ensureLoadItem(content, loadItem);
35
+ content = ensureValidatorSchema(content, envSchema);
36
+
37
+ if (!content.includes(moduleLine)) {
38
+ const beforeAnchor = beforeAnchors.find((anchor) => content.includes(anchor));
39
+ if (beforeAnchor) {
40
+ content = ensureLineBefore(content, beforeAnchor, moduleLine);
41
+ } else {
42
+ const afterAnchor = afterAnchors.find((anchor) => content.includes(anchor)) ?? fallbackAnchor;
43
+ content = ensureLineAfter(content, afterAnchor, moduleLine);
44
+ }
45
+ }
46
+
47
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
48
+ }
49
+
50
+ export function patchHealthControllerServiceProbe(targetRoot, probeTargets, options) {
51
+ if (!probeTargets.allowApi) {
52
+ return;
53
+ }
54
+
55
+ const {
56
+ importLine,
57
+ constructorMember,
58
+ routePath,
59
+ methodName,
60
+ serviceCall,
61
+ className = 'HealthController',
62
+ classAnchor = 'export class HealthController {',
63
+ beforeNeedles = [],
64
+ beforeNeedle = 'private translate(',
65
+ } = options;
66
+
67
+ const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
68
+ if (!fs.existsSync(filePath)) {
69
+ return;
70
+ }
71
+
72
+ let content = normalize(fs.readFileSync(filePath, 'utf8'));
73
+ content = ensureImportLine(content, importLine);
74
+
75
+ if (!content.includes(constructorMember)) {
76
+ const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
77
+ if (constructorMatch) {
78
+ const original = constructorMatch[0];
79
+ const inner = constructorMatch[1].trimEnd();
80
+ const normalizedInner = inner.replace(/,\s*$/, '');
81
+ const separator = normalizedInner.length > 0 ? ',' : '';
82
+ const next = `constructor(${normalizedInner}${separator}
83
+ ${constructorMember},
84
+ ) {`;
85
+ content = content.replace(original, next);
86
+ } else if (content.includes(classAnchor)) {
87
+ content = content.replace(
88
+ classAnchor,
89
+ `${classAnchor}
90
+ constructor(${constructorMember}) {}
91
+ `,
92
+ );
93
+ }
94
+ }
95
+
96
+ const routeDecorator = `@Get('${routePath}')`;
97
+ if (!content.includes(routeDecorator)) {
98
+ const method = `
99
+ ${routeDecorator}
100
+ async ${methodName}() {
101
+ return ${serviceCall};
102
+ }
103
+ `;
104
+ const resolvedBeforeNeedle =
105
+ beforeNeedles.find((needle) => content.includes(needle)) ?? beforeNeedle;
106
+ content = ensureClassMember(content, className, method, { beforeNeedle: resolvedBeforeNeedle });
107
+ }
108
+
109
+ fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
110
+ }
@@ -113,6 +113,34 @@ export function ensureImportLine(content, importLine) {
113
113
  return `${content.slice(0, insertAt)}\n${importLine}${content.slice(insertAt)}`;
114
114
  }
115
115
 
116
+ function escapeRegExp(value) {
117
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
118
+ }
119
+
120
+ export function ensureNamedImportSpecifier(content, moduleSpecifier, importName) {
121
+ const pattern = new RegExp(
122
+ `import\\s*\\{([^}]*)\\}\\s*from ['"]${escapeRegExp(moduleSpecifier)}['"];`,
123
+ 'm',
124
+ );
125
+ const match = content.match(pattern);
126
+ if (!match) {
127
+ return ensureImportLine(content, `import { ${importName} } from '${moduleSpecifier}';`);
128
+ }
129
+
130
+ const names = match[1]
131
+ .split(',')
132
+ .map((item) => item.trim())
133
+ .filter(Boolean);
134
+
135
+ if (names.includes(importName)) {
136
+ return content;
137
+ }
138
+
139
+ names.push(importName);
140
+ const replacement = `import { ${names.join(', ')} } from '${moduleSpecifier}';`;
141
+ return content.replace(pattern, replacement);
142
+ }
143
+
116
144
  function findClassRange(content, className) {
117
145
  const classPattern = new RegExp(`export\\s+class\\s+${className}\\b`);
118
146
  const classMatch = classPattern.exec(content);
@@ -259,4 +287,4 @@ export function ensureNestCommonImport(content, importName) {
259
287
 
260
288
  const replacement = `import { ${names.join(', ')} } from '@nestjs/common';`;
261
289
  return content.replace(pattern, replacement);
262
- }
290
+ }