@tunghtml/strapi-plugin-export-import-clsx 1.0.2 → 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,103 +42,98 @@ 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);
45
+ const parsedFilters = this.parseFilters(rawFilters);
46
+
47
+ if (rawFilters["_q"]) {
48
+ parsedFilters._q = rawFilters["_q"];
49
+ }
25
50
 
26
- strapi.log.info(`Exporting ${ct} with raw filters:`, filters);
27
- strapi.log.info(`Parsed filters:`, parsedFilters);
28
- strapi.log.info(`Selected IDs:`, selectedIds);
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 = [];
58
+ let filters = parsedFilters.filters;
31
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
- // 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
- }
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
+ }
97
104
 
98
- entries = Array.from(uniqueEntries.values());
99
- strapi.log.info(`Found ${allEntries.length} total entries, ${entries.length} unique entries after deduplication`);
105
+ if (numberSearchable.length > 0 && !isNaN(parsedFilters._q)) {
106
+ orConditions.push(
107
+ ...numberSearchable.map((field) => ({
108
+ [field]: { $eq: Number(parsedFilters._q) },
109
+ }))
110
+ );
111
+ }
100
112
 
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`);
113
+ if (orConditions.length > 0) {
114
+ filters = {
115
+ ...filters,
116
+ $and: [...(filters?.$and || []), { $or: orConditions }],
117
+ };
114
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
+ );
115
129
  } catch (error) {
116
130
  strapi.log.error(`Failed to query entries:`, error);
117
131
  }
118
132
  }
119
133
 
120
- strapi.log.info(`Final result: ${entries?.length || 0} entries for ${ct} (total found: ${entries?.length || 0})`);
134
+ strapi.log.info(
135
+ `Final result: ${entries?.length || 0} entries for ${ct} (total found: ${entries?.length || 0})`
136
+ );
121
137
 
122
138
  if (entries && entries.length > 0) {
123
139
  exportData.data[ct] = entries;
@@ -126,39 +142,90 @@ module.exports = ({ strapi }) => ({
126
142
  strapi.log.error(`Failed to export ${ct}:`, error);
127
143
  }
128
144
  }
129
-
130
- if (format === 'excel') {
131
- console.log(exportData.data)
145
+
146
+ if (format === "excel") {
132
147
  return this.convertToExcel(exportData.data);
133
148
  }
134
149
 
135
150
  return exportData;
136
151
  },
137
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
+
138
192
  parseFilters(filters) {
139
193
  const parsed = {};
140
194
  for (const [key, value] of Object.entries(filters)) {
141
195
  // Skip pagination and sorting params
142
- 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
+ ) {
143
207
  continue;
144
208
  }
145
209
 
146
210
  // Handle URL encoded filter format like filters[$and][0][shortName][$contains]
147
- if (key.startsWith('filters[')) {
211
+ if (key.startsWith("filters[")) {
148
212
  // Extract the actual filter structure
149
- const match = key.match(/filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/);
213
+ const match = key.match(
214
+ /filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/
215
+ );
150
216
  if (match) {
151
217
  const [, operator, index, field, condition] = match;
152
218
 
153
219
  if (!parsed.filters) parsed.filters = {};
154
220
 
155
- if (operator === '$and') {
221
+ if (operator === "$and") {
156
222
  if (!parsed.filters.$and) parsed.filters.$and = [];
157
223
  const idx = parseInt(index) || 0;
158
224
  if (!parsed.filters.$and[idx]) parsed.filters.$and[idx] = {};
159
225
 
160
226
  if (condition) {
161
- if (!parsed.filters.$and[idx][field]) parsed.filters.$and[idx][field] = {};
227
+ if (!parsed.filters.$and[idx][field])
228
+ parsed.filters.$and[idx][field] = {};
162
229
  parsed.filters.$and[idx][field][condition] = value;
163
230
  } else {
164
231
  parsed.filters.$and[idx][field] = value;
@@ -173,95 +240,74 @@ module.exports = ({ strapi }) => ({
173
240
  return parsed;
174
241
  },
175
242
 
176
- applyClientSideFilters(entries, filters) {
177
- if (!filters || Object.keys(filters).length === 0) {
178
- return entries;
179
- }
180
-
181
- const filtered = entries.filter(entry => {
182
- // Handle structured filters
183
- if (filters.filters && filters.filters.$and) {
184
- for (const condition of filters.filters.$and) {
185
- for (const [field, criteria] of Object.entries(condition)) {
186
- if (typeof criteria === 'object' && criteria.$contains) {
187
- // Handle $contains filter
188
- if (entry[field]) {
189
- const fieldValue = String(entry[field]).toLowerCase();
190
- const searchValue = String(criteria.$contains).toLowerCase();
191
- if (!fieldValue.includes(searchValue)) {
192
- return false;
193
- }
194
- } else {
195
- return false; // Field doesn't exist, exclude entry
196
- }
197
- } else {
198
- // Handle exact match
199
- if (entry[field] !== criteria) {
200
- return false;
201
- }
202
- }
203
- }
204
- }
205
- }
206
- // Handle other filter formats
207
- for (const [key, value] of Object.entries(filters)) {
208
- if (key === 'filters') continue; // Already handled above
209
-
210
- // Handle simple search (global search)
211
- if (key === '_q' || key === 'search') {
212
- // Global search across main fields
213
- const searchFields = ['shortName', 'name', 'title'];
214
- const searchValue = String(value).toLowerCase();
215
- const found = searchFields.some(field => {
216
- if (entry[field]) {
217
- return String(entry[field]).toLowerCase().includes(searchValue);
218
- }
219
- return false;
220
- });
221
- if (!found) {
222
- return false;
223
- }
224
- }
225
- }
226
- return true;
227
- });
228
-
229
- return filtered;
230
- },
231
-
232
243
  convertToExcel(data) {
233
244
  const workbook = XLSX.utils.book_new();
234
245
  let hasData = false;
235
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
+
236
266
  for (const [contentType, entries] of Object.entries(data)) {
237
267
  // Clean sheet name (Excel has restrictions)
238
- const sheetName = contentType.replace(/[^\w\s]/gi, '_').substring(0, 31);
268
+ const sheetName = contentType
269
+ .split(".")
270
+ .pop()
271
+ .replace(/[^\w\s-]/gi, "_")
272
+ .substring(0, 31);
273
+
239
274
  if (entries && entries.length > 0) {
240
275
  hasData = true;
241
276
 
242
277
  const attr = strapi.contentTypes[contentType].attributes;
243
278
  const customFields = Object.entries(attr)
244
279
  .filter(([key, definition]) => definition.customField)
245
- .map(([key, definition]) => key);
246
-
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];
300
+ }
301
+ }
302
+ }
303
+ return undefined;
304
+ }
247
305
  // Clean and flatten entries for Excel
248
- const cleanedEntries = entries.map(entry => {
249
- const SYSTEM_KEYS = [
250
- 'documentId',
251
- 'locale',
252
- 'createdAt',
253
- 'updatedAt',
254
- 'publishedAt',
255
- 'createdBy',
256
- 'updatedBy',
257
- 'localizations',
258
- 'status'
259
- ];
260
-
306
+ const cleanedEntries = entries.map((entry) => {
261
307
  function cleanAndFlatten(obj) {
262
308
  if (Array.isArray(obj)) {
263
309
  return obj.map(cleanAndFlatten);
264
- } else if (obj !== null && typeof obj === 'object') {
310
+ } else if (obj !== null && typeof obj === "object") {
265
311
  const result = {};
266
312
 
267
313
  for (const key in obj) {
@@ -270,35 +316,42 @@ module.exports = ({ strapi }) => ({
270
316
  // Skip system keys
271
317
  if (SYSTEM_KEYS.includes(key)) continue;
272
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
+ }
273
329
 
274
- // Null or primitive
275
- if (value === null || typeof value !== 'object') {
330
+ if (value === null || typeof value !== "object") {
276
331
  result[key] = value;
277
332
  continue;
278
333
  }
279
334
 
280
- // Array handling
281
- if (Array.isArray(value)) {
282
- // Array of objects
283
- if (value.length > 0 && typeof value[0] === 'object') {
284
- result[key] = value.map(cleanAndFlatten);
285
- } else {
286
- // Array of primitives
287
- result[key] = value;
335
+ if (!Array.isArray(value) && typeof value === "object") {
336
+ let temp = handleObject(key, value);
337
+ if (temp !== undefined) {
338
+ result[key] = temp;
288
339
  }
289
340
  continue;
290
341
  }
291
342
 
292
- // Component (no documentId)
293
- if (!('documentId' in value)) {
294
- for (const subKey in value) {
295
- if (subKey === 'id') continue; // skip id
296
- result[`${key}_${subKey}`] = value[subKey];
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;
297
352
  }
298
- continue; // skip keeping the original key
353
+ continue;
299
354
  }
300
- // Relation object (has documentId)
301
- result[key] = cleanAndFlatten(value);
302
355
  }
303
356
  return result;
304
357
  } else {
@@ -315,19 +368,23 @@ module.exports = ({ strapi }) => ({
315
368
  for (const key in obj) {
316
369
  const value = obj[key];
317
370
  if (Array.isArray(value)) {
318
- result[key] = JSON.stringify(value);
371
+ result[key] = value.join("|");
319
372
  } else {
320
373
  result[key] = value;
321
374
  }
322
375
  }
323
376
  return result;
324
377
  }
325
- const cleanedFlat = cleanedEntries.map(entry => flattenForXLSX(entry));
378
+ const cleanedFlat = cleanedEntries.map((entry) =>
379
+ flattenForXLSX(entry)
380
+ );
326
381
  const worksheet = XLSX.utils.json_to_sheet(cleanedFlat);
327
382
  XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
328
383
  } else {
329
384
  // Create empty sheet with headers if no data
330
- 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
+ ]);
331
388
  XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
332
389
  hasData = true; // Prevent empty workbook error
333
390
  }
@@ -335,28 +392,30 @@ module.exports = ({ strapi }) => ({
335
392
 
336
393
  // If still no data, create a default sheet
337
394
  if (!hasData) {
338
- const worksheet = XLSX.utils.json_to_sheet([{ message: 'No data to export' }]);
339
- 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");
340
399
  }
341
400
 
342
- return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
401
+ return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
343
402
  },
344
403
 
345
404
  async exportSingleEntry(contentType, entryId) {
346
405
  try {
347
406
  const entry = await strapi.entityService.findOne(contentType, entryId, {
348
- populate: '*',
407
+ populate: "*",
349
408
  });
350
409
 
351
410
  if (!entry) {
352
- throw new Error('Entry not found');
411
+ throw new Error("Entry not found");
353
412
  }
354
413
 
355
414
  const exportData = {
356
- version: strapi.config.get('info.strapi'),
415
+ version: strapi.config.get("info.strapi"),
357
416
  timestamp: new Date().toISOString(),
358
417
  data: {
359
- [contentType]: [entry]
418
+ [contentType]: [entry],
360
419
  },
361
420
  };
362
421
 
@@ -365,4 +424,4 @@ module.exports = ({ strapi }) => ({
365
424
  throw error;
366
425
  }
367
426
  },
368
- });
427
+ });