create-forgeon 0.3.14 → 0.3.16
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/core/docs.test.mjs +79 -40
- package/src/core/scaffold.test.mjs +99 -0
- package/src/modules/db-prisma.mjs +23 -55
- package/src/modules/executor.test.mjs +2575 -2419
- package/src/modules/files-access.mjs +27 -98
- package/src/modules/files-image.mjs +26 -100
- package/src/modules/files-quotas.mjs +67 -87
- package/src/modules/files.mjs +35 -104
- package/src/modules/i18n.mjs +17 -121
- package/src/modules/idempotency.test.mjs +174 -0
- package/src/modules/jwt-auth.mjs +90 -209
- package/src/modules/logger.mjs +0 -9
- package/src/modules/probes.test.mjs +202 -0
- package/src/modules/queue.mjs +325 -412
- package/src/modules/rate-limit.mjs +22 -66
- package/src/modules/rbac.mjs +27 -67
- package/src/modules/scheduler.mjs +44 -167
- package/src/modules/shared/nest-runtime-wiring.mjs +110 -0
- package/src/modules/shared/probes.mjs +235 -0
- package/src/modules/sync-integrations.mjs +54 -21
- package/src/modules/sync-integrations.test.mjs +220 -0
- package/src/run-add-module.test.mjs +153 -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 +55 -11
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +12 -4
- 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/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-presets/logger/packages/logger/src/http-logging.interceptor.ts +0 -94
package/src/modules/queue.mjs
CHANGED
|
@@ -1,412 +1,325 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
-
import {
|
|
5
|
-
ensureBuildSteps,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
]
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
redis:
|
|
327
|
-
condition: service_healthy
|
|
328
|
-
`,
|
|
329
|
-
);
|
|
330
|
-
apiBlock =
|
|
331
|
-
withDependsOn === apiBlock
|
|
332
|
-
? `${apiBlock.trimEnd()}\n depends_on:\n redis:\n condition: service_healthy\n`
|
|
333
|
-
: withDependsOn;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
content = content.replace(apiBlockMatch[0], apiBlock);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function patchReadme(targetRoot) {
|
|
343
|
-
const readmePath = path.join(targetRoot, 'README.md');
|
|
344
|
-
if (!fs.existsSync(readmePath)) {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const marker = '## Queue Module';
|
|
349
|
-
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
350
|
-
if (content.includes(marker)) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const section = `## Queue Module
|
|
355
|
-
|
|
356
|
-
The queue add-module provides an async job runtime baseline backed by Redis.
|
|
357
|
-
|
|
358
|
-
What it adds:
|
|
359
|
-
- \`@forgeon/queue\` package
|
|
360
|
-
- typed queue env config (module-owned)
|
|
361
|
-
- queue probe endpoint: \`GET /api/health/queue\`
|
|
362
|
-
- web probe button for quick runtime verification
|
|
363
|
-
- Redis service wiring in Docker Compose
|
|
364
|
-
|
|
365
|
-
Configuration (env):
|
|
366
|
-
- \`QUEUE_ENABLED=true\`
|
|
367
|
-
- \`QUEUE_REDIS_URL=redis://localhost:6379\`
|
|
368
|
-
- \`QUEUE_PREFIX=forgeon\`
|
|
369
|
-
- \`QUEUE_DEFAULT_ATTEMPTS=3\`
|
|
370
|
-
- \`QUEUE_DEFAULT_BACKOFF_MS=1000\`
|
|
371
|
-
|
|
372
|
-
Operational notes:
|
|
373
|
-
- this stage is the queue foundation (runtime + connectivity)
|
|
374
|
-
- cron orchestration is available through the scheduler module
|
|
375
|
-
- job execution workers remain intentionally deferred to a later module`;
|
|
376
|
-
|
|
377
|
-
if (content.includes('## Prisma In Docker Start')) {
|
|
378
|
-
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
379
|
-
} else {
|
|
380
|
-
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export function applyQueueModule({ packageRoot, targetRoot }) {
|
|
387
|
-
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'queue'));
|
|
388
|
-
|
|
389
|
-
patchApiPackage(targetRoot);
|
|
390
|
-
patchAppModule(targetRoot);
|
|
391
|
-
patchHealthController(targetRoot);
|
|
392
|
-
patchWebApp(targetRoot);
|
|
393
|
-
patchApiDockerfile(targetRoot);
|
|
394
|
-
patchCompose(targetRoot);
|
|
395
|
-
patchReadme(targetRoot);
|
|
396
|
-
|
|
397
|
-
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
398
|
-
'QUEUE_ENABLED=true',
|
|
399
|
-
'QUEUE_REDIS_URL=redis://localhost:6379',
|
|
400
|
-
'QUEUE_PREFIX=forgeon',
|
|
401
|
-
'QUEUE_DEFAULT_ATTEMPTS=3',
|
|
402
|
-
'QUEUE_DEFAULT_BACKOFF_MS=1000',
|
|
403
|
-
]);
|
|
404
|
-
upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
|
|
405
|
-
'QUEUE_ENABLED=true',
|
|
406
|
-
'QUEUE_REDIS_URL=redis://redis:6379',
|
|
407
|
-
'QUEUE_PREFIX=forgeon',
|
|
408
|
-
'QUEUE_DEFAULT_ATTEMPTS=3',
|
|
409
|
-
'QUEUE_DEFAULT_BACKOFF_MS=1000',
|
|
410
|
-
]);
|
|
411
|
-
}
|
|
412
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
|
+
import {
|
|
5
|
+
ensureBuildSteps,
|
|
6
|
+
ensureDependency,
|
|
7
|
+
ensureLineAfter,
|
|
8
|
+
ensureLineBefore,
|
|
9
|
+
upsertEnvLines,
|
|
10
|
+
} from './shared/patch-utils.mjs';
|
|
11
|
+
import { patchAppModuleRegistration, patchHealthControllerServiceProbe } from './shared/nest-runtime-wiring.mjs';
|
|
12
|
+
import { ensureWebProbeDefinition, resolveProbeTargets } from './shared/probes.mjs';
|
|
13
|
+
|
|
14
|
+
function copyFromPreset(packageRoot, targetRoot, relativePath) {
|
|
15
|
+
const source = path.join(packageRoot, 'templates', 'module-presets', 'queue', relativePath);
|
|
16
|
+
if (!fs.existsSync(source)) {
|
|
17
|
+
throw new Error(`Missing queue preset template: ${source}`);
|
|
18
|
+
}
|
|
19
|
+
const destination = path.join(targetRoot, relativePath);
|
|
20
|
+
copyRecursive(source, destination);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function patchApiPackage(targetRoot) {
|
|
24
|
+
const packagePath = path.join(targetRoot, 'apps', 'api', 'package.json');
|
|
25
|
+
if (!fs.existsSync(packagePath)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
30
|
+
ensureDependency(packageJson, '@forgeon/queue', 'workspace:*');
|
|
31
|
+
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/queue build']);
|
|
32
|
+
writeJson(packagePath, packageJson);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function patchAppModule(targetRoot) {
|
|
36
|
+
patchAppModuleRegistration(targetRoot, {
|
|
37
|
+
importLine: "import { ForgeonQueueModule, queueConfig, queueEnvSchema } from '@forgeon/queue';",
|
|
38
|
+
loadItem: 'queueConfig',
|
|
39
|
+
envSchema: 'queueEnvSchema',
|
|
40
|
+
moduleLine: ' ForgeonQueueModule,',
|
|
41
|
+
beforeAnchors: [
|
|
42
|
+
' ForgeonI18nModule.register({',
|
|
43
|
+
' ForgeonAuthModule.register({',
|
|
44
|
+
' ForgeonAuthModule.register(),',
|
|
45
|
+
],
|
|
46
|
+
afterAnchors: [
|
|
47
|
+
' DbPrismaModule,',
|
|
48
|
+
' ForgeonLoggerModule,',
|
|
49
|
+
' ForgeonSwaggerModule,',
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function patchHealthController(targetRoot, probeTargets) {
|
|
55
|
+
patchHealthControllerServiceProbe(targetRoot, probeTargets, {
|
|
56
|
+
importLine: "import { QueueService } from '@forgeon/queue';",
|
|
57
|
+
constructorMember: 'private readonly queueService: QueueService',
|
|
58
|
+
routePath: 'queue',
|
|
59
|
+
methodName: 'getQueueProbe',
|
|
60
|
+
serviceCall: 'this.queueService.getProbeStatus()',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function registerWebProbe(targetRoot, probeTargets) {
|
|
65
|
+
ensureWebProbeDefinition({
|
|
66
|
+
targetRoot,
|
|
67
|
+
probeTargets,
|
|
68
|
+
definition: {
|
|
69
|
+
id: 'queue',
|
|
70
|
+
title: 'Queue',
|
|
71
|
+
buttonLabel: 'Check queue health',
|
|
72
|
+
resultTitle: 'Queue probe response',
|
|
73
|
+
path: '/health/queue',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function patchApiDockerfile(targetRoot) {
|
|
79
|
+
const dockerfilePath = path.join(targetRoot, 'apps', 'api', 'Dockerfile');
|
|
80
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let content = fs.readFileSync(dockerfilePath, 'utf8').replace(/\r\n/g, '\n');
|
|
85
|
+
const packageAnchors = [
|
|
86
|
+
'COPY packages/files-image/package.json packages/files-image/package.json',
|
|
87
|
+
'COPY packages/files-quotas/package.json packages/files-quotas/package.json',
|
|
88
|
+
'COPY packages/files-access/package.json packages/files-access/package.json',
|
|
89
|
+
'COPY packages/files-s3/package.json packages/files-s3/package.json',
|
|
90
|
+
'COPY packages/files-local/package.json packages/files-local/package.json',
|
|
91
|
+
'COPY packages/files/package.json packages/files/package.json',
|
|
92
|
+
'COPY packages/auth-api/package.json packages/auth-api/package.json',
|
|
93
|
+
'COPY packages/rbac/package.json packages/rbac/package.json',
|
|
94
|
+
'COPY packages/rate-limit/package.json packages/rate-limit/package.json',
|
|
95
|
+
'COPY packages/logger/package.json packages/logger/package.json',
|
|
96
|
+
'COPY packages/swagger/package.json packages/swagger/package.json',
|
|
97
|
+
'COPY packages/i18n/package.json packages/i18n/package.json',
|
|
98
|
+
'COPY packages/db-prisma/package.json packages/db-prisma/package.json',
|
|
99
|
+
'COPY packages/core/package.json packages/core/package.json',
|
|
100
|
+
];
|
|
101
|
+
const packageAnchor = packageAnchors.find((line) => content.includes(line)) ?? packageAnchors.at(-1);
|
|
102
|
+
content = ensureLineAfter(content, packageAnchor, 'COPY packages/queue/package.json packages/queue/package.json');
|
|
103
|
+
|
|
104
|
+
const sourceAnchors = [
|
|
105
|
+
'COPY packages/files-image packages/files-image',
|
|
106
|
+
'COPY packages/files-quotas packages/files-quotas',
|
|
107
|
+
'COPY packages/files-access packages/files-access',
|
|
108
|
+
'COPY packages/files-s3 packages/files-s3',
|
|
109
|
+
'COPY packages/files-local packages/files-local',
|
|
110
|
+
'COPY packages/files packages/files',
|
|
111
|
+
'COPY packages/auth-api packages/auth-api',
|
|
112
|
+
'COPY packages/rbac packages/rbac',
|
|
113
|
+
'COPY packages/rate-limit packages/rate-limit',
|
|
114
|
+
'COPY packages/logger packages/logger',
|
|
115
|
+
'COPY packages/swagger packages/swagger',
|
|
116
|
+
'COPY packages/i18n packages/i18n',
|
|
117
|
+
'COPY packages/db-prisma packages/db-prisma',
|
|
118
|
+
'COPY packages/core packages/core',
|
|
119
|
+
];
|
|
120
|
+
const sourceAnchor = sourceAnchors.find((line) => content.includes(line)) ?? sourceAnchors.at(-1);
|
|
121
|
+
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/queue packages/queue');
|
|
122
|
+
|
|
123
|
+
content = content.replace(/^RUN pnpm --filter @forgeon\/queue build\r?\n?/gm, '');
|
|
124
|
+
const buildAnchors = [
|
|
125
|
+
'RUN pnpm --filter @forgeon/scheduler build',
|
|
126
|
+
'RUN pnpm --filter @forgeon/api prisma:generate',
|
|
127
|
+
'RUN pnpm --filter @forgeon/api build',
|
|
128
|
+
];
|
|
129
|
+
const buildAnchor = buildAnchors.find((line) => content.includes(line)) ?? 'RUN pnpm --filter @forgeon/api build';
|
|
130
|
+
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/queue build');
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ensureApiDependsOnRedis(apiBlock) {
|
|
136
|
+
let lines = apiBlock.replace(/\n$/, '').split('\n');
|
|
137
|
+
const buildIndex = lines.findIndex((line) => line === ' build:');
|
|
138
|
+
|
|
139
|
+
// Heal a previously broken queue patch where depends_on was injected inside the build block.
|
|
140
|
+
if (buildIndex >= 0 && lines[buildIndex + 1] === ' depends_on:') {
|
|
141
|
+
let malformedBlockEnd = lines.length;
|
|
142
|
+
for (let index = buildIndex + 2; index < lines.length; index += 1) {
|
|
143
|
+
if (lines[index].startsWith(' ') && !lines[index].startsWith(' ')) {
|
|
144
|
+
malformedBlockEnd = index;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const misplacedBuildLines = lines
|
|
150
|
+
.slice(buildIndex + 2, malformedBlockEnd)
|
|
151
|
+
.filter((line) => /^( context:| dockerfile:| args:| target:)/.test(line));
|
|
152
|
+
|
|
153
|
+
if (misplacedBuildLines.length > 0) {
|
|
154
|
+
lines = [
|
|
155
|
+
...lines.slice(0, buildIndex + 1),
|
|
156
|
+
...misplacedBuildLines,
|
|
157
|
+
...lines.slice(malformedBlockEnd),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const dependsOnIndex = lines.findIndex((line) => line === ' depends_on:');
|
|
163
|
+
|
|
164
|
+
if (dependsOnIndex < 0) {
|
|
165
|
+
const restartIndex = lines.findIndex((line) => line === ' restart: unless-stopped');
|
|
166
|
+
const environmentIndex = lines.findIndex((line) => line === ' environment:');
|
|
167
|
+
const insertAt = restartIndex >= 0 ? restartIndex + 1 : environmentIndex >= 0 ? environmentIndex : lines.length;
|
|
168
|
+
lines.splice(insertAt, 0, ' depends_on:', ' redis:', ' condition: service_healthy');
|
|
169
|
+
return `${lines.join('\n')}\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let blockEnd = lines.length;
|
|
173
|
+
for (let index = dependsOnIndex + 1; index < lines.length; index += 1) {
|
|
174
|
+
if (lines[index].startsWith(' ') && !lines[index].startsWith(' ')) {
|
|
175
|
+
blockEnd = index;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dependsOnBlock = lines.slice(dependsOnIndex + 1, blockEnd);
|
|
181
|
+
const hasRedisMapping = dependsOnBlock.includes(' redis:');
|
|
182
|
+
const hasRedisList = dependsOnBlock.some((line) => line.trim() === '- redis');
|
|
183
|
+
if (hasRedisMapping || hasRedisList) {
|
|
184
|
+
return `${lines.join('\n')}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const isListStyle = dependsOnBlock.some((line) => line.trimStart().startsWith('- '));
|
|
188
|
+
const insertLines = isListStyle
|
|
189
|
+
? [' - redis']
|
|
190
|
+
: [' redis:', ' condition: service_healthy'];
|
|
191
|
+
|
|
192
|
+
lines.splice(blockEnd, 0, ...insertLines);
|
|
193
|
+
return `${lines.join('\n')}\n`;
|
|
194
|
+
}
|
|
195
|
+
function patchCompose(targetRoot) {
|
|
196
|
+
const composePath = path.join(targetRoot, 'infra', 'docker', 'compose.yml');
|
|
197
|
+
if (!fs.existsSync(composePath)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let content = fs.readFileSync(composePath, 'utf8').replace(/\r\n/g, '\n');
|
|
202
|
+
|
|
203
|
+
const redisServiceBlock = ` redis:
|
|
204
|
+
image: redis:7-alpine
|
|
205
|
+
restart: unless-stopped
|
|
206
|
+
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
|
207
|
+
ports:
|
|
208
|
+
- "6379:6379"
|
|
209
|
+
healthcheck:
|
|
210
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
211
|
+
interval: 10s
|
|
212
|
+
timeout: 5s
|
|
213
|
+
retries: 10`;
|
|
214
|
+
|
|
215
|
+
if (!/\n\s{2}redis:\n/.test(content)) {
|
|
216
|
+
content = content.replace(/^services:\n/m, `services:\n${redisServiceBlock}\n\n`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!content.includes('QUEUE_ENABLED: ${QUEUE_ENABLED}')) {
|
|
220
|
+
const anchors = [
|
|
221
|
+
/^(\s+FILES_IMAGE_STRIP_METADATA:.*)$/m,
|
|
222
|
+
/^(\s+FILES_QUOTA_MAX_BYTES_PER_OWNER:.*)$/m,
|
|
223
|
+
/^(\s+FILES_ACCESS_DEFAULT_VISIBILITY:.*)$/m,
|
|
224
|
+
/^(\s+FILES_S3_MAX_ATTEMPTS:.*)$/m,
|
|
225
|
+
/^(\s+FILES_LOCAL_ROOT:.*)$/m,
|
|
226
|
+
/^(\s+FILES_PUBLIC_BASE_PATH:.*)$/m,
|
|
227
|
+
/^(\s+AUTH_DEMO_PASSWORD:.*)$/m,
|
|
228
|
+
/^(\s+THROTTLE_TRUST_PROXY:.*)$/m,
|
|
229
|
+
/^(\s+LOGGER_REQUEST_ID_HEADER:.*)$/m,
|
|
230
|
+
/^(\s+SWAGGER_DOCS_PATH:.*)$/m,
|
|
231
|
+
/^(\s+I18N_FALLBACK_LANG:.*)$/m,
|
|
232
|
+
/^(\s+DATABASE_URL:.*)$/m,
|
|
233
|
+
/^(\s+API_PREFIX:.*)$/m,
|
|
234
|
+
];
|
|
235
|
+
const anchorPattern = anchors.find((pattern) => pattern.test(content)) ?? anchors.at(-1);
|
|
236
|
+
content = content.replace(
|
|
237
|
+
anchorPattern,
|
|
238
|
+
`$1
|
|
239
|
+
QUEUE_ENABLED: \${QUEUE_ENABLED}
|
|
240
|
+
QUEUE_REDIS_URL: \${QUEUE_REDIS_URL}
|
|
241
|
+
QUEUE_PREFIX: \${QUEUE_PREFIX}
|
|
242
|
+
QUEUE_DEFAULT_ATTEMPTS: \${QUEUE_DEFAULT_ATTEMPTS}
|
|
243
|
+
QUEUE_DEFAULT_BACKOFF_MS: \${QUEUE_DEFAULT_BACKOFF_MS}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const apiBlockMatch = content.match(/^ api:\n[\s\S]*?(?=^ [a-zA-Z0-9_-]+:\n|^volumes:\n|(?![\s\S]))/m);
|
|
248
|
+
if (apiBlockMatch) {
|
|
249
|
+
const apiBlock = ensureApiDependsOnRedis(apiBlockMatch[0]);
|
|
250
|
+
content = content.replace(apiBlockMatch[0], apiBlock);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fs.writeFileSync(composePath, `${content.trimEnd()}\n`, 'utf8');
|
|
254
|
+
}
|
|
255
|
+
function patchReadme(targetRoot) {
|
|
256
|
+
const readmePath = path.join(targetRoot, 'README.md');
|
|
257
|
+
if (!fs.existsSync(readmePath)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const marker = '## Queue Module';
|
|
262
|
+
let content = fs.readFileSync(readmePath, 'utf8').replace(/\r\n/g, '\n');
|
|
263
|
+
if (content.includes(marker)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const section = `## Queue Module
|
|
268
|
+
|
|
269
|
+
The queue add-module provides an async job runtime baseline backed by Redis.
|
|
270
|
+
|
|
271
|
+
What it adds:
|
|
272
|
+
- \`@forgeon/queue\` package
|
|
273
|
+
- typed queue env config (module-owned)
|
|
274
|
+
- queue probe endpoint: \`GET /api/health/queue\`
|
|
275
|
+
- web probe button for quick runtime verification
|
|
276
|
+
- Redis service wiring in Docker Compose
|
|
277
|
+
|
|
278
|
+
Configuration (env):
|
|
279
|
+
- \`QUEUE_ENABLED=true\`
|
|
280
|
+
- \`QUEUE_REDIS_URL=redis://localhost:6379\`
|
|
281
|
+
- \`QUEUE_PREFIX=forgeon\`
|
|
282
|
+
- \`QUEUE_DEFAULT_ATTEMPTS=3\`
|
|
283
|
+
- \`QUEUE_DEFAULT_BACKOFF_MS=1000\`
|
|
284
|
+
|
|
285
|
+
Operational notes:
|
|
286
|
+
- this stage is the queue foundation (runtime + connectivity)
|
|
287
|
+
- cron orchestration is available through the scheduler module
|
|
288
|
+
- job execution workers remain intentionally deferred to a later module`;
|
|
289
|
+
|
|
290
|
+
if (content.includes('## Prisma In Docker Start')) {
|
|
291
|
+
content = content.replace('## Prisma In Docker Start', `${section}\n\n## Prisma In Docker Start`);
|
|
292
|
+
} else {
|
|
293
|
+
content = `${content.trimEnd()}\n\n${section}\n`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fs.writeFileSync(readmePath, `${content.trimEnd()}\n`, 'utf8');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function applyQueueModule({ packageRoot, targetRoot }) {
|
|
300
|
+
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'queue'));
|
|
301
|
+
const probeTargets = resolveProbeTargets({ targetRoot, moduleId: 'queue' });
|
|
302
|
+
|
|
303
|
+
patchApiPackage(targetRoot);
|
|
304
|
+
patchAppModule(targetRoot);
|
|
305
|
+
patchHealthController(targetRoot, probeTargets);
|
|
306
|
+
registerWebProbe(targetRoot, probeTargets);
|
|
307
|
+
patchApiDockerfile(targetRoot);
|
|
308
|
+
patchCompose(targetRoot);
|
|
309
|
+
patchReadme(targetRoot);
|
|
310
|
+
|
|
311
|
+
upsertEnvLines(path.join(targetRoot, 'apps', 'api', '.env.example'), [
|
|
312
|
+
'QUEUE_ENABLED=true',
|
|
313
|
+
'QUEUE_REDIS_URL=redis://localhost:6379',
|
|
314
|
+
'QUEUE_PREFIX=forgeon',
|
|
315
|
+
'QUEUE_DEFAULT_ATTEMPTS=3',
|
|
316
|
+
'QUEUE_DEFAULT_BACKOFF_MS=1000',
|
|
317
|
+
]);
|
|
318
|
+
upsertEnvLines(path.join(targetRoot, 'infra', 'docker', '.env.example'), [
|
|
319
|
+
'QUEUE_ENABLED=true',
|
|
320
|
+
'QUEUE_REDIS_URL=redis://redis:6379',
|
|
321
|
+
'QUEUE_PREFIX=forgeon',
|
|
322
|
+
'QUEUE_DEFAULT_ATTEMPTS=3',
|
|
323
|
+
'QUEUE_DEFAULT_BACKOFF_MS=1000',
|
|
324
|
+
]);
|
|
325
|
+
}
|