@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,19 +1,34 @@
|
|
|
1
|
-
const XLSX = require(
|
|
1
|
+
const XLSX = require("xlsx");
|
|
2
2
|
|
|
3
3
|
module.exports = ({ strapi }) => ({
|
|
4
|
-
async exportData(format =
|
|
4
|
+
async exportData(format = "json", contentType = null, rawFilters = {}) {
|
|
5
|
+
// Normalize content type - handle both content-manager and event-manager formats
|
|
6
|
+
if (contentType && !contentType.startsWith("api::")) {
|
|
7
|
+
// If it's already in api:: format from event-manager, use as is
|
|
8
|
+
// If it's from content-manager, it should already be in correct format
|
|
9
|
+
contentType = contentType;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
// Get only API content types (collections)
|
|
6
13
|
let contentTypes;
|
|
7
14
|
if (contentType) {
|
|
15
|
+
// Validate that the content type exists
|
|
16
|
+
if (!strapi.contentTypes[contentType]) {
|
|
17
|
+
strapi.log.error(
|
|
18
|
+
`Content type ${contentType} not found. Available types:`,
|
|
19
|
+
Object.keys(strapi.contentTypes)
|
|
20
|
+
);
|
|
21
|
+
throw new Error(`Content type ${contentType} not found`);
|
|
22
|
+
}
|
|
8
23
|
contentTypes = [contentType];
|
|
9
24
|
} else {
|
|
10
|
-
contentTypes = Object.keys(strapi.contentTypes).filter(
|
|
11
|
-
|
|
25
|
+
contentTypes = Object.keys(strapi.contentTypes).filter((key) =>
|
|
26
|
+
key.startsWith("api::")
|
|
12
27
|
);
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
const exportData = {
|
|
16
|
-
version: strapi.config.get(
|
|
31
|
+
version: strapi.config.get("info.strapi"),
|
|
17
32
|
timestamp: new Date().toISOString(),
|
|
18
33
|
data: {},
|
|
19
34
|
};
|
|
@@ -23,81 +38,76 @@ module.exports = ({ strapi }) => ({
|
|
|
23
38
|
// Parse filters from URL format
|
|
24
39
|
const parsedFilters = this.parseFilters(rawFilters);
|
|
25
40
|
|
|
26
|
-
if (rawFilters[
|
|
27
|
-
parsedFilters._q = rawFilters[
|
|
41
|
+
if (rawFilters["_q"]) {
|
|
42
|
+
parsedFilters._q = rawFilters["_q"];
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
strapi.log.info(
|
|
45
|
+
strapi.log.info(
|
|
46
|
+
`Exporting ${ct} with raw filters: ${JSON.stringify(rawFilters)}`
|
|
47
|
+
);
|
|
31
48
|
strapi.log.info(`Parsed filters: ${JSON.stringify(parsedFilters)}`);
|
|
32
|
-
strapi.log.info(`Selected IDs: ${JSON.stringify(selectedIds)}`);
|
|
33
49
|
|
|
34
50
|
let entries = [];
|
|
35
51
|
let filters = parsedFilters.filters;
|
|
36
52
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// Export all entries with filters
|
|
54
|
+
const searchable = this.getSearchableFields(strapi.contentTypes[ct]);
|
|
55
|
+
const numberSearchable = this.getNumberFields(strapi.contentTypes[ct]);
|
|
56
|
+
|
|
57
|
+
if (parsedFilters._q) {
|
|
58
|
+
strapi.log.info(
|
|
59
|
+
`Applying search query: ${
|
|
60
|
+
parsedFilters._q
|
|
61
|
+
} for fields: ${JSON.stringify([
|
|
62
|
+
...searchable,
|
|
63
|
+
...numberSearchable,
|
|
64
|
+
])}`
|
|
65
|
+
);
|
|
66
|
+
const orConditions = [];
|
|
67
|
+
|
|
68
|
+
if (searchable.length > 0) {
|
|
69
|
+
orConditions.push(
|
|
70
|
+
...searchable.map((field) => ({
|
|
71
|
+
[field]: { $containsi: parsedFilters._q },
|
|
72
|
+
}))
|
|
73
|
+
);
|
|
52
74
|
}
|
|
53
|
-
} else {
|
|
54
|
-
// Export all entries with filters
|
|
55
|
-
const searchable = this.getSearchableFields(strapi.contentTypes[ct]);
|
|
56
|
-
const numberSearchable = this.getNumberFields(strapi.contentTypes[ct]);
|
|
57
|
-
|
|
58
|
-
if (parsedFilters._q) {
|
|
59
|
-
strapi.log.info(`Applying search query: ${parsedFilters._q} for fields: ${JSON.stringify([...searchable, ...numberSearchable])}`);
|
|
60
|
-
const orConditions = [];
|
|
61
|
-
|
|
62
|
-
if (searchable.length > 0) {
|
|
63
|
-
orConditions.push(
|
|
64
|
-
...searchable.map((field) => ({
|
|
65
|
-
[field]: { $containsi: parsedFilters._q }
|
|
66
|
-
}))
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (numberSearchable.length > 0 && !isNaN(parsedFilters._q)) {
|
|
71
|
-
orConditions.push(
|
|
72
|
-
...numberSearchable.map((field) => ({
|
|
73
|
-
[field]: { $eq: Number(parsedFilters._q) }
|
|
74
|
-
}))
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
$
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
]
|
|
85
|
-
};
|
|
86
|
-
}
|
|
76
|
+
if (numberSearchable.length > 0 && !isNaN(parsedFilters._q)) {
|
|
77
|
+
orConditions.push(
|
|
78
|
+
...numberSearchable.map((field) => ({
|
|
79
|
+
[field]: { $eq: Number(parsedFilters._q) },
|
|
80
|
+
}))
|
|
81
|
+
);
|
|
87
82
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
strapi.log.info(`EntityService found ${entries?.length || 0} entries`);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
strapi.log.error(`Failed to query entries:`, error);
|
|
83
|
+
|
|
84
|
+
if (orConditions.length > 0) {
|
|
85
|
+
filters = {
|
|
86
|
+
...filters,
|
|
87
|
+
$and: [...(filters?.$and || []), { $or: orConditions }],
|
|
88
|
+
};
|
|
97
89
|
}
|
|
98
90
|
}
|
|
99
91
|
|
|
100
|
-
strapi.log.info(`
|
|
92
|
+
strapi.log.info(`Parsed query filters: ${JSON.stringify(filters)}`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
entries = await strapi.documents(ct).findMany({
|
|
96
|
+
filters: { ...filters },
|
|
97
|
+
populate: "*",
|
|
98
|
+
});
|
|
99
|
+
strapi.log.info(
|
|
100
|
+
`EntityService found ${entries?.length || 0} entries`
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
strapi.log.error(`Failed to query entries:`, error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
strapi.log.info(
|
|
107
|
+
`Final result: ${
|
|
108
|
+
entries?.length || 0
|
|
109
|
+
} entries for ${ct} (total found: ${entries?.length || 0})`
|
|
110
|
+
);
|
|
101
111
|
|
|
102
112
|
if (entries && entries.length > 0) {
|
|
103
113
|
exportData.data[ct] = entries;
|
|
@@ -107,7 +117,7 @@ module.exports = ({ strapi }) => ({
|
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
if (format ===
|
|
120
|
+
if (format === "excel") {
|
|
111
121
|
return this.convertToExcel(exportData.data);
|
|
112
122
|
}
|
|
113
123
|
|
|
@@ -117,8 +127,15 @@ module.exports = ({ strapi }) => ({
|
|
|
117
127
|
getSearchableFields(contentTypeSchema) {
|
|
118
128
|
const searchable = [];
|
|
119
129
|
|
|
120
|
-
for (const [fieldName, field] of Object.entries(
|
|
121
|
-
|
|
130
|
+
for (const [fieldName, field] of Object.entries(
|
|
131
|
+
contentTypeSchema.attributes
|
|
132
|
+
)) {
|
|
133
|
+
if (
|
|
134
|
+
["string", "text", "richtext", "email", "uid", "enumeration"].includes(
|
|
135
|
+
field.type
|
|
136
|
+
) &&
|
|
137
|
+
fieldName !== "locale"
|
|
138
|
+
) {
|
|
122
139
|
searchable.push(fieldName);
|
|
123
140
|
}
|
|
124
141
|
}
|
|
@@ -129,13 +146,19 @@ module.exports = ({ strapi }) => ({
|
|
|
129
146
|
getNumberFields(contentTypeSchema) {
|
|
130
147
|
const numberFields = [];
|
|
131
148
|
|
|
132
|
-
for (const [fieldName, field] of Object.entries(
|
|
133
|
-
|
|
149
|
+
for (const [fieldName, field] of Object.entries(
|
|
150
|
+
contentTypeSchema.attributes
|
|
151
|
+
)) {
|
|
152
|
+
if (
|
|
153
|
+
["number", "integer", "biginteger", "float", "decimal"].includes(
|
|
154
|
+
field.type
|
|
155
|
+
)
|
|
156
|
+
) {
|
|
134
157
|
numberFields.push(fieldName);
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
|
|
138
|
-
numberFields.push(
|
|
161
|
+
numberFields.push("id");
|
|
139
162
|
|
|
140
163
|
return numberFields;
|
|
141
164
|
},
|
|
@@ -144,26 +167,39 @@ module.exports = ({ strapi }) => ({
|
|
|
144
167
|
const parsed = {};
|
|
145
168
|
for (const [key, value] of Object.entries(filters)) {
|
|
146
169
|
// Skip pagination and sorting params
|
|
147
|
-
if (
|
|
170
|
+
if (
|
|
171
|
+
[
|
|
172
|
+
"page",
|
|
173
|
+
"pageSize",
|
|
174
|
+
"sort",
|
|
175
|
+
"locale",
|
|
176
|
+
"format",
|
|
177
|
+
"contentType",
|
|
178
|
+
"_q",
|
|
179
|
+
].includes(key)
|
|
180
|
+
) {
|
|
148
181
|
continue;
|
|
149
182
|
}
|
|
150
183
|
|
|
151
184
|
// Handle URL encoded filter format like filters[$and][0][shortName][$contains]
|
|
152
|
-
if (key.startsWith(
|
|
185
|
+
if (key.startsWith("filters[")) {
|
|
153
186
|
// Extract the actual filter structure
|
|
154
|
-
const match = key.match(
|
|
187
|
+
const match = key.match(
|
|
188
|
+
/filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/
|
|
189
|
+
);
|
|
155
190
|
if (match) {
|
|
156
191
|
const [, operator, index, field, condition] = match;
|
|
157
192
|
|
|
158
193
|
if (!parsed.filters) parsed.filters = {};
|
|
159
194
|
|
|
160
|
-
if (operator ===
|
|
195
|
+
if (operator === "$and") {
|
|
161
196
|
if (!parsed.filters.$and) parsed.filters.$and = [];
|
|
162
197
|
const idx = parseInt(index) || 0;
|
|
163
198
|
if (!parsed.filters.$and[idx]) parsed.filters.$and[idx] = {};
|
|
164
199
|
|
|
165
200
|
if (condition) {
|
|
166
|
-
if (!parsed.filters.$and[idx][field])
|
|
201
|
+
if (!parsed.filters.$and[idx][field])
|
|
202
|
+
parsed.filters.$and[idx][field] = {};
|
|
167
203
|
parsed.filters.$and[idx][field][condition] = value;
|
|
168
204
|
} else {
|
|
169
205
|
parsed.filters.$and[idx][field] = value;
|
|
@@ -183,49 +219,50 @@ module.exports = ({ strapi }) => ({
|
|
|
183
219
|
let hasData = false;
|
|
184
220
|
|
|
185
221
|
const SYSTEM_KEYS = [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
222
|
+
"documentId",
|
|
223
|
+
"locale",
|
|
224
|
+
"createdAt",
|
|
225
|
+
"updatedAt",
|
|
226
|
+
"publishedAt",
|
|
227
|
+
"createdBy",
|
|
228
|
+
"updatedBy",
|
|
229
|
+
"localizations",
|
|
230
|
+
"status",
|
|
195
231
|
];
|
|
196
232
|
const SHORTCUT_FIELDS = [
|
|
197
|
-
|
|
198
|
-
|
|
233
|
+
"email",
|
|
234
|
+
"businessEmail",
|
|
235
|
+
"name",
|
|
236
|
+
"title",
|
|
237
|
+
"tickerCode",
|
|
238
|
+
];
|
|
199
239
|
|
|
200
240
|
for (const [contentType, entries] of Object.entries(data)) {
|
|
201
241
|
// Clean sheet name (Excel has restrictions)
|
|
202
242
|
const sheetName = contentType
|
|
203
|
-
.split(
|
|
243
|
+
.split(".")
|
|
204
244
|
.pop()
|
|
205
|
-
.replace(/[^\w\s-]/gi,
|
|
245
|
+
.replace(/[^\w\s-]/gi, "_")
|
|
206
246
|
.substring(0, 31);
|
|
207
247
|
|
|
208
|
-
|
|
209
248
|
if (entries && entries.length > 0) {
|
|
210
249
|
hasData = true;
|
|
211
250
|
|
|
212
251
|
const attr = strapi.contentTypes[contentType].attributes;
|
|
213
252
|
const customFields = Object.entries(attr)
|
|
214
|
-
.filter(([key, definition]) =>
|
|
215
|
-
definition.customField
|
|
216
|
-
)
|
|
253
|
+
.filter(([key, definition]) => definition.customField)
|
|
217
254
|
.map(([key]) => key);
|
|
218
255
|
|
|
219
256
|
const relationFields = Object.entries(attr)
|
|
220
|
-
.filter(([key, definition]) => definition.type ===
|
|
257
|
+
.filter(([key, definition]) => definition.type === "relation")
|
|
221
258
|
.map(([key]) => key);
|
|
222
259
|
|
|
223
260
|
const skipFields = Object.entries(attr)
|
|
224
|
-
.filter(([key, definition]) => definition.type ===
|
|
261
|
+
.filter(([key, definition]) => definition.type === "media")
|
|
225
262
|
.map(([key]) => key);
|
|
226
263
|
|
|
227
264
|
const componentFields = Object.entries(attr)
|
|
228
|
-
.filter(([key, definition]) => definition.type ===
|
|
265
|
+
.filter(([key, definition]) => definition.type === "component")
|
|
229
266
|
.map(([key]) => key);
|
|
230
267
|
|
|
231
268
|
function handleObject(key, value) {
|
|
@@ -237,14 +274,14 @@ module.exports = ({ strapi }) => ({
|
|
|
237
274
|
}
|
|
238
275
|
}
|
|
239
276
|
}
|
|
240
|
-
return undefined
|
|
277
|
+
return undefined;
|
|
241
278
|
}
|
|
242
279
|
// Clean and flatten entries for Excel
|
|
243
|
-
const cleanedEntries = entries.map(entry => {
|
|
280
|
+
const cleanedEntries = entries.map((entry) => {
|
|
244
281
|
function cleanAndFlatten(obj) {
|
|
245
282
|
if (Array.isArray(obj)) {
|
|
246
283
|
return obj.map(cleanAndFlatten);
|
|
247
|
-
} else if (obj !== null && typeof obj ===
|
|
284
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
248
285
|
const result = {};
|
|
249
286
|
|
|
250
287
|
for (const key in obj) {
|
|
@@ -253,22 +290,23 @@ module.exports = ({ strapi }) => ({
|
|
|
253
290
|
// Skip system keys
|
|
254
291
|
if (SYSTEM_KEYS.includes(key)) continue;
|
|
255
292
|
if (customFields.includes(key)) continue;
|
|
256
|
-
if ([...skipFields,
|
|
293
|
+
if ([...skipFields, "wishlist", "availableSlot"].includes(key))
|
|
294
|
+
continue;
|
|
257
295
|
|
|
258
296
|
if (componentFields.includes(key)) {
|
|
259
297
|
for (const subKey in value) {
|
|
260
|
-
if (subKey ===
|
|
298
|
+
if (subKey === "id") continue;
|
|
261
299
|
result[`${key}_${subKey}`] = value[subKey];
|
|
262
300
|
}
|
|
263
301
|
continue;
|
|
264
302
|
}
|
|
265
303
|
|
|
266
|
-
if (value === null || typeof value !==
|
|
304
|
+
if (value === null || typeof value !== "object") {
|
|
267
305
|
result[key] = value;
|
|
268
306
|
continue;
|
|
269
307
|
}
|
|
270
308
|
|
|
271
|
-
if (!Array.isArray(value) && typeof value ===
|
|
309
|
+
if (!Array.isArray(value) && typeof value === "object") {
|
|
272
310
|
let temp = handleObject(key, value);
|
|
273
311
|
if (temp !== undefined) {
|
|
274
312
|
result[key] = temp;
|
|
@@ -277,7 +315,7 @@ module.exports = ({ strapi }) => ({
|
|
|
277
315
|
}
|
|
278
316
|
|
|
279
317
|
if (Array.isArray(value)) {
|
|
280
|
-
if (value.length > 0 && typeof value[0] ===
|
|
318
|
+
if (value.length > 0 && typeof value[0] === "object") {
|
|
281
319
|
let arrValue = [];
|
|
282
320
|
for (const subValue in value) {
|
|
283
321
|
arrValue.push(handleObject(key, value[subValue]));
|
|
@@ -311,12 +349,16 @@ module.exports = ({ strapi }) => ({
|
|
|
311
349
|
}
|
|
312
350
|
return result;
|
|
313
351
|
}
|
|
314
|
-
const cleanedFlat = cleanedEntries.map(entry =>
|
|
352
|
+
const cleanedFlat = cleanedEntries.map((entry) =>
|
|
353
|
+
flattenForXLSX(entry)
|
|
354
|
+
);
|
|
315
355
|
const worksheet = XLSX.utils.json_to_sheet(cleanedFlat);
|
|
316
356
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
317
357
|
} else {
|
|
318
358
|
// Create empty sheet with headers if no data
|
|
319
|
-
const worksheet = XLSX.utils.json_to_sheet([
|
|
359
|
+
const worksheet = XLSX.utils.json_to_sheet([
|
|
360
|
+
{ message: "No data found" },
|
|
361
|
+
]);
|
|
320
362
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
321
363
|
hasData = true; // Prevent empty workbook error
|
|
322
364
|
}
|
|
@@ -324,28 +366,30 @@ module.exports = ({ strapi }) => ({
|
|
|
324
366
|
|
|
325
367
|
// If still no data, create a default sheet
|
|
326
368
|
if (!hasData) {
|
|
327
|
-
const worksheet = XLSX.utils.json_to_sheet([
|
|
328
|
-
|
|
369
|
+
const worksheet = XLSX.utils.json_to_sheet([
|
|
370
|
+
{ message: "No data to export" },
|
|
371
|
+
]);
|
|
372
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, "NoData");
|
|
329
373
|
}
|
|
330
374
|
|
|
331
|
-
return XLSX.write(workbook, { type:
|
|
375
|
+
return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
|
|
332
376
|
},
|
|
333
377
|
|
|
334
378
|
async exportSingleEntry(contentType, entryId) {
|
|
335
379
|
try {
|
|
336
380
|
const entry = await strapi.entityService.findOne(contentType, entryId, {
|
|
337
|
-
populate:
|
|
381
|
+
populate: "*",
|
|
338
382
|
});
|
|
339
383
|
|
|
340
384
|
if (!entry) {
|
|
341
|
-
throw new Error(
|
|
385
|
+
throw new Error("Entry not found");
|
|
342
386
|
}
|
|
343
387
|
|
|
344
388
|
const exportData = {
|
|
345
|
-
version: strapi.config.get(
|
|
389
|
+
version: strapi.config.get("info.strapi"),
|
|
346
390
|
timestamp: new Date().toISOString(),
|
|
347
391
|
data: {
|
|
348
|
-
[contentType]: [entry]
|
|
392
|
+
[contentType]: [entry],
|
|
349
393
|
},
|
|
350
394
|
};
|
|
351
395
|
|
|
@@ -354,4 +398,4 @@ module.exports = ({ strapi }) => ({
|
|
|
354
398
|
throw error;
|
|
355
399
|
}
|
|
356
400
|
},
|
|
357
|
-
});
|
|
401
|
+
});
|