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,4 +1,4 @@
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 {
@@ -14,6 +14,7 @@ import {
14
14
  ensureValidatorSchema,
15
15
  upsertEnvLines,
16
16
  } from './shared/patch-utils.mjs';
17
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
17
18
 
18
19
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
19
20
  const source = path.join(packageRoot, 'templates', 'module-presets', 'files-quotas', relativePath);
@@ -49,7 +50,7 @@ function patchFilesPackage(targetRoot) {
49
50
  }
50
51
 
51
52
  const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
52
- ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
53
+ ensureDependency(packageJson, '@nestjs/core', '^11.0.1');
53
54
  writeJson(packagePath, packageJson);
54
55
  }
55
56
 
@@ -74,10 +75,10 @@ function patchAppModule(targetRoot) {
74
75
  content = ensureLineAfter(content, ' ForgeonFilesModule,', ' ForgeonFilesQuotasModule,');
75
76
  } else if (content.includes(' ForgeonI18nModule.register({')) {
76
77
  content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonFilesQuotasModule,');
77
- } else if (content.includes(' ForgeonAuthModule.register({')) {
78
- content = ensureLineBefore(content, ' ForgeonAuthModule.register({', ' ForgeonFilesQuotasModule,');
79
- } else if (content.includes(' ForgeonAuthModule.register(),')) {
80
- content = ensureLineBefore(content, ' ForgeonAuthModule.register(),', ' ForgeonFilesQuotasModule,');
78
+ } else if (content.includes(' ForgeonAccountsModule.register({')) {
79
+ content = ensureLineBefore(content, ' ForgeonAccountsModule.register({', ' ForgeonFilesQuotasModule,');
80
+ } else if (content.includes(' ForgeonAccountsModule.register(),')) {
81
+ content = ensureLineBefore(content, ' ForgeonAccountsModule.register(),', ' ForgeonFilesQuotasModule,');
81
82
  } else if (content.includes(' DbPrismaModule,')) {
82
83
  content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonFilesQuotasModule,');
83
84
  } else if (content.includes(' ForgeonLoggerModule,')) {
@@ -99,9 +100,24 @@ function patchFilesController(targetRoot) {
99
100
  }
100
101
 
101
102
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
102
- content = ensureImportLine(content, "import { FilesQuotasService } from '@forgeon/files-quotas';");
103
+ content = ensureImportLine(content, "import { ModuleRef } from '@nestjs/core';");
103
104
 
104
- if (!content.includes('private readonly filesQuotasService: FilesQuotasService')) {
105
+ if (!content.includes("const FORGEON_FILES_UPLOAD_QUOTA_SERVICE = 'FORGEON_FILES_UPLOAD_QUOTA_SERVICE';")) {
106
+ content = content.replace(
107
+ '};\n\n@Controller(\'files\')',
108
+ `};
109
+
110
+ const FORGEON_FILES_UPLOAD_QUOTA_SERVICE = 'FORGEON_FILES_UPLOAD_QUOTA_SERVICE';
111
+
112
+ type FilesUploadQuotaService = {
113
+ assertUploadAllowed(input: { ownerType: string; ownerId: string | null; fileSize: number }): Promise<void>;
114
+ };
115
+
116
+ @Controller('files')`,
117
+ );
118
+ }
119
+
120
+ if (!content.includes('private readonly moduleRef: ModuleRef')) {
105
121
  const constructorMatch = content.match(/constructor\(([\s\S]*?)\)\s*\{/m);
106
122
  if (constructorMatch) {
107
123
  const original = constructorMatch[0];
@@ -109,29 +125,51 @@ function patchFilesController(targetRoot) {
109
125
  const normalizedInner = inner.replace(/,\s*$/, '');
110
126
  const separator = normalizedInner.length > 0 ? ',' : '';
111
127
  const next = `constructor(${normalizedInner}${separator}
112
- private readonly filesQuotasService: FilesQuotasService,
128
+ private readonly moduleRef: ModuleRef,
113
129
  ) {`;
114
130
  content = content.replace(original, next);
115
131
  }
116
132
  }
117
133
 
118
- if (!content.includes('filesQuotasService.assertUploadAllowed')) {
134
+ if (!content.includes('const filesQuotasService = this.getFilesUploadQuotaService();')) {
119
135
  content = content.replace(
120
136
  ' return this.filesService.create({',
121
- ` await this.filesQuotasService.assertUploadAllowed({
122
- ownerType: body.ownerType ?? 'system',
123
- ownerId: body.ownerId ?? null,
124
- fileSize: file.size,
125
- });
137
+ ` const filesQuotasService = this.getFilesUploadQuotaService();
138
+ if (filesQuotasService) {
139
+ await filesQuotasService.assertUploadAllowed({
140
+ ownerType: body.ownerType ?? 'system',
141
+ ownerId: body.ownerId ?? null,
142
+ fileSize: file.size,
143
+ });
144
+ }
126
145
 
127
146
  return this.filesService.create({`,
128
147
  );
129
148
  }
130
149
 
150
+ if (!content.includes('private getFilesUploadQuotaService(): FilesUploadQuotaService | null {')) {
151
+ content = content.replace(
152
+ ' private parseVariant(variantQuery?: string): FileVariantKey {',
153
+ ` private getFilesUploadQuotaService(): FilesUploadQuotaService | null {
154
+ try {
155
+ return this.moduleRef.get(FORGEON_FILES_UPLOAD_QUOTA_SERVICE, { strict: false });
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ private parseVariant(variantQuery?: string): FileVariantKey {`,
162
+ );
163
+ }
164
+
131
165
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
132
166
  }
133
167
 
134
- function patchHealthController(targetRoot) {
168
+ function patchHealthController(targetRoot, probeTargets) {
169
+ if (!probeTargets.allowApi) {
170
+ return;
171
+ }
172
+
135
173
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
136
174
  if (!fs.existsSync(filePath)) {
137
175
  return;
@@ -177,79 +215,18 @@ function patchHealthController(targetRoot) {
177
215
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
178
216
  }
179
217
 
180
- function patchWebApp(targetRoot) {
181
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
182
- if (!fs.existsSync(filePath)) {
183
- return;
184
- }
185
-
186
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
187
- content = content
188
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
189
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
190
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
191
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
192
-
193
- if (!content.includes('filesQuotasProbeResult')) {
194
- const stateAnchors = [
195
- ' const [filesAccessProbeResult, setFilesAccessProbeResult] = useState<ProbeResult | null>(null);',
196
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
197
- ' const [rbacProbeResult, setRbacProbeResult] = useState<ProbeResult | null>(null);',
198
- ' const [rateLimitProbeResult, setRateLimitProbeResult] = useState<ProbeResult | null>(null);',
199
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
200
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
201
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
202
- ];
203
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
204
- if (stateAnchor) {
205
- content = ensureLineAfter(
206
- content,
207
- stateAnchor,
208
- ' const [filesQuotasProbeResult, setFilesQuotasProbeResult] = useState<ProbeResult | null>(null);',
209
- );
210
- }
211
- }
212
-
213
- if (!content.includes('Check files quotas')) {
214
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
215
- ? '/health/files-quotas'
216
- : '/api/health/files-quotas';
217
- const button = ` <button onClick={() => runProbe(setFilesQuotasProbeResult, '${probePath}')}>
218
- Check files quotas
219
- </button>`;
220
-
221
- const actionsStart = content.indexOf('<div className="actions">');
222
- if (actionsStart >= 0) {
223
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
224
- if (actionsEnd >= 0) {
225
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
226
- }
227
- }
228
- }
229
-
230
- if (!content.includes("{renderResult('Files quotas probe response', filesQuotasProbeResult)}")) {
231
- const resultLine = " {renderResult('Files quotas probe response', filesQuotasProbeResult)}";
232
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
233
- if (content.includes(networkLine)) {
234
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
235
- } else {
236
- const anchors = [
237
- " {renderResult('Files access probe response', filesAccessProbeResult)}",
238
- " {renderResult('Files probe response', filesProbeResult)}",
239
- " {renderResult('RBAC probe response', rbacProbeResult)}",
240
- " {renderResult('Rate limit probe response', rateLimitProbeResult)}",
241
- " {renderResult('Auth probe response', authProbeResult)}",
242
- " {renderResult('DB probe response', dbProbeResult)}",
243
- " {renderResult('Validation probe response', validationProbeResult)}",
244
- ];
245
- const anchor = anchors.find((line) => content.includes(line));
246
- if (anchor) {
247
- content = ensureLineAfter(content, anchor, resultLine);
248
- }
249
- }
250
- }
251
-
252
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
218
+ function registerWebProbe(targetRoot, probeTargets) {
219
+ ensureWebProbeDefinition({
220
+ targetRoot,
221
+ probeTargets,
222
+ definition: {
223
+ id: 'files-quotas',
224
+ title: 'Files Quotas',
225
+ buttonLabel: 'Check files quotas',
226
+ resultTitle: 'Files quotas probe response',
227
+ path: '/health/files-quotas',
228
+ },
229
+ });
253
230
  }
254
231
 
255
232
  function patchApiDockerfile(targetRoot) {
@@ -264,7 +241,7 @@ function patchApiDockerfile(targetRoot) {
264
241
  'COPY packages/files/package.json packages/files/package.json',
265
242
  'COPY packages/files-local/package.json packages/files-local/package.json',
266
243
  'COPY packages/files-s3/package.json packages/files-s3/package.json',
267
- 'COPY packages/auth-api/package.json packages/auth-api/package.json',
244
+ 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
268
245
  'COPY packages/rbac/package.json packages/rbac/package.json',
269
246
  'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
270
247
  'COPY packages/logger/package.json packages/logger/package.json',
@@ -285,7 +262,7 @@ function patchApiDockerfile(targetRoot) {
285
262
  'COPY packages/files packages/files',
286
263
  'COPY packages/files-local packages/files-local',
287
264
  'COPY packages/files-s3 packages/files-s3',
288
- 'COPY packages/auth-api packages/auth-api',
265
+ 'COPY packages/accounts-api packages/accounts-api',
289
266
  'COPY packages/rbac packages/rbac',
290
267
  'COPY packages/rate-limit packages/rate-limit',
291
268
  'COPY packages/logger packages/logger',
@@ -379,12 +356,14 @@ Key env:
379
356
 
380
357
  export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
381
358
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
359
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files-quotas' });
360
+
382
361
  patchApiPackage(targetRoot);
383
362
  patchFilesPackage(targetRoot);
384
363
  patchAppModule(targetRoot);
385
364
  patchFilesController(targetRoot);
386
- patchHealthController(targetRoot);
387
- patchWebApp(targetRoot);
365
+ patchHealthController(targetRoot, probeTargets);
366
+ registerWebProbe(targetRoot, probeTargets);
388
367
  patchApiDockerfile(targetRoot);
389
368
  patchCompose(targetRoot);
390
369
  patchReadme(targetRoot);
@@ -400,3 +379,6 @@ export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
400
379
  'FILES_QUOTA_MAX_BYTES_PER_OWNER=104857600',
401
380
  ]);
402
381
  }
382
+
383
+
384
+
@@ -9,8 +9,10 @@ import {
9
9
  ensureLineBefore,
10
10
  ensureLoadItem,
11
11
  ensureValidatorSchema,
12
+ ensureNamedImportSpecifier,
12
13
  upsertEnvLines,
13
14
  } from './shared/patch-utils.mjs';
15
+ import { resolveFilesStorageRuntimeModule, upsertFilesModuleRegistration } from './shared/files-runtime-wiring.mjs';
14
16
 
15
17
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
16
18
  const source = path.join(packageRoot, 'templates', 'module-presets', 'files-s3', relativePath);
@@ -40,16 +42,18 @@ function patchAppModule(targetRoot) {
40
42
  }
41
43
 
42
44
  let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
43
- content = ensureImportLine(
44
- content,
45
- "import { filesS3Config, filesS3EnvSchemaZod, FilesS3ConfigModule } from '@forgeon/files-s3';",
46
- );
45
+ content = ensureNamedImportSpecifier(content, '@forgeon/files-s3', 'filesS3Config');
46
+ content = ensureNamedImportSpecifier(content, '@forgeon/files-s3', 'filesS3EnvSchemaZod');
47
+ content = ensureNamedImportSpecifier(content, '@forgeon/files-s3', 'FilesS3ConfigModule');
48
+ content = ensureNamedImportSpecifier(content, '@forgeon/files-s3', 'ForgeonFilesS3StorageModule');
47
49
  content = ensureLoadItem(content, 'filesS3Config');
48
50
  content = ensureValidatorSchema(content, 'filesS3EnvSchemaZod');
49
51
 
50
52
  if (!content.includes(' FilesS3ConfigModule,')) {
51
53
  if (content.includes(' ForgeonFilesModule,')) {
52
54
  content = ensureLineAfter(content, ' ForgeonFilesModule,', ' FilesS3ConfigModule,');
55
+ } else if (content.includes(' ForgeonFilesModule.register({')) {
56
+ content = ensureLineBefore(content, ' ForgeonFilesModule.register({', ' FilesS3ConfigModule,');
53
57
  } else if (content.includes(' ForgeonI18nModule.register({')) {
54
58
  content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' FilesS3ConfigModule,');
55
59
  } else if (content.includes(' DbPrismaModule,')) {
@@ -61,6 +65,10 @@ function patchAppModule(targetRoot) {
61
65
  }
62
66
  }
63
67
 
68
+ if (content.includes('ForgeonFilesModule')) {
69
+ content = upsertFilesModuleRegistration(content, resolveFilesStorageRuntimeModule(targetRoot));
70
+ }
71
+
64
72
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
65
73
  }
66
74
 
@@ -75,7 +83,7 @@ function patchApiDockerfile(targetRoot) {
75
83
  const packageAnchors = [
76
84
  'COPY packages/files/package.json packages/files/package.json',
77
85
  'COPY packages/files-local/package.json packages/files-local/package.json',
78
- 'COPY packages/auth-api/package.json packages/auth-api/package.json',
86
+ 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
79
87
  'COPY packages/rbac/package.json packages/rbac/package.json',
80
88
  'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
81
89
  'COPY packages/logger/package.json packages/logger/package.json',
@@ -94,7 +102,7 @@ function patchApiDockerfile(targetRoot) {
94
102
  const sourceAnchors = [
95
103
  'COPY packages/files packages/files',
96
104
  'COPY packages/files-local packages/files-local',
97
- 'COPY packages/auth-api packages/auth-api',
105
+ 'COPY packages/accounts-api packages/accounts-api',
98
106
  'COPY packages/rbac packages/rbac',
99
107
  'COPY packages/rate-limit packages/rate-limit',
100
108
  'COPY packages/logger packages/logger',
@@ -263,4 +271,7 @@ export function applyFilesS3Module({ packageRoot, targetRoot }) {
263
271
 
264
272
  setEnvValue(path.join(targetRoot, 'apps', 'api', '.env.example'), 'FILES_STORAGE_DRIVER', 's3');
265
273
  setEnvValue(path.join(targetRoot, 'infra', 'docker', '.env.example'), 'FILES_STORAGE_DRIVER', 's3');
274
+
275
+ patchAppModule(targetRoot);
266
276
  }
277
+
@@ -11,8 +11,11 @@ import {
11
11
  ensureLoadItem,
12
12
  ensureNestCommonImport,
13
13
  ensureValidatorSchema,
14
+ ensureNamedImportSpecifier,
14
15
  upsertEnvLines,
15
16
  } from './shared/patch-utils.mjs';
17
+ import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
18
+ import { resolveFilesStorageRuntimeModule, upsertFilesModuleRegistration } from './shared/files-runtime-wiring.mjs';
16
19
 
17
20
  function copyFromPreset(packageRoot, targetRoot, relativePath) {
18
21
  const source = path.join(packageRoot, 'templates', 'module-presets', 'files', relativePath);
@@ -167,27 +170,22 @@ function patchAppModule(targetRoot) {
167
170
  content,
168
171
  "import { filesConfig, filesEnvSchema, ForgeonFilesModule } from '@forgeon/files';",
169
172
  );
170
- content = ensureLoadItem(content, 'filesConfig');
171
- content = ensureValidatorSchema(content, 'filesEnvSchema');
173
+ content = ensureImportLine(
174
+ content,
175
+ "import { ForgeonFilesDbPrismaModule } from './files/forgeon-files-db-prisma.module';",
176
+ );
172
177
 
173
- if (!content.includes(' ForgeonFilesModule,')) {
174
- if (content.includes(' ForgeonI18nModule.register({')) {
175
- content = ensureLineBefore(content, ' ForgeonI18nModule.register({', ' ForgeonFilesModule,');
176
- } else if (content.includes(' ForgeonAuthModule.register({')) {
177
- content = ensureLineBefore(content, ' ForgeonAuthModule.register({', ' ForgeonFilesModule,');
178
- } else if (content.includes(' ForgeonAuthModule.register(),')) {
179
- content = ensureLineBefore(content, ' ForgeonAuthModule.register(),', ' ForgeonFilesModule,');
180
- } else if (content.includes(' DbPrismaModule,')) {
181
- content = ensureLineAfter(content, ' DbPrismaModule,', ' ForgeonFilesModule,');
182
- } else if (content.includes(' ForgeonLoggerModule,')) {
183
- content = ensureLineAfter(content, ' ForgeonLoggerModule,', ' ForgeonFilesModule,');
184
- } else if (content.includes(' ForgeonSwaggerModule,')) {
185
- content = ensureLineAfter(content, ' ForgeonSwaggerModule,', ' ForgeonFilesModule,');
186
- } else {
187
- content = ensureLineAfter(content, ' CoreErrorsModule,', ' ForgeonFilesModule,');
188
- }
178
+ if (fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json'))) {
179
+ content = ensureNamedImportSpecifier(content, '@forgeon/files-local', 'ForgeonFilesLocalStorageModule');
180
+ }
181
+ if (fs.existsSync(path.join(targetRoot, 'packages', 'files-s3', 'package.json'))) {
182
+ content = ensureNamedImportSpecifier(content, '@forgeon/files-s3', 'ForgeonFilesS3StorageModule');
189
183
  }
190
184
 
185
+ content = ensureLoadItem(content, 'filesConfig');
186
+ content = ensureValidatorSchema(content, 'filesEnvSchema');
187
+ content = upsertFilesModuleRegistration(content, resolveFilesStorageRuntimeModule(targetRoot));
188
+
191
189
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
192
190
  }
193
191
 
@@ -200,7 +198,7 @@ function patchApiDockerfile(targetRoot) {
200
198
  let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
201
199
 
202
200
  const packageAnchors = [
203
- 'COPY packages/auth-api/package.json packages/auth-api/package.json',
201
+ 'COPY packages/accounts-api/package.json packages/accounts-api/package.json',
204
202
  'COPY packages/rbac/package.json packages/rbac/package.json',
205
203
  'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
206
204
  'COPY packages/logger/package.json packages/logger/package.json',
@@ -213,7 +211,7 @@ function patchApiDockerfile(targetRoot) {
213
211
  content = ensureLineAfter(content, packageAnchor, 'COPY packages/files/package.json packages/files/package.json');
214
212
 
215
213
  const sourceAnchors = [
216
- 'COPY packages/auth-api packages/auth-api',
214
+ 'COPY packages/accounts-api packages/accounts-api',
217
215
  'COPY packages/rbac packages/rbac',
218
216
  'COPY packages/rate-limit packages/rate-limit',
219
217
  'COPY packages/logger packages/logger',
@@ -234,7 +232,11 @@ function patchApiDockerfile(targetRoot) {
234
232
  fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
235
233
  }
236
234
 
237
- function patchHealthController(targetRoot) {
235
+ function patchHealthController(targetRoot, probeTargets) {
236
+ if (!probeTargets.allowApi) {
237
+ return;
238
+ }
239
+
238
240
  const filePath = path.join(targetRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts');
239
241
  if (!fs.existsSync(filePath)) {
240
242
  return;
@@ -303,107 +305,31 @@ function patchHealthController(targetRoot) {
303
305
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
304
306
  }
305
307
 
306
- function patchWebApp(targetRoot) {
307
- const filePath = path.join(targetRoot, 'apps', 'web', 'src', 'App.tsx');
308
- if (!fs.existsSync(filePath)) {
309
- return;
310
- }
311
-
312
- let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
313
- content = content
314
- .replace(/^\s*\{\/\* forgeon:probes:actions:start \*\/\}\r?\n?/gm, '')
315
- .replace(/^\s*\{\/\* forgeon:probes:actions:end \*\/\}\r?\n?/gm, '')
316
- .replace(/^\s*\{\/\* forgeon:probes:results:start \*\/\}\r?\n?/gm, '')
317
- .replace(/^\s*\{\/\* forgeon:probes:results:end \*\/\}\r?\n?/gm, '');
318
-
319
- if (!content.includes('filesProbeResult')) {
320
- const stateAnchors = [
321
- ' const [dbProbeResult, setDbProbeResult] = useState<ProbeResult | null>(null);',
322
- ' const [authProbeResult, setAuthProbeResult] = useState<ProbeResult | null>(null);',
323
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
324
- ];
325
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
326
- if (stateAnchor) {
327
- content = ensureLineAfter(
328
- content,
329
- stateAnchor,
330
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
331
- );
332
- }
333
- }
334
-
335
- if (!content.includes('filesVariantsProbeResult')) {
336
- const stateAnchors = [
337
- ' const [filesProbeResult, setFilesProbeResult] = useState<ProbeResult | null>(null);',
338
- ' const [validationProbeResult, setValidationProbeResult] = useState<ProbeResult | null>(null);',
339
- ];
340
- const stateAnchor = stateAnchors.find((line) => content.includes(line));
341
- if (stateAnchor) {
342
- content = ensureLineAfter(
343
- content,
344
- stateAnchor,
345
- ' const [filesVariantsProbeResult, setFilesVariantsProbeResult] = useState<ProbeResult | null>(null);',
346
- );
347
- }
348
- }
349
-
350
- if (!content.includes('Check files probe (create metadata)')) {
351
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
352
- ? '/health/files'
353
- : '/api/health/files';
354
- const button = ` <button onClick={() => runProbe(setFilesProbeResult, '${probePath}', { method: 'POST' })}>\n Check files probe (create metadata)\n </button>`;
355
-
356
- const actionsStart = content.indexOf('<div className="actions">');
357
- if (actionsStart >= 0) {
358
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
359
- if (actionsEnd >= 0) {
360
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
361
- }
362
- }
363
- }
364
-
365
- if (!content.includes("{renderResult('Files probe response', filesProbeResult)}")) {
366
- const resultLine = " {renderResult('Files probe response', filesProbeResult)}";
367
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
368
- if (content.includes(networkLine)) {
369
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
370
- } else {
371
- const anchor = "{renderResult('Validation probe response', validationProbeResult)}";
372
- if (content.includes(anchor)) {
373
- content = ensureLineAfter(content, anchor, resultLine);
374
- }
375
- }
376
- }
377
-
378
- if (!content.includes('Check files variants capability')) {
379
- const probePath = content.includes("runProbe(setHealthResult, '/health')")
380
- ? '/health/files-variants'
381
- : '/api/health/files-variants';
382
- const button = ` <button onClick={() => runProbe(setFilesVariantsProbeResult, '${probePath}')}>\n Check files variants capability\n </button>`;
383
-
384
- const actionsStart = content.indexOf('<div className="actions">');
385
- if (actionsStart >= 0) {
386
- const actionsEnd = content.indexOf('\n </div>', actionsStart);
387
- if (actionsEnd >= 0) {
388
- content = `${content.slice(0, actionsEnd)}\n${button}${content.slice(actionsEnd)}`;
389
- }
390
- }
391
- }
392
-
393
- if (!content.includes("{renderResult('Files variants probe response', filesVariantsProbeResult)}")) {
394
- const resultLine = " {renderResult('Files variants probe response', filesVariantsProbeResult)}";
395
- const networkLine = ' {networkError ? <p className="error">{networkError}</p> : null}';
396
- if (content.includes(networkLine)) {
397
- content = content.replace(networkLine, `${resultLine}\n${networkLine}`);
398
- } else {
399
- const anchor = "{renderResult('Files probe response', filesProbeResult)}";
400
- if (content.includes(anchor)) {
401
- content = ensureLineAfter(content, anchor, resultLine);
402
- }
403
- }
404
- }
405
-
406
- fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
308
+ function registerWebProbe(targetRoot, probeTargets) {
309
+ ensureWebProbeDefinition({
310
+ targetRoot,
311
+ probeTargets,
312
+ definition: {
313
+ id: 'files',
314
+ title: 'Files',
315
+ buttonLabel: 'Check files probe (create metadata)',
316
+ resultTitle: 'Files probe response',
317
+ path: '/health/files',
318
+ request: { method: 'POST' },
319
+ },
320
+ });
321
+
322
+ ensureWebProbeDefinition({
323
+ targetRoot,
324
+ probeTargets,
325
+ definition: {
326
+ id: 'files-variants',
327
+ title: 'Files Variants',
328
+ buttonLabel: 'Check files variants capability',
329
+ resultTitle: 'Files variants probe response',
330
+ path: '/health/files-variants',
331
+ },
332
+ });
407
333
  }
408
334
 
409
335
  function patchCompose(targetRoot) {
@@ -415,7 +341,7 @@ function patchCompose(targetRoot) {
415
341
  let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
416
342
  if (!content.includes('FILES_ENABLED: ${FILES_ENABLED}')) {
417
343
  const anchors = [
418
- /^(\s+AUTH_DEMO_PASSWORD:.*)$/m,
344
+ /^(\s+AUTH_ARGON2_PARALLELISM:.*)$/m,
419
345
  /^(\s+THROTTLE_TRUST_PROXY:.*)$/m,
420
346
  /^(\s+LOGGER_LEVEL:.*)$/m,
421
347
  /^(\s+SWAGGER_ENABLED:.*)$/m,
@@ -499,13 +425,16 @@ Key env:
499
425
 
500
426
  export function applyFilesModule({ packageRoot, targetRoot }) {
501
427
  copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files'));
428
+ copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'files'));
429
+ const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files' });
430
+
502
431
 
503
432
  patchApiPackage(targetRoot);
504
433
  patchPrismaSchema(targetRoot);
505
434
  patchPrismaMigration(packageRoot, targetRoot);
506
435
  patchAppModule(targetRoot);
507
- patchHealthController(targetRoot);
508
- patchWebApp(targetRoot);
436
+ patchHealthController(targetRoot, probeTargets);
437
+ registerWebProbe(targetRoot, probeTargets);
509
438
  patchApiDockerfile(targetRoot);
510
439
  patchCompose(targetRoot);
511
440
  patchReadme(targetRoot);
@@ -525,3 +454,5 @@ export function applyFilesModule({ packageRoot, targetRoot }) {
525
454
  'FILES_ALLOWED_MIME_PREFIXES=image/,application/pdf,text/',
526
455
  ]);
527
456
  }
457
+
458
+