@nkhang1902/strapi-plugin-export-import-clsx 1.0.5 → 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/ExportImportButtons/index.jsx +281 -0
- package/package.json +3 -3
- package/server/controllers/export-controller.js +56 -43
- package/server/controllers/import-controller.js +14 -12
- package/server/services/export-service.js +160 -116
- package/server/services/import-service.js +352 -344
- package/strapi-admin.js +10 -69
- package/admin/src/components/BulkActions/index.js +0 -70
- package/admin/src/components/ExportButton/index.js +0 -48
- package/admin/src/components/ExportImportButtons/index.js +0 -279
- package/admin/src/components/ImportButton/index.js +0 -54
- package/admin/src/translations/en.json +0 -14
|
@@ -1,55 +1,59 @@
|
|
|
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
8
|
const SHORTCUT_FIELDS = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
"email",
|
|
10
|
+
"businessEmail",
|
|
11
|
+
"name",
|
|
12
|
+
"title",
|
|
13
|
+
"tickerCode",
|
|
14
|
+
];
|
|
15
|
+
module.exports = ({ strapi }) => ({
|
|
16
|
+
async importData(file, targetContentType = null) {
|
|
17
|
+
let result;
|
|
18
|
+
try {
|
|
19
|
+
let importData;
|
|
20
|
+
// Check file extension
|
|
21
|
+
const fileName = file.name || file.originalFilename || "unknown.json";
|
|
22
|
+
const fileExtension = fileName.split(".").pop().toLowerCase();
|
|
23
|
+
const filePath = file.path || file.filepath;
|
|
24
|
+
if (!filePath) {
|
|
25
|
+
throw new Error("File path not found");
|
|
26
|
+
}
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
if (fileExtension === "json") {
|
|
29
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
30
|
+
importData = JSON.parse(fileContent);
|
|
31
|
+
strapi.log.info("Parsed JSON data:", Object.keys(importData));
|
|
32
|
+
} else if (fileExtension === "xlsx" || fileExtension === "xls") {
|
|
33
|
+
importData = this.transformExcelData(filePath, targetContentType);
|
|
34
|
+
}
|
|
35
|
+
result = await this.bulkInsertData(importData);
|
|
36
|
+
return result;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Clean up uploaded file on error
|
|
39
|
+
const filePath = file && (file.path || file.filepath);
|
|
40
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
41
|
+
fs.unlinkSync(filePath);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
45
|
+
},
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
transformExcelData(filePath, targetContentType = null) {
|
|
44
48
|
const workbook = XLSX.readFile(filePath);
|
|
45
49
|
const importData = {};
|
|
46
50
|
|
|
47
51
|
const isComponentField = (key) => {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const parts = key.split("_");
|
|
53
|
+
return parts.length === 2; // exactly one underscore
|
|
50
54
|
};
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
const unflattenRow = (rows, targetContentType) => {
|
|
53
57
|
const result = [];
|
|
54
58
|
const attr = strapi.contentTypes[targetContentType].attributes;
|
|
55
59
|
for (const row of rows) {
|
|
@@ -75,369 +79,373 @@ function transformExcelData(filePath) {
|
|
|
75
79
|
};
|
|
76
80
|
|
|
77
81
|
const mapSheetNameToContentType = (sheetName) => {
|
|
78
|
-
|
|
82
|
+
// If targetContentType is provided, use it instead of guessing from sheet name
|
|
83
|
+
if (targetContentType) {
|
|
84
|
+
return targetContentType;
|
|
85
|
+
}
|
|
86
|
+
return "api::" + sheetName + "." + sheetName;
|
|
79
87
|
};
|
|
80
88
|
|
|
81
|
-
workbook.SheetNames.forEach(sheetName => {
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
workbook.SheetNames.forEach((sheetName) => {
|
|
90
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
91
|
+
const rows = XLSX.utils.sheet_to_json(worksheet);
|
|
84
92
|
|
|
85
|
-
|
|
93
|
+
if (!rows.length) return;
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
const contentTypeName = mapSheetNameToContentType(sheetName);
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
strapi.log.info(`Reading sheet "${sheetName}" -> ${rows.length} rows`);
|
|
98
|
+
strapi.log.info(`Mapped sheet to content-type: ${contentTypeName}`);
|
|
91
99
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
strapi.log.error(
|
|
100
|
+
if (contentTypeName.startsWith("api::")) {
|
|
101
|
+
// Validate that the content type exists
|
|
102
|
+
if (!strapi.contentTypes[contentTypeName]) {
|
|
103
|
+
strapi.log.error(
|
|
104
|
+
`Content type ${contentTypeName} not found. Available types:`,
|
|
105
|
+
Object.keys(strapi.contentTypes)
|
|
106
|
+
);
|
|
96
107
|
return;
|
|
97
108
|
}
|
|
109
|
+
importData[contentTypeName] = unflattenRow(rows, contentTypeName);
|
|
110
|
+
} else {
|
|
111
|
+
strapi.log.error(`Unknown content-type: ${contentTypeName}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
98
114
|
});
|
|
99
115
|
|
|
100
|
-
strapi.log.info(
|
|
116
|
+
strapi.log.info("Final import data keys:", Object.keys(importData));
|
|
101
117
|
return importData;
|
|
102
|
-
}
|
|
118
|
+
},
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!schema) {
|
|
108
|
-
strapi.log.warn(`Content type ${contentType} not found`);
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return Object.entries(schema.attributes)
|
|
113
|
-
.filter(([_, attr]) => attr.type === "relation")
|
|
114
|
-
.map(([fieldName, attr]) => ({
|
|
115
|
-
field: toCamel(fieldName),
|
|
116
|
-
target: attr.target, // e.g. "api::category.category"
|
|
117
|
-
relation: attr.relation,
|
|
118
|
-
}));
|
|
119
|
-
}
|
|
120
|
+
getRelationFields(contentType) {
|
|
121
|
+
const schema = strapi.contentTypes[contentType];
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
if (!schema) {
|
|
124
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
return Object.entries(schema.attributes)
|
|
129
|
+
.filter(([_, attr]) => attr.type === "relation")
|
|
130
|
+
.map(([fieldName, attr]) => ({
|
|
131
|
+
field: toCamel(fieldName),
|
|
132
|
+
target: attr.target, // e.g. "api::category.category"
|
|
133
|
+
relation: attr.relation,
|
|
134
|
+
}));
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
getComponentFields(contentType) {
|
|
138
|
+
const schema = strapi.contentTypes[contentType];
|
|
139
|
+
|
|
140
|
+
if (!schema) {
|
|
141
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
128
144
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
145
|
+
return Object.entries(schema.attributes)
|
|
146
|
+
.filter(([_, attr]) => attr.type === "component")
|
|
147
|
+
.map(([fieldName, attr]) => toCamel(fieldName));
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async handleRelations(entry, contentType) {
|
|
151
|
+
const resolveRelationValue = async (field, value, target) => {
|
|
152
|
+
const targetAttr = strapi.contentTypes[target].attributes;
|
|
153
|
+
for (const field of SHORTCUT_FIELDS) {
|
|
154
|
+
if (!targetAttr[field]) continue;
|
|
155
|
+
const existing = await strapi.documents(target).findFirst({
|
|
156
|
+
filters: { [field]: { $eq: value } },
|
|
157
|
+
});
|
|
158
|
+
if (existing) return { id: existing.id };
|
|
159
|
+
throw new Error(`Data with ${field} ${value} not found`);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
133
163
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const targetAttr = strapi.contentTypes[target].attributes;
|
|
137
|
-
for (const field of SHORTCUT_FIELDS) {
|
|
138
|
-
if (!targetAttr[field]) continue;
|
|
139
|
-
const existing = await strapi.documents(target).findFirst({
|
|
140
|
-
filters: { [field]: { $eq: value } },
|
|
141
|
-
});
|
|
142
|
-
if (existing) return {id: existing.id};
|
|
143
|
-
throw new Error(`Data with ${field} ${value} not found`);
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
164
|
+
const relationFields = this.getRelationFields(contentType);
|
|
165
|
+
if (relationFields.length === 0) return entry;
|
|
147
166
|
|
|
148
|
-
|
|
149
|
-
if (relationFields.length === 0) return entry;
|
|
167
|
+
const updatedEntry = { ...entry };
|
|
150
168
|
|
|
151
|
-
|
|
169
|
+
for (const rel of relationFields) {
|
|
170
|
+
const { field, target, relation } = rel;
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
let value = entry[field];
|
|
173
|
+
if (!value || value === "") {
|
|
174
|
+
if (relation === "manyToMany" || relation === "oneToMany") {
|
|
175
|
+
updatedEntry[field] = [];
|
|
176
|
+
} else {
|
|
177
|
+
updatedEntry[field] = null;
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
155
181
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
182
|
+
// Convert CSV to array
|
|
183
|
+
if (
|
|
184
|
+
typeof value === "string" &&
|
|
185
|
+
(relation === "manyToMany" || relation === "oneToMany")
|
|
186
|
+
) {
|
|
187
|
+
value = value.split("|");
|
|
188
|
+
} else if (typeof value === "string" && value.includes("|")) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Invalid value for field ${field}: ${value}, ${field} is not an array`
|
|
191
|
+
);
|
|
162
192
|
}
|
|
163
|
-
continue;
|
|
164
|
-
};
|
|
165
193
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} else if (typeof value === "string" && value.includes("|")) {
|
|
170
|
-
throw new Error(`Invalid value for field ${field}: ${value}, ${field} is not an array`);
|
|
171
|
-
}
|
|
194
|
+
const values = Array.isArray(value) ? value : [value];
|
|
195
|
+
try {
|
|
196
|
+
const processed = [];
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
198
|
+
for (const v of values) {
|
|
199
|
+
if (!v || v === "") continue;
|
|
200
|
+
const resolved = await resolveRelationValue(field, v, target);
|
|
201
|
+
if (resolved) processed.push(resolved);
|
|
202
|
+
}
|
|
176
203
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
updatedEntry[field] = Array.isArray(value) ? processed : processed[0];
|
|
205
|
+
} catch (err) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Failed processing field ${field} with value ${JSON.stringify(value)}: ${err.message}`
|
|
208
|
+
);
|
|
181
209
|
}
|
|
182
|
-
|
|
183
|
-
updatedEntry[field] = Array.isArray(value) ? processed : processed[0];
|
|
184
|
-
} catch (err) {
|
|
185
|
-
throw new Error(
|
|
186
|
-
`Failed processing field ${field} with value ${JSON.stringify(value)}: ${err.message}`
|
|
187
|
-
);
|
|
188
210
|
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return updatedEntry;
|
|
192
|
-
}
|
|
193
211
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const compFields = getComponentFields(contentType);
|
|
212
|
+
return updatedEntry;
|
|
213
|
+
},
|
|
197
214
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
215
|
+
handleComponents(data, existing, contentType) {
|
|
216
|
+
// Get the component fields for this content type
|
|
217
|
+
const compFields = this.getComponentFields(contentType);
|
|
201
218
|
|
|
202
|
-
|
|
219
|
+
for (const field of compFields) {
|
|
220
|
+
const newValue = data[field];
|
|
221
|
+
const oldValue = existing?.[field];
|
|
203
222
|
|
|
204
|
-
|
|
205
|
-
if (!Array.isArray(newValue)) {
|
|
206
|
-
if (oldValue?.id) {
|
|
207
|
-
data[field].id = oldValue.id;
|
|
208
|
-
}
|
|
209
|
-
for (const key of Object.keys(data[field])) {
|
|
210
|
-
if (Array.isArray(oldValue[key])) {
|
|
211
|
-
data[field][key] = data[field][key].split("|");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
223
|
+
if (!newValue || !oldValue) continue;
|
|
216
224
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (oldBlock?.id) {
|
|
222
|
-
return { id: oldBlock.id, ...block };
|
|
225
|
+
//single component
|
|
226
|
+
if (!Array.isArray(newValue)) {
|
|
227
|
+
if (oldValue?.id) {
|
|
228
|
+
data[field].id = oldValue.id;
|
|
223
229
|
}
|
|
224
|
-
for (const key of Object.keys(
|
|
225
|
-
if (Array.isArray(
|
|
226
|
-
|
|
230
|
+
for (const key of Object.keys(data[field])) {
|
|
231
|
+
if (Array.isArray(oldValue[key])) {
|
|
232
|
+
data[field][key] = data[field][key].split("|");
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
|
-
return block;
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return data;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function sanitizeEntryBeforeWrite(data, uid, path = '', errors = []) {
|
|
238
|
-
const schema = strapi.contentTypes[uid];
|
|
239
|
-
const cleaned = {};
|
|
240
|
-
|
|
241
|
-
for (const [key, attr] of Object.entries(schema.attributes)) {
|
|
242
|
-
const value = data[key];
|
|
243
|
-
const fieldPath = path ? `${path}.${key}` : key;
|
|
244
|
-
|
|
245
|
-
if (value === undefined) continue;
|
|
246
|
-
|
|
247
|
-
if (attr.type === 'component') {
|
|
248
|
-
if (!value) {
|
|
249
|
-
cleaned[key] = attr.repeatable ? [] : null;
|
|
250
235
|
continue;
|
|
251
236
|
}
|
|
252
237
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return cleaned;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function sanitizeComponent(data, uid, path, errors) {
|
|
272
|
-
const schema = strapi.components[uid];
|
|
273
|
-
if (!schema) return data;
|
|
274
|
-
|
|
275
|
-
const cleaned = {};
|
|
276
|
-
|
|
277
|
-
for (const [key, attr] of Object.entries(schema.attributes)) {
|
|
278
|
-
const value = data?.[key];
|
|
279
|
-
const fieldPath = `${path}.${key}`;
|
|
280
|
-
|
|
281
|
-
if (attr.type === 'component') {
|
|
282
|
-
cleaned[key] = attr.repeatable
|
|
283
|
-
? (value || []).map((v, i) =>
|
|
284
|
-
sanitizeComponent(v, attr.component, `${fieldPath}[${i}]`, errors)
|
|
285
|
-
)
|
|
286
|
-
: sanitizeComponent(value, attr.component, fieldPath, errors);
|
|
287
|
-
} else {
|
|
288
|
-
cleaned[key] = sanitizePrimitive(value, attr, fieldPath, errors);
|
|
238
|
+
//multiple components
|
|
239
|
+
if (Array.isArray(newValue) && Array.isArray(oldValue)) {
|
|
240
|
+
data[field] = newValue.map((block, i) => {
|
|
241
|
+
const oldBlock = oldValue[i];
|
|
242
|
+
if (oldBlock?.id) {
|
|
243
|
+
return { id: oldBlock.id, ...block };
|
|
244
|
+
}
|
|
245
|
+
for (const key of Object.keys(block)) {
|
|
246
|
+
if (Array.isArray(oldBlock[key])) {
|
|
247
|
+
block[key] = block[key].split("|");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return block;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
289
253
|
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return cleaned;
|
|
293
|
-
}
|
|
294
254
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
255
|
+
return data;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
sanitizeComponent(data, uid, rowIndex, errors, path) {
|
|
259
|
+
const schema = strapi.components[uid];
|
|
260
|
+
if (!schema) return data;
|
|
261
|
+
|
|
262
|
+
const cleaned = {};
|
|
263
|
+
|
|
264
|
+
for (const [key, attr] of Object.entries(schema.attributes)) {
|
|
265
|
+
const value = data?.[key];
|
|
266
|
+
const fieldPath = `${path}.${key}`;
|
|
267
|
+
|
|
268
|
+
if (attr.type === 'component') {
|
|
269
|
+
cleaned[key] = attr.repeatable
|
|
270
|
+
? (value || []).map((v, i) =>
|
|
271
|
+
sanitizeComponent(v, attr.component, rowIndex, errors, `${fieldPath}[${i}]`)
|
|
272
|
+
)
|
|
273
|
+
: sanitizeComponent(value, attr.component, rowIndex, errors, fieldPath);
|
|
274
|
+
} else {
|
|
275
|
+
cleaned[key] = sanitizePrimitive(value, attr, rowIndex, errors, fieldPath);
|
|
276
|
+
}
|
|
314
277
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
278
|
+
|
|
279
|
+
return cleaned;
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
sanitizeEntryBeforeWrite(data, uid, rowIndex, errors, path = '') {
|
|
283
|
+
const schema = strapi.contentTypes[uid];
|
|
284
|
+
const cleaned = {};
|
|
285
|
+
|
|
286
|
+
for (const [key, attr] of Object.entries(schema.attributes)) {
|
|
287
|
+
const value = data[key];
|
|
288
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
289
|
+
|
|
290
|
+
if (value === undefined) continue;
|
|
291
|
+
|
|
292
|
+
if (attr.type === 'component') {
|
|
293
|
+
if (!value) {
|
|
294
|
+
cleaned[key] = attr.repeatable ? [] : null;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
cleaned[key] = attr.repeatable
|
|
299
|
+
? value.map((v, i) =>
|
|
300
|
+
sanitizeComponent(v, attr.component, rowIndex, errors, `${fieldPath}[${i}]`)
|
|
301
|
+
)
|
|
302
|
+
: sanitizeComponent(value, attr.component, rowIndex, errors, fieldPath);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
cleaned[key] = sanitizePrimitive(value, attr, rowIndex, errors, fieldPath);
|
|
320
307
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
308
|
+
|
|
309
|
+
return cleaned;
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
sanitizePrimitive(value, attr) {
|
|
313
|
+
if (value === null || value === undefined || value === '') return null;
|
|
314
|
+
switch (attr.type) {
|
|
315
|
+
case 'string':
|
|
316
|
+
case 'text':
|
|
317
|
+
case 'richtext':
|
|
318
|
+
case 'email':
|
|
319
|
+
return String(value).trim();
|
|
320
|
+
case 'boolean':
|
|
321
|
+
if ([true, 'true', 1, '1', 'yes', 'y'].includes(value)) return true;
|
|
322
|
+
if ([false, 'false', 0, '0', 'no', 'n'].includes(value)) return false;
|
|
323
|
+
return false; // fallback
|
|
324
|
+
case 'integer':
|
|
325
|
+
case 'biginteger': {
|
|
326
|
+
const i = parseInt(value, 10);
|
|
327
|
+
return Number.isNaN(i) ? 0 : i;
|
|
328
|
+
}
|
|
329
|
+
case 'float':
|
|
330
|
+
case 'decimal': {
|
|
331
|
+
const f = parseFloat(value);
|
|
332
|
+
return Number.isNaN(f) ? 0 : f;
|
|
333
|
+
}
|
|
334
|
+
case 'date':
|
|
335
|
+
case 'datetime': {
|
|
336
|
+
const d = new Date(value);
|
|
337
|
+
return isNaN(d.getTime()) ? null : d.toISOString();
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
return value;
|
|
326
341
|
}
|
|
342
|
+
},
|
|
327
343
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
344
|
+
async bulkInsertData(importData) {
|
|
345
|
+
const results = {
|
|
346
|
+
created: 0,
|
|
347
|
+
updated: 0,
|
|
348
|
+
errors: [],
|
|
349
|
+
};
|
|
332
350
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
results.errors.push(`Content type ${contentType} not found`);
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
if (!Array.isArray(entries)) {
|
|
347
|
-
results.errors.push(`Invalid data format for ${contentType}`);
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
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
|
+
}
|
|
350
361
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
}
|
|
358
373
|
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return results;
|
|
362
|
-
}
|
|
363
374
|
|
|
364
|
-
|
|
365
|
-
|
|
375
|
+
return results;
|
|
376
|
+
},
|
|
366
377
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
strapi.log.error("Transaction rolled back due to an error!");
|
|
370
|
-
strapi.log.error(results.errors);
|
|
371
|
-
});
|
|
378
|
+
async importEntries(entries, contentType) {
|
|
379
|
+
const results = { created: 0, updated: 0, errors: [] };
|
|
372
380
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
});
|
|
376
386
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
|
390
404
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
405
|
+
// Handle relations & components
|
|
406
|
+
data = await this.handleRelations(data, contentType, trx);
|
|
407
|
+
data = await this.handleComponents(data, existing, contentType);
|
|
408
|
+
const sanitizeErrors = [];
|
|
409
|
+
data = sanitizeEntryBeforeWrite(data, contentType, '', sanitizeErrors);
|
|
394
410
|
|
|
395
|
-
|
|
396
|
-
|
|
411
|
+
if (sanitizeErrors.length) {
|
|
412
|
+
throw new Error(`Data validation failed:\n${sanitizeErrors.join('\n')}`);
|
|
413
|
+
}
|
|
397
414
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
415
|
+
// Update
|
|
416
|
+
if (existing) {
|
|
417
|
+
await strapi.documents(contentType).update(
|
|
418
|
+
{
|
|
419
|
+
documentId: existing.documentId,
|
|
420
|
+
data,
|
|
421
|
+
},
|
|
422
|
+
{ transaction: trx }
|
|
423
|
+
);
|
|
424
|
+
results.updated++;
|
|
425
|
+
}
|
|
401
426
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
427
|
+
// Create
|
|
428
|
+
else {
|
|
429
|
+
await strapi
|
|
430
|
+
.documents(contentType)
|
|
431
|
+
.create({ data }, { transaction: trx });
|
|
432
|
+
results.created++;
|
|
433
|
+
}
|
|
434
|
+
} catch (err) {
|
|
435
|
+
results.errors.push(
|
|
436
|
+
`Failed ${existing ? "updating" : "creating"} on row ${
|
|
437
|
+
i + 2
|
|
438
|
+
}: ${err.message}`
|
|
410
439
|
);
|
|
411
|
-
results.
|
|
412
|
-
|
|
440
|
+
results.created = 0;
|
|
441
|
+
results.updated = 0;
|
|
413
442
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
await strapi.documents(contentType).create(
|
|
417
|
-
{ data },
|
|
418
|
-
{ transaction: trx }
|
|
419
|
-
);
|
|
420
|
-
results.created++;
|
|
443
|
+
// IMPORTANT: force rollback
|
|
444
|
+
throw err;
|
|
421
445
|
}
|
|
422
|
-
} catch (err) {
|
|
423
|
-
results.errors.push(
|
|
424
|
-
`Failed ${existing ? "updating" : "creating"} on row ${
|
|
425
|
-
i + 2
|
|
426
|
-
}: ${err.message}`
|
|
427
|
-
);
|
|
428
|
-
results.created = 0;
|
|
429
|
-
results.updated = 0;
|
|
430
|
-
|
|
431
|
-
// IMPORTANT: force rollback
|
|
432
|
-
throw err;
|
|
433
446
|
}
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
return results;
|
|
438
|
-
}
|
|
439
|
-
|
|
447
|
+
});
|
|
440
448
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
};
|
|
449
|
+
return results;
|
|
450
|
+
},
|
|
451
|
+
});
|