@nmakarov/cli-toolkit 0.33.0 → 0.37.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-runner.cjs +473 -73
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +473 -73
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +332 -3
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +325 -2
- package/dist/db.js.map +1 -1
- package/dist/index.cjs +498 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +492 -73
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +55 -0
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +55 -0
- package/dist/init.js.map +1 -1
- package/dist/params.cjs +55 -0
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +55 -0
- package/dist/params.js.map +1 -1
- package/dist/tasks.cjs +196 -72
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +196 -72
- package/dist/tasks.js.map +1 -1
- package/package.json +1 -1
package/dist/tasks.cjs
CHANGED
|
@@ -164,6 +164,91 @@ var import_node_os = __toESM(require("os"), 1);
|
|
|
164
164
|
// src/tasks/taskUtils.js
|
|
165
165
|
var import_node_crypto = require("crypto");
|
|
166
166
|
|
|
167
|
+
// src/db/ensure.js
|
|
168
|
+
var dbLabel = (db) => db?.config?.name ?? "db";
|
|
169
|
+
async function ensureExtension(db, name, options = {}) {
|
|
170
|
+
const action = `CREATE EXTENSION IF NOT EXISTS "${name}"`;
|
|
171
|
+
if (!options.dryRun) {
|
|
172
|
+
await db.raw(action);
|
|
173
|
+
}
|
|
174
|
+
options.logger?.silly?.(`[ensure] ${dbLabel(db)}: ${action}${options.dryRun ? " (dryRun)" : ""}`);
|
|
175
|
+
return { action };
|
|
176
|
+
}
|
|
177
|
+
async function ensureTable(db, tableName, spec, options = {}) {
|
|
178
|
+
const { dryRun = false, logger } = options;
|
|
179
|
+
const actions = [];
|
|
180
|
+
const exists = await db.tableExists(tableName);
|
|
181
|
+
if (!exists) {
|
|
182
|
+
actions.push(`CREATE TABLE ${tableName} (${Object.keys(spec.columns).length} columns)`);
|
|
183
|
+
if (!dryRun) {
|
|
184
|
+
await db.schema.createTable(tableName, (t) => {
|
|
185
|
+
for (const define of Object.values(spec.columns)) {
|
|
186
|
+
define(t, db);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
const missing = [];
|
|
192
|
+
for (const column of Object.keys(spec.columns)) {
|
|
193
|
+
if (!await db.schema.hasColumn(tableName, column)) {
|
|
194
|
+
missing.push(column);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (missing.length > 0) {
|
|
198
|
+
actions.push(`ALTER TABLE ${tableName} ADD COLUMN ${missing.join(", ")}`);
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
await db.schema.alterTable(tableName, (t) => {
|
|
201
|
+
for (const column of missing) {
|
|
202
|
+
spec.columns[column](t, db);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const index of spec.indexes ?? []) {
|
|
209
|
+
const indexActions = await ensureIndex(db, tableName, index, options);
|
|
210
|
+
actions.push(...indexActions);
|
|
211
|
+
}
|
|
212
|
+
for (const action of actions) {
|
|
213
|
+
logger?.silly?.(`[ensure] ${dbLabel(db)}: ${action}${dryRun ? " (dryRun)" : ""}`);
|
|
214
|
+
}
|
|
215
|
+
return actions;
|
|
216
|
+
}
|
|
217
|
+
async function ensureIndex(db, tableName, index, options = {}) {
|
|
218
|
+
const { dryRun = false } = options;
|
|
219
|
+
const kind = index.unique ? "UNIQUE INDEX" : "INDEX";
|
|
220
|
+
const cols = index.columns.map((c) => `"${c}"`).join(", ");
|
|
221
|
+
const isPg = String(db?.config?.connectionString ?? "").startsWith("postgresql");
|
|
222
|
+
if (isPg) {
|
|
223
|
+
const sql2 = `CREATE ${kind} IF NOT EXISTS "${index.name}" ON "${tableName}" (${cols})`;
|
|
224
|
+
const { rows } = await db.raw(`SELECT 1 FROM pg_indexes WHERE indexname = ?`, [index.name]);
|
|
225
|
+
if (rows.length > 0) return [];
|
|
226
|
+
if (!dryRun) await db.raw(sql2);
|
|
227
|
+
return [sql2];
|
|
228
|
+
}
|
|
229
|
+
const sql = `CREATE ${kind} ${index.name} ON ${tableName} (${cols})`;
|
|
230
|
+
if (!dryRun) {
|
|
231
|
+
try {
|
|
232
|
+
await db.raw(sql);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (!/already exists|duplicate/i.test(error?.message ?? "")) throw error;
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return [sql];
|
|
239
|
+
}
|
|
240
|
+
async function ensureSchema(db, spec, options = {}) {
|
|
241
|
+
const actions = [];
|
|
242
|
+
for (const extension of spec.extensions ?? []) {
|
|
243
|
+
const { action } = await ensureExtension(db, extension, options);
|
|
244
|
+
if (options.dryRun) actions.push(action);
|
|
245
|
+
}
|
|
246
|
+
for (const [tableName, tableSpec] of Object.entries(spec.tables ?? {})) {
|
|
247
|
+
actions.push(...await ensureTable(db, tableName, tableSpec, options));
|
|
248
|
+
}
|
|
249
|
+
return { database: dbLabel(db), actions };
|
|
250
|
+
}
|
|
251
|
+
|
|
167
252
|
// src/tasks/time-matcher.js
|
|
168
253
|
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
169
254
|
function resolveAsterisks(field, range) {
|
|
@@ -248,29 +333,90 @@ function queueToTableNames(queueName) {
|
|
|
248
333
|
registryTable: `${queueName}_services_registry`
|
|
249
334
|
};
|
|
250
335
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
336
|
+
function tasksTableSpec(tableNameForIndex) {
|
|
337
|
+
return {
|
|
338
|
+
columns: {
|
|
339
|
+
id: (t, db) => t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()")),
|
|
340
|
+
created_at: (t, db) => t.timestamp("created_at").notNullable().defaultTo(db.fn.now()),
|
|
341
|
+
started_at: (t) => t.timestamp("started_at"),
|
|
342
|
+
completed_at: (t) => t.timestamp("completed_at"),
|
|
343
|
+
/*
|
|
344
|
+
* Priority: lower number = claimed first (see claimNextRunnableTask: ORDER BY priority ASC).
|
|
345
|
+
* Suggested range 0–100: 0 = most urgent, 100 = least; default 50 for normal work.
|
|
346
|
+
*/
|
|
347
|
+
priority: (t) => t.integer("priority").notNullable().defaultTo(50),
|
|
348
|
+
schedule: (t) => t.text("schedule"),
|
|
349
|
+
next_run_at: (t) => t.timestamp("next_run_at").defaultTo(null),
|
|
350
|
+
past_due: (t) => t.timestamp("past_due").defaultTo(null),
|
|
351
|
+
name: (t) => t.text("name").notNullable(),
|
|
352
|
+
opid: (t) => t.text("opid"),
|
|
353
|
+
params: (t) => t.jsonb("params"),
|
|
354
|
+
// those are target identifiers, kind of who is going to run a task.
|
|
355
|
+
service_group: (t) => t.text("service_group"),
|
|
356
|
+
// harvester, loader, photos, ...
|
|
357
|
+
instance_number: (t) => t.integer("instance_number"),
|
|
358
|
+
service_name: (t) => t.text("service_name"),
|
|
359
|
+
// that's a "<server_name>_<service_group>_<instance_number>"
|
|
360
|
+
server_name: (t) => t.text("server_name"),
|
|
361
|
+
// filled by runner when registering, auto.
|
|
362
|
+
status: (t) => t.text("status").notNullable().defaultTo("idle"),
|
|
363
|
+
// idle, running, completed, failed, paused
|
|
364
|
+
status_changed_at: (t) => t.timestamp("status_changed_at").defaultTo(null),
|
|
365
|
+
progress: (t) => t.text("progress"),
|
|
366
|
+
success: (t) => t.boolean("success"),
|
|
367
|
+
results: (t) => t.jsonb("results")
|
|
368
|
+
},
|
|
369
|
+
indexes: [
|
|
370
|
+
{
|
|
371
|
+
columns: ["service_group", "status", "priority", "created_at"],
|
|
372
|
+
name: `${tableNameForIndex}_claim_idx`
|
|
373
|
+
},
|
|
374
|
+
{ columns: ["service_group", "name"], name: `${tableNameForIndex}_group_name_idx` }
|
|
375
|
+
]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function registryTableSpec(registryTable) {
|
|
379
|
+
return {
|
|
380
|
+
columns: {
|
|
381
|
+
id: (t, db) => t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()")),
|
|
382
|
+
queue_name: (t) => t.text("queue_name").notNullable(),
|
|
383
|
+
service_group: (t) => t.text("service_group").notNullable(),
|
|
384
|
+
// harvester, loader, photos, ...
|
|
385
|
+
instance_number: (t) => t.integer("instance_number").notNullable().defaultTo(1),
|
|
386
|
+
service_name: (t) => t.text("service_name").notNullable(),
|
|
387
|
+
// that's a "<server_name>_<service_group>_<instance_number>"
|
|
388
|
+
server_name: (t) => t.text("server_name").notNullable(),
|
|
389
|
+
// filled by runner when registering, auto.
|
|
390
|
+
pid: (t) => t.integer("pid"),
|
|
391
|
+
metadata: (t) => t.json("metadata"),
|
|
392
|
+
created_at: (t, db) => t.timestamp("created_at").notNullable().defaultTo(db.fn.now()),
|
|
393
|
+
last_seen_at: (t, db) => t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now())
|
|
394
|
+
},
|
|
395
|
+
indexes: [
|
|
396
|
+
{
|
|
397
|
+
columns: ["queue_name", "service_name"],
|
|
398
|
+
name: `${registryTable}_queue_name_service_name_uniq`,
|
|
399
|
+
unique: true
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
columns: ["queue_name", "service_group", "last_seen_at"],
|
|
403
|
+
name: `${registryTable}_queue_group_seen_idx`
|
|
404
|
+
},
|
|
405
|
+
{ columns: ["queue_name", "last_seen_at"], name: `${registryTable}_queue_seen_idx` }
|
|
406
|
+
]
|
|
407
|
+
// TODO: a reference is needed - task_history entry should reference the registry entry, so that we can easily find all executed tasks for a given service and calculate the average workload or identify if there's a bottleneck. Also may be used for the load balancing.
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function tasksSchemaSpec(queueName = "tasks") {
|
|
411
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
412
|
+
return {
|
|
413
|
+
extensions: ["uuid-ossp"],
|
|
414
|
+
tables: {
|
|
415
|
+
[tasksTable]: tasksTableSpec(tasksTable),
|
|
416
|
+
[historyTable]: tasksTableSpec(historyTable),
|
|
417
|
+
[registryTable]: registryTableSpec(registryTable)
|
|
418
|
+
}
|
|
419
|
+
};
|
|
274
420
|
}
|
|
275
421
|
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
276
422
|
const { id, ...snapshot } = row;
|
|
@@ -284,60 +430,38 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
284
430
|
const queueName = options.queueName ?? "tasks";
|
|
285
431
|
const recreate = options.recreate ?? false;
|
|
286
432
|
const dryRun = options.dryRun ?? false;
|
|
287
|
-
const
|
|
433
|
+
const databases = options.databases ?? [getDb(context)];
|
|
288
434
|
const log = context.logger ?? console;
|
|
289
435
|
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
if (dryRun) {
|
|
294
|
-
const plan = [];
|
|
436
|
+
const spec = tasksSchemaSpec(queueName);
|
|
437
|
+
for (const db of databases) {
|
|
438
|
+
const label = db?.config?.name ?? "db";
|
|
295
439
|
if (recreate) {
|
|
296
|
-
|
|
440
|
+
if (dryRun) {
|
|
441
|
+
log.info?.(
|
|
442
|
+
`[tasks-schema] dryRun \u2014 ${label}: DROP TABLE IF EXISTS ${historyTable}, ${tasksTable}, ${registryTable}`
|
|
443
|
+
);
|
|
444
|
+
} else {
|
|
445
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
446
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
447
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
448
|
+
}
|
|
297
449
|
}
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
450
|
+
const { actions } = await ensureSchema(db, spec, { dryRun, logger: context.logger });
|
|
451
|
+
if (dryRun) {
|
|
452
|
+
if (actions.length === 0) {
|
|
453
|
+
log.info?.(`[tasks-schema] dryRun \u2014 ${label}: queue "${queueName}" already up to date; no DDL`);
|
|
454
|
+
} else {
|
|
455
|
+
log.info?.(
|
|
456
|
+
`[tasks-schema] dryRun \u2014 ${label}: would run ${actions.length} statement(s) for queue "${queueName}":`
|
|
457
|
+
);
|
|
458
|
+
for (const s of actions) log.info?.(` - ${s}`);
|
|
459
|
+
}
|
|
460
|
+
} else if (actions.length > 0) {
|
|
461
|
+
log.info?.(
|
|
462
|
+
`[tasks-schema] ${label}: applied ${actions.length} DDL statement(s) for queue "${queueName}"`
|
|
463
|
+
);
|
|
306
464
|
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (recreate) {
|
|
310
|
-
await db.schema.dropTableIfExists(historyTable);
|
|
311
|
-
await db.schema.dropTableIfExists(tasksTable);
|
|
312
|
-
await db.schema.dropTableIfExists(registryTable);
|
|
313
|
-
}
|
|
314
|
-
if (needsTasks) {
|
|
315
|
-
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
316
|
-
await db.schema.createTable(tasksTable, (t) => {
|
|
317
|
-
defineTasksTable(t, db, tasksTable);
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
if (needsHistory) {
|
|
321
|
-
await db.schema.createTable(historyTable, (t) => {
|
|
322
|
-
defineTasksTable(t, db, historyTable);
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
if (needsRegistry) {
|
|
326
|
-
await db.schema.createTable(registryTable, (t) => {
|
|
327
|
-
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
328
|
-
t.text("queue_name").notNullable();
|
|
329
|
-
t.text("service_group").notNullable();
|
|
330
|
-
t.integer("instance_number").notNullable().defaultTo(1);
|
|
331
|
-
t.text("service_name").notNullable();
|
|
332
|
-
t.text("server_name").notNullable();
|
|
333
|
-
t.integer("pid");
|
|
334
|
-
t.json("metadata");
|
|
335
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
336
|
-
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
337
|
-
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
338
|
-
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
339
|
-
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
340
|
-
});
|
|
341
465
|
}
|
|
342
466
|
}
|
|
343
467
|
async function enqueueTask(context, options) {
|