@focus-reactive/payload-plugin-translator 0.2.0 → 0.3.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.
Files changed (28) hide show
  1. package/dist/client/entities/translation/api/queries/useCollectionTranslationStatus.d.ts +77 -13
  2. package/dist/client/entities/translation/api/queries/useDocumentTranslation.d.ts +77 -13
  3. package/dist/client/features/collection-translation-form/model/schema.d.ts +2 -2
  4. package/dist/client/features/translate-document-form/model/schema.d.ts +3 -3
  5. package/dist/server/features/enqueue-translation/model.d.ts +4 -4
  6. package/dist/server/features/get-document-status/model.d.ts +1 -1
  7. package/dist/server/features/translate-document/handler.d.ts +4 -4
  8. package/dist/server/features/translate-document/handler.js +4 -4
  9. package/dist/server/features/translate-document/index.d.ts +2 -3
  10. package/dist/server/features/translate-document/index.js +1 -2
  11. package/dist/server/features/translate-document/model.d.ts +4 -31
  12. package/dist/server/features/translate-document/model.js +3 -27
  13. package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +5 -4
  14. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +4 -4
  15. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +46 -20
  16. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.d.ts +22 -56
  17. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +45 -85
  18. package/dist/server/modules/task-runner/payload-jobs-runner/normalizeJob.d.ts +2 -2
  19. package/dist/server/modules/task-runner/payload-jobs-runner/normalizeJob.js +14 -12
  20. package/dist/server/modules/task-runner/payload-jobs-runner/readCollectionRef.d.ts +22 -0
  21. package/dist/server/modules/task-runner/payload-jobs-runner/readCollectionRef.js +22 -0
  22. package/dist/server/modules/task-runner/payload-jobs-runner/types.d.ts +13 -5
  23. package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.d.ts +5 -5
  24. package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.js +7 -6
  25. package/dist/server/modules/task-runner/types.d.ts +15 -5
  26. package/package.json +1 -1
  27. package/dist/server/features/translate-document/task.d.ts +0 -40
  28. package/dist/server/features/translate-document/task.js +0 -44
@@ -1,17 +1,18 @@
1
- import { PayloadJobsTaskRunner } from './PayloadJobsTaskRunner';
1
+ import { PayloadJobsTaskRunner } from "./PayloadJobsTaskRunner";
2
+ import { readCollectionRef } from "./readCollectionRef";
2
3
  const defaultAutoRun = {
3
- cron: '* * * * *',
4
+ cron: "* * * * *",
4
5
  limit: 50
5
6
  };
