@nkhang1902/strapi-plugin-export-import-clsx 1.0.4 → 1.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkhang1902/strapi-plugin-export-import-clsx",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "A powerful Strapi plugin for exporting and importing data with Excel support and advanced filtering",
5
5
  "main": "./strapi-server.js",
6
6
  "scripts": {
@@ -5,18 +5,6 @@ function toCamel(str) {
5
5
  return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
6
6
  }
7
7
 
8
- const SYSTEM_KEYS = [
9
- 'documentId',
10
- 'locale',
11
- 'createdAt',
12
- 'updatedAt',
13
- 'publishedAt',
14
- 'createdBy',
15
- 'updatedBy',
16
- 'localizations',
17
- 'status'
18
- ];
19
-
20
8
  const SHORTCUT_FIELDS = [
21
9
  'email','businessEmail','name','title','tickerCode',
22
10
  ]
@@ -56,18 +44,6 @@ function transformExcelData(filePath) {
56
44
  const workbook = XLSX.readFile(filePath);
57
45
  const importData = {};
58
46
 
59
- const parseJsonIfNeeded = (value) => {
60
- if (typeof value !== 'string') return value;
61
- const trimmed = value.trim();
62
- if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return value;
63
-
64
- try {
65
- return JSON.parse(trimmed);
66
- } catch {
67
- return value; // keep as string if invalid JSON
68
- }
69
- };
70
-
71
47
  const isComponentField = (key) => {
72
48
  const parts = key.split('_');
73
49
  return parts.length === 2; // exactly one underscore
@@ -83,13 +59,13 @@ function transformExcelData(filePath) {
83
59
  if (value === null || value === undefined || value === '') {
84
60
  rowData[key] = null
85
61
  } else if (attr[key] && attr[key].customField && attr[key].type === 'json' && attr[key].default === '[]') {
86
- rowData[key] = parseJsonIfNeeded(value).split('|');
62
+ rowData[key] = value.split('|');
87
63
  } else if (isComponentField(key)) {
88
64
  const [comp, field] = key.split('_');
89
65
  if (!rowData[comp]) rowData[comp] = {};
90
- rowData[comp][field] = parseJsonIfNeeded(value);
66
+ rowData[comp][field] = value;
91
67
  } else {
92
- rowData[key] = parseJsonIfNeeded(value);
68
+ rowData[key] = value;
93
69
  }
94
70
  }
95
71
  result.push(rowData);
@@ -258,55 +234,101 @@ function handleComponents(data, existing, contentType) {
258
234
  return data;
259
235
  }
260
236
 
261
- function hasChanges(existing, incoming) {
262
- if (!incoming || typeof incoming !== "object") return false;
263
- if (!existing || typeof existing !== "object") return true;
264
- for (const key of Object.keys(incoming)) {
265
- // Skip system keys
266
- if (SYSTEM_KEYS.includes(key)) continue;
267
- const newVal = incoming[key];
268
- const oldVal = existing[key];
269
-
270
- // If incoming defines a field but existing doesn't → change
271
- if (oldVal === undefined || newVal === undefined) {
272
- continue;
273
- }
237
+ function sanitizeEntryBeforeWrite(data, uid, path = '', errors = []) {
238
+ const schema = strapi.contentTypes[uid];
239
+ const cleaned = {};
240
+
241
+ for (const [key, attr] of Object.entries(schema.attributes)) {
242
+ const value = data[key];
243
+ const fieldPath = path ? `${path}.${key}` : key;
274
244
 
275
- // Primitive comparison
276
- if (newVal === null || typeof newVal !== "object") {
277
- if (oldVal !== newVal) {
278
- return true;
245
+ if (value === undefined) continue;
246
+
247
+ if (attr.type === 'component') {
248
+ if (!value) {
249
+ cleaned[key] = attr.repeatable ? [] : null;
250
+ continue;
279
251
  }
252
+
253
+ cleaned[key] = attr.repeatable
254
+ ? value.map((v, i) =>
255
+ sanitizeComponent(v, attr.component, `${fieldPath}[${i}]`, errors)
256
+ )
257
+ : sanitizeComponent(value, attr.component, fieldPath, errors);
280
258
  continue;
281
259
  }
282
260
 
283
- // ARRAY comparison
284
- if (Array.isArray(newVal)) {
285
- if (!Array.isArray(oldVal)) return true;
286
- if (newVal.length !== oldVal.length) return true;
287
- // Compare values shallowly
288
- for (let i = 0; i < newVal.length; i++) {
289
- if (typeof newVal[i] === "object" && typeof oldVal[i] === "object" && hasChanges(oldVal[i], newVal[i])) {
290
- return true;
291
- } else if (typeof newVal[i] !== "object" && typeof oldVal[i] !== "object" && newVal[i] !== oldVal[i]) {
292
- return true;
293
- }
294
- }
295
- continue;
261
+ if (attr.type === 'relation') {
262
+ cleaned[key] = value;
263
+ } else {
264
+ cleaned[key] = sanitizePrimitive(value, attr, fieldPath, errors);
296
265
  }
266
+ }
297
267
 
298
- // OBJECT comparison (recursive, but ONLY fields in incoming object)
299
- if (typeof newVal === "object" && typeof oldVal === "object") {
300
- if (hasChanges(oldVal, newVal)) {
301
- return true;
302
- }
303
- continue;
268
+ return cleaned;
269
+ }
270
+
271
+ function sanitizeComponent(data, uid, path, errors) {
272
+ const schema = strapi.components[uid];
273
+ if (!schema) return data;
274
+
275
+ const cleaned = {};
276
+
277
+ for (const [key, attr] of Object.entries(schema.attributes)) {
278
+ const value = data?.[key];
279
+ const fieldPath = `${path}.${key}`;
280
+
281
+ if (attr.type === 'component') {
282
+ cleaned[key] = attr.repeatable
283
+ ? (value || []).map((v, i) =>
284
+ sanitizeComponent(v, attr.component, `${fieldPath}[${i}]`, errors)
285
+ )
286
+ : sanitizeComponent(value, attr.component, fieldPath, errors);
287
+ } else {
288
+ cleaned[key] = sanitizePrimitive(value, attr, fieldPath, errors);
304
289
  }
305
290
  }
306
291
 
307
- return false;
292
+ return cleaned;
308
293
  }
309
294
 
295
+ function sanitizePrimitive(value, attr, path, errors) {
296
+ if (value === null || value === undefined || value === '') return null;
297
+
298
+ switch (attr.type) {
299
+ case 'string':
300
+ case 'text':
301
+ case 'richtext':
302
+ case 'email':
303
+ return String(value).trim();
304
+
305
+ case 'boolean':
306
+ if ([true, 'true', 1, '1', 'yes', 'y'].includes(value)) return true;
307
+ if ([false, 'false', 0, '0', 'no', 'n'].includes(value)) return false;
308
+ return false;
309
+
310
+ case 'integer':
311
+ case 'biginteger': {
312
+ const i = parseInt(value, 10);
313
+ return Number.isNaN(i) ? 0 : i;
314
+ }
315
+
316
+ case 'float':
317
+ case 'decimal': {
318
+ const f = parseFloat(value);
319
+ return Number.isNaN(f) ? 0 : f;
320
+ }
321
+
322
+ case 'date':
323
+ case 'datetime': {
324
+ const d = new Date(value);
325
+ return isNaN(d.getTime()) ? null : d.toISOString();
326
+ }
327
+
328
+ default:
329
+ return value;
330
+ }
331
+ }
310
332
 
311
333
  async function bulkInsertData(importData) {
312
334
  const results = {
@@ -367,21 +389,26 @@ async function importEntries(entries, contentType) {
367
389
  }
368
390
 
369
391
  // Handle relations & components
370
- data = await handleRelations(data, contentType, trx);
392
+ data = await handleRelations(data, contentType);
371
393
  data = await handleComponents(data, existing, contentType);
372
394
 
395
+ const sanitizeErrors = [];
396
+ data = sanitizeEntryBeforeWrite(data, contentType, '', sanitizeErrors);
397
+
398
+ if (sanitizeErrors.length) {
399
+ throw new Error(`Data validation failed:\n${sanitizeErrors.join('\n')}`);
400
+ }
401
+
373
402
  // Update
374
403
  if (existing) {
375
- if (hasChanges(existing, data)) {
376
- await strapi.documents(contentType).update(
377
- {
378
- documentId: existing.documentId,
379
- data,
380
- },
381
- { transaction: trx }
382
- );
383
- results.updated++;
384
- }
404
+ await strapi.documents(contentType).update(
405
+ {
406
+ documentId: existing.documentId,
407
+ data,
408
+ },
409
+ { transaction: trx }
410
+ );
411
+ results.updated++;
385
412
  }
386
413
 
387
414
  // Create