@tunghtml/strapi-plugin-export-import-clsx 1.0.2 → 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 +3 -3
- package/server/controllers/export-controller.js +2 -2
- package/server/controllers/import-controller.js +14 -12
- package/server/services/export-service.js +251 -192
- package/server/services/import-service.js +355 -266
- 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,102 +1,106 @@
|
|
|
1
|
-
const XLSX = require(
|
|
2
|
-
const fs = require(
|
|
1
|
+
const XLSX = require("xlsx");
|
|
2
|
+
const fs = require("fs");
|
|
3
3
|
|
|
4
4
|
function toCamel(str) {
|
|
5
5
|
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
6
6
|
}
|
|
7
|
+
|
|
7
8
|
const SYSTEM_KEYS = [
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
"documentId",
|
|
10
|
+
"locale",
|
|
11
|
+
"createdAt",
|
|
12
|
+
"updatedAt",
|
|
13
|
+
"publishedAt",
|
|
14
|
+
"createdBy",
|
|
15
|
+
"updatedBy",
|
|
16
|
+
"localizations",
|
|
17
|
+
"status",
|
|
17
18
|
];
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const SHORTCUT_FIELDS = [
|
|
21
|
+
"email",
|
|
22
|
+
"businessEmail",
|
|
23
|
+
"name",
|
|
24
|
+
"title",
|
|
25
|
+
"tickerCode",
|
|
26
|
+
];
|
|
27
|
+
module.exports = ({ strapi }) => ({
|
|
28
|
+
async importData(file, targetContentType = null) {
|
|
29
|
+
let result;
|
|
30
|
+
try {
|
|
31
|
+
let importData;
|
|
32
|
+
// Check file extension
|
|
33
|
+
const fileName = file.name || file.originalFilename || "unknown.json";
|
|
34
|
+
const fileExtension = fileName.split(".").pop().toLowerCase();
|
|
35
|
+
const filePath = file.path || file.filepath;
|
|
36
|
+
if (!filePath) {
|
|
37
|
+
throw new Error("File path not found");
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
if (fileExtension === "json") {
|
|
41
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
42
|
+
importData = JSON.parse(fileContent);
|
|
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;
|
|
45
56
|
}
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
}
|
|
57
|
+
},
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
function transformExcelData(filePath) {
|
|
59
|
+
transformExcelData(filePath, targetContentType = null) {
|
|
52
60
|
const workbook = XLSX.readFile(filePath);
|
|
53
61
|
const importData = {};
|
|
54
62
|
|
|
55
63
|
const parseJsonIfNeeded = (value) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
if (typeof value !== "string") return value;
|
|
65
|
+
const trimmed = value.trim();
|
|
66
|
+
if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) return value;
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
try {
|
|
61
69
|
return JSON.parse(trimmed);
|
|
62
|
-
|
|
70
|
+
} catch {
|
|
63
71
|
return value; // keep as string if invalid JSON
|
|
64
|
-
|
|
72
|
+
}
|
|
65
73
|
};
|
|
66
74
|
|
|
67
75
|
const isComponentField = (key) => {
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
const parts = key.split("_");
|
|
77
|
+
return parts.length === 2; // exactly one underscore
|
|
70
78
|
};
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
const unflattenRow = (rows, targetContentType) => {
|
|
73
81
|
const result = [];
|
|
82
|
+
const attr = strapi.contentTypes[targetContentType].attributes;
|
|
74
83
|
for (const row of rows) {
|
|
75
84
|
const rowData = {};
|
|
76
85
|
|
|
77
86
|
for (const [key, value] of Object.entries(row)) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
existedComponents = getComponentFields(targetContentType);
|
|
94
|
-
for (const comp of existedComponents) {
|
|
95
|
-
if (!rowData[comp]) {
|
|
96
|
-
rowData[comp] = {};
|
|
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);
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
|
-
|
|
100
104
|
result.push(rowData);
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -104,255 +108,340 @@ function transformExcelData(filePath) {
|
|
|
104
108
|
};
|
|
105
109
|
|
|
106
110
|
const mapSheetNameToContentType = (sheetName) => {
|
|
107
|
-
|
|
108
|
-
|
|
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;
|
|
109
116
|
};
|
|
110
117
|
|
|
111
|
-
workbook.SheetNames.forEach(sheetName => {
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
workbook.SheetNames.forEach((sheetName) => {
|
|
119
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
120
|
+
const rows = XLSX.utils.sheet_to_json(worksheet);
|
|
114
121
|
|
|
115
|
-
|
|
122
|
+
if (!rows.length) return;
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
const contentTypeName = mapSheetNameToContentType(sheetName);
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
strapi.log.info(`Reading sheet "${sheetName}" -> ${rows.length} rows`);
|
|
127
|
+
strapi.log.info(`Mapped sheet to content-type: ${contentTypeName}`);
|
|
121
128
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
strapi.log.error(
|
|
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
|
+
);
|
|
126
136
|
return;
|
|
127
137
|
}
|
|
138
|
+
importData[contentTypeName] = unflattenRow(rows, contentTypeName);
|
|
139
|
+
} else {
|
|
140
|
+
strapi.log.error(`Unknown content-type: ${contentTypeName}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
128
143
|
});
|
|
129
144
|
|
|
130
|
-
strapi.log.info(
|
|
145
|
+
strapi.log.info("Final import data keys:", Object.keys(importData));
|
|
131
146
|
return importData;
|
|
132
|
-
}
|
|
147
|
+
},
|
|
133
148
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!schema) {
|
|
138
|
-
strapi.log.warn(`Content type ${contentType} not found`);
|
|
139
|
-
return [];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return Object.entries(schema.attributes)
|
|
143
|
-
.filter(([_, attr]) => attr.type === "relation")
|
|
144
|
-
.map(([fieldName, attr]) => ({
|
|
145
|
-
field: toCamel(fieldName),
|
|
146
|
-
target: attr.target, // e.g. "api::category.category"
|
|
147
|
-
relation: attr.relation,
|
|
148
|
-
}));
|
|
149
|
-
}
|
|
149
|
+
getRelationFields(contentType) {
|
|
150
|
+
const schema = strapi.contentTypes[contentType];
|
|
150
151
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
strapi.log.warn(`Content type ${contentType} not found`);
|
|
156
|
-
return [];
|
|
157
|
-
}
|
|
152
|
+
if (!schema) {
|
|
153
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
158
156
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
},
|
|
165
|
+
|
|
166
|
+
getComponentFields(contentType) {
|
|
167
|
+
const schema = strapi.contentTypes[contentType];
|
|
168
|
+
|
|
169
|
+
if (!schema) {
|
|
170
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
163
173
|
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
};
|
|
166
192
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return [];
|
|
170
|
-
}
|
|
193
|
+
const relationFields = this.getRelationFields(contentType);
|
|
194
|
+
if (relationFields.length === 0) return entry;
|
|
171
195
|
|
|
172
|
-
|
|
173
|
-
.filter(([_, attr]) => attr.type === "component")
|
|
174
|
-
.map(([fieldName, attr]) => toCamel(fieldName));
|
|
175
|
-
}
|
|
196
|
+
const updatedEntry = { ...entry };
|
|
176
197
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (relationFields.length === 0) {
|
|
180
|
-
return entry;
|
|
181
|
-
}
|
|
198
|
+
for (const rel of relationFields) {
|
|
199
|
+
const { field, target, relation } = rel;
|
|
182
200
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
201
|
+
let value = entry[field];
|
|
202
|
+
if (!value || value === "") {
|
|
203
|
+
if (relation === "manyToMany" || relation === "oneToMany") {
|
|
204
|
+
updatedEntry[field] = [];
|
|
205
|
+
} else {
|
|
206
|
+
updatedEntry[field] = null;
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
);
|
|
221
|
+
}
|
|
192
222
|
|
|
193
|
-
|
|
223
|
+
const values = Array.isArray(value) ? value : [value];
|
|
224
|
+
try {
|
|
194
225
|
const processed = [];
|
|
195
226
|
|
|
196
|
-
for (const
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
id: { $eq: item.id }
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
if (existing && hasChanges(existing, item, getRelationFieldsStrArr(target))) {
|
|
204
|
-
await strapi.documents(target).update({ documentId: existing.documentId, data: item });
|
|
205
|
-
isUpdated = true;
|
|
206
|
-
}
|
|
207
|
-
processed.push({ id: item.id });
|
|
208
|
-
} else {
|
|
209
|
-
const created = await strapi.documents(target).create({ data: item });
|
|
210
|
-
processed.push({ id: created.id });
|
|
211
|
-
}
|
|
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);
|
|
212
231
|
}
|
|
213
|
-
newEntry[field] = processed;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
232
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
filters: {
|
|
223
|
-
id: { $eq: relValue.id }
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
if (hasChanges(existing, relValue, getRelationFieldsStrArr(target))) {
|
|
227
|
-
await strapi.documents(target).update({ documentId: existing.documentId, data: relValue });
|
|
228
|
-
isUpdated = true;
|
|
229
|
-
}
|
|
230
|
-
newEntry[field] = { id: relValue.id };
|
|
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
|
+
);
|
|
231
238
|
}
|
|
232
|
-
} catch (err) {
|
|
233
|
-
throw new Error(`Field: ${field}, data: ${JSON.stringify(relValue)}, error: ${err.message}`);
|
|
234
239
|
}
|
|
235
|
-
}
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
}
|
|
241
|
+
return updatedEntry;
|
|
242
|
+
},
|
|
239
243
|
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
handleComponents(data, existing, contentType) {
|
|
245
|
+
// Get the component fields for this content type
|
|
246
|
+
const compFields = this.getComponentFields(contentType);
|
|
242
247
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
for (const field of compFields) {
|
|
249
|
+
const newValue = data[field];
|
|
250
|
+
const oldValue = existing?.[field];
|
|
246
251
|
|
|
247
|
-
|
|
248
|
-
if (relationFieldStrArr.includes(key)) continue;
|
|
252
|
+
if (!newValue || !oldValue) continue;
|
|
249
253
|
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
});
|
|
281
|
+
}
|
|
256
282
|
}
|
|
257
283
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
299
|
}
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
300
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (newVal.length !== oldVal.length) return true;
|
|
270
|
-
// Compare values shallowly
|
|
271
|
-
for (let i = 0; i < newVal.length; i++) {
|
|
272
|
-
if (typeof newVal[i] === "object" && typeof oldVal[i] === "object" && hasChanges(oldVal[i], newVal[i])) {
|
|
301
|
+
// Primitive comparison
|
|
302
|
+
if (newVal === null || typeof newVal !== "object") {
|
|
303
|
+
if (oldVal !== newVal) {
|
|
273
304
|
return true;
|
|
274
|
-
}
|
|
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)) {
|
|
275
335
|
return true;
|
|
276
336
|
}
|
|
337
|
+
continue;
|
|
277
338
|
}
|
|
278
|
-
continue;
|
|
279
339
|
}
|
|
280
340
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
341
|
+
return false;
|
|
342
|
+
},
|
|
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);
|
|
285
372
|
}
|
|
286
|
-
continue;
|
|
287
373
|
}
|
|
288
|
-
}
|
|
289
374
|
|
|
290
|
-
|
|
291
|
-
}
|
|
375
|
+
return results;
|
|
376
|
+
},
|
|
292
377
|
|
|
378
|
+
async importEntries(entries, contentType) {
|
|
379
|
+
const results = { created: 0, updated: 0, errors: [] };
|
|
293
380
|
|
|
294
|
-
async
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
};
|
|
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
|
+
});
|
|
300
386
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
results.errors.push(`Content type ${contentType} not found`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
if (!Array.isArray(entries)) {
|
|
308
|
-
results.errors.push(`Invalid data format for ${contentType}`);
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
387
|
+
for (let i = 0; i < entries.length; i++) {
|
|
388
|
+
const entry = entries[i];
|
|
389
|
+
let existing = null;
|
|
311
390
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
});
|
|
326
|
-
}
|
|
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
|
+
}
|
|
327
404
|
|
|
328
|
-
|
|
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
|
+
}
|
|
329
422
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await strapi
|
|
333
|
-
|
|
334
|
-
data,
|
|
335
|
-
|
|
336
|
-
isUpdated = true;
|
|
423
|
+
// Create
|
|
424
|
+
else {
|
|
425
|
+
await strapi
|
|
426
|
+
.documents(contentType)
|
|
427
|
+
.create({ data }, { transaction: trx });
|
|
428
|
+
results.created++;
|
|
337
429
|
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
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;
|
|
341
441
|
}
|
|
342
|
-
if (isUpdated) {
|
|
343
|
-
results.updated++;
|
|
344
|
-
} else if (isCreated) {
|
|
345
|
-
results.created++;
|
|
346
|
-
}
|
|
347
|
-
} catch (err) {
|
|
348
|
-
results.errors.push(`Failed ${existing ? 'updating' : 'creating'} on row ${i+2}: ${err.message}`);
|
|
349
442
|
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return results;
|
|
354
|
-
}
|
|
443
|
+
});
|
|
355
444
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
};
|
|
445
|
+
return results;
|
|
446
|
+
},
|
|
447
|
+
});
|