@tunghtml/strapi-plugin-export-import-clsx 1.0.1 → 1.0.2

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": "@tunghtml/strapi-plugin-export-import-clsx",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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": {
@@ -57,4 +57,4 @@
57
57
  "kind": "plugin",
58
58
  "category": "data-management"
59
59
  }
60
- }
60
+ }
@@ -12,7 +12,7 @@ module.exports = ({ strapi }) => ({
12
12
 
13
13
  const importService = strapi.plugin('export-import-clsx').service('import-service');
14
14
 
15
- const result = await importService.importData(file, targetContentType);
15
+ const result = await importService.importData(file);
16
16
 
17
17
  // Create appropriate message based on results
18
18
  let message = 'Import completed successfully';
@@ -22,13 +22,13 @@ module.exports = ({ strapi }) => ({
22
22
  try {
23
23
  // Parse filters from URL format
24
24
  const parsedFilters = this.parseFilters(filters);
25
-
25
+
26
26
  strapi.log.info(`Exporting ${ct} with raw filters:`, filters);
27
27
  strapi.log.info(`Parsed filters:`, parsedFilters);
28
28
  strapi.log.info(`Selected IDs:`, selectedIds);
29
29
 
30
30
  let entries = [];
31
-
31
+
32
32
  // If specific IDs are selected, export only those
33
33
  if (selectedIds && selectedIds.length > 0) {
34
34
  try {
@@ -74,22 +74,18 @@ module.exports = ({ strapi }) => ({
74
74
  populate: '*',
75
75
  // Don't specify status to get all
76
76
  });
77
-
78
77
  // Group by documentId and keep only the best version (published > modified draft > draft)
79
78
  const uniqueEntries = new Map();
80
-
81
79
  for (const entry of allEntries) {
82
80
  const docId = entry.documentId;
83
81
  const isPublished = !!entry.publishedAt;
84
82
  const isModified = entry.updatedAt !== entry.createdAt;
85
-
86
83
  if (!uniqueEntries.has(docId)) {
87
84
  uniqueEntries.set(docId, entry);
88
85
  } else {
89
86
  const existing = uniqueEntries.get(docId);
90
87
  const existingIsPublished = !!existing.publishedAt;
91
88
  const existingIsModified = existing.updatedAt !== existing.createdAt;
92
-
93
89
  // Priority: published > modified draft > draft
94
90
  if (isPublished && !existingIsPublished) {
95
91
  uniqueEntries.set(docId, entry);
@@ -98,10 +94,10 @@ module.exports = ({ strapi }) => ({
98
94
  }
99
95
  }
100
96
  }
101
-
97
+
102
98
  entries = Array.from(uniqueEntries.values());
103
99
  strapi.log.info(`Found ${allEntries.length} total entries, ${entries.length} unique entries after deduplication`);
104
-
100
+
105
101
  // Apply filters
106
102
  if (parsedFilters && Object.keys(parsedFilters).length > 0) {
107
103
  strapi.log.info('Applying filters:', parsedFilters);
@@ -120,9 +116,9 @@ module.exports = ({ strapi }) => ({
120
116
  strapi.log.error(`Failed to query entries:`, error);
121
117
  }
122
118
  }
123
-
119
+
124
120
  strapi.log.info(`Final result: ${entries?.length || 0} entries for ${ct} (total found: ${entries?.length || 0})`);
125
-
121
+
126
122
  if (entries && entries.length > 0) {
127
123
  exportData.data[ct] = entries;
128
124
  }
@@ -130,8 +126,9 @@ module.exports = ({ strapi }) => ({
130
126
  strapi.log.error(`Failed to export ${ct}:`, error);
131
127
  }
132
128
  }
133
-
129
+
134
130
  if (format === 'excel') {
131
+ console.log(exportData.data)
135
132
  return this.convertToExcel(exportData.data);
136
133
  }
137
134
 
@@ -140,27 +137,26 @@ module.exports = ({ strapi }) => ({
140
137
 
141
138
  parseFilters(filters) {
142
139
  const parsed = {};
143
-
144
140
  for (const [key, value] of Object.entries(filters)) {
145
141
  // Skip pagination and sorting params
146
142
  if (['page', 'pageSize', 'sort', 'locale', 'format', 'contentType', 'selectedIds'].includes(key)) {
147
143
  continue;
148
144
  }
149
-
145
+
150
146
  // Handle URL encoded filter format like filters[$and][0][shortName][$contains]
151
147
  if (key.startsWith('filters[')) {
152
148
  // Extract the actual filter structure
153
149
  const match = key.match(/filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/);
154
150
  if (match) {
155
151
  const [, operator, index, field, condition] = match;
156
-
152
+
157
153
  if (!parsed.filters) parsed.filters = {};
158
-
154
+
159
155
  if (operator === '$and') {
160
156
  if (!parsed.filters.$and) parsed.filters.$and = [];
161
157
  const idx = parseInt(index) || 0;
162
158
  if (!parsed.filters.$and[idx]) parsed.filters.$and[idx] = {};
163
-
159
+
164
160
  if (condition) {
165
161
  if (!parsed.filters.$and[idx][field]) parsed.filters.$and[idx][field] = {};
166
162
  parsed.filters.$and[idx][field][condition] = value;
@@ -173,7 +169,7 @@ module.exports = ({ strapi }) => ({
173
169
  parsed[key] = value;
174
170
  }
175
171
  }
176
-
172
+
177
173
  return parsed;
178
174
  },
179
175
 
@@ -192,7 +188,6 @@ module.exports = ({ strapi }) => ({
192
188
  if (entry[field]) {
193
189
  const fieldValue = String(entry[field]).toLowerCase();
194
190
  const searchValue = String(criteria.$contains).toLowerCase();
195
-
196
191
  if (!fieldValue.includes(searchValue)) {
197
192
  return false;
198
193
  }
@@ -208,30 +203,26 @@ module.exports = ({ strapi }) => ({
208
203
  }
209
204
  }
210
205
  }
211
-
212
206
  // Handle other filter formats
213
207
  for (const [key, value] of Object.entries(filters)) {
214
208
  if (key === 'filters') continue; // Already handled above
215
-
209
+
216
210
  // Handle simple search (global search)
217
211
  if (key === '_q' || key === 'search') {
218
212
  // Global search across main fields
219
213
  const searchFields = ['shortName', 'name', 'title'];
220
214
  const searchValue = String(value).toLowerCase();
221
-
222
215
  const found = searchFields.some(field => {
223
216
  if (entry[field]) {
224
217
  return String(entry[field]).toLowerCase().includes(searchValue);
225
218
  }
226
219
  return false;
227
220
  });
228
-
229
221
  if (!found) {
230
222
  return false;
231
223
  }
232
224
  }
233
225
  }
234
-
235
226
  return true;
236
227
  });
237
228
 
@@ -245,53 +236,94 @@ module.exports = ({ strapi }) => ({
245
236
  for (const [contentType, entries] of Object.entries(data)) {
246
237
  // Clean sheet name (Excel has restrictions)
247
238
  const sheetName = contentType.replace(/[^\w\s]/gi, '_').substring(0, 31);
248
-
249
239
  if (entries && entries.length > 0) {
250
240
  hasData = true;
251
-
241
+
242
+ const attr = strapi.contentTypes[contentType].attributes;
243
+ const customFields = Object.entries(attr)
244
+ .filter(([key, definition]) => definition.customField)
245
+ .map(([key, definition]) => key);
246
+
252
247
  // Clean and flatten entries for Excel
253
248
  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;
274
- }
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') {
249
+ const SYSTEM_KEYS = [
250
+ 'documentId',
251
+ 'locale',
252
+ 'createdAt',
253
+ 'updatedAt',
254
+ 'publishedAt',
255
+ 'createdBy',
256
+ 'updatedBy',
257
+ 'localizations',
258
+ 'status'
259
+ ];
260
+
261
+ function cleanAndFlatten(obj) {
262
+ if (Array.isArray(obj)) {
263
+ return obj.map(cleanAndFlatten);
264
+ } else if (obj !== null && typeof obj === 'object') {
265
+ const result = {};
266
+
267
+ for (const key in obj) {
268
+ const value = obj[key];
269
+
270
+ // Skip system keys
271
+ if (SYSTEM_KEYS.includes(key)) continue;
272
+ if (customFields.includes(key)) continue;
273
+
274
+ // Null or primitive
275
+ if (value === null || typeof value !== 'object') {
276
+ result[key] = value;
279
277
  continue;
280
278
  }
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];
279
+
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;
288
+ }
289
+ continue;
290
+ }
291
+
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];
297
+ }
298
+ continue; // skip keeping the original key
299
+ }
300
+ // Relation object (has documentId)
301
+ result[key] = cleanAndFlatten(value);
286
302
  }
303
+ return result;
304
+ } else {
305
+ return obj; // primitive
287
306
  }
288
- };
289
-
290
- flatten(entryWithSystemFields);
291
- return flattened;
307
+ }
308
+ // Example usage
309
+ const cleaned = cleanAndFlatten(entry);
310
+ return cleaned;
292
311
  });
293
312
 
294
- const worksheet = XLSX.utils.json_to_sheet(cleanedEntries);
313
+ function flattenForXLSX(obj) {
314
+ const result = {};
315
+ for (const key in obj) {
316
+ const value = obj[key];
317
+ if (Array.isArray(value)) {
318
+ result[key] = JSON.stringify(value);
319
+ } else {
320
+ result[key] = value;
321
+ }
322
+ }
323
+ return result;
324
+ }
325
+ const cleanedFlat = cleanedEntries.map(entry => flattenForXLSX(entry));
326
+ const worksheet = XLSX.utils.json_to_sheet(cleanedFlat);
295
327
  XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
296
328
  } else {
297
329
  // Create empty sheet with headers if no data
@@ -1,306 +1,358 @@
1
- const fs = require('fs');
2
1
  const XLSX = require('xlsx');
2
+ const fs = require('fs');
3
3
 
4
- module.exports = ({ strapi }) => ({
5
- async importData(file, targetContentType = null) {
6
- try {
7
- let importData;
8
-
9
- // Check file extension
10
- const fileName = file.name || file.originalFilename || 'unknown.json';
11
- const fileExtension = fileName.split('.').pop().toLowerCase();
12
-
13
- const filePath = file.path || file.filepath;
14
-
15
- if (!filePath) {
16
- throw new Error('File path not found');
17
- }
4
+ function toCamel(str) {
5
+ return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
6
+ }
7
+ const SYSTEM_KEYS = [
8
+ 'documentId',
9
+ 'locale',
10
+ 'createdAt',
11
+ 'updatedAt',
12
+ 'publishedAt',
13
+ 'createdBy',
14
+ 'updatedBy',
15
+ 'localizations',
16
+ 'status'
17
+ ];
18
+
19
+ async function importData(file) {
20
+ let result;
21
+ try {
22
+ let importData;
23
+ // Check file extension
24
+ const fileName = file.name || file.originalFilename || 'unknown.json';
25
+ const fileExtension = fileName.split('.').pop().toLowerCase();
26
+ const filePath = file.path || file.filepath;
27
+ if (!filePath) {
28
+ throw new Error('File path not found');
29
+ }
30
+
31
+ if (fileExtension === 'json') {
32
+ const fileContent = fs.readFileSync(filePath, 'utf8');
33
+ importData = JSON.parse(fileContent);
34
+ strapi.log.info('Parsed JSON data:', Object.keys(importData));
35
+ } else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
36
+ importData = transformExcelData(filePath);
37
+ }
38
+ result = await bulkInsertData(importData);
39
+ return result;
40
+ } catch (error) {
41
+ // Clean up uploaded file on error
42
+ const filePath = file && (file.path || file.filepath);
43
+ if (filePath && fs.existsSync(filePath)) {
44
+ fs.unlinkSync(filePath);
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+
50
+
51
+ function transformExcelData(filePath) {
52
+ const workbook = XLSX.readFile(filePath);
53
+ const importData = {};
18
54
 
19
- if (fileExtension === 'json') {
20
- const fileContent = fs.readFileSync(filePath, 'utf8');
21
- importData = JSON.parse(fileContent);
22
- strapi.log.info('Parsed JSON data:', Object.keys(importData));
23
- } else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
24
- const workbook = XLSX.readFile(filePath);
25
- importData = {};
26
-
27
- strapi.log.info('Excel sheet names:', workbook.SheetNames);
28
-
29
- // Convert each sheet to data
30
- workbook.SheetNames.forEach(sheetName => {
31
- const worksheet = workbook.Sheets[sheetName];
32
- const jsonData = XLSX.utils.sheet_to_json(worksheet);
33
- strapi.log.info(`Sheet ${sheetName} has ${jsonData.length} rows`);
34
- if (jsonData.length > 0) {
35
- // Map sheet name back to content type
36
- let contentTypeName = sheetName;
37
- if (sheetName.startsWith('api__')) {
38
- // Convert api__corporate_corporate to api::corporate.corporate
39
- contentTypeName = sheetName.replace(/^api__/, 'api::').replace(/_/g, '.');
55
+ const parseJsonIfNeeded = (value) => {
56
+ if (typeof value !== 'string') return value;
57
+ const trimmed = value.trim();
58
+ if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) return value;
59
+
60
+ try {
61
+ return JSON.parse(trimmed);
62
+ } catch {
63
+ return value; // keep as string if invalid JSON
64
+ }
65
+ };
66
+
67
+ const isComponentField = (key) => {
68
+ const parts = key.split('_');
69
+ return parts.length === 2; // exactly one underscore
70
+ };
71
+
72
+ function unflattenRow(rows, targetContentType) {
73
+ const result = [];
74
+ for (const row of rows) {
75
+ const rowData = {};
76
+
77
+ for (const [key, value] of Object.entries(row)) {
78
+ if (value === null || value === undefined || value === '') {
79
+ rowData[key] = null
40
80
  }
41
-
42
- // Unflatten Excel data back to nested objects
43
- const unflattened = jsonData.map((row, index) => {
44
- const result = {};
45
-
46
- // Known component prefixes that should be unflattened
47
- const componentPrefixes = ['corporateInfo', 'meetingRequirements', 'internalInfo'];
48
-
49
- for (const [key, value] of Object.entries(row)) {
50
- // Skip completely empty values but keep 0, false, etc.
51
- if (value === null || value === undefined || value === '') {
52
- continue;
53
- }
54
-
55
- // Check if this key should be unflattened
56
- const shouldUnflatten = key.includes('_') &&
57
- !['createdAt', 'updatedAt', 'publishedAt'].includes(key) &&
58
- componentPrefixes.some(prefix => key.startsWith(prefix + '_'));
59
-
60
- if (shouldUnflatten) {
61
- // Handle nested objects like corporateInfo_companyName
62
- const parts = key.split('_');
63
- let current = result;
64
-
65
- for (let i = 0; i < parts.length - 1; i++) {
66
- if (!current[parts[i]]) {
67
- current[parts[i]] = {};
68
- }
69
- current = current[parts[i]];
70
- }
71
-
72
- current[parts[parts.length - 1]] = value;
73
- } else {
74
- // Handle arrays/JSON strings
75
- if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
76
- try {
77
- result[key] = JSON.parse(value);
78
- } catch (error) {
79
- result[key] = value; // Keep as string if parsing fails
80
- }
81
- } else {
82
- result[key] = value;
83
- }
84
- }
85
- }
86
-
87
- // Debug info removed for cleaner logs
88
-
89
- return result;
90
- });
91
-
92
- importData[contentTypeName] = unflattened;
93
- strapi.log.info(`Mapped sheet ${sheetName} to ${contentTypeName}`);
81
+
82
+ if (isComponentField(key)) {
83
+ const [comp, field] = key.split('_');
84
+
85
+ if (!rowData[comp]) rowData[comp] = {};
86
+ rowData[comp][field] = parseJsonIfNeeded(value);
87
+
88
+ } else {
89
+ rowData[key] = parseJsonIfNeeded(value);
90
+ }
91
+ }
92
+
93
+ existedComponents = getComponentFields(targetContentType);
94
+ for (const comp of existedComponents) {
95
+ if (!rowData[comp]) {
96
+ rowData[comp] = {};
94
97
  }
95
- });
96
-
97
- strapi.log.info('Final import data keys:', Object.keys(importData));
98
- } else {
99
- throw new Error('Unsupported file format. Please use JSON or Excel files.');
98
+ }
99
+
100
+ result.push(rowData);
100
101
  }
101
102
 
102
- const results = {
103
- created: 0,
104
- updated: 0,
105
- errors: [],
106
- };
103
+ return result;
104
+ };
107
105
 
108
- // Handle different data structures
109
- const dataToProcess = importData.data || importData;
106
+ const mapSheetNameToContentType = (sheetName) => {
107
+ if (!sheetName.startsWith('api__')) return sheetName;
108
+ return sheetName.replace(/^api__/, 'api::').replace(/_/g, '.');
109
+ };
110
110
 
111
- for (const [contentType, entries] of Object.entries(dataToProcess)) {
112
- try {
113
- // If targetContentType is specified, only process that content type
114
- if (targetContentType && contentType !== targetContentType) {
115
- continue;
116
- }
111
+ workbook.SheetNames.forEach(sheetName => {
112
+ const worksheet = workbook.Sheets[sheetName];
113
+ const rows = XLSX.utils.sheet_to_json(worksheet);
117
114
 
118
- // Skip if not an API content type
119
- if (!contentType.startsWith('api::')) {
120
- continue;
121
- }
115
+ if (!rows.length) return;
122
116
 
123
- // Check if content type exists
124
- if (!strapi.contentTypes[contentType]) {
125
- results.errors.push(`Content type ${contentType} not found`);
126
- continue;
127
- }
117
+ const contentTypeName = mapSheetNameToContentType(sheetName);
128
118
 
129
- if (!Array.isArray(entries)) {
130
- results.errors.push(`Invalid data format for ${contentType}`);
131
- continue;
132
- }
119
+ strapi.log.info(`Reading sheet "${sheetName}" -> ${rows.length} rows`);
120
+ strapi.log.info(`Mapped sheet to content-type: ${contentTypeName}`);
121
+
122
+ if (contentTypeName.startsWith('api::')) {
123
+ importData[contentTypeName] = unflattenRow(rows, contentTypeName);
124
+ } else {
125
+ strapi.log.error(`Unknown content-type: ${contentTypeName}`);
126
+ return;
127
+ }
128
+ });
129
+
130
+ strapi.log.info('Final import data keys:', Object.keys(importData));
131
+ return importData;
132
+ }
133
+
134
+ function getRelationFields(contentType) {
135
+ const schema = strapi.contentTypes[contentType];
136
+
137
+ if (!schema) {
138
+ strapi.log.warn(`Content type ${contentType} not found`);
139
+ return [];
140
+ }
141
+
142
+ return Object.entries(schema.attributes)
143
+ .filter(([_, attr]) => attr.type === "relation")
144
+ .map(([fieldName, attr]) => ({
145
+ field: toCamel(fieldName),
146
+ target: attr.target, // e.g. "api::category.category"
147
+ relation: attr.relation,
148
+ }));
149
+ }
150
+
151
+ function getRelationFieldsStrArr(contentType) {
152
+ const schema = strapi.contentTypes[contentType];
153
+
154
+ if (!schema) {
155
+ strapi.log.warn(`Content type ${contentType} not found`);
156
+ return [];
157
+ }
158
+
159
+ return Object.entries(schema.attributes)
160
+ .filter(([_, attr]) => attr.type === "relation")
161
+ .map(([fieldName, attr]) => toCamel(fieldName));
162
+ }
163
+
164
+ function getComponentFields(contentType) {
165
+ const schema = strapi.contentTypes[contentType];
133
166
 
134
- for (const entry of entries) {
135
- try {
136
- // Extract system fields for update/create logic
137
- const {
138
- id,
139
- documentId,
140
- status,
141
- createdAt,
142
- updatedAt,
143
- publishedAt,
144
- createdBy,
145
- updatedBy,
146
- localizations,
147
- locale,
148
- ...cleanEntry
149
- } = entry;
150
-
151
- // Skip empty entries
152
- if (!cleanEntry || Object.keys(cleanEntry).length === 0) {
153
- strapi.log.warn('Skipping empty entry');
154
- continue;
155
- }
156
-
157
- // Clean up empty string values and convert to null
158
- for (const [key, value] of Object.entries(cleanEntry)) {
159
- if (value === '' || value === 'null' || value === 'undefined') {
160
- cleanEntry[key] = null;
161
- }
162
- }
163
-
164
- let existingEntry = null;
165
- let updateMode = false;
166
-
167
- // Only try to find existing entry if documentId is provided and valid
168
- if (documentId && documentId !== '' && documentId !== 'null' && documentId !== 'undefined') {
169
- try {
170
- if (strapi.documents) {
171
- existingEntry = await strapi.documents(contentType).findOne({
172
- documentId: documentId,
173
- });
174
- if (existingEntry) {
175
- updateMode = true;
176
- strapi.log.info(`Found existing entry for update: ${documentId}`);
177
- }
178
- }
179
- } catch (error) {
180
- // Entry not found, will create new one
181
- strapi.log.info(`DocumentId ${documentId} not found, will create new entry`);
182
- }
183
- }
184
-
185
- // If no documentId provided or not found, this will be a new entry
186
- if (!existingEntry) {
187
- strapi.log.info(`Creating new entry for: ${cleanEntry.shortName || cleanEntry.name || cleanEntry.title || 'Unknown'}`);
188
- }
189
-
190
- // Skip entries without basic required fields
191
- if (!cleanEntry.shortName && !cleanEntry.name && !cleanEntry.title) {
192
- continue;
193
- }
194
-
195
- // Ensure required components exist for corporate
196
- if (contentType === 'api::corporate.corporate') {
197
- if (!cleanEntry.corporateInfo) cleanEntry.corporateInfo = {};
198
- if (!cleanEntry.meetingRequirements) cleanEntry.meetingRequirements = {};
199
- if (!cleanEntry.internalInfo) cleanEntry.internalInfo = {};
200
- }
201
-
202
- if (existingEntry) {
203
- // Check if there are actual changes before updating
204
- const hasChanges = this.hasDataChanges(existingEntry, cleanEntry);
205
- const statusChanged = (status === 'published') !== !!existingEntry.publishedAt;
206
-
207
- if (hasChanges || statusChanged) {
208
- try {
209
- if (strapi.documents) {
210
- const updateData = {
211
- documentId: existingEntry.documentId,
212
- data: cleanEntry,
213
- };
214
-
215
- // Handle status change
216
- if (statusChanged) {
217
- updateData.status = status === 'published' ? 'published' : 'draft';
218
- }
219
-
220
- await strapi.documents(contentType).update(updateData);
221
- results.updated++;
222
- } else {
223
- await strapi.entityService.update(contentType, existingEntry.id, {
224
- data: cleanEntry,
225
- });
226
- results.updated++;
227
- }
228
- } catch (updateError) {
229
- results.errors.push(`Failed to update entry: ${updateError.message}`);
230
- }
231
- } else {
232
- // No changes, skip update
233
- strapi.log.info(`No changes detected for entry: ${cleanEntry.shortName || 'Unknown'}`);
234
- }
235
- } else {
236
- // Create new entry
237
- try {
238
- if (strapi.documents) {
239
- await strapi.documents(contentType).create({
240
- data: cleanEntry,
241
- status: status === 'published' ? 'published' : 'draft',
242
- });
243
- results.created++;
244
- } else {
245
- await strapi.entityService.create(contentType, {
246
- data: cleanEntry,
247
- });
248
- results.created++;
249
- }
250
- } catch (createError) {
251
- results.errors.push(`Failed to create entry: ${createError.message}`);
252
- }
253
- }
254
- } catch (error) {
255
- results.errors.push(`Failed to import entry in ${contentType}: ${error.message}`);
256
- strapi.log.error('Import entry error:', error);
167
+ if (!schema) {
168
+ strapi.log.warn(`Content type ${contentType} not found`);
169
+ return [];
170
+ }
171
+
172
+ return Object.entries(schema.attributes)
173
+ .filter(([_, attr]) => attr.type === "component")
174
+ .map(([fieldName, attr]) => toCamel(fieldName));
175
+ }
176
+
177
+ async function handleRelations(entry, contentType) {
178
+ let relationFields = getRelationFields(contentType);
179
+ if (relationFields.length === 0) {
180
+ return entry;
181
+ }
182
+
183
+ let existing = null;
184
+ const newEntry = { ...entry };
185
+ let isUpdated = false;
186
+
187
+ for (const rel of relationFields) {
188
+ const { field, target } = rel;
189
+ const relValue = entry[field];
190
+ try {
191
+ if (!relValue) continue;
192
+
193
+ if (Array.isArray(relValue)) {
194
+ const processed = [];
195
+
196
+ for (const item of relValue) {
197
+ if (item.id) {
198
+ existing = await strapi.documents(target).findFirst({
199
+ filters: {
200
+ id: { $eq: item.id }
201
+ },
202
+ });
203
+ if (existing && hasChanges(existing, item, getRelationFieldsStrArr(target))) {
204
+ await strapi.documents(target).update({ documentId: existing.documentId, data: item });
205
+ isUpdated = true;
257
206
  }
207
+ processed.push({ id: item.id });
208
+ } else {
209
+ const created = await strapi.documents(target).create({ data: item });
210
+ processed.push({ id: created.id });
258
211
  }
259
- } catch (error) {
260
- results.errors.push(`Failed to process ${contentType}: ${error.message}`);
261
- strapi.log.error('Import process error:', error);
262
212
  }
213
+ newEntry[field] = processed;
214
+ continue;
263
215
  }
264
216
 
265
- // Clean up uploaded file
266
- if (filePath && fs.existsSync(filePath)) {
267
- fs.unlinkSync(filePath);
217
+ if (!relValue.id) {
218
+ const created = await strapi.documents(target).create({ data: relValue });
219
+ newEntry[field] = { id: created.id };
220
+ } else {
221
+ existing = await strapi.documents(target).findFirst({
222
+ filters: {
223
+ id: { $eq: relValue.id }
224
+ },
225
+ });
226
+ if (hasChanges(existing, relValue, getRelationFieldsStrArr(target))) {
227
+ await strapi.documents(target).update({ documentId: existing.documentId, data: relValue });
228
+ isUpdated = true;
229
+ }
230
+ newEntry[field] = { id: relValue.id };
268
231
  }
232
+ } catch (err) {
233
+ throw new Error(`Field: ${field}, data: ${JSON.stringify(relValue)}, error: ${err.message}`);
234
+ }
235
+ }
269
236
 
270
- return results;
271
- } catch (error) {
272
- // Clean up uploaded file on error
273
- const filePath = file && (file.path || file.filepath);
274
- if (filePath && fs.existsSync(filePath)) {
275
- fs.unlinkSync(filePath);
276
- }
277
- throw error;
237
+ return [newEntry, isUpdated];
238
+ }
239
+
240
+ function hasChanges(existing, incoming, relationFieldStrArr = []) {
241
+ if (!incoming || typeof incoming !== "object") return false;
242
+
243
+ for (const key of Object.keys(incoming)) {
244
+ // Skip system keys
245
+ if (SYSTEM_KEYS.includes(key)) continue;
246
+
247
+ // Skip relation fields entirely
248
+ if (relationFieldStrArr.includes(key)) continue;
249
+
250
+ const newVal = incoming[key];
251
+ const oldVal = existing[key];
252
+
253
+ // If incoming defines a field but existing doesn't → change
254
+ if (oldVal === undefined || newVal === undefined) {
255
+ continue;
278
256
  }
279
- },
280
-
281
- hasDataChanges(existingEntry, newData) {
282
- // Compare key fields to detect changes
283
- const fieldsToCompare = ['shortName', 'name', 'title'];
284
-
285
- for (const field of fieldsToCompare) {
286
- if (existingEntry[field] !== newData[field]) {
257
+
258
+ // Primitive comparison
259
+ if (newVal === null || typeof newVal !== "object") {
260
+ if (oldVal !== newVal) {
287
261
  return true;
288
262
  }
263
+ continue;
289
264
  }
290
-
291
- // Compare nested objects (components)
292
- const componentsToCompare = ['corporateInfo', 'meetingRequirements', 'internalInfo'];
293
-
294
- for (const component of componentsToCompare) {
295
- if (existingEntry[component] && newData[component]) {
296
- const existingStr = JSON.stringify(existingEntry[component]);
297
- const newStr = JSON.stringify(newData[component]);
298
- if (existingStr !== newStr) {
265
+
266
+ // ARRAY comparison
267
+ if (Array.isArray(newVal)) {
268
+ if (!Array.isArray(oldVal)) return true;
269
+ if (newVal.length !== oldVal.length) return true;
270
+ // Compare values shallowly
271
+ for (let i = 0; i < newVal.length; i++) {
272
+ if (typeof newVal[i] === "object" && typeof oldVal[i] === "object" && hasChanges(oldVal[i], newVal[i])) {
273
+ return true;
274
+ } else if (typeof newVal[i] !== "object" && typeof oldVal[i] !== "object" && newVal[i] !== oldVal[i]) {
299
275
  return true;
300
276
  }
301
277
  }
278
+ continue;
302
279
  }
303
-
304
- return false;
305
- },
306
- });
280
+
281
+ // OBJECT comparison (recursive, but ONLY fields in incoming object)
282
+ if (typeof newVal === "object" && typeof oldVal === "object") {
283
+ if (hasChanges(oldVal, newVal)) {
284
+ return true;
285
+ }
286
+ continue;
287
+ }
288
+ }
289
+
290
+ return false;
291
+ }
292
+
293
+
294
+ async function bulkInsertData(importData) {
295
+ const results = {
296
+ created: 0,
297
+ updated: 0,
298
+ errors: [],
299
+ };
300
+
301
+ for (const [contentType, entries] of Object.entries(importData)) {
302
+ // Validate entries
303
+ if (!strapi.contentTypes[contentType]) {
304
+ results.errors.push(`Content type ${contentType} not found`);
305
+ continue;
306
+ }
307
+ if (!Array.isArray(entries)) {
308
+ results.errors.push(`Invalid data format for ${contentType}`);
309
+ continue;
310
+ }
311
+
312
+ for (i = 0; i < entries.length; i++) {
313
+ const entry = entries[i];
314
+ let existing = null;
315
+ try {
316
+ let { id, ...data } = entry; // keep id out, keep everything else
317
+ let isUpdated = false;
318
+ let isCreated = false;
319
+ if (id && id !== 'null' && id !== 'undefined') {
320
+ existing = await strapi.documents(contentType).findFirst({
321
+ filters: {
322
+ id: { $eq: id }
323
+ },
324
+ populate: '*'
325
+ });
326
+ }
327
+
328
+ [data, isUpdated] = await handleRelations(data, contentType);
329
+
330
+ if (existing) {
331
+ if (hasChanges(existing, data)) {
332
+ await strapi.documents(contentType).update({
333
+ documentId: existing.documentId,
334
+ data,
335
+ });
336
+ isUpdated = true;
337
+ }
338
+ } else {
339
+ await strapi.documents(contentType).create({ data });
340
+ isCreated = true;
341
+ }
342
+ if (isUpdated) {
343
+ results.updated++;
344
+ } else if (isCreated) {
345
+ results.created++;
346
+ }
347
+ } catch (err) {
348
+ results.errors.push(`Failed ${existing ? 'updating' : 'creating'} on row ${i+2}: ${err.message}`);
349
+ }
350
+ }
351
+ }
352
+
353
+ return results;
354
+ }
355
+
356
+ module.exports = {
357
+ importData,
358
+ };