@focus-reactive/payload-plugin-translator 0.2.1 → 0.4.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/client/features/collection-translation-form/model/schema.d.ts +2 -2
- package/dist/client/features/translate-document-form/model/schema.d.ts +3 -3
- package/dist/server/features/cancel/handler.d.ts +2 -2
- package/dist/server/features/cancel/handler.js +3 -3
- package/dist/server/features/cancel-by-collection/handler.d.ts +3 -3
- package/dist/server/features/cancel-by-collection/handler.js +5 -5
- package/dist/server/features/enqueue-translation/handler.d.ts +3 -3
- package/dist/server/features/enqueue-translation/handler.js +4 -4
- package/dist/server/features/enqueue-translation/model.d.ts +4 -4
- package/dist/server/features/get-collection-status/handler.d.ts +3 -3
- package/dist/server/features/get-collection-status/handler.js +4 -4
- package/dist/server/features/get-document-status/handler.d.ts +3 -3
- package/dist/server/features/get-document-status/handler.js +4 -4
- package/dist/server/features/get-document-status/model.d.ts +1 -1
- package/dist/server/features/run-translation/handler.d.ts +2 -2
- package/dist/server/features/run-translation/handler.js +7 -7
- package/dist/server/features/translate-document/handler.d.ts +4 -4
- package/dist/server/features/translate-document/handler.js +4 -4
- package/dist/server/features/translate-document/index.d.ts +2 -3
- package/dist/server/features/translate-document/index.js +1 -2
- package/dist/server/features/translate-document/model.d.ts +4 -31
- package/dist/server/features/translate-document/model.js +3 -27
- package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +6 -4
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +4 -4
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +72 -20
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.d.ts +51 -56
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +146 -91
- package/dist/server/modules/task-runner/payload-jobs-runner/normalizeJob.d.ts +2 -2
- package/dist/server/modules/task-runner/payload-jobs-runner/normalizeJob.js +14 -12
- package/dist/server/modules/task-runner/payload-jobs-runner/readCollectionRef.d.ts +22 -0
- package/dist/server/modules/task-runner/payload-jobs-runner/readCollectionRef.js +22 -0
- package/dist/server/modules/task-runner/payload-jobs-runner/types.d.ts +30 -5
- package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.d.ts +5 -5
- package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.js +7 -6
- package/dist/server/modules/task-runner/types.d.ts +15 -5
- package/package.json +1 -1
- package/dist/server/features/translate-document/task.d.ts +0 -40
- package/dist/server/features/translate-document/task.js +0 -44
|
@@ -15,80 +15,75 @@ export declare class PayloadJobsTaskRunner implements TaskRunner {
|
|
|
15
15
|
cancel(taskIds: string[]): Promise<void>;
|
|
16
16
|
run(taskId: string): Promise<RunResult>;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* Reset stale processing locks so abandoned jobs become eligible for the
|
|
19
|
+
* autorun picker again. The picker requires processing:false, no error, and
|
|
20
|
+
* no pending waitUntil; a job abandoned mid-run (no error, no waitUntil)
|
|
21
|
+
* satisfies the rest, so clearing processing is sufficient for that case.
|
|
22
|
+
* A job that already exhausted retries (hasError:true) stays excluded from
|
|
23
|
+
* autorun and is only recoverable via a manual run().
|
|
19
24
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
25
|
+
* A job is stale when it is still `processing: true`, not yet completed, and
|
|
26
|
+
* its `updatedAt` is older than `staleJobTimeoutMs` — i.e. a process was
|
|
27
|
+
* killed mid-run (deploy/crash/timeout). Threshold-based, so a job genuinely
|
|
28
|
+
* in flight on another live instance (fresh `updatedAt`) is left alone.
|
|
29
|
+
* Filters on real `payload-jobs` columns only (no JSON-path traversal), so
|
|
30
|
+
* the drizzle SQLite issue in `findByCollection` does not apply here.
|
|
31
|
+
* @returns the number of jobs reclaimed.
|
|
32
|
+
*/
|
|
33
|
+
reclaimStaleJobs(): Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* Clear the `processing` lock on every job matching `where`, returning how
|
|
36
|
+
* many were reset. Shared by the per-job reset in `run()` (a stale lock) and
|
|
37
|
+
* the bulk boot/recovery reset in `reclaimStaleJobs()`. `depth: 0` because
|
|
38
|
+
* only the count is needed — no relationships to populate.
|
|
39
|
+
*/
|
|
40
|
+
private resetProcessing;
|
|
41
|
+
/**
|
|
42
|
+
* A processing lock is stale once `updatedAt` is older than the configured
|
|
43
|
+
* timeout — the owning run is presumed dead.
|
|
44
|
+
*/
|
|
45
|
+
private isStale;
|
|
46
|
+
/**
|
|
47
|
+
* Find translation jobs for a collection, optionally narrowed by document IDs.
|
|
24
48
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* }
|
|
49
|
+
* Narrowing is by `taskSlug` only in SQL; the collection slug and document
|
|
50
|
+
* IDs are matched in memory (via the normalized `Task`, which reads both the
|
|
51
|
+
* current flat-text shape and the legacy relationship shape). This is the
|
|
52
|
+
* one path that must transparently span both stored shapes during the
|
|
53
|
+
* ID-agnostic migration — see docs/DEPRECATIONS.md#jobs-input-collection-field.
|
|
31
54
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
55
|
+
* IMPORTANT — why slug/id are matched in memory, not in the SQL WHERE
|
|
56
|
+
* ------------------------------------------------------------------------
|
|
57
|
+
* The "natural" implementation would push `input.collection_id` into the
|
|
58
|
+
* where clause. This DOES NOT work on SQLite (and is unreliable on any
|
|
59
|
+
* adapter) because of two compounding bugs in Payload's drizzle layer.
|
|
34
60
|
*
|
|
35
61
|
* 1. The `input` field on `payload-jobs` is declared `type: 'json'`. The
|
|
36
62
|
* drizzle path resolver (`@payloadcms/drizzle/queries/getTableColumnFromPath`)
|
|
37
63
|
* has no `case 'json'` branch, so the value is left as a raw column and
|
|
38
64
|
* the path segments are passed through to `parseParams.js`, which on
|
|
39
65
|
* SQLite builds raw SQL using `convertPathToJSONTraversal` — generating
|
|
40
|
-
* expressions like `input->>'
|
|
66
|
+
* expressions like `input->>'collection_id'`.
|
|
41
67
|
*
|
|
42
68
|
* 2. When `parseParams.js` formats the right-hand side of `in`/`not_in`
|
|
43
69
|
* (and even `equals` when `!isNaN(val)`), it inlines values via JS
|
|
44
70
|
* template literals WITHOUT wrapping strings in quotes. The string
|
|
45
71
|
* `'1'` from our WHERE becomes raw `1` in the SQL. Drizzle therefore
|
|
46
|
-
* emits queries like
|
|
47
|
-
*
|
|
48
|
-
* WHERE input->>'collection'->>'value' IN (1)
|
|
49
|
-
*
|
|
50
|
-
* even though the caller passed `['1']` (an array of strings).
|
|
51
|
-
*
|
|
52
|
-
* On SQLite, `->>` preserves the JSON value's type — if the stored JSON
|
|
53
|
-
* has `"value": "1"` (a JSON string), `->>` returns SQLite TEXT `'1'`;
|
|
54
|
-
* if the JSON has `"value": 1` (a JSON number), `->>` returns INTEGER `1`.
|
|
55
|
-
* SQLite's `IN (...)` does NOT coerce between TEXT and INTEGER. So:
|
|
72
|
+
* emits queries like `WHERE input->>'collection_id' IN (1)` even though
|
|
73
|
+
* the caller passed `['1']` (an array of strings).
|
|
56
74
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* we
|
|
62
|
-
* the generated SQL and never matches the stored JSON.
|
|
63
|
-
*
|
|
64
|
-
* Postgres avoids most of this because `jsonb_path_query` returns text
|
|
65
|
-
* uniformly and PG's type coercion is more permissive, but the same
|
|
66
|
-
* un-quoted-string bug technically affects it too.
|
|
67
|
-
*
|
|
68
|
-
* Why we don't fix it upstream / patch the dep
|
|
69
|
-
* --------------------------------------------
|
|
70
|
-
* - This plugin is published to npm. Consumers install it with their own
|
|
71
|
-
* Payload version and would not receive any local `bun patch` /
|
|
72
|
-
* `patch-package` overrides on `@payloadcms/drizzle`. The plugin must
|
|
73
|
-
* work against vanilla Payload.
|
|
74
|
-
* - A PR to Payload core is the proper long-term fix, but the plugin
|
|
75
|
-
* cannot block on its merge/release cycle.
|
|
76
|
-
* - Forcing a non-numeric prefix on the stored ID (e.g., `"id:1"`) would
|
|
77
|
-
* work around bug #2, but bloats the data shape and breaks anything
|
|
78
|
-
* that reads `input.collection.value` expecting a plain id.
|
|
75
|
+
* On SQLite, `->>` preserves the JSON value's type and `IN (...)` does NOT
|
|
76
|
+
* coerce between TEXT and INTEGER, so a numeric-looking string id never
|
|
77
|
+
* matches once bug #2 strips its quotes. Storing the id as text (this
|
|
78
|
+
* migration) does not fix the SQL path — drizzle re-numbers it anyway — so
|
|
79
|
+
* we keep matching in memory.
|
|
79
80
|
*
|
|
80
81
|
* Why in-memory filtering is acceptable here
|
|
81
82
|
* ------------------------------------------
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* jobs so they don't accumulate), so a Set-membership check in JS is
|
|
87
|
-
* effectively free.
|
|
88
|
-
*
|
|
89
|
-
* If/when the upstream drizzle bug is fixed (or this plugin gains a
|
|
90
|
-
* mirror collection with indexed flat columns), this method can collapse
|
|
91
|
-
* back to a single SQL query.
|
|
83
|
+
* Per-task job sets are small (typically <100 rows; the plugin actively
|
|
84
|
+
* cancels superseded jobs so they don't accumulate), so the JS filtering
|
|
85
|
+
* is effectively free. If/when the upstream drizzle bug is fixed, this can
|
|
86
|
+
* collapse back to a single SQL query.
|
|
92
87
|
*/
|
|
93
88
|
findByCollection(collectionSlug: CollectionSlug, documentIds?: Array<string | number>): Promise<Task[]>;
|
|
94
89
|
/**
|
|
@@ -13,7 +13,7 @@ import { normalizeJob } from "./normalizeJob";
|
|
|
13
13
|
async enqueue(tasks) {
|
|
14
14
|
const byCollection = this.groupByCollection(tasks);
|
|
15
15
|
for (const [collectionSlug, items] of byCollection){
|
|
16
|
-
const documentIds = items.map((t)=>
|
|
16
|
+
const documentIds = items.map((t)=>t.collectionId);
|
|
17
17
|
const existing = await this.findByCollection(collectionSlug, documentIds);
|
|
18
18
|
if (existing.length > 0) {
|
|
19
19
|
await this.cancelInternal(existing.map((t)=>t.id));
|
|
@@ -23,19 +23,13 @@ import { normalizeJob } from "./normalizeJob";
|
|
|
23
23
|
task: this.config.taskName,
|
|
24
24
|
queue: this.config.queueName,
|
|
25
25
|
input: {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// task handler. `findByCollection` reads back via in-memory
|
|
34
|
-
// filtering and normalizes both sides with String(...) for the
|
|
35
|
-
// comparison, so it does not need write-side normalization.
|
|
36
|
-
relationTo: task.collectionSlug,
|
|
37
|
-
value: task.collectionId
|
|
38
|
-
},
|
|
26
|
+
// Flat text reference (ID-agnostic). Stored as a string — no
|
|
27
|
+
// relationship type validation against the collection's ID type,
|
|
28
|
+
// which is what previously left number-id jobs stuck in processing.
|
|
29
|
+
// This is the single write boundary, so `String(...)` here is the
|
|
30
|
+
// one place IDs are normalized for storage.
|
|
31
|
+
collection_slug: task.collectionSlug,
|
|
32
|
+
collection_id: String(task.collectionId),
|
|
39
33
|
source_lng: task.sourceLng,
|
|
40
34
|
target_lng: task.targetLng,
|
|
41
35
|
strategy: task.strategy,
|
|
@@ -69,104 +63,164 @@ import { normalizeJob } from "./normalizeJob";
|
|
|
69
63
|
};
|
|
70
64
|
}
|
|
71
65
|
if (task.status === "running") {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
// A genuinely in-flight job is refused. A stale processing lock (left by
|
|
67
|
+
// a process killed mid-run) is reclaimable: clear it first so the queue
|
|
68
|
+
// picker below — which only selects `processing: false` — can re-run it.
|
|
69
|
+
if (!this.isStale(task.updatedAt)) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: "already_running"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
await this.resetProcessing({
|
|
76
|
+
id: {
|
|
77
|
+
equals: taskId
|
|
78
|
+
}
|
|
79
|
+
});
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
// Execute synchronously via the queue + `where` picker so the job runs to
|
|
82
|
+
// completion within this request (nothing is abandoned after the HTTP
|
|
83
|
+
// response — reliable on serverless too).
|
|
84
|
+
//
|
|
85
|
+
// NOT `payload.jobs.runByID({ id })`: on the drizzle adapter the id-path
|
|
86
|
+
// (`db.updateJobs({ id })`) writes `processing: true` but returns no rows,
|
|
87
|
+
// so `runJobs` reports `noJobsRemaining` and the handler never runs —
|
|
88
|
+
// leaving the job stuck at `processing: true` forever. The `where`-based
|
|
89
|
+
// picker selects, runs, and finalizes the job correctly (verified against
|
|
90
|
+
// sqlite). The picker also enforces processing:false / no-error / no
|
|
91
|
+
// pending waitUntil, so a failed (max-retries) job is not re-run here.
|
|
92
|
+
await this.payload.jobs.run({
|
|
93
|
+
queue: this.config.queueName,
|
|
94
|
+
where: {
|
|
95
|
+
id: {
|
|
96
|
+
equals: taskId
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
limit: 1
|
|
79
100
|
});
|
|
80
101
|
return {
|
|
81
102
|
success: true
|
|
82
103
|
};
|
|
83
104
|
}
|
|
84
105
|
/**
|
|
85
|
-
*
|
|
106
|
+
* Reset stale processing locks so abandoned jobs become eligible for the
|
|
107
|
+
* autorun picker again. The picker requires processing:false, no error, and
|
|
108
|
+
* no pending waitUntil; a job abandoned mid-run (no error, no waitUntil)
|
|
109
|
+
* satisfies the rest, so clearing processing is sufficient for that case.
|
|
110
|
+
* A job that already exhausted retries (hasError:true) stays excluded from
|
|
111
|
+
* autorun and is only recoverable via a manual run().
|
|
86
112
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
113
|
+
* A job is stale when it is still `processing: true`, not yet completed, and
|
|
114
|
+
* its `updatedAt` is older than `staleJobTimeoutMs` — i.e. a process was
|
|
115
|
+
* killed mid-run (deploy/crash/timeout). Threshold-based, so a job genuinely
|
|
116
|
+
* in flight on another live instance (fresh `updatedAt`) is left alone.
|
|
117
|
+
* Filters on real `payload-jobs` columns only (no JSON-path traversal), so
|
|
118
|
+
* the drizzle SQLite issue in `findByCollection` does not apply here.
|
|
119
|
+
* @returns the number of jobs reclaimed.
|
|
120
|
+
*/ async reclaimStaleJobs() {
|
|
121
|
+
const cutoff = new Date(Date.now() - this.config.staleJobTimeoutMs).toISOString();
|
|
122
|
+
return this.resetProcessing({
|
|
123
|
+
and: [
|
|
124
|
+
{
|
|
125
|
+
taskSlug: {
|
|
126
|
+
equals: this.config.taskName
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
processing: {
|
|
131
|
+
equals: true
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
completedAt: {
|
|
136
|
+
exists: false
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
updatedAt: {
|
|
141
|
+
less_than: cutoff
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Clear the `processing` lock on every job matching `where`, returning how
|
|
149
|
+
* many were reset. Shared by the per-job reset in `run()` (a stale lock) and
|
|
150
|
+
* the bulk boot/recovery reset in `reclaimStaleJobs()`. `depth: 0` because
|
|
151
|
+
* only the count is needed — no relationships to populate.
|
|
152
|
+
*/ async resetProcessing(where) {
|
|
153
|
+
const result = await this.payload.update({
|
|
154
|
+
collection: this.config.jobsCollection,
|
|
155
|
+
depth: 0,
|
|
156
|
+
where,
|
|
157
|
+
data: {
|
|
158
|
+
processing: false
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return result.docs.length;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A processing lock is stale once `updatedAt` is older than the configured
|
|
165
|
+
* timeout — the owning run is presumed dead.
|
|
166
|
+
*/ isStale(updatedAt) {
|
|
167
|
+
const parsed = Date.parse(updatedAt);
|
|
168
|
+
// Unknown/corrupt timestamp → treat as stale so the job can be recovered
|
|
169
|
+
// rather than permanently refused as already-running.
|
|
170
|
+
if (Number.isNaN(parsed)) return true;
|
|
171
|
+
return Date.now() - parsed > this.config.staleJobTimeoutMs;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Find translation jobs for a collection, optionally narrowed by document IDs.
|
|
91
175
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* }
|
|
176
|
+
* Narrowing is by `taskSlug` only in SQL; the collection slug and document
|
|
177
|
+
* IDs are matched in memory (via the normalized `Task`, which reads both the
|
|
178
|
+
* current flat-text shape and the legacy relationship shape). This is the
|
|
179
|
+
* one path that must transparently span both stored shapes during the
|
|
180
|
+
* ID-agnostic migration — see docs/DEPRECATIONS.md#jobs-input-collection-field.
|
|
98
181
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
182
|
+
* IMPORTANT — why slug/id are matched in memory, not in the SQL WHERE
|
|
183
|
+
* ------------------------------------------------------------------------
|
|
184
|
+
* The "natural" implementation would push `input.collection_id` into the
|
|
185
|
+
* where clause. This DOES NOT work on SQLite (and is unreliable on any
|
|
186
|
+
* adapter) because of two compounding bugs in Payload's drizzle layer.
|
|
101
187
|
*
|
|
102
188
|
* 1. The `input` field on `payload-jobs` is declared `type: 'json'`. The
|
|
103
189
|
* drizzle path resolver (`@payloadcms/drizzle/queries/getTableColumnFromPath`)
|
|
104
190
|
* has no `case 'json'` branch, so the value is left as a raw column and
|
|
105
191
|
* the path segments are passed through to `parseParams.js`, which on
|
|
106
192
|
* SQLite builds raw SQL using `convertPathToJSONTraversal` — generating
|
|
107
|
-
* expressions like `input->>'
|
|
193
|
+
* expressions like `input->>'collection_id'`.
|
|
108
194
|
*
|
|
109
195
|
* 2. When `parseParams.js` formats the right-hand side of `in`/`not_in`
|
|
110
196
|
* (and even `equals` when `!isNaN(val)`), it inlines values via JS
|
|
111
197
|
* template literals WITHOUT wrapping strings in quotes. The string
|
|
112
198
|
* `'1'` from our WHERE becomes raw `1` in the SQL. Drizzle therefore
|
|
113
|
-
* emits queries like
|
|
114
|
-
*
|
|
115
|
-
* WHERE input->>'collection'->>'value' IN (1)
|
|
116
|
-
*
|
|
117
|
-
* even though the caller passed `['1']` (an array of strings).
|
|
118
|
-
*
|
|
119
|
-
* On SQLite, `->>` preserves the JSON value's type — if the stored JSON
|
|
120
|
-
* has `"value": "1"` (a JSON string), `->>` returns SQLite TEXT `'1'`;
|
|
121
|
-
* if the JSON has `"value": 1` (a JSON number), `->>` returns INTEGER `1`.
|
|
122
|
-
* SQLite's `IN (...)` does NOT coerce between TEXT and INTEGER. So:
|
|
199
|
+
* emits queries like `WHERE input->>'collection_id' IN (1)` even though
|
|
200
|
+
* the caller passed `['1']` (an array of strings).
|
|
123
201
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* we
|
|
129
|
-
* the generated SQL and never matches the stored JSON.
|
|
130
|
-
*
|
|
131
|
-
* Postgres avoids most of this because `jsonb_path_query` returns text
|
|
132
|
-
* uniformly and PG's type coercion is more permissive, but the same
|
|
133
|
-
* un-quoted-string bug technically affects it too.
|
|
134
|
-
*
|
|
135
|
-
* Why we don't fix it upstream / patch the dep
|
|
136
|
-
* --------------------------------------------
|
|
137
|
-
* - This plugin is published to npm. Consumers install it with their own
|
|
138
|
-
* Payload version and would not receive any local `bun patch` /
|
|
139
|
-
* `patch-package` overrides on `@payloadcms/drizzle`. The plugin must
|
|
140
|
-
* work against vanilla Payload.
|
|
141
|
-
* - A PR to Payload core is the proper long-term fix, but the plugin
|
|
142
|
-
* cannot block on its merge/release cycle.
|
|
143
|
-
* - Forcing a non-numeric prefix on the stored ID (e.g., `"id:1"`) would
|
|
144
|
-
* work around bug #2, but bloats the data shape and breaks anything
|
|
145
|
-
* that reads `input.collection.value` expecting a plain id.
|
|
202
|
+
* On SQLite, `->>` preserves the JSON value's type and `IN (...)` does NOT
|
|
203
|
+
* coerce between TEXT and INTEGER, so a numeric-looking string id never
|
|
204
|
+
* matches once bug #2 strips its quotes. Storing the id as text (this
|
|
205
|
+
* migration) does not fix the SQL path — drizzle re-numbers it anyway — so
|
|
206
|
+
* we keep matching in memory.
|
|
146
207
|
*
|
|
147
208
|
* Why in-memory filtering is acceptable here
|
|
148
209
|
* ------------------------------------------
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* jobs so they don't accumulate), so a Set-membership check in JS is
|
|
154
|
-
* effectively free.
|
|
155
|
-
*
|
|
156
|
-
* If/when the upstream drizzle bug is fixed (or this plugin gains a
|
|
157
|
-
* mirror collection with indexed flat columns), this method can collapse
|
|
158
|
-
* back to a single SQL query.
|
|
210
|
+
* Per-task job sets are small (typically <100 rows; the plugin actively
|
|
211
|
+
* cancels superseded jobs so they don't accumulate), so the JS filtering
|
|
212
|
+
* is effectively free. If/when the upstream drizzle bug is fixed, this can
|
|
213
|
+
* collapse back to a single SQL query.
|
|
159
214
|
*/ async findByCollection(collectionSlug, documentIds) {
|
|
160
|
-
const
|
|
161
|
-
"input.collection.relationTo": {
|
|
162
|
-
equals: collectionSlug
|
|
163
|
-
}
|
|
164
|
-
}, {
|
|
215
|
+
const all = await this.findJobsInternal(undefined, {
|
|
165
216
|
pagination: false
|
|
166
217
|
});
|
|
167
|
-
|
|
218
|
+
const bySlug = all.filter((t)=>t.input.collectionSlug === collectionSlug);
|
|
219
|
+
if (!documentIds?.length) return bySlug;
|
|
220
|
+
// `documentIds` is the public `Array<string | number>` param, so normalize
|
|
221
|
+
// it here; `t.input.collectionId` is already `ID` (string) via normalizeJob.
|
|
168
222
|
const wanted = new Set(documentIds.map(String));
|
|
169
|
-
return
|
|
223
|
+
return bySlug.filter((t)=>wanted.has(t.input.collectionId));
|
|
170
224
|
}
|
|
171
225
|
/**
|
|
172
226
|
* Group tasks by collection slug
|
|
@@ -203,19 +257,20 @@ import { normalizeJob } from "./normalizeJob";
|
|
|
203
257
|
/**
|
|
204
258
|
* Internal method to find jobs with where clause
|
|
205
259
|
*/ async findJobsInternal(where, params) {
|
|
260
|
+
const and = [
|
|
261
|
+
{
|
|
262
|
+
taskSlug: {
|
|
263
|
+
equals: this.config.taskName
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
if (where) and.push(where);
|
|
206
268
|
const response = await this.payload.find({
|
|
207
269
|
collection: this.config.jobsCollection,
|
|
208
270
|
limit: params?.limit,
|
|
209
271
|
pagination: params?.pagination,
|
|
210
272
|
where: {
|
|
211
|
-
and
|
|
212
|
-
{
|
|
213
|
-
taskSlug: {
|
|
214
|
-
equals: this.config.taskName
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
where
|
|
218
|
-
]
|
|
273
|
+
and
|
|
219
274
|
}
|
|
220
275
|
});
|
|
221
276
|
return response.docs.map(normalizeJob);
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { readCollectionRef } from "./readCollectionRef";
|
|
1
2
|
/**
|
|
2
3
|
* Transform Payload job to normalized Task
|
|
3
4
|
*/ export function normalizeJob(job) {
|
|
5
|
+
const { collectionSlug, collectionId } = readCollectionRef(job.input);
|
|
4
6
|
return {
|
|
5
7
|
id: job.id,
|
|
6
8
|
status: getJobStatus(job),
|
|
7
9
|
input: {
|
|
8
|
-
collectionSlug
|
|
9
|
-
collectionId
|
|
10
|
-
sourceLng: job.input?.source_lng ??
|
|
11
|
-
targetLng: job.input?.target_lng ??
|
|
12
|
-
strategy: job.input?.strategy ??
|
|
10
|
+
collectionSlug,
|
|
11
|
+
collectionId,
|
|
12
|
+
sourceLng: job.input?.source_lng ?? "",
|
|
13
|
+
targetLng: job.input?.target_lng ?? "",
|
|
14
|
+
strategy: job.input?.strategy ?? "overwrite",
|
|
13
15
|
publishOnTranslation: job.input?.publish_on_translation ?? false
|
|
14
16
|
},
|
|
15
17
|
createdAt: job.createdAt,
|
|
@@ -22,19 +24,19 @@
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
function getJobStatus(job) {
|
|
25
|
-
if (job.completedAt) return
|
|
26
|
-
if (job.processing) return
|
|
27
|
-
if (job.error) return
|
|
28
|
-
return
|
|
27
|
+
if (job.completedAt) return "completed";
|
|
28
|
+
if (job.processing) return "running";
|
|
29
|
+
if (job.error) return "failed";
|
|
30
|
+
return "pending";
|
|
29
31
|
}
|
|
30
32
|
function extractErrorMessage(error) {
|
|
31
|
-
if (error && typeof error ===
|
|
33
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
32
34
|
return error.message;
|
|
33
35
|
}
|
|
34
|
-
return
|
|
36
|
+
return "Unknown error";
|
|
35
37
|
}
|
|
36
38
|
function isCancelled(error) {
|
|
37
|
-
return error !== null && typeof error ===
|
|
39
|
+
return error !== null && typeof error === "object" && "cancelled" in error && typeof error.cancelled === "boolean" && error.cancelled;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
//# sourceMappingURL=normalizeJob.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CollectionSlug } from "payload";
|
|
2
|
+
import type { ID } from "../types";
|
|
3
|
+
import type { PayloadJob } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Normalized, ID-agnostic document reference parsed out of a stored job input.
|
|
6
|
+
*/
|
|
7
|
+
export type CollectionRef = {
|
|
8
|
+
collectionSlug: CollectionSlug;
|
|
9
|
+
collectionId: ID;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Single storage-read boundary for a job's collection reference.
|
|
13
|
+
*
|
|
14
|
+
* Reads the current flat-text shape (`collection_slug` / `collection_id`) and
|
|
15
|
+
* falls back to the legacy relationship shape (`collection.{relationTo,value}`)
|
|
16
|
+
* for jobs queued before the ID-agnostic migration. This is the one place a
|
|
17
|
+
* stored id is coerced to string — the coercion exists only because the legacy
|
|
18
|
+
* relationship `value` is typed `string | number`, and it goes away together
|
|
19
|
+
* with the legacy field in the next major.
|
|
20
|
+
* See docs/DEPRECATIONS.md#jobs-input-collection-field
|
|
21
|
+
*/
|
|
22
|
+
export declare function readCollectionRef(input: PayloadJob["input"]): CollectionRef;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single storage-read boundary for a job's collection reference.
|
|
3
|
+
*
|
|
4
|
+
* Reads the current flat-text shape (`collection_slug` / `collection_id`) and
|
|
5
|
+
* falls back to the legacy relationship shape (`collection.{relationTo,value}`)
|
|
6
|
+
* for jobs queued before the ID-agnostic migration. This is the one place a
|
|
7
|
+
* stored id is coerced to string — the coercion exists only because the legacy
|
|
8
|
+
* relationship `value` is typed `string | number`, and it goes away together
|
|
9
|
+
* with the legacy field in the next major.
|
|
10
|
+
* See docs/DEPRECATIONS.md#jobs-input-collection-field
|
|
11
|
+
*/ export function readCollectionRef(input) {
|
|
12
|
+
// `??` is intentional: `collection_slug` / `collection_id` are `required: true`
|
|
13
|
+
// in the inputSchema, so they are never an empty string for any job this plugin
|
|
14
|
+
// writes. The fallback to the legacy shape therefore only fires when the new
|
|
15
|
+
// field is genuinely absent (i.e. a job queued before the ID-agnostic migration).
|
|
16
|
+
return {
|
|
17
|
+
collectionSlug: input?.collection_slug ?? input?.collection?.relationTo ?? "",
|
|
18
|
+
collectionId: String(input?.collection_id ?? input?.collection?.value ?? "")
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=readCollectionRef.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CollectionSlug } from
|
|
1
|
+
import type { CollectionSlug } from "payload";
|
|
2
2
|
/**
|
|
3
3
|
* Configuration for automatic job processing.
|
|
4
4
|
*/
|
|
@@ -32,7 +32,7 @@ export type PayloadJobsRunnerOptions = {
|
|
|
32
32
|
* Name of the Payload jobs collection.
|
|
33
33
|
* @default 'payload-jobs'
|
|
34
34
|
*/
|
|
35
|
-
jobsCollection?:
|
|
35
|
+
jobsCollection?: CollectionSlug;
|
|
36
36
|
/**
|
|
37
37
|
* Automatic job processing configuration.
|
|
38
38
|
* Set to `false` to disable (for Vercel/serverless deployments).
|
|
@@ -40,6 +40,22 @@ export type PayloadJobsRunnerOptions = {
|
|
|
40
40
|
* @default { cron: '* * * * *', limit: 50 }
|
|
41
41
|
*/
|
|
42
42
|
autoRun?: false | AutoRunConfig;
|
|
43
|
+
/**
|
|
44
|
+
* How long (ms) a job may stay `processing: true` before its lock is
|
|
45
|
+
* considered stale and the job becomes eligible to be re-run.
|
|
46
|
+
*
|
|
47
|
+
* A process killed mid-run (deploy, crash, request timeout) leaves a job
|
|
48
|
+
* stuck at `processing: true`; the autorun picker only takes
|
|
49
|
+
* `processing: false`, so without recovery such a job would hang forever.
|
|
50
|
+
* On boot the runner resets stale locks, and manual `run()` will re-claim a
|
|
51
|
+
* stale-locked job instead of refusing it as already-running.
|
|
52
|
+
*
|
|
53
|
+
* MUST be larger than the longest a single document translation can
|
|
54
|
+
* legitimately take, otherwise a genuinely in-flight job could be reclaimed
|
|
55
|
+
* and run twice (safe under the idempotent `overwrite` strategy, but wasteful).
|
|
56
|
+
* @default 300000 (5 minutes)
|
|
57
|
+
*/
|
|
58
|
+
staleJobTimeoutMs?: number;
|
|
43
59
|
/**
|
|
44
60
|
* Retry configuration for failed jobs.
|
|
45
61
|
*/
|
|
@@ -47,7 +63,7 @@ export type PayloadJobsRunnerOptions = {
|
|
|
47
63
|
attempts?: number;
|
|
48
64
|
backoff?: {
|
|
49
65
|
delay?: number;
|
|
50
|
-
type:
|
|
66
|
+
type: "exponential" | "fixed";
|
|
51
67
|
};
|
|
52
68
|
};
|
|
53
69
|
};
|
|
@@ -57,9 +73,10 @@ export type PayloadJobsRunnerOptions = {
|
|
|
57
73
|
export type PayloadJobsRunnerConfig = {
|
|
58
74
|
taskName: string;
|
|
59
75
|
queueName: string;
|
|
60
|
-
jobsCollection:
|
|
76
|
+
jobsCollection: CollectionSlug;
|
|
61
77
|
autoRun: false | Required<AutoRunConfig>;
|
|
62
|
-
|
|
78
|
+
staleJobTimeoutMs: number;
|
|
79
|
+
retries?: PayloadJobsRunnerOptions["retries"];
|
|
63
80
|
};
|
|
64
81
|
/**
|
|
65
82
|
* Raw Payload job structure
|
|
@@ -72,6 +89,14 @@ export type PayloadJob = {
|
|
|
72
89
|
error?: unknown;
|
|
73
90
|
processing?: boolean | null;
|
|
74
91
|
input?: {
|
|
92
|
+
/** Document reference (flat text, ID-agnostic). Current shape. */
|
|
93
|
+
collection_slug?: string;
|
|
94
|
+
collection_id?: string;
|
|
95
|
+
/**
|
|
96
|
+
* @deprecated Legacy relationship shape, read-only fallback for jobs queued
|
|
97
|
+
* before the ID-agnostic migration. Removed in next major.
|
|
98
|
+
* See docs/DEPRECATIONS.md#jobs-input-collection-field
|
|
99
|
+
*/
|
|
75
100
|
collection?: {
|
|
76
101
|
relationTo: CollectionSlug;
|
|
77
102
|
value: string | number;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Payload, CollectionSlug } from
|
|
2
|
-
import type { TaskRunner } from
|
|
3
|
-
import type { TaskHandler } from
|
|
4
|
-
import type { Task, TaskInput, RunResult } from
|
|
5
|
-
import type { LazyMap } from
|
|
1
|
+
import type { Payload, CollectionSlug } from "payload";
|
|
2
|
+
import type { TaskRunner } from "../TaskRunner.interface";
|
|
3
|
+
import type { TaskHandler } from "../TaskRunnerProvider.interface";
|
|
4
|
+
import type { Task, TaskInput, RunResult } from "../types";
|
|
5
|
+
import type { LazyMap } from "../../../shared/utils";
|
|
6
6
|
/**
|
|
7
7
|
* Synchronous TaskRunner implementation.
|
|
8
8
|
*
|