@tunghtml/strapi-plugin-export-import-clsx 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/components/ExportButton/index.jsx +71 -0
- package/admin/src/components/ExportImportButtons/index.jsx +374 -0
- package/admin/src/components/ImportButton/index.jsx +81 -0
- package/package.json +4 -4
- package/server/controllers/export-controller.js +2 -2
- package/server/controllers/import-controller.js +13 -11
- package/server/services/export-service.js +304 -213
- package/server/services/import-service.js +409 -268
- package/strapi-admin.js +20 -17
- package/admin/src/components/ExportButton/index.js +0 -48
- package/admin/src/components/ExportImportButtons/index.js +0 -245
- package/admin/src/components/ImportButton/index.js +0 -54
|
@@ -1,306 +1,447 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const XLSX = require("xlsx");
|
|
2
|
+
const fs = require("fs");
|
|
3
3
|
|
|
4
|
+
function toCamel(str) {
|
|
5
|
+
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SYSTEM_KEYS = [
|
|
9
|
+
"documentId",
|
|
10
|
+
"locale",
|
|
11
|
+
"createdAt",
|
|
12
|
+
"updatedAt",
|
|
13
|
+
"publishedAt",
|
|
14
|
+
"createdBy",
|
|
15
|
+
"updatedBy",
|
|
16
|
+
"localizations",
|
|
17
|
+
"status",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SHORTCUT_FIELDS = [
|
|
21
|
+
"email",
|
|
22
|
+
"businessEmail",
|
|
23
|
+
"name",
|
|
24
|
+
"title",
|
|
25
|
+
"tickerCode",
|
|
26
|
+
];
|
|
4
27
|
module.exports = ({ strapi }) => ({
|
|
5
28
|
async importData(file, targetContentType = null) {
|
|
29
|
+
let result;
|
|
6
30
|
try {
|
|
7
31
|
let importData;
|
|
8
|
-
|
|
9
32
|
// Check file extension
|
|
10
|
-
const fileName = file.name || file.originalFilename ||
|
|
11
|
-
const fileExtension = fileName.split(
|
|
12
|
-
|
|
33
|
+
const fileName = file.name || file.originalFilename || "unknown.json";
|
|
34
|
+
const fileExtension = fileName.split(".").pop().toLowerCase();
|
|
13
35
|
const filePath = file.path || file.filepath;
|
|
14
|
-
|
|
15
36
|
if (!filePath) {
|
|
16
|
-
throw new Error(
|
|
37
|
+
throw new Error("File path not found");
|
|
17
38
|
}
|
|
18
39
|
|
|
19
|
-
if (fileExtension ===
|
|
20
|
-
const fileContent = fs.readFileSync(filePath,
|
|
40
|
+
if (fileExtension === "json") {
|
|
41
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
21
42
|
importData = JSON.parse(fileContent);
|
|
22
|
-
strapi.log.info(
|
|
23
|
-
} else if (fileExtension ===
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} else {
|
|
82
|
-
result[key] = value;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Debug info removed for cleaner logs
|
|
88
|
-
|
|
89
|
-
return result;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
importData[contentTypeName] = unflattened;
|
|
93
|
-
strapi.log.info(`Mapped sheet ${sheetName} to ${contentTypeName}`);
|
|
43
|
+
strapi.log.info("Parsed JSON data:", Object.keys(importData));
|
|
44
|
+
} else if (fileExtension === "xlsx" || fileExtension === "xls") {
|
|
45
|
+
importData = this.transformExcelData(filePath, targetContentType);
|
|
46
|
+
}
|
|
47
|
+
result = await this.bulkInsertData(importData);
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Clean up uploaded file on error
|
|
51
|
+
const filePath = file && (file.path || file.filepath);
|
|
52
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
53
|
+
fs.unlinkSync(filePath);
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
transformExcelData(filePath, targetContentType = null) {
|
|
60
|
+
const workbook = XLSX.readFile(filePath);
|
|
61
|
+
const importData = {};
|
|
62
|
+
|
|
63
|
+
const parseJsonIfNeeded = (value) => {
|
|
64
|
+
if (typeof value !== "string") return value;
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) return value;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(trimmed);
|
|
70
|
+
} catch {
|
|
71
|
+
return value; // keep as string if invalid JSON
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isComponentField = (key) => {
|
|
76
|
+
const parts = key.split("_");
|
|
77
|
+
return parts.length === 2; // exactly one underscore
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const unflattenRow = (rows, targetContentType) => {
|
|
81
|
+
const result = [];
|
|
82
|
+
const attr = strapi.contentTypes[targetContentType].attributes;
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const rowData = {};
|
|
85
|
+
|
|
86
|
+
for (const [key, value] of Object.entries(row)) {
|
|
87
|
+
if (value === null || value === undefined || value === "") {
|
|
88
|
+
rowData[key] = null;
|
|
89
|
+
} else if (
|
|
90
|
+
attr[key] &&
|
|
91
|
+
attr[key].customField &&
|
|
92
|
+
attr[key].type === "json" &&
|
|
93
|
+
attr[key].default === "[]"
|
|
94
|
+
) {
|
|
95
|
+
rowData[key] = parseJsonIfNeeded(value).split("|");
|
|
96
|
+
} else if (isComponentField(key)) {
|
|
97
|
+
const [comp, field] = key.split("_");
|
|
98
|
+
if (!rowData[comp]) rowData[comp] = {};
|
|
99
|
+
rowData[comp][field] = parseJsonIfNeeded(value);
|
|
100
|
+
} else {
|
|
101
|
+
rowData[key] = parseJsonIfNeeded(value);
|
|
94
102
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
}
|
|
104
|
+
result.push(rowData);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const mapSheetNameToContentType = (sheetName) => {
|
|
111
|
+
// If targetContentType is provided, use it instead of guessing from sheet name
|
|
112
|
+
if (targetContentType) {
|
|
113
|
+
return targetContentType;
|
|
114
|
+
}
|
|
115
|
+
return "api::" + sheetName + "." + sheetName;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
workbook.SheetNames.forEach((sheetName) => {
|
|
119
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
120
|
+
const rows = XLSX.utils.sheet_to_json(worksheet);
|
|
121
|
+
|
|
122
|
+
if (!rows.length) return;
|
|
123
|
+
|
|
124
|
+
const contentTypeName = mapSheetNameToContentType(sheetName);
|
|
125
|
+
|
|
126
|
+
strapi.log.info(`Reading sheet "${sheetName}" -> ${rows.length} rows`);
|
|
127
|
+
strapi.log.info(`Mapped sheet to content-type: ${contentTypeName}`);
|
|
128
|
+
|
|
129
|
+
if (contentTypeName.startsWith("api::")) {
|
|
130
|
+
// Validate that the content type exists
|
|
131
|
+
if (!strapi.contentTypes[contentTypeName]) {
|
|
132
|
+
strapi.log.error(
|
|
133
|
+
`Content type ${contentTypeName} not found. Available types:`,
|
|
134
|
+
Object.keys(strapi.contentTypes)
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
importData[contentTypeName] = unflattenRow(rows, contentTypeName);
|
|
98
139
|
} else {
|
|
99
|
-
|
|
140
|
+
strapi.log.error(`Unknown content-type: ${contentTypeName}`);
|
|
141
|
+
return;
|
|
100
142
|
}
|
|
143
|
+
});
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
errors: [],
|
|
106
|
-
};
|
|
145
|
+
strapi.log.info("Final import data keys:", Object.keys(importData));
|
|
146
|
+
return importData;
|
|
147
|
+
},
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
getRelationFields(contentType) {
|
|
150
|
+
const schema = strapi.contentTypes[contentType];
|
|
110
151
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
152
|
+
if (!schema) {
|
|
153
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
117
156
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
157
|
+
return Object.entries(schema.attributes)
|
|
158
|
+
.filter(([_, attr]) => attr.type === "relation")
|
|
159
|
+
.map(([fieldName, attr]) => ({
|
|
160
|
+
field: toCamel(fieldName),
|
|
161
|
+
target: attr.target, // e.g. "api::category.category"
|
|
162
|
+
relation: attr.relation,
|
|
163
|
+
}));
|
|
164
|
+
},
|
|
122
165
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
results.errors.push(`Content type ${contentType} not found`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
166
|
+
getComponentFields(contentType) {
|
|
167
|
+
const schema = strapi.contentTypes[contentType];
|
|
128
168
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
169
|
+
if (!schema) {
|
|
170
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
133
173
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Only try to find existing entry if documentId is provided and valid
|
|
168
|
-
if (documentId && documentId !== '' && documentId !== 'null' && documentId !== 'undefined') {
|
|
169
|
-
try {
|
|
170
|
-
if (strapi.documents) {
|
|
171
|
-
existingEntry = await strapi.documents(contentType).findOne({
|
|
172
|
-
documentId: documentId,
|
|
173
|
-
});
|
|
174
|
-
if (existingEntry) {
|
|
175
|
-
updateMode = true;
|
|
176
|
-
strapi.log.info(`Found existing entry for update: ${documentId}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
// Entry not found, will create new one
|
|
181
|
-
strapi.log.info(`DocumentId ${documentId} not found, will create new entry`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// If no documentId provided or not found, this will be a new entry
|
|
186
|
-
if (!existingEntry) {
|
|
187
|
-
strapi.log.info(`Creating new entry for: ${cleanEntry.shortName || cleanEntry.name || cleanEntry.title || 'Unknown'}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Skip entries without basic required fields
|
|
191
|
-
if (!cleanEntry.shortName && !cleanEntry.name && !cleanEntry.title) {
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Ensure required components exist for corporate
|
|
196
|
-
if (contentType === 'api::corporate.corporate') {
|
|
197
|
-
if (!cleanEntry.corporateInfo) cleanEntry.corporateInfo = {};
|
|
198
|
-
if (!cleanEntry.meetingRequirements) cleanEntry.meetingRequirements = {};
|
|
199
|
-
if (!cleanEntry.internalInfo) cleanEntry.internalInfo = {};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (existingEntry) {
|
|
203
|
-
// Check if there are actual changes before updating
|
|
204
|
-
const hasChanges = this.hasDataChanges(existingEntry, cleanEntry);
|
|
205
|
-
const statusChanged = (status === 'published') !== !!existingEntry.publishedAt;
|
|
206
|
-
|
|
207
|
-
if (hasChanges || statusChanged) {
|
|
208
|
-
try {
|
|
209
|
-
if (strapi.documents) {
|
|
210
|
-
const updateData = {
|
|
211
|
-
documentId: existingEntry.documentId,
|
|
212
|
-
data: cleanEntry,
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Handle status change
|
|
216
|
-
if (statusChanged) {
|
|
217
|
-
updateData.status = status === 'published' ? 'published' : 'draft';
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
await strapi.documents(contentType).update(updateData);
|
|
221
|
-
results.updated++;
|
|
222
|
-
} else {
|
|
223
|
-
await strapi.entityService.update(contentType, existingEntry.id, {
|
|
224
|
-
data: cleanEntry,
|
|
225
|
-
});
|
|
226
|
-
results.updated++;
|
|
227
|
-
}
|
|
228
|
-
} catch (updateError) {
|
|
229
|
-
results.errors.push(`Failed to update entry: ${updateError.message}`);
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
// No changes, skip update
|
|
233
|
-
strapi.log.info(`No changes detected for entry: ${cleanEntry.shortName || 'Unknown'}`);
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
// Create new entry
|
|
237
|
-
try {
|
|
238
|
-
if (strapi.documents) {
|
|
239
|
-
await strapi.documents(contentType).create({
|
|
240
|
-
data: cleanEntry,
|
|
241
|
-
status: status === 'published' ? 'published' : 'draft',
|
|
242
|
-
});
|
|
243
|
-
results.created++;
|
|
244
|
-
} else {
|
|
245
|
-
await strapi.entityService.create(contentType, {
|
|
246
|
-
data: cleanEntry,
|
|
247
|
-
});
|
|
248
|
-
results.created++;
|
|
249
|
-
}
|
|
250
|
-
} catch (createError) {
|
|
251
|
-
results.errors.push(`Failed to create entry: ${createError.message}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} catch (error) {
|
|
255
|
-
results.errors.push(`Failed to import entry in ${contentType}: ${error.message}`);
|
|
256
|
-
strapi.log.error('Import entry error:', error);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} catch (error) {
|
|
260
|
-
results.errors.push(`Failed to process ${contentType}: ${error.message}`);
|
|
261
|
-
strapi.log.error('Import process error:', error);
|
|
174
|
+
return Object.entries(schema.attributes)
|
|
175
|
+
.filter(([_, attr]) => attr.type === "component")
|
|
176
|
+
.map(([fieldName, attr]) => toCamel(fieldName));
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async handleRelations(entry, contentType) {
|
|
180
|
+
const resolveRelationValue = async (field, value, target) => {
|
|
181
|
+
const targetAttr = strapi.contentTypes[target].attributes;
|
|
182
|
+
for (const field of SHORTCUT_FIELDS) {
|
|
183
|
+
if (!targetAttr[field]) continue;
|
|
184
|
+
const existing = await strapi.documents(target).findFirst({
|
|
185
|
+
filters: { [field]: { $eq: value } },
|
|
186
|
+
});
|
|
187
|
+
if (existing) return { id: existing.id };
|
|
188
|
+
throw new Error(`Data with ${field} ${value} not found`);
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const relationFields = this.getRelationFields(contentType);
|
|
194
|
+
if (relationFields.length === 0) return entry;
|
|
195
|
+
|
|
196
|
+
const updatedEntry = { ...entry };
|
|
197
|
+
|
|
198
|
+
for (const rel of relationFields) {
|
|
199
|
+
const { field, target, relation } = rel;
|
|
200
|
+
|
|
201
|
+
let value = entry[field];
|
|
202
|
+
if (!value || value === "") {
|
|
203
|
+
if (relation === "manyToMany" || relation === "oneToMany") {
|
|
204
|
+
updatedEntry[field] = [];
|
|
205
|
+
} else {
|
|
206
|
+
updatedEntry[field] = null;
|
|
262
207
|
}
|
|
208
|
+
continue;
|
|
263
209
|
}
|
|
264
210
|
|
|
265
|
-
//
|
|
266
|
-
if (
|
|
267
|
-
|
|
211
|
+
// Convert CSV to array
|
|
212
|
+
if (
|
|
213
|
+
typeof value === "string" &&
|
|
214
|
+
(relation === "manyToMany" || relation === "oneToMany")
|
|
215
|
+
) {
|
|
216
|
+
value = value.split("|");
|
|
217
|
+
} else if (typeof value === "string" && value.includes("|")) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Invalid value for field ${field}: ${value}, ${field} is not an array`
|
|
220
|
+
);
|
|
268
221
|
}
|
|
269
222
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
223
|
+
const values = Array.isArray(value) ? value : [value];
|
|
224
|
+
try {
|
|
225
|
+
const processed = [];
|
|
226
|
+
|
|
227
|
+
for (const v of values) {
|
|
228
|
+
if (!v || v === "") continue;
|
|
229
|
+
const resolved = await resolveRelationValue(field, v, target);
|
|
230
|
+
if (resolved) processed.push(resolved);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
updatedEntry[field] = Array.isArray(value) ? processed : processed[0];
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Failed processing field ${field} with value ${JSON.stringify(value)}: ${err.message}`
|
|
237
|
+
);
|
|
276
238
|
}
|
|
277
|
-
throw error;
|
|
278
239
|
}
|
|
240
|
+
|
|
241
|
+
return updatedEntry;
|
|
279
242
|
},
|
|
280
243
|
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
for (const field of
|
|
286
|
-
|
|
287
|
-
|
|
244
|
+
handleComponents(data, existing, contentType) {
|
|
245
|
+
// Get the component fields for this content type
|
|
246
|
+
const compFields = this.getComponentFields(contentType);
|
|
247
|
+
|
|
248
|
+
for (const field of compFields) {
|
|
249
|
+
const newValue = data[field];
|
|
250
|
+
const oldValue = existing?.[field];
|
|
251
|
+
|
|
252
|
+
if (!newValue || !oldValue) continue;
|
|
253
|
+
|
|
254
|
+
//single component
|
|
255
|
+
if (!Array.isArray(newValue)) {
|
|
256
|
+
if (oldValue?.id) {
|
|
257
|
+
data[field].id = oldValue.id;
|
|
258
|
+
}
|
|
259
|
+
for (const key of Object.keys(data[field])) {
|
|
260
|
+
if (Array.isArray(oldValue[key])) {
|
|
261
|
+
data[field][key] = data[field][key].split("|");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//multiple components
|
|
268
|
+
if (Array.isArray(newValue) && Array.isArray(oldValue)) {
|
|
269
|
+
data[field] = newValue.map((block, i) => {
|
|
270
|
+
const oldBlock = oldValue[i];
|
|
271
|
+
if (oldBlock?.id) {
|
|
272
|
+
return { id: oldBlock.id, ...block };
|
|
273
|
+
}
|
|
274
|
+
for (const key of Object.keys(block)) {
|
|
275
|
+
if (Array.isArray(oldBlock[key])) {
|
|
276
|
+
block[key] = block[key].split("|");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return block;
|
|
280
|
+
});
|
|
288
281
|
}
|
|
289
282
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
283
|
+
|
|
284
|
+
return data;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
hasChanges(existing, incoming) {
|
|
288
|
+
if (!incoming || typeof incoming !== "object") return false;
|
|
289
|
+
if (!existing || typeof existing !== "object") return true;
|
|
290
|
+
for (const key of Object.keys(incoming)) {
|
|
291
|
+
// Skip system keys
|
|
292
|
+
if (SYSTEM_KEYS.includes(key)) continue;
|
|
293
|
+
const newVal = incoming[key];
|
|
294
|
+
const oldVal = existing[key];
|
|
295
|
+
|
|
296
|
+
// If incoming defines a field but existing doesn't → change
|
|
297
|
+
if (oldVal === undefined || newVal === undefined) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Primitive comparison
|
|
302
|
+
if (newVal === null || typeof newVal !== "object") {
|
|
303
|
+
if (oldVal !== newVal) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ARRAY comparison
|
|
310
|
+
if (Array.isArray(newVal)) {
|
|
311
|
+
if (!Array.isArray(oldVal)) return true;
|
|
312
|
+
if (newVal.length !== oldVal.length) return true;
|
|
313
|
+
// Compare values shallowly
|
|
314
|
+
for (let i = 0; i < newVal.length; i++) {
|
|
315
|
+
if (
|
|
316
|
+
typeof newVal[i] === "object" &&
|
|
317
|
+
typeof oldVal[i] === "object" &&
|
|
318
|
+
this.hasChanges(oldVal[i], newVal[i])
|
|
319
|
+
) {
|
|
320
|
+
return true;
|
|
321
|
+
} else if (
|
|
322
|
+
typeof newVal[i] !== "object" &&
|
|
323
|
+
typeof oldVal[i] !== "object" &&
|
|
324
|
+
newVal[i] !== oldVal[i]
|
|
325
|
+
) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// OBJECT comparison (recursive, but ONLY fields in incoming object)
|
|
333
|
+
if (typeof newVal === "object" && typeof oldVal === "object") {
|
|
334
|
+
if (this.hasChanges(oldVal, newVal)) {
|
|
299
335
|
return true;
|
|
300
336
|
}
|
|
337
|
+
continue;
|
|
301
338
|
}
|
|
302
339
|
}
|
|
303
|
-
|
|
340
|
+
|
|
304
341
|
return false;
|
|
305
342
|
},
|
|
306
|
-
|
|
343
|
+
|
|
344
|
+
async bulkInsertData(importData) {
|
|
345
|
+
const results = {
|
|
346
|
+
created: 0,
|
|
347
|
+
updated: 0,
|
|
348
|
+
errors: [],
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
for (const [contentType, entries] of Object.entries(importData)) {
|
|
352
|
+
// Validate entries
|
|
353
|
+
if (!strapi.contentTypes[contentType]) {
|
|
354
|
+
results.errors.push(`Content type ${contentType} not found`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!Array.isArray(entries)) {
|
|
358
|
+
results.errors.push(`Invalid data format for ${contentType}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const { created, updated, errors } = await this.importEntries(
|
|
364
|
+
entries,
|
|
365
|
+
contentType
|
|
366
|
+
);
|
|
367
|
+
results.created += created;
|
|
368
|
+
results.updated += updated;
|
|
369
|
+
results.errors = results.errors.concat(errors);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
results.errors.push(err.message);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return results;
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
async importEntries(entries, contentType) {
|
|
379
|
+
const results = { created: 0, updated: 0, errors: [] };
|
|
380
|
+
|
|
381
|
+
await strapi.db.transaction(async ({ trx, rollback, onRollback }) => {
|
|
382
|
+
onRollback(() => {
|
|
383
|
+
strapi.log.error("Transaction rolled back due to an error!");
|
|
384
|
+
strapi.log.error(results.errors);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < entries.length; i++) {
|
|
388
|
+
const entry = entries[i];
|
|
389
|
+
let existing = null;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
let { id, ...data } = entry;
|
|
393
|
+
|
|
394
|
+
// Check if document exists
|
|
395
|
+
if (id && id !== "null" && id !== "undefined") {
|
|
396
|
+
existing = await strapi.documents(contentType).findFirst(
|
|
397
|
+
{
|
|
398
|
+
filters: { id },
|
|
399
|
+
populate: "*",
|
|
400
|
+
},
|
|
401
|
+
{ transaction: trx }
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Handle relations & components
|
|
406
|
+
data = await this.handleRelations(data, contentType, trx);
|
|
407
|
+
data = await this.handleComponents(data, existing, contentType);
|
|
408
|
+
|
|
409
|
+
// Update
|
|
410
|
+
if (existing) {
|
|
411
|
+
if (this.hasChanges(existing, data)) {
|
|
412
|
+
await strapi.documents(contentType).update(
|
|
413
|
+
{
|
|
414
|
+
documentId: existing.documentId,
|
|
415
|
+
data,
|
|
416
|
+
},
|
|
417
|
+
{ transaction: trx }
|
|
418
|
+
);
|
|
419
|
+
results.updated++;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Create
|
|
424
|
+
else {
|
|
425
|
+
await strapi
|
|
426
|
+
.documents(contentType)
|
|
427
|
+
.create({ data }, { transaction: trx });
|
|
428
|
+
results.created++;
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
results.errors.push(
|
|
432
|
+
`Failed ${existing ? "updating" : "creating"} on row ${
|
|
433
|
+
i + 2
|
|
434
|
+
}: ${err.message}`
|
|
435
|
+
);
|
|
436
|
+
results.created = 0;
|
|
437
|
+
results.updated = 0;
|
|
438
|
+
|
|
439
|
+
// IMPORTANT: force rollback
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return results;
|
|
446
|
+
},
|
|
447
|
+
});
|