duron 0.1.1 → 0.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/README.md +4 -4
- package/dist/adapters/postgres/base.d.ts +52 -0
- package/dist/adapters/postgres/base.d.ts.map +1 -0
- package/dist/adapters/postgres/base.js +832 -0
- package/dist/adapters/postgres/pglite.d.ts +10 -5
- package/dist/adapters/postgres/pglite.d.ts.map +1 -1
- package/dist/adapters/postgres/pglite.js +19 -7
- package/dist/adapters/postgres/postgres.d.ts +6 -39
- package/dist/adapters/postgres/postgres.d.ts.map +1 -1
- package/dist/adapters/postgres/postgres.js +9 -822
- package/dist/adapters/postgres/schema.d.ts +89 -135
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.d.ts +89 -135
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
- package/migrations/postgres/20251203223656_conscious_johnny_blaze/migration.sql +64 -0
- package/migrations/postgres/20251203223656_conscious_johnny_blaze/snapshot.json +954 -0
- package/package.json +3 -3
- package/src/adapters/postgres/base.ts +1297 -0
- package/src/adapters/postgres/pglite.ts +36 -18
- package/src/adapters/postgres/postgres.ts +19 -1244
- package/migrations/postgres/0000_lethal_speed_demon.sql +0 -64
- package/migrations/postgres/meta/0000_snapshot.json +0 -606
- package/migrations/postgres/meta/_journal.json +0 -13
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { and, asc, between, desc, eq, gt, gte, ilike, inArray, isNull, ne, or, sql } from 'drizzle-orm';
|
|
3
2
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
4
3
|
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
|
5
|
-
import {
|
|
6
|
-
import { Adapter, } from '../adapter.js';
|
|
7
|
-
import createSchema from './schema.js';
|
|
4
|
+
import { PostgresBaseAdapter } from './base.js';
|
|
8
5
|
const noop = () => {
|
|
9
6
|
};
|
|
10
|
-
export class PostgresAdapter extends
|
|
11
|
-
options;
|
|
12
|
-
db;
|
|
13
|
-
tables;
|
|
14
|
-
constructor(options) {
|
|
15
|
-
super();
|
|
16
|
-
this.options = {
|
|
17
|
-
connection: options.connection,
|
|
18
|
-
schema: options.schema ?? 'duron',
|
|
19
|
-
migrateOnStart: options.migrateOnStart ?? true,
|
|
20
|
-
};
|
|
21
|
-
this.tables = createSchema(this.options.schema);
|
|
22
|
-
this._initDb();
|
|
23
|
-
}
|
|
7
|
+
export class PostgresAdapter extends PostgresBaseAdapter {
|
|
24
8
|
_initDb() {
|
|
25
|
-
const postgresConnection = typeof this.
|
|
9
|
+
const postgresConnection = typeof this.connection === 'string'
|
|
26
10
|
? {
|
|
27
|
-
url: this.
|
|
11
|
+
url: this.connection,
|
|
28
12
|
}
|
|
29
|
-
: this.
|
|
13
|
+
: this.connection;
|
|
30
14
|
this.db = drizzle({
|
|
31
15
|
connection: {
|
|
32
16
|
...postgresConnection,
|
|
@@ -39,828 +23,31 @@ export class PostgresAdapter extends Adapter {
|
|
|
39
23
|
});
|
|
40
24
|
}
|
|
41
25
|
async _start() {
|
|
42
|
-
if (this.
|
|
26
|
+
if (this.migrateOnStart) {
|
|
43
27
|
await migrate(this.db, {
|
|
44
28
|
migrationsFolder: join(import.meta.dirname, '..', '..', '..', 'migrations', 'postgres'),
|
|
45
29
|
migrationsTable: 'migrations',
|
|
46
30
|
migrationsSchema: 'duron',
|
|
47
31
|
});
|
|
48
32
|
}
|
|
49
|
-
await
|
|
50
|
-
const fromOwnerId = JSON.parse(payload).fromOwnerId;
|
|
51
|
-
await this._notify(`pong-${fromOwnerId}`, { toOwnerId: this.id });
|
|
52
|
-
});
|
|
53
|
-
await this._listen(`job-status-changed`, (payload) => {
|
|
54
|
-
if (this.listenerCount('job-status-changed') > 0) {
|
|
55
|
-
const { jobId, status, ownerId } = JSON.parse(payload);
|
|
56
|
-
this.emit('job-status-changed', { jobId, status, ownerId });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
await this._listen(`job-available`, (payload) => {
|
|
60
|
-
if (this.listenerCount('job-available') > 0) {
|
|
61
|
-
const { jobId } = JSON.parse(payload);
|
|
62
|
-
this.emit('job-available', { jobId });
|
|
63
|
-
}
|
|
64
|
-
});
|
|
33
|
+
await super._start();
|
|
65
34
|
}
|
|
66
35
|
async _stop() {
|
|
67
36
|
await this.db.$client.end({
|
|
68
37
|
timeout: 5_000,
|
|
69
38
|
});
|
|
70
39
|
}
|
|
71
|
-
async _createJob({ queue, groupKey, input, timeoutMs, checksum, concurrencyLimit }) {
|
|
72
|
-
const [result] = await this.db
|
|
73
|
-
.insert(this.tables.jobsTable)
|
|
74
|
-
.values({
|
|
75
|
-
action_name: queue,
|
|
76
|
-
group_key: groupKey,
|
|
77
|
-
checksum,
|
|
78
|
-
input,
|
|
79
|
-
status: JOB_STATUS_CREATED,
|
|
80
|
-
timeout_ms: timeoutMs,
|
|
81
|
-
concurrency_limit: concurrencyLimit,
|
|
82
|
-
})
|
|
83
|
-
.returning({ id: this.tables.jobsTable.id });
|
|
84
|
-
if (!result) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
return result.id;
|
|
88
|
-
}
|
|
89
|
-
async _completeJob({ jobId, output }) {
|
|
90
|
-
const result = await this.db
|
|
91
|
-
.update(this.tables.jobsTable)
|
|
92
|
-
.set({
|
|
93
|
-
status: JOB_STATUS_COMPLETED,
|
|
94
|
-
output,
|
|
95
|
-
finished_at: sql `now()`,
|
|
96
|
-
})
|
|
97
|
-
.where(and(eq(this.tables.jobsTable.id, jobId), eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE), eq(this.tables.jobsTable.owner_id, this.id), gt(this.tables.jobsTable.expires_at, sql `now()`)))
|
|
98
|
-
.returning({ id: this.tables.jobsTable.id });
|
|
99
|
-
return result.length > 0;
|
|
100
|
-
}
|
|
101
|
-
async _failJob({ jobId, error }) {
|
|
102
|
-
const result = await this.db
|
|
103
|
-
.update(this.tables.jobsTable)
|
|
104
|
-
.set({
|
|
105
|
-
status: JOB_STATUS_FAILED,
|
|
106
|
-
error,
|
|
107
|
-
finished_at: sql `now()`,
|
|
108
|
-
})
|
|
109
|
-
.where(and(eq(this.tables.jobsTable.id, jobId), eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE), eq(this.tables.jobsTable.owner_id, this.id)))
|
|
110
|
-
.returning({ id: this.tables.jobsTable.id });
|
|
111
|
-
return result.length > 0;
|
|
112
|
-
}
|
|
113
|
-
async _cancelJob({ jobId }) {
|
|
114
|
-
const result = await this.db
|
|
115
|
-
.update(this.tables.jobsTable)
|
|
116
|
-
.set({
|
|
117
|
-
status: JOB_STATUS_CANCELLED,
|
|
118
|
-
finished_at: sql `now()`,
|
|
119
|
-
})
|
|
120
|
-
.where(and(eq(this.tables.jobsTable.id, jobId), or(eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE), eq(this.tables.jobsTable.status, JOB_STATUS_CREATED))))
|
|
121
|
-
.returning({ id: this.tables.jobsTable.id });
|
|
122
|
-
return result.length > 0;
|
|
123
|
-
}
|
|
124
|
-
async _retryJob({ jobId }) {
|
|
125
|
-
const result = this._map(await this.db.execute(sql `
|
|
126
|
-
WITH locked_source AS (
|
|
127
|
-
-- Lock the source job row to prevent concurrent retries
|
|
128
|
-
SELECT
|
|
129
|
-
j.action_name,
|
|
130
|
-
j.group_key,
|
|
131
|
-
j.checksum,
|
|
132
|
-
j.input,
|
|
133
|
-
j.timeout_ms,
|
|
134
|
-
j.created_at,
|
|
135
|
-
j.concurrency_limit
|
|
136
|
-
FROM ${this.tables.jobsTable} j
|
|
137
|
-
WHERE j.id = ${jobId}
|
|
138
|
-
AND j.status IN (${JOB_STATUS_COMPLETED}, ${JOB_STATUS_CANCELLED}, ${JOB_STATUS_FAILED})
|
|
139
|
-
FOR UPDATE OF j SKIP LOCKED
|
|
140
|
-
),
|
|
141
|
-
existing_retry AS (
|
|
142
|
-
-- Check if a retry already exists (a newer job with same checksum, group_key, and input)
|
|
143
|
-
SELECT j.id
|
|
144
|
-
FROM ${this.tables.jobsTable} j
|
|
145
|
-
INNER JOIN locked_source ls
|
|
146
|
-
ON j.action_name = ls.action_name
|
|
147
|
-
AND j.group_key = ls.group_key
|
|
148
|
-
AND j.checksum = ls.checksum
|
|
149
|
-
AND j.input = ls.input
|
|
150
|
-
AND j.created_at > ls.created_at
|
|
151
|
-
WHERE j.status IN (${JOB_STATUS_CREATED}, ${JOB_STATUS_ACTIVE})
|
|
152
|
-
LIMIT 1
|
|
153
|
-
),
|
|
154
|
-
inserted_retry AS (
|
|
155
|
-
-- Insert the retry only if no existing retry was found
|
|
156
|
-
-- Get concurrency_limit from the latest job at insertion time to avoid stale values
|
|
157
|
-
INSERT INTO ${this.tables.jobsTable} (
|
|
158
|
-
action_name,
|
|
159
|
-
group_key,
|
|
160
|
-
checksum,
|
|
161
|
-
input,
|
|
162
|
-
status,
|
|
163
|
-
timeout_ms,
|
|
164
|
-
concurrency_limit
|
|
165
|
-
)
|
|
166
|
-
SELECT
|
|
167
|
-
ls.action_name,
|
|
168
|
-
ls.group_key,
|
|
169
|
-
ls.checksum,
|
|
170
|
-
ls.input,
|
|
171
|
-
${JOB_STATUS_CREATED},
|
|
172
|
-
ls.timeout_ms,
|
|
173
|
-
COALESCE(
|
|
174
|
-
(
|
|
175
|
-
SELECT j.concurrency_limit
|
|
176
|
-
FROM ${this.tables.jobsTable} j
|
|
177
|
-
WHERE j.action_name = ls.action_name
|
|
178
|
-
AND j.group_key = ls.group_key
|
|
179
|
-
AND (j.expires_at IS NULL OR j.expires_at > now())
|
|
180
|
-
ORDER BY j.created_at DESC, j.id DESC
|
|
181
|
-
LIMIT 1
|
|
182
|
-
),
|
|
183
|
-
ls.concurrency_limit
|
|
184
|
-
)
|
|
185
|
-
FROM locked_source ls
|
|
186
|
-
WHERE NOT EXISTS (SELECT 1 FROM existing_retry)
|
|
187
|
-
RETURNING id
|
|
188
|
-
)
|
|
189
|
-
-- Return only the newly inserted retry ID (not existing retries)
|
|
190
|
-
SELECT id FROM inserted_retry
|
|
191
|
-
LIMIT 1
|
|
192
|
-
`));
|
|
193
|
-
if (result.length === 0) {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
return result[0].id;
|
|
197
|
-
}
|
|
198
|
-
async _deleteJob({ jobId }) {
|
|
199
|
-
const result = await this.db
|
|
200
|
-
.delete(this.tables.jobsTable)
|
|
201
|
-
.where(and(eq(this.tables.jobsTable.id, jobId), ne(this.tables.jobsTable.status, JOB_STATUS_ACTIVE)))
|
|
202
|
-
.returning({ id: this.tables.jobsTable.id });
|
|
203
|
-
if (result.length > 0) {
|
|
204
|
-
await this.db.delete(this.tables.jobStepsTable).where(eq(this.tables.jobStepsTable.job_id, jobId));
|
|
205
|
-
}
|
|
206
|
-
return result.length > 0;
|
|
207
|
-
}
|
|
208
|
-
async _deleteJobs(options) {
|
|
209
|
-
const jobsTable = this.tables.jobsTable;
|
|
210
|
-
const filters = options?.filters ?? {};
|
|
211
|
-
const where = this._buildJobsWhereClause(filters);
|
|
212
|
-
const result = await this.db.delete(jobsTable).where(where).returning({ id: jobsTable.id });
|
|
213
|
-
return result.length;
|
|
214
|
-
}
|
|
215
|
-
async _fetch({ batch }) {
|
|
216
|
-
const result = this._map(await this.db.execute(sql `
|
|
217
|
-
WITH group_concurrency AS (
|
|
218
|
-
-- Get the concurrency limit from the latest job for each group
|
|
219
|
-
SELECT DISTINCT ON (j.group_key, j.action_name)
|
|
220
|
-
j.group_key as group_key,
|
|
221
|
-
j.action_name as action_name,
|
|
222
|
-
j.concurrency_limit as concurrency_limit
|
|
223
|
-
FROM ${this.tables.jobsTable} j
|
|
224
|
-
WHERE j.group_key IS NOT NULL
|
|
225
|
-
AND (j.expires_at IS NULL OR j.expires_at > now())
|
|
226
|
-
ORDER BY j.group_key, j.action_name, j.created_at DESC, j.id DESC
|
|
227
|
-
),
|
|
228
|
-
eligible_groups AS (
|
|
229
|
-
-- Find all groups with their active counts that are below their concurrency limit
|
|
230
|
-
SELECT
|
|
231
|
-
gc.group_key,
|
|
232
|
-
gc.action_name,
|
|
233
|
-
gc.concurrency_limit,
|
|
234
|
-
COUNT(*) FILTER (WHERE j.status = ${JOB_STATUS_ACTIVE}) as active_count
|
|
235
|
-
FROM group_concurrency gc
|
|
236
|
-
LEFT JOIN ${this.tables.jobsTable} j
|
|
237
|
-
ON j.group_key = gc.group_key
|
|
238
|
-
AND j.action_name = gc.action_name
|
|
239
|
-
AND (j.expires_at IS NULL OR j.expires_at > now())
|
|
240
|
-
GROUP BY gc.group_key, gc.action_name, gc.concurrency_limit
|
|
241
|
-
HAVING COUNT(*) FILTER (WHERE j.status = ${JOB_STATUS_ACTIVE}) < gc.concurrency_limit
|
|
242
|
-
),
|
|
243
|
-
candidate_jobs AS (
|
|
244
|
-
-- Lock candidate jobs first (before applying window functions)
|
|
245
|
-
SELECT
|
|
246
|
-
j.id,
|
|
247
|
-
j.action_name,
|
|
248
|
-
j.group_key as job_group_key,
|
|
249
|
-
j.created_at
|
|
250
|
-
FROM ${this.tables.jobsTable} j
|
|
251
|
-
INNER JOIN eligible_groups eg
|
|
252
|
-
ON j.group_key = eg.group_key
|
|
253
|
-
AND j.action_name = eg.action_name
|
|
254
|
-
WHERE j.status = ${JOB_STATUS_CREATED}
|
|
255
|
-
FOR UPDATE OF j SKIP LOCKED
|
|
256
|
-
),
|
|
257
|
-
ranked_jobs AS (
|
|
258
|
-
-- Rank jobs within each group after locking
|
|
259
|
-
SELECT
|
|
260
|
-
cj.id,
|
|
261
|
-
cj.action_name,
|
|
262
|
-
cj.job_group_key,
|
|
263
|
-
cj.created_at,
|
|
264
|
-
ROW_NUMBER() OVER (
|
|
265
|
-
PARTITION BY cj.job_group_key, cj.action_name
|
|
266
|
-
ORDER BY cj.created_at ASC, cj.id ASC
|
|
267
|
-
) as job_rank
|
|
268
|
-
FROM candidate_jobs cj
|
|
269
|
-
),
|
|
270
|
-
next_job AS (
|
|
271
|
-
-- Select only jobs that fit within the concurrency limit per group
|
|
272
|
-
-- Ordered globally by created_at to respect job creation order
|
|
273
|
-
SELECT rj.id, rj.action_name, rj.job_group_key
|
|
274
|
-
FROM ranked_jobs rj
|
|
275
|
-
INNER JOIN eligible_groups eg
|
|
276
|
-
ON rj.job_group_key = eg.group_key
|
|
277
|
-
AND rj.action_name = eg.action_name
|
|
278
|
-
WHERE rj.job_rank <= (eg.concurrency_limit - eg.active_count)
|
|
279
|
-
ORDER BY rj.created_at ASC, rj.id ASC
|
|
280
|
-
LIMIT ${batch}
|
|
281
|
-
),
|
|
282
|
-
verify_concurrency AS (
|
|
283
|
-
-- Double-check concurrency limit after acquiring lock
|
|
284
|
-
SELECT
|
|
285
|
-
nj.id,
|
|
286
|
-
nj.action_name,
|
|
287
|
-
nj.job_group_key,
|
|
288
|
-
eg.concurrency_limit,
|
|
289
|
-
(SELECT COUNT(*)
|
|
290
|
-
FROM ${this.tables.jobsTable}
|
|
291
|
-
WHERE action_name = nj.action_name
|
|
292
|
-
AND group_key = nj.job_group_key
|
|
293
|
-
AND status = ${JOB_STATUS_ACTIVE}) as current_active
|
|
294
|
-
FROM next_job nj
|
|
295
|
-
INNER JOIN eligible_groups eg
|
|
296
|
-
ON nj.job_group_key = eg.group_key
|
|
297
|
-
AND nj.action_name = eg.action_name
|
|
298
|
-
)
|
|
299
|
-
UPDATE ${this.tables.jobsTable} j
|
|
300
|
-
SET status = ${JOB_STATUS_ACTIVE},
|
|
301
|
-
started_at = now(),
|
|
302
|
-
expires_at = now() + (timeout_ms || ' milliseconds')::interval,
|
|
303
|
-
owner_id = ${this.id}
|
|
304
|
-
FROM verify_concurrency vc
|
|
305
|
-
WHERE j.id = vc.id
|
|
306
|
-
AND vc.current_active < vc.concurrency_limit -- Final concurrency check using job's concurrency limit
|
|
307
|
-
RETURNING
|
|
308
|
-
j.id,
|
|
309
|
-
j.action_name as "actionName",
|
|
310
|
-
j.group_key as "groupKey",
|
|
311
|
-
j.input,
|
|
312
|
-
j.output,
|
|
313
|
-
j.error,
|
|
314
|
-
j.status,
|
|
315
|
-
j.timeout_ms as "timeoutMs",
|
|
316
|
-
j.expires_at as "expiresAt",
|
|
317
|
-
j.started_at as "startedAt",
|
|
318
|
-
j.finished_at as "finishedAt",
|
|
319
|
-
j.created_at as "createdAt",
|
|
320
|
-
j.updated_at as "updatedAt",
|
|
321
|
-
j.concurrency_limit as "concurrencyLimit"
|
|
322
|
-
`));
|
|
323
|
-
return result;
|
|
324
|
-
}
|
|
325
|
-
async _recoverJobs(options) {
|
|
326
|
-
const { checksums, multiProcessMode = false, processTimeout = 5_000 } = options;
|
|
327
|
-
const unresponsiveOwnerIds = [this.id];
|
|
328
|
-
if (multiProcessMode) {
|
|
329
|
-
const result = (await this.db
|
|
330
|
-
.selectDistinct({
|
|
331
|
-
ownerId: this.tables.jobsTable.owner_id,
|
|
332
|
-
})
|
|
333
|
-
.from(this.tables.jobsTable)
|
|
334
|
-
.where(and(eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE), ne(this.tables.jobsTable.owner_id, this.id))));
|
|
335
|
-
if (result.length > 0) {
|
|
336
|
-
const pongCount = new Set();
|
|
337
|
-
const { unlisten } = await this._listen(`pong-${this.id}`, (payload) => {
|
|
338
|
-
const toOwnerId = JSON.parse(payload).toOwnerId;
|
|
339
|
-
pongCount.add(toOwnerId);
|
|
340
|
-
if (pongCount.size >= result.length) {
|
|
341
|
-
unlisten();
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
await Promise.all(result.map((row) => this._notify(`ping-${row.ownerId}`, { fromOwnerId: this.id })));
|
|
345
|
-
let waitForSeconds = processTimeout / 1_000;
|
|
346
|
-
while (pongCount.size < result.length && waitForSeconds > 0) {
|
|
347
|
-
await new Promise((resolve) => setTimeout(resolve, 1000).unref?.());
|
|
348
|
-
waitForSeconds--;
|
|
349
|
-
}
|
|
350
|
-
unresponsiveOwnerIds.push(...result.filter((row) => !pongCount.has(row.ownerId)).map((row) => row.ownerId));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (unresponsiveOwnerIds.length > 0) {
|
|
354
|
-
const result = this._map(await this.db.execute(sql `
|
|
355
|
-
WITH locked_jobs AS (
|
|
356
|
-
SELECT j.id
|
|
357
|
-
FROM ${this.tables.jobsTable} j
|
|
358
|
-
WHERE j.status = ${JOB_STATUS_ACTIVE}
|
|
359
|
-
AND j.owner_id IN ${unresponsiveOwnerIds}
|
|
360
|
-
FOR UPDATE OF j SKIP LOCKED
|
|
361
|
-
),
|
|
362
|
-
updated_jobs AS (
|
|
363
|
-
UPDATE ${this.tables.jobsTable} j
|
|
364
|
-
SET status = ${JOB_STATUS_CREATED},
|
|
365
|
-
started_at = NULL,
|
|
366
|
-
expires_at = NULL,
|
|
367
|
-
finished_at = NULL,
|
|
368
|
-
output = NULL,
|
|
369
|
-
error = NULL
|
|
370
|
-
WHERE EXISTS (SELECT 1 FROM locked_jobs lj WHERE lj.id = j.id)
|
|
371
|
-
RETURNING id, checksum
|
|
372
|
-
),
|
|
373
|
-
deleted_steps AS (
|
|
374
|
-
DELETE FROM ${this.tables.jobStepsTable} s
|
|
375
|
-
WHERE EXISTS (
|
|
376
|
-
SELECT 1 FROM updated_jobs uj
|
|
377
|
-
WHERE uj.id = s.job_id
|
|
378
|
-
AND uj.checksum NOT IN ${checksums}
|
|
379
|
-
)
|
|
380
|
-
)
|
|
381
|
-
SELECT id FROM updated_jobs
|
|
382
|
-
`));
|
|
383
|
-
return result.length;
|
|
384
|
-
}
|
|
385
|
-
return 0;
|
|
386
|
-
}
|
|
387
|
-
async _createOrRecoverJobStep({ jobId, name, timeoutMs, retriesLimit, }) {
|
|
388
|
-
const [result] = this._map(await this.db.execute(sql `
|
|
389
|
-
WITH job_check AS (
|
|
390
|
-
SELECT j.id
|
|
391
|
-
FROM ${this.tables.jobsTable} j
|
|
392
|
-
WHERE j.id = ${jobId}
|
|
393
|
-
AND j.status = ${JOB_STATUS_ACTIVE}
|
|
394
|
-
AND (j.expires_at IS NULL OR j.expires_at > now())
|
|
395
|
-
),
|
|
396
|
-
step_existed AS (
|
|
397
|
-
SELECT EXISTS(
|
|
398
|
-
SELECT 1 FROM ${this.tables.jobStepsTable} s
|
|
399
|
-
WHERE s.job_id = ${jobId} AND s.name = ${name}
|
|
400
|
-
) AS existed
|
|
401
|
-
),
|
|
402
|
-
upserted_step AS (
|
|
403
|
-
INSERT INTO ${this.tables.jobStepsTable} (
|
|
404
|
-
job_id,
|
|
405
|
-
name,
|
|
406
|
-
timeout_ms,
|
|
407
|
-
retries_limit,
|
|
408
|
-
status,
|
|
409
|
-
started_at,
|
|
410
|
-
expires_at,
|
|
411
|
-
retries_count,
|
|
412
|
-
delayed_ms
|
|
413
|
-
)
|
|
414
|
-
SELECT
|
|
415
|
-
${jobId},
|
|
416
|
-
${name},
|
|
417
|
-
${timeoutMs},
|
|
418
|
-
${retriesLimit},
|
|
419
|
-
${STEP_STATUS_ACTIVE},
|
|
420
|
-
now(),
|
|
421
|
-
now() + interval '${sql.raw(timeoutMs.toString())} milliseconds',
|
|
422
|
-
0,
|
|
423
|
-
NULL
|
|
424
|
-
WHERE EXISTS (SELECT 1 FROM job_check)
|
|
425
|
-
ON CONFLICT (job_id, name) DO UPDATE
|
|
426
|
-
SET
|
|
427
|
-
timeout_ms = ${timeoutMs},
|
|
428
|
-
expires_at = now() + interval '${sql.raw(timeoutMs.toString())} milliseconds',
|
|
429
|
-
retries_count = 0,
|
|
430
|
-
retries_limit = ${retriesLimit},
|
|
431
|
-
delayed_ms = NULL,
|
|
432
|
-
started_at = now(),
|
|
433
|
-
history_failed_attempts = '{}'::jsonb
|
|
434
|
-
WHERE ${this.tables.jobStepsTable}.status = ${STEP_STATUS_ACTIVE}
|
|
435
|
-
RETURNING
|
|
436
|
-
id,
|
|
437
|
-
status,
|
|
438
|
-
retries_limit AS "retriesLimit",
|
|
439
|
-
retries_count AS "retriesCount",
|
|
440
|
-
timeout_ms AS "timeoutMs",
|
|
441
|
-
error,
|
|
442
|
-
output
|
|
443
|
-
),
|
|
444
|
-
final_upserted AS (
|
|
445
|
-
SELECT
|
|
446
|
-
us.*,
|
|
447
|
-
CASE WHEN se.existed THEN false ELSE true END AS "isNew"
|
|
448
|
-
FROM upserted_step us
|
|
449
|
-
CROSS JOIN step_existed se
|
|
450
|
-
),
|
|
451
|
-
existing_step AS (
|
|
452
|
-
SELECT
|
|
453
|
-
s.id,
|
|
454
|
-
s.status,
|
|
455
|
-
s.retries_limit AS "retriesLimit",
|
|
456
|
-
s.retries_count AS "retriesCount",
|
|
457
|
-
s.timeout_ms AS "timeoutMs",
|
|
458
|
-
s.error,
|
|
459
|
-
s.output,
|
|
460
|
-
false AS "isNew"
|
|
461
|
-
FROM ${this.tables.jobStepsTable} s
|
|
462
|
-
INNER JOIN job_check jc ON s.job_id = jc.id
|
|
463
|
-
WHERE s.job_id = ${jobId}
|
|
464
|
-
AND s.name = ${name}
|
|
465
|
-
AND NOT EXISTS (SELECT 1 FROM final_upserted)
|
|
466
|
-
)
|
|
467
|
-
SELECT * FROM final_upserted
|
|
468
|
-
UNION ALL
|
|
469
|
-
SELECT * FROM existing_step
|
|
470
|
-
`));
|
|
471
|
-
if (!result) {
|
|
472
|
-
this.logger?.error({ jobId }, `[PostgresAdapter] Job ${jobId} is not active or has expired`);
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
return result;
|
|
476
|
-
}
|
|
477
|
-
async _completeJobStep({ stepId, output }) {
|
|
478
|
-
const result = await this.db
|
|
479
|
-
.update(this.tables.jobStepsTable)
|
|
480
|
-
.set({
|
|
481
|
-
status: STEP_STATUS_COMPLETED,
|
|
482
|
-
output,
|
|
483
|
-
finished_at: sql `now()`,
|
|
484
|
-
})
|
|
485
|
-
.from(this.tables.jobsTable)
|
|
486
|
-
.where(and(eq(this.tables.jobStepsTable.job_id, this.tables.jobsTable.id), eq(this.tables.jobStepsTable.id, stepId), eq(this.tables.jobStepsTable.status, STEP_STATUS_ACTIVE), eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE), or(isNull(this.tables.jobsTable.expires_at), gt(this.tables.jobsTable.expires_at, sql `now()`))))
|
|
487
|
-
.returning({ id: this.tables.jobStepsTable.id });
|
|
488
|
-
return result.length > 0;
|
|
489
|
-
}
|
|
490
|
-
async _failJobStep({ stepId, error }) {
|
|
491
|
-
const result = await this.db
|
|
492
|
-
.update(this.tables.jobStepsTable)
|
|
493
|
-
.set({
|
|
494
|
-
status: STEP_STATUS_FAILED,
|
|
495
|
-
error,
|
|
496
|
-
finished_at: sql `now()`,
|
|
497
|
-
})
|
|
498
|
-
.from(this.tables.jobsTable)
|
|
499
|
-
.where(and(eq(this.tables.jobStepsTable.job_id, this.tables.jobsTable.id), eq(this.tables.jobStepsTable.id, stepId), eq(this.tables.jobStepsTable.status, STEP_STATUS_ACTIVE), eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE)))
|
|
500
|
-
.returning({ id: this.tables.jobStepsTable.id });
|
|
501
|
-
return result.length > 0;
|
|
502
|
-
}
|
|
503
|
-
async _delayJobStep({ stepId, delayMs, error }) {
|
|
504
|
-
const jobStepsTable = this.tables.jobStepsTable;
|
|
505
|
-
const jobsTable = this.tables.jobsTable;
|
|
506
|
-
const result = await this.db
|
|
507
|
-
.update(jobStepsTable)
|
|
508
|
-
.set({
|
|
509
|
-
delayed_ms: delayMs,
|
|
510
|
-
retries_count: sql `${jobStepsTable.retries_count} + 1`,
|
|
511
|
-
expires_at: sql `now() + (${jobStepsTable.timeout_ms} || ' milliseconds')::interval + (${delayMs} || ' milliseconds')::interval`,
|
|
512
|
-
history_failed_attempts: sql `COALESCE(${jobStepsTable.history_failed_attempts}, '{}'::jsonb) || jsonb_build_object(
|
|
513
|
-
extract(epoch from now())::text,
|
|
514
|
-
jsonb_build_object(
|
|
515
|
-
'failedAt', now(),
|
|
516
|
-
'error', ${JSON.stringify(error)}::jsonb,
|
|
517
|
-
'delayedMs', ${delayMs}::integer
|
|
518
|
-
)
|
|
519
|
-
)`,
|
|
520
|
-
})
|
|
521
|
-
.from(jobsTable)
|
|
522
|
-
.where(and(eq(jobStepsTable.job_id, jobsTable.id), eq(jobStepsTable.id, stepId), eq(jobStepsTable.status, STEP_STATUS_ACTIVE), eq(jobsTable.status, JOB_STATUS_ACTIVE)))
|
|
523
|
-
.returning({ id: jobStepsTable.id });
|
|
524
|
-
return result.length > 0;
|
|
525
|
-
}
|
|
526
|
-
async _cancelJobStep({ stepId }) {
|
|
527
|
-
const result = await this.db
|
|
528
|
-
.update(this.tables.jobStepsTable)
|
|
529
|
-
.set({
|
|
530
|
-
status: STEP_STATUS_CANCELLED,
|
|
531
|
-
finished_at: sql `now()`,
|
|
532
|
-
})
|
|
533
|
-
.from(this.tables.jobsTable)
|
|
534
|
-
.where(and(eq(this.tables.jobStepsTable.job_id, this.tables.jobsTable.id), eq(this.tables.jobStepsTable.id, stepId), eq(this.tables.jobStepsTable.status, STEP_STATUS_ACTIVE), or(eq(this.tables.jobsTable.status, JOB_STATUS_ACTIVE), eq(this.tables.jobsTable.status, JOB_STATUS_CANCELLED))))
|
|
535
|
-
.returning({ id: this.tables.jobStepsTable.id });
|
|
536
|
-
return result.length > 0;
|
|
537
|
-
}
|
|
538
|
-
async _getJobById(jobId) {
|
|
539
|
-
const [job] = await this.db
|
|
540
|
-
.select({
|
|
541
|
-
id: this.tables.jobsTable.id,
|
|
542
|
-
actionName: this.tables.jobsTable.action_name,
|
|
543
|
-
groupKey: this.tables.jobsTable.group_key,
|
|
544
|
-
input: this.tables.jobsTable.input,
|
|
545
|
-
output: this.tables.jobsTable.output,
|
|
546
|
-
error: this.tables.jobsTable.error,
|
|
547
|
-
status: this.tables.jobsTable.status,
|
|
548
|
-
timeoutMs: this.tables.jobsTable.timeout_ms,
|
|
549
|
-
expiresAt: this.tables.jobsTable.expires_at,
|
|
550
|
-
startedAt: this.tables.jobsTable.started_at,
|
|
551
|
-
finishedAt: this.tables.jobsTable.finished_at,
|
|
552
|
-
createdAt: this.tables.jobsTable.created_at,
|
|
553
|
-
updatedAt: this.tables.jobsTable.updated_at,
|
|
554
|
-
concurrencyLimit: this.tables.jobsTable.concurrency_limit,
|
|
555
|
-
})
|
|
556
|
-
.from(this.tables.jobsTable)
|
|
557
|
-
.where(eq(this.tables.jobsTable.id, jobId))
|
|
558
|
-
.limit(1);
|
|
559
|
-
return job ?? null;
|
|
560
|
-
}
|
|
561
|
-
async _getJobSteps(options) {
|
|
562
|
-
const { jobId, page = 1, pageSize = 10, search } = options;
|
|
563
|
-
const jobStepsTable = this.tables.jobStepsTable;
|
|
564
|
-
const fuzzySearch = search?.trim();
|
|
565
|
-
const where = and(eq(jobStepsTable.job_id, jobId), fuzzySearch && fuzzySearch.length > 0
|
|
566
|
-
? or(ilike(jobStepsTable.name, `%${fuzzySearch}%`), sql `to_tsvector('english', ${jobStepsTable.output}::text) @@ plainto_tsquery('english', ${fuzzySearch})`)
|
|
567
|
-
: undefined, options.updatedAfter
|
|
568
|
-
? sql `date_trunc('milliseconds', ${jobStepsTable.updated_at}) > ${options.updatedAfter.toISOString()}::timestamptz`
|
|
569
|
-
: undefined);
|
|
570
|
-
const total = await this.db.$count(jobStepsTable, where);
|
|
571
|
-
if (!total) {
|
|
572
|
-
return {
|
|
573
|
-
steps: [],
|
|
574
|
-
total: 0,
|
|
575
|
-
page,
|
|
576
|
-
pageSize,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
const steps = await this.db
|
|
580
|
-
.select({
|
|
581
|
-
id: jobStepsTable.id,
|
|
582
|
-
jobId: jobStepsTable.job_id,
|
|
583
|
-
name: jobStepsTable.name,
|
|
584
|
-
status: jobStepsTable.status,
|
|
585
|
-
error: jobStepsTable.error,
|
|
586
|
-
startedAt: jobStepsTable.started_at,
|
|
587
|
-
finishedAt: jobStepsTable.finished_at,
|
|
588
|
-
timeoutMs: jobStepsTable.timeout_ms,
|
|
589
|
-
expiresAt: jobStepsTable.expires_at,
|
|
590
|
-
retriesLimit: jobStepsTable.retries_limit,
|
|
591
|
-
retriesCount: jobStepsTable.retries_count,
|
|
592
|
-
delayedMs: jobStepsTable.delayed_ms,
|
|
593
|
-
historyFailedAttempts: jobStepsTable.history_failed_attempts,
|
|
594
|
-
createdAt: jobStepsTable.created_at,
|
|
595
|
-
updatedAt: jobStepsTable.updated_at,
|
|
596
|
-
})
|
|
597
|
-
.from(jobStepsTable)
|
|
598
|
-
.where(where)
|
|
599
|
-
.orderBy(asc(jobStepsTable.created_at))
|
|
600
|
-
.limit(pageSize)
|
|
601
|
-
.offset((page - 1) * pageSize);
|
|
602
|
-
return {
|
|
603
|
-
steps,
|
|
604
|
-
total,
|
|
605
|
-
page,
|
|
606
|
-
pageSize,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
_buildJobsWhereClause(filters) {
|
|
610
|
-
if (!filters) {
|
|
611
|
-
return undefined;
|
|
612
|
-
}
|
|
613
|
-
const jobsTable = this.tables.jobsTable;
|
|
614
|
-
const fuzzySearch = filters.search?.trim();
|
|
615
|
-
return and(filters.status
|
|
616
|
-
? inArray(jobsTable.status, Array.isArray(filters.status) ? filters.status : [filters.status])
|
|
617
|
-
: undefined, filters.actionName
|
|
618
|
-
? inArray(jobsTable.action_name, Array.isArray(filters.actionName) ? filters.actionName : [filters.actionName])
|
|
619
|
-
: undefined, filters.groupKey && Array.isArray(filters.groupKey)
|
|
620
|
-
? sql `j.group_key LIKE ANY(ARRAY[${sql.raw(filters.groupKey.map((key) => `'${key}'`).join(','))}]::text[])`
|
|
621
|
-
: undefined, filters.groupKey && !Array.isArray(filters.groupKey)
|
|
622
|
-
? ilike(jobsTable.group_key, `%${filters.groupKey}%`)
|
|
623
|
-
: undefined, filters.ownerId
|
|
624
|
-
? inArray(jobsTable.owner_id, Array.isArray(filters.ownerId) ? filters.ownerId : [filters.ownerId])
|
|
625
|
-
: undefined, filters.createdAt && Array.isArray(filters.createdAt)
|
|
626
|
-
? between(sql `date_trunc('second', ${jobsTable.created_at})`, filters.createdAt[0].toISOString(), filters.createdAt[1].toISOString())
|
|
627
|
-
: undefined, filters.createdAt && !Array.isArray(filters.createdAt)
|
|
628
|
-
? gte(sql `date_trunc('second', ${jobsTable.created_at})`, filters.createdAt.toISOString())
|
|
629
|
-
: undefined, filters.startedAt && Array.isArray(filters.startedAt)
|
|
630
|
-
? between(sql `date_trunc('second', ${jobsTable.started_at})`, filters.startedAt[0].toISOString(), filters.startedAt[1].toISOString())
|
|
631
|
-
: undefined, filters.startedAt && !Array.isArray(filters.startedAt)
|
|
632
|
-
? gte(sql `date_trunc('second', ${jobsTable.started_at})`, filters.startedAt.toISOString())
|
|
633
|
-
: undefined, filters.finishedAt && Array.isArray(filters.finishedAt)
|
|
634
|
-
? between(sql `date_trunc('second', ${jobsTable.finished_at})`, filters.finishedAt[0].toISOString(), filters.finishedAt[1].toISOString())
|
|
635
|
-
: undefined, filters.finishedAt && !Array.isArray(filters.finishedAt)
|
|
636
|
-
? gte(sql `date_trunc('second', ${jobsTable.finished_at})`, filters.finishedAt.toISOString())
|
|
637
|
-
: undefined, filters.updatedAfter
|
|
638
|
-
? sql `date_trunc('milliseconds', ${jobsTable.updated_at}) > ${filters.updatedAfter.toISOString()}::timestamptz`
|
|
639
|
-
: undefined, fuzzySearch && fuzzySearch.length > 0
|
|
640
|
-
? or(ilike(jobsTable.action_name, `%${fuzzySearch}%`), ilike(jobsTable.group_key, `%${fuzzySearch}%`), ilike(jobsTable.owner_id, `%${fuzzySearch}%`), sql `${jobsTable.id}::text ilike ${`%${fuzzySearch}%`}`, sql `to_tsvector('english', ${jobsTable.input}::text) @@ plainto_tsquery('english', ${fuzzySearch})`, sql `to_tsvector('english', ${jobsTable.output}::text) @@ plainto_tsquery('english', ${fuzzySearch})`)
|
|
641
|
-
: undefined, ...(filters.inputFilter && Object.keys(filters.inputFilter).length > 0
|
|
642
|
-
? this.#buildJsonbWhereConditions(filters.inputFilter, jobsTable.input)
|
|
643
|
-
: []), ...(filters.outputFilter && Object.keys(filters.outputFilter).length > 0
|
|
644
|
-
? this.#buildJsonbWhereConditions(filters.outputFilter, jobsTable.output)
|
|
645
|
-
: []));
|
|
646
|
-
}
|
|
647
|
-
async _getJobs(options) {
|
|
648
|
-
const jobsTable = this.tables.jobsTable;
|
|
649
|
-
const page = options?.page ?? 1;
|
|
650
|
-
const pageSize = options?.pageSize ?? 10;
|
|
651
|
-
const filters = options?.filters ?? {};
|
|
652
|
-
const sortInput = options?.sort ?? { field: 'startedAt', order: 'desc' };
|
|
653
|
-
const sorts = Array.isArray(sortInput) ? sortInput : [sortInput];
|
|
654
|
-
const where = this._buildJobsWhereClause(filters);
|
|
655
|
-
const total = await this.db.$count(jobsTable, where);
|
|
656
|
-
if (!total) {
|
|
657
|
-
return {
|
|
658
|
-
jobs: [],
|
|
659
|
-
total: 0,
|
|
660
|
-
page,
|
|
661
|
-
pageSize,
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
const sortFieldMap = {
|
|
665
|
-
createdAt: jobsTable.created_at,
|
|
666
|
-
startedAt: jobsTable.started_at,
|
|
667
|
-
finishedAt: jobsTable.finished_at,
|
|
668
|
-
status: jobsTable.status,
|
|
669
|
-
actionName: jobsTable.action_name,
|
|
670
|
-
expiresAt: jobsTable.expires_at,
|
|
671
|
-
};
|
|
672
|
-
const jobs = await this.db
|
|
673
|
-
.select({
|
|
674
|
-
id: jobsTable.id,
|
|
675
|
-
actionName: jobsTable.action_name,
|
|
676
|
-
groupKey: jobsTable.group_key,
|
|
677
|
-
input: jobsTable.input,
|
|
678
|
-
output: jobsTable.output,
|
|
679
|
-
error: jobsTable.error,
|
|
680
|
-
status: jobsTable.status,
|
|
681
|
-
timeoutMs: jobsTable.timeout_ms,
|
|
682
|
-
expiresAt: jobsTable.expires_at,
|
|
683
|
-
startedAt: jobsTable.started_at,
|
|
684
|
-
finishedAt: jobsTable.finished_at,
|
|
685
|
-
createdAt: jobsTable.created_at,
|
|
686
|
-
updatedAt: jobsTable.updated_at,
|
|
687
|
-
concurrencyLimit: jobsTable.concurrency_limit,
|
|
688
|
-
})
|
|
689
|
-
.from(jobsTable)
|
|
690
|
-
.where(where)
|
|
691
|
-
.orderBy(...sorts
|
|
692
|
-
.filter((sortItem) => sortItem.field in sortFieldMap)
|
|
693
|
-
.map((sortItem) => {
|
|
694
|
-
const sortField = sortFieldMap[sortItem.field];
|
|
695
|
-
if (sortItem.order.toUpperCase() === 'ASC') {
|
|
696
|
-
return asc(sortField);
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
return desc(sortField);
|
|
700
|
-
}
|
|
701
|
-
}))
|
|
702
|
-
.limit(pageSize)
|
|
703
|
-
.offset((page - 1) * pageSize);
|
|
704
|
-
return {
|
|
705
|
-
jobs,
|
|
706
|
-
total,
|
|
707
|
-
page,
|
|
708
|
-
pageSize,
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
async _getJobStepById(stepId) {
|
|
712
|
-
const [step] = await this.db
|
|
713
|
-
.select({
|
|
714
|
-
id: this.tables.jobStepsTable.id,
|
|
715
|
-
jobId: this.tables.jobStepsTable.job_id,
|
|
716
|
-
name: this.tables.jobStepsTable.name,
|
|
717
|
-
output: this.tables.jobStepsTable.output,
|
|
718
|
-
status: this.tables.jobStepsTable.status,
|
|
719
|
-
error: this.tables.jobStepsTable.error,
|
|
720
|
-
startedAt: this.tables.jobStepsTable.started_at,
|
|
721
|
-
finishedAt: this.tables.jobStepsTable.finished_at,
|
|
722
|
-
timeoutMs: this.tables.jobStepsTable.timeout_ms,
|
|
723
|
-
expiresAt: this.tables.jobStepsTable.expires_at,
|
|
724
|
-
retriesLimit: this.tables.jobStepsTable.retries_limit,
|
|
725
|
-
retriesCount: this.tables.jobStepsTable.retries_count,
|
|
726
|
-
delayedMs: this.tables.jobStepsTable.delayed_ms,
|
|
727
|
-
historyFailedAttempts: this.tables.jobStepsTable.history_failed_attempts,
|
|
728
|
-
createdAt: this.tables.jobStepsTable.created_at,
|
|
729
|
-
updatedAt: this.tables.jobStepsTable.updated_at,
|
|
730
|
-
})
|
|
731
|
-
.from(this.tables.jobStepsTable)
|
|
732
|
-
.where(eq(this.tables.jobStepsTable.id, stepId))
|
|
733
|
-
.limit(1);
|
|
734
|
-
return step ?? null;
|
|
735
|
-
}
|
|
736
|
-
async _getJobStatus(jobId) {
|
|
737
|
-
const [job] = await this.db
|
|
738
|
-
.select({
|
|
739
|
-
status: this.tables.jobsTable.status,
|
|
740
|
-
updatedAt: this.tables.jobsTable.updated_at,
|
|
741
|
-
})
|
|
742
|
-
.from(this.tables.jobsTable)
|
|
743
|
-
.where(eq(this.tables.jobsTable.id, jobId))
|
|
744
|
-
.limit(1);
|
|
745
|
-
return job ?? null;
|
|
746
|
-
}
|
|
747
|
-
async _getJobStepStatus(stepId) {
|
|
748
|
-
const [step] = await this.db
|
|
749
|
-
.select({
|
|
750
|
-
status: this.tables.jobStepsTable.status,
|
|
751
|
-
updatedAt: this.tables.jobStepsTable.updated_at,
|
|
752
|
-
})
|
|
753
|
-
.from(this.tables.jobStepsTable)
|
|
754
|
-
.where(eq(this.tables.jobStepsTable.id, stepId))
|
|
755
|
-
.limit(1);
|
|
756
|
-
return step ?? null;
|
|
757
|
-
}
|
|
758
|
-
async _getActions() {
|
|
759
|
-
const actionStats = this.db.$with('action_stats').as(this.db
|
|
760
|
-
.select({
|
|
761
|
-
name: this.tables.jobsTable.action_name,
|
|
762
|
-
last_job_created: sql `MAX(${this.tables.jobsTable.created_at})`.as('last_job_created'),
|
|
763
|
-
active: sql `COUNT(*) FILTER (WHERE ${this.tables.jobsTable.status} = ${JOB_STATUS_ACTIVE})`.as('active'),
|
|
764
|
-
completed: sql `COUNT(*) FILTER (WHERE ${this.tables.jobsTable.status} = ${JOB_STATUS_COMPLETED})`.as('completed'),
|
|
765
|
-
failed: sql `COUNT(*) FILTER (WHERE ${this.tables.jobsTable.status} = ${JOB_STATUS_FAILED})`.as('failed'),
|
|
766
|
-
cancelled: sql `COUNT(*) FILTER (WHERE ${this.tables.jobsTable.status} = ${JOB_STATUS_CANCELLED})`.as('cancelled'),
|
|
767
|
-
})
|
|
768
|
-
.from(this.tables.jobsTable)
|
|
769
|
-
.groupBy(this.tables.jobsTable.action_name));
|
|
770
|
-
const actions = await this.db
|
|
771
|
-
.with(actionStats)
|
|
772
|
-
.select({
|
|
773
|
-
name: actionStats.name,
|
|
774
|
-
lastJobCreated: actionStats.last_job_created,
|
|
775
|
-
active: sql `${actionStats.active}::int`,
|
|
776
|
-
completed: sql `${actionStats.completed}::int`,
|
|
777
|
-
failed: sql `${actionStats.failed}::int`,
|
|
778
|
-
cancelled: sql `${actionStats.cancelled}::int`,
|
|
779
|
-
})
|
|
780
|
-
.from(actionStats)
|
|
781
|
-
.orderBy(actionStats.name);
|
|
782
|
-
return {
|
|
783
|
-
actions: actions.map((action) => ({
|
|
784
|
-
...action,
|
|
785
|
-
lastJobCreated: action.lastJobCreated ?? null,
|
|
786
|
-
})),
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
#buildJsonbWhereConditions(filter, jsonbColumn) {
|
|
790
|
-
const conditions = [];
|
|
791
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
792
|
-
const parts = key.split('.').filter((p) => p.length > 0);
|
|
793
|
-
if (parts.length === 0) {
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
let jsonbPath = sql `${jsonbColumn}`;
|
|
797
|
-
if (parts.length === 1) {
|
|
798
|
-
jsonbPath = sql `${jsonbPath} ->> ${parts[0]}`;
|
|
799
|
-
}
|
|
800
|
-
else {
|
|
801
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
802
|
-
const part = parts[i];
|
|
803
|
-
if (part) {
|
|
804
|
-
jsonbPath = sql `${jsonbPath} -> ${part}`;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
const lastPart = parts[parts.length - 1];
|
|
808
|
-
if (lastPart) {
|
|
809
|
-
jsonbPath = sql `${jsonbPath} ->> ${lastPart}`;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
if (Array.isArray(value)) {
|
|
813
|
-
const arrayValueConditions = value.map((arrayValue) => {
|
|
814
|
-
const arrayValueStr = String(arrayValue);
|
|
815
|
-
let arrayPath = sql `${jsonbColumn}`;
|
|
816
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
817
|
-
const part = parts[i];
|
|
818
|
-
if (part) {
|
|
819
|
-
arrayPath = sql `${arrayPath} -> ${part}`;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
const lastPart = parts[parts.length - 1];
|
|
823
|
-
if (lastPart) {
|
|
824
|
-
arrayPath = sql `${arrayPath} -> ${lastPart}`;
|
|
825
|
-
}
|
|
826
|
-
if (typeof arrayValue === 'string') {
|
|
827
|
-
return sql `EXISTS (
|
|
828
|
-
SELECT 1
|
|
829
|
-
FROM jsonb_array_elements_text(${arrayPath}) AS elem
|
|
830
|
-
WHERE LOWER(elem) ILIKE LOWER(${`%${arrayValueStr}%`})
|
|
831
|
-
)`;
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
return sql `${arrayPath} @> ${sql.raw(JSON.stringify([arrayValue]))}::jsonb`;
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
if (arrayValueConditions.length > 0) {
|
|
838
|
-
conditions.push(arrayValueConditions.reduce((acc, condition, idx) => (idx === 0 ? condition : sql `${acc} OR ${condition}`)));
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
else if (typeof value === 'string') {
|
|
842
|
-
conditions.push(sql `COALESCE(${jsonbPath}, '') ILIKE ${`%${value}%`}`);
|
|
843
|
-
}
|
|
844
|
-
else {
|
|
845
|
-
conditions.push(sql `${jsonbPath}::text = ${String(value)}`);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
return conditions;
|
|
849
|
-
}
|
|
850
40
|
async _notify(event, data) {
|
|
851
41
|
this.logger?.debug({ event, data }, `[PostgresAdapter] Notify ${event}`);
|
|
852
|
-
await this.db.$client.notify(`${this.
|
|
42
|
+
await this.db.$client.notify(`${this.schema}.${event}`, JSON.stringify(data)).catch((err) => {
|
|
853
43
|
this.logger?.error({ err, data }, `[PostgresAdapter] Failed to notify ${event}`);
|
|
854
44
|
});
|
|
855
45
|
}
|
|
856
46
|
async _listen(event, callback) {
|
|
857
|
-
return await this.db.$client.listen(`${this.
|
|
47
|
+
return await this.db.$client.listen(`${this.schema}.${event}`, (payload) => {
|
|
858
48
|
callback(payload);
|
|
859
49
|
});
|
|
860
50
|
}
|
|
861
|
-
_map(result) {
|
|
862
|
-
return result;
|
|
863
|
-
}
|
|
864
51
|
}
|
|
865
52
|
export const postgresAdapter = (options) => {
|
|
866
53
|
return new PostgresAdapter(options);
|