@nkhang1902/strapi-plugin-export-import-clsx 1.0.3
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/README.md +95 -0
- package/admin/src/components/BulkActions/index.js +70 -0
- package/admin/src/components/ExportButton/index.js +48 -0
- package/admin/src/components/ExportImportButtons/index.js +245 -0
- package/admin/src/components/ImportButton/index.js +54 -0
- package/admin/src/components/Initializer/index.js +15 -0
- package/admin/src/components/PluginIcon/index.js +6 -0
- package/admin/src/pages/App/index.js +8 -0
- package/admin/src/pages/HomePage/index.js +297 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/en.json +14 -0
- package/package.json +60 -0
- package/server/bootstrap.js +3 -0
- package/server/config/index.js +4 -0
- package/server/content-types/index.js +1 -0
- package/server/controllers/export-controller.js +62 -0
- package/server/controllers/import-controller.js +42 -0
- package/server/controllers/index.js +7 -0
- package/server/destroy.js +3 -0
- package/server/middlewares/index.js +1 -0
- package/server/policies/index.js +1 -0
- package/server/register.js +3 -0
- package/server/routes/index.js +29 -0
- package/server/services/export-service.js +407 -0
- package/server/services/import-service.js +416 -0
- package/server/services/index.js +7 -0
- package/strapi-admin.js +88 -0
- package/strapi-server.js +34 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
const XLSX = require('xlsx');
|
|
2
|
+
|
|
3
|
+
module.exports = ({ strapi }) => ({
|
|
4
|
+
async exportData(format = 'json', contentType = null, filters = {}, selectedIds = []) {
|
|
5
|
+
// Get only API content types (collections)
|
|
6
|
+
let contentTypes;
|
|
7
|
+
if (contentType) {
|
|
8
|
+
contentTypes = [contentType];
|
|
9
|
+
} else {
|
|
10
|
+
contentTypes = Object.keys(strapi.contentTypes).filter(
|
|
11
|
+
(key) => key.startsWith('api::')
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const exportData = {
|
|
16
|
+
version: strapi.config.get('info.strapi'),
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
data: {},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (const ct of contentTypes) {
|
|
22
|
+
try {
|
|
23
|
+
// Parse filters from URL format
|
|
24
|
+
const parsedFilters = this.parseFilters(filters);
|
|
25
|
+
|
|
26
|
+
strapi.log.info(`Exporting ${ct} with raw filters:`, filters);
|
|
27
|
+
strapi.log.info(`Parsed filters:`, parsedFilters);
|
|
28
|
+
strapi.log.info(`Selected IDs:`, selectedIds);
|
|
29
|
+
|
|
30
|
+
let entries = [];
|
|
31
|
+
|
|
32
|
+
// If specific IDs are selected, export only those
|
|
33
|
+
if (selectedIds && selectedIds.length > 0) {
|
|
34
|
+
try {
|
|
35
|
+
if (strapi.documents) {
|
|
36
|
+
// Get entries by documentId for Strapi v5
|
|
37
|
+
for (const id of selectedIds) {
|
|
38
|
+
try {
|
|
39
|
+
const entry = await strapi.documents(ct).findOne({
|
|
40
|
+
documentId: id,
|
|
41
|
+
populate: '*',
|
|
42
|
+
});
|
|
43
|
+
if (entry) {
|
|
44
|
+
entries.push(entry);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
strapi.log.warn(`Failed to find entry ${id}:`, error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Fallback for older Strapi versions
|
|
52
|
+
for (const id of selectedIds) {
|
|
53
|
+
try {
|
|
54
|
+
const entry = await strapi.entityService.findOne(ct, id, {
|
|
55
|
+
populate: '*',
|
|
56
|
+
});
|
|
57
|
+
if (entry) {
|
|
58
|
+
entries.push(entry);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
strapi.log.warn(`Failed to find entry ${id}:`, error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
strapi.log.error(`Failed to export selected entries:`, error);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Export all entries with filters
|
|
70
|
+
try {
|
|
71
|
+
if (strapi.documents) {
|
|
72
|
+
// Get all entries (both published and draft) but avoid duplicates
|
|
73
|
+
const allEntries = await strapi.documents(ct).findMany({
|
|
74
|
+
populate: '*',
|
|
75
|
+
// Don't specify status to get all
|
|
76
|
+
});
|
|
77
|
+
// Group by documentId and keep only the best version (published > modified draft > draft)
|
|
78
|
+
const uniqueEntries = new Map();
|
|
79
|
+
for (const entry of allEntries) {
|
|
80
|
+
const docId = entry.documentId;
|
|
81
|
+
const isPublished = !!entry.publishedAt;
|
|
82
|
+
const isModified = entry.updatedAt !== entry.createdAt;
|
|
83
|
+
if (!uniqueEntries.has(docId)) {
|
|
84
|
+
uniqueEntries.set(docId, entry);
|
|
85
|
+
} else {
|
|
86
|
+
const existing = uniqueEntries.get(docId);
|
|
87
|
+
const existingIsPublished = !!existing.publishedAt;
|
|
88
|
+
const existingIsModified = existing.updatedAt !== existing.createdAt;
|
|
89
|
+
// Priority: published > modified draft > draft
|
|
90
|
+
if (isPublished && !existingIsPublished) {
|
|
91
|
+
uniqueEntries.set(docId, entry);
|
|
92
|
+
} else if (!isPublished && !existingIsPublished && isModified && !existingIsModified) {
|
|
93
|
+
uniqueEntries.set(docId, entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
entries = Array.from(uniqueEntries.values());
|
|
99
|
+
strapi.log.info(`Found ${allEntries.length} total entries, ${entries.length} unique entries after deduplication`);
|
|
100
|
+
|
|
101
|
+
// Apply filters
|
|
102
|
+
if (parsedFilters && Object.keys(parsedFilters).length > 0) {
|
|
103
|
+
strapi.log.info('Applying filters:', parsedFilters);
|
|
104
|
+
entries = this.applyClientSideFilters(entries, parsedFilters);
|
|
105
|
+
strapi.log.info(`After filtering: ${entries.length} entries`);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Fallback for older Strapi versions
|
|
109
|
+
entries = await strapi.entityService.findMany(ct, {
|
|
110
|
+
populate: '*',
|
|
111
|
+
filters: parsedFilters,
|
|
112
|
+
});
|
|
113
|
+
strapi.log.info(`EntityService found ${entries?.length || 0} entries`);
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
strapi.log.error(`Failed to query entries:`, error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
strapi.log.info(`Final result: ${entries?.length || 0} entries for ${ct} (total found: ${entries?.length || 0})`);
|
|
121
|
+
|
|
122
|
+
if (entries && entries.length > 0) {
|
|
123
|
+
exportData.data[ct] = entries;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
strapi.log.error(`Failed to export ${ct}:`, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (format === 'excel') {
|
|
131
|
+
return this.convertToExcel(exportData.data);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return exportData;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
parseFilters(filters) {
|
|
138
|
+
const parsed = {};
|
|
139
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
140
|
+
// Skip pagination and sorting params
|
|
141
|
+
if (['page', 'pageSize', 'sort', 'locale', 'format', 'contentType', 'selectedIds'].includes(key)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle URL encoded filter format like filters[$and][0][shortName][$contains]
|
|
146
|
+
if (key.startsWith('filters[')) {
|
|
147
|
+
// Extract the actual filter structure
|
|
148
|
+
const match = key.match(/filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/);
|
|
149
|
+
if (match) {
|
|
150
|
+
const [, operator, index, field, condition] = match;
|
|
151
|
+
|
|
152
|
+
if (!parsed.filters) parsed.filters = {};
|
|
153
|
+
|
|
154
|
+
if (operator === '$and') {
|
|
155
|
+
if (!parsed.filters.$and) parsed.filters.$and = [];
|
|
156
|
+
const idx = parseInt(index) || 0;
|
|
157
|
+
if (!parsed.filters.$and[idx]) parsed.filters.$and[idx] = {};
|
|
158
|
+
|
|
159
|
+
if (condition) {
|
|
160
|
+
if (!parsed.filters.$and[idx][field]) parsed.filters.$and[idx][field] = {};
|
|
161
|
+
parsed.filters.$and[idx][field][condition] = value;
|
|
162
|
+
} else {
|
|
163
|
+
parsed.filters.$and[idx][field] = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
parsed[key] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return parsed;
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
applyClientSideFilters(entries, filters) {
|
|
176
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
177
|
+
return entries;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const filtered = entries.filter(entry => {
|
|
181
|
+
// Handle structured filters
|
|
182
|
+
if (filters.filters && filters.filters.$and) {
|
|
183
|
+
for (const condition of filters.filters.$and) {
|
|
184
|
+
for (const [field, criteria] of Object.entries(condition)) {
|
|
185
|
+
if (typeof criteria === 'object' && criteria.$contains) {
|
|
186
|
+
// Handle $contains filter
|
|
187
|
+
if (entry[field]) {
|
|
188
|
+
const fieldValue = String(entry[field]).toLowerCase();
|
|
189
|
+
const searchValue = String(criteria.$contains).toLowerCase();
|
|
190
|
+
if (!fieldValue.includes(searchValue)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
return false; // Field doesn't exist, exclude entry
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// Handle exact match
|
|
198
|
+
if (entry[field] !== criteria) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Handle other filter formats
|
|
206
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
207
|
+
if (key === 'filters') continue; // Already handled above
|
|
208
|
+
|
|
209
|
+
// Handle simple search (global search)
|
|
210
|
+
if (key === '_q' || key === 'search') {
|
|
211
|
+
// Global search across main fields
|
|
212
|
+
const searchFields = ['shortName', 'name', 'title'];
|
|
213
|
+
const searchValue = String(value).toLowerCase();
|
|
214
|
+
const found = searchFields.some(field => {
|
|
215
|
+
if (entry[field]) {
|
|
216
|
+
return String(entry[field]).toLowerCase().includes(searchValue);
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
});
|
|
220
|
+
if (!found) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return filtered;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
convertToExcel(data) {
|
|
232
|
+
const workbook = XLSX.utils.book_new();
|
|
233
|
+
let hasData = false;
|
|
234
|
+
|
|
235
|
+
const SYSTEM_KEYS = [
|
|
236
|
+
'documentId',
|
|
237
|
+
'locale',
|
|
238
|
+
'createdAt',
|
|
239
|
+
'updatedAt',
|
|
240
|
+
'publishedAt',
|
|
241
|
+
'createdBy',
|
|
242
|
+
'updatedBy',
|
|
243
|
+
'localizations',
|
|
244
|
+
'status'
|
|
245
|
+
];
|
|
246
|
+
const SHORTCUT_FIELDS = [
|
|
247
|
+
'email','businessEmail','name','title','tickerCode',
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
for (const [contentType, entries] of Object.entries(data)) {
|
|
251
|
+
// Clean sheet name (Excel has restrictions)
|
|
252
|
+
const sheetName = contentType
|
|
253
|
+
.split('.')
|
|
254
|
+
.pop()
|
|
255
|
+
.replace(/[^\w\s-]/gi, '_')
|
|
256
|
+
.substring(0, 31);
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
if (entries && entries.length > 0) {
|
|
260
|
+
hasData = true;
|
|
261
|
+
|
|
262
|
+
const attr = strapi.contentTypes[contentType].attributes;
|
|
263
|
+
const customFields = Object.entries(attr)
|
|
264
|
+
.filter(([key, definition]) =>
|
|
265
|
+
definition.customField
|
|
266
|
+
)
|
|
267
|
+
.map(([key]) => key);
|
|
268
|
+
|
|
269
|
+
const relationFields = Object.entries(attr)
|
|
270
|
+
.filter(([key, definition]) => definition.type === 'relation')
|
|
271
|
+
.map(([key]) => key);
|
|
272
|
+
|
|
273
|
+
const skipFields = Object.entries(attr)
|
|
274
|
+
.filter(([key, definition]) => definition.type === 'media')
|
|
275
|
+
.map(([key]) => key);
|
|
276
|
+
|
|
277
|
+
const componentFields = Object.entries(attr)
|
|
278
|
+
.filter(([key, definition]) => definition.type === 'component')
|
|
279
|
+
.map(([key]) => key);
|
|
280
|
+
|
|
281
|
+
function handleObject(key, value) {
|
|
282
|
+
if (!value) return;
|
|
283
|
+
if (relationFields.includes(key)) {
|
|
284
|
+
for (const field of SHORTCUT_FIELDS) {
|
|
285
|
+
if (value[field]) {
|
|
286
|
+
return value[field];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return undefined
|
|
291
|
+
}
|
|
292
|
+
// Clean and flatten entries for Excel
|
|
293
|
+
const cleanedEntries = entries.map(entry => {
|
|
294
|
+
function cleanAndFlatten(obj) {
|
|
295
|
+
if (Array.isArray(obj)) {
|
|
296
|
+
return obj.map(cleanAndFlatten);
|
|
297
|
+
} else if (obj !== null && typeof obj === 'object') {
|
|
298
|
+
const result = {};
|
|
299
|
+
|
|
300
|
+
for (const key in obj) {
|
|
301
|
+
const value = obj[key];
|
|
302
|
+
|
|
303
|
+
// Skip system keys
|
|
304
|
+
if (SYSTEM_KEYS.includes(key)) continue;
|
|
305
|
+
if (customFields.includes(key)) continue;
|
|
306
|
+
if ([...skipFields, 'wishlist', 'availableSlot'].includes(key)) continue;
|
|
307
|
+
|
|
308
|
+
if (componentFields.includes(key)) {
|
|
309
|
+
for (const subKey in value) {
|
|
310
|
+
if (subKey === 'id') continue;
|
|
311
|
+
result[`${key}_${subKey}`] = value[subKey];
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (value === null || typeof value !== 'object') {
|
|
317
|
+
result[key] = value;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!Array.isArray(value) && typeof value === 'object') {
|
|
322
|
+
let temp = handleObject(key, value);
|
|
323
|
+
if (temp !== undefined) {
|
|
324
|
+
result[key] = temp;
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (Array.isArray(value)) {
|
|
330
|
+
if (value.length > 0 && typeof value[0] === 'object') {
|
|
331
|
+
let arrValue = [];
|
|
332
|
+
for (const subValue in value) {
|
|
333
|
+
arrValue.push(handleObject(key, value[subValue]));
|
|
334
|
+
}
|
|
335
|
+
result[key] = arrValue;
|
|
336
|
+
} else {
|
|
337
|
+
result[key] = value;
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
} else {
|
|
344
|
+
return obj; // primitive
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Example usage
|
|
348
|
+
const cleaned = cleanAndFlatten(entry);
|
|
349
|
+
return cleaned;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
function flattenForXLSX(obj) {
|
|
353
|
+
const result = {};
|
|
354
|
+
for (const key in obj) {
|
|
355
|
+
const value = obj[key];
|
|
356
|
+
if (Array.isArray(value)) {
|
|
357
|
+
result[key] = value.join("|");
|
|
358
|
+
} else {
|
|
359
|
+
result[key] = value;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
const cleanedFlat = cleanedEntries.map(entry => flattenForXLSX(entry));
|
|
365
|
+
const worksheet = XLSX.utils.json_to_sheet(cleanedFlat);
|
|
366
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
367
|
+
} else {
|
|
368
|
+
// Create empty sheet with headers if no data
|
|
369
|
+
const worksheet = XLSX.utils.json_to_sheet([{ message: 'No data found' }]);
|
|
370
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
371
|
+
hasData = true; // Prevent empty workbook error
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// If still no data, create a default sheet
|
|
376
|
+
if (!hasData) {
|
|
377
|
+
const worksheet = XLSX.utils.json_to_sheet([{ message: 'No data to export' }]);
|
|
378
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, 'NoData');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async exportSingleEntry(contentType, entryId) {
|
|
385
|
+
try {
|
|
386
|
+
const entry = await strapi.entityService.findOne(contentType, entryId, {
|
|
387
|
+
populate: '*',
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (!entry) {
|
|
391
|
+
throw new Error('Entry not found');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const exportData = {
|
|
395
|
+
version: strapi.config.get('info.strapi'),
|
|
396
|
+
timestamp: new Date().toISOString(),
|
|
397
|
+
data: {
|
|
398
|
+
[contentType]: [entry]
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
return this.convertToExcel(exportData.data);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
});
|