@nmakarov/cli-toolkit 0.33.0 → 0.36.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 +455 -73
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +455 -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 +480 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +474 -73
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +37 -0
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +37 -0
- package/dist/init.js.map +1 -1
- package/dist/params.cjs +37 -0
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +37 -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.js
CHANGED
|
@@ -88,6 +88,91 @@ import os from "os";
|
|
|
88
88
|
// src/tasks/taskUtils.js
|
|
89
89
|
import { randomUUID } from "crypto";
|
|
90
90
|
|
|
91
|
+
// src/db/ensure.js
|
|
92
|
+
var dbLabel = (db) => db?.config?.name ?? "db";
|
|
93
|
+
async function ensureExtension(db, name, options = {}) {
|
|
94
|
+
const action = `CREATE EXTENSION IF NOT EXISTS "${name}"`;
|
|
95
|
+
if (!options.dryRun) {
|
|
96
|
+
await db.raw(action);
|
|
97
|
+
}
|
|
98
|
+
options.logger?.silly?.(`[ensure] ${dbLabel(db)}: ${action}${options.dryRun ? " (dryRun)" : ""}`);
|
|
99
|
+
return { action };
|
|
100
|
+
}
|
|
101
|
+
async function ensureTable(db, tableName, spec, options = {}) {
|
|
102
|
+
const { dryRun = false, logger } = options;
|
|
103
|
+
const actions = [];
|
|
104
|
+
const exists = await db.tableExists(tableName);
|
|
105
|
+
if (!exists) {
|
|
106
|
+
actions.push(`CREATE TABLE ${tableName} (${Object.keys(spec.columns).length} columns)`);
|
|
107
|
+
if (!dryRun) {
|
|
108
|
+
await db.schema.createTable(tableName, (t) => {
|
|
109
|
+
for (const define of Object.values(spec.columns)) {
|
|
110
|
+
define(t, db);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
const missing = [];
|
|
116
|
+
for (const column of Object.keys(spec.columns)) {
|
|
117
|
+
if (!await db.schema.hasColumn(tableName, column)) {
|
|
118
|
+
missing.push(column);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (missing.length > 0) {
|
|
122
|
+
actions.push(`ALTER TABLE ${tableName} ADD COLUMN ${missing.join(", ")}`);
|
|
123
|
+
if (!dryRun) {
|
|
124
|
+
await db.schema.alterTable(tableName, (t) => {
|
|
125
|
+
for (const column of missing) {
|
|
126
|
+
spec.columns[column](t, db);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const index of spec.indexes ?? []) {
|
|
133
|
+
const indexActions = await ensureIndex(db, tableName, index, options);
|
|
134
|
+
actions.push(...indexActions);
|
|
135
|
+
}
|
|
136
|
+
for (const action of actions) {
|
|
137
|
+
logger?.silly?.(`[ensure] ${dbLabel(db)}: ${action}${dryRun ? " (dryRun)" : ""}`);
|
|
138
|
+
}
|
|
139
|
+
return actions;
|
|
140
|
+
}
|
|
141
|
+
async function ensureIndex(db, tableName, index, options = {}) {
|
|
142
|
+
const { dryRun = false } = options;
|
|
143
|
+
const kind = index.unique ? "UNIQUE INDEX" : "INDEX";
|
|
144
|
+
const cols = index.columns.map((c) => `"${c}"`).join(", ");
|
|
145
|
+
const isPg = String(db?.config?.connectionString ?? "").startsWith("postgresql");
|
|
146
|
+
if (isPg) {
|
|
147
|
+
const sql2 = `CREATE ${kind} IF NOT EXISTS "${index.name}" ON "${tableName}" (${cols})`;
|
|
148
|
+
const { rows } = await db.raw(`SELECT 1 FROM pg_indexes WHERE indexname = ?`, [index.name]);
|
|
149
|
+
if (rows.length > 0) return [];
|
|
150
|
+
if (!dryRun) await db.raw(sql2);
|
|
151
|
+
return [sql2];
|
|
152
|
+
}
|
|
153
|
+
const sql = `CREATE ${kind} ${index.name} ON ${tableName} (${cols})`;
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
try {
|
|
156
|
+
await db.raw(sql);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!/already exists|duplicate/i.test(error?.message ?? "")) throw error;
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [sql];
|
|
163
|
+
}
|
|
164
|
+
async function ensureSchema(db, spec, options = {}) {
|
|
165
|
+
const actions = [];
|
|
166
|
+
for (const extension of spec.extensions ?? []) {
|
|
167
|
+
const { action } = await ensureExtension(db, extension, options);
|
|
168
|
+
if (options.dryRun) actions.push(action);
|
|
169
|
+
}
|
|
170
|
+
for (const [tableName, tableSpec] of Object.entries(spec.tables ?? {})) {
|
|
171
|
+
actions.push(...await ensureTable(db, tableName, tableSpec, options));
|
|
172
|
+
}
|
|
173
|
+
return { database: dbLabel(db), actions };
|
|
174
|
+
}
|
|
175
|
+
|
|
91
176
|
// src/tasks/time-matcher.js
|
|
92
177
|
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
93
178
|
function resolveAsterisks(field, range) {
|
|
@@ -172,29 +257,90 @@ function queueToTableNames(queueName) {
|
|
|
172
257
|
registryTable: `${queueName}_services_registry`
|
|
173
258
|
};
|
|
174
259
|
}
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
260
|
+
function tasksTableSpec(tableNameForIndex) {
|
|
261
|
+
return {
|
|
262
|
+
columns: {
|
|
263
|
+
id: (t, db) => t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()")),
|
|
264
|
+
created_at: (t, db) => t.timestamp("created_at").notNullable().defaultTo(db.fn.now()),
|
|
265
|
+
started_at: (t) => t.timestamp("started_at"),
|
|
266
|
+
completed_at: (t) => t.timestamp("completed_at"),
|
|
267
|
+
/*
|
|
268
|
+
* Priority: lower number = claimed first (see claimNextRunnableTask: ORDER BY priority ASC).
|
|
269
|
+
* Suggested range 0–100: 0 = most urgent, 100 = least; default 50 for normal work.
|
|
270
|
+
*/
|
|
271
|
+
priority: (t) => t.integer("priority").notNullable().defaultTo(50),
|
|
272
|
+
schedule: (t) => t.text("schedule"),
|
|
273
|
+
next_run_at: (t) => t.timestamp("next_run_at").defaultTo(null),
|
|
274
|
+
past_due: (t) => t.timestamp("past_due").defaultTo(null),
|
|
275
|
+
name: (t) => t.text("name").notNullable(),
|
|
276
|
+
opid: (t) => t.text("opid"),
|
|
277
|
+
params: (t) => t.jsonb("params"),
|
|
278
|
+
// those are target identifiers, kind of who is going to run a task.
|
|
279
|
+
service_group: (t) => t.text("service_group"),
|
|
280
|
+
// harvester, loader, photos, ...
|
|
281
|
+
instance_number: (t) => t.integer("instance_number"),
|
|
282
|
+
service_name: (t) => t.text("service_name"),
|
|
283
|
+
// that's a "<server_name>_<service_group>_<instance_number>"
|
|
284
|
+
server_name: (t) => t.text("server_name"),
|
|
285
|
+
// filled by runner when registering, auto.
|
|
286
|
+
status: (t) => t.text("status").notNullable().defaultTo("idle"),
|
|
287
|
+
// idle, running, completed, failed, paused
|
|
288
|
+
status_changed_at: (t) => t.timestamp("status_changed_at").defaultTo(null),
|
|
289
|
+
progress: (t) => t.text("progress"),
|
|
290
|
+
success: (t) => t.boolean("success"),
|
|
291
|
+
results: (t) => t.jsonb("results")
|
|
292
|
+
},
|
|
293
|
+
indexes: [
|
|
294
|
+
{
|
|
295
|
+
columns: ["service_group", "status", "priority", "created_at"],
|
|
296
|
+
name: `${tableNameForIndex}_claim_idx`
|
|
297
|
+
},
|
|
298
|
+
{ columns: ["service_group", "name"], name: `${tableNameForIndex}_group_name_idx` }
|
|
299
|
+
]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function registryTableSpec(registryTable) {
|
|
303
|
+
return {
|
|
304
|
+
columns: {
|
|
305
|
+
id: (t, db) => t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()")),
|
|
306
|
+
queue_name: (t) => t.text("queue_name").notNullable(),
|
|
307
|
+
service_group: (t) => t.text("service_group").notNullable(),
|
|
308
|
+
// harvester, loader, photos, ...
|
|
309
|
+
instance_number: (t) => t.integer("instance_number").notNullable().defaultTo(1),
|
|
310
|
+
service_name: (t) => t.text("service_name").notNullable(),
|
|
311
|
+
// that's a "<server_name>_<service_group>_<instance_number>"
|
|
312
|
+
server_name: (t) => t.text("server_name").notNullable(),
|
|
313
|
+
// filled by runner when registering, auto.
|
|
314
|
+
pid: (t) => t.integer("pid"),
|
|
315
|
+
metadata: (t) => t.json("metadata"),
|
|
316
|
+
created_at: (t, db) => t.timestamp("created_at").notNullable().defaultTo(db.fn.now()),
|
|
317
|
+
last_seen_at: (t, db) => t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now())
|
|
318
|
+
},
|
|
319
|
+
indexes: [
|
|
320
|
+
{
|
|
321
|
+
columns: ["queue_name", "service_name"],
|
|
322
|
+
name: `${registryTable}_queue_name_service_name_uniq`,
|
|
323
|
+
unique: true
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
columns: ["queue_name", "service_group", "last_seen_at"],
|
|
327
|
+
name: `${registryTable}_queue_group_seen_idx`
|
|
328
|
+
},
|
|
329
|
+
{ columns: ["queue_name", "last_seen_at"], name: `${registryTable}_queue_seen_idx` }
|
|
330
|
+
]
|
|
331
|
+
// 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.
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function tasksSchemaSpec(queueName = "tasks") {
|
|
335
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
336
|
+
return {
|
|
337
|
+
extensions: ["uuid-ossp"],
|
|
338
|
+
tables: {
|
|
339
|
+
[tasksTable]: tasksTableSpec(tasksTable),
|
|
340
|
+
[historyTable]: tasksTableSpec(historyTable),
|
|
341
|
+
[registryTable]: registryTableSpec(registryTable)
|
|
342
|
+
}
|
|
343
|
+
};
|
|
198
344
|
}
|
|
199
345
|
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
200
346
|
const { id, ...snapshot } = row;
|
|
@@ -208,60 +354,38 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
208
354
|
const queueName = options.queueName ?? "tasks";
|
|
209
355
|
const recreate = options.recreate ?? false;
|
|
210
356
|
const dryRun = options.dryRun ?? false;
|
|
211
|
-
const
|
|
357
|
+
const databases = options.databases ?? [getDb(context)];
|
|
212
358
|
const log = context.logger ?? console;
|
|
213
359
|
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
if (dryRun) {
|
|
218
|
-
const plan = [];
|
|
360
|
+
const spec = tasksSchemaSpec(queueName);
|
|
361
|
+
for (const db of databases) {
|
|
362
|
+
const label = db?.config?.name ?? "db";
|
|
219
363
|
if (recreate) {
|
|
220
|
-
|
|
364
|
+
if (dryRun) {
|
|
365
|
+
log.info?.(
|
|
366
|
+
`[tasks-schema] dryRun \u2014 ${label}: DROP TABLE IF EXISTS ${historyTable}, ${tasksTable}, ${registryTable}`
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
370
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
371
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
372
|
+
}
|
|
221
373
|
}
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
374
|
+
const { actions } = await ensureSchema(db, spec, { dryRun, logger: context.logger });
|
|
375
|
+
if (dryRun) {
|
|
376
|
+
if (actions.length === 0) {
|
|
377
|
+
log.info?.(`[tasks-schema] dryRun \u2014 ${label}: queue "${queueName}" already up to date; no DDL`);
|
|
378
|
+
} else {
|
|
379
|
+
log.info?.(
|
|
380
|
+
`[tasks-schema] dryRun \u2014 ${label}: would run ${actions.length} statement(s) for queue "${queueName}":`
|
|
381
|
+
);
|
|
382
|
+
for (const s of actions) log.info?.(` - ${s}`);
|
|
383
|
+
}
|
|
384
|
+
} else if (actions.length > 0) {
|
|
385
|
+
log.info?.(
|
|
386
|
+
`[tasks-schema] ${label}: applied ${actions.length} DDL statement(s) for queue "${queueName}"`
|
|
387
|
+
);
|
|
230
388
|
}
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (recreate) {
|
|
234
|
-
await db.schema.dropTableIfExists(historyTable);
|
|
235
|
-
await db.schema.dropTableIfExists(tasksTable);
|
|
236
|
-
await db.schema.dropTableIfExists(registryTable);
|
|
237
|
-
}
|
|
238
|
-
if (needsTasks) {
|
|
239
|
-
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
240
|
-
await db.schema.createTable(tasksTable, (t) => {
|
|
241
|
-
defineTasksTable(t, db, tasksTable);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
if (needsHistory) {
|
|
245
|
-
await db.schema.createTable(historyTable, (t) => {
|
|
246
|
-
defineTasksTable(t, db, historyTable);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
if (needsRegistry) {
|
|
250
|
-
await db.schema.createTable(registryTable, (t) => {
|
|
251
|
-
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
252
|
-
t.text("queue_name").notNullable();
|
|
253
|
-
t.text("service_group").notNullable();
|
|
254
|
-
t.integer("instance_number").notNullable().defaultTo(1);
|
|
255
|
-
t.text("service_name").notNullable();
|
|
256
|
-
t.text("server_name").notNullable();
|
|
257
|
-
t.integer("pid");
|
|
258
|
-
t.json("metadata");
|
|
259
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
260
|
-
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
261
|
-
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
262
|
-
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
263
|
-
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
264
|
-
});
|
|
265
389
|
}
|
|
266
390
|
}
|
|
267
391
|
async function enqueueTask(context, options) {
|