6
7
  const defaultValues = {
7
- taskName: 'translate_document',
8
- queueName: 'translations',
9
- jobsCollection: 'payload-jobs',
8
+ taskName: "translate_document",
9
+ queueName: "translations",
10
+ jobsCollection: "payload-jobs",
10
11
  autoRun: defaultAutoRun,
11
12
  retries: {
12
13
  attempts: 3,
13
14
  backoff: {
14
- type: 'exponential',
15
+ type: "exponential",
15
16
  delay: 5000
16
17
  }
17
18
  }
@@ -43,33 +44,57 @@ const defaultValues = {
43
44
  const { handler, collections } = context;
44
45
  return (config)=>{
45
46
  const inputSchema = [
47
+ // Flat text reference (ID-agnostic). Current shape that jobs are
48
+ // written with — no relationship type validation against the target
49
+ // collection's ID type, so string IDs work for number-id collections.
46
50
  {
47
- type: 'relationship',
48
- name: 'collection',
49
- relationTo: collections,
51
+ type: "text",
52
+ name: "collection_slug",
53
+ required: true
54
+ },
55
+ {
56
+ type: "text",
57
+ name: "collection_id",
50
58
  required: true
51
59
  },
60
+ /**
61
+ * Legacy relationship reference, kept as a read-only fallback so jobs
62
+ * queued before the ID-agnostic migration stay readable. No longer
63
+ * written; demoted to `required: false` so new jobs (which omit it)
64
+ * pass validation. Removed in the next major.
65
+ * See docs/DEPRECATIONS.md#jobs-input-collection-field
66
+ * @deprecated
67
+ */ {
68
+ type: "relationship",
69
+ name: "collection",
70
+ relationTo: collections,
71
+ required: false,
72
+ admin: {
73
+ readOnly: true,
74
+ description: "Deprecated. See docs/DEPRECATIONS.md#jobs-input-collection-field"
75
+ }
76
+ },
52
77
  {
53
- type: 'text',
78
+ type: "text",
54
79
  maxLength: 256,
55
- name: 'source_lng',
80
+ name: "source_lng",
56
81
  required: true
57
82
  },
58
83
  {
59
- type: 'text',
84
+ type: "text",
60
85
  maxLength: 256,
61
- name: 'target_lng',
86
+ name: "target_lng",
62
87
  required: true
63
88
  },
64
89
  {
65
- type: 'text',
90
+ type: "text",
66
91
  maxLength: 256,
67
- name: 'strategy',
92
+ name: "strategy",
68
93
  required: true
69
94
  },
70
95
  {
71
- type: 'checkbox',
72
- name: 'publish_on_translation',
96
+ type: "checkbox",
97
+ name: "publish_on_translation",
73
98
  defaultValue: false
74
99
  }
75
100
  ];
@@ -78,9 +103,10 @@ const defaultValues = {
78
103
  inputSchema,
79
104
  retries,
80
105
  handler: async (args)=>{
106
+ const { collectionSlug, collectionId } = readCollectionRef(args.input);
81
107
  await handler(args.req.payload, {
82
- collection: args.input.collection.relationTo,
83
- collectionId: args.input.collection.value,
108
+ collection: collectionSlug,
109
+ collectionId,
84
110
  sourceLng: args.input.source_lng,
85
111
  targetLng: args.input.target_lng,
86
112
  strategy: args.input.strategy,
@@ -104,7 +130,7 @@ const defaultValues = {
104
130
  const existingAutoRun = config.jobs.autoRun;
105
131
  if (Array.isArray(existingAutoRun)) {
106
132
  existingAutoRun.push(autoRunConfig);
107
- } else if (typeof existingAutoRun === 'function') {
133
+ } else if (typeof existingAutoRun === "function") {
108
134
  config.jobs.autoRun = async (payload)=>[
109
135
  ...await existingAutoRun(payload),
110
136
  autoRunConfig
@@ -17,78 +17,44 @@ export declare class PayloadJobsTaskRunner implements TaskRunner {
17
17
  /**
18
18
  * Find translation jobs for a collection, optionally narrowed by document IDs.
19
19
  *
20
- * IMPORTANT why we filter `collection.value` in memory instead of in WHERE
21
- * ------------------------------------------------------------------------
22
- * The "natural" implementation would be a single `payload.find` with the
23
- * full where clause:
24
- *
25
- * where: {
26
- * and: [
27
- * { 'input.collection.relationTo': { equals: collectionSlug } },
28
- * { 'input.collection.value': { in: documentIds } },
29
- * ],
30
- * }
20
+ * Narrowing is by `taskSlug` only in SQL; the collection slug and document
21
+ * IDs are matched in memory (via the normalized `Task`, which reads both the
22
+ * current flat-text shape and the legacy relationship shape). This is the
23
+ * one path that must transparently span both stored shapes during the
24
+ * ID-agnostic migration — see docs/DEPRECATIONS.md#jobs-input-collection-field.
31
25
  *
32
- * This DOES NOT work on SQLite (and is unreliable on any adapter) because
33
- * of two compounding bugs in Payload's drizzle layer.
26
+ * IMPORTANT why slug/id are matched in memory, not in the SQL WHERE
27
+ * ------------------------------------------------------------------------
28
+ * The "natural" implementation would push `input.collection_id` into the
29
+ * where clause. This DOES NOT work on SQLite (and is unreliable on any
30
+ * adapter) because of two compounding bugs in Payload's drizzle layer.
34
31
  *
35
32
  * 1. The `input` field on `payload-jobs` is declared `type: 'json'`. The
36
33
  * drizzle path resolver (`@payloadcms/drizzle/queries/getTableColumnFromPath`)
37
34
  * has no `case 'json'` branch, so the value is left as a raw column and
38
35
  * the path segments are passed through to `parseParams.js`, which on
39
36
  * SQLite builds raw SQL using `convertPathToJSONTraversal` — generating
40
- * expressions like `input->>'collection'->>'value'`.
37
+ * expressions like `input->>'collection_id'`.
41
38
  *
42
39
  * 2. When `parseParams.js` formats the right-hand side of `in`/`not_in`
43
40
  * (and even `equals` when `!isNaN(val)`), it inlines values via JS
44
41
  * template literals WITHOUT wrapping strings in quotes. The string
45
42
  * `'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).
43
+ * emits queries like `WHERE input->>'collection_id' IN (1)` even though
44
+ * the caller passed `['1']` (an array of strings).
51
45
  *
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:
56
- *
57
- * TEXT '1' IN (1) → false (text vs integer, no match)
58
- * INTEGER 1 IN ('1') → false (integer vs text, no match)
59
- *
60
- * Combined with bug #2 above, any value passed by the caller — even if
61
- * we normalize it to a string on write — gets re-coerced to a number in
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.
46
+ * On SQLite, `->>` preserves the JSON value's type and `IN (...)` does NOT
47
+ * coerce between TEXT and INTEGER, so a numeric-looking string id never
48
+ * matches once bug #2 strips its quotes. Storing the id as text (this
49
+ * migration) does not fix the SQL path drizzle re-numbers it anyway — so
50
+ * we keep matching in memory.
79
51
  *
80
52
  * Why in-memory filtering is acceptable here
81
53
  * ------------------------------------------
82
- * We narrow the SQL query to `taskSlug + relationTo` (both string
83
- * equality, which Payload quotes correctly), then filter the result set
84
- * by `collection.value` in JavaScript. Per-collection job sets are
85
- * small (typically <100 rows; the plugin actively cancels superseded
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.
54
+ * Per-task job sets are small (typically <100 rows; the plugin actively
55
+ * cancels superseded jobs so they don't accumulate), so the JS filtering
56
+ * is effectively free. If/when the upstream drizzle bug is fixed, this can
57
+ * collapse back to a single SQL query.
92
58
  */
93
59
  findByCollection(collectionSlug: CollectionSlug, documentIds?: Array<string | number>): Promise<Task[]>;
94
60
  /**
@@ -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)=>String(t.collectionId));
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
- collection: {
27
- // Pass `value` through verbatim. The Payload Jobs `input` schema
28
- // declares this as a `relationship` field, which validates the
29
- // value's type against the target collection's ID type (number
30
- // for autoincrement, string for uuid). Coercing to string here
31
- // would silently fail validation for number-id collections and
32
- // leave jobs stuck in processing without ever invoking the
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,
@@ -84,89 +78,54 @@ import { normalizeJob } from "./normalizeJob";
84
78
  /**
85
79
  * Find translation jobs for a collection, optionally narrowed by document IDs.
86
80
  *
87
- * IMPORTANT why we filter `collection.value` in memory instead of in WHERE
88
- * ------------------------------------------------------------------------
89
- * The "natural" implementation would be a single `payload.find` with the
90
- * full where clause:
91
- *
92
- * where: {
93
- * and: [
94
- * { 'input.collection.relationTo': { equals: collectionSlug } },
95
- * { 'input.collection.value': { in: documentIds } },
96
- * ],
97
- * }
81
+ * Narrowing is by `taskSlug` only in SQL; the collection slug and document
82
+ * IDs are matched in memory (via the normalized `Task`, which reads both the
83
+ * current flat-text shape and the legacy relationship shape). This is the
84
+ * one path that must transparently span both stored shapes during the
85
+ * ID-agnostic migration — see docs/DEPRECATIONS.md#jobs-input-collection-field.
98
86
  *
99
- * This DOES NOT work on SQLite (and is unreliable on any adapter) because
100
- * of two compounding bugs in Payload's drizzle layer.
87
+ * IMPORTANT why slug/id are matched in memory, not in the SQL WHERE
88
+ * ------------------------------------------------------------------------
89
+ * The "natural" implementation would push `input.collection_id` into the
90
+ * where clause. This DOES NOT work on SQLite (and is unreliable on any
91
+ * adapter) because of two compounding bugs in Payload's drizzle layer.
101
92
  *
102
93
  * 1. The `input` field on `payload-jobs` is declared `type: 'json'`. The
103
94
  * drizzle path resolver (`@payloadcms/drizzle/queries/getTableColumnFromPath`)
104
95
  * has no `case 'json'` branch, so the value is left as a raw column and
105
96
  * the path segments are passed through to `parseParams.js`, which on
106
97
  * SQLite builds raw SQL using `convertPathToJSONTraversal` — generating
107
- * expressions like `input->>'collection'->>'value'`.
98
+ * expressions like `input->>'collection_id'`.
108
99
  *
109
100
  * 2. When `parseParams.js` formats the right-hand side of `in`/`not_in`
110
101
  * (and even `equals` when `!isNaN(val)`), it inlines values via JS
111
102
  * template literals WITHOUT wrapping strings in quotes. The string
112
103
  * `'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:
123
- *
124
- * TEXT '1' IN (1) → false (text vs integer, no match)
125
- * INTEGER 1 IN ('1') → false (integer vs text, no match)
126
- *
127
- * Combined with bug #2 above, any value passed by the caller — even if
128
- * we normalize it to a string on write — gets re-coerced to a number in
129
- * the generated SQL and never matches the stored JSON.
104
+ * emits queries like `WHERE input->>'collection_id' IN (1)` even though
105
+ * the caller passed `['1']` (an array of strings).
130
106
  *
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.
107
+ * On SQLite, `->>` preserves the JSON value's type and `IN (...)` does NOT
108
+ * coerce between TEXT and INTEGER, so a numeric-looking string id never
109
+ * matches once bug #2 strips its quotes. Storing the id as text (this
110
+ * migration) does not fix the SQL path — drizzle re-numbers it anyway — so
111
+ * we keep matching in memory.
146
112
  *
147
113
  * Why in-memory filtering is acceptable here
148
114
  * ------------------------------------------
149
- * We narrow the SQL query to `taskSlug + relationTo` (both string
150
- * equality, which Payload quotes correctly), then filter the result set
151
- * by `collection.value` in JavaScript. Per-collection job sets are
152
- * small (typically <100 rows; the plugin actively cancels superseded
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.
115
+ * Per-task job sets are small (typically <100 rows; the plugin actively
116
+ * cancels superseded jobs so they don't accumulate), so the JS filtering
117
+ * is effectively free. If/when the upstream drizzle bug is fixed, this can
118
+ * collapse back to a single SQL query.
159
119
  */ async findByCollection(collectionSlug, documentIds) {
160
- const tasks = await this.findJobsInternal({
161
- "input.collection.relationTo": {
162
- equals: collectionSlug
163
- }
164
- }, {
120
+ const all = await this.findJobsInternal(undefined, {
165
121
  pagination: false
166
122
  });
167
- if (!documentIds?.length) return tasks;
123
+ const bySlug = all.filter((t)=>t.input.collectionSlug === collectionSlug);
124
+ if (!documentIds?.length) return bySlug;
125
+ // `documentIds` is the public `Array<string | number>` param, so normalize
126
+ // it here; `t.input.collectionId` is already `ID` (string) via normalizeJob.
168
127
  const wanted = new Set(documentIds.map(String));
169
- return tasks.filter((t)=>wanted.has(String(t.input.collectionId)));
128
+ return bySlug.filter((t)=>wanted.has(t.input.collectionId));
170
129
  }
171
130
  /**
172
131
  * Group tasks by collection slug
@@ -203,19 +162,20 @@ import { normalizeJob } from "./normalizeJob";
203
162
  /**
204
163
  * Internal method to find jobs with where clause
205
164
  */ async findJobsInternal(where, params) {
165
+ const and = [
166
+ {
167
+ taskSlug: {
168
+ equals: this.config.taskName
169
+ }
170
+ }
171
+ ];
172
+ if (where) and.push(where);
206
173
  const response = await this.payload.find({
207
174
  collection: this.config.jobsCollection,
208
175
  limit: params?.limit,
209
176
  pagination: params?.pagination,
210
177
  where: {
211
- and: [
212
- {
213
- taskSlug: {
214
- equals: this.config.taskName
215
- }
216
- },
217
- where
218
- ]
178
+ and
219
179
  }
220
180
  });
221
181
  return response.docs.map(normalizeJob);
@@ -1,5 +1,5 @@
1
- import type { Task } from '../types';
2
- import type { PayloadJob } from './types';
1
+ import type { Task } from "../types";
2
+ import type { PayloadJob } from "./types";
3
3
  /**
4
4
  * Transform Payload job to normalized Task
5
5
  */
@@ -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: job.input?.collection?.relationTo ?? '',
9
- collectionId: job.input?.collection?.value ?? '',
10
- sourceLng: job.input?.source_lng ?? '',
11
- targetLng: job.input?.target_lng ?? '',
12
- strategy: job.input?.strategy ?? 'overwrite',
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 'completed';
26
- if (job.processing) return 'running';
27
- if (job.error) return 'failed';
28
- return 'pending';
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 === 'object' && 'message' in error && typeof error.message === 'string') {
33
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
32
34
  return error.message;
33
35
  }
34
- return 'Unknown error';
36
+ return "Unknown error";
35
37
  }
36
38
  function isCancelled(error) {
37
- return error !== null && typeof error === 'object' && 'cancelled' in error && typeof error.cancelled === 'boolean' && error.cancelled;
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 'payload';
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?: string;
35
+ jobsCollection?: CollectionSlug;
36
36
  /**
37
37
  * Automatic job processing configuration.
38
38
  * Set to `false` to disable (for Vercel/serverless deployments).
@@ -47,7 +47,7 @@ export type PayloadJobsRunnerOptions = {
47
47
  attempts?: number;
48
48
  backoff?: {
49
49
  delay?: number;
50
- type: 'exponential' | 'fixed';
50
+ type: "exponential" | "fixed";
51
51
  };
52
52
  };
53
53
  };
@@ -57,9 +57,9 @@ export type PayloadJobsRunnerOptions = {
57
57
  export type PayloadJobsRunnerConfig = {
58
58
  taskName: string;
59
59
  queueName: string;
60
- jobsCollection: string;
60
+ jobsCollection: CollectionSlug;
61
61
  autoRun: false | Required<AutoRunConfig>;
62
- retries?: PayloadJobsRunnerOptions['retries'];
62
+ retries?: PayloadJobsRunnerOptions["retries"];
63
63
  };
64
64
  /**
65
65
  * Raw Payload job structure
@@ -72,6 +72,14 @@ export type PayloadJob = {
72
72
  error?: unknown;
73
73
  processing?: boolean | null;
74
74
  input?: {
75
+ /** Document reference (flat text, ID-agnostic). Current shape. */
76
+ collection_slug?: string;
77
+ collection_id?: string;
78
+ /**
79
+ * @deprecated Legacy relationship shape, read-only fallback for jobs queued
80
+ * before the ID-agnostic migration. Removed in next major.
81
+ * See docs/DEPRECATIONS.md#jobs-input-collection-field
82
+ */
75
83
  collection?: {
76
84
  relationTo: CollectionSlug;
77
85
  value: string | number;
@@ -1,8 +1,8 @@
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';
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
  *
@@ -18,7 +18,7 @@
18
18
  const now = new Date().toISOString();
19
19
  const task = {
20
20
  id: crypto.randomUUID(),
21
- status: 'running',
21
+ status: "running",
22
22
  input,
23
23
  createdAt: now,
24
24
  updatedAt: now,
@@ -34,12 +34,12 @@
34
34
  strategy: input.strategy,
35
35
  publishOnTranslation: input.publishOnTranslation
36
36
  });
37
- task.status = 'completed';
37
+ task.status = "completed";
38
38
  task.completedAt = new Date().toISOString();
39
39
  } catch (error) {
40
- task.status = 'failed';
40
+ task.status = "failed";
41
41
  task.error = {
42
- message: error instanceof Error ? error.message : 'Unknown error'
42
+ message: error instanceof Error ? error.message : "Unknown error"
43
43
  };
44
44
  }
45
45
  task.updatedAt = new Date().toISOString();
@@ -52,14 +52,15 @@
52
52
  // Sync runner executes tasks immediately, no pending tasks to run
53
53
  return {
54
54
  success: false,
55
- error: 'not_found'
55
+ error: "not_found"
56
56
  };
57
57
  }
58
58
  async findByCollection(collectionSlug, documentIds) {
59
59
  const results = [];
60
+ const wanted = documentIds ? new Set(documentIds.map(String)) : undefined;
60
61
  for (const [, task] of this.tasks){
61
62
  if (task.input.collectionSlug !== collectionSlug) continue;
62
- if (documentIds && !documentIds.includes(task.input.collectionId)) continue;
63
+ if (wanted && !wanted.has(task.input.collectionId)) continue;
63
64
  results.push(task);
64
65
  }
65
66
  return results;