create-forgeon 0.3.19 → 0.3.20
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 +1 -1
- package/src/cli/add-help.mjs +3 -2
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/modules/accounts.mjs +9 -18
- package/src/modules/dependencies.mjs +153 -4
- package/src/modules/dependencies.test.mjs +58 -0
- package/src/modules/executor.test.mjs +69 -40
- package/src/modules/files-image.mjs +6 -4
- package/src/modules/files.mjs +5 -6
- package/src/modules/idempotency.test.mjs +3 -2
- package/src/modules/registry.mjs +20 -0
- package/src/modules/shared/files-runtime-wiring.mjs +13 -10
- package/src/run-add-module.mjs +39 -26
- package/src/run-add-module.test.mjs +76 -0
- package/src/run-scan-integrations.mjs +1 -0
- package/templates/base/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
- package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
- package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
- package/templates/module-presets/files/packages/files/package.json +1 -0
- package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
- package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
- package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
- package/templates/module-presets/files/packages/files/src/index.ts +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
package/package.json
CHANGED
package/src/cli/add-help.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function printAddHelp() {
|
|
2
2
|
console.log(`create-forgeon add
|
|
3
3
|
|
|
4
|
-
Usage:
|
|
5
|
-
npx create-forgeon@latest add <module-id> [options]
|
|
4
|
+
Usage:
|
|
5
|
+
npx create-forgeon@latest add <module-id|all> [options]
|
|
6
6
|
|
|
7
7
|
Options:
|
|
8
8
|
--project <path> Target project path (default: current directory)
|
|
@@ -14,6 +14,7 @@ Options:
|
|
|
14
14
|
-h, --help Show this help
|
|
15
15
|
|
|
16
16
|
Note:
|
|
17
|
+
Use "add all" to install every implemented module and choose one provider per provider-choice capability.
|
|
17
18
|
Hard prerequisites are resolved explicitly.
|
|
18
19
|
Pair integrations remain explicit follow-up actions.
|
|
19
20
|
Run "pnpm forgeon:sync-integrations" in the target project after add-module steps.
|
package/src/core/docs.test.mjs
CHANGED
|
@@ -117,6 +117,7 @@ describe('generateDocs', () => {
|
|
|
117
117
|
assert.doesNotMatch(readme, /docs\/Agents\.md/);
|
|
118
118
|
assert.doesNotMatch(packageJson, /"create:forgeon"/);
|
|
119
119
|
assert.match(packageJson, /"forgeon:sync-integrations"/);
|
|
120
|
+
assert.match(packageJson, /"add-all": "npx create-forgeon@latest add all --project \."?/);
|
|
120
121
|
} finally {
|
|
121
122
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
122
123
|
}
|
|
@@ -26,6 +26,7 @@ function assertProxyPreset(targetRoot, proxy) {
|
|
|
26
26
|
assert.equal(fs.existsSync(path.join(dockerDir, 'compose.nginx.yml')), false);
|
|
27
27
|
assert.equal(fs.existsSync(path.join(dockerDir, 'compose.none.yml')), false);
|
|
28
28
|
assert.match(packageJson, /"forgeon:sync-integrations"/);
|
|
29
|
+
assert.match(packageJson, /"add-all": "npx create-forgeon@latest add all --project \."?/);
|
|
29
30
|
assert.doesNotMatch(packageJson, /"create:forgeon"/);
|
|
30
31
|
assert.match(compose, /^services:\s*$/m);
|
|
31
32
|
assert.match(compose, /^\s{2}api:\s*$/m);
|
package/src/modules/accounts.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 {
|
|
@@ -171,20 +171,20 @@ function patchAppModule(targetRoot) {
|
|
|
171
171
|
|
|
172
172
|
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
173
173
|
content = content.replace(
|
|
174
|
-
"import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
|
|
175
174
|
"import { ACCOUNTS_PERSISTENCE_PORT, authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
175
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
176
176
|
);
|
|
177
177
|
content = content.replace(
|
|
178
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule } from '@forgeon/accounts-api';",
|
|
178
179
|
"import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
179
|
-
"import { ACCOUNTS_PERSISTENCE_PORT, authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
180
180
|
);
|
|
181
181
|
content = ensureImportLine(
|
|
182
182
|
content,
|
|
183
|
-
"import {
|
|
183
|
+
"import { authConfig, authEnvSchema, ForgeonAccountsModule, UsersModule } from '@forgeon/accounts-api';",
|
|
184
184
|
);
|
|
185
|
-
content =
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
content = content.replace(
|
|
186
|
+
/^import \{ PrismaAccountsPersistenceStore \} from '\.\/accounts\/prisma-accounts-persistence\.store';\r?\n/m,
|
|
187
|
+
'',
|
|
188
188
|
);
|
|
189
189
|
content = content.replace(
|
|
190
190
|
/^import \{ ForgeonAccountsDbPrismaModule \} from '\.\/accounts\/forgeon-accounts-db-prisma\.module';\r?\n/m,
|
|
@@ -195,20 +195,12 @@ function patchAppModule(targetRoot) {
|
|
|
195
195
|
content = content.replace(/^\s*ForgeonAccountsDbPrismaModule,\r?\n/gm, '');
|
|
196
196
|
|
|
197
197
|
const accountsModuleLine = ` ForgeonAccountsModule.register({
|
|
198
|
-
imports: [DbPrismaModule],
|
|
199
|
-
providers: [
|
|
200
|
-
PrismaAccountsPersistenceStore,
|
|
201
|
-
{
|
|
202
|
-
provide: ACCOUNTS_PERSISTENCE_PORT,
|
|
203
|
-
useExisting: PrismaAccountsPersistenceStore,
|
|
204
|
-
},
|
|
205
|
-
],
|
|
206
198
|
users: UsersModule.register({}),
|
|
207
199
|
}),`;
|
|
208
200
|
|
|
209
201
|
if (content.includes(' ForgeonAccountsModule.register({')) {
|
|
210
202
|
content = content.replace(
|
|
211
|
-
/ {4}ForgeonAccountsModule\.register\(
|
|
203
|
+
/ {4}ForgeonAccountsModule\.register\([\s\S]*? {4}\}\),/m,
|
|
212
204
|
accountsModuleLine,
|
|
213
205
|
);
|
|
214
206
|
} else if (content.includes(' ForgeonI18nModule.register({')) {
|
|
@@ -221,7 +213,6 @@ function patchAppModule(targetRoot) {
|
|
|
221
213
|
|
|
222
214
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
223
215
|
}
|
|
224
|
-
|
|
225
216
|
function patchHealthController(targetRoot, probeTargets) {
|
|
226
217
|
patchHealthControllerServiceProbe(targetRoot, probeTargets, {
|
|
227
218
|
importLine: "import { AuthService } from '@forgeon/accounts-api';",
|
|
@@ -386,7 +377,6 @@ function patchReadme(targetRoot) {
|
|
|
386
377
|
export function applyAccountsModule({ packageRoot, targetRoot }) {
|
|
387
378
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-contracts'));
|
|
388
379
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'accounts-api'));
|
|
389
|
-
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'accounts'));
|
|
390
380
|
copyFromPreset(
|
|
391
381
|
packageRoot,
|
|
392
382
|
targetRoot,
|
|
@@ -429,3 +419,4 @@ export function applyAccountsModule({ packageRoot, targetRoot }) {
|
|
|
429
419
|
|
|
430
420
|
|
|
431
421
|
|
|
422
|
+
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { promptSelect } from '../cli/prompt-select.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getCapabilityProviders,
|
|
6
|
+
getRecommendedCapabilityProvider,
|
|
7
|
+
listModulePresets,
|
|
8
|
+
} from './registry.mjs';
|
|
5
9
|
|
|
6
10
|
function getPresetMap(presets) {
|
|
7
11
|
return new Map(presets.map((preset) => [preset.id, preset]));
|
|
@@ -94,15 +98,16 @@ async function selectProviderForCapability({
|
|
|
94
98
|
);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
const
|
|
98
|
-
|
|
101
|
+
const recommendedProvider = getRecommendedCapabilityProvider(capabilityId, providers) ?? providers[0];
|
|
102
|
+
const choices = providers.map((provider) => ({
|
|
103
|
+
label: provider.id === recommendedProvider.id ? `${provider.id} (Recommended)` : provider.id,
|
|
99
104
|
value: provider.id,
|
|
100
105
|
}));
|
|
101
106
|
choices.push({ label: 'Cancel', value: '__cancel' });
|
|
102
107
|
|
|
103
108
|
const picked = await promptSelectImpl({
|
|
104
109
|
message: `Module "${moduleId}" requires capability: ${capabilityId}`,
|
|
105
|
-
defaultValue:
|
|
110
|
+
defaultValue: recommendedProvider.id,
|
|
106
111
|
choices,
|
|
107
112
|
});
|
|
108
113
|
|
|
@@ -113,6 +118,53 @@ async function selectProviderForCapability({
|
|
|
113
118
|
return picked;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
function getAmbiguousCapabilityProviders(presets = listModulePresets()) {
|
|
122
|
+
const providersByCapability = new Map();
|
|
123
|
+
|
|
124
|
+
for (const preset of presets) {
|
|
125
|
+
if (preset.implemented === false || !Array.isArray(preset.provides)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const capabilityId of preset.provides) {
|
|
130
|
+
const providers = providersByCapability.get(capabilityId) ?? [];
|
|
131
|
+
providers.push(preset);
|
|
132
|
+
providersByCapability.set(capabilityId, providers);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new Map(
|
|
137
|
+
[...providersByCapability.entries()].filter(([, providers]) => providers.length > 1),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getAllModulesInstallTargets({ presets = listModulePresets(), selectedProviders = {} }) {
|
|
142
|
+
const ambiguousCapabilityProviders = getAmbiguousCapabilityProviders(presets);
|
|
143
|
+
|
|
144
|
+
return presets
|
|
145
|
+
.filter((preset) => preset.implemented !== false)
|
|
146
|
+
.filter((preset) => {
|
|
147
|
+
const providedCapabilities = Array.isArray(preset.provides) ? preset.provides : [];
|
|
148
|
+
const ambiguousCapabilities = providedCapabilities.filter((capabilityId) =>
|
|
149
|
+
ambiguousCapabilityProviders.has(capabilityId),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (ambiguousCapabilities.length === 0) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const hasNonAmbiguousCapability = providedCapabilities.some(
|
|
157
|
+
(capabilityId) => !ambiguousCapabilityProviders.has(capabilityId),
|
|
158
|
+
);
|
|
159
|
+
if (hasNonAmbiguousCapability) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ambiguousCapabilities.some((capabilityId) => selectedProviders[capabilityId] === preset.id);
|
|
164
|
+
})
|
|
165
|
+
.map((preset) => preset.id);
|
|
166
|
+
}
|
|
167
|
+
|
|
116
168
|
export async function resolveModuleInstallPlan({
|
|
117
169
|
moduleId,
|
|
118
170
|
targetRoot,
|
|
@@ -229,6 +281,103 @@ export async function resolveModuleInstallPlan({
|
|
|
229
281
|
};
|
|
230
282
|
}
|
|
231
283
|
|
|
284
|
+
export async function resolveAllModulesInstallPlan({
|
|
285
|
+
targetRoot,
|
|
286
|
+
presets = listModulePresets(),
|
|
287
|
+
providerSelections = {},
|
|
288
|
+
promptSelectImpl = promptSelect,
|
|
289
|
+
isInteractive = process.stdin.isTTY && process.stdout.isTTY,
|
|
290
|
+
}) {
|
|
291
|
+
const ambiguousCapabilityProviders = getAmbiguousCapabilityProviders(presets);
|
|
292
|
+
const selectedProviders = { ...providerSelections };
|
|
293
|
+
|
|
294
|
+
for (const [capabilityId, providers] of ambiguousCapabilityProviders.entries()) {
|
|
295
|
+
const explicitProvider = selectedProviders[capabilityId];
|
|
296
|
+
if (explicitProvider) {
|
|
297
|
+
const matchedProvider = providers.find((provider) => provider.id === explicitProvider);
|
|
298
|
+
if (!matchedProvider) {
|
|
299
|
+
throw createResolutionError('all', { type: 'capability', id: capabilityId }, providers, explicitProvider);
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (isInteractive) {
|
|
305
|
+
const pickedProvider = await selectProviderForCapability({
|
|
306
|
+
moduleId: 'all',
|
|
307
|
+
capabilityId,
|
|
308
|
+
providers,
|
|
309
|
+
promptSelectImpl,
|
|
310
|
+
});
|
|
311
|
+
if (!pickedProvider) {
|
|
312
|
+
return {
|
|
313
|
+
cancelled: true,
|
|
314
|
+
moduleSequence: [],
|
|
315
|
+
selectedProviders: {},
|
|
316
|
+
rootModuleIds: [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
selectedProviders[capabilityId] = pickedProvider;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const recommendedProvider = getRecommendedCapabilityProvider(capabilityId, providers);
|
|
324
|
+
if (!recommendedProvider) {
|
|
325
|
+
throw createResolutionError('all', { type: 'capability', id: capabilityId }, providers);
|
|
326
|
+
}
|
|
327
|
+
selectedProviders[capabilityId] = recommendedProvider.id;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const rootModuleIds = getAllModulesInstallTargets({
|
|
331
|
+
presets,
|
|
332
|
+
selectedProviders,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const moduleSequence = [];
|
|
336
|
+
const plannedSet = new Set();
|
|
337
|
+
let accumulatedProviderSelections = { ...selectedProviders };
|
|
338
|
+
|
|
339
|
+
for (const moduleId of rootModuleIds) {
|
|
340
|
+
const plan = await resolveModuleInstallPlan({
|
|
341
|
+
moduleId,
|
|
342
|
+
targetRoot,
|
|
343
|
+
presets,
|
|
344
|
+
withRequired: true,
|
|
345
|
+
providerSelections: accumulatedProviderSelections,
|
|
346
|
+
promptSelectImpl,
|
|
347
|
+
isInteractive: false,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (plan.cancelled) {
|
|
351
|
+
return {
|
|
352
|
+
cancelled: true,
|
|
353
|
+
moduleSequence,
|
|
354
|
+
selectedProviders: accumulatedProviderSelections,
|
|
355
|
+
rootModuleIds,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
accumulatedProviderSelections = {
|
|
360
|
+
...accumulatedProviderSelections,
|
|
361
|
+
...plan.selectedProviders,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
for (const plannedModuleId of plan.moduleSequence) {
|
|
365
|
+
if (plannedSet.has(plannedModuleId)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
plannedSet.add(plannedModuleId);
|
|
369
|
+
moduleSequence.push(plannedModuleId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
cancelled: false,
|
|
375
|
+
moduleSequence,
|
|
376
|
+
selectedProviders: accumulatedProviderSelections,
|
|
377
|
+
rootModuleIds,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
232
381
|
function requirementIsMissing(requirement, installedModules, providedCapabilities) {
|
|
233
382
|
if (requirement.type === 'capability') {
|
|
234
383
|
return !providedCapabilities.has(requirement.id);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
detectInstalledModules,
|
|
9
9
|
getPendingOptionalIntegrations,
|
|
10
10
|
getPendingRecommendedCompanions,
|
|
11
|
+
resolveAllModulesInstallPlan,
|
|
11
12
|
resolveModuleInstallPlan,
|
|
12
13
|
} from './dependencies.mjs';
|
|
13
14
|
|
|
@@ -361,6 +362,63 @@ describe('module dependency helpers', () => {
|
|
|
361
362
|
}
|
|
362
363
|
});
|
|
363
364
|
|
|
365
|
+
it('builds a full install plan with the recommended provider for ambiguous capabilities', async () => {
|
|
366
|
+
const targetRoot = mkTmp('forgeon-deps-all-plan-');
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const result = await resolveAllModulesInstallPlan({
|
|
370
|
+
targetRoot,
|
|
371
|
+
presets: TEST_PRESETS,
|
|
372
|
+
isInteractive: false,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
assert.equal(result.cancelled, false);
|
|
376
|
+
assert.deepEqual(result.moduleSequence, [
|
|
377
|
+
'db-prisma',
|
|
378
|
+
'accounts',
|
|
379
|
+
'rbac',
|
|
380
|
+
'files-local',
|
|
381
|
+
'files',
|
|
382
|
+
'files-access',
|
|
383
|
+
'files-quotas',
|
|
384
|
+
'files-image',
|
|
385
|
+
'queue',
|
|
386
|
+
'scheduler',
|
|
387
|
+
]);
|
|
388
|
+
assert.deepEqual(result.selectedProviders, {
|
|
389
|
+
'files-storage-adapter': 'files-local',
|
|
390
|
+
'db-adapter': 'db-prisma',
|
|
391
|
+
'files-runtime': 'files',
|
|
392
|
+
'queue-runtime': 'queue',
|
|
393
|
+
});
|
|
394
|
+
assert.equal(result.rootModuleIds.includes('files-s3'), false);
|
|
395
|
+
} finally {
|
|
396
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('builds a full install plan with an explicit provider override', async () => {
|
|
401
|
+
const targetRoot = mkTmp('forgeon-deps-all-provider-');
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const result = await resolveAllModulesInstallPlan({
|
|
405
|
+
targetRoot,
|
|
406
|
+
presets: TEST_PRESETS,
|
|
407
|
+
isInteractive: false,
|
|
408
|
+
providerSelections: {
|
|
409
|
+
'files-storage-adapter': 'files-s3',
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
assert.equal(result.cancelled, false);
|
|
414
|
+
assert.equal(result.moduleSequence.includes('files-s3'), true);
|
|
415
|
+
assert.equal(result.moduleSequence.includes('files-local'), false);
|
|
416
|
+
assert.equal(result.selectedProviders['files-storage-adapter'], 'files-s3');
|
|
417
|
+
} finally {
|
|
418
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
364
422
|
it('reports pending optional integrations for accounts when rbac is missing', () => {
|
|
365
423
|
const targetRoot = mkTmp('forgeon-deps-optional-');
|
|
366
424
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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';
|
|
@@ -239,10 +239,10 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
|
239
239
|
assert.match(appModule, /filesConfig/);
|
|
240
240
|
assert.match(appModule, /filesEnvSchema/);
|
|
241
241
|
assert.match(appModule, /ForgeonFilesModule\.register\(\{/);
|
|
242
|
-
assert.
|
|
242
|
+
assert.doesNotMatch(appModule, /ForgeonFilesDbPrismaModule/);
|
|
243
243
|
if (expectedStorageDriver === 's3') {
|
|
244
244
|
assert.match(appModule, /ForgeonFilesS3StorageModule/);
|
|
245
|
-
assert.doesNotMatch(appModule, /
|
|
245
|
+
assert.doesNotMatch(appModule, /ForgeonFilesLocalStorageModule/);
|
|
246
246
|
} else {
|
|
247
247
|
assert.match(appModule, /ForgeonFilesLocalStorageModule/);
|
|
248
248
|
}
|
|
@@ -285,14 +285,16 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
|
285
285
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
286
286
|
'utf8',
|
|
287
287
|
);
|
|
288
|
-
assert.match(filesService, /
|
|
288
|
+
assert.match(filesService, /FilesStore/);
|
|
289
289
|
assert.match(filesService, /FILES_STORAGE_ADAPTER/);
|
|
290
|
+
assert.match(filesService, /requireStorageAdapter/);
|
|
290
291
|
assert.match(filesService, /getOrCreateBlob/);
|
|
291
292
|
assert.match(filesService, /cleanupReferencedBlobs/);
|
|
292
293
|
assert.match(filesService, /isUniqueConstraintError/);
|
|
293
294
|
assert.match(filesService, /storageAdapter\.put/);
|
|
294
|
-
assert.match(filesService, /
|
|
295
|
-
assert.match(filesService, /
|
|
295
|
+
assert.match(filesService, /filesStore\.createBlob/);
|
|
296
|
+
assert.match(filesService, /filesStore\.deleteBlobIfUnreferenced/);
|
|
297
|
+
assert.doesNotMatch(filesService, /FILES_PERSISTENCE_PORT/);
|
|
296
298
|
assert.doesNotMatch(filesService, /PrismaService/);
|
|
297
299
|
assert.doesNotMatch(filesService, /@aws-sdk\/client-s3/);
|
|
298
300
|
|
|
@@ -300,38 +302,34 @@ function assertFilesWiring(projectRoot, expectedStorageDriver = 'local') {
|
|
|
300
302
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.ports.ts'),
|
|
301
303
|
'utf8',
|
|
302
304
|
);
|
|
303
|
-
assert.
|
|
305
|
+
assert.doesNotMatch(filesPorts, /FILES_PERSISTENCE_PORT/);
|
|
306
|
+
assert.doesNotMatch(filesPorts, /interface FilesPersistencePort/);
|
|
304
307
|
assert.match(filesPorts, /FILES_STORAGE_ADAPTER/);
|
|
305
|
-
assert.match(filesPorts, /interface FilesPersistencePort/);
|
|
306
308
|
assert.match(filesPorts, /interface FilesStorageAdapter/);
|
|
307
309
|
|
|
310
|
+
const filesStore = fs.readFileSync(
|
|
311
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.store.ts'),
|
|
312
|
+
'utf8',
|
|
313
|
+
);
|
|
314
|
+
assert.match(filesStore, /PrismaService/);
|
|
315
|
+
assert.match(filesStore, /fileBlob\.deleteMany/);
|
|
316
|
+
|
|
308
317
|
const filesModule = fs.readFileSync(
|
|
309
318
|
path.join(projectRoot, 'packages', 'files', 'src', 'forgeon-files.module.ts'),
|
|
310
319
|
'utf8',
|
|
311
320
|
);
|
|
312
321
|
assert.match(filesModule, /ForgeonFilesModuleOptions/);
|
|
313
322
|
assert.match(filesModule, /static register\(options: ForgeonFilesModuleOptions = \{\}\)/);
|
|
314
|
-
assert.match(filesModule, /
|
|
315
|
-
assert.match(filesModule, /
|
|
323
|
+
assert.match(filesModule, /DbPrismaModule/);
|
|
324
|
+
assert.match(filesModule, /FilesStore/);
|
|
325
|
+
assert.doesNotMatch(filesModule, /FILES_PERSISTENCE_PORT/);
|
|
316
326
|
|
|
317
327
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
318
|
-
assert.
|
|
328
|
+
assert.match(filesPackage, /@forgeon\/db-prisma/);
|
|
319
329
|
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
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/);
|
|
330
|
+
const prismaFilesDir = path.join(projectRoot, 'apps', 'api', 'src', 'files');
|
|
331
|
+
assert.equal(fs.existsSync(path.join(prismaFilesDir, 'prisma-files-persistence.store.ts')), false);
|
|
332
|
+
assert.equal(fs.existsSync(path.join(prismaFilesDir, 'forgeon-files-db-prisma.module.ts')), false);
|
|
335
333
|
|
|
336
334
|
assertWebProbeShell(projectRoot);
|
|
337
335
|
const probesTs = readWebProbes(projectRoot);
|
|
@@ -619,14 +617,19 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
619
617
|
);
|
|
620
618
|
assert.match(filesModule, /ForgeonFilesImageModule/);
|
|
621
619
|
|
|
622
|
-
const filesService = fs.readFileSync(
|
|
623
|
-
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
624
|
-
'utf8',
|
|
625
|
-
);
|
|
626
|
-
assert.match(filesService, /FilesImageService/);
|
|
627
|
-
assert.match(filesService, /filesImageService\.sanitizeForStorage/);
|
|
628
|
-
assert.match(filesService, /sanitizeForStorage\({/);
|
|
629
|
-
assert.match(filesService, /auditContext: input\.auditContext/);
|
|
620
|
+
const filesService = fs.readFileSync(
|
|
621
|
+
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
622
|
+
'utf8',
|
|
623
|
+
);
|
|
624
|
+
assert.match(filesService, /FilesImageService/);
|
|
625
|
+
assert.match(filesService, /filesImageService\.sanitizeForStorage/);
|
|
626
|
+
assert.match(filesService, /sanitizeForStorage\({/);
|
|
627
|
+
assert.match(filesService, /auditContext: input\.auditContext/);
|
|
628
|
+
assert.equal(
|
|
629
|
+
(filesService.match(/protected normalizeFileName\(originalName: string, extension: string, suffix\?: string\): string \{/g) ?? [])
|
|
630
|
+
.length,
|
|
631
|
+
1,
|
|
632
|
+
);
|
|
630
633
|
|
|
631
634
|
const filesController = fs.readFileSync(
|
|
632
635
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
@@ -717,11 +720,9 @@ function assertAccountsWiring(projectRoot) {
|
|
|
717
720
|
assert.match(appModule, /authConfig/);
|
|
718
721
|
assert.match(appModule, /authEnvSchema/);
|
|
719
722
|
assert.match(appModule, /ForgeonAccountsModule\.register\(\{/);
|
|
720
|
-
assert.match(appModule, /imports: \[DbPrismaModule\]/);
|
|
721
|
-
assert.match(appModule, /PrismaAccountsPersistenceStore/);
|
|
722
|
-
assert.match(appModule, /provide: ACCOUNTS_PERSISTENCE_PORT/);
|
|
723
|
-
assert.match(appModule, /useExisting: PrismaAccountsPersistenceStore/);
|
|
724
723
|
assert.match(appModule, /UsersModule\.register\(\{\}\)/);
|
|
724
|
+
assert.doesNotMatch(appModule, /ACCOUNTS_PERSISTENCE_PORT/);
|
|
725
|
+
assert.doesNotMatch(appModule, /PrismaAccountsPersistenceStore/);
|
|
725
726
|
assert.doesNotMatch(appModule, /AUTH_REFRESH_TOKEN_STORE/);
|
|
726
727
|
|
|
727
728
|
const healthController = fs.readFileSync(
|
|
@@ -773,6 +774,33 @@ function assertAccountsWiring(projectRoot) {
|
|
|
773
774
|
);
|
|
774
775
|
assert.match(authServiceSource, /import type \{ RegisterRequest \} from '@forgeon\/accounts-contracts';/);
|
|
775
776
|
|
|
777
|
+
const authCoreSource = fs.readFileSync(
|
|
778
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth-core.service.ts'),
|
|
779
|
+
'utf8',
|
|
780
|
+
);
|
|
781
|
+
assert.match(authCoreSource, /AuthStore/);
|
|
782
|
+
assert.doesNotMatch(authCoreSource, /ACCOUNTS_PERSISTENCE_PORT/);
|
|
783
|
+
|
|
784
|
+
const accountsApiPackage = fs.readFileSync(
|
|
785
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'package.json'),
|
|
786
|
+
'utf8',
|
|
787
|
+
);
|
|
788
|
+
assert.match(accountsApiPackage, /@forgeon\/db-prisma/);
|
|
789
|
+
|
|
790
|
+
const authStoreSource = fs.readFileSync(
|
|
791
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'auth.store.ts'),
|
|
792
|
+
'utf8',
|
|
793
|
+
);
|
|
794
|
+
assert.match(authStoreSource, /PrismaService/);
|
|
795
|
+
assert.match(authStoreSource, /authRefreshToken\.updateMany/);
|
|
796
|
+
|
|
797
|
+
const usersStoreSource = fs.readFileSync(
|
|
798
|
+
path.join(projectRoot, 'packages', 'accounts-api', 'src', 'users.store.ts'),
|
|
799
|
+
'utf8',
|
|
800
|
+
);
|
|
801
|
+
assert.match(usersStoreSource, /PrismaService/);
|
|
802
|
+
assert.match(usersStoreSource, /userProfile\.upsert/);
|
|
803
|
+
|
|
776
804
|
const prismaStorePath = path.join(
|
|
777
805
|
projectRoot,
|
|
778
806
|
'apps',
|
|
@@ -781,7 +809,7 @@ function assertAccountsWiring(projectRoot) {
|
|
|
781
809
|
'accounts',
|
|
782
810
|
'prisma-accounts-persistence.store.ts',
|
|
783
811
|
);
|
|
784
|
-
assert.equal(fs.existsSync(prismaStorePath),
|
|
812
|
+
assert.equal(fs.existsSync(prismaStorePath), false);
|
|
785
813
|
}
|
|
786
814
|
function stripDbPrismaArtifacts(projectRoot) {
|
|
787
815
|
const dbPackageDir = path.join(projectRoot, 'packages', 'db-prisma');
|
|
@@ -1550,7 +1578,7 @@ describe('addModule', () => {
|
|
|
1550
1578
|
|
|
1551
1579
|
const appModule = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'src', 'app.module.ts'), 'utf8');
|
|
1552
1580
|
assert.match(appModule, /ForgeonFilesS3StorageModule/);
|
|
1553
|
-
assert.doesNotMatch(appModule, /
|
|
1581
|
+
assert.doesNotMatch(appModule, /ForgeonFilesLocalStorageModule/);
|
|
1554
1582
|
|
|
1555
1583
|
const filesService = fs.readFileSync(
|
|
1556
1584
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.service.ts'),
|
|
@@ -2650,3 +2678,4 @@ describe('addModule', () => {
|
|
|
2650
2678
|
|
|
2651
2679
|
|
|
2652
2680
|
|
|
2681
|
+
|
|
@@ -258,9 +258,10 @@ function patchFilesService(targetRoot) {
|
|
|
258
258
|
}`,
|
|
259
259
|
);
|
|
260
260
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
if (!content.includes('protected normalizeFileName(originalName: string, extension: string, suffix?: string): string')) {
|
|
262
|
+
content = content.replace(
|
|
263
|
+
` private extensionFromMime(mimeType: string): string | null {`,
|
|
264
|
+
` protected normalizeFileName(originalName: string, extension: string, suffix?: string): string {
|
|
264
265
|
const parsed = path.parse(originalName);
|
|
265
266
|
const safeExtension = extension.startsWith('.') ? extension : \`.\${extension}\`;
|
|
266
267
|
const base = suffix ? \`\${parsed.name}-\${suffix}\` : parsed.name;
|
|
@@ -268,7 +269,8 @@ function patchFilesService(targetRoot) {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
private extensionFromMime(mimeType: string): string | null {`,
|
|
271
|
-
|
|
272
|
+
);
|
|
273
|
+
}
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
package/src/modules/files.mjs
CHANGED
|
@@ -170,9 +170,9 @@ function patchAppModule(targetRoot) {
|
|
|
170
170
|
content,
|
|
171
171
|
"import { filesConfig, filesEnvSchema, ForgeonFilesModule } from '@forgeon/files';",
|
|
172
172
|
);
|
|
173
|
-
content =
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
content = content.replace(
|
|
174
|
+
/^import \{ ForgeonFilesDbPrismaModule \} from '\.\/files\/forgeon-files-db-prisma\.module';\r?\n/m,
|
|
175
|
+
'',
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
if (fs.existsSync(path.join(targetRoot, 'packages', 'files-local', 'package.json'))) {
|
|
@@ -188,7 +188,6 @@ function patchAppModule(targetRoot) {
|
|
|
188
188
|
|
|
189
189
|
fs.writeFileSync(filePath, `${content.trimEnd()}\n`, 'utf8');
|
|
190
190
|
}
|
|
191
|
-
|
|
192
191
|
function patchApiDockerfile(targetRoot) {
|
|
193
192
|
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
194
193
|
if (!fs.existsSync(dockerfilePath)) {
|
|
@@ -378,7 +377,7 @@ function patchReadme(targetRoot) {
|
|
|
378
377
|
|
|
379
378
|
const section = `## Files Module
|
|
380
379
|
|
|
381
|
-
The files module adds runtime file endpoints with
|
|
380
|
+
The files module adds runtime file endpoints with Prisma-backed metadata and storage-driver-aware behavior.
|
|
382
381
|
|
|
383
382
|
What it currently adds:
|
|
384
383
|
- \`@forgeon/files\` package with:
|
|
@@ -425,7 +424,6 @@ Key env:
|
|
|
425
424
|
|
|
426
425
|
export function applyFilesModule({ packageRoot, targetRoot }) {
|
|
427
426
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files'));
|
|
428
|
-
copyFromPreset(packageRoot, targetRoot, path.join('apps', 'api', 'src', 'files'));
|
|
429
427
|
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'files' });
|
|
430
428
|
|
|
431
429
|
|
|
@@ -456,3 +454,4 @@ export function applyFilesModule({ packageRoot, targetRoot }) {
|
|
|
456
454
|
}
|
|
457
455
|
|
|
458
456
|
|
|
457
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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';
|
|
@@ -91,7 +91,7 @@ describe('addModule idempotency', () => {
|
|
|
91
91
|
fs.existsSync(
|
|
92
92
|
path.join(projectRoot, 'apps', 'api', 'src', 'accounts', 'prisma-accounts-persistence.store.ts'),
|
|
93
93
|
),
|
|
94
|
-
|
|
94
|
+
false,
|
|
95
95
|
);
|
|
96
96
|
assert.match(
|
|
97
97
|
readFile(path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts')),
|
|
@@ -178,3 +178,4 @@ describe('addModule idempotency', () => {
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
|