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