payload-wordpress-migrator 0.0.22

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 (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +586 -0
  3. package/dist/components/BeforeDashboardClient.d.ts +14 -0
  4. package/dist/components/BeforeDashboardClient.js +225 -0
  5. package/dist/components/BeforeDashboardClient.js.map +1 -0
  6. package/dist/components/BeforeDashboardClient.module.css +175 -0
  7. package/dist/components/BeforeDashboardServer.d.ts +1 -0
  8. package/dist/components/BeforeDashboardServer.js +29 -0
  9. package/dist/components/BeforeDashboardServer.js.map +1 -0
  10. package/dist/components/ContentTypeSelect.d.ts +4 -0
  11. package/dist/components/ContentTypeSelect.js +147 -0
  12. package/dist/components/ContentTypeSelect.js.map +1 -0
  13. package/dist/components/FieldMappingConfiguration.d.ts +5 -0
  14. package/dist/components/FieldMappingConfiguration.js +361 -0
  15. package/dist/components/FieldMappingConfiguration.js.map +1 -0
  16. package/dist/components/FieldMappingConfiguration.module.css +75 -0
  17. package/dist/components/MigrationDashboardClient.d.ts +6 -0
  18. package/dist/components/MigrationDashboardClient.js +49 -0
  19. package/dist/components/MigrationDashboardClient.js.map +1 -0
  20. package/dist/components/MigrationDashboardClient.module.css +749 -0
  21. package/dist/components/SimpleFieldMapping.d.ts +5 -0
  22. package/dist/components/SimpleFieldMapping.js +437 -0
  23. package/dist/components/SimpleFieldMapping.js.map +1 -0
  24. package/dist/components/dashboard/JobActionButtons.d.ts +8 -0
  25. package/dist/components/dashboard/JobActionButtons.js +91 -0
  26. package/dist/components/dashboard/JobActionButtons.js.map +1 -0
  27. package/dist/components/dashboard/JobsTable.d.ts +6 -0
  28. package/dist/components/dashboard/JobsTable.js +86 -0
  29. package/dist/components/dashboard/JobsTable.js.map +1 -0
  30. package/dist/components/dashboard/LogViewer.d.ts +3 -0
  31. package/dist/components/dashboard/LogViewer.js +35 -0
  32. package/dist/components/dashboard/LogViewer.js.map +1 -0
  33. package/dist/components/dashboard/SiteConfigPanel.d.ts +12 -0
  34. package/dist/components/dashboard/SiteConfigPanel.js +205 -0
  35. package/dist/components/dashboard/SiteConfigPanel.js.map +1 -0
  36. package/dist/components/dashboard/StatsOverview.d.ts +5 -0
  37. package/dist/components/dashboard/StatsOverview.js +72 -0
  38. package/dist/components/dashboard/StatsOverview.js.map +1 -0
  39. package/dist/components/dashboard/index.d.ts +7 -0
  40. package/dist/components/dashboard/index.js +7 -0
  41. package/dist/components/dashboard/index.js.map +1 -0
  42. package/dist/components/dashboard/types.d.ts +46 -0
  43. package/dist/components/dashboard/types.js +2 -0
  44. package/dist/components/dashboard/types.js.map +1 -0
  45. package/dist/components/dashboard/useMigrationDashboard.d.ts +15 -0
  46. package/dist/components/dashboard/useMigrationDashboard.js +584 -0
  47. package/dist/components/dashboard/useMigrationDashboard.js.map +1 -0
  48. package/dist/exports/client.d.ts +4 -0
  49. package/dist/exports/client.js +5 -0
  50. package/dist/exports/client.js.map +1 -0
  51. package/dist/exports/rsc.d.ts +1 -0
  52. package/dist/exports/rsc.js +2 -0
  53. package/dist/exports/rsc.js.map +1 -0
  54. package/dist/index.d.ts +101 -0
  55. package/dist/index.js +443 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/utils/content/blocks.d.ts +6 -0
  58. package/dist/utils/content/blocks.js +93 -0
  59. package/dist/utils/content/blocks.js.map +1 -0
  60. package/dist/utils/content/fieldMapping.d.ts +9 -0
  61. package/dist/utils/content/fieldMapping.js +218 -0
  62. package/dist/utils/content/fieldMapping.js.map +1 -0
  63. package/dist/utils/content/index.d.ts +4 -0
  64. package/dist/utils/content/index.js +4 -0
  65. package/dist/utils/content/index.js.map +1 -0
  66. package/dist/utils/content/transformer.d.ts +5 -0
  67. package/dist/utils/content/transformer.js +323 -0
  68. package/dist/utils/content/transformer.js.map +1 -0
  69. package/dist/utils/endpoints/handlers.d.ts +9 -0
  70. package/dist/utils/endpoints/handlers.js +201 -0
  71. package/dist/utils/endpoints/handlers.js.map +1 -0
  72. package/dist/utils/endpoints/index.d.ts +2 -0
  73. package/dist/utils/endpoints/index.js +2 -0
  74. package/dist/utils/endpoints/index.js.map +1 -0
  75. package/dist/utils/fields/analyzer.d.ts +7 -0
  76. package/dist/utils/fields/analyzer.js +502 -0
  77. package/dist/utils/fields/analyzer.js.map +1 -0
  78. package/dist/utils/fields/index.d.ts +2 -0
  79. package/dist/utils/fields/index.js +2 -0
  80. package/dist/utils/fields/index.js.map +1 -0
  81. package/dist/utils/helpers/auth.d.ts +9 -0
  82. package/dist/utils/helpers/auth.js +50 -0
  83. package/dist/utils/helpers/auth.js.map +1 -0
  84. package/dist/utils/helpers/cache.d.ts +11 -0
  85. package/dist/utils/helpers/cache.js +47 -0
  86. package/dist/utils/helpers/cache.js.map +1 -0
  87. package/dist/utils/helpers/concurrency.d.ts +2 -0
  88. package/dist/utils/helpers/concurrency.js +26 -0
  89. package/dist/utils/helpers/concurrency.js.map +1 -0
  90. package/dist/utils/helpers/index.d.ts +8 -0
  91. package/dist/utils/helpers/index.js +8 -0
  92. package/dist/utils/helpers/index.js.map +1 -0
  93. package/dist/utils/helpers/objectHelpers.d.ts +3 -0
  94. package/dist/utils/helpers/objectHelpers.js +22 -0
  95. package/dist/utils/helpers/objectHelpers.js.map +1 -0
  96. package/dist/utils/helpers/rateLimiter.d.ts +10 -0
  97. package/dist/utils/helpers/rateLimiter.js +29 -0
  98. package/dist/utils/helpers/rateLimiter.js.map +1 -0
  99. package/dist/utils/helpers/responses.d.ts +3 -0
  100. package/dist/utils/helpers/responses.js +23 -0
  101. package/dist/utils/helpers/responses.js.map +1 -0
  102. package/dist/utils/helpers/wpHelpers.d.ts +6 -0
  103. package/dist/utils/helpers/wpHelpers.js +29 -0
  104. package/dist/utils/helpers/wpHelpers.js.map +1 -0
  105. package/dist/utils/lexical/constants.d.ts +37 -0
  106. package/dist/utils/lexical/constants.js +58 -0
  107. package/dist/utils/lexical/constants.js.map +1 -0
  108. package/dist/utils/lexical/htmlParser.d.ts +20 -0
  109. package/dist/utils/lexical/htmlParser.js +253 -0
  110. package/dist/utils/lexical/htmlParser.js.map +1 -0
  111. package/dist/utils/lexical/htmlToLexicalConverter.d.ts +55 -0
  112. package/dist/utils/lexical/htmlToLexicalConverter.js +999 -0
  113. package/dist/utils/lexical/htmlToLexicalConverter.js.map +1 -0
  114. package/dist/utils/lexical/index.d.ts +5 -0
  115. package/dist/utils/lexical/index.js +4 -0
  116. package/dist/utils/lexical/index.js.map +1 -0
  117. package/dist/utils/lexical/nodeFactories.d.ts +21 -0
  118. package/dist/utils/lexical/nodeFactories.js +91 -0
  119. package/dist/utils/lexical/nodeFactories.js.map +1 -0
  120. package/dist/utils/lexical/preprocessor.d.ts +4 -0
  121. package/dist/utils/lexical/preprocessor.js +302 -0
  122. package/dist/utils/lexical/preprocessor.js.map +1 -0
  123. package/dist/utils/media/download.d.ts +7 -0
  124. package/dist/utils/media/download.js +85 -0
  125. package/dist/utils/media/download.js.map +1 -0
  126. package/dist/utils/media/extraction.d.ts +12 -0
  127. package/dist/utils/media/extraction.js +58 -0
  128. package/dist/utils/media/extraction.js.map +1 -0
  129. package/dist/utils/media/import.d.ts +7 -0
  130. package/dist/utils/media/import.js +146 -0
  131. package/dist/utils/media/import.js.map +1 -0
  132. package/dist/utils/media/index.d.ts +6 -0
  133. package/dist/utils/media/index.js +6 -0
  134. package/dist/utils/media/index.js.map +1 -0
  135. package/dist/utils/media/upload.d.ts +4 -0
  136. package/dist/utils/media/upload.js +46 -0
  137. package/dist/utils/media/upload.js.map +1 -0
  138. package/dist/utils/media/validation.d.ts +8 -0
  139. package/dist/utils/media/validation.js +60 -0
  140. package/dist/utils/media/validation.js.map +1 -0
  141. package/dist/utils/migration/index.d.ts +3 -0
  142. package/dist/utils/migration/index.js +3 -0
  143. package/dist/utils/migration/index.js.map +1 -0
  144. package/dist/utils/migration/jobCrud.d.ts +4 -0
  145. package/dist/utils/migration/jobCrud.js +380 -0
  146. package/dist/utils/migration/jobCrud.js.map +1 -0
  147. package/dist/utils/migration/orchestrator.d.ts +5 -0
  148. package/dist/utils/migration/orchestrator.js +756 -0
  149. package/dist/utils/migration/orchestrator.js.map +1 -0
  150. package/dist/utils/types.d.ts +201 -0
  151. package/dist/utils/types.js +14 -0
  152. package/dist/utils/types.js.map +1 -0
  153. package/dist/utils/wordpress/client.d.ts +61 -0
  154. package/dist/utils/wordpress/client.js +365 -0
  155. package/dist/utils/wordpress/client.js.map +1 -0
  156. package/dist/utils/wordpress/index.d.ts +2 -0
  157. package/dist/utils/wordpress/index.js +2 -0
  158. package/dist/utils/wordpress/index.js.map +1 -0
  159. package/dist/utils/wordpressApi.d.ts +11 -0
  160. package/dist/utils/wordpressApi.js +25 -0
  161. package/dist/utils/wordpressApi.js.map +1 -0
  162. package/package.json +155 -0
@@ -0,0 +1,756 @@
1
+ import { createSafeLexicalRoot, cleanEmptyTextNodes } from '../lexical/nodeFactories.js';
2
+ import { checkMigrationFields } from '../media/validation.js';
3
+ import { uploadMediaToPayload } from '../media/upload.js';
4
+ import { MIGRATION_COLLECTION, isMediaUploadResult } from '../types.js';
5
+ import { WordPressClient } from '../wordpress/client.js';
6
+ import { invalidateMigrationCache } from '../helpers/cache.js';
7
+ import { getContentTypeEndpoint } from '../helpers/wpHelpers.js';
8
+ import { createConcurrencyLimiter } from '../helpers/concurrency.js';
9
+ import { transformWordPressContent } from '../content/transformer.js';
10
+ import { applyFieldMapping } from '../content/fieldMapping.js';
11
+
12
+ const logBuffer = [];
13
+ const addJobLog = (level, message)=>{
14
+ logBuffer.push({
15
+ level,
16
+ message,
17
+ timestamp: new Date()
18
+ });
19
+ };
20
+ const flushJobLogs = async (payload, jobId)=>{
21
+ if (logBuffer.length === 0) return;
22
+ try {
23
+ const currentJob = await payload.findByID({
24
+ id: jobId,
25
+ collection: MIGRATION_COLLECTION
26
+ });
27
+ await payload.update({
28
+ id: jobId,
29
+ collection: MIGRATION_COLLECTION,
30
+ data: {
31
+ logs: [
32
+ ...currentJob?.logs || [],
33
+ ...logBuffer
34
+ ]
35
+ }
36
+ });
37
+ logBuffer.length = 0;
38
+ } catch (error) {
39
+ payload.logger.error(`Failed to flush ${logBuffer.length} logs for job ${jobId}: ${error}`);
40
+ }
41
+ };
42
+ const processItem = async (item, job, payload, pluginOptions, client, action)=>{
43
+ try {
44
+ const hasMigrationFields = await checkMigrationFields(payload, job.targetCollection);
45
+ // Dedup check
46
+ if (hasMigrationFields) {
47
+ try {
48
+ const existingItem = await payload.find({
49
+ collection: job.targetCollection,
50
+ limit: 1,
51
+ where: {
52
+ 'migratedFromWordPress.wpPostId': {
53
+ equals: item.id
54
+ }
55
+ }
56
+ });
57
+ if (existingItem.docs.length > 0) {
58
+ const existingDoc = existingItem.docs[0];
59
+ // Update mode: fill only empty fields on existing items, preserving any manual
60
+ // edits the user has made post-migration. Only meta (title, description, image),
61
+ // categories, and tags are selectively updated — content is never overwritten.
62
+ if (action === 'update') {
63
+ const payloadData = await transformWordPressContent(item, job.contentType, job.configuration, payload, pluginOptions, {
64
+ apiUrl: client.wpApiUrl,
65
+ headers: client.wpHeaders
66
+ });
67
+ const mappedData = await applyFieldMapping(payloadData, item, job.configuration?.fieldMapping, payload);
68
+ const updateData = {};
69
+ let hasUpdates = false;
70
+ if (!existingDoc.meta?.title && mappedData.meta?.title) {
71
+ updateData.meta = updateData.meta || {};
72
+ updateData.meta.title = mappedData.meta.title;
73
+ hasUpdates = true;
74
+ }
75
+ if (!existingDoc.meta?.description && mappedData.meta?.description) {
76
+ updateData.meta = updateData.meta || {};
77
+ updateData.meta.description = mappedData.meta.description;
78
+ hasUpdates = true;
79
+ }
80
+ if (!existingDoc.meta?.image) {
81
+ const metaImageValue = existingDoc.heroImage || mappedData.meta?.image;
82
+ if (metaImageValue && typeof metaImageValue !== 'string') {
83
+ updateData.meta = updateData.meta || {};
84
+ updateData.meta.image = metaImageValue;
85
+ hasUpdates = true;
86
+ }
87
+ }
88
+ if (!existingDoc.categories?.length && mappedData.categories?.length) {
89
+ updateData.categories = mappedData.categories;
90
+ hasUpdates = true;
91
+ }
92
+ if (!existingDoc.tags?.length && mappedData.tags?.length) {
93
+ updateData.tags = mappedData.tags;
94
+ hasUpdates = true;
95
+ }
96
+ if (hasUpdates) {
97
+ try {
98
+ await payload.update({
99
+ id: existingDoc.id,
100
+ collection: job.targetCollection,
101
+ data: updateData
102
+ });
103
+ } catch (updateError) {
104
+ payload.logger.error(`Failed to update existing post ${item.id}: ${updateError}`);
105
+ return {
106
+ incrementProcessed: true,
107
+ status: 'failed',
108
+ wpId: item.id,
109
+ error: `Update failed: ${updateError}`,
110
+ failedRecord: {
111
+ error: `Update failed: ${updateError}`,
112
+ errorType: 'UpdateError',
113
+ stage: 'update',
114
+ timestamp: new Date(),
115
+ wpId: item.id
116
+ }
117
+ };
118
+ }
119
+ }
120
+ return {
121
+ incrementProcessed: true,
122
+ status: 'success',
123
+ wpId: item.id
124
+ };
125
+ } else {
126
+ // Already exists, skip
127
+ return {
128
+ incrementProcessed: true,
129
+ status: 'skipped',
130
+ wpId: item.id
131
+ };
132
+ }
133
+ }
134
+ } catch (queryError) {
135
+ payload.logger.warn(`Error checking for existing items in collection ${job.targetCollection}: ${queryError}`);
136
+ addJobLog('warning', `Error checking for existing items: ${queryError instanceof Error ? queryError.message : String(queryError)}`);
137
+ }
138
+ }
139
+ // Transform
140
+ const payloadData = await transformWordPressContent(item, job.contentType, job.configuration, payload, pluginOptions, {
141
+ apiUrl: client.wpApiUrl,
142
+ headers: client.wpHeaders
143
+ });
144
+ // Media upload path
145
+ if (isMediaUploadResult(payloadData)) {
146
+ if (job.dryRun) {
147
+ addJobLog('info', `[DRY RUN] Would upload media: "${payloadData.mediaMetadata?.title || `Item ${item.id}`}" (wpId: ${item.id})`);
148
+ return {
149
+ incrementProcessed: true,
150
+ status: 'success',
151
+ wpId: item.id
152
+ };
153
+ }
154
+ try {
155
+ const hasMigFields = await checkMigrationFields(payload, job.targetCollection);
156
+ await uploadMediaToPayload(payload, job.targetCollection, payloadData.fileData, payloadData.mediaMetadata, hasMigFields);
157
+ return {
158
+ incrementProcessed: true,
159
+ status: 'success',
160
+ wpId: item.id
161
+ };
162
+ } catch (uploadError) {
163
+ const errorMessage = uploadError instanceof Error ? uploadError.message : 'Unknown error';
164
+ payload.logger.error(`Failed to upload media file for item ${item.id}: ${uploadError}`);
165
+ return {
166
+ error: `Media upload failed: ${errorMessage}`,
167
+ failedRecord: {
168
+ error: `Media upload failed: ${errorMessage}`,
169
+ errorType: uploadError?.constructor?.name || 'Error',
170
+ stage: 'media_upload',
171
+ timestamp: new Date(),
172
+ wpId: item.id
173
+ },
174
+ incrementProcessed: true,
175
+ status: 'failed',
176
+ wpId: item.id
177
+ };
178
+ }
179
+ } else if (job.contentType === 'media' && payloadData.migrationNote) {
180
+ return {
181
+ incrementProcessed: true,
182
+ status: 'skipped_media_note',
183
+ wpId: item.id
184
+ };
185
+ }
186
+ // Regular content creation path
187
+ const hasMigFields = await checkMigrationFields(payload, job.targetCollection);
188
+ const mappedData = await applyFieldMapping(payloadData, item, job.configuration?.fieldMapping, payload);
189
+ const createData = {
190
+ ...mappedData
191
+ };
192
+ if (hasMigFields) {
193
+ createData.migratedFromWordPress = {
194
+ migrationDate: new Date(),
195
+ wpPostId: item.id,
196
+ wpPostType: job.contentType
197
+ };
198
+ }
199
+ try {
200
+ // Content validation and cleanup
201
+ if (createData.content !== undefined) {
202
+ if (typeof createData.content === 'string') {
203
+ if (createData.content.trim().length === 0) {
204
+ payload.logger.warn(`Pre-validation: Empty content string for item ${item.id}, creating safe Lexical structure`);
205
+ createData.content = createSafeLexicalRoot();
206
+ } else {
207
+ payload.logger.warn(`Pre-validation: Plain text content for item ${item.id}, converting to Lexical`);
208
+ createData.content = {
209
+ type: 'root',
210
+ children: [
211
+ {
212
+ type: 'paragraph',
213
+ children: [
214
+ {
215
+ type: 'text',
216
+ detail: 0,
217
+ format: 0,
218
+ mode: 'normal',
219
+ style: '',
220
+ text: createData.content,
221
+ version: 1
222
+ }
223
+ ],
224
+ direction: 'ltr',
225
+ format: '',
226
+ indent: 0,
227
+ version: 1
228
+ }
229
+ ],
230
+ direction: 'ltr',
231
+ format: '',
232
+ indent: 0,
233
+ version: 1
234
+ };
235
+ }
236
+ } else if (typeof createData.content === 'object') {
237
+ const contentStr = JSON.stringify(createData.content);
238
+ if (contentStr.includes('"text":""')) {
239
+ payload.logger.warn(`Pre-validation: Cleaning empty text nodes for item ${item.id}`);
240
+ if (createData.content.root) {
241
+ createData.content = cleanEmptyTextNodes(createData.content);
242
+ } else {
243
+ const wrapped = {
244
+ root: createData.content
245
+ };
246
+ const cleaned = cleanEmptyTextNodes(wrapped);
247
+ createData.content = cleaned.root || cleaned;
248
+ }
249
+ }
250
+ if (!createData.content.root || !createData.content.root.type || createData.content.root.type !== 'root' || !createData.content.root.children) {
251
+ payload.logger.warn(`Pre-validation: Invalid content structure for item ${item.id}, using safe fallback`);
252
+ createData.content = createSafeLexicalRoot();
253
+ } else {
254
+ let hasNonWhitespaceContent = false;
255
+ const cleanedChildren = createData.content.root.children.filter((child)=>{
256
+ if (child.type === 'paragraph' && child.children) {
257
+ const hasText = child.children.some((textNode)=>{
258
+ return textNode.type === 'text' && textNode.text && textNode.text.trim().length > 0;
259
+ });
260
+ if (hasText) {
261
+ hasNonWhitespaceContent = true;
262
+ return true;
263
+ } else {
264
+ return false;
265
+ }
266
+ } else {
267
+ hasNonWhitespaceContent = true;
268
+ return true;
269
+ }
270
+ });
271
+ createData.content.root.children = cleanedChildren;
272
+ if (!hasNonWhitespaceContent || cleanedChildren.length === 0) {
273
+ payload.logger.warn(`Pre-validation: Content only contains whitespace for item ${item.id}, adding placeholder`);
274
+ createData.content = createSafeLexicalRoot();
275
+ }
276
+ }
277
+ }
278
+ } else {
279
+ payload.logger.warn(`Pre-validation: No content field for item ${item.id}, creating safe default`);
280
+ createData.content = createSafeLexicalRoot();
281
+ }
282
+ if (createData.content && createData.content.root && createData.content.root.type === 'root' && createData.content.root.children) {
283
+ createData.content.root.children = createData.content.root.children.map((child)=>{
284
+ if (child.type === 'paragraph' && child.children) {
285
+ const validNodes = child.children.map((node)=>{
286
+ if (node.type === 'text') {
287
+ return node.text && node.text.trim().length > 0 ? node : null;
288
+ }
289
+ if (node.type === 'link') {
290
+ if (!node.url || node.url.trim().length === 0) {
291
+ payload.logger.warn(`Converting link with empty URL to text for item ${item.id}`);
292
+ return {
293
+ type: 'text',
294
+ detail: 0,
295
+ format: 0,
296
+ mode: 'normal',
297
+ style: '',
298
+ text: node.children && node.children[0] && node.children[0].text ? node.children[0].text : 'link',
299
+ version: 1
300
+ };
301
+ }
302
+ return {
303
+ ...node,
304
+ rel: node.rel || '',
305
+ target: node.target || '',
306
+ title: node.title || '',
307
+ url: node.url.trim()
308
+ };
309
+ }
310
+ return node;
311
+ }).filter(Boolean);
312
+ if (validNodes.length === 0) {
313
+ validNodes.push({
314
+ type: 'text',
315
+ detail: 0,
316
+ format: 0,
317
+ mode: 'normal',
318
+ style: '',
319
+ text: '(Empty paragraph)',
320
+ version: 1
321
+ });
322
+ }
323
+ return {
324
+ ...child,
325
+ children: validNodes
326
+ };
327
+ }
328
+ return child;
329
+ });
330
+ }
331
+ if (job.dryRun) {
332
+ const title = createData.title || createData.name || `Item ${item.id}`;
333
+ addJobLog('info', `[DRY RUN] Would create ${job.contentType}: "${title}" (wpId: ${item.id}, fields: ${Object.keys(createData).length})`);
334
+ return {
335
+ incrementProcessed: true,
336
+ status: 'success',
337
+ wpId: item.id
338
+ };
339
+ }
340
+ await payload.create({
341
+ collection: job.targetCollection,
342
+ data: createData
343
+ });
344
+ return {
345
+ incrementProcessed: true,
346
+ status: 'success',
347
+ wpId: item.id
348
+ };
349
+ } catch (createError) {
350
+ const errorMessage = createError instanceof Error ? createError.message : String(createError);
351
+ if (errorMessage.includes('Content') || errorMessage.includes('ValidationError')) {
352
+ payload.logger.error(`Content validation error for item ${item.id}: ${errorMessage} | title="${item.title?.rendered || 'N/A'}" contentLength=${item.content?.rendered?.length || 0}`);
353
+ }
354
+ throw createError;
355
+ }
356
+ } catch (itemError) {
357
+ let errorMessage = itemError instanceof Error ? itemError.message : String(itemError);
358
+ if (itemError instanceof Error && 'data' in itemError) {
359
+ const errorData = itemError.data;
360
+ payload.logger.error(`Detailed error data for item ${item.id}: ${JSON.stringify(errorData, null, 2)}`);
361
+ if (errorData && errorData.errors) {
362
+ payload.logger.error(`Field validation errors: ${JSON.stringify(errorData.errors, null, 2)}`);
363
+ if (Array.isArray(errorData.errors)) {
364
+ const fieldErrors = errorData.errors.map((e)=>`${e.field || e.path || 'unknown'}: ${e.message}`).join(', ');
365
+ errorMessage += ` [Field errors: ${fieldErrors}]`;
366
+ }
367
+ }
368
+ }
369
+ const itemTitle = item.title?.rendered || item.slug || `Item ${item.id}`;
370
+ const itemType = job.contentType || 'content';
371
+ const shortError = errorMessage.length > 100 ? errorMessage.substring(0, 100) + '...' : errorMessage;
372
+ addJobLog('error', `Failed to migrate ${itemType} "${itemTitle}" (ID: ${item.id}) - ${shortError}`);
373
+ payload.logger.error(`MIGRATION FAILURE: ${itemType} "${itemTitle}" (ID: ${item.id}) - ${shortError}`);
374
+ payload.logger.error(`Failed to migrate item ${item.id}: ${itemError}`);
375
+ return {
376
+ error: errorMessage,
377
+ failedRecord: {
378
+ error: errorMessage,
379
+ errorType: itemError?.constructor?.name || 'Error',
380
+ stage: 'create',
381
+ timestamp: new Date(),
382
+ wpId: item.id
383
+ },
384
+ incrementProcessed: true,
385
+ status: 'failed',
386
+ wpId: item.id
387
+ };
388
+ }
389
+ };
390
+ const processBatch = async (batch, job, payload, pluginOptions, client, counters, action)=>{
391
+ const concurrency = job.configuration?.concurrency || pluginOptions.migrationConcurrency || 1;
392
+ const limit = createConcurrencyLimiter(concurrency);
393
+ const batchResults = await Promise.allSettled(batch.map((item)=>limit(()=>processItem(item, job, payload, pluginOptions, client, action))));
394
+ for (const result of batchResults){
395
+ if (result.status === 'fulfilled') {
396
+ const r = result.value;
397
+ if (r.incrementProcessed) {
398
+ counters.processedItems++;
399
+ }
400
+ if (r.status === 'success' || r.status === 'skipped') {
401
+ counters.successfulItems++;
402
+ } else if (r.status === 'failed') {
403
+ counters.failedItems++;
404
+ if (r.failedRecord) {
405
+ counters.failedItemIds.push(r.failedRecord);
406
+ }
407
+ }
408
+ // skipped_media_note: only increments processedItems (already done above)
409
+ } else {
410
+ // Should not happen since processItem catches internally
411
+ counters.processedItems++;
412
+ counters.failedItems++;
413
+ }
414
+ }
415
+ };
416
+ // ---------------------------------------------------------------------------
417
+ // Main orchestrator
418
+ // ---------------------------------------------------------------------------
419
+ const processMigrationJob = async (jobId, job, payload, pluginOptions, siteConfig, action)=>{
420
+ try {
421
+ const client = new WordPressClient({
422
+ wpPassword: siteConfig.wpPassword,
423
+ wpSiteUrl: siteConfig.wpSiteUrl,
424
+ wpUsername: siteConfig.wpUsername
425
+ });
426
+ const batchSize = job.configuration?.batchSize || pluginOptions.migrationBatchSize || 10;
427
+ const batchDelay = job.configuration?.batchDelay ?? 200;
428
+ const requestDelay = job.configuration?.requestDelay || pluginOptions.wpRequestDelay || 0;
429
+ addJobLog('info', `Starting migration for ${job.contentType} from ${siteConfig.siteName || 'WordPress Site'}`);
430
+ invalidateMigrationCache();
431
+ const endpoint = getContentTypeEndpoint(client.wpApiUrl, job.contentType);
432
+ const rawIncludeIds = job.includeIds || job.configuration?.includeIds;
433
+ let includeIds;
434
+ if (rawIncludeIds) {
435
+ if (typeof rawIncludeIds === 'string') {
436
+ includeIds = rawIncludeIds.split(',').map((id)=>parseInt(id.trim(), 10)).filter((id)=>!isNaN(id) && id > 0);
437
+ } else if (Array.isArray(rawIncludeIds)) {
438
+ includeIds = rawIncludeIds.map((id)=>typeof id === 'number' ? id : parseInt(String(id), 10)).filter((id)=>!isNaN(id) && id > 0);
439
+ }
440
+ }
441
+ addJobLog('info', `Connecting to WordPress API at ${client.wpApiUrl}...`);
442
+ await flushJobLogs(payload, jobId);
443
+ // Retry and resume need the full item array for filtering; fresh start / restart / update can stream
444
+ const needsFullFetch = action === 'retry' || action === 'resume';
445
+ const counters = {
446
+ failedItemIds: [],
447
+ failedItems: 0,
448
+ processedItems: 0,
449
+ successfulItems: 0
450
+ };
451
+ let totalItems = 0;
452
+ if (needsFullFetch) {
453
+ // -----------------------------------------------------------------------
454
+ // RETRY / RESUME path — fetch all items, then filter and batch-process
455
+ // -----------------------------------------------------------------------
456
+ const wpContent = await client.fetchAllPages(endpoint, async (page, totalPages, itemCount)=>{
457
+ addJobLog('info', `Fetched page ${page} of ${totalPages} (${itemCount} items)`);
458
+ }, job.contentType, includeIds);
459
+ await flushJobLogs(payload, jobId);
460
+ totalItems = wpContent.length;
461
+ await payload.update({
462
+ id: jobId,
463
+ collection: MIGRATION_COLLECTION,
464
+ data: {
465
+ progress: {
466
+ failedItems: 0,
467
+ processedItems: 0,
468
+ successfulItems: 0,
469
+ totalItems
470
+ }
471
+ }
472
+ });
473
+ invalidateMigrationCache();
474
+ const currentJob = await payload.findByID({
475
+ id: jobId,
476
+ collection: MIGRATION_COLLECTION
477
+ });
478
+ counters.processedItems = currentJob.progress?.processedItems || 0;
479
+ counters.successfulItems = currentJob.progress?.successfulItems || 0;
480
+ counters.failedItems = currentJob.progress?.failedItems || 0;
481
+ counters.failedItemIds = currentJob.progress?.failedItemIds || [];
482
+ let itemsToProcess = wpContent;
483
+ if (action === 'retry' && currentJob.progress?.failedItemIds && currentJob.progress.failedItemIds.length > 0) {
484
+ const failedIds = currentJob.progress.failedItemIds.map((item)=>item.wpId);
485
+ itemsToProcess = wpContent.filter((item)=>failedIds.includes(item.id));
486
+ addJobLog('info', `Retrying ${itemsToProcess.length} previously failed items`);
487
+ // Resume deduplication: build a Set of WordPress post IDs that already exist in the
488
+ // target collection by paginating through all migrated items. We then filter the
489
+ // fetched WordPress content to skip items that were already successfully imported,
490
+ // so the job continues from where it left off without creating duplicates.
491
+ } else if (action === 'resume' && counters.processedItems > 0 && counters.processedItems < totalItems) {
492
+ const processedIds = new Set();
493
+ try {
494
+ const hasMigrationFields = await checkMigrationFields(payload, currentJob.targetCollection);
495
+ if (hasMigrationFields) {
496
+ let page = 1;
497
+ let hasMore = true;
498
+ while(hasMore){
499
+ const existingItems = await payload.find({
500
+ collection: currentJob.targetCollection,
501
+ limit: 100,
502
+ page,
503
+ where: {
504
+ 'migratedFromWordPress.wpPostType': {
505
+ equals: currentJob.contentType
506
+ }
507
+ }
508
+ });
509
+ existingItems.docs.forEach((doc)=>{
510
+ if (doc.migratedFromWordPress?.wpPostId) {
511
+ processedIds.add(doc.migratedFromWordPress.wpPostId);
512
+ }
513
+ });
514
+ hasMore = page < existingItems.totalPages;
515
+ page++;
516
+ }
517
+ }
518
+ } catch (error) {
519
+ payload.logger.warn('Could not determine processed items for resume, continuing from beginning');
520
+ addJobLog('warning', 'Could not determine processed items for resume, continuing from beginning');
521
+ }
522
+ itemsToProcess = wpContent.filter((item)=>!processedIds.has(item.id));
523
+ addJobLog('info', `Resuming migration from ${counters.processedItems}/${totalItems} items (${itemsToProcess.length} remaining)`);
524
+ await flushJobLogs(payload, jobId);
525
+ }
526
+ // Batch-process the filtered items
527
+ for(let i = 0; i < itemsToProcess.length; i += batchSize){
528
+ // Pause check
529
+ const jobState = await payload.findByID({
530
+ id: jobId,
531
+ collection: MIGRATION_COLLECTION
532
+ });
533
+ if (jobState.status === 'paused') {
534
+ addJobLog('info', `Migration paused at ${counters.processedItems}/${totalItems} items processed`);
535
+ await flushJobLogs(payload, jobId);
536
+ await payload.update({
537
+ id: jobId,
538
+ collection: MIGRATION_COLLECTION,
539
+ data: {
540
+ progress: {
541
+ failedItemIds: counters.failedItemIds,
542
+ failedItems: counters.failedItems,
543
+ processedItems: counters.processedItems,
544
+ successfulItems: counters.successfulItems,
545
+ totalItems
546
+ }
547
+ }
548
+ });
549
+ invalidateMigrationCache();
550
+ return;
551
+ }
552
+ const batch = itemsToProcess.slice(i, i + batchSize);
553
+ await processBatch(batch, job, payload, pluginOptions, client, counters, action);
554
+ // Progress update after each batch
555
+ await payload.update({
556
+ id: jobId,
557
+ collection: MIGRATION_COLLECTION,
558
+ data: {
559
+ progress: {
560
+ failedItemIds: counters.failedItemIds,
561
+ failedItems: counters.failedItems,
562
+ processedItems: counters.processedItems,
563
+ successfulItems: counters.successfulItems,
564
+ totalItems
565
+ }
566
+ }
567
+ });
568
+ await flushJobLogs(payload, jobId);
569
+ if (batchDelay > 0) {
570
+ await new Promise((resolve)=>setTimeout(resolve, batchDelay));
571
+ }
572
+ }
573
+ } else {
574
+ // -----------------------------------------------------------------------
575
+ // FRESH START / RESTART / UPDATE path — stream pages, process in batches
576
+ // -----------------------------------------------------------------------
577
+ if (action === 'restart') {
578
+ counters.processedItems = 0;
579
+ counters.successfulItems = 0;
580
+ counters.failedItems = 0;
581
+ }
582
+ let totalItemsSet = false;
583
+ let batchBuffer = [];
584
+ for await (const { items, totalItems: headerTotal } of client.fetchPages(endpoint, {
585
+ contentType: job.contentType,
586
+ includeIds,
587
+ onProgress: (page, totalPages, count)=>{
588
+ addJobLog('info', `Fetched page ${page} of ${totalPages} (${count} items)`);
589
+ },
590
+ requestDelay
591
+ })){
592
+ // Set totalItems from X-WP-Total header on first page
593
+ if (!totalItemsSet) {
594
+ totalItems = headerTotal || items.length;
595
+ totalItemsSet = true;
596
+ await payload.update({
597
+ id: jobId,
598
+ collection: MIGRATION_COLLECTION,
599
+ data: {
600
+ progress: {
601
+ failedItems: 0,
602
+ processedItems: 0,
603
+ successfulItems: 0,
604
+ totalItems
605
+ }
606
+ }
607
+ });
608
+ invalidateMigrationCache();
609
+ await flushJobLogs(payload, jobId);
610
+ }
611
+ // Accumulate items into batch buffer
612
+ for (const item of items){
613
+ batchBuffer.push(item);
614
+ if (batchBuffer.length >= batchSize) {
615
+ // Pause check before each batch
616
+ const jobState = await payload.findByID({
617
+ id: jobId,
618
+ collection: MIGRATION_COLLECTION
619
+ });
620
+ if (jobState.status === 'paused') {
621
+ addJobLog('info', `Migration paused at ${counters.processedItems}/${totalItems} items processed`);
622
+ await flushJobLogs(payload, jobId);
623
+ await payload.update({
624
+ id: jobId,
625
+ collection: MIGRATION_COLLECTION,
626
+ data: {
627
+ progress: {
628
+ failedItemIds: counters.failedItemIds,
629
+ failedItems: counters.failedItems,
630
+ processedItems: counters.processedItems,
631
+ successfulItems: counters.successfulItems,
632
+ totalItems
633
+ }
634
+ }
635
+ });
636
+ invalidateMigrationCache();
637
+ return;
638
+ }
639
+ await processBatch(batchBuffer, job, payload, pluginOptions, client, counters, action);
640
+ // Progress update after each batch
641
+ await payload.update({
642
+ id: jobId,
643
+ collection: MIGRATION_COLLECTION,
644
+ data: {
645
+ progress: {
646
+ failedItemIds: counters.failedItemIds,
647
+ failedItems: counters.failedItems,
648
+ processedItems: counters.processedItems,
649
+ successfulItems: counters.successfulItems,
650
+ totalItems
651
+ }
652
+ }
653
+ });
654
+ await flushJobLogs(payload, jobId);
655
+ if (batchDelay > 0) {
656
+ await new Promise((resolve)=>setTimeout(resolve, batchDelay));
657
+ }
658
+ batchBuffer = [];
659
+ }
660
+ }
661
+ }
662
+ // Process remaining items in buffer
663
+ if (batchBuffer.length > 0) {
664
+ const jobState = await payload.findByID({
665
+ id: jobId,
666
+ collection: MIGRATION_COLLECTION
667
+ });
668
+ if (jobState.status === 'paused') {
669
+ addJobLog('info', `Migration paused at ${counters.processedItems}/${totalItems} items processed`);
670
+ await flushJobLogs(payload, jobId);
671
+ await payload.update({
672
+ id: jobId,
673
+ collection: MIGRATION_COLLECTION,
674
+ data: {
675
+ progress: {
676
+ failedItemIds: counters.failedItemIds,
677
+ failedItems: counters.failedItems,
678
+ processedItems: counters.processedItems,
679
+ successfulItems: counters.successfulItems,
680
+ totalItems
681
+ }
682
+ }
683
+ });
684
+ invalidateMigrationCache();
685
+ return;
686
+ }
687
+ await processBatch(batchBuffer, job, payload, pluginOptions, client, counters, action);
688
+ await payload.update({
689
+ id: jobId,
690
+ collection: MIGRATION_COLLECTION,
691
+ data: {
692
+ progress: {
693
+ failedItemIds: counters.failedItemIds,
694
+ failedItems: counters.failedItems,
695
+ processedItems: counters.processedItems,
696
+ successfulItems: counters.successfulItems,
697
+ totalItems
698
+ }
699
+ }
700
+ });
701
+ await flushJobLogs(payload, jobId);
702
+ }
703
+ // If no items were fetched at all, ensure totalItems is set
704
+ if (!totalItemsSet) {
705
+ totalItems = 0;
706
+ await payload.update({
707
+ id: jobId,
708
+ collection: MIGRATION_COLLECTION,
709
+ data: {
710
+ progress: {
711
+ failedItems: 0,
712
+ processedItems: 0,
713
+ successfulItems: 0,
714
+ totalItems: 0
715
+ }
716
+ }
717
+ });
718
+ invalidateMigrationCache();
719
+ }
720
+ }
721
+ addJobLog('info', `Migration completed. ${counters.successfulItems} items successfully migrated, ${counters.failedItems} failed.`);
722
+ await flushJobLogs(payload, jobId);
723
+ await payload.update({
724
+ id: jobId,
725
+ collection: MIGRATION_COLLECTION,
726
+ data: {
727
+ endTime: new Date(),
728
+ progress: {
729
+ failedItemIds: counters.failedItemIds,
730
+ failedItems: counters.failedItems,
731
+ processedItems: counters.processedItems,
732
+ successfulItems: counters.successfulItems,
733
+ totalItems
734
+ },
735
+ status: 'completed'
736
+ }
737
+ });
738
+ invalidateMigrationCache();
739
+ } catch (error) {
740
+ addJobLog('error', `Migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
741
+ await flushJobLogs(payload, jobId);
742
+ await payload.update({
743
+ id: jobId,
744
+ collection: MIGRATION_COLLECTION,
745
+ data: {
746
+ endTime: new Date(),
747
+ status: 'failed'
748
+ }
749
+ });
750
+ invalidateMigrationCache();
751
+ throw error;
752
+ }
753
+ };
754
+
755
+ export { processMigrationJob };
756
+ //# sourceMappingURL=orchestrator.js.map