@nkhang1902/strapi-plugin-export-import-clsx 1.0.5 → 1.1.1
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 +382 -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
|
-
}
|
|
45
|
+
},
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
function transformExcelData(filePath) {
|
|
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,403 @@ 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
|
-
|
|
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
|
+
}
|
|
265
253
|
}
|
|
266
|
-
}
|
|
267
254
|
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
}
|
|
289
277
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
314
307
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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;
|
|
320
341
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
sanitizeEntryBeforeWrite(data, uid, rowIndex, errors, path = '') {
|
|
345
|
+
const schema = strapi.contentTypes[uid];
|
|
346
|
+
const cleaned = {};
|
|
347
|
+
|
|
348
|
+
for (const [key, attr] of Object.entries(schema.attributes)) {
|
|
349
|
+
const value = data[key];
|
|
350
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
351
|
+
|
|
352
|
+
if (value === undefined) continue;
|
|
353
|
+
|
|
354
|
+
if (attr.type === 'component') {
|
|
355
|
+
if (!value) {
|
|
356
|
+
cleaned[key] = attr.repeatable ? [] : null;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
cleaned[key] = attr.repeatable
|
|
361
|
+
? value.map((v, i) =>
|
|
362
|
+
sanitizeComponent(v, attr.component, rowIndex, errors, `${fieldPath}[${i}]`)
|
|
363
|
+
)
|
|
364
|
+
: sanitizeComponent(value, attr.component, rowIndex, errors, fieldPath);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
cleaned[key] = sanitizePrimitive(value, attr, rowIndex, errors, fieldPath);
|
|
326
369
|
}
|
|
370
|
+
|
|
371
|
+
return cleaned;
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
async bulkInsertData(importData) {
|
|
375
|
+
const results = {
|
|
376
|
+
created: 0,
|
|
377
|
+
updated: 0,
|
|
378
|
+
errors: [],
|
|
379
|
+
};
|
|
327
380
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
for (const [contentType, entries] of Object.entries(importData)) {
|
|
341
|
-
// Validate entries
|
|
342
|
-
if (!strapi.contentTypes[contentType]) {
|
|
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
|
-
}
|
|
381
|
+
for (const [contentType, entries] of Object.entries(importData)) {
|
|
382
|
+
// Validate entries
|
|
383
|
+
if (!strapi.contentTypes[contentType]) {
|
|
384
|
+
results.errors.push(`Content type ${contentType} not found`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!Array.isArray(entries)) {
|
|
388
|
+
results.errors.push(`Invalid data format for ${contentType}`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
350
391
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
392
|
+
try {
|
|
393
|
+
const { created, updated, errors } = await this.importEntries(
|
|
394
|
+
entries,
|
|
395
|
+
contentType
|
|
396
|
+
);
|
|
397
|
+
results.created += created;
|
|
398
|
+
results.updated += updated;
|
|
399
|
+
results.errors = results.errors.concat(errors);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
results.errors.push(err.message);
|
|
402
|
+
}
|
|
358
403
|
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return results;
|
|
362
|
-
}
|
|
363
404
|
|
|
364
|
-
|
|
365
|
-
|
|
405
|
+
return results;
|
|
406
|
+
},
|
|
366
407
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
strapi.log.error("Transaction rolled back due to an error!");
|
|
370
|
-
strapi.log.error(results.errors);
|
|
371
|
-
});
|
|
408
|
+
async importEntries(entries, contentType) {
|
|
409
|
+
const results = { created: 0, updated: 0, errors: [] };
|
|
372
410
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
411
|
+
await strapi.db.transaction(async ({ trx, rollback, onRollback }) => {
|
|
412
|
+
onRollback(() => {
|
|
413
|
+
strapi.log.error("Transaction rolled back due to an error!");
|
|
414
|
+
strapi.log.error(results.errors);
|
|
415
|
+
});
|
|
376
416
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
417
|
+
for (let i = 0; i < entries.length; i++) {
|
|
418
|
+
const entry = entries[i];
|
|
419
|
+
let existing = null;
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
let { id, ...data } = entry;
|
|
423
|
+
|
|
424
|
+
// Check if document exists
|
|
425
|
+
if (id && id !== "null" && id !== "undefined") {
|
|
426
|
+
existing = await strapi.documents(contentType).findFirst(
|
|
427
|
+
{
|
|
428
|
+
filters: { id },
|
|
429
|
+
populate: "*",
|
|
430
|
+
},
|
|
431
|
+
{ transaction: trx }
|
|
432
|
+
);
|
|
433
|
+
}
|
|
390
434
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
435
|
+
// Handle relations & components
|
|
436
|
+
data = await this.handleRelations(data, contentType, trx);
|
|
437
|
+
data = await this.handleComponents(data, existing, contentType);
|
|
438
|
+
const sanitizeErrors = [];
|
|
439
|
+
data = sanitizeEntryBeforeWrite(data, contentType, '', sanitizeErrors);
|
|
394
440
|
|
|
395
|
-
|
|
396
|
-
|
|
441
|
+
if (sanitizeErrors.length) {
|
|
442
|
+
throw new Error(`Data validation failed:\n${sanitizeErrors.join('\n')}`);
|
|
443
|
+
}
|
|
397
444
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
445
|
+
// Update
|
|
446
|
+
if (existing) {
|
|
447
|
+
await strapi.documents(contentType).update(
|
|
448
|
+
{
|
|
449
|
+
documentId: existing.documentId,
|
|
450
|
+
data,
|
|
451
|
+
},
|
|
452
|
+
{ transaction: trx }
|
|
453
|
+
);
|
|
454
|
+
results.updated++;
|
|
455
|
+
}
|
|
401
456
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
457
|
+
// Create
|
|
458
|
+
else {
|
|
459
|
+
await strapi
|
|
460
|
+
.documents(contentType)
|
|
461
|
+
.create({ data }, { transaction: trx });
|
|
462
|
+
results.created++;
|
|
463
|
+
}
|
|
464
|
+
} catch (err) {
|
|
465
|
+
results.errors.push(
|
|
466
|
+
`Failed ${existing ? "updating" : "creating"} on row ${
|
|
467
|
+
i + 2
|
|
468
|
+
}: ${err.message}`
|
|
410
469
|
);
|
|
411
|
-
results.
|
|
412
|
-
|
|
470
|
+
results.created = 0;
|
|
471
|
+
results.updated = 0;
|
|
413
472
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
await strapi.documents(contentType).create(
|
|
417
|
-
{ data },
|
|
418
|
-
{ transaction: trx }
|
|
419
|
-
);
|
|
420
|
-
results.created++;
|
|
473
|
+
// IMPORTANT: force rollback
|
|
474
|
+
throw err;
|
|
421
475
|
}
|
|
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
476
|
}
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
return results;
|
|
438
|
-
}
|
|
439
|
-
|
|
477
|
+
});
|
|
440
478
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
};
|
|
479
|
+
return results;
|
|
480
|
+
},
|
|
481
|
+
});
|