@sqlane/core 0.0.2 → 0.1.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/README.md +44 -6
- package/package.json +5 -1
- package/src/adapters/mysql2.js +61 -0
- package/src/adapters/pg.js +129 -1
- package/src/backoff.js +24 -0
- package/src/create_sqlane.js +191 -15
- package/src/defaults.js +38 -0
- package/src/define_job.js +34 -3
- package/src/duration.js +27 -0
- package/src/enqueue.js +132 -0
- package/src/error_json.js +30 -0
- package/src/errors.js +12 -0
- package/src/hint_channel.js +107 -0
- package/src/index.js +1 -0
- package/src/migrations.js +155 -2
- package/src/mutex_keys.js +34 -0
- package/src/new_job.js +128 -0
- package/src/payload.js +54 -0
- package/src/registry.js +169 -0
- package/src/runner.js +974 -0
- package/src/schema/mysql.js +74 -0
- package/src/schema/postgres.js +67 -0
- package/src/store/mysql_enqueue.js +98 -0
- package/src/store/mysql_final.js +424 -0
- package/src/store/mysql_worker.js +250 -0
- package/src/store/postgres_enqueue.js +54 -0
- package/src/store/postgres_final.js +407 -0
- package/src/store/postgres_worker.js +224 -0
- package/src/store/store.js +136 -0
- package/src/transient_error.js +22 -0
- package/src/types.js +14 -2
- package/src/validate.js +181 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The store: the SQL of one dialect, bound to an adapter and a table prefix.
|
|
2
|
+
import * as mysqlFinal from './mysql_final.js';
|
|
3
|
+
import * as mysqlWorker from './mysql_worker.js';
|
|
4
|
+
import * as postgresFinal from './postgres_final.js';
|
|
5
|
+
import * as postgresWorker from './postgres_worker.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} ProbeRow
|
|
9
|
+
* @property {string} name A job name with pending jobs.
|
|
10
|
+
* @property {number} delayMs Until its oldest pending job is due: 0 or less means due now.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} Candidate
|
|
15
|
+
* @property {string} id
|
|
16
|
+
* @property {string} name
|
|
17
|
+
* @property {number} runAtMs
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A job that a claim locked, and then claimed: it runs.
|
|
22
|
+
* @typedef {object} ClaimedJob
|
|
23
|
+
* @property {string} id
|
|
24
|
+
* @property {string} name
|
|
25
|
+
* @property {string[]} mutexKeys
|
|
26
|
+
* @property {number} attempts The failed attempts so far.
|
|
27
|
+
* @property {number} maxAttempts
|
|
28
|
+
* @property {number} stalls
|
|
29
|
+
* @property {number} maxStalls
|
|
30
|
+
* @property {string | null} failureJob
|
|
31
|
+
* @property {Date} enqueuedAt
|
|
32
|
+
* @property {number} waitedMs Since it was due.
|
|
33
|
+
* @property {string} lockToken
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} ClaimParams
|
|
38
|
+
* @property {string[]} candidateIds
|
|
39
|
+
* @property {string} lockToken Of this claim.
|
|
40
|
+
* @property {string} lockedBy The process.
|
|
41
|
+
* @property {number} leaseMs
|
|
42
|
+
* @property {(locked: ClaimedJob[], busyKeys: Set<string>) => ClaimedJob[]} pick The jobs to take among the locked
|
|
43
|
+
* ones (in the order of run_at): the free slots, the free keys. The keys of the picked jobs must be distinct.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A job that this process claimed: every write of it checks the lock token.
|
|
48
|
+
* @typedef {object} OwnedJob
|
|
49
|
+
* @property {string} id
|
|
50
|
+
* @property {string} lockToken
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The job that fails for good, as the failure job gets it.
|
|
55
|
+
* @typedef {object} FailedJob
|
|
56
|
+
* @property {string} id
|
|
57
|
+
* @property {string} name
|
|
58
|
+
* @property {unknown} payload Null: its payload is missing.
|
|
59
|
+
* @property {number} attempt
|
|
60
|
+
* @property {number} maxAttempts
|
|
61
|
+
* @property {string} failureJob The name of its failure job.
|
|
62
|
+
* @property {import('../error_json.js').ErrorJson} error
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds the failure job: its options from its definition, else the defaults; no failure job of its own.
|
|
67
|
+
* @typedef {(failed: FailedJob) => import('../new_job.js').NewJob} FailureJobBuilder
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/** @typedef {{ added: import('../new_job.js').NewJob } | { notAdded: unknown }} FailureJobResult */
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {object} Store
|
|
74
|
+
* @property {() => Promise<ProbeRow[]>} probe
|
|
75
|
+
* @property {(limitsByName: Map<string, number>, keyedNames: string[]) => Promise<Candidate[]>} findCandidates
|
|
76
|
+
* @property {(params: ClaimParams) => Promise<ClaimedJob[]>} claim In a queue transaction of its own.
|
|
77
|
+
* @property {(ids: string[]) => Promise<Map<string, unknown>>} readPayloads
|
|
78
|
+
* @property {(jobs: OwnedJob[], leaseMs: number) => Promise<Set<string>>} renewLeases
|
|
79
|
+
* @property {(job: OwnedJob) => Promise<boolean>} markTimedOut
|
|
80
|
+
* @property {(job: OwnedJob) => Promise<boolean>} completeJob
|
|
81
|
+
* @property {(job: OwnedJob, params: { delayMs: number, error: import('../error_json.js').ErrorJson })
|
|
82
|
+
* => Promise<boolean>} retryJob
|
|
83
|
+
* @property {(
|
|
84
|
+
* job: OwnedJob,
|
|
85
|
+
* params: { error: import('../error_json.js').ErrorJson, buildFailureJob: FailureJobBuilder },
|
|
86
|
+
* ) => Promise<{ changed: boolean, failureJob: FailureJobResult | null }>} failJob
|
|
87
|
+
* @property {(job: OwnedJob, params: { delayMs: number, lingerMs: number }) => Promise<boolean>} releaseJob
|
|
88
|
+
* @property {(limit: number) => Promise<{ id: string, name: string }[]>} findStaleJobs
|
|
89
|
+
* @property {(id: string, params: { leaseMs: number, buildFailureJob: FailureJobBuilder })
|
|
90
|
+
* => Promise<{ result: 'released' | 'failed' | null, failureJob: FailureJobResult | null }>} recoverStaleJob
|
|
91
|
+
* @property {(limit: number) => Promise<{ lingers: number, orphans: number }>} sweepMutexes
|
|
92
|
+
* @property {(limit: number) => Promise<number>} deleteExpiredJobs
|
|
93
|
+
* @property {(names: string[]) => Promise<void>} makeDue For drain({ includeDelayed }).
|
|
94
|
+
* @property {() => Promise<import('../types.js').JobStats[]>} getStats
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {import('../types.js').Adapter<any>} adapter
|
|
99
|
+
* @param {string} tablePrefix Checked.
|
|
100
|
+
* @returns {Store}
|
|
101
|
+
*/
|
|
102
|
+
export function createStore(adapter, tablePrefix) {
|
|
103
|
+
const sql = adapter.dialect === 'mysql' ? mysqlWorker : postgresWorker;
|
|
104
|
+
const final = adapter.dialect === 'mysql' ? mysqlFinal : postgresFinal;
|
|
105
|
+
/**
|
|
106
|
+
* @template T
|
|
107
|
+
* @param {(executor: import('../types.js').Executor) => Promise<T>} func
|
|
108
|
+
* @returns {Promise<T>}
|
|
109
|
+
*/
|
|
110
|
+
const inQueue = func => adapter.queueTransaction(func);
|
|
111
|
+
const p = tablePrefix;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
probe: () => sql.probe(adapter.queue, tablePrefix),
|
|
115
|
+
findCandidates: (limitsByName, keyedNames) => sql.findCandidates(
|
|
116
|
+
adapter.queue,
|
|
117
|
+
tablePrefix,
|
|
118
|
+
limitsByName,
|
|
119
|
+
keyedNames,
|
|
120
|
+
),
|
|
121
|
+
claim: params => adapter.queueTransaction(executor => sql.claim(executor, tablePrefix, params)),
|
|
122
|
+
readPayloads: ids => sql.readPayloads(adapter.queue, tablePrefix, ids),
|
|
123
|
+
renewLeases: (jobs, leaseMs) => inQueue(executor => final.renewLeases(executor, p, jobs, leaseMs)),
|
|
124
|
+
markTimedOut: job => final.markTimedOut(adapter.queue, p, job),
|
|
125
|
+
completeJob: job => inQueue(executor => final.completeJob(executor, p, job)),
|
|
126
|
+
retryJob: (job, params) => inQueue(executor => final.retryJob(executor, p, job, params)),
|
|
127
|
+
failJob: (job, params) => inQueue(executor => final.failJob(executor, p, job, params)),
|
|
128
|
+
releaseJob: (job, params) => inQueue(executor => final.releaseJob(executor, p, job, params)),
|
|
129
|
+
findStaleJobs: limit => final.findStaleJobs(adapter.queue, p, limit),
|
|
130
|
+
recoverStaleJob: (id, params) => inQueue(executor => final.recoverStaleJob(executor, p, id, params)),
|
|
131
|
+
sweepMutexes: limit => inQueue(executor => final.sweepMutexes(executor, p, limit)),
|
|
132
|
+
deleteExpiredJobs: limit => inQueue(executor => final.deleteExpiredJobs(executor, p, limit)),
|
|
133
|
+
makeDue: names => inQueue(executor => final.makeDue(executor, p, names)),
|
|
134
|
+
getStats: () => final.getStats(adapter.queue, p),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** MySQL: deadlock, lock wait timeout. PostgreSQL: serialization failure, deadlock. */
|
|
2
|
+
const TRANSIENT_CODES = new Set(['ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT', '40001', '40P01']);
|
|
3
|
+
const TRANSIENT_ERRNOS = new Set([1205, 1213]);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A database error that a retry of the same work can pass: the transaction lost a lock race.
|
|
7
|
+
* @param {unknown} error
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isTransientDbError(error) {
|
|
11
|
+
for (let current = error, depth = 0; current && depth < 5; depth++) {
|
|
12
|
+
const { code, errno, cause } = /** @type {{ code?: unknown, errno?: unknown, cause?: unknown }} */ (current);
|
|
13
|
+
|
|
14
|
+
if (TRANSIENT_CODES.has(String(code)) || TRANSIENT_ERRNOS.has(Number(errno))) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
current = cause;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return false;
|
|
22
|
+
}
|
package/src/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The public types of sqlane. See docs/design.md.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A duration: a string (`'500ms'`, `'5s'`, `'
|
|
4
|
+
* A duration: milliseconds, or a string of parse-duration (`'500ms'`, `'5s'`, `'4m30s'`, `'1h'`, `'2 days'`).
|
|
5
5
|
* @typedef {string | number} Duration
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -183,12 +183,23 @@
|
|
|
183
183
|
* @property {'mysql' | 'postgres'} dialect
|
|
184
184
|
* @property {(connection: C) => Executor} wrap Used by sqlane: wraps a connection given to `enqueue`.
|
|
185
185
|
* @property {Executor} queue Used by sqlane: the queue's own pool.
|
|
186
|
+
* @property {<T>(func: (executor: Executor) => Promise<T>) => Promise<T>} withConnection Used by sqlane: runs func on
|
|
187
|
+
* one connection of the queue's pool, then releases it.
|
|
188
|
+
* @property {<T>(func: (executor: Executor) => Promise<T>) => Promise<T>} queueTransaction Used by sqlane: runs func
|
|
189
|
+
* in a `READ COMMITTED` transaction on a connection of the queue's pool.
|
|
190
|
+
* @property {<T>(connection: C, func: (executor: Executor) => Promise<T>) => Promise<T>} inTransaction Used by sqlane:
|
|
191
|
+
* a pool → func in a transaction of its own; a connection or a client → func on it, in the caller's transaction.
|
|
192
|
+
* @property {boolean} [notify] PostgreSQL: hints by LISTEN/NOTIFY.
|
|
193
|
+
* @property {(channel: string, onMessage: (message: string) => void) => Promise<() => Promise<void>>} [listen] Used by
|
|
194
|
+
* sqlane, with `notify`: LISTEN on a connection of its own; resolves to the unlisten function.
|
|
186
195
|
*/
|
|
187
196
|
|
|
188
197
|
/**
|
|
189
198
|
* Runs SQL. Used by sqlane, through an adapter.
|
|
190
199
|
* @typedef {object} Executor
|
|
191
200
|
* @property {(sql: string, params: readonly unknown[]) => Promise<readonly Record<string, unknown>[]>} query
|
|
201
|
+
* @property {(sql: string, params: readonly unknown[]) => Promise<number>} run Runs an INSERT, UPDATE or DELETE:
|
|
202
|
+
* the number of rows it changed.
|
|
192
203
|
*/
|
|
193
204
|
|
|
194
205
|
/**
|
|
@@ -273,7 +284,8 @@
|
|
|
273
284
|
* @property {(options?: DrainOptions) => Promise<DrainResult>} drain For tests: runs the due jobs of the handled
|
|
274
285
|
* names until none is left, without `start()`.
|
|
275
286
|
* @property {(options?: CloseOptions) => Promise<void>} close
|
|
276
|
-
* @property {(options?: MigrateOptions) => Promise<
|
|
287
|
+
* @property {(options?: MigrateOptions) => Promise<{ applied: string[] }>} migrate Applies the missing migrations:
|
|
288
|
+
* their ids.
|
|
277
289
|
* @property {() => Promise<JobStats[]>} getStats
|
|
278
290
|
* @property {(jobName: string) => void} wake Low level: probes this name now.
|
|
279
291
|
*/
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { parseDuration } from './duration.js';
|
|
2
|
+
|
|
3
|
+
/** A job name: no `:` (the job id is `<name>:<jobId>`), lowercase. */
|
|
4
|
+
export const JOB_NAME_REGEXP = /^[a-z][a-z0-9_.-]{0,127}$/;
|
|
5
|
+
|
|
6
|
+
/** A table prefix: the longest table name, `<prefix>migrations`, stays within the 63 characters of PostgreSQL. */
|
|
7
|
+
export const TABLE_PREFIX_REGEXP = /^[a-z][a-z0-9_]{0,31}$/;
|
|
8
|
+
|
|
9
|
+
/** Printable ASCII, no space at the ends: MySQL `ascii_bin` ignores trailing spaces in comparisons. */
|
|
10
|
+
export const PRINTABLE_ID_REGEXP = /^[\x21-\x7E](?:[\x20-\x7E]*[\x21-\x7E])?$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where a check runs: for the error message and its class.
|
|
14
|
+
* @typedef {object} Scope
|
|
15
|
+
* @property {string} where E.g. `defineJob("build_report")`.
|
|
16
|
+
* @property {new (message: string) => Error} ErrorClass
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {unknown} value
|
|
21
|
+
* @returns {value is Record<string, unknown>}
|
|
22
|
+
*/
|
|
23
|
+
export function isPlainObject(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Throws if the value is not an object with only the allowed keys.
|
|
29
|
+
* @param {unknown} value
|
|
30
|
+
* @param {readonly string[]} allowedKeys
|
|
31
|
+
* @param {string} label E.g. `"options"`.
|
|
32
|
+
* @param {Scope} scope
|
|
33
|
+
* @returns {Record<string, unknown>}
|
|
34
|
+
*/
|
|
35
|
+
export function checkObject(value, allowedKeys, label, { where, ErrorClass }) {
|
|
36
|
+
if (!isPlainObject(value)) {
|
|
37
|
+
throw new ErrorClass(`${where}: ${label} must be an object`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const unknownKeys = Object.keys(value).filter(key => !allowedKeys.includes(key));
|
|
41
|
+
|
|
42
|
+
if (unknownKeys.length) {
|
|
43
|
+
throw new ErrorClass(`${where}: unknown ${label}: ${unknownKeys.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {unknown} value
|
|
51
|
+
* @param {string} label
|
|
52
|
+
* @param {Scope} scope
|
|
53
|
+
* @param {number} [min] Default: 1.
|
|
54
|
+
*/
|
|
55
|
+
export function checkInteger(value, label, { where, ErrorClass }, min = 1) {
|
|
56
|
+
if (!Number.isSafeInteger(value) || /** @type {number} */ (value) < min) {
|
|
57
|
+
throw new ErrorClass(`${where}: ${label} must be an integer of at least ${min}, got ${JSON.stringify(value)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {unknown} value
|
|
63
|
+
* @param {string} label
|
|
64
|
+
* @param {Scope} scope
|
|
65
|
+
* @param {boolean} [positive] Default: false: 0 is a duration.
|
|
66
|
+
* @returns {number} Milliseconds.
|
|
67
|
+
*/
|
|
68
|
+
export function checkDuration(value, label, { where, ErrorClass }, positive = false) {
|
|
69
|
+
const ms = parseDuration(value);
|
|
70
|
+
|
|
71
|
+
if (ms === null || (positive && ms === 0)) {
|
|
72
|
+
throw new ErrorClass(
|
|
73
|
+
`${where}: ${label} must be a ${positive ? 'positive ' : ''}duration ('5s', '4m30s', or milliseconds), `
|
|
74
|
+
+ `got ${JSON.stringify(value)}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return ms;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {unknown} value
|
|
83
|
+
* @param {Scope} scope
|
|
84
|
+
*/
|
|
85
|
+
export function checkBackoff(value, scope) {
|
|
86
|
+
const { where, ErrorClass } = scope;
|
|
87
|
+
const backoff = checkObject(value, ['type', 'delay', 'maxDelay', 'jitter'], '"backoff" options', scope);
|
|
88
|
+
|
|
89
|
+
if (backoff.type !== 'exponential' && backoff.type !== 'fixed') {
|
|
90
|
+
throw new ErrorClass(`${where}: "backoff.type" must be 'exponential' or 'fixed'`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const delayMs = checkDuration(backoff.delay, '"backoff.delay"', scope);
|
|
94
|
+
|
|
95
|
+
if (backoff.maxDelay !== undefined) {
|
|
96
|
+
if (backoff.type === 'fixed') {
|
|
97
|
+
throw new ErrorClass(`${where}: "backoff.maxDelay" is only for the exponential backoff`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (checkDuration(backoff.maxDelay, '"backoff.maxDelay"', scope) < delayMs) {
|
|
101
|
+
throw new ErrorClass(`${where}: "backoff.maxDelay" must be at least "backoff.delay"`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { jitter } = backoff;
|
|
106
|
+
|
|
107
|
+
if (jitter !== undefined && (typeof jitter !== 'number' || !(jitter >= 0 && jitter <= 1))) {
|
|
108
|
+
throw new ErrorClass(`${where}: "backoff.jitter" must be a number from 0 to 1`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {unknown} value
|
|
114
|
+
* @param {Scope} scope
|
|
115
|
+
*/
|
|
116
|
+
export function checkRetention(value, scope) {
|
|
117
|
+
const retention = checkObject(value, ['completed', 'failed'], '"retention" options', scope);
|
|
118
|
+
|
|
119
|
+
for (const key of ['completed', 'failed']) {
|
|
120
|
+
if (retention[key] !== undefined) {
|
|
121
|
+
checkDuration(retention[key], `"retention.${key}"`, scope);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks the options that a definition, the instance defaults and an enqueue share.
|
|
128
|
+
* @param {Record<string, unknown>} options
|
|
129
|
+
* @param {Scope} scope
|
|
130
|
+
*/
|
|
131
|
+
export function checkJobFields(options, scope) {
|
|
132
|
+
if (options.maxAttempts !== undefined) {
|
|
133
|
+
checkInteger(options.maxAttempts, '"maxAttempts"', scope);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.maxStalls !== undefined) {
|
|
137
|
+
checkInteger(options.maxStalls, '"maxStalls"', scope, 0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.backoff !== undefined) {
|
|
141
|
+
checkBackoff(options.backoff, scope);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.retention !== undefined) {
|
|
145
|
+
checkRetention(options.retention, scope);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (options.timeout !== undefined) {
|
|
149
|
+
checkDuration(options.timeout, '"timeout"', scope, true);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {unknown} value A name, or a definition.
|
|
155
|
+
* @param {string} jobName The job that names it.
|
|
156
|
+
* @param {Scope} scope
|
|
157
|
+
* @returns {string} The name of the failure job.
|
|
158
|
+
*/
|
|
159
|
+
export function checkFailureJob(value, jobName, { where, ErrorClass }) {
|
|
160
|
+
const name = isPlainObject(value) ? value.name : value;
|
|
161
|
+
|
|
162
|
+
if (typeof name !== 'string' || !JOB_NAME_REGEXP.test(name)) {
|
|
163
|
+
throw new ErrorClass(`${where}: "failureJob" must be a job name or a job definition`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (name === jobName) {
|
|
167
|
+
throw new ErrorClass(`${where}: a job cannot be its own failure job`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return name;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {unknown} tablePrefix
|
|
175
|
+
* @param {Scope} scope
|
|
176
|
+
*/
|
|
177
|
+
export function checkTablePrefix(tablePrefix, { where, ErrorClass }) {
|
|
178
|
+
if (typeof tablePrefix !== 'string' || !TABLE_PREFIX_REGEXP.test(tablePrefix)) {
|
|
179
|
+
throw new ErrorClass(`${where}: "tablePrefix" must match ${TABLE_PREFIX_REGEXP}`);
|
|
180
|
+
}
|
|
181
|
+
}
|