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.
- package/package.json +4 -2
- package/src/cli/add-options.test.mjs +5 -2
- package/src/cli/options.test.mjs +1 -0
- package/src/cli/prompt-select.test.mjs +1 -0
- package/src/core/docs.test.mjs +80 -40
- package/src/core/scaffold.test.mjs +100 -0
- package/src/core/validate.test.mjs +1 -0
- package/src/modules/accounts.mjs +416 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/dependencies.test.mjs +71 -29
- package/src/modules/executor.mjs +3 -2
- package/src/modules/executor.test.mjs +631 -500
- package/src/modules/files-access.mjs +36 -105
- package/src/modules/files-image.mjs +35 -107
- package/src/modules/files-local.mjs +15 -6
- package/src/modules/files-quotas.mjs +75 -93
- package/src/modules/files-s3.mjs +17 -6
- package/src/modules/files.mjs +56 -125
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +180 -0
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +204 -0
- package/src/modules/queue.mjs +325 -440
- package/src/modules/rate-limit.mjs +36 -76
- package/src/modules/rbac.mjs +39 -78
- package/src/modules/registry.mjs +22 -35
- package/src/modules/scheduler.mjs +51 -171
- package/src/modules/shared/files-runtime-wiring.mjs +81 -0
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/patch-utils.mjs +29 -1
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +109 -396
- package/src/modules/sync-integrations.test.mjs +141 -0
- package/src/run-add-module.test.mjs +154 -0
- package/templates/base/README.md +7 -55
- package/templates/base/apps/web/src/App.tsx +70 -42
- package/templates/base/apps/web/src/probes.ts +61 -0
- package/templates/base/apps/web/src/styles.css +86 -25
- package/templates/base/package.json +21 -15
- package/templates/base/scripts/forgeon-sync-integrations.mjs +65 -281
- package/templates/module-fragments/{jwt-auth → accounts}/00_title.md +2 -1
- package/templates/module-fragments/{jwt-auth → accounts}/10_overview.md +5 -5
- package/templates/module-fragments/accounts/20_scope.md +29 -0
- package/templates/module-fragments/accounts/90_status_implemented.md +8 -0
- package/templates/module-fragments/accounts/90_status_planned.md +7 -0
- package/templates/module-fragments/rbac/30_what_it_adds.md +3 -2
- package/templates/module-fragments/rbac/40_how_it_works.md +2 -1
- package/templates/module-fragments/rbac/50_how_to_use.md +2 -1
- package/templates/module-fragments/swagger/20_scope.md +2 -1
- package/templates/module-presets/accounts/apps/api/prisma/migrations/0002_accounts_core/migration.sql +97 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/forgeon-accounts-db-prisma.module.ts +17 -0
- package/templates/module-presets/accounts/apps/api/src/accounts/prisma-accounts-persistence.store.ts +332 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/package.json +5 -5
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-email.port.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +67 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-rbac.port.ts +14 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.loader.ts +7 -7
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.service.ts +7 -7
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +318 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-env.schema.ts +4 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-jwt.service.ts +58 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-password.service.ts +21 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.controller.ts +93 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.service.ts +48 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth.types.ts +17 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/change-password.dto.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/confirm-password-reset.dto.ts +12 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/index.ts +10 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/login.dto.ts +1 -1
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/dto/refresh.dto.ts +1 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/register.dto.ts +23 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/request-password-reset.dto.ts +7 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-profile.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user-settings.dto.ts +16 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/update-user.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/dto/verify-email.dto.ts +8 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +82 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +24 -0
- package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/jwt.strategy.ts +3 -3
- package/templates/module-presets/accounts/packages/accounts-api/src/owner-access.guard.ts +39 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users-config.ts +13 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.controller.ts +65 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +87 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +65 -0
- package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/package.json +1 -1
- package/templates/module-presets/accounts/packages/accounts-contracts/src/index.ts +119 -0
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +17 -0
- package/templates/module-presets/files/apps/api/src/files/prisma-files-persistence.store.ts +164 -0
- package/templates/module-presets/files/packages/files/package.json +1 -2
- package/templates/module-presets/files/packages/files/src/files.ports.ts +107 -0
- package/templates/module-presets/files/packages/files/src/files.service.ts +81 -395
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +126 -2
- package/templates/module-presets/files/packages/files/src/index.ts +2 -1
- package/templates/module-presets/files-local/packages/files-local/src/forgeon-files-local-storage.module.ts +18 -0
- package/templates/module-presets/files-local/packages/files-local/src/index.ts +2 -0
- package/templates/module-presets/files-local/packages/files-local/src/local-files-storage.adapter.ts +53 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- package/templates/module-presets/files-s3/packages/files-s3/src/forgeon-files-s3-storage.module.ts +18 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/index.ts +2 -0
- package/templates/module-presets/files-s3/packages/files-s3/src/s3-files-storage.adapter.ts +130 -0
- package/templates/module-presets/i18n/apps/web/src/App.tsx +68 -41
- package/templates/module-presets/logger/packages/logger/src/index.ts +0 -1
- package/src/modules/jwt-auth.mjs +0 -390
- package/templates/base/docs/AI/ARCHITECTURE.md +0 -85
- package/templates/base/docs/AI/MODULE_CHECKS.md +0 -28
- package/templates/base/docs/AI/MODULE_SPEC.md +0 -77
- package/templates/base/docs/AI/PROJECT.md +0 -43
- package/templates/base/docs/AI/ROADMAP.md +0 -171
- package/templates/base/docs/AI/TASKS.md +0 -60
- package/templates/base/docs/AI/VALIDATION.md +0 -31
- package/templates/base/docs/README.md +0 -18
- package/templates/module-fragments/jwt-auth/20_scope.md +0 -19
- package/templates/module-fragments/jwt-auth/90_status_implemented.md +0 -8
- package/templates/module-fragments/jwt-auth/90_status_planned.md +0 -3
- package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +0 -3
- package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +0 -36
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +0 -23
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +0 -71
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +0 -175
- package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +0 -6
- package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +0 -2
- package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +0 -47
- package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +0 -12
- package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +0 -47
- package/templates/module-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
- /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
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/src/auth-config.module.ts +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-api → accounts/packages/accounts-api}/tsconfig.json +0 -0
- /package/templates/module-presets/{jwt-auth/packages/auth-contracts → accounts/packages/accounts-contracts}/tsconfig.json +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -18,6 +18,21 @@ function createMinimalForgeonProject(targetRoot) {
|
|
|
18
18
|
fs.writeFileSync(path.join(targetRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n', 'utf8');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function readFile(filePath) {
|
|
22
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readWebProbes(projectRoot) {
|
|
26
|
+
return readFile(path.join(projectRoot, 'apps', 'web', 'src', 'probes.ts'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertWebProbeShell(projectRoot) {
|
|
30
|
+
const appTsx = readFile(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'));
|
|
31
|
+
assert.match(appTsx, /id="probes"/);
|
|
32
|
+
assert.match(appTsx, /from '\.\/probes'/);
|
|
33
|
+
return appTsx;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
function assertDbPrismaWiring(projectRoot) {
|
|
22
37
|
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
23
38
|
assert.match(appModule, /dbPrismaConfig/);
|
|
@@ -75,10 +90,12 @@ function assertRateLimitWiring(projectRoot) {
|
|
|
75
90
|
assert.match(healthController, /@Get\('rate-limit'\)/);
|
|
76
91
|
assert.match(healthController, /TOO_MANY_REQUESTS/);
|
|
77
92
|
|
|
78
|
-
const appTsx =
|
|
93
|
+
const appTsx = assertWebProbeShell(projectRoot);
|
|
94
|
+
const probesTs = readWebProbes(projectRoot);
|
|
79
95
|
assert.match(appTsx, /cache: 'no-store'/);
|
|
80
|
-
assert.match(
|
|
81
|
-
assert.match(
|
|
96
|
+
assert.match(probesTs, /"id": "rate-limit"/);
|
|
97
|
+
assert.match(probesTs, /"buttonLabel": "Check rate limit \(click repeatedly\)"/);
|
|
98
|
+
assert.match(probesTs, /"resultTitle": "Rate limit probe response"/);
|
|
82
99
|
|
|
83
100
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
84
101
|
assert.match(readme, /## Rate Limit Module/);
|
|
@@ -109,9 +126,11 @@ function assertQueueWiring(projectRoot) {
|
|
|
109
126
|
assert.match(healthController, /@Get\('queue'\)/);
|
|
110
127
|
assert.match(healthController, /queueService\.getProbeStatus/);
|
|
111
128
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
assert.match(
|
|
129
|
+
assertWebProbeShell(projectRoot);
|
|
130
|
+
const probesTs = readWebProbes(projectRoot);
|
|
131
|
+
assert.match(probesTs, /"id": "queue"/);
|
|
132
|
+
assert.match(probesTs, /"buttonLabel": "Check queue health"/);
|
|
133
|
+
assert.match(probesTs, /"resultTitle": "Queue probe response"/);
|
|
115
134
|
|
|
116
135
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
117
136
|
assert.match(apiEnv, /QUEUE_ENABLED=true/);
|
|
@@ -156,9 +175,11 @@ function assertSchedulerWiring(projectRoot) {
|
|
|
156
175
|
assert.match(healthController, /@Get\('scheduler'\)/);
|
|
157
176
|
assert.match(healthController, /schedulerService\.getProbeStatus/);
|
|
158
177
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
assert.match(
|
|
178
|
+
assertWebProbeShell(projectRoot);
|
|
179
|
+
const probesTs = readWebProbes(projectRoot);
|
|
180
|
+
assert.match(probesTs, /"id": "scheduler"/);
|
|
181
|
+
assert.match(probesTs, /"buttonLabel": "Check scheduler health"/);
|
|
182
|
+
assert.match(probesTs, /"resultTitle": "Scheduler probe response"/);
|
|
162
183
|
|
|
163
184
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
164
185
|
assert.match(apiEnv, /SCHEDULER_ENABLED=true/);
|
|
@@ -200,169 +221,254 @@ function assertRbacWiring(projectRoot) {
|
|
|
200
221
|
assert.match(healthController, /@Get\('rbac'\)/);
|
|
201
222
|
assert.match(healthController, /@Permissions\('health\.rbac'\)/);
|
|
202
223
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
assert.match(
|
|
206
|
-
assert.match(
|
|
224
|
+
assertWebProbeShell(projectRoot);
|
|
225
|
+
const probesTs = readWebProbes(projectRoot);
|
|
226
|
+
assert.match(probesTs, /"id": "rbac"/);
|
|
227
|
+
assert.match(probesTs, /"buttonLabel": "Check RBAC access"/);
|
|
228
|
+
assert.match(probesTs, /"resultTitle": "RBAC probe response"/);
|
|
229
|
+
assert.match(probesTs, /x-forgeon-permissions/);
|
|
207
230
|
|
|
208
231
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
209
232
|
assert.match(readme, /## RBAC \/ Permissions Module/);
|
|
210
233
|
assert.match(readme, /installs independently/i);
|
|
211
|
-
assert.match(readme, /
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
215
|
-
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
216
|
-
assert.match(appModule, /filesConfig/);
|
|
217
|
-
assert.match(appModule, /filesEnvSchema/);
|
|
218
|
-
assert.match(appModule, /ForgeonFilesModule/);
|
|
219
|
-
|
|
220
|
-
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
221
|
-
assert.match(apiPackage, /@forgeon\/files/);
|
|
222
|
-
assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
|
|
223
|
-
|
|
224
|
-
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
225
|
-
assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
|
|
226
|
-
assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
|
|
227
|
-
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
|
|
228
|
-
|
|
229
|
-
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
230
|
-
assert.match(apiEnv, /FILES_ENABLED=true/);
|
|
231
|
-
assert.match(apiEnv, new RegExp(`FILES_STORAGE_DRIVER=${expectedStorageDriver}`));
|
|
232
|
-
assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
|
|
233
|
-
assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
|
|
234
|
-
assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
|
|
235
|
-
|
|
236
|
-
const healthController = fs.readFileSync(
|
|
237
|
-
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
238
|
-
'utf8',
|
|
239
|
-
);
|
|
240
|
-
assert.match(healthController, /@Post\('files'\)/);
|
|
241
|
-
assert.match(healthController, /@Get\('files-variants'\)/);
|
|
242
|
-
assert.match(healthController, /filesService\.createProbeRecord/);
|
|
243
|
-
assert.match(healthController, /filesService\.getVariantsProbeStatus/);
|
|
244
|
-
assert.match(healthController, /filesService\.deleteByPublicId/);
|
|
245
|
-
|
|
246
|
-
const filesController = fs.readFileSync(
|
|
247
|
-
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
248
|
-
'utf8',
|
|
249
|
-
);
|
|
250
|
-
assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
|
|
251
|
-
assert.match(filesController, /parseVariant\(variantQuery\)/);
|
|
252
|
-
assert.match(filesController, /@Delete\(':publicId'\)/);
|
|
253
|
-
|
|
254
|
-
const filesService = fs.readFileSync(
|
|
255
|
-
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
256
|
-
'utf8',
|
|
257
|
-
);
|
|
258
|
-
assert.match(filesService, /getOrCreateBlob/);
|
|
259
|
-
assert.match(filesService, /cleanupReferencedBlobs/);
|
|
260
|
-
assert.match(filesService, /isUniqueConstraintError/);
|
|
261
|
-
assert.match(filesService, /fileBlob\.deleteMany/);
|
|
262
|
-
assert.match(filesService, /variants:\s*\{[\s\S]*?none:\s*\{[\s\S]*?\}/);
|
|
263
|
-
assert.match(filesService, /prisma\.fileBlob/);
|
|
264
|
-
|
|
265
|
-
const appTsx = fs.readFileSync(path.join(projectRoot, 'apps', 'web', 'src', 'App.tsx'), 'utf8');
|
|
266
|
-
assert.match(appTsx, /Check files probe \(create metadata\)/);
|
|
267
|
-
assert.match(appTsx, /Check files variants capability/);
|
|
268
|
-
assert.match(appTsx, /Files probe response/);
|
|
269
|
-
assert.match(appTsx, /Files variants probe response/);
|
|
270
|
-
|
|
271
|
-
const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
|
|
272
|
-
assert.match(schema, /model FileRecord \{/);
|
|
273
|
-
assert.match(schema, /variants\s+FileVariant\[\]/);
|
|
274
|
-
assert.match(schema, /model FileVariant \{/);
|
|
275
|
-
assert.match(schema, /model FileBlob \{/);
|
|
276
|
-
assert.match(schema, /blobId\s+String/);
|
|
277
|
-
assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
|
|
278
|
-
assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
|
|
279
|
-
assert.match(schema, /publicId\s+String\s+@unique/);
|
|
280
|
-
assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
|
|
281
|
-
|
|
282
|
-
const migration = path.join(
|
|
283
|
-
projectRoot,
|
|
284
|
-
'apps',
|
|
285
|
-
'api',
|
|
286
|
-
'prisma',
|
|
287
|
-
'migrations',
|
|
288
|
-
'20260306_files_file_record',
|
|
289
|
-
'migration.sql',
|
|
290
|
-
);
|
|
291
|
-
assert.equal(fs.existsSync(migration), true);
|
|
292
|
-
|
|
293
|
-
const variantMigration = path.join(
|
|
294
|
-
projectRoot,
|
|
295
|
-
'apps',
|
|
296
|
-
'api',
|
|
297
|
-
'prisma',
|
|
298
|
-
'migrations',
|
|
299
|
-
'20260306_files_file_variant',
|
|
300
|
-
'migration.sql',
|
|
301
|
-
);
|
|
302
|
-
assert.equal(fs.existsSync(variantMigration), true);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function assertFilesLocalWiring(projectRoot) {
|
|
306
|
-
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
307
|
-
assert.match(appModule, /filesLocalConfig/);
|
|
308
|
-
assert.match(appModule, /filesLocalEnvSchemaZod/);
|
|
309
|
-
assert.match(appModule, /FilesLocalConfigModule/);
|
|
310
|
-
|
|
311
|
-
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
312
|
-
assert.match(apiPackage, /@forgeon\/files-local/);
|
|
313
|
-
assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
|
|
314
|
-
|
|
315
|
-
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
316
|
-
assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
|
|
317
|
-
assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
|
|
318
|
-
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
|
|
319
|
-
|
|
320
|
-
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
321
|
-
assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
|
|
322
|
-
|
|
323
|
-
const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
|
|
324
|
-
assert.match(gitignore, /storage\//);
|
|
325
|
-
|
|
326
|
-
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
327
|
-
assert.match(compose, /files_data:\/app\/storage/);
|
|
328
|
-
assert.match(compose, /^\s{2}files_data:\s*$/m);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function assertFilesS3Wiring(projectRoot) {
|
|
332
|
-
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
333
|
-
assert.match(appModule, /filesS3Config/);
|
|
334
|
-
assert.match(appModule, /filesS3EnvSchemaZod/);
|
|
335
|
-
assert.match(appModule, /FilesS3ConfigModule/);
|
|
336
|
-
|
|
337
|
-
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
338
|
-
assert.match(apiPackage, /@forgeon\/files-s3/);
|
|
339
|
-
assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
|
|
340
|
-
|
|
341
|
-
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
342
|
-
assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
|
|
343
|
-
assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
|
|
344
|
-
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
|
|
345
|
-
|
|
346
|
-
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
347
|
-
assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
|
|
348
|
-
assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
|
|
349
|
-
assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
|
|
350
|
-
assert.match(apiEnv, /FILES_S3_REGION=/);
|
|
351
|
-
assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
|
|
352
|
-
assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
|
|
353
|
-
assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
|
|
354
|
-
|
|
355
|
-
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
356
|
-
assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
|
|
357
|
-
assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
|
|
358
|
-
|
|
359
|
-
const filesS3Package = fs.readFileSync(
|
|
360
|
-
path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
|
|
361
|
-
'utf8',
|
|
362
|
-
);
|
|
363
|
-
assert.match(filesS3Package, /@aws-sdk\/client-s3/);
|
|
234
|
+
assert.match(readme, /accounts.*optional/i);
|
|
364
235
|
}
|
|
365
236
|
|
|
237
|
+
function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
238
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
239
|
+
assert.match(appModule, /filesConfig/);
|
|
240
|
+
assert.match(appModule, /filesEnvSchema/);
|
|
241
|
+
assert.match(appModule, /ForgeonFilesModule\.register\(\{/);
|
|
242
|
+
assert.match(appModule, /ForgeonFilesDbPrismaModule/);
|
|
243
|
+
if (expectedStorageDriver === 's3') {
|
|
244
|
+
assert.match(appModule, /ForgeonFilesS3StorageModule/);
|
|
245
|
+
assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
|
|
246
|
+
} else {
|
|
247
|
+
assert.match(appModule, /ForgeonFilesLocalStorageModule/);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
251
|
+
assert.match(apiPackage, /@forgeon\/files/);
|
|
252
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files build/);
|
|
253
|
+
|
|
254
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
255
|
+
assert.match(apiDockerfile, /COPY packages\/files\/package\.json packages\/files\/package\.json/);
|
|
256
|
+
assert.match(apiDockerfile, /COPY packages\/files packages\/files/);
|
|
257
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files build/);
|
|
258
|
+
|
|
259
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
260
|
+
assert.match(apiEnv, /FILES_ENABLED=true/);
|
|
261
|
+
assert.match(apiEnv, new RegExp('FILES_STORAGE_DRIVER=' + expectedStorageDriver));
|
|
262
|
+
assert.match(apiEnv, /FILES_PUBLIC_BASE_PATH=\/files/);
|
|
263
|
+
assert.match(apiEnv, /FILES_MAX_FILE_SIZE_BYTES=10485760/);
|
|
264
|
+
assert.match(apiEnv, /FILES_ALLOWED_MIME_PREFIXES=image\/,application\/pdf,text\//);
|
|
265
|
+
|
|
266
|
+
const healthController = fs.readFileSync(
|
|
267
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
268
|
+
'utf8',
|
|
269
|
+
);
|
|
270
|
+
assert.match(healthController, /@Post\('files'\)/);
|
|
271
|
+
assert.match(healthController, /@Get\('files-variants'\)/);
|
|
272
|
+
assert.match(healthController, /filesService\.createProbeRecord/);
|
|
273
|
+
assert.match(healthController, /filesService\.getVariantsProbeStatus/);
|
|
274
|
+
assert.match(healthController, /filesService\.deleteByPublicId/);
|
|
275
|
+
|
|
276
|
+
const filesController = fs.readFileSync(
|
|
277
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
278
|
+
'utf8',
|
|
279
|
+
);
|
|
280
|
+
assert.match(filesController, /@Query\('variant'\) variantQuery\?: string/);
|
|
281
|
+
assert.match(filesController, /parseVariant\(variantQuery\)/);
|
|
282
|
+
assert.match(filesController, /@Delete\(':publicId'\)/);
|
|
283
|
+
|
|
284
|
+
const filesService = fs.readFileSync(
|
|
285
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
286
|
+
'utf8',
|
|
287
|
+
);
|
|
288
|
+
assert.match(filesService, /FILES_PERSISTENCE_PORT/);
|
|
289
|
+
assert.match(filesService, /FILES_STORAGE_ADAPTER/);
|
|
290
|
+
assert.match(filesService, /getOrCreateBlob/);
|
|
291
|
+
assert.match(filesService, /cleanupReferencedBlobs/);
|
|
292
|
+
assert.match(filesService, /isUniqueConstraintError/);
|
|
293
|
+
assert.match(filesService, /storageAdapter\.put/);
|
|
294
|
+
assert.match(filesService, /persistence\.createBlob/);
|
|
295
|
+
assert.match(filesService, /persistence\.deleteBlobIfUnreferenced/);
|
|
296
|
+
assert.doesNotMatch(filesService, /PrismaService/);
|
|
297
|
+
assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
|
|
298
|
+
|
|
299
|
+
const filesPorts = fs.readFileSync(
|
|
300
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.ports.ts'),
|
|
301
|
+
'utf8',
|
|
302
|
+
);
|
|
303
|
+
assert.match(filesPorts, /FILES_PERSISTENCE_PORT/);
|
|
304
|
+
assert.match(filesPorts, /FILES_STORAGE_ADAPTER/);
|
|
305
|
+
assert.match(filesPorts, /interface FilesPersistencePort/);
|
|
306
|
+
assert.match(filesPorts, /interface FilesStorageAdapter/);
|
|
307
|
+
|
|
308
|
+
const filesModule = fs.readFileSync(
|
|
309
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
|
|
310
|
+
'utf8',
|
|
311
|
+
);
|
|
312
|
+
assert.match(filesModule, /ForgeonFilesModuleOptions/);
|
|
313
|
+
assert.match(filesModule, /static register\(options: ForgeonFilesModuleOptions = \{\}\)/);
|
|
314
|
+
assert.match(filesModule, /FILES_PERSISTENCE_PORT/);
|
|
315
|
+
assert.match(filesModule, /FILES_STORAGE_ADAPTER/);
|
|
316
|
+
|
|
317
|
+
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
318
|
+
assert.doesNotMatch(filesPackage, /@forgeon\/db-prisma/);
|
|
319
|
+
|
|
320
|
+
const prismaFilesStore = fs.readFileSync(
|
|
321
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'files', 'prisma-files-persistence.store.ts'),
|
|
322
|
+
'utf8',
|
|
323
|
+
);
|
|
324
|
+
assert.match(prismaFilesStore, /PrismaService/);
|
|
325
|
+
assert.match(prismaFilesStore, /FILES_PERSISTENCE_PORT/);
|
|
326
|
+
assert.match(prismaFilesStore, /fileBlob\.deleteMany/);
|
|
327
|
+
|
|
328
|
+
const prismaFilesModule = fs.readFileSync(
|
|
329
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'files', 'forgeon-files-db-prisma.module.ts'),
|
|
330
|
+
'utf8',
|
|
331
|
+
);
|
|
332
|
+
assert.match(prismaFilesModule, /ForgeonFilesDbPrismaModule/);
|
|
333
|
+
assert.match(prismaFilesModule, /DbPrismaModule/);
|
|
334
|
+
assert.match(prismaFilesModule, /FILES_PERSISTENCE_PORT/);
|
|
335
|
+
|
|
336
|
+
assertWebProbeShell(projectRoot);
|
|
337
|
+
const probesTs = readWebProbes(projectRoot);
|
|
338
|
+
assert.match(probesTs, /"id": "files"/);
|
|
339
|
+
assert.match(probesTs, /"buttonLabel": "Check files probe \(create metadata\)"/);
|
|
340
|
+
assert.match(probesTs, /"resultTitle": "Files probe response"/);
|
|
341
|
+
assert.match(probesTs, /"id": "files-variants"/);
|
|
342
|
+
assert.match(probesTs, /"buttonLabel": "Check files variants capability"/);
|
|
343
|
+
assert.match(probesTs, /"resultTitle": "Files variants probe response"/);
|
|
344
|
+
|
|
345
|
+
const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
|
|
346
|
+
assert.match(schema, /model FileRecord \{/);
|
|
347
|
+
assert.match(schema, /variants\s+FileVariant\[\]/);
|
|
348
|
+
assert.match(schema, /model FileVariant \{/);
|
|
349
|
+
assert.match(schema, /model FileBlob \{/);
|
|
350
|
+
assert.match(schema, /blobId\s+String/);
|
|
351
|
+
assert.match(schema, /@@unique\(\[hash,\s*size,\s*mimeType,\s*storageDriver\]\)/);
|
|
352
|
+
assert.match(schema, /@@unique\(\[fileId,\s*variantKey\]\)/);
|
|
353
|
+
assert.match(schema, /publicId\s+String\s+@unique/);
|
|
354
|
+
assert.match(schema, /@@index\(\[ownerType,\s*ownerId,\s*createdAt\]\)/);
|
|
355
|
+
|
|
356
|
+
const migration = path.join(
|
|
357
|
+
projectRoot,
|
|
358
|
+
'apps',
|
|
359
|
+
'api',
|
|
360
|
+
'prisma',
|
|
361
|
+
'migrations',
|
|
362
|
+
'20260306_files_file_record',
|
|
363
|
+
'migration.sql',
|
|
364
|
+
);
|
|
365
|
+
assert.equal(fs.existsSync(migration), true);
|
|
366
|
+
|
|
367
|
+
const variantMigration = path.join(
|
|
368
|
+
projectRoot,
|
|
369
|
+
'apps',
|
|
370
|
+
'api',
|
|
371
|
+
'prisma',
|
|
372
|
+
'migrations',
|
|
373
|
+
'20260306_files_file_variant',
|
|
374
|
+
'migration.sql',
|
|
375
|
+
);
|
|
376
|
+
assert.equal(fs.existsSync(variantMigration), true);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function assertFilesLocalWiring(projectRoot) {
|
|
380
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
381
|
+
assert.match(appModule, /filesLocalConfig/);
|
|
382
|
+
assert.match(appModule, /filesLocalEnvSchemaZod/);
|
|
383
|
+
assert.match(appModule, /FilesLocalConfigModule/);
|
|
384
|
+
assert.match(appModule, /ForgeonFilesLocalStorageModule/);
|
|
385
|
+
|
|
386
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
387
|
+
assert.match(apiPackage, /@forgeon\/files-local/);
|
|
388
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-local build/);
|
|
389
|
+
|
|
390
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
391
|
+
assert.match(apiDockerfile, /COPY packages\/files-local\/package\.json packages\/files-local\/package\.json/);
|
|
392
|
+
assert.match(apiDockerfile, /COPY packages\/files-local packages\/files-local/);
|
|
393
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-local build/);
|
|
394
|
+
|
|
395
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
396
|
+
assert.match(apiEnv, /FILES_LOCAL_ROOT=storage\/uploads/);
|
|
397
|
+
|
|
398
|
+
const localModule = fs.readFileSync(
|
|
399
|
+
path.join(projectRoot, 'packages', 'files-local', 'src', 'forgeon-files-local-storage.module.ts'),
|
|
400
|
+
'utf8',
|
|
401
|
+
);
|
|
402
|
+
assert.match(localModule, /ForgeonFilesLocalStorageModule/);
|
|
403
|
+
assert.match(localModule, /FORGEON_FILES_STORAGE_ADAPTER/);
|
|
404
|
+
|
|
405
|
+
const localAdapter = fs.readFileSync(
|
|
406
|
+
path.join(projectRoot, 'packages', 'files-local', 'src', 'local-files-storage.adapter.ts'),
|
|
407
|
+
'utf8',
|
|
408
|
+
);
|
|
409
|
+
assert.match(localAdapter, /readonly driver = 'local'/);
|
|
410
|
+
assert.match(localAdapter, /createReadStream/);
|
|
411
|
+
assert.match(localAdapter, /writeFile/);
|
|
412
|
+
|
|
413
|
+
const gitignore = fs.readFileSync(path.join(projectRoot, '.gitignore'), 'utf8');
|
|
414
|
+
assert.match(gitignore, /storage\//);
|
|
415
|
+
|
|
416
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
417
|
+
assert.match(compose, /files_data:\/app\/storage/);
|
|
418
|
+
assert.match(compose, /^\s{2}files_data:\s*$/m);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function assertFilesS3Wiring(projectRoot) {
|
|
422
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
423
|
+
assert.match(appModule, /filesS3Config/);
|
|
424
|
+
assert.match(appModule, /filesS3EnvSchemaZod/);
|
|
425
|
+
assert.match(appModule, /FilesS3ConfigModule/);
|
|
426
|
+
assert.match(appModule, /ForgeonFilesS3StorageModule/);
|
|
427
|
+
|
|
428
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
429
|
+
assert.match(apiPackage, /@forgeon\/files-s3/);
|
|
430
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/files-s3 build/);
|
|
431
|
+
|
|
432
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
433
|
+
assert.match(apiDockerfile, /COPY packages\/files-s3\/package\.json packages\/files-s3\/package\.json/);
|
|
434
|
+
assert.match(apiDockerfile, /COPY packages\/files-s3 packages\/files-s3/);
|
|
435
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-s3 build/);
|
|
436
|
+
|
|
437
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
438
|
+
assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
|
|
439
|
+
assert.match(apiEnv, /FILES_S3_PROVIDER_PRESET=minio/);
|
|
440
|
+
assert.match(apiEnv, /FILES_S3_BUCKET=forgeon-files/);
|
|
441
|
+
assert.match(apiEnv, /FILES_S3_REGION=/);
|
|
442
|
+
assert.match(apiEnv, /FILES_S3_ENDPOINT=/);
|
|
443
|
+
assert.match(apiEnv, /FILES_S3_FORCE_PATH_STYLE=/);
|
|
444
|
+
assert.match(apiEnv, /FILES_S3_MAX_ATTEMPTS=3/);
|
|
445
|
+
|
|
446
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
447
|
+
assert.match(compose, /FILES_S3_PROVIDER_PRESET: \$\{FILES_S3_PROVIDER_PRESET\}/);
|
|
448
|
+
assert.match(compose, /FILES_S3_MAX_ATTEMPTS: \$\{FILES_S3_MAX_ATTEMPTS\}/);
|
|
449
|
+
|
|
450
|
+
const filesS3Package = fs.readFileSync(
|
|
451
|
+
path.join(projectRoot, 'packages', 'files-s3', 'package.json'),
|
|
452
|
+
'utf8',
|
|
453
|
+
);
|
|
454
|
+
assert.match(filesS3Package, /@aws-sdk\/client-s3/);
|
|
455
|
+
|
|
456
|
+
const s3Module = fs.readFileSync(
|
|
457
|
+
path.join(projectRoot, 'packages', 'files-s3', 'src', 'forgeon-files-s3-storage.module.ts'),
|
|
458
|
+
'utf8',
|
|
459
|
+
);
|
|
460
|
+
assert.match(s3Module, /ForgeonFilesS3StorageModule/);
|
|
461
|
+
assert.match(s3Module, /FORGEON_FILES_STORAGE_ADAPTER/);
|
|
462
|
+
|
|
463
|
+
const s3Adapter = fs.readFileSync(
|
|
464
|
+
path.join(projectRoot, 'packages', 'files-s3', 'src', 's3-files-storage.adapter.ts'),
|
|
465
|
+
'utf8',
|
|
466
|
+
);
|
|
467
|
+
assert.match(s3Adapter, /readonly driver = 's3'/);
|
|
468
|
+
assert.match(s3Adapter, /@aws-sdk\/client-s3/);
|
|
469
|
+
assert.match(s3Adapter, /loadS3Module/);
|
|
470
|
+
}
|
|
471
|
+
|
|
366
472
|
function assertFilesAccessWiring(projectRoot) {
|
|
367
473
|
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
368
474
|
assert.match(appModule, /ForgeonFilesAccessModule/);
|
|
@@ -410,10 +516,12 @@ function assertFilesAccessWiring(projectRoot) {
|
|
|
410
516
|
assert.match(healthController, /extractFilesAccessSubject/);
|
|
411
517
|
assert.match(healthController, /filesAccessService\.canRead/);
|
|
412
518
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
assert.match(
|
|
416
|
-
assert.match(
|
|
519
|
+
assertWebProbeShell(projectRoot);
|
|
520
|
+
const probesTs = readWebProbes(projectRoot);
|
|
521
|
+
assert.match(probesTs, /"id": "files-access"/);
|
|
522
|
+
assert.match(probesTs, /"buttonLabel": "Check files access"/);
|
|
523
|
+
assert.match(probesTs, /"resultTitle": "Files access probe response"/);
|
|
524
|
+
assert.match(probesTs, /x-forgeon-user-id/);
|
|
417
525
|
|
|
418
526
|
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
419
527
|
assert.match(readme, /## Files Access Module/);
|
|
@@ -436,7 +544,8 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
436
544
|
);
|
|
437
545
|
|
|
438
546
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
439
|
-
assert.
|
|
547
|
+
assert.doesNotMatch(filesPackage, /@forgeon\/files-quotas/);
|
|
548
|
+
assert.match(filesPackage, /@nestjs\/core/);
|
|
440
549
|
|
|
441
550
|
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
442
551
|
assert.match(
|
|
@@ -455,7 +564,9 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
455
564
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
456
565
|
'utf8',
|
|
457
566
|
);
|
|
458
|
-
assert.match(filesController, /
|
|
567
|
+
assert.match(filesController, /ModuleRef/);
|
|
568
|
+
assert.match(filesController, /FORGEON_FILES_UPLOAD_QUOTA_SERVICE/);
|
|
569
|
+
assert.match(filesController, /getFilesUploadQuotaService/);
|
|
459
570
|
assert.match(filesController, /filesQuotasService\.assertUploadAllowed/);
|
|
460
571
|
|
|
461
572
|
const healthController = fs.readFileSync(
|
|
@@ -465,9 +576,11 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
465
576
|
assert.match(healthController, /@Get\('files-quotas'\)/);
|
|
466
577
|
assert.match(healthController, /filesQuotasService\.getProbeStatus/);
|
|
467
578
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
assert.match(
|
|
579
|
+
assertWebProbeShell(projectRoot);
|
|
580
|
+
const probesTs = readWebProbes(projectRoot);
|
|
581
|
+
assert.match(probesTs, /"id": "files-quotas"/);
|
|
582
|
+
assert.match(probesTs, /"buttonLabel": "Check files quotas"/);
|
|
583
|
+
assert.match(probesTs, /"resultTitle": "Files quotas probe response"/);
|
|
471
584
|
|
|
472
585
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
473
586
|
assert.match(apiEnv, /FILES_QUOTAS_ENABLED=true/);
|
|
@@ -552,9 +665,11 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
552
665
|
assert.match(healthController, /@Get\('files-image'\)/);
|
|
553
666
|
assert.match(healthController, /filesImageService\.getProbeStatus/);
|
|
554
667
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
assert.match(
|
|
668
|
+
assertWebProbeShell(projectRoot);
|
|
669
|
+
const probesTs = readWebProbes(projectRoot);
|
|
670
|
+
assert.match(probesTs, /"id": "files-image"/);
|
|
671
|
+
assert.match(probesTs, /"buttonLabel": "Check files image sanitize"/);
|
|
672
|
+
assert.match(probesTs, /"resultTitle": "Files image probe response"/);
|
|
558
673
|
|
|
559
674
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
560
675
|
assert.match(apiEnv, /FILES_IMAGE_ENABLED=true/);
|
|
@@ -591,68 +706,78 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
591
706
|
assert.match(readme, /metadata is stripped before storage/i);
|
|
592
707
|
}
|
|
593
708
|
|
|
594
|
-
function
|
|
595
|
-
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
596
|
-
assert.match(apiPackage, /@forgeon\/
|
|
597
|
-
assert.match(apiPackage, /@forgeon\/
|
|
598
|
-
assert.match(apiPackage, /pnpm --filter @forgeon\/
|
|
599
|
-
assert.match(apiPackage, /pnpm --filter @forgeon\/
|
|
600
|
-
|
|
601
|
-
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
602
|
-
assert.match(appModule, /authConfig/);
|
|
603
|
-
assert.match(appModule, /authEnvSchema/);
|
|
604
|
-
assert.match(appModule, /
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
assert.match(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
);
|
|
629
|
-
assert.match(apiDockerfile, /COPY packages\/
|
|
630
|
-
assert.match(apiDockerfile, /COPY packages\/
|
|
631
|
-
assert.match(apiDockerfile, /
|
|
632
|
-
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
assert.match(apiEnv, /
|
|
637
|
-
assert.match(apiEnv, /
|
|
638
|
-
assert.match(apiEnv, /
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
assert.match(compose, /
|
|
643
|
-
assert.match(compose, /
|
|
644
|
-
|
|
645
|
-
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
646
|
-
assert.match(readme, /##
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
709
|
+
function assertAccountsWiring(projectRoot) {
|
|
710
|
+
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
711
|
+
assert.match(apiPackage, /@forgeon\/accounts-api/);
|
|
712
|
+
assert.match(apiPackage, /@forgeon\/accounts-contracts/);
|
|
713
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-contracts build/);
|
|
714
|
+
assert.match(apiPackage, /pnpm --filter @forgeon\/accounts-api build/);
|
|
715
|
+
|
|
716
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
717
|
+
assert.match(appModule, /authConfig/);
|
|
718
|
+
assert.match(appModule, /authEnvSchema/);
|
|
719
|
+
assert.match(appModule, /ForgeonAccountsDbPrismaModule/);
|
|
720
|
+
assert.match(appModule, /ForgeonAccountsModule\.register\(\{/);
|
|
721
|
+
assert.match(appModule, /UsersModule\.register\(\{\}\)/);
|
|
722
|
+
assert.doesNotMatch(appModule, /AUTH_REFRESH_TOKEN_STORE/);
|
|
723
|
+
|
|
724
|
+
const healthController = fs.readFileSync(
|
|
725
|
+
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
726
|
+
'utf8',
|
|
727
|
+
);
|
|
728
|
+
assert.match(healthController, /@Get\('auth'\)/);
|
|
729
|
+
assert.match(healthController, /authService\.getProbeStatus/);
|
|
730
|
+
assert.doesNotMatch(healthController, /,\s*,/);
|
|
731
|
+
|
|
732
|
+
assertWebProbeShell(projectRoot);
|
|
733
|
+
const probesTs = readWebProbes(projectRoot);
|
|
734
|
+
assert.match(probesTs, /"id": "auth"/);
|
|
735
|
+
assert.match(probesTs, /"buttonLabel": "Check accounts probe"/);
|
|
736
|
+
assert.match(probesTs, /"resultTitle": "Accounts probe response"/);
|
|
737
|
+
|
|
738
|
+
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
739
|
+
assert.match(
|
|
740
|
+
apiDockerfile,
|
|
741
|
+
/COPY packages\/accounts-contracts\/package\.json packages\/accounts-contracts\/package\.json/,
|
|
742
|
+
);
|
|
743
|
+
assert.match(apiDockerfile, /COPY packages\/accounts-api\/package\.json packages\/accounts-api\/package\.json/);
|
|
744
|
+
assert.match(apiDockerfile, /COPY packages\/accounts-contracts packages\/accounts-contracts/);
|
|
745
|
+
assert.match(apiDockerfile, /COPY packages\/accounts-api packages\/accounts-api/);
|
|
746
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-contracts build/);
|
|
747
|
+
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/accounts-api build/);
|
|
748
|
+
|
|
749
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
750
|
+
assert.match(apiEnv, /JWT_ACCESS_SECRET=/);
|
|
751
|
+
assert.match(apiEnv, /JWT_REFRESH_SECRET=/);
|
|
752
|
+
assert.match(apiEnv, /AUTH_ARGON2_MEMORY_COST=/);
|
|
753
|
+
assert.match(apiEnv, /AUTH_ARGON2_PARALLELISM=/);
|
|
754
|
+
|
|
755
|
+
const compose = fs.readFileSync(path.join(projectRoot, 'infra', 'docker', 'compose.yml'), 'utf8');
|
|
756
|
+
assert.match(compose, /JWT_ACCESS_SECRET: \$\{JWT_ACCESS_SECRET\}/);
|
|
757
|
+
assert.match(compose, /JWT_REFRESH_SECRET: \$\{JWT_REFRESH_SECRET\}/);
|
|
758
|
+
assert.match(compose, /AUTH_ARGON2_MEMORY_COST: \$\{AUTH_ARGON2_MEMORY_COST\}/);
|
|
759
|
+
|
|
760
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
761
|
+
assert.match(readme, /## Accounts Module/);
|
|
762
|
+
assert.match(readme, /owner-scoped user routes/);
|
|
763
|
+
assert.match(readme, /AccountsEmailPort/);
|
|
764
|
+
|
|
765
|
+
const authServiceSource = fs.readFileSync(
|
|
766
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.service.ts'),
|
|
767
|
+
'utf8',
|
|
768
|
+
);
|
|
769
|
+
assert.match(authServiceSource, /import type \{ RegisterRequest \} from '@forgeon\/accounts-contracts';/);
|
|
770
|
+
|
|
771
|
+
const prismaStorePath = path.join(
|
|
772
|
+
projectRoot,
|
|
773
|
+
'apps',
|
|
774
|
+
'api',
|
|
775
|
+
'src',
|
|
776
|
+
'accounts',
|
|
777
|
+
'prisma-accounts-persistence.store.ts',
|
|
778
|
+
);
|
|
779
|
+
assert.equal(fs.existsSync(prismaStorePath), true);
|
|
780
|
+
}
|
|
656
781
|
function stripDbPrismaArtifacts(projectRoot) {
|
|
657
782
|
const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
|
|
658
783
|
if (fs.existsSync(dbPackageDir)) {
|
|
@@ -1188,6 +1313,16 @@ describe('addModule', () => {
|
|
|
1188
1313
|
assert.match(loggerModule, /ForgeonHttpLoggingMiddleware/);
|
|
1189
1314
|
assert.match(loggerModule, /consumer\.apply\(RequestIdMiddleware, ForgeonHttpLoggingMiddleware\)\.forRoutes\('\*'\);/);
|
|
1190
1315
|
|
|
1316
|
+
const loggerIndex = fs.readFileSync(
|
|
1317
|
+
path.join(projectRoot, 'packages', 'logger', 'src', 'index.ts'),
|
|
1318
|
+
'utf8',
|
|
1319
|
+
);
|
|
1320
|
+
assert.doesNotMatch(loggerIndex, /http-logging\.interceptor/);
|
|
1321
|
+
assert.equal(
|
|
1322
|
+
fs.existsSync(path.join(projectRoot, 'packages', 'logger', 'src', 'http-logging.interceptor.ts')),
|
|
1323
|
+
false,
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1191
1326
|
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
1192
1327
|
assert.match(apiEnv, /LOGGER_LEVEL=log/);
|
|
1193
1328
|
assert.match(apiEnv, /LOGGER_HTTP_ENABLED=true/);
|
|
@@ -1405,17 +1540,21 @@ describe('addModule', () => {
|
|
|
1405
1540
|
packageRoot,
|
|
1406
1541
|
});
|
|
1407
1542
|
|
|
1408
|
-
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
1409
|
-
assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
|
|
1410
|
-
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1543
|
+
const apiEnv = fs.readFileSync(path.join(projectRoot, 'apps', 'api', '.env.example'), 'utf8');
|
|
1544
|
+
assert.match(apiEnv, /FILES_STORAGE_DRIVER=s3/);
|
|
1545
|
+
|
|
1546
|
+
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
1547
|
+
assert.match(appModule, /ForgeonFilesS3StorageModule/);
|
|
1548
|
+
assert.doesNotMatch(appModule, /imports: \[ForgeonFilesDbPrismaModule, ForgeonFilesLocalStorageModule\]/);
|
|
1549
|
+
|
|
1550
|
+
const filesService = fs.readFileSync(
|
|
1551
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
1552
|
+
'utf8',
|
|
1553
|
+
);
|
|
1554
|
+
assert.doesNotMatch(filesService, /storeS3/);
|
|
1555
|
+
assert.doesNotMatch(filesService, /openS3/);
|
|
1556
|
+
assert.doesNotMatch(filesService, /deleteS3/);
|
|
1557
|
+
assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
|
|
1419
1558
|
} finally {
|
|
1420
1559
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
1421
1560
|
}
|
|
@@ -1604,8 +1743,9 @@ describe('addModule', () => {
|
|
|
1604
1743
|
assert.match(healthController, /@Get\('files-access'\)/);
|
|
1605
1744
|
assert.match(healthController, /@Get\('files-quotas'\)/);
|
|
1606
1745
|
|
|
1607
|
-
|
|
1608
|
-
const
|
|
1746
|
+
assertWebProbeShell(projectRoot);
|
|
1747
|
+
const probesTs = readWebProbes(projectRoot);
|
|
1748
|
+
const filesChecks = probesTs.match(/"buttonLabel": "Check files /g) ?? [];
|
|
1609
1749
|
assert.equal(filesChecks.length, 5);
|
|
1610
1750
|
} finally {
|
|
1611
1751
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
@@ -1925,6 +2065,51 @@ describe('addModule', () => {
|
|
|
1925
2065
|
}
|
|
1926
2066
|
});
|
|
1927
2067
|
|
|
2068
|
+
|
|
2069
|
+
it('applies i18n after probe modules and preserves managed probe registry entries', () => {
|
|
2070
|
+
const targetRoot = mkTmp('forgeon-module-i18n-probes-');
|
|
2071
|
+
const projectRoot = path.join(targetRoot, 'demo-i18n-probes');
|
|
2072
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
2073
|
+
|
|
2074
|
+
try {
|
|
2075
|
+
scaffoldProject({
|
|
2076
|
+
templateRoot,
|
|
2077
|
+
packageRoot,
|
|
2078
|
+
targetRoot: projectRoot,
|
|
2079
|
+
projectName: 'demo-i18n-probes',
|
|
2080
|
+
frontend: 'react',
|
|
2081
|
+
db: 'prisma',
|
|
2082
|
+
dbPrismaEnabled: true,
|
|
2083
|
+
i18nEnabled: false,
|
|
2084
|
+
proxy: 'caddy',
|
|
2085
|
+
});
|
|
2086
|
+
|
|
2087
|
+
addModule({ moduleId: 'rate-limit', targetRoot: projectRoot, packageRoot });
|
|
2088
|
+
addModule({ moduleId: 'rbac', targetRoot: projectRoot, packageRoot });
|
|
2089
|
+
|
|
2090
|
+
const probesBeforeI18n = readWebProbes(projectRoot);
|
|
2091
|
+
assert.match(probesBeforeI18n, /"id": "rate-limit"/);
|
|
2092
|
+
assert.match(probesBeforeI18n, /"id": "rbac"/);
|
|
2093
|
+
|
|
2094
|
+
const i18nResult = addModule({
|
|
2095
|
+
moduleId: 'i18n',
|
|
2096
|
+
targetRoot: projectRoot,
|
|
2097
|
+
packageRoot,
|
|
2098
|
+
});
|
|
2099
|
+
assert.equal(i18nResult.applied, true);
|
|
2100
|
+
|
|
2101
|
+
const probesAfterI18n = readWebProbes(projectRoot);
|
|
2102
|
+
assert.match(probesAfterI18n, /"id": "rate-limit"/);
|
|
2103
|
+
assert.match(probesAfterI18n, /"id": "rbac"/);
|
|
2104
|
+
|
|
2105
|
+
const rateLimitIndex = probesAfterI18n.indexOf('"id": "rate-limit"');
|
|
2106
|
+
const rbacIndex = probesAfterI18n.indexOf('"id": "rbac"');
|
|
2107
|
+
assert.equal(rbacIndex >= 0 && rateLimitIndex > rbacIndex, true);
|
|
2108
|
+
} finally {
|
|
2109
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
|
|
1928
2113
|
it('applies swagger -> logger -> i18n and keeps all module wiring', () => {
|
|
1929
2114
|
const targetRoot = mkTmp('forgeon-module-mixed-order-');
|
|
1930
2115
|
const projectRoot = path.join(targetRoot, 'demo-mixed-order');
|
|
@@ -1979,239 +2164,175 @@ describe('addModule', () => {
|
|
|
1979
2164
|
}
|
|
1980
2165
|
});
|
|
1981
2166
|
|
|
1982
|
-
it('applies
|
|
1983
|
-
const targetRoot = mkTmp('forgeon-module-
|
|
1984
|
-
const projectRoot = path.join(targetRoot, 'demo-
|
|
1985
|
-
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
1986
|
-
|
|
1987
|
-
try {
|
|
1988
|
-
scaffoldProject({
|
|
1989
|
-
templateRoot,
|
|
1990
|
-
packageRoot,
|
|
1991
|
-
targetRoot: projectRoot,
|
|
1992
|
-
projectName: 'demo-
|
|
1993
|
-
frontend: 'react',
|
|
1994
|
-
db: 'prisma',
|
|
1995
|
-
dbPrismaEnabled: true,
|
|
1996
|
-
i18nEnabled: true,
|
|
1997
|
-
proxy: 'caddy',
|
|
1998
|
-
});
|
|
1999
|
-
|
|
2000
|
-
const result = addModule({
|
|
2001
|
-
moduleId: '
|
|
2002
|
-
targetRoot: projectRoot,
|
|
2003
|
-
packageRoot,
|
|
2004
|
-
});
|
|
2005
|
-
|
|
2006
|
-
assert.equal(result.applied, true);
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
assert.
|
|
2012
|
-
assert.
|
|
2013
|
-
assert.
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
'
|
|
2021
|
-
'
|
|
2022
|
-
'
|
|
2023
|
-
'
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
assert.match(
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
fs.
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
targetRoot: projectRoot,
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
});
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
targetRoot: projectRoot,
|
|
2135
|
-
packageRoot,
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
const
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
const
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
);
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
);
|
|
2152
|
-
assert.match(authService, /permissions: \['health\.rbac'\]/);
|
|
2153
|
-
assert.match(authService, /permissions: user\.permissions,/);
|
|
2154
|
-
assert.match(
|
|
2155
|
-
authService,
|
|
2156
|
-
/permissions: Array\.isArray\(payload\.permissions\) \? payload\.permissions : \[\],/,
|
|
2157
|
-
);
|
|
2158
|
-
|
|
2159
|
-
const authController = fs.readFileSync(
|
|
2160
|
-
path.join(projectRoot, 'packages', 'auth-api', 'src', 'auth.controller.ts'),
|
|
2161
|
-
'utf8',
|
|
2162
|
-
);
|
|
2163
|
-
assert.match(
|
|
2164
|
-
authController,
|
|
2165
|
-
/permissions: Array\.isArray\(payload\.permissions\) \? payload\.permissions : \[\],/,
|
|
2166
|
-
);
|
|
2167
|
-
} finally {
|
|
2168
|
-
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2169
|
-
}
|
|
2170
|
-
});
|
|
2171
|
-
|
|
2172
|
-
it('scans auth persistence as db-adapter participant while remaining triggerable from db-prisma install order', () => {
|
|
2173
|
-
const targetRoot = mkTmp('forgeon-module-jwt-db-scan-');
|
|
2174
|
-
const projectRoot = path.join(targetRoot, 'demo-jwt-db-scan');
|
|
2175
|
-
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
2176
|
-
|
|
2177
|
-
try {
|
|
2178
|
-
scaffoldProject({
|
|
2179
|
-
templateRoot,
|
|
2180
|
-
packageRoot,
|
|
2181
|
-
targetRoot: projectRoot,
|
|
2182
|
-
projectName: 'demo-jwt-db-scan',
|
|
2183
|
-
frontend: 'react',
|
|
2184
|
-
db: 'prisma',
|
|
2185
|
-
dbPrismaEnabled: false,
|
|
2186
|
-
i18nEnabled: false,
|
|
2187
|
-
proxy: 'caddy',
|
|
2188
|
-
});
|
|
2189
|
-
|
|
2190
|
-
addModule({
|
|
2191
|
-
moduleId: 'jwt-auth',
|
|
2192
|
-
targetRoot: projectRoot,
|
|
2193
|
-
packageRoot,
|
|
2194
|
-
});
|
|
2195
|
-
addModule({
|
|
2196
|
-
moduleId: 'db-prisma',
|
|
2197
|
-
targetRoot: projectRoot,
|
|
2198
|
-
packageRoot,
|
|
2199
|
-
});
|
|
2200
|
-
|
|
2201
|
-
const scan = scanIntegrations({
|
|
2202
|
-
targetRoot: projectRoot,
|
|
2203
|
-
relatedModuleId: 'db-prisma',
|
|
2204
|
-
});
|
|
2205
|
-
const persistenceGroup = scan.groups.find((group) => group.id === 'auth-persistence');
|
|
2206
|
-
|
|
2207
|
-
assert.ok(persistenceGroup);
|
|
2208
|
-
assert.deepEqual(persistenceGroup.modules, ['jwt-auth', 'db-adapter']);
|
|
2209
|
-
} finally {
|
|
2210
|
-
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2211
|
-
}
|
|
2212
|
-
});
|
|
2213
|
-
|
|
2214
|
-
it('applies logger then jwt-auth on db/i18n-disabled scaffold without breaking health controller syntax', () => {
|
|
2167
|
+
it('applies accounts with db-prisma and wires the DB-backed runtime immediately', () => {
|
|
2168
|
+
const targetRoot = mkTmp('forgeon-module-accounts-db-');
|
|
2169
|
+
const projectRoot = path.join(targetRoot, 'demo-accounts-db');
|
|
2170
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
2171
|
+
|
|
2172
|
+
try {
|
|
2173
|
+
scaffoldProject({
|
|
2174
|
+
templateRoot,
|
|
2175
|
+
packageRoot,
|
|
2176
|
+
targetRoot: projectRoot,
|
|
2177
|
+
projectName: 'demo-accounts-db',
|
|
2178
|
+
frontend: 'react',
|
|
2179
|
+
db: 'prisma',
|
|
2180
|
+
dbPrismaEnabled: true,
|
|
2181
|
+
i18nEnabled: true,
|
|
2182
|
+
proxy: 'caddy',
|
|
2183
|
+
});
|
|
2184
|
+
|
|
2185
|
+
const result = addModule({
|
|
2186
|
+
moduleId: 'accounts',
|
|
2187
|
+
targetRoot: projectRoot,
|
|
2188
|
+
packageRoot,
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
assert.equal(result.applied, true);
|
|
2192
|
+
assertAccountsWiring(projectRoot);
|
|
2193
|
+
|
|
2194
|
+
const schema = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'prisma', 'schema.prisma'), 'utf8');
|
|
2195
|
+
assert.match(schema, /model UserProfile/);
|
|
2196
|
+
assert.match(schema, /model UserSettings/);
|
|
2197
|
+
assert.match(schema, /model AuthIdentity/);
|
|
2198
|
+
assert.match(schema, /model AuthCredential/);
|
|
2199
|
+
assert.match(schema, /model AuthRefreshToken/);
|
|
2200
|
+
assert.doesNotMatch(schema, /roles\s+/i);
|
|
2201
|
+
assert.doesNotMatch(schema, /permissions\s+/i);
|
|
2202
|
+
|
|
2203
|
+
const migrationPath = path.join(
|
|
2204
|
+
projectRoot,
|
|
2205
|
+
'apps',
|
|
2206
|
+
'api',
|
|
2207
|
+
'prisma',
|
|
2208
|
+
'migrations',
|
|
2209
|
+
'0002_accounts_core',
|
|
2210
|
+
'migration.sql',
|
|
2211
|
+
);
|
|
2212
|
+
assert.equal(fs.existsSync(migrationPath), true);
|
|
2213
|
+
|
|
2214
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
2215
|
+
assert.match(readme, /POST \/api\/auth\/register/);
|
|
2216
|
+
assert.match(readme, /POST \/api\/auth\/password-reset\/request/);
|
|
2217
|
+
assert.match(readme, /\/api\/users\/:id\/settings/);
|
|
2218
|
+
|
|
2219
|
+
const moduleDoc = fs.readFileSync(result.docsPath, 'utf8');
|
|
2220
|
+
assert.match(moduleDoc, /Status: implemented/);
|
|
2221
|
+
assert.match(moduleDoc, /db-adapter/);
|
|
2222
|
+
assert.match(moduleDoc, /owner-scoped/);
|
|
2223
|
+
} finally {
|
|
2224
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
it('detects and applies accounts-rbac compatibility sync explicitly', () => {
|
|
2229
|
+
const targetRoot = mkTmp('forgeon-module-accounts-rbac-');
|
|
2230
|
+
const projectRoot = path.join(targetRoot, 'demo-accounts-rbac');
|
|
2231
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
2232
|
+
|
|
2233
|
+
try {
|
|
2234
|
+
scaffoldProject({
|
|
2235
|
+
templateRoot,
|
|
2236
|
+
packageRoot,
|
|
2237
|
+
targetRoot: projectRoot,
|
|
2238
|
+
projectName: 'demo-accounts-rbac',
|
|
2239
|
+
frontend: 'react',
|
|
2240
|
+
db: 'prisma',
|
|
2241
|
+
dbPrismaEnabled: true,
|
|
2242
|
+
i18nEnabled: false,
|
|
2243
|
+
proxy: 'caddy',
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
addModule({
|
|
2247
|
+
moduleId: 'rbac',
|
|
2248
|
+
targetRoot: projectRoot,
|
|
2249
|
+
packageRoot,
|
|
2250
|
+
});
|
|
2251
|
+
addModule({
|
|
2252
|
+
moduleId: 'accounts',
|
|
2253
|
+
targetRoot: projectRoot,
|
|
2254
|
+
packageRoot,
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
const scan = scanIntegrations({
|
|
2258
|
+
targetRoot: projectRoot,
|
|
2259
|
+
relatedModuleId: 'accounts',
|
|
2260
|
+
});
|
|
2261
|
+
assert.equal(scan.groups.some((group) => group.id === 'accounts-rbac'), true);
|
|
2262
|
+
|
|
2263
|
+
const syncResult = syncIntegrations({
|
|
2264
|
+
targetRoot: projectRoot,
|
|
2265
|
+
packageRoot,
|
|
2266
|
+
groupIds: ['accounts-rbac'],
|
|
2267
|
+
});
|
|
2268
|
+
const claimsPair = syncResult.summary.find((item) => item.id === 'accounts-rbac');
|
|
2269
|
+
assert.ok(claimsPair);
|
|
2270
|
+
assert.equal(claimsPair.result.applied, true);
|
|
2271
|
+
|
|
2272
|
+
const contracts = fs.readFileSync(
|
|
2273
|
+
path.join(projectRoot, 'packages', 'accounts-contracts', 'src', 'index.ts'),
|
|
2274
|
+
'utf8',
|
|
2275
|
+
);
|
|
2276
|
+
assert.match(contracts, /roles\?: string\[\];/);
|
|
2277
|
+
assert.match(contracts, /permissions\?: string\[\];/);
|
|
2278
|
+
|
|
2279
|
+
const authTypes = fs.readFileSync(
|
|
2280
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.types.ts'),
|
|
2281
|
+
'utf8',
|
|
2282
|
+
);
|
|
2283
|
+
assert.match(authTypes, /roles\?: string\[\];/);
|
|
2284
|
+
assert.match(authTypes, /permissions\?: string\[\];/);
|
|
2285
|
+
|
|
2286
|
+
const readme = fs.readFileSync(path.join(projectRoot, 'README.md'), 'utf8');
|
|
2287
|
+
assert.match(readme, /forgeon:accounts:rbac:start/);
|
|
2288
|
+
assert.match(readme, /base accounts schema remains free of roles and permissions/);
|
|
2289
|
+
} finally {
|
|
2290
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2294
|
+
it('scans accounts-rbac compatibility when accounts and rbac are both installed', () => {
|
|
2295
|
+
const targetRoot = mkTmp('forgeon-module-accounts-rbac-scan-');
|
|
2296
|
+
const projectRoot = path.join(targetRoot, 'demo-accounts-rbac-scan');
|
|
2297
|
+
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
2298
|
+
|
|
2299
|
+
try {
|
|
2300
|
+
scaffoldProject({
|
|
2301
|
+
templateRoot,
|
|
2302
|
+
packageRoot,
|
|
2303
|
+
targetRoot: projectRoot,
|
|
2304
|
+
projectName: 'demo-accounts-rbac-scan',
|
|
2305
|
+
frontend: 'react',
|
|
2306
|
+
db: 'prisma',
|
|
2307
|
+
dbPrismaEnabled: true,
|
|
2308
|
+
i18nEnabled: false,
|
|
2309
|
+
proxy: 'caddy',
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
addModule({
|
|
2313
|
+
moduleId: 'accounts',
|
|
2314
|
+
targetRoot: projectRoot,
|
|
2315
|
+
packageRoot,
|
|
2316
|
+
});
|
|
2317
|
+
addModule({
|
|
2318
|
+
moduleId: 'rbac',
|
|
2319
|
+
targetRoot: projectRoot,
|
|
2320
|
+
packageRoot,
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2323
|
+
const scan = scanIntegrations({
|
|
2324
|
+
targetRoot: projectRoot,
|
|
2325
|
+
relatedModuleId: 'rbac',
|
|
2326
|
+
});
|
|
2327
|
+
const compatibilityGroup = scan.groups.find((group) => group.id === 'accounts-rbac');
|
|
2328
|
+
|
|
2329
|
+
assert.ok(compatibilityGroup);
|
|
2330
|
+
assert.deepEqual(compatibilityGroup.modules, ['accounts', 'rbac']);
|
|
2331
|
+
} finally {
|
|
2332
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
it('applies logger then accounts on db/i18n-disabled scaffold without breaking health controller syntax', () => {
|
|
2215
2336
|
const targetRoot = mkTmp('forgeon-module-jwt-nodb-noi18n-');
|
|
2216
2337
|
const projectRoot = path.join(targetRoot, 'demo-jwt-nodb-noi18n');
|
|
2217
2338
|
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
@@ -2235,7 +2356,7 @@ describe('addModule', () => {
|
|
|
2235
2356
|
packageRoot,
|
|
2236
2357
|
});
|
|
2237
2358
|
addModule({
|
|
2238
|
-
moduleId: '
|
|
2359
|
+
moduleId: 'accounts',
|
|
2239
2360
|
targetRoot: projectRoot,
|
|
2240
2361
|
packageRoot,
|
|
2241
2362
|
});
|
|
@@ -2259,7 +2380,7 @@ describe('addModule', () => {
|
|
|
2259
2380
|
}
|
|
2260
2381
|
});
|
|
2261
2382
|
|
|
2262
|
-
it('keeps health controller valid for add sequence
|
|
2383
|
+
it('keeps health controller valid for add sequence accounts -> logger -> swagger -> i18n -> db-prisma on db/i18n-disabled scaffold', () => {
|
|
2263
2384
|
const targetRoot = mkTmp('forgeon-module-seq-health-valid-');
|
|
2264
2385
|
const projectRoot = path.join(targetRoot, 'demo-seq-health-valid');
|
|
2265
2386
|
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
@@ -2277,7 +2398,7 @@ describe('addModule', () => {
|
|
|
2277
2398
|
proxy: 'caddy',
|
|
2278
2399
|
});
|
|
2279
2400
|
|
|
2280
|
-
for (const moduleId of ['
|
|
2401
|
+
for (const moduleId of ['accounts', 'logger', 'swagger', 'i18n', 'db-prisma']) {
|
|
2281
2402
|
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
2282
2403
|
}
|
|
2283
2404
|
|
|
@@ -2312,9 +2433,9 @@ describe('addModule', () => {
|
|
|
2312
2433
|
}
|
|
2313
2434
|
});
|
|
2314
2435
|
|
|
2315
|
-
it('applies swagger then
|
|
2316
|
-
const targetRoot = mkTmp('forgeon-module-
|
|
2317
|
-
const projectRoot = path.join(targetRoot, 'demo-
|
|
2436
|
+
it('applies swagger then accounts without forcing swagger dependency in accounts-api', () => {
|
|
2437
|
+
const targetRoot = mkTmp('forgeon-module-accounts-swagger-');
|
|
2438
|
+
const projectRoot = path.join(targetRoot, 'demo-accounts-swagger');
|
|
2318
2439
|
const templateRoot = path.join(packageRoot, 'templates', 'base');
|
|
2319
2440
|
|
|
2320
2441
|
try {
|
|
@@ -2322,7 +2443,7 @@ describe('addModule', () => {
|
|
|
2322
2443
|
templateRoot,
|
|
2323
2444
|
packageRoot,
|
|
2324
2445
|
targetRoot: projectRoot,
|
|
2325
|
-
projectName: 'demo-
|
|
2446
|
+
projectName: 'demo-accounts-swagger',
|
|
2326
2447
|
frontend: 'react',
|
|
2327
2448
|
db: 'prisma',
|
|
2328
2449
|
dbPrismaEnabled: false,
|
|
@@ -2336,15 +2457,15 @@ describe('addModule', () => {
|
|
|
2336
2457
|
packageRoot,
|
|
2337
2458
|
});
|
|
2338
2459
|
addModule({
|
|
2339
|
-
moduleId: '
|
|
2460
|
+
moduleId: 'accounts',
|
|
2340
2461
|
targetRoot: projectRoot,
|
|
2341
2462
|
packageRoot,
|
|
2342
2463
|
});
|
|
2343
2464
|
|
|
2344
|
-
const
|
|
2345
|
-
fs.readFileSync(path.join(projectRoot, 'packages', '
|
|
2465
|
+
const accountsApiPackage = JSON.parse(
|
|
2466
|
+
fs.readFileSync(path.join(projectRoot, 'packages', 'accounts-api', 'package.json'), 'utf8'),
|
|
2346
2467
|
);
|
|
2347
|
-
assert.equal(Object.hasOwn(
|
|
2468
|
+
assert.equal(Object.hasOwn(accountsApiPackage.dependencies ?? {}, '@nestjs/swagger'), false);
|
|
2348
2469
|
} finally {
|
|
2349
2470
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
2350
2471
|
}
|
|
@@ -2368,7 +2489,7 @@ describe('addModule', () => {
|
|
|
2368
2489
|
proxy: 'caddy',
|
|
2369
2490
|
});
|
|
2370
2491
|
|
|
2371
|
-
for (const moduleId of ['
|
|
2492
|
+
for (const moduleId of ['accounts', 'logger', 'swagger', 'rate-limit', 'i18n', 'db-prisma']) {
|
|
2372
2493
|
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
2373
2494
|
}
|
|
2374
2495
|
|
|
@@ -2405,7 +2526,7 @@ describe('addModule', () => {
|
|
|
2405
2526
|
proxy: 'caddy',
|
|
2406
2527
|
});
|
|
2407
2528
|
|
|
2408
|
-
for (const moduleId of ['
|
|
2529
|
+
for (const moduleId of ['accounts', 'logger', 'rate-limit', 'rbac', 'swagger', 'i18n', 'db-prisma']) {
|
|
2409
2530
|
addModule({ moduleId, targetRoot: projectRoot, packageRoot });
|
|
2410
2531
|
}
|
|
2411
2532
|
|
|
@@ -2503,3 +2624,13 @@ describe('addModule', () => {
|
|
|
2503
2624
|
});
|
|
2504
2625
|
|
|
2505
2626
|
|
|
2627
|
+
|
|
2628
|
+
|
|
2629
|
+
|
|
2630
|
+
|
|
2631
|
+
|
|
2632
|
+
|
|
2633
|
+
|
|
2634
|
+
|
|
2635
|
+
|
|
2636
|
+
|