@nicnocquee/dataqueue 1.32.0 → 1.33.0
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/dist/cli.cjs +593 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +16 -1
- package/dist/cli.d.ts +16 -1
- package/dist/cli.js +593 -3
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.test.ts +17 -6
- package/src/cli.ts +28 -2
- package/src/init-command.test.ts +449 -0
- package/src/init-command.ts +709 -0
package/dist/cli.cjs
CHANGED
|
@@ -3,28 +3,603 @@
|
|
|
3
3
|
var child_process = require('child_process');
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var url = require('url');
|
|
6
|
+
var fs = require('fs');
|
|
6
7
|
|
|
7
8
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
9
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
10
|
|
|
10
11
|
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
12
|
|
|
13
|
+
// src/cli.ts
|
|
14
|
+
var DEPENDENCIES_TO_ADD = [
|
|
15
|
+
"@nicnocquee/dataqueue",
|
|
16
|
+
"@nicnocquee/dataqueue-dashboard",
|
|
17
|
+
"@nicnocquee/dataqueue-react"
|
|
18
|
+
];
|
|
19
|
+
var DEV_DEPENDENCIES_TO_ADD = [
|
|
20
|
+
"dotenv-cli",
|
|
21
|
+
"ts-node",
|
|
22
|
+
"node-pg-migrate"
|
|
23
|
+
];
|
|
24
|
+
var SCRIPTS_TO_ADD = {
|
|
25
|
+
cron: "bash cron.sh",
|
|
26
|
+
"migrate-dataqueue": "dotenv -e .env.local -- dataqueue-cli migrate"
|
|
27
|
+
};
|
|
28
|
+
var APP_ROUTER_ROUTE_TEMPLATE = `/**
|
|
29
|
+
* This end point is used to manage the job queue.
|
|
30
|
+
* It supports the following tasks:
|
|
31
|
+
* - reclaim: Reclaim stuck jobs
|
|
32
|
+
* - cleanup: Cleanup old jobs
|
|
33
|
+
* - process: Process jobs
|
|
34
|
+
*
|
|
35
|
+
* Example usage with default values (reclaim stuck jobs for 10 minutes, cleanup old jobs for 30 days, and process jobs with batch size 3, concurrency 2, and verbose true):
|
|
36
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET"
|
|
37
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET"
|
|
38
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET"
|
|
39
|
+
*
|
|
40
|
+
* Example usage with custom values:
|
|
41
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET" -d '{"maxProcessingTimeMinutes": 15}' -H "Content-Type: application/json"
|
|
42
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET" -d '{"daysToKeep": 15}' -H "Content-Type: application/json"
|
|
43
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET" -d '{"batchSize": 5, "concurrency": 3, "verbose": false, "workerId": "custom-worker-id"}' -H "Content-Type: application/json"
|
|
44
|
+
*
|
|
45
|
+
* During development, you can run the following script to run the cron jobs continuously in the background:
|
|
46
|
+
* pnpm cron
|
|
47
|
+
*/
|
|
48
|
+
import { getJobQueue, jobHandlers } from '@/lib/dataqueue/queue';
|
|
49
|
+
import { NextResponse } from 'next/server';
|
|
50
|
+
|
|
51
|
+
export async function POST(
|
|
52
|
+
request: Request,
|
|
53
|
+
{ params }: { params: Promise<{ task: string[] }> },
|
|
54
|
+
) {
|
|
55
|
+
const { task } = await params;
|
|
56
|
+
const authHeader = request.headers.get('authorization');
|
|
57
|
+
if (authHeader !== \`Bearer \${process.env.CRON_SECRET}\`) {
|
|
58
|
+
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!task || task.length === 0) {
|
|
62
|
+
return NextResponse.json({ message: 'Task is required' }, { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const supportedTasks = ['reclaim', 'cleanup', 'process'];
|
|
66
|
+
const theTask = task[0];
|
|
67
|
+
if (!supportedTasks.includes(theTask)) {
|
|
68
|
+
return NextResponse.json(
|
|
69
|
+
{ message: 'Task not supported' },
|
|
70
|
+
{ status: 400 },
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const jobQueue = getJobQueue();
|
|
76
|
+
|
|
77
|
+
if (theTask === 'reclaim') {
|
|
78
|
+
let maxProcessingTimeMinutes = 10;
|
|
79
|
+
try {
|
|
80
|
+
const body = await request.json();
|
|
81
|
+
maxProcessingTimeMinutes = body.maxProcessingTimeMinutes || 10;
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore parsing error and use default value
|
|
84
|
+
}
|
|
85
|
+
const reclaimed = await jobQueue.reclaimStuckJobs(
|
|
86
|
+
maxProcessingTimeMinutes,
|
|
87
|
+
);
|
|
88
|
+
console.log(\`Reclaimed \${reclaimed} stuck jobs\`);
|
|
89
|
+
return NextResponse.json({
|
|
90
|
+
message: \`Stuck jobs reclaimed: \${reclaimed} with maxProcessingTimeMinutes: \${maxProcessingTimeMinutes}\`,
|
|
91
|
+
reclaimed,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (theTask === 'cleanup') {
|
|
96
|
+
let daysToKeep = 30;
|
|
97
|
+
try {
|
|
98
|
+
const body = await request.json();
|
|
99
|
+
daysToKeep = body.daysToKeep || 30;
|
|
100
|
+
} catch {
|
|
101
|
+
// ignore parsing error and use default value
|
|
102
|
+
}
|
|
103
|
+
const deleted = await jobQueue.cleanupOldJobs(daysToKeep);
|
|
104
|
+
console.log(\`Deleted \${deleted} old jobs\`);
|
|
105
|
+
return NextResponse.json({
|
|
106
|
+
message: \`Old jobs cleaned up: \${deleted} with daysToKeep: \${daysToKeep}\`,
|
|
107
|
+
deleted,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (theTask === 'process') {
|
|
112
|
+
let batchSize = 3;
|
|
113
|
+
let concurrency = 2;
|
|
114
|
+
let verbose = true;
|
|
115
|
+
let workerId = \`manage-\${theTask}-\${Date.now()}\`;
|
|
116
|
+
try {
|
|
117
|
+
const body = await request.json();
|
|
118
|
+
batchSize = body.batchSize || 3;
|
|
119
|
+
concurrency = body.concurrency || 2;
|
|
120
|
+
verbose = body.verbose || true;
|
|
121
|
+
workerId = body.workerId || \`manage-\${theTask}-\${Date.now()}\`;
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore parsing error and use default value
|
|
124
|
+
}
|
|
125
|
+
const processor = jobQueue.createProcessor(jobHandlers, {
|
|
126
|
+
workerId,
|
|
127
|
+
batchSize,
|
|
128
|
+
concurrency,
|
|
129
|
+
verbose,
|
|
130
|
+
});
|
|
131
|
+
const processed = await processor.start();
|
|
132
|
+
|
|
133
|
+
return NextResponse.json({
|
|
134
|
+
message: \`Jobs processed: \${processed} with workerId: \${workerId}, batchSize: \${batchSize}, concurrency: \${concurrency}, and verbose: \${verbose}\`,
|
|
135
|
+
processed,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return NextResponse.json(
|
|
140
|
+
{ message: 'Task not supported' },
|
|
141
|
+
{ status: 400 },
|
|
142
|
+
);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error processing jobs:', error);
|
|
145
|
+
return NextResponse.json(
|
|
146
|
+
{ message: 'Failed to process jobs' },
|
|
147
|
+
{ status: 500 },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
var PAGES_ROUTER_ROUTE_TEMPLATE = `/**
|
|
153
|
+
* This end point is used to manage the job queue.
|
|
154
|
+
* It supports the following tasks:
|
|
155
|
+
* - reclaim: Reclaim stuck jobs
|
|
156
|
+
* - cleanup: Cleanup old jobs
|
|
157
|
+
* - process: Process jobs
|
|
158
|
+
*
|
|
159
|
+
* Example usage with default values (reclaim stuck jobs for 10 minutes, cleanup old jobs for 30 days, and process jobs with batch size 3, concurrency 2, and verbose true):
|
|
160
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET"
|
|
161
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET"
|
|
162
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET"
|
|
163
|
+
*
|
|
164
|
+
* Example usage with custom values:
|
|
165
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/reclaim -H "Authorization: Bearer $CRON_SECRET" -d '{"maxProcessingTimeMinutes": 15}' -H "Content-Type: application/json"
|
|
166
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/cleanup -H "Authorization: Bearer $CRON_SECRET" -d '{"daysToKeep": 15}' -H "Content-Type: application/json"
|
|
167
|
+
* curl -X POST http://localhost:3000/api/dataqueue/manage/process -H "Authorization: Bearer $CRON_SECRET" -d '{"batchSize": 5, "concurrency": 3, "verbose": false, "workerId": "custom-worker-id"}' -H "Content-Type: application/json"
|
|
168
|
+
*
|
|
169
|
+
* During development, you can run the following script to run the cron jobs continuously in the background:
|
|
170
|
+
* pnpm cron
|
|
171
|
+
*/
|
|
172
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
173
|
+
import { getJobQueue, jobHandlers } from '@/lib/dataqueue/queue';
|
|
174
|
+
|
|
175
|
+
type ResponseBody = {
|
|
176
|
+
message: string;
|
|
177
|
+
reclaimed?: number;
|
|
178
|
+
deleted?: number;
|
|
179
|
+
processed?: number;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default async function handler(
|
|
183
|
+
req: NextApiRequest,
|
|
184
|
+
res: NextApiResponse<ResponseBody>,
|
|
185
|
+
) {
|
|
186
|
+
if (req.method !== 'POST') {
|
|
187
|
+
res.setHeader('Allow', 'POST');
|
|
188
|
+
return res.status(405).json({ message: 'Method not allowed' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const authHeader = req.headers.authorization;
|
|
192
|
+
if (authHeader !== \`Bearer \${process.env.CRON_SECRET}\`) {
|
|
193
|
+
return res.status(401).json({ message: 'Unauthorized' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const task = req.query.task;
|
|
197
|
+
const taskArray = Array.isArray(task) ? task : task ? [task] : [];
|
|
198
|
+
if (!taskArray.length) {
|
|
199
|
+
return res.status(400).json({ message: 'Task is required' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const supportedTasks = ['reclaim', 'cleanup', 'process'];
|
|
203
|
+
const theTask = taskArray[0];
|
|
204
|
+
if (!supportedTasks.includes(theTask)) {
|
|
205
|
+
return res.status(400).json({ message: 'Task not supported' });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const jobQueue = getJobQueue();
|
|
210
|
+
const body = typeof req.body === 'object' && req.body ? req.body : {};
|
|
211
|
+
|
|
212
|
+
if (theTask === 'reclaim') {
|
|
213
|
+
const maxProcessingTimeMinutes = body.maxProcessingTimeMinutes || 10;
|
|
214
|
+
const reclaimed = await jobQueue.reclaimStuckJobs(maxProcessingTimeMinutes);
|
|
215
|
+
console.log(\`Reclaimed \${reclaimed} stuck jobs\`);
|
|
216
|
+
return res.status(200).json({
|
|
217
|
+
message: \`Stuck jobs reclaimed: \${reclaimed} with maxProcessingTimeMinutes: \${maxProcessingTimeMinutes}\`,
|
|
218
|
+
reclaimed,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (theTask === 'cleanup') {
|
|
223
|
+
const daysToKeep = body.daysToKeep || 30;
|
|
224
|
+
const deleted = await jobQueue.cleanupOldJobs(daysToKeep);
|
|
225
|
+
console.log(\`Deleted \${deleted} old jobs\`);
|
|
226
|
+
return res.status(200).json({
|
|
227
|
+
message: \`Old jobs cleaned up: \${deleted} with daysToKeep: \${daysToKeep}\`,
|
|
228
|
+
deleted,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const batchSize = body.batchSize || 3;
|
|
233
|
+
const concurrency = body.concurrency || 2;
|
|
234
|
+
const verbose = body.verbose || true;
|
|
235
|
+
const workerId = body.workerId || \`manage-\${theTask}-\${Date.now()}\`;
|
|
236
|
+
const processor = jobQueue.createProcessor(jobHandlers, {
|
|
237
|
+
workerId,
|
|
238
|
+
batchSize,
|
|
239
|
+
concurrency,
|
|
240
|
+
verbose,
|
|
241
|
+
});
|
|
242
|
+
const processed = await processor.start();
|
|
243
|
+
|
|
244
|
+
return res.status(200).json({
|
|
245
|
+
message: \`Jobs processed: \${processed} with workerId: \${workerId}, batchSize: \${batchSize}, concurrency: \${concurrency}, and verbose: \${verbose}\`,
|
|
246
|
+
processed,
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('Error processing jobs:', error);
|
|
250
|
+
return res.status(500).json({ message: 'Failed to process jobs' });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
var CRON_SH_TEMPLATE = `#!/bin/bash
|
|
255
|
+
|
|
256
|
+
# This script is used to run the cron jobs for the demo app during development.
|
|
257
|
+
# Run it with \`pnpm cron\` from the apps/demo directory.
|
|
258
|
+
|
|
259
|
+
set -a
|
|
260
|
+
source "$(dirname "$0")/.env.local"
|
|
261
|
+
set +a
|
|
262
|
+
|
|
263
|
+
if [ -z "$CRON_SECRET" ]; then
|
|
264
|
+
echo "Error: CRON_SECRET environment variable is not set in .env.local"
|
|
265
|
+
exit 1
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
cleanup() {
|
|
269
|
+
kill 0
|
|
270
|
+
wait
|
|
271
|
+
}
|
|
272
|
+
trap cleanup SIGINT SIGTERM
|
|
273
|
+
|
|
274
|
+
while true; do
|
|
275
|
+
echo "Processing jobs..."
|
|
276
|
+
curl http://localhost:3000/api/dataqueue/manage/process -X POST -H "Authorization: Bearer $CRON_SECRET"
|
|
277
|
+
echo ""
|
|
278
|
+
sleep 10 # Process jobs every 10 seconds
|
|
279
|
+
done &
|
|
280
|
+
|
|
281
|
+
while true; do
|
|
282
|
+
echo "Reclaiming stuck jobs..."
|
|
283
|
+
curl http://localhost:3000/api/dataqueue/manage/reclaim -X POST -H "Authorization: Bearer $CRON_SECRET"
|
|
284
|
+
echo ""
|
|
285
|
+
sleep 20 # Reclaim stuck jobs every 20 seconds
|
|
286
|
+
done &
|
|
287
|
+
|
|
288
|
+
while true; do
|
|
289
|
+
echo "Cleaning up old jobs..."
|
|
290
|
+
curl http://localhost:3000/api/dataqueue/manage/cleanup -X POST -H "Authorization: Bearer $CRON_SECRET"
|
|
291
|
+
echo ""
|
|
292
|
+
sleep 30 # Cleanup old jobs every 30 seconds
|
|
293
|
+
done &
|
|
294
|
+
|
|
295
|
+
wait
|
|
296
|
+
`;
|
|
297
|
+
var QUEUE_TEMPLATE = `import { initJobQueue, JobHandlers } from '@nicnocquee/dataqueue';
|
|
298
|
+
|
|
299
|
+
export type JobPayloadMap = {
|
|
300
|
+
send_email: {
|
|
301
|
+
to: string;
|
|
302
|
+
subject: string;
|
|
303
|
+
body: string;
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
let jobQueue: ReturnType<typeof initJobQueue<JobPayloadMap>> | null = null;
|
|
308
|
+
|
|
309
|
+
export const getJobQueue = () => {
|
|
310
|
+
if (!jobQueue) {
|
|
311
|
+
jobQueue = initJobQueue<JobPayloadMap>({
|
|
312
|
+
databaseConfig: {
|
|
313
|
+
connectionString: process.env.PG_DATAQUEUE_DATABASE,
|
|
314
|
+
},
|
|
315
|
+
verbose: process.env.NODE_ENV === 'development',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return jobQueue;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export const jobHandlers: JobHandlers<JobPayloadMap> = {
|
|
322
|
+
send_email: async (payload) => {
|
|
323
|
+
const { to, subject, body } = payload;
|
|
324
|
+
console.log('send_email placeholder:', { to, subject, body });
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
`;
|
|
328
|
+
function runInit({
|
|
329
|
+
log = console.log,
|
|
330
|
+
error = console.error,
|
|
331
|
+
exit = (code) => process.exit(code),
|
|
332
|
+
cwd = process.cwd(),
|
|
333
|
+
readFileSyncImpl = fs.readFileSync,
|
|
334
|
+
writeFileSyncImpl = fs.writeFileSync,
|
|
335
|
+
existsSyncImpl = fs.existsSync,
|
|
336
|
+
mkdirSyncImpl = fs.mkdirSync,
|
|
337
|
+
chmodSyncImpl = fs.chmodSync
|
|
338
|
+
} = {}) {
|
|
339
|
+
try {
|
|
340
|
+
log(`dataqueue: Initializing in ${cwd}...`);
|
|
341
|
+
log("");
|
|
342
|
+
const details = detectNextJsAndRouter({
|
|
343
|
+
cwd,
|
|
344
|
+
existsSyncImpl,
|
|
345
|
+
readFileSyncImpl
|
|
346
|
+
});
|
|
347
|
+
createScaffoldFiles({
|
|
348
|
+
details,
|
|
349
|
+
log,
|
|
350
|
+
existsSyncImpl,
|
|
351
|
+
mkdirSyncImpl,
|
|
352
|
+
writeFileSyncImpl,
|
|
353
|
+
chmodSyncImpl
|
|
354
|
+
});
|
|
355
|
+
updatePackageJson({
|
|
356
|
+
details,
|
|
357
|
+
log,
|
|
358
|
+
writeFileSyncImpl
|
|
359
|
+
});
|
|
360
|
+
log("");
|
|
361
|
+
log(
|
|
362
|
+
"Done! Run your package manager's install command to install new dependencies."
|
|
363
|
+
);
|
|
364
|
+
exit(0);
|
|
365
|
+
} catch (cause) {
|
|
366
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
367
|
+
error(`dataqueue: ${message}`);
|
|
368
|
+
exit(1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function detectNextJsAndRouter({
|
|
372
|
+
cwd,
|
|
373
|
+
existsSyncImpl,
|
|
374
|
+
readFileSyncImpl
|
|
375
|
+
}) {
|
|
376
|
+
const packageJsonPath = path__default.default.join(cwd, "package.json");
|
|
377
|
+
if (!existsSyncImpl(packageJsonPath)) {
|
|
378
|
+
throw new Error("package.json not found in current directory.");
|
|
379
|
+
}
|
|
380
|
+
const packageJson = parsePackageJson(
|
|
381
|
+
readFileSyncImpl(packageJsonPath, "utf8"),
|
|
382
|
+
packageJsonPath
|
|
383
|
+
);
|
|
384
|
+
if (!isNextJsProject(packageJson)) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
"Not a Next.js project. Could not find 'next' in package.json dependencies."
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const srcDir = path__default.default.join(cwd, "src");
|
|
390
|
+
const srcRoot = existsSyncImpl(srcDir) ? "src" : ".";
|
|
391
|
+
const appDir = path__default.default.join(cwd, srcRoot, "app");
|
|
392
|
+
const pagesDir = path__default.default.join(cwd, srcRoot, "pages");
|
|
393
|
+
const hasAppDir = existsSyncImpl(appDir);
|
|
394
|
+
const hasPagesDir = existsSyncImpl(pagesDir);
|
|
395
|
+
if (!hasAppDir && !hasPagesDir) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
"Could not detect Next.js router. Expected either app/ or pages/ directory."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const router = hasAppDir ? "app" : "pages";
|
|
401
|
+
return { cwd, packageJsonPath, packageJson, srcRoot, router };
|
|
402
|
+
}
|
|
403
|
+
function updatePackageJson({
|
|
404
|
+
details,
|
|
405
|
+
log,
|
|
406
|
+
writeFileSyncImpl
|
|
407
|
+
}) {
|
|
408
|
+
const packageJson = details.packageJson;
|
|
409
|
+
const dependencies = ensureStringMapSection(packageJson, "dependencies");
|
|
410
|
+
const devDependencies = ensureStringMapSection(
|
|
411
|
+
packageJson,
|
|
412
|
+
"devDependencies"
|
|
413
|
+
);
|
|
414
|
+
const scripts = ensureStringMapSection(packageJson, "scripts");
|
|
415
|
+
for (const dependency of DEPENDENCIES_TO_ADD) {
|
|
416
|
+
if (dependencies[dependency]) {
|
|
417
|
+
log(` [skipped] dependency ${dependency} (already exists)`);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
dependencies[dependency] = "latest";
|
|
421
|
+
log(` [added] dependency ${dependency}`);
|
|
422
|
+
}
|
|
423
|
+
for (const devDependency of DEV_DEPENDENCIES_TO_ADD) {
|
|
424
|
+
if (devDependencies[devDependency]) {
|
|
425
|
+
log(` [skipped] devDependency ${devDependency} (already exists)`);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
devDependencies[devDependency] = "latest";
|
|
429
|
+
log(` [added] devDependency ${devDependency}`);
|
|
430
|
+
}
|
|
431
|
+
for (const [scriptName, scriptValue] of Object.entries(SCRIPTS_TO_ADD)) {
|
|
432
|
+
if (scripts[scriptName]) {
|
|
433
|
+
log(` [skipped] script "${scriptName}" (already exists)`);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
scripts[scriptName] = scriptValue;
|
|
437
|
+
log(` [added] script "${scriptName}"`);
|
|
438
|
+
}
|
|
439
|
+
writeFileSyncImpl(
|
|
440
|
+
details.packageJsonPath,
|
|
441
|
+
`${JSON.stringify(packageJson, null, 2)}
|
|
442
|
+
`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
function createScaffoldFiles({
|
|
446
|
+
details,
|
|
447
|
+
log,
|
|
448
|
+
existsSyncImpl,
|
|
449
|
+
mkdirSyncImpl,
|
|
450
|
+
writeFileSyncImpl,
|
|
451
|
+
chmodSyncImpl
|
|
452
|
+
}) {
|
|
453
|
+
const appRoutePath = path__default.default.join(
|
|
454
|
+
details.cwd,
|
|
455
|
+
details.srcRoot,
|
|
456
|
+
"app",
|
|
457
|
+
"api",
|
|
458
|
+
"dataqueue",
|
|
459
|
+
"manage",
|
|
460
|
+
"[[...task]]",
|
|
461
|
+
"route.ts"
|
|
462
|
+
);
|
|
463
|
+
const pagesRoutePath = path__default.default.join(
|
|
464
|
+
details.cwd,
|
|
465
|
+
details.srcRoot,
|
|
466
|
+
"pages",
|
|
467
|
+
"api",
|
|
468
|
+
"dataqueue",
|
|
469
|
+
"manage",
|
|
470
|
+
"[[...task]].ts"
|
|
471
|
+
);
|
|
472
|
+
const queuePath = path__default.default.join(
|
|
473
|
+
details.cwd,
|
|
474
|
+
details.srcRoot,
|
|
475
|
+
"lib",
|
|
476
|
+
"dataqueue",
|
|
477
|
+
"queue.ts"
|
|
478
|
+
);
|
|
479
|
+
const cronPath = path__default.default.join(details.cwd, "cron.sh");
|
|
480
|
+
if (details.router === "app") {
|
|
481
|
+
createFileIfMissing({
|
|
482
|
+
absolutePath: appRoutePath,
|
|
483
|
+
content: APP_ROUTER_ROUTE_TEMPLATE,
|
|
484
|
+
existsSyncImpl,
|
|
485
|
+
mkdirSyncImpl,
|
|
486
|
+
writeFileSyncImpl,
|
|
487
|
+
log,
|
|
488
|
+
logPath: toRelativePath(details.cwd, appRoutePath)
|
|
489
|
+
});
|
|
490
|
+
log(
|
|
491
|
+
" [skipped] pages/api/dataqueue/manage/[[...task]].ts (router not selected)"
|
|
492
|
+
);
|
|
493
|
+
} else {
|
|
494
|
+
log(
|
|
495
|
+
" [skipped] app/api/dataqueue/manage/[[...task]]/route.ts (router not selected)"
|
|
496
|
+
);
|
|
497
|
+
createFileIfMissing({
|
|
498
|
+
absolutePath: pagesRoutePath,
|
|
499
|
+
content: PAGES_ROUTER_ROUTE_TEMPLATE,
|
|
500
|
+
existsSyncImpl,
|
|
501
|
+
mkdirSyncImpl,
|
|
502
|
+
writeFileSyncImpl,
|
|
503
|
+
log,
|
|
504
|
+
logPath: toRelativePath(details.cwd, pagesRoutePath)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
createFileIfMissing({
|
|
508
|
+
absolutePath: cronPath,
|
|
509
|
+
content: CRON_SH_TEMPLATE,
|
|
510
|
+
existsSyncImpl,
|
|
511
|
+
mkdirSyncImpl,
|
|
512
|
+
writeFileSyncImpl,
|
|
513
|
+
log,
|
|
514
|
+
logPath: "cron.sh"
|
|
515
|
+
});
|
|
516
|
+
if (existsSyncImpl(cronPath)) {
|
|
517
|
+
chmodSyncImpl(cronPath, 493);
|
|
518
|
+
}
|
|
519
|
+
createFileIfMissing({
|
|
520
|
+
absolutePath: queuePath,
|
|
521
|
+
content: QUEUE_TEMPLATE,
|
|
522
|
+
existsSyncImpl,
|
|
523
|
+
mkdirSyncImpl,
|
|
524
|
+
writeFileSyncImpl,
|
|
525
|
+
log,
|
|
526
|
+
logPath: toRelativePath(details.cwd, queuePath)
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function createFileIfMissing({
|
|
530
|
+
absolutePath,
|
|
531
|
+
content,
|
|
532
|
+
existsSyncImpl,
|
|
533
|
+
mkdirSyncImpl,
|
|
534
|
+
writeFileSyncImpl,
|
|
535
|
+
log,
|
|
536
|
+
logPath
|
|
537
|
+
}) {
|
|
538
|
+
if (existsSyncImpl(absolutePath)) {
|
|
539
|
+
log(` [skipped] ${logPath} (already exists)`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
mkdirSyncImpl(path__default.default.dirname(absolutePath), { recursive: true });
|
|
543
|
+
writeFileSyncImpl(absolutePath, content);
|
|
544
|
+
log(` [created] ${logPath}`);
|
|
545
|
+
}
|
|
546
|
+
function parsePackageJson(content, filePath) {
|
|
547
|
+
try {
|
|
548
|
+
const parsed = JSON.parse(content);
|
|
549
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
550
|
+
throw new Error("package.json must contain an object.");
|
|
551
|
+
}
|
|
552
|
+
return parsed;
|
|
553
|
+
} catch (cause) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`Failed to parse package.json at ${filePath}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function isNextJsProject(packageJson) {
|
|
560
|
+
const dependencies = packageJson.dependencies;
|
|
561
|
+
const devDependencies = packageJson.devDependencies;
|
|
562
|
+
return hasPackage(dependencies, "next") || hasPackage(devDependencies, "next");
|
|
563
|
+
}
|
|
564
|
+
function hasPackage(section, packageName) {
|
|
565
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
return Boolean(section[packageName]);
|
|
569
|
+
}
|
|
570
|
+
function ensureStringMapSection(packageJson, sectionName) {
|
|
571
|
+
const currentValue = packageJson[sectionName];
|
|
572
|
+
if (!currentValue || typeof currentValue !== "object" || Array.isArray(currentValue)) {
|
|
573
|
+
packageJson[sectionName] = {};
|
|
574
|
+
}
|
|
575
|
+
return packageJson[sectionName];
|
|
576
|
+
}
|
|
577
|
+
function toRelativePath(cwd, absolutePath) {
|
|
578
|
+
const relative = path__default.default.relative(cwd, absolutePath);
|
|
579
|
+
return relative || ".";
|
|
580
|
+
}
|
|
581
|
+
|
|
12
582
|
// src/cli.ts
|
|
13
583
|
var __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
14
584
|
var __dirname$1 = path__default.default.dirname(__filename$1);
|
|
15
585
|
function runCli(argv, {
|
|
16
586
|
log = console.log,
|
|
587
|
+
error = console.error,
|
|
17
588
|
exit = (code) => process.exit(code),
|
|
18
589
|
spawnSyncImpl = child_process.spawnSync,
|
|
19
|
-
migrationsDir = path__default.default.join(__dirname$1, "../migrations")
|
|
590
|
+
migrationsDir = path__default.default.join(__dirname$1, "../migrations"),
|
|
591
|
+
initDeps,
|
|
592
|
+
runInitImpl = runInit
|
|
20
593
|
} = {}) {
|
|
21
594
|
const [, , command, ...restArgs] = argv;
|
|
22
595
|
function printUsage() {
|
|
596
|
+
log("Usage:");
|
|
23
597
|
log(
|
|
24
|
-
"
|
|
598
|
+
" dataqueue-cli migrate [--envPath <path>] [-s <schema> | --schema <schema>]"
|
|
25
599
|
);
|
|
600
|
+
log(" dataqueue-cli init");
|
|
26
601
|
log("");
|
|
27
|
-
log("Options:");
|
|
602
|
+
log("Options for migrate:");
|
|
28
603
|
log(
|
|
29
604
|
" --envPath <path> Path to a .env file to load environment variables (passed to node-pg-migrate)"
|
|
30
605
|
);
|
|
@@ -42,6 +617,14 @@ function runCli(argv, {
|
|
|
42
617
|
log(
|
|
43
618
|
" Example: PGSSLMODE=require NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.crt PG_DATAQUEUE_DATABASE=... npx dataqueue-cli migrate"
|
|
44
619
|
);
|
|
620
|
+
log("");
|
|
621
|
+
log("Notes for init:");
|
|
622
|
+
log(
|
|
623
|
+
" - Supports both Next.js App Router and Pages Router (prefers App Router if both exist)."
|
|
624
|
+
);
|
|
625
|
+
log(
|
|
626
|
+
" - Scaffolds endpoint, cron.sh, queue placeholder, and package.json entries."
|
|
627
|
+
);
|
|
45
628
|
exit(1);
|
|
46
629
|
}
|
|
47
630
|
if (command === "migrate") {
|
|
@@ -78,6 +661,13 @@ function runCli(argv, {
|
|
|
78
661
|
{ stdio: "inherit" }
|
|
79
662
|
);
|
|
80
663
|
exit(result.status ?? 1);
|
|
664
|
+
} else if (command === "init") {
|
|
665
|
+
runInitImpl({
|
|
666
|
+
log,
|
|
667
|
+
error,
|
|
668
|
+
exit,
|
|
669
|
+
...initDeps
|
|
670
|
+
});
|
|
81
671
|
} else {
|
|
82
672
|
printUsage();
|
|
83
673
|
}
|