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.
- package/LICENSE +21 -0
- package/README.md +586 -0
- package/dist/components/BeforeDashboardClient.d.ts +14 -0
- package/dist/components/BeforeDashboardClient.js +225 -0
- package/dist/components/BeforeDashboardClient.js.map +1 -0
- package/dist/components/BeforeDashboardClient.module.css +175 -0
- package/dist/components/BeforeDashboardServer.d.ts +1 -0
- package/dist/components/BeforeDashboardServer.js +29 -0
- package/dist/components/BeforeDashboardServer.js.map +1 -0
- package/dist/components/ContentTypeSelect.d.ts +4 -0
- package/dist/components/ContentTypeSelect.js +147 -0
- package/dist/components/ContentTypeSelect.js.map +1 -0
- package/dist/components/FieldMappingConfiguration.d.ts +5 -0
- package/dist/components/FieldMappingConfiguration.js +361 -0
- package/dist/components/FieldMappingConfiguration.js.map +1 -0
- package/dist/components/FieldMappingConfiguration.module.css +75 -0
- package/dist/components/MigrationDashboardClient.d.ts +6 -0
- package/dist/components/MigrationDashboardClient.js +49 -0
- package/dist/components/MigrationDashboardClient.js.map +1 -0
- package/dist/components/MigrationDashboardClient.module.css +749 -0
- package/dist/components/SimpleFieldMapping.d.ts +5 -0
- package/dist/components/SimpleFieldMapping.js +437 -0
- package/dist/components/SimpleFieldMapping.js.map +1 -0
- package/dist/components/dashboard/JobActionButtons.d.ts +8 -0
- package/dist/components/dashboard/JobActionButtons.js +91 -0
- package/dist/components/dashboard/JobActionButtons.js.map +1 -0
- package/dist/components/dashboard/JobsTable.d.ts +6 -0
- package/dist/components/dashboard/JobsTable.js +86 -0
- package/dist/components/dashboard/JobsTable.js.map +1 -0
- package/dist/components/dashboard/LogViewer.d.ts +3 -0
- package/dist/components/dashboard/LogViewer.js +35 -0
- package/dist/components/dashboard/LogViewer.js.map +1 -0
- package/dist/components/dashboard/SiteConfigPanel.d.ts +12 -0
- package/dist/components/dashboard/SiteConfigPanel.js +205 -0
- package/dist/components/dashboard/SiteConfigPanel.js.map +1 -0
- package/dist/components/dashboard/StatsOverview.d.ts +5 -0
- package/dist/components/dashboard/StatsOverview.js +72 -0
- package/dist/components/dashboard/StatsOverview.js.map +1 -0
- package/dist/components/dashboard/index.d.ts +7 -0
- package/dist/components/dashboard/index.js +7 -0
- package/dist/components/dashboard/index.js.map +1 -0
- package/dist/components/dashboard/types.d.ts +46 -0
- package/dist/components/dashboard/types.js +2 -0
- package/dist/components/dashboard/types.js.map +1 -0
- package/dist/components/dashboard/useMigrationDashboard.d.ts +15 -0
- package/dist/components/dashboard/useMigrationDashboard.js +584 -0
- package/dist/components/dashboard/useMigrationDashboard.js.map +1 -0
- package/dist/exports/client.d.ts +4 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +443 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/content/blocks.d.ts +6 -0
- package/dist/utils/content/blocks.js +93 -0
- package/dist/utils/content/blocks.js.map +1 -0
- package/dist/utils/content/fieldMapping.d.ts +9 -0
- package/dist/utils/content/fieldMapping.js +218 -0
- package/dist/utils/content/fieldMapping.js.map +1 -0
- package/dist/utils/content/index.d.ts +4 -0
- package/dist/utils/content/index.js +4 -0
- package/dist/utils/content/index.js.map +1 -0
- package/dist/utils/content/transformer.d.ts +5 -0
- package/dist/utils/content/transformer.js +323 -0
- package/dist/utils/content/transformer.js.map +1 -0
- package/dist/utils/endpoints/handlers.d.ts +9 -0
- package/dist/utils/endpoints/handlers.js +201 -0
- package/dist/utils/endpoints/handlers.js.map +1 -0
- package/dist/utils/endpoints/index.d.ts +2 -0
- package/dist/utils/endpoints/index.js +2 -0
- package/dist/utils/endpoints/index.js.map +1 -0
- package/dist/utils/fields/analyzer.d.ts +7 -0
- package/dist/utils/fields/analyzer.js +502 -0
- package/dist/utils/fields/analyzer.js.map +1 -0
- package/dist/utils/fields/index.d.ts +2 -0
- package/dist/utils/fields/index.js +2 -0
- package/dist/utils/fields/index.js.map +1 -0
- package/dist/utils/helpers/auth.d.ts +9 -0
- package/dist/utils/helpers/auth.js +50 -0
- package/dist/utils/helpers/auth.js.map +1 -0
- package/dist/utils/helpers/cache.d.ts +11 -0
- package/dist/utils/helpers/cache.js +47 -0
- package/dist/utils/helpers/cache.js.map +1 -0
- package/dist/utils/helpers/concurrency.d.ts +2 -0
- package/dist/utils/helpers/concurrency.js +26 -0
- package/dist/utils/helpers/concurrency.js.map +1 -0
- package/dist/utils/helpers/index.d.ts +8 -0
- package/dist/utils/helpers/index.js +8 -0
- package/dist/utils/helpers/index.js.map +1 -0
- package/dist/utils/helpers/objectHelpers.d.ts +3 -0
- package/dist/utils/helpers/objectHelpers.js +22 -0
- package/dist/utils/helpers/objectHelpers.js.map +1 -0
- package/dist/utils/helpers/rateLimiter.d.ts +10 -0
- package/dist/utils/helpers/rateLimiter.js +29 -0
- package/dist/utils/helpers/rateLimiter.js.map +1 -0
- package/dist/utils/helpers/responses.d.ts +3 -0
- package/dist/utils/helpers/responses.js +23 -0
- package/dist/utils/helpers/responses.js.map +1 -0
- package/dist/utils/helpers/wpHelpers.d.ts +6 -0
- package/dist/utils/helpers/wpHelpers.js +29 -0
- package/dist/utils/helpers/wpHelpers.js.map +1 -0
- package/dist/utils/lexical/constants.d.ts +37 -0
- package/dist/utils/lexical/constants.js +58 -0
- package/dist/utils/lexical/constants.js.map +1 -0
- package/dist/utils/lexical/htmlParser.d.ts +20 -0
- package/dist/utils/lexical/htmlParser.js +253 -0
- package/dist/utils/lexical/htmlParser.js.map +1 -0
- package/dist/utils/lexical/htmlToLexicalConverter.d.ts +55 -0
- package/dist/utils/lexical/htmlToLexicalConverter.js +999 -0
- package/dist/utils/lexical/htmlToLexicalConverter.js.map +1 -0
- package/dist/utils/lexical/index.d.ts +5 -0
- package/dist/utils/lexical/index.js +4 -0
- package/dist/utils/lexical/index.js.map +1 -0
- package/dist/utils/lexical/nodeFactories.d.ts +21 -0
- package/dist/utils/lexical/nodeFactories.js +91 -0
- package/dist/utils/lexical/nodeFactories.js.map +1 -0
- package/dist/utils/lexical/preprocessor.d.ts +4 -0
- package/dist/utils/lexical/preprocessor.js +302 -0
- package/dist/utils/lexical/preprocessor.js.map +1 -0
- package/dist/utils/media/download.d.ts +7 -0
- package/dist/utils/media/download.js +85 -0
- package/dist/utils/media/download.js.map +1 -0
- package/dist/utils/media/extraction.d.ts +12 -0
- package/dist/utils/media/extraction.js +58 -0
- package/dist/utils/media/extraction.js.map +1 -0
- package/dist/utils/media/import.d.ts +7 -0
- package/dist/utils/media/import.js +146 -0
- package/dist/utils/media/import.js.map +1 -0
- package/dist/utils/media/index.d.ts +6 -0
- package/dist/utils/media/index.js +6 -0
- package/dist/utils/media/index.js.map +1 -0
- package/dist/utils/media/upload.d.ts +4 -0
- package/dist/utils/media/upload.js +46 -0
- package/dist/utils/media/upload.js.map +1 -0
- package/dist/utils/media/validation.d.ts +8 -0
- package/dist/utils/media/validation.js +60 -0
- package/dist/utils/media/validation.js.map +1 -0
- package/dist/utils/migration/index.d.ts +3 -0
- package/dist/utils/migration/index.js +3 -0
- package/dist/utils/migration/index.js.map +1 -0
- package/dist/utils/migration/jobCrud.d.ts +4 -0
- package/dist/utils/migration/jobCrud.js +380 -0
- package/dist/utils/migration/jobCrud.js.map +1 -0
- package/dist/utils/migration/orchestrator.d.ts +5 -0
- package/dist/utils/migration/orchestrator.js +756 -0
- package/dist/utils/migration/orchestrator.js.map +1 -0
- package/dist/utils/types.d.ts +201 -0
- package/dist/utils/types.js +14 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/utils/wordpress/client.d.ts +61 -0
- package/dist/utils/wordpress/client.js +365 -0
- package/dist/utils/wordpress/client.js.map +1 -0
- package/dist/utils/wordpress/index.d.ts +2 -0
- package/dist/utils/wordpress/index.js +2 -0
- package/dist/utils/wordpress/index.js.map +1 -0
- package/dist/utils/wordpressApi.d.ts +11 -0
- package/dist/utils/wordpressApi.js +25 -0
- package/dist/utils/wordpressApi.js.map +1 -0
- 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
|