@tunghtml/strapi-plugin-export-import-clsx 1.0.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.
- package/README.md +93 -0
- package/admin/src/components/BulkActions/index.js +70 -0
- package/admin/src/components/ExportButton/index.js +48 -0
- package/admin/src/components/ExportImportButtons/index.js +245 -0
- package/admin/src/components/ImportButton/index.js +54 -0
- package/admin/src/components/Initializer/index.js +15 -0
- package/admin/src/components/PluginIcon/index.js +6 -0
- package/admin/src/pages/App/index.js +8 -0
- package/admin/src/pages/HomePage/index.js +297 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/en.json +14 -0
- package/package.json +60 -0
- package/server/bootstrap.js +3 -0
- package/server/config/index.js +4 -0
- package/server/content-types/index.js +1 -0
- package/server/controllers/export-controller.js +62 -0
- package/server/controllers/import-controller.js +42 -0
- package/server/controllers/index.js +7 -0
- package/server/destroy.js +3 -0
- package/server/middlewares/index.js +1 -0
- package/server/policies/index.js +1 -0
- package/server/register.js +3 -0
- package/server/routes/index.js +29 -0
- package/server/services/export-service.js +336 -0
- package/server/services/import-service.js +306 -0
- package/server/services/index.js +7 -0
- package/strapi-admin.js +88 -0
- package/strapi-server.js +34 -0
|
@@ -0,0 +1,336 @@
|
|
|
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
|
+
|
|
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`);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
strapi.log.error(`Failed to query entries:`, error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
strapi.log.info(`Final result: ${entries?.length || 0} entries for ${ct} (total found: ${entries?.length || 0})`);
|
|
125
|
+
|
|
126
|
+
if (entries && entries.length > 0) {
|
|
127
|
+
exportData.data[ct] = entries;
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
strapi.log.error(`Failed to export ${ct}:`, error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (format === 'excel') {
|
|
135
|
+
return this.convertToExcel(exportData.data);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return exportData;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
parseFilters(filters) {
|
|
142
|
+
const parsed = {};
|
|
143
|
+
|
|
144
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
145
|
+
// Skip pagination and sorting params
|
|
146
|
+
if (['page', 'pageSize', 'sort', 'locale', 'format', 'contentType', 'selectedIds'].includes(key)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle URL encoded filter format like filters[$and][0][shortName][$contains]
|
|
151
|
+
if (key.startsWith('filters[')) {
|
|
152
|
+
// Extract the actual filter structure
|
|
153
|
+
const match = key.match(/filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/);
|
|
154
|
+
if (match) {
|
|
155
|
+
const [, operator, index, field, condition] = match;
|
|
156
|
+
|
|
157
|
+
if (!parsed.filters) parsed.filters = {};
|
|
158
|
+
|
|
159
|
+
if (operator === '$and') {
|
|
160
|
+
if (!parsed.filters.$and) parsed.filters.$and = [];
|
|
161
|
+
const idx = parseInt(index) || 0;
|
|
162
|
+
if (!parsed.filters.$and[idx]) parsed.filters.$and[idx] = {};
|
|
163
|
+
|
|
164
|
+
if (condition) {
|
|
165
|
+
if (!parsed.filters.$and[idx][field]) parsed.filters.$and[idx][field] = {};
|
|
166
|
+
parsed.filters.$and[idx][field][condition] = value;
|
|
167
|
+
} else {
|
|
168
|
+
parsed.filters.$and[idx][field] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
parsed[key] = value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return parsed;
|
|
178
|
+
},
|
|
179
|
+
|
|
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;
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
convertToExcel(data) {
|
|
242
|
+
const workbook = XLSX.utils.book_new();
|
|
243
|
+
let hasData = false;
|
|
244
|
+
|
|
245
|
+
for (const [contentType, entries] of Object.entries(data)) {
|
|
246
|
+
// Clean sheet name (Excel has restrictions)
|
|
247
|
+
const sheetName = contentType.replace(/[^\w\s]/gi, '_').substring(0, 31);
|
|
248
|
+
|
|
249
|
+
if (entries && entries.length > 0) {
|
|
250
|
+
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;
|
|
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') {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
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
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
flatten(entryWithSystemFields);
|
|
291
|
+
return flattened;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const worksheet = XLSX.utils.json_to_sheet(cleanedEntries);
|
|
295
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
296
|
+
} else {
|
|
297
|
+
// Create empty sheet with headers if no data
|
|
298
|
+
const worksheet = XLSX.utils.json_to_sheet([{ message: 'No data found' }]);
|
|
299
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
300
|
+
hasData = true; // Prevent empty workbook error
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If still no data, create a default sheet
|
|
305
|
+
if (!hasData) {
|
|
306
|
+
const worksheet = XLSX.utils.json_to_sheet([{ message: 'No data to export' }]);
|
|
307
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, 'NoData');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
async exportSingleEntry(contentType, entryId) {
|
|
314
|
+
try {
|
|
315
|
+
const entry = await strapi.entityService.findOne(contentType, entryId, {
|
|
316
|
+
populate: '*',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!entry) {
|
|
320
|
+
throw new Error('Entry not found');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const exportData = {
|
|
324
|
+
version: strapi.config.get('info.strapi'),
|
|
325
|
+
timestamp: new Date().toISOString(),
|
|
326
|
+
data: {
|
|
327
|
+
[contentType]: [entry]
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return this.convertToExcel(exportData.data);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const XLSX = require('xlsx');
|
|
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
|
+
}
|
|
18
|
+
|
|
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, '.');
|
|
40
|
+
}
|
|
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}`);
|
|
94
|
+
}
|
|
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.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const results = {
|
|
103
|
+
created: 0,
|
|
104
|
+
updated: 0,
|
|
105
|
+
errors: [],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Handle different data structures
|
|
109
|
+
const dataToProcess = importData.data || importData;
|
|
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
|
+
}
|
|
117
|
+
|
|
118
|
+
// Skip if not an API content type
|
|
119
|
+
if (!contentType.startsWith('api::')) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if content type exists
|
|
124
|
+
if (!strapi.contentTypes[contentType]) {
|
|
125
|
+
results.errors.push(`Content type ${contentType} not found`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!Array.isArray(entries)) {
|
|
130
|
+
results.errors.push(`Invalid data format for ${contentType}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
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);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
results.errors.push(`Failed to process ${contentType}: ${error.message}`);
|
|
261
|
+
strapi.log.error('Import process error:', error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Clean up uploaded file
|
|
266
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
267
|
+
fs.unlinkSync(filePath);
|
|
268
|
+
}
|
|
269
|
+
|
|
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;
|
|
278
|
+
}
|
|
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]) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
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) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
},
|
|
306
|
+
});
|