@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.
@@ -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
+ });