@nkhang1902/strapi-plugin-export-import-clsx 1.0.4 → 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 +344 -309
- 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,79 +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
|
-
const SYSTEM_KEYS = [
|
|
9
|
-
'documentId',
|
|
10
|
-
'locale',
|
|
11
|
-
'createdAt',
|
|
12
|
-
'updatedAt',
|
|
13
|
-
'publishedAt',
|
|
14
|
-
'createdBy',
|
|
15
|
-
'updatedBy',
|
|
16
|
-
'localizations',
|
|
17
|
-
'status'
|
|
18
|
-
];
|
|
19
|
-
|
|
20
8
|
const SHORTCUT_FIELDS = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
}
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
49
44
|
}
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
45
|
+
},
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
transformExcelData(filePath, targetContentType = null) {
|
|
56
48
|
const workbook = XLSX.readFile(filePath);
|
|
57
49
|
const importData = {};
|
|
58
50
|
|
|
59
|
-
const parseJsonIfNeeded = (value) => {
|
|
60
|
-
if (typeof value !== 'string') return value;
|
|
61
|
-
const trimmed = value.trim();
|
|
62
|
-
if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return value;
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(trimmed);
|
|
66
|
-
} catch {
|
|
67
|
-
return value; // keep as string if invalid JSON
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
51
|
const isComponentField = (key) => {
|
|
72
|
-
|
|
73
|
-
|
|
52
|
+
const parts = key.split("_");
|
|
53
|
+
return parts.length === 2; // exactly one underscore
|
|
74
54
|
};
|
|
75
55
|
|
|
76
|
-
|
|
56
|
+
const unflattenRow = (rows, targetContentType) => {
|
|
77
57
|
const result = [];
|
|
78
58
|
const attr = strapi.contentTypes[targetContentType].attributes;
|
|
79
59
|
for (const row of rows) {
|
|
@@ -83,13 +63,13 @@ function transformExcelData(filePath) {
|
|
|
83
63
|
if (value === null || value === undefined || value === '') {
|
|
84
64
|
rowData[key] = null
|
|
85
65
|
} else if (attr[key] && attr[key].customField && attr[key].type === 'json' && attr[key].default === '[]') {
|
|
86
|
-
rowData[key] =
|
|
66
|
+
rowData[key] = value.split('|');
|
|
87
67
|
} else if (isComponentField(key)) {
|
|
88
68
|
const [comp, field] = key.split('_');
|
|
89
69
|
if (!rowData[comp]) rowData[comp] = {};
|
|
90
|
-
rowData[comp][field] =
|
|
70
|
+
rowData[comp][field] = value;
|
|
91
71
|
} else {
|
|
92
|
-
rowData[key] =
|
|
72
|
+
rowData[key] = value;
|
|
93
73
|
}
|
|
94
74
|
}
|
|
95
75
|
result.push(rowData);
|
|
@@ -99,280 +79,341 @@ function transformExcelData(filePath) {
|
|
|
99
79
|
};
|
|
100
80
|
|
|
101
81
|
const mapSheetNameToContentType = (sheetName) => {
|
|
102
|
-
|
|
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;
|
|
103
87
|
};
|
|
104
88
|
|
|
105
|
-
workbook.SheetNames.forEach(sheetName => {
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
workbook.SheetNames.forEach((sheetName) => {
|
|
90
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
91
|
+
const rows = XLSX.utils.sheet_to_json(worksheet);
|
|
108
92
|
|
|
109
|
-
|
|
93
|
+
if (!rows.length) return;
|
|
110
94
|
|
|
111
|
-
|
|
95
|
+
const contentTypeName = mapSheetNameToContentType(sheetName);
|
|
112
96
|
|
|
113
|
-
|
|
114
|
-
|
|
97
|
+
strapi.log.info(`Reading sheet "${sheetName}" -> ${rows.length} rows`);
|
|
98
|
+
strapi.log.info(`Mapped sheet to content-type: ${contentTypeName}`);
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
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
|
+
);
|
|
120
107
|
return;
|
|
121
108
|
}
|
|
109
|
+
importData[contentTypeName] = unflattenRow(rows, contentTypeName);
|
|
110
|
+
} else {
|
|
111
|
+
strapi.log.error(`Unknown content-type: ${contentTypeName}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
122
114
|
});
|
|
123
115
|
|
|
124
|
-
strapi.log.info(
|
|
116
|
+
strapi.log.info("Final import data keys:", Object.keys(importData));
|
|
125
117
|
return importData;
|
|
126
|
-
}
|
|
118
|
+
},
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (!schema) {
|
|
132
|
-
strapi.log.warn(`Content type ${contentType} not found`);
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return Object.entries(schema.attributes)
|
|
137
|
-
.filter(([_, attr]) => attr.type === "relation")
|
|
138
|
-
.map(([fieldName, attr]) => ({
|
|
139
|
-
field: toCamel(fieldName),
|
|
140
|
-
target: attr.target, // e.g. "api::category.category"
|
|
141
|
-
relation: attr.relation,
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
120
|
+
getRelationFields(contentType) {
|
|
121
|
+
const schema = strapi.contentTypes[contentType];
|
|
144
122
|
|
|
145
|
-
|
|
146
|
-
|
|
123
|
+
if (!schema) {
|
|
124
|
+
strapi.log.warn(`Content type ${contentType} not found`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
147
127
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
}
|
|
152
144
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
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
|
+
};
|
|
157
163
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const targetAttr = strapi.contentTypes[target].attributes;
|
|
161
|
-
for (const field of SHORTCUT_FIELDS) {
|
|
162
|
-
if (!targetAttr[field]) continue;
|
|
163
|
-
const existing = await strapi.documents(target).findFirst({
|
|
164
|
-
filters: { [field]: { $eq: value } },
|
|
165
|
-
});
|
|
166
|
-
if (existing) return {id: existing.id};
|
|
167
|
-
throw new Error(`Data with ${field} ${value} not found`);
|
|
168
|
-
}
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
164
|
+
const relationFields = this.getRelationFields(contentType);
|
|
165
|
+
if (relationFields.length === 0) return entry;
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
if (relationFields.length === 0) return entry;
|
|
167
|
+
const updatedEntry = { ...entry };
|
|
174
168
|
|
|
175
|
-
|
|
169
|
+
for (const rel of relationFields) {
|
|
170
|
+
const { field, target, relation } = rel;
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
);
|
|
186
192
|
}
|
|
187
|
-
continue;
|
|
188
|
-
};
|
|
189
193
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
} else if (typeof value === "string" && value.includes("|")) {
|
|
194
|
-
throw new Error(`Invalid value for field ${field}: ${value}, ${field} is not an array`);
|
|
195
|
-
}
|
|
194
|
+
const values = Array.isArray(value) ? value : [value];
|
|
195
|
+
try {
|
|
196
|
+
const processed = [];
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
203
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
);
|
|
205
209
|
}
|
|
206
|
-
|
|
207
|
-
updatedEntry[field] = Array.isArray(value) ? processed : processed[0];
|
|
208
|
-
} catch (err) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`Failed processing field ${field} with value ${JSON.stringify(value)}: ${err.message}`
|
|
211
|
-
);
|
|
212
210
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return updatedEntry;
|
|
216
|
-
}
|
|
217
211
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const compFields = getComponentFields(contentType);
|
|
212
|
+
return updatedEntry;
|
|
213
|
+
},
|
|
221
214
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
215
|
+
handleComponents(data, existing, contentType) {
|
|
216
|
+
// Get the component fields for this content type
|
|
217
|
+
const compFields = this.getComponentFields(contentType);
|
|
225
218
|
|
|
226
|
-
|
|
219
|
+
for (const field of compFields) {
|
|
220
|
+
const newValue = data[field];
|
|
221
|
+
const oldValue = existing?.[field];
|
|
227
222
|
|
|
228
|
-
|
|
229
|
-
if (!Array.isArray(newValue)) {
|
|
230
|
-
if (oldValue?.id) {
|
|
231
|
-
data[field].id = oldValue.id;
|
|
232
|
-
}
|
|
233
|
-
for (const key of Object.keys(data[field])) {
|
|
234
|
-
if (Array.isArray(oldValue[key])) {
|
|
235
|
-
data[field][key] = data[field][key].split("|");
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
223
|
+
if (!newValue || !oldValue) continue;
|
|
240
224
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (oldBlock?.id) {
|
|
246
|
-
return { id: oldBlock.id, ...block };
|
|
225
|
+
//single component
|
|
226
|
+
if (!Array.isArray(newValue)) {
|
|
227
|
+
if (oldValue?.id) {
|
|
228
|
+
data[field].id = oldValue.id;
|
|
247
229
|
}
|
|
248
|
-
for (const key of Object.keys(
|
|
249
|
-
if (Array.isArray(
|
|
250
|
-
|
|
230
|
+
for (const key of Object.keys(data[field])) {
|
|
231
|
+
if (Array.isArray(oldValue[key])) {
|
|
232
|
+
data[field][key] = data[field][key].split("|");
|
|
251
233
|
}
|
|
252
234
|
}
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return data;
|
|
259
|
-
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
260
237
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
+
}
|
|
273
253
|
}
|
|
274
254
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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);
|
|
279
276
|
}
|
|
280
|
-
continue;
|
|
281
277
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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;
|
|
293
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;
|
|
294
304
|
}
|
|
295
|
-
|
|
305
|
+
|
|
306
|
+
cleaned[key] = sanitizePrimitive(value, attr, rowIndex, errors, fieldPath);
|
|
296
307
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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();
|
|
302
338
|
}
|
|
303
|
-
|
|
339
|
+
default:
|
|
340
|
+
return value;
|
|
304
341
|
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
342
|
+
},
|
|
309
343
|
|
|
344
|
+
async bulkInsertData(importData) {
|
|
345
|
+
const results = {
|
|
346
|
+
created: 0,
|
|
347
|
+
updated: 0,
|
|
348
|
+
errors: [],
|
|
349
|
+
};
|
|
310
350
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
}
|
|
317
361
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
}
|
|
327
373
|
}
|
|
328
374
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
results.created += created;
|
|
332
|
-
results.updated += updated;
|
|
333
|
-
results.errors = results.errors.concat(errors);
|
|
334
|
-
} catch (err) {
|
|
335
|
-
results.errors.push(err.message);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
375
|
+
return results;
|
|
376
|
+
},
|
|
338
377
|
|
|
339
|
-
|
|
340
|
-
}
|
|
378
|
+
async importEntries(entries, contentType) {
|
|
379
|
+
const results = { created: 0, updated: 0, errors: [] };
|
|
341
380
|
|
|
342
|
-
async
|
|
343
|
-
|
|
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
|
+
});
|
|
344
386
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
strapi.log.error(results.errors);
|
|
349
|
-
});
|
|
387
|
+
for (let i = 0; i < entries.length; i++) {
|
|
388
|
+
const entry = entries[i];
|
|
389
|
+
let existing = null;
|
|
350
390
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
let existing = null;
|
|
391
|
+
try {
|
|
392
|
+
let { id, ...data } = entry;
|
|
354
393
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{ transaction: trx }
|
|
366
|
-
);
|
|
367
|
-
}
|
|
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
|
+
}
|
|
368
404
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
410
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
411
|
+
if (sanitizeErrors.length) {
|
|
412
|
+
throw new Error(`Data validation failed:\n${sanitizeErrors.join('\n')}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Update
|
|
416
|
+
if (existing) {
|
|
376
417
|
await strapi.documents(contentType).update(
|
|
377
418
|
{
|
|
378
419
|
documentId: existing.documentId,
|
|
@@ -382,35 +423,29 @@ async function importEntries(entries, contentType) {
|
|
|
382
423
|
);
|
|
383
424
|
results.updated++;
|
|
384
425
|
}
|
|
385
|
-
}
|
|
386
426
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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}`
|
|
392
439
|
);
|
|
393
|
-
results.created
|
|
394
|
-
|
|
395
|
-
} catch (err) {
|
|
396
|
-
results.errors.push(
|
|
397
|
-
`Failed ${existing ? "updating" : "creating"} on row ${
|
|
398
|
-
i + 2
|
|
399
|
-
}: ${err.message}`
|
|
400
|
-
);
|
|
401
|
-
results.created = 0;
|
|
402
|
-
results.updated = 0;
|
|
440
|
+
results.created = 0;
|
|
441
|
+
results.updated = 0;
|
|
403
442
|
|
|
404
|
-
|
|
405
|
-
|
|
443
|
+
// IMPORTANT: force rollback
|
|
444
|
+
throw err;
|
|
445
|
+
}
|
|
406
446
|
}
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
return results;
|
|
411
|
-
}
|
|
412
|
-
|
|
447
|
+
});
|
|
413
448
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
449
|
+
return results;
|
|
450
|
+
},
|
|
451
|
+
});
|