@usehenri/jobs 0.0.0 → 1.2.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/CHANGELOG.md +311 -0
- package/LICENSE +21 -0
- package/README.md +8 -1
- package/index.js +45 -0
- package/module.js +8 -0
- package/package.json +50 -10
- package/src/batch.js +379 -0
- package/src/config.js +186 -0
- package/src/cron.js +237 -0
- package/src/definitions.js +236 -0
- package/src/duration.js +112 -0
- package/src/errors.js +115 -0
- package/src/jobs.js +1839 -0
- package/src/keys.js +65 -0
- package/src/module.js +442 -0
- package/src/runner.js +918 -0
- package/src/serialize.js +177 -0
- package/src/store/index.js +37 -0
- package/src/store/mongo.js +1334 -0
- package/src/store/schema.js +499 -0
- package/src/store/sql.js +1744 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tables `@usehenri/jobs` owns, and the DDL of every SQL dialect henri
|
|
3
|
+
* can talk to.
|
|
4
|
+
*
|
|
5
|
+
* The queue never goes through a henri model: it owns four tables of its own
|
|
6
|
+
* so it cannot collide with the application's schema, and so a store that
|
|
7
|
+
* has no models at all (a fresh application) still has a queue.
|
|
8
|
+
*
|
|
9
|
+
* ## The upgrade block
|
|
10
|
+
*
|
|
11
|
+
* These tables are `CREATE TABLE IF NOT EXISTS` and there is no migration
|
|
12
|
+
* chain behind them, so a table an older henri created is the table an
|
|
13
|
+
* installation still has. A **new table** is therefore free -- the guarded
|
|
14
|
+
* create makes it appear -- and a **new column** is not.
|
|
15
|
+
*
|
|
16
|
+
* `upgrade()` is the answer: the statements that bring an existing table up
|
|
17
|
+
* to what this version writes, every one of them idempotent, and every one
|
|
18
|
+
* of them **tolerated** by the store (see `SqlStore#install`). A database
|
|
19
|
+
* user who may not `ALTER` never fails a boot over a feature the
|
|
20
|
+
* application does not use; the feature itself asks whether its column is
|
|
21
|
+
* there (`SqlStore#concurrent`) and refuses with the install line when it is
|
|
22
|
+
* not. The same statements run on a fresh database, where they find their
|
|
23
|
+
* work already done.
|
|
24
|
+
*
|
|
25
|
+
* Every moment is stored as a BIGINT of milliseconds since the epoch rather
|
|
26
|
+
* than a timestamp column: sqlite has no date type, MySQL, PostgreSQL and
|
|
27
|
+
* MSSQL disagree on the precision and on the time zone of a bare
|
|
28
|
+
* `TIMESTAMP`, and the claim compares `run_at` to the runner's clock -- a
|
|
29
|
+
* comparison that has to mean the same thing on every dialect. Numbers read
|
|
30
|
+
* back as strings on some drivers (BIGINT over the pg protocol), so every
|
|
31
|
+
* read coerces.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** Names an application may give the tables */
|
|
35
|
+
const { coded } = require('../errors');
|
|
36
|
+
|
|
37
|
+
const SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
38
|
+
|
|
39
|
+
const DIALECTS = {
|
|
40
|
+
mssql: {
|
|
41
|
+
/**
|
|
42
|
+
* Adds a column, only when the table has none by that name
|
|
43
|
+
*
|
|
44
|
+
* @param {string} table The table name
|
|
45
|
+
* @param {string} column The column name
|
|
46
|
+
* @param {string} type The column type
|
|
47
|
+
* @returns {string} The statement
|
|
48
|
+
*/
|
|
49
|
+
addColumn: (table, column, type) =>
|
|
50
|
+
`IF COL_LENGTH('${table}', '${column}') IS NULL ALTER TABLE [${table}] ADD ${column} ${type} NULL`,
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wraps a statement so it only runs when the index is missing
|
|
54
|
+
*
|
|
55
|
+
* @param {string} table The table name
|
|
56
|
+
* @param {string} index The index name
|
|
57
|
+
* @param {string} statement The CREATE INDEX statement
|
|
58
|
+
* @returns {string} The guarded statement
|
|
59
|
+
*/
|
|
60
|
+
guardIndex: (table, index, statement) =>
|
|
61
|
+
`IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = '${index}' AND object_id = OBJECT_ID('${table}')) ${statement}`,
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wraps a CREATE TABLE so it only runs when the table is missing
|
|
65
|
+
*
|
|
66
|
+
* @param {string} table The table name
|
|
67
|
+
* @param {string} statement The CREATE TABLE statement
|
|
68
|
+
* @returns {string} The guarded statement
|
|
69
|
+
*/
|
|
70
|
+
guardTable: (table, statement) =>
|
|
71
|
+
`IF OBJECT_ID('${table}', 'U') IS NULL ${statement}`,
|
|
72
|
+
|
|
73
|
+
ifNotExists: '',
|
|
74
|
+
indexIfNotExists: '',
|
|
75
|
+
inlineIndexes: false,
|
|
76
|
+
int: 'INT',
|
|
77
|
+
// MSSQL is the one dialect that treats NULLs as equal in a unique index
|
|
78
|
+
partialUnique: true,
|
|
79
|
+
quote: (identifier) => `[${identifier}]`,
|
|
80
|
+
text: 'NVARCHAR(MAX)',
|
|
81
|
+
},
|
|
82
|
+
mysql: {
|
|
83
|
+
// MySQL has no ADD COLUMN IF NOT EXISTS: a second run answers 1060,
|
|
84
|
+
// which the store tolerates like every other upgrade statement
|
|
85
|
+
addColumn: (table, column, type) =>
|
|
86
|
+
`ALTER TABLE \`${table}\` ADD COLUMN ${column} ${type} NULL`,
|
|
87
|
+
ifNotExists: 'IF NOT EXISTS',
|
|
88
|
+
// MySQL has no CREATE INDEX IF NOT EXISTS: the indexes are declared in
|
|
89
|
+
// the CREATE TABLE, which is guarded. An index the create cannot carry
|
|
90
|
+
// (a `late` one, on a table that is already there) is written bare and
|
|
91
|
+
// tolerated when it answers 1061
|
|
92
|
+
indexIfNotExists: '',
|
|
93
|
+
inlineIndexes: true,
|
|
94
|
+
int: 'INT',
|
|
95
|
+
partialUnique: false,
|
|
96
|
+
quote: (identifier) => `\`${identifier}\``,
|
|
97
|
+
// TEXT stops at 65535 bytes and the arguments alone may reach 65536
|
|
98
|
+
text: 'MEDIUMTEXT',
|
|
99
|
+
},
|
|
100
|
+
postgres: {
|
|
101
|
+
addColumn: (table, column, type) =>
|
|
102
|
+
`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS ${column} ${type} NULL`,
|
|
103
|
+
ifNotExists: 'IF NOT EXISTS',
|
|
104
|
+
indexIfNotExists: 'IF NOT EXISTS',
|
|
105
|
+
inlineIndexes: false,
|
|
106
|
+
int: 'INTEGER',
|
|
107
|
+
partialUnique: false,
|
|
108
|
+
quote: (identifier) => `"${identifier}"`,
|
|
109
|
+
text: 'TEXT',
|
|
110
|
+
},
|
|
111
|
+
sqlite: {
|
|
112
|
+
// SQLite has no ADD COLUMN IF NOT EXISTS either: a second run answers
|
|
113
|
+
// "duplicate column name", which the store tolerates
|
|
114
|
+
addColumn: (table, column, type) =>
|
|
115
|
+
`ALTER TABLE "${table}" ADD COLUMN ${column} ${type} NULL`,
|
|
116
|
+
ifNotExists: 'IF NOT EXISTS',
|
|
117
|
+
indexIfNotExists: 'IF NOT EXISTS',
|
|
118
|
+
inlineIndexes: false,
|
|
119
|
+
int: 'INTEGER',
|
|
120
|
+
partialUnique: false,
|
|
121
|
+
quote: (identifier) => `"${identifier}"`,
|
|
122
|
+
text: 'TEXT',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The columns an older henri did not write, and the type they take.
|
|
128
|
+
*
|
|
129
|
+
* One entry per column added after a version that shipped: the create
|
|
130
|
+
* statement declares them and `upgrade()` adds them to a table that has
|
|
131
|
+
* them not.
|
|
132
|
+
*/
|
|
133
|
+
const ADDED = [
|
|
134
|
+
{ column: 'concurrency_key', type: 'VARCHAR(190)' },
|
|
135
|
+
{ column: 'batch_id', type: 'VARCHAR(36)' },
|
|
136
|
+
// 190 is the width core gives a tenant (`base/tenancy.js`, MAX_TENANT)
|
|
137
|
+
// and the width `@usehenri/webhooks` gives an endpoint's owner: what
|
|
138
|
+
// MySQL indexes in a utf8mb4 key. A tenant is never truncated to fit
|
|
139
|
+
{ column: 'tenant', type: 'VARCHAR(190)' },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The columns of the jobs table, in order
|
|
144
|
+
*
|
|
145
|
+
* @param {object} dialect A dialect description
|
|
146
|
+
* @returns {Array<string>} The column definitions
|
|
147
|
+
*/
|
|
148
|
+
const jobColumns = (dialect) => [
|
|
149
|
+
'id VARCHAR(36) NOT NULL',
|
|
150
|
+
'queue VARCHAR(120) NOT NULL',
|
|
151
|
+
'name VARCHAR(120) NOT NULL',
|
|
152
|
+
`args ${dialect.text} NOT NULL`,
|
|
153
|
+
'state VARCHAR(16) NOT NULL',
|
|
154
|
+
`priority ${dialect.int} NOT NULL`,
|
|
155
|
+
`attempts ${dialect.int} NOT NULL`,
|
|
156
|
+
`max_attempts ${dialect.int} NOT NULL`,
|
|
157
|
+
`timeout_ms ${dialect.int} NULL`,
|
|
158
|
+
'run_at BIGINT NOT NULL',
|
|
159
|
+
'created_at BIGINT NOT NULL',
|
|
160
|
+
'updated_at BIGINT NOT NULL',
|
|
161
|
+
'started_at BIGINT NULL',
|
|
162
|
+
'finished_at BIGINT NULL',
|
|
163
|
+
`duration_ms ${dialect.int} NULL`,
|
|
164
|
+
'claimed_by VARCHAR(120) NULL',
|
|
165
|
+
'claimed_at BIGINT NULL',
|
|
166
|
+
'heartbeat_at BIGINT NULL',
|
|
167
|
+
'claim_token VARCHAR(36) NULL',
|
|
168
|
+
`error_message ${dialect.text} NULL`,
|
|
169
|
+
`error_stack ${dialect.text} NULL`,
|
|
170
|
+
`history ${dialect.text} NULL`,
|
|
171
|
+
'unique_key VARCHAR(190) NULL',
|
|
172
|
+
'concurrency_key VARCHAR(190) NULL',
|
|
173
|
+
'batch_id VARCHAR(36) NULL',
|
|
174
|
+
'tenant VARCHAR(190) NULL',
|
|
175
|
+
'PRIMARY KEY (id)',
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The columns of the concurrency table, in order
|
|
180
|
+
*
|
|
181
|
+
* One row is one slot of one key: `(limit_key, slot)` is the primary key, so
|
|
182
|
+
* `slot` counts from zero to the job's limit and an INSERT is what takes it.
|
|
183
|
+
* That unique index is the whole bound -- see `SqlStore#takeSlot`.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} dialect A dialect description
|
|
186
|
+
* @returns {Array<string>} The column definitions
|
|
187
|
+
*/
|
|
188
|
+
const limitColumns = (dialect) => [
|
|
189
|
+
'limit_key VARCHAR(190) NOT NULL',
|
|
190
|
+
`slot ${dialect.int} NOT NULL`,
|
|
191
|
+
'job_id VARCHAR(36) NULL',
|
|
192
|
+
'runner VARCHAR(120) NOT NULL',
|
|
193
|
+
'taken_at BIGINT NOT NULL',
|
|
194
|
+
'heartbeat_at BIGINT NOT NULL',
|
|
195
|
+
'PRIMARY KEY (limit_key, slot)',
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The indexes of the concurrency table
|
|
200
|
+
*
|
|
201
|
+
* @param {string} table The table name
|
|
202
|
+
* @returns {Array<object>} `{ name, columns, unique }` entries
|
|
203
|
+
*/
|
|
204
|
+
const limitIndexes = (table) => [
|
|
205
|
+
{ columns: ['heartbeat_at'], name: `${table}_stale`, unique: false },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The columns of the batches table, in order
|
|
210
|
+
*
|
|
211
|
+
* A batch counts: `total` is what was enqueued under it and is written once,
|
|
212
|
+
* when the batch is sealed; `done` and `failed` are advanced by the same
|
|
213
|
+
* token-guarded write that records an attempt's outcome, one statement per
|
|
214
|
+
* job. `finished_at` is stamped after the callback has been enqueued, so a
|
|
215
|
+
* crash in between leaves the batch unfinished and the sweep settles it
|
|
216
|
+
* again -- the enqueue is idempotent (see `../keys.js`).
|
|
217
|
+
*
|
|
218
|
+
* @param {object} dialect A dialect description
|
|
219
|
+
* @returns {Array<string>} The column definitions
|
|
220
|
+
*/
|
|
221
|
+
const batchColumns = (dialect) => [
|
|
222
|
+
'id VARCHAR(36) NOT NULL',
|
|
223
|
+
'name VARCHAR(190) NULL',
|
|
224
|
+
'callback VARCHAR(120) NULL',
|
|
225
|
+
`callback_args ${dialect.text} NULL`,
|
|
226
|
+
`callback_options ${dialect.text} NULL`,
|
|
227
|
+
'callback_id VARCHAR(36) NULL',
|
|
228
|
+
`total ${dialect.int} NOT NULL`,
|
|
229
|
+
`done ${dialect.int} NOT NULL`,
|
|
230
|
+
`failed ${dialect.int} NOT NULL`,
|
|
231
|
+
'created_at BIGINT NOT NULL',
|
|
232
|
+
'updated_at BIGINT NOT NULL',
|
|
233
|
+
'sealed_at BIGINT NULL',
|
|
234
|
+
'finished_at BIGINT NULL',
|
|
235
|
+
'PRIMARY KEY (id)',
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The indexes of the batches table
|
|
240
|
+
*
|
|
241
|
+
* @param {string} table The table name
|
|
242
|
+
* @returns {Array<object>} `{ name, columns, unique }` entries
|
|
243
|
+
*/
|
|
244
|
+
const batchIndexes = (table) => [
|
|
245
|
+
{
|
|
246
|
+
columns: ['finished_at', 'updated_at'],
|
|
247
|
+
name: `${table}_open`,
|
|
248
|
+
unique: false,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The columns of the schedules table, in order
|
|
254
|
+
*
|
|
255
|
+
* @param {object} dialect A dialect description
|
|
256
|
+
* @returns {Array<string>} The column definitions
|
|
257
|
+
*/
|
|
258
|
+
const scheduleColumns = (dialect) => [
|
|
259
|
+
'name VARCHAR(190) NOT NULL',
|
|
260
|
+
'job VARCHAR(120) NOT NULL',
|
|
261
|
+
'spec VARCHAR(190) NOT NULL',
|
|
262
|
+
'next_run_at BIGINT NOT NULL',
|
|
263
|
+
'last_run_at BIGINT NULL',
|
|
264
|
+
'token VARCHAR(36) NULL',
|
|
265
|
+
'created_at BIGINT NOT NULL',
|
|
266
|
+
'updated_at BIGINT NOT NULL',
|
|
267
|
+
'PRIMARY KEY (name)',
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The indexes of the jobs table
|
|
272
|
+
*
|
|
273
|
+
* @param {string} table The table name
|
|
274
|
+
* @returns {Array<object>} `{ name, columns, unique }` entries
|
|
275
|
+
*/
|
|
276
|
+
const jobIndexes = (table) => [
|
|
277
|
+
{
|
|
278
|
+
columns: ['state', 'queue', 'priority', 'run_at'],
|
|
279
|
+
name: `${table}_claim`,
|
|
280
|
+
unique: false,
|
|
281
|
+
},
|
|
282
|
+
{ columns: ['claim_token'], name: `${table}_token`, unique: false },
|
|
283
|
+
{
|
|
284
|
+
columns: ['state', 'finished_at'],
|
|
285
|
+
name: `${table}_finished`,
|
|
286
|
+
unique: false,
|
|
287
|
+
},
|
|
288
|
+
{ columns: ['unique_key'], name: `${table}_unique`, unique: true },
|
|
289
|
+
// `late`: it arrived with a column an older table has not, so it belongs
|
|
290
|
+
// to the upgrade block on every dialect rather than to the create
|
|
291
|
+
{
|
|
292
|
+
columns: ['state', 'concurrency_key', 'run_at'],
|
|
293
|
+
late: true,
|
|
294
|
+
name: `${table}_limited`,
|
|
295
|
+
unique: false,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
columns: ['batch_id'],
|
|
299
|
+
late: true,
|
|
300
|
+
name: `${table}_batch`,
|
|
301
|
+
unique: false,
|
|
302
|
+
},
|
|
303
|
+
// The listing `henri jobs:list --tenant` makes, and nothing else: the
|
|
304
|
+
// claim is deliberately **not** narrowed by tenant, so this index is
|
|
305
|
+
// never on the hot path (see `SqlStore#claimStatement`)
|
|
306
|
+
{
|
|
307
|
+
columns: ['tenant', 'state', 'run_at'],
|
|
308
|
+
late: true,
|
|
309
|
+
name: `${table}_tenant`,
|
|
310
|
+
unique: false,
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The statement that creates one index
|
|
316
|
+
*
|
|
317
|
+
* @param {object} dialect A dialect description
|
|
318
|
+
* @param {string} table The table name
|
|
319
|
+
* @param {object} index An index description
|
|
320
|
+
* @returns {string} The statement
|
|
321
|
+
*/
|
|
322
|
+
const indexStatement = (dialect, table, index) => {
|
|
323
|
+
const filter =
|
|
324
|
+
index.unique && dialect.partialUnique
|
|
325
|
+
? ` WHERE ${index.columns[0]} IS NOT NULL`
|
|
326
|
+
: '';
|
|
327
|
+
const statement = [
|
|
328
|
+
`CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX`,
|
|
329
|
+
dialect.guardIndex ? '' : dialect.indexIfNotExists,
|
|
330
|
+
`${dialect.quote(index.name)} ON ${dialect.quote(table)} (${index.columns.join(', ')})${filter}`,
|
|
331
|
+
]
|
|
332
|
+
.filter(Boolean)
|
|
333
|
+
.join(' ');
|
|
334
|
+
|
|
335
|
+
return dialect.guardIndex
|
|
336
|
+
? dialect.guardIndex(table, index.name, statement)
|
|
337
|
+
: statement;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The statements that create a table and its indexes, in order
|
|
342
|
+
*
|
|
343
|
+
* An index marked `late` is left out: it names a column an older table does
|
|
344
|
+
* not have, so it is the upgrade block's, on every dialect at once.
|
|
345
|
+
*
|
|
346
|
+
* @param {object} dialect A dialect description
|
|
347
|
+
* @param {string} table The table name
|
|
348
|
+
* @param {Array<string>} columns The column definitions
|
|
349
|
+
* @param {Array<object>} all The indexes
|
|
350
|
+
* @returns {Array<string>} The statements to run, in order
|
|
351
|
+
*/
|
|
352
|
+
const statementsFor = (dialect, table, columns, all) => {
|
|
353
|
+
const quoted = dialect.quote(table);
|
|
354
|
+
const definitions = [...columns];
|
|
355
|
+
const statements = [];
|
|
356
|
+
const indexes = all.filter((index) => !index.late);
|
|
357
|
+
|
|
358
|
+
if (dialect.inlineIndexes) {
|
|
359
|
+
for (const index of indexes) {
|
|
360
|
+
definitions.push(
|
|
361
|
+
`${index.unique ? 'UNIQUE KEY' : 'KEY'} ${dialect.quote(index.name)} (${index.columns.join(', ')})`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const create = [
|
|
367
|
+
'CREATE TABLE',
|
|
368
|
+
dialect.ifNotExists,
|
|
369
|
+
`${quoted} (\n ${definitions.join(',\n ')}\n)`,
|
|
370
|
+
]
|
|
371
|
+
.filter(Boolean)
|
|
372
|
+
.join(' ');
|
|
373
|
+
|
|
374
|
+
statements.push(
|
|
375
|
+
dialect.guardTable ? dialect.guardTable(table, create) : create
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
if (dialect.inlineIndexes) {
|
|
379
|
+
return statements;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const index of indexes) {
|
|
383
|
+
statements.push(indexStatement(dialect, table, index));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return statements;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The statements that bring a table an older henri created up to date
|
|
391
|
+
*
|
|
392
|
+
* Every one of them is idempotent and every one of them is tolerated by the
|
|
393
|
+
* store: a database user who may not `ALTER` never fails a boot over a
|
|
394
|
+
* feature the application does not use. What decides whether the feature
|
|
395
|
+
* works is asking the table, not whether these ran.
|
|
396
|
+
*
|
|
397
|
+
* @param {string} name The dialect (sqlite, postgres, mysql, mssql)
|
|
398
|
+
* @param {object} tables `{ jobs, schedules, limits, batches }` table names
|
|
399
|
+
* @returns {Array<string>} The statements
|
|
400
|
+
* @throws {Error} When the dialect is unknown
|
|
401
|
+
*/
|
|
402
|
+
const upgrade = (name, tables) => {
|
|
403
|
+
const dialect = DIALECTS[name];
|
|
404
|
+
|
|
405
|
+
if (!dialect) {
|
|
406
|
+
throw coded(
|
|
407
|
+
'HENRI_JOB_UNSUPPORTED_STORE',
|
|
408
|
+
`@usehenri/jobs: unsupported SQL dialect "${name}"`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return [
|
|
413
|
+
...ADDED.map((added) =>
|
|
414
|
+
dialect.addColumn(tables.jobs, added.column, added.type)
|
|
415
|
+
),
|
|
416
|
+
...jobIndexes(tables.jobs)
|
|
417
|
+
.filter((index) => index.late)
|
|
418
|
+
.map((index) => indexStatement(dialect, tables.jobs, index)),
|
|
419
|
+
];
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Every statement `henri jobs:install` runs, in order
|
|
424
|
+
*
|
|
425
|
+
* All of them are idempotent: running the install twice, or against a
|
|
426
|
+
* database another runner already prepared, changes nothing.
|
|
427
|
+
*
|
|
428
|
+
* @param {string} name The dialect (sqlite, postgres, mysql, mssql)
|
|
429
|
+
* @param {object} tables `{ jobs, schedules, limits, batches }` table names
|
|
430
|
+
* @returns {Array<string>} The statements
|
|
431
|
+
* @throws {Error} When the dialect or a table name is unknown
|
|
432
|
+
*/
|
|
433
|
+
const install = (name, tables) => {
|
|
434
|
+
const dialect = DIALECTS[name];
|
|
435
|
+
|
|
436
|
+
if (!dialect) {
|
|
437
|
+
throw coded(
|
|
438
|
+
'HENRI_JOB_UNSUPPORTED_STORE',
|
|
439
|
+
`@usehenri/jobs: unsupported SQL dialect "${name}"`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
for (const table of Object.values(tables)) {
|
|
444
|
+
if (!SAFE_NAME.test(table)) {
|
|
445
|
+
throw coded(
|
|
446
|
+
'HENRI_CONFIG_INVALID',
|
|
447
|
+
`@usehenri/jobs: invalid table name "${table}": letters, digits and underscores only`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return [
|
|
453
|
+
...statementsFor(
|
|
454
|
+
dialect,
|
|
455
|
+
tables.jobs,
|
|
456
|
+
jobColumns(dialect),
|
|
457
|
+
jobIndexes(tables.jobs)
|
|
458
|
+
),
|
|
459
|
+
...statementsFor(dialect, tables.schedules, scheduleColumns(dialect), []),
|
|
460
|
+
...statementsFor(
|
|
461
|
+
dialect,
|
|
462
|
+
tables.limits,
|
|
463
|
+
limitColumns(dialect),
|
|
464
|
+
limitIndexes(tables.limits)
|
|
465
|
+
),
|
|
466
|
+
...statementsFor(
|
|
467
|
+
dialect,
|
|
468
|
+
tables.batches,
|
|
469
|
+
batchColumns(dialect),
|
|
470
|
+
batchIndexes(tables.batches)
|
|
471
|
+
),
|
|
472
|
+
...upgrade(name, tables),
|
|
473
|
+
];
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The statements that drop the tables, newest first
|
|
478
|
+
*
|
|
479
|
+
* @param {string} name The dialect
|
|
480
|
+
* @param {object} tables `{ jobs, schedules, limits, batches }` table names
|
|
481
|
+
* @returns {Array<string>} The statements
|
|
482
|
+
* @throws {Error} When the dialect is unknown
|
|
483
|
+
*/
|
|
484
|
+
const uninstall = (name, tables) => {
|
|
485
|
+
const dialect = DIALECTS[name];
|
|
486
|
+
|
|
487
|
+
if (!dialect) {
|
|
488
|
+
throw coded(
|
|
489
|
+
'HENRI_JOB_UNSUPPORTED_STORE',
|
|
490
|
+
`@usehenri/jobs: unsupported SQL dialect "${name}"`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return [tables.batches, tables.limits, tables.schedules, tables.jobs].map(
|
|
495
|
+
(table) => `DROP TABLE IF EXISTS ${dialect.quote(table)}`
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
module.exports = { ADDED, DIALECTS, install, uninstall, upgrade };
|