strapi-plugin-bulk-updater 1.4.3 → 1.4.6
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/dist/_chunks/{BulkUpdater-CbQk_OQG.mjs → BulkUpdater-CMkPdA9_.mjs} +942 -328
- package/dist/_chunks/BulkUpdater-CMkPdA9_.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-mSvmiLwc.js → BulkUpdater-DtBH2c0-.js} +939 -325
- package/dist/_chunks/BulkUpdater-DtBH2c0-.js.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +1341 -629
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1341 -629
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/_chunks/BulkUpdater-CbQk_OQG.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-mSvmiLwc.js.map +0 -1
package/dist/server/index.mjs
CHANGED
|
@@ -211,6 +211,30 @@ const routes = {
|
|
|
211
211
|
config: {
|
|
212
212
|
policies: []
|
|
213
213
|
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
method: "POST",
|
|
217
|
+
path: "/csv/import-chunk",
|
|
218
|
+
handler: "csv.importChunk",
|
|
219
|
+
config: {
|
|
220
|
+
policies: []
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
method: "POST",
|
|
225
|
+
path: "/csv/validate-preflight",
|
|
226
|
+
handler: "csv.validatePreflight",
|
|
227
|
+
config: {
|
|
228
|
+
policies: []
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
method: "POST",
|
|
233
|
+
path: "/csv/submit-batch",
|
|
234
|
+
handler: "csv.submitBatch",
|
|
235
|
+
config: {
|
|
236
|
+
policies: []
|
|
237
|
+
}
|
|
214
238
|
}
|
|
215
239
|
]
|
|
216
240
|
}
|
|
@@ -663,6 +687,86 @@ const csvController = ({ strapi }) => ({
|
|
|
663
687
|
ctx.status = 500;
|
|
664
688
|
ctx.body = { success: false, error: err.message };
|
|
665
689
|
}
|
|
690
|
+
},
|
|
691
|
+
async importChunk(ctx) {
|
|
692
|
+
try {
|
|
693
|
+
const { uid, rows, batchId, isFirstChunk, isLastChunk, startRowNumber, mode, publish } = ctx.request.body;
|
|
694
|
+
if (!uid) {
|
|
695
|
+
ctx.status = 400;
|
|
696
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (!Array.isArray(rows)) {
|
|
700
|
+
ctx.status = 400;
|
|
701
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
705
|
+
const result = await csvService2.importChunk(uid, rows, {
|
|
706
|
+
batchId: batchId ? String(batchId) : void 0,
|
|
707
|
+
isFirstChunk: Boolean(isFirstChunk),
|
|
708
|
+
isLastChunk: Boolean(isLastChunk),
|
|
709
|
+
startRowNumber: typeof startRowNumber === "number" ? startRowNumber : void 0,
|
|
710
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
|
|
711
|
+
publish: publish !== false,
|
|
712
|
+
user: ctx.state.user
|
|
713
|
+
});
|
|
714
|
+
ctx.body = result;
|
|
715
|
+
} catch (err) {
|
|
716
|
+
ctx.status = 500;
|
|
717
|
+
ctx.body = { success: false, error: err.message };
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
async validatePreflight(ctx) {
|
|
721
|
+
try {
|
|
722
|
+
const { uid, rows, mode } = ctx.request.body;
|
|
723
|
+
if (!uid) {
|
|
724
|
+
ctx.status = 400;
|
|
725
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!Array.isArray(rows)) {
|
|
729
|
+
ctx.status = 400;
|
|
730
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
734
|
+
const result = await csvService2.validatePreflight(uid, rows, {
|
|
735
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto"
|
|
736
|
+
});
|
|
737
|
+
ctx.body = result;
|
|
738
|
+
} catch (err) {
|
|
739
|
+
ctx.status = 500;
|
|
740
|
+
ctx.body = { success: false, error: err.message };
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
async submitBatch(ctx) {
|
|
744
|
+
try {
|
|
745
|
+
const { uid, rows, executionMode, makerNotes, forceImmediate, mode, publish } = ctx.request.body;
|
|
746
|
+
if (!uid) {
|
|
747
|
+
ctx.status = 400;
|
|
748
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (!Array.isArray(rows)) {
|
|
752
|
+
ctx.status = 400;
|
|
753
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
757
|
+
const result = await csvService2.submitBatch(uid, rows, {
|
|
758
|
+
executionMode,
|
|
759
|
+
makerNotes,
|
|
760
|
+
forceImmediate: Boolean(forceImmediate),
|
|
761
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
|
|
762
|
+
publish: publish !== false,
|
|
763
|
+
user: ctx.state.user
|
|
764
|
+
});
|
|
765
|
+
ctx.body = result;
|
|
766
|
+
} catch (err) {
|
|
767
|
+
ctx.status = 500;
|
|
768
|
+
ctx.body = { success: false, error: err.message };
|
|
769
|
+
}
|
|
666
770
|
}
|
|
667
771
|
});
|
|
668
772
|
const FIELDS_TO_IGNORE = [
|
|
@@ -945,13 +1049,15 @@ function getAllNestedValues(obj, path) {
|
|
|
945
1049
|
if (!item || typeof item !== "object") return;
|
|
946
1050
|
if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) return;
|
|
947
1051
|
if (parsed.targetComponentUid && item.__component !== parsed.targetComponentUid) return;
|
|
1052
|
+
const compSchema = global.strapi?.components?.[item.__component];
|
|
1053
|
+
const fieldInSchema = tail.length > 0 && compSchema?.attributes ? tail[0] in compSchema.attributes : false;
|
|
948
1054
|
const subVal = tail.length > 0 ? getDeepProperty(item, tail.join(".")) : item;
|
|
949
|
-
if (subVal !== void 0) {
|
|
1055
|
+
if (subVal !== void 0 || fieldInSchema) {
|
|
950
1056
|
results.push({
|
|
951
1057
|
index: idx,
|
|
952
1058
|
componentUid: item.__component,
|
|
953
1059
|
componentName: formatComponentName(item.__component),
|
|
954
|
-
value: subVal,
|
|
1060
|
+
value: subVal !== void 0 ? subVal : null,
|
|
955
1061
|
path: `${head}[#${idx}${item.__component ? ` ${formatComponentName(item.__component)}` : ""}].${tail.join(".")}`
|
|
956
1062
|
});
|
|
957
1063
|
}
|
|
@@ -1050,7 +1156,9 @@ function setDeepProperty(obj, pathParts, value, options) {
|
|
|
1050
1156
|
if (options?.targetComponentUid) {
|
|
1051
1157
|
if (item.__component !== options.targetComponentUid) return item;
|
|
1052
1158
|
}
|
|
1053
|
-
|
|
1159
|
+
const compSchema = global.strapi?.components?.[item.__component];
|
|
1160
|
+
const fieldExistsInSchema = compSchema?.attributes ? head in compSchema.attributes : true;
|
|
1161
|
+
if (head in item || fieldExistsInSchema || tail.length === 0) {
|
|
1054
1162
|
return {
|
|
1055
1163
|
...item,
|
|
1056
1164
|
[head]: setDeepProperty(item[head], tail, value, options)
|
|
@@ -1164,6 +1272,12 @@ const pendingBatches = /* @__PURE__ */ new Map();
|
|
|
1164
1272
|
const batchJobsMap = /* @__PURE__ */ new Map();
|
|
1165
1273
|
let isRunnerRunning = false;
|
|
1166
1274
|
let runnerInterval = null;
|
|
1275
|
+
function registerPendingBatch(batch, jobState) {
|
|
1276
|
+
pendingBatches.set(batch.id, batch);
|
|
1277
|
+
if (jobState) {
|
|
1278
|
+
batchJobsMap.set(batch.id, jobState);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1167
1281
|
function invalidateCollectionCache(uid) {
|
|
1168
1282
|
modelsCache.delete(uid);
|
|
1169
1283
|
for (const key of queryCache.keys()) {
|
|
@@ -1786,6 +1900,22 @@ async function executePublishBatchInternal(strapi, {
|
|
|
1786
1900
|
results
|
|
1787
1901
|
};
|
|
1788
1902
|
}
|
|
1903
|
+
async function executeBatchByPayload(strapi, batch, currentUser) {
|
|
1904
|
+
if (batch.payload?.action === "csv_import") {
|
|
1905
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
1906
|
+
return await csvService2.executeCsvPendingBatch(
|
|
1907
|
+
batch.payload.uid,
|
|
1908
|
+
batch.payload.rows,
|
|
1909
|
+
batch.id,
|
|
1910
|
+
batch.payload.options,
|
|
1911
|
+
currentUser
|
|
1912
|
+
);
|
|
1913
|
+
} else if (batch.payload?.action === "publish") {
|
|
1914
|
+
return await executePublishBatchInternal(strapi, batch.payload, currentUser, batch.id);
|
|
1915
|
+
} else {
|
|
1916
|
+
return await executeBatchInternal(strapi, batch.payload, currentUser, batch.id);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1789
1919
|
const bulkUpdaterService = ({ strapi }) => {
|
|
1790
1920
|
if (!runnerInterval) {
|
|
1791
1921
|
runnerInterval = setInterval(async () => {
|
|
@@ -1797,7 +1927,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
1797
1927
|
for (const [id, batch] of pendingBatches.entries()) {
|
|
1798
1928
|
if (batch.status === "queued_offpeak" && !isBusinessHours) {
|
|
1799
1929
|
strapi.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
|
|
1800
|
-
const result =
|
|
1930
|
+
const result = await executeBatchByPayload(strapi, batch, batch.createdBy);
|
|
1801
1931
|
batch.status = "executed";
|
|
1802
1932
|
batch.executionResult = result;
|
|
1803
1933
|
}
|
|
@@ -2410,7 +2540,12 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2410
2540
|
for (const item of selectedEntries) {
|
|
2411
2541
|
if (!item.uid || !item.documentId) continue;
|
|
2412
2542
|
const key = `${item.uid}@@@${item.documentId}`;
|
|
2413
|
-
targetsMap.set(key, {
|
|
2543
|
+
targetsMap.set(key, {
|
|
2544
|
+
uid: item.uid,
|
|
2545
|
+
documentId: item.documentId,
|
|
2546
|
+
dynamicZoneIndexes: item.dynamicZoneIndexes,
|
|
2547
|
+
targetComponentUid: item.targetComponentUid
|
|
2548
|
+
});
|
|
2414
2549
|
}
|
|
2415
2550
|
if (targetsMap.size === 0) {
|
|
2416
2551
|
throw new Error("No collections or entries were selected for preview.");
|
|
@@ -2474,6 +2609,49 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2474
2609
|
}
|
|
2475
2610
|
const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
|
|
2476
2611
|
if (isComponentNested) {
|
|
2612
|
+
const parsed = parseDynamicZonePath(attribute);
|
|
2613
|
+
const rootKey = parsed.rootKey;
|
|
2614
|
+
const isRootDz = contentType?.attributes?.[rootKey]?.type === "dynamiczone";
|
|
2615
|
+
const targetIndexes = target.dynamicZoneIndexes;
|
|
2616
|
+
const targetComponentUid = target.targetComponentUid || parsed.targetComponentUid;
|
|
2617
|
+
if (isRootDz && currentEntry?.[rootKey] && Array.isArray(currentEntry[rootKey])) {
|
|
2618
|
+
let affectedMatches = getAllNestedValues({ [rootKey]: currentEntry[rootKey] }, attribute);
|
|
2619
|
+
if (targetIndexes && targetIndexes.length > 0) {
|
|
2620
|
+
affectedMatches = affectedMatches.filter((m) => targetIndexes.includes(m.index));
|
|
2621
|
+
}
|
|
2622
|
+
if (targetComponentUid) {
|
|
2623
|
+
affectedMatches = affectedMatches.filter((m) => m.componentUid === targetComponentUid);
|
|
2624
|
+
}
|
|
2625
|
+
if (affectedMatches.length > 0) {
|
|
2626
|
+
for (const match of affectedMatches) {
|
|
2627
|
+
let blockNewVal = value;
|
|
2628
|
+
if (regexObj && typeof match.value === "string") {
|
|
2629
|
+
blockNewVal = match.value.replace(regexObj, value);
|
|
2630
|
+
}
|
|
2631
|
+
const hasChange3 = String(match.value ?? "") !== String(blockNewVal ?? "");
|
|
2632
|
+
if (hasChange3) {
|
|
2633
|
+
willUpdateCount++;
|
|
2634
|
+
} else {
|
|
2635
|
+
unchangedCount++;
|
|
2636
|
+
}
|
|
2637
|
+
diffs.push({
|
|
2638
|
+
uid: target.uid,
|
|
2639
|
+
collectionName: displayName,
|
|
2640
|
+
documentId: target.documentId,
|
|
2641
|
+
label: `${entryLabel} [Block #${match.index} (${match.componentName})]`,
|
|
2642
|
+
attribute: `${rootKey}[#${match.index} ${match.componentName}].${parsed.subPath}`,
|
|
2643
|
+
oldValue: match.value ?? null,
|
|
2644
|
+
newValue: blockNewVal,
|
|
2645
|
+
oldDisplay: match.value !== void 0 && match.value !== null && String(match.value).trim() !== "" ? String(match.value) : "(empty)",
|
|
2646
|
+
newDisplay: String(blockNewVal ?? ""),
|
|
2647
|
+
hasChange: hasChange3,
|
|
2648
|
+
status: hasChange3 ? "will_update" : "unchanged",
|
|
2649
|
+
message: hasChange3 ? "Block value will be updated" : "Block already has target value"
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2477
2655
|
const currentVal2 = getDeepProperty(currentEntry, attribute);
|
|
2478
2656
|
let newValue2 = value;
|
|
2479
2657
|
if (regexObj && typeof currentVal2 === "string") {
|
|
@@ -3053,7 +3231,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3053
3231
|
const { isBusinessHours } = checkBusinessHours(config);
|
|
3054
3232
|
if (options.executeImmediately || !isBusinessHours) {
|
|
3055
3233
|
batch.status = "approved";
|
|
3056
|
-
const result =
|
|
3234
|
+
const result = await executeBatchByPayload(strapi, batch, checkerUser);
|
|
3057
3235
|
batch.status = "executed";
|
|
3058
3236
|
batch.executionResult = result;
|
|
3059
3237
|
return {
|
|
@@ -3114,7 +3292,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3114
3292
|
if (batch.status === "executed") {
|
|
3115
3293
|
throw new Error(`Batch ${batchId} has already been executed.`);
|
|
3116
3294
|
}
|
|
3117
|
-
const result =
|
|
3295
|
+
const result = await executeBatchByPayload(strapi, batch, currentUser);
|
|
3118
3296
|
batch.status = "executed";
|
|
3119
3297
|
batch.executionResult = result;
|
|
3120
3298
|
return {
|
|
@@ -3361,6 +3539,20 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3361
3539
|
},
|
|
3362
3540
|
invalidateCollectionCache(uid) {
|
|
3363
3541
|
invalidateCollectionCache(uid);
|
|
3542
|
+
},
|
|
3543
|
+
registerPendingBatch(batch, jobState) {
|
|
3544
|
+
pendingBatches.set(batch.id, batch);
|
|
3545
|
+
if (jobState) {
|
|
3546
|
+
batchJobsMap.set(batch.id, jobState);
|
|
3547
|
+
}
|
|
3548
|
+
return batch;
|
|
3549
|
+
},
|
|
3550
|
+
getExecutionWindowConfig() {
|
|
3551
|
+
return getExecutionWindowConfig(strapi);
|
|
3552
|
+
},
|
|
3553
|
+
checkBusinessHours(config) {
|
|
3554
|
+
const cfg = config || getExecutionWindowConfig(strapi);
|
|
3555
|
+
return checkBusinessHours(cfg);
|
|
3364
3556
|
}
|
|
3365
3557
|
};
|
|
3366
3558
|
};
|
|
@@ -4567,92 +4759,645 @@ function getNestedExportValue(entry, path) {
|
|
|
4567
4759
|
}
|
|
4568
4760
|
return current;
|
|
4569
4761
|
}
|
|
4570
|
-
const
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
if (!ct) {
|
|
4577
|
-
throw new Error(`Content type ${uid} not found`);
|
|
4762
|
+
const csvImportSessions = /* @__PURE__ */ new Map();
|
|
4763
|
+
function cleanupStaleSessions() {
|
|
4764
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
4765
|
+
for (const [id, session] of csvImportSessions.entries()) {
|
|
4766
|
+
if (session.createdAt < oneHourAgo) {
|
|
4767
|
+
csvImportSessions.delete(id);
|
|
4578
4768
|
}
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
const SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
4772
|
+
"documentid",
|
|
4773
|
+
"document_id",
|
|
4774
|
+
"docid",
|
|
4775
|
+
"id",
|
|
4776
|
+
"createdat",
|
|
4777
|
+
"updatedat",
|
|
4778
|
+
"publishedat",
|
|
4779
|
+
"createdby",
|
|
4780
|
+
"updatedby",
|
|
4781
|
+
"localizations",
|
|
4782
|
+
"locale",
|
|
4783
|
+
"status"
|
|
4784
|
+
]);
|
|
4785
|
+
async function findExistingDocument(strapi, uid, targetDocId, docLocale) {
|
|
4786
|
+
if (!targetDocId) return null;
|
|
4787
|
+
try {
|
|
4788
|
+
const queryArgs = { documentId: targetDocId };
|
|
4789
|
+
if (docLocale) queryArgs.locale = docLocale;
|
|
4790
|
+
const found = await strapi.documents(uid).findOne(queryArgs);
|
|
4791
|
+
if (found) return found;
|
|
4792
|
+
} catch {
|
|
4793
|
+
}
|
|
4794
|
+
if (/^\d+$/.test(targetDocId)) {
|
|
4795
|
+
try {
|
|
4796
|
+
const queryArgs = {
|
|
4797
|
+
filters: { id: parseInt(targetDocId, 10) },
|
|
4798
|
+
limit: 1
|
|
4799
|
+
};
|
|
4800
|
+
if (docLocale) queryArgs.locale = docLocale;
|
|
4801
|
+
const list = await strapi.documents(uid).findMany(queryArgs);
|
|
4802
|
+
if (list && list.length > 0) return list[0];
|
|
4803
|
+
} catch {
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
return null;
|
|
4807
|
+
}
|
|
4808
|
+
async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
4809
|
+
const attrs = ct.attributes || {};
|
|
4810
|
+
const { mode = "auto", publish = true, user } = options;
|
|
4811
|
+
const stats = {
|
|
4812
|
+
total: rows.length,
|
|
4813
|
+
created: 0,
|
|
4814
|
+
updated: 0,
|
|
4815
|
+
skipped: 0,
|
|
4816
|
+
newWithoutDocId: 0,
|
|
4817
|
+
errors: [],
|
|
4818
|
+
diffs: []
|
|
4819
|
+
};
|
|
4820
|
+
for (let i = 0; i < rows.length; i++) {
|
|
4821
|
+
const rowNumber = startRowNumber + i;
|
|
4822
|
+
const row = rows[i];
|
|
4823
|
+
let docId = extractDocId(row);
|
|
4824
|
+
const rowLocale = extractLocale(row);
|
|
4825
|
+
if (ct.kind === "singleType" && !docId) {
|
|
4826
|
+
try {
|
|
4827
|
+
const findSingleArgs = {};
|
|
4828
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
4829
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
4830
|
+
if (singleDoc?.documentId) {
|
|
4831
|
+
docId = singleDoc.documentId;
|
|
4832
|
+
}
|
|
4833
|
+
} catch {
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
const payload = {};
|
|
4837
|
+
for (const [key, rawVal] of Object.entries(row)) {
|
|
4838
|
+
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
4839
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
4840
|
+
if (key.includes(".")) {
|
|
4841
|
+
const [parent, child] = key.split(".");
|
|
4842
|
+
const parentDef = attrs[parent];
|
|
4843
|
+
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
4844
|
+
const comp = strapi.components[parentDef.component];
|
|
4845
|
+
const childDef = comp?.attributes?.[child];
|
|
4846
|
+
if (childDef) {
|
|
4847
|
+
if (!payload[parent]) payload[parent] = {};
|
|
4848
|
+
let val = rawVal;
|
|
4849
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4850
|
+
val = null;
|
|
4851
|
+
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
4852
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
4853
|
+
val = isNaN(parsedInt) ? null : parsedInt;
|
|
4854
|
+
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
4855
|
+
const parsedFloat = parseFloat(rawVal);
|
|
4856
|
+
val = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4857
|
+
} else if (childDef.type === "boolean") {
|
|
4858
|
+
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4859
|
+
} else if (childDef.type === "json") {
|
|
4860
|
+
try {
|
|
4861
|
+
val = JSON.parse(rawVal);
|
|
4862
|
+
} catch {
|
|
4863
|
+
val = rawVal;
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
payload[parent][child] = val;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
continue;
|
|
4870
|
+
}
|
|
4871
|
+
let attrDef = attrs[key];
|
|
4872
|
+
if (!attrDef) {
|
|
4873
|
+
const matchKey = Object.keys(attrs).find(
|
|
4874
|
+
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
4875
|
+
);
|
|
4876
|
+
if (matchKey) {
|
|
4877
|
+
attrDef = attrs[matchKey];
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
if (!attrDef) continue;
|
|
4881
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4882
|
+
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
4883
|
+
payload[key] = null;
|
|
4884
|
+
}
|
|
4885
|
+
continue;
|
|
4886
|
+
}
|
|
4589
4887
|
switch (attrDef.type) {
|
|
4590
|
-
case "string":
|
|
4591
|
-
case "text":
|
|
4592
|
-
case "richtext":
|
|
4593
|
-
sampleVal = `Sample ${attrName}`;
|
|
4594
|
-
break;
|
|
4595
4888
|
case "integer":
|
|
4596
|
-
case "biginteger":
|
|
4889
|
+
case "biginteger": {
|
|
4890
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
4891
|
+
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
4892
|
+
break;
|
|
4893
|
+
}
|
|
4597
4894
|
case "float":
|
|
4598
|
-
case "decimal":
|
|
4599
|
-
|
|
4895
|
+
case "decimal": {
|
|
4896
|
+
const parsedFloat = parseFloat(rawVal);
|
|
4897
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4600
4898
|
break;
|
|
4899
|
+
}
|
|
4601
4900
|
case "boolean":
|
|
4602
|
-
|
|
4901
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4603
4902
|
break;
|
|
4604
|
-
case "
|
|
4605
|
-
|
|
4903
|
+
case "json":
|
|
4904
|
+
try {
|
|
4905
|
+
payload[key] = JSON.parse(rawVal);
|
|
4906
|
+
} catch {
|
|
4907
|
+
payload[key] = rawVal;
|
|
4908
|
+
}
|
|
4606
4909
|
break;
|
|
4607
|
-
case "
|
|
4608
|
-
|
|
4910
|
+
case "dynamiczone": {
|
|
4911
|
+
try {
|
|
4912
|
+
const parsed = JSON.parse(rawVal);
|
|
4913
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
4914
|
+
payload[key] = parsed;
|
|
4915
|
+
}
|
|
4916
|
+
} catch {
|
|
4917
|
+
}
|
|
4609
4918
|
break;
|
|
4610
|
-
|
|
4611
|
-
|
|
4919
|
+
}
|
|
4920
|
+
case "component": {
|
|
4921
|
+
if (attrDef.repeatable) {
|
|
4922
|
+
try {
|
|
4923
|
+
const parsed = JSON.parse(rawVal);
|
|
4924
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
4925
|
+
payload[key] = parsed;
|
|
4926
|
+
}
|
|
4927
|
+
} catch {
|
|
4928
|
+
}
|
|
4929
|
+
} else {
|
|
4930
|
+
try {
|
|
4931
|
+
const parsed = JSON.parse(rawVal);
|
|
4932
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
4933
|
+
payload[key] = parsed;
|
|
4934
|
+
}
|
|
4935
|
+
} catch {
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4612
4938
|
break;
|
|
4939
|
+
}
|
|
4940
|
+
case "relation": {
|
|
4941
|
+
try {
|
|
4942
|
+
const parsed = JSON.parse(rawVal);
|
|
4943
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
4944
|
+
payload[key] = parsed;
|
|
4945
|
+
break;
|
|
4946
|
+
}
|
|
4947
|
+
} catch {
|
|
4948
|
+
}
|
|
4949
|
+
if (isValidDocId(rawVal)) {
|
|
4950
|
+
payload[key] = rawVal;
|
|
4951
|
+
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
4952
|
+
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
4953
|
+
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
4954
|
+
payload[key] = parts;
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
break;
|
|
4958
|
+
}
|
|
4959
|
+
case "media": {
|
|
4960
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
4961
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
4962
|
+
} else {
|
|
4963
|
+
try {
|
|
4964
|
+
const parsed = JSON.parse(rawVal);
|
|
4965
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
4966
|
+
payload[key] = parsed;
|
|
4967
|
+
}
|
|
4968
|
+
} catch {
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
break;
|
|
4972
|
+
}
|
|
4613
4973
|
default:
|
|
4614
|
-
|
|
4974
|
+
payload[key] = rawVal;
|
|
4615
4975
|
}
|
|
4616
|
-
sampleRow.push(sampleVal);
|
|
4617
|
-
}
|
|
4618
|
-
const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
|
|
4619
|
-
return {
|
|
4620
|
-
filename: `${ct.info?.singularName || "template"}_template.csv`,
|
|
4621
|
-
csvContent,
|
|
4622
|
-
headers
|
|
4623
|
-
};
|
|
4624
|
-
},
|
|
4625
|
-
/**
|
|
4626
|
-
* Exports entries of a collection to CSV with deep nested component support,
|
|
4627
|
-
* RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
|
|
4628
|
-
*/
|
|
4629
|
-
async exportCollectionCsv(uid, options = {}) {
|
|
4630
|
-
const ct = strapi.contentTypes[uid];
|
|
4631
|
-
if (!ct) {
|
|
4632
|
-
throw new Error(`Content type ${uid} not found`);
|
|
4633
|
-
}
|
|
4634
|
-
const allComponents = strapi.components || {};
|
|
4635
|
-
const fields = discoverExportFields(ct, allComponents);
|
|
4636
|
-
const filters = {};
|
|
4637
|
-
const { attribute, filterOperator, filterQuery, search, documentIds } = options;
|
|
4638
|
-
if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
|
|
4639
|
-
filters.documentId = { $in: documentIds };
|
|
4640
4976
|
}
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4977
|
+
try {
|
|
4978
|
+
if (mode === "update") {
|
|
4979
|
+
if (!docId) {
|
|
4980
|
+
stats.skipped++;
|
|
4981
|
+
stats.errors.push({
|
|
4982
|
+
row: rowNumber,
|
|
4983
|
+
error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
|
|
4984
|
+
});
|
|
4985
|
+
continue;
|
|
4986
|
+
}
|
|
4987
|
+
const existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
4988
|
+
if (!existing) {
|
|
4989
|
+
stats.skipped++;
|
|
4990
|
+
stats.errors.push({
|
|
4991
|
+
row: rowNumber,
|
|
4992
|
+
documentId: docId,
|
|
4993
|
+
error: `Document with ID "${docId}" not found in database.`
|
|
4994
|
+
});
|
|
4995
|
+
continue;
|
|
4996
|
+
}
|
|
4997
|
+
const resolvedDocId = existing.documentId || docId;
|
|
4998
|
+
const mergedPayload = { ...payload };
|
|
4999
|
+
for (const key of Object.keys(payload)) {
|
|
5000
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5001
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
if (Object.keys(mergedPayload).length === 0) {
|
|
5005
|
+
stats.skipped++;
|
|
5006
|
+
stats.diffs.push({
|
|
5007
|
+
uid,
|
|
5008
|
+
collectionName: ct.info?.displayName || uid,
|
|
5009
|
+
documentId: resolvedDocId,
|
|
5010
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5011
|
+
attribute: "",
|
|
5012
|
+
oldValue: existing,
|
|
5013
|
+
newValue: {},
|
|
5014
|
+
status: "skipped",
|
|
5015
|
+
message: `Skipped (no editable attributes in row ${rowNumber})`
|
|
5016
|
+
});
|
|
5017
|
+
continue;
|
|
5018
|
+
}
|
|
5019
|
+
const updateArgs = {
|
|
5020
|
+
documentId: resolvedDocId,
|
|
5021
|
+
data: mergedPayload
|
|
5022
|
+
};
|
|
5023
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5024
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5025
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5026
|
+
try {
|
|
5027
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5028
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5029
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5030
|
+
} catch (pubErr) {
|
|
5031
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
try {
|
|
5035
|
+
await recordContentHistoryVersion(strapi, {
|
|
5036
|
+
uid,
|
|
5037
|
+
documentId: resolvedDocId,
|
|
5038
|
+
userId: user?.id,
|
|
5039
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5040
|
+
});
|
|
5041
|
+
} catch {
|
|
5042
|
+
}
|
|
5043
|
+
stats.updated++;
|
|
5044
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5045
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5046
|
+
const newVal = payload[attrKey];
|
|
5047
|
+
stats.diffs.push({
|
|
5048
|
+
uid,
|
|
5049
|
+
collectionName: ct.info?.displayName || uid,
|
|
5050
|
+
documentId: resolvedDocId,
|
|
5051
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5052
|
+
attribute: attrKey,
|
|
5053
|
+
oldValue: oldVal,
|
|
5054
|
+
newValue: newVal,
|
|
5055
|
+
status: "updated",
|
|
5056
|
+
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5057
|
+
});
|
|
5058
|
+
}
|
|
5059
|
+
} else if (mode === "create_or_override") {
|
|
5060
|
+
let existing = null;
|
|
5061
|
+
if (docId) {
|
|
5062
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5063
|
+
}
|
|
5064
|
+
if (existing) {
|
|
5065
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5066
|
+
const mergedPayload = { ...payload };
|
|
5067
|
+
for (const key of Object.keys(payload)) {
|
|
5068
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5069
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
const updateArgs = {
|
|
5073
|
+
documentId: resolvedDocId,
|
|
5074
|
+
data: mergedPayload
|
|
5075
|
+
};
|
|
5076
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5077
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5078
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5079
|
+
try {
|
|
5080
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5081
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5082
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5083
|
+
} catch (pubErr) {
|
|
5084
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
try {
|
|
5088
|
+
await recordContentHistoryVersion(strapi, {
|
|
5089
|
+
uid,
|
|
5090
|
+
documentId: resolvedDocId,
|
|
5091
|
+
userId: user?.id,
|
|
5092
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5093
|
+
});
|
|
5094
|
+
} catch {
|
|
5095
|
+
}
|
|
5096
|
+
stats.updated++;
|
|
5097
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5098
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5099
|
+
const newVal = payload[attrKey];
|
|
5100
|
+
stats.diffs.push({
|
|
5101
|
+
uid,
|
|
5102
|
+
collectionName: ct.info?.displayName || uid,
|
|
5103
|
+
documentId: resolvedDocId,
|
|
5104
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5105
|
+
attribute: attrKey,
|
|
5106
|
+
oldValue: oldVal,
|
|
5107
|
+
newValue: newVal,
|
|
5108
|
+
status: "updated",
|
|
5109
|
+
message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5110
|
+
});
|
|
5111
|
+
}
|
|
5112
|
+
continue;
|
|
5113
|
+
}
|
|
5114
|
+
const createArgs = { data: payload };
|
|
5115
|
+
if (rowLocale) createArgs.locale = rowLocale;
|
|
5116
|
+
const created = await strapi.documents(uid).create(createArgs);
|
|
5117
|
+
const newDocId = created?.documentId || docId;
|
|
5118
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
5119
|
+
try {
|
|
5120
|
+
const pubArgs = { documentId: newDocId };
|
|
5121
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5122
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5123
|
+
} catch (pubErr) {
|
|
5124
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
try {
|
|
5128
|
+
await recordContentHistoryVersion(strapi, {
|
|
5129
|
+
uid,
|
|
5130
|
+
documentId: newDocId,
|
|
5131
|
+
userId: user?.id,
|
|
5132
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5133
|
+
});
|
|
5134
|
+
} catch {
|
|
5135
|
+
}
|
|
5136
|
+
stats.created++;
|
|
5137
|
+
if (!docId) {
|
|
5138
|
+
stats.newWithoutDocId++;
|
|
5139
|
+
}
|
|
5140
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5141
|
+
stats.diffs.push({
|
|
5142
|
+
uid,
|
|
5143
|
+
collectionName: ct.info?.displayName || uid,
|
|
5144
|
+
documentId: newDocId,
|
|
5145
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5146
|
+
attribute: attrKey,
|
|
5147
|
+
oldValue: null,
|
|
5148
|
+
newValue: payload[attrKey],
|
|
5149
|
+
status: "created",
|
|
5150
|
+
message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
|
|
5151
|
+
});
|
|
5152
|
+
}
|
|
5153
|
+
} else {
|
|
5154
|
+
let existing = null;
|
|
5155
|
+
if (docId) {
|
|
5156
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5157
|
+
}
|
|
5158
|
+
if (existing) {
|
|
5159
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5160
|
+
const mergedPayload = { ...payload };
|
|
5161
|
+
for (const key of Object.keys(payload)) {
|
|
5162
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5163
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
const updateArgs = {
|
|
5167
|
+
documentId: resolvedDocId,
|
|
5168
|
+
data: mergedPayload
|
|
5169
|
+
};
|
|
5170
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5171
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5172
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5173
|
+
try {
|
|
5174
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5175
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5176
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5177
|
+
} catch (pubErr) {
|
|
5178
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
try {
|
|
5182
|
+
await recordContentHistoryVersion(strapi, {
|
|
5183
|
+
uid,
|
|
5184
|
+
documentId: resolvedDocId,
|
|
5185
|
+
userId: user?.id,
|
|
5186
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5187
|
+
});
|
|
5188
|
+
} catch {
|
|
5189
|
+
}
|
|
5190
|
+
stats.updated++;
|
|
5191
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5192
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5193
|
+
const newVal = payload[attrKey];
|
|
5194
|
+
stats.diffs.push({
|
|
5195
|
+
uid,
|
|
5196
|
+
collectionName: ct.info?.displayName || uid,
|
|
5197
|
+
documentId: resolvedDocId,
|
|
5198
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5199
|
+
attribute: attrKey,
|
|
5200
|
+
oldValue: oldVal,
|
|
5201
|
+
newValue: newVal,
|
|
5202
|
+
status: "updated",
|
|
5203
|
+
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5204
|
+
});
|
|
5205
|
+
}
|
|
5206
|
+
continue;
|
|
5207
|
+
}
|
|
5208
|
+
const createArgs = { data: payload };
|
|
5209
|
+
if (rowLocale) createArgs.locale = rowLocale;
|
|
5210
|
+
const created = await strapi.documents(uid).create(createArgs);
|
|
5211
|
+
const newDocId = created?.documentId || docId;
|
|
5212
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
5213
|
+
try {
|
|
5214
|
+
const pubArgs = { documentId: newDocId };
|
|
5215
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5216
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5217
|
+
} catch (pubErr) {
|
|
5218
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
try {
|
|
5222
|
+
await recordContentHistoryVersion(strapi, {
|
|
5223
|
+
uid,
|
|
5224
|
+
documentId: newDocId,
|
|
5225
|
+
userId: user?.id,
|
|
5226
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5227
|
+
});
|
|
5228
|
+
} catch {
|
|
5229
|
+
}
|
|
5230
|
+
stats.created++;
|
|
5231
|
+
if (!docId) {
|
|
5232
|
+
stats.newWithoutDocId++;
|
|
5233
|
+
}
|
|
5234
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5235
|
+
stats.diffs.push({
|
|
5236
|
+
uid,
|
|
5237
|
+
collectionName: ct.info?.displayName || uid,
|
|
5238
|
+
documentId: newDocId,
|
|
5239
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5240
|
+
attribute: attrKey,
|
|
5241
|
+
oldValue: null,
|
|
5242
|
+
newValue: payload[attrKey],
|
|
5243
|
+
status: "created",
|
|
5244
|
+
message: docId ? `Created "${attrKey}" as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created "${attrKey}" as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
} catch (err) {
|
|
5249
|
+
stats.errors.push({
|
|
5250
|
+
row: rowNumber,
|
|
5251
|
+
documentId: docId || void 0,
|
|
5252
|
+
error: extractDetailedErrorMessage(err)
|
|
5253
|
+
});
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
return stats;
|
|
5257
|
+
}
|
|
5258
|
+
async function recordCsvAuditLog(strapi, ct, uid, batchId, stats, user) {
|
|
5259
|
+
if (stats.updated === 0 && stats.created === 0) return;
|
|
5260
|
+
try {
|
|
5261
|
+
const auditService2 = strapi.plugin("bulk-updater").service("audit");
|
|
5262
|
+
if (!auditService2) return;
|
|
5263
|
+
const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
|
|
5264
|
+
if (!isEnabled) return;
|
|
5265
|
+
const userEmail = user?.email || "admin@strapi.io";
|
|
5266
|
+
const userName = user ? `${user.firstname || ""} ${user.lastname || ""}`.trim() : "CSV Importer";
|
|
5267
|
+
const collectionDisplayName = ct.info?.displayName || uid;
|
|
5268
|
+
const updatedDiffs = stats.diffs.filter(
|
|
5269
|
+
(d) => d.status === "updated" || d.status === "created"
|
|
5270
|
+
);
|
|
5271
|
+
const uniqueAttributes = Array.from(
|
|
5272
|
+
new Set(updatedDiffs.map((d) => d.attribute))
|
|
5273
|
+
).filter(Boolean);
|
|
5274
|
+
const attrListStr = uniqueAttributes.length > 0 ? uniqueAttributes.join(", ") : "fields";
|
|
5275
|
+
const batchAttribute = `CSV [${collectionDisplayName}]: ${attrListStr}`;
|
|
5276
|
+
let batchValue = "";
|
|
5277
|
+
if (updatedDiffs.length === 1) {
|
|
5278
|
+
const d = updatedDiffs[0];
|
|
5279
|
+
const newStr = d.newValue !== null && d.newValue !== void 0 ? String(d.newValue) : "(empty)";
|
|
5280
|
+
batchValue = `"${d.label}": ${d.attribute} → "${newStr}"`;
|
|
5281
|
+
} else if (updatedDiffs.length > 1 && stats.updated === 1 && stats.created === 0) {
|
|
5282
|
+
const d = updatedDiffs[0];
|
|
5283
|
+
const changes = updatedDiffs.map((ud) => `${ud.attribute}: "${ud.newValue ?? ""}"`).join(", ");
|
|
5284
|
+
batchValue = `"${d.label}": ${changes}`;
|
|
5285
|
+
} else {
|
|
5286
|
+
const sampleVal = updatedDiffs[0] ? ` (e.g. ${updatedDiffs[0].attribute}: "${updatedDiffs[0].newValue ?? ""}")` : "";
|
|
5287
|
+
batchValue = `${stats.updated} updated, ${stats.created} created in ${collectionDisplayName}${sampleVal}`;
|
|
5288
|
+
}
|
|
5289
|
+
const batchSummary = {
|
|
5290
|
+
batchId,
|
|
5291
|
+
attribute: batchAttribute,
|
|
5292
|
+
value: batchValue,
|
|
5293
|
+
updatedCount: stats.updated + stats.created,
|
|
5294
|
+
skippedCount: stats.skipped,
|
|
5295
|
+
totalTargets: stats.total
|
|
5296
|
+
};
|
|
5297
|
+
const userObj = {
|
|
5298
|
+
email: userEmail || "admin@strapi.io",
|
|
5299
|
+
firstname: userName || "Admin"
|
|
5300
|
+
};
|
|
5301
|
+
if (typeof auditService2.recordBatch === "function") {
|
|
5302
|
+
await auditService2.recordBatch(batchSummary, stats.diffs, userObj);
|
|
5303
|
+
} else if (typeof auditService2.logOperation === "function") {
|
|
5304
|
+
await auditService2.logOperation({
|
|
5305
|
+
...batchSummary,
|
|
5306
|
+
userEmail,
|
|
5307
|
+
userName,
|
|
5308
|
+
diffs: stats.diffs
|
|
5309
|
+
});
|
|
5310
|
+
}
|
|
5311
|
+
} catch (auditErr) {
|
|
5312
|
+
strapi.log.warn("[bulk-updater] Failed logging CSV import audit:", auditErr);
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
const csvService = ({ strapi }) => ({
|
|
5316
|
+
/**
|
|
5317
|
+
* Generates a sample template CSV with schema attribute headers and sample values
|
|
5318
|
+
*/
|
|
5319
|
+
async getTemplateCsv(uid) {
|
|
5320
|
+
const ct = strapi.contentTypes[uid];
|
|
5321
|
+
if (!ct) {
|
|
5322
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5323
|
+
}
|
|
5324
|
+
const attrs = ct.attributes || {};
|
|
5325
|
+
const excluded = ["createdAt", "updatedAt", "publishedAt", "createdBy", "updatedBy", "localizations"];
|
|
5326
|
+
const headers = ["documentId"];
|
|
5327
|
+
const sampleRow = ["(Leave empty to create new, or enter existing docId)"];
|
|
5328
|
+
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
5329
|
+
if (excluded.includes(attrName)) continue;
|
|
5330
|
+
if (attrDef.type === "relation") continue;
|
|
5331
|
+
if (attrDef.type === "component" || attrDef.type === "dynamiczone") continue;
|
|
5332
|
+
headers.push(attrName);
|
|
5333
|
+
let sampleVal = "";
|
|
5334
|
+
switch (attrDef.type) {
|
|
5335
|
+
case "string":
|
|
5336
|
+
case "text":
|
|
5337
|
+
case "richtext":
|
|
5338
|
+
sampleVal = `Sample ${attrName}`;
|
|
5339
|
+
break;
|
|
5340
|
+
case "integer":
|
|
5341
|
+
case "biginteger":
|
|
5342
|
+
case "float":
|
|
5343
|
+
case "decimal":
|
|
5344
|
+
sampleVal = "10";
|
|
5345
|
+
break;
|
|
5346
|
+
case "boolean":
|
|
5347
|
+
sampleVal = "true";
|
|
5348
|
+
break;
|
|
5349
|
+
case "email":
|
|
5350
|
+
sampleVal = "sample@example.com";
|
|
5351
|
+
break;
|
|
5352
|
+
case "enumeration":
|
|
5353
|
+
sampleVal = attrDef.enum?.[0] || "option1";
|
|
5354
|
+
break;
|
|
5355
|
+
case "customField":
|
|
5356
|
+
sampleVal = "value";
|
|
5357
|
+
break;
|
|
5358
|
+
default:
|
|
5359
|
+
sampleVal = "sample";
|
|
5360
|
+
}
|
|
5361
|
+
sampleRow.push(sampleVal);
|
|
5362
|
+
}
|
|
5363
|
+
const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
|
|
5364
|
+
return {
|
|
5365
|
+
filename: `${ct.info?.singularName || "template"}_template.csv`,
|
|
5366
|
+
csvContent,
|
|
5367
|
+
headers
|
|
5368
|
+
};
|
|
5369
|
+
},
|
|
5370
|
+
/**
|
|
5371
|
+
* Exports entries of a collection to CSV with deep nested component support,
|
|
5372
|
+
* RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
|
|
5373
|
+
*/
|
|
5374
|
+
async exportCollectionCsv(uid, options = {}) {
|
|
5375
|
+
const ct = strapi.contentTypes[uid];
|
|
5376
|
+
if (!ct) {
|
|
5377
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5378
|
+
}
|
|
5379
|
+
const allComponents = strapi.components || {};
|
|
5380
|
+
const fields = discoverExportFields(ct, allComponents);
|
|
5381
|
+
const filters = {};
|
|
5382
|
+
const { attribute, filterOperator, filterQuery, search, documentIds } = options;
|
|
5383
|
+
if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
|
|
5384
|
+
filters.documentId = { $in: documentIds };
|
|
5385
|
+
}
|
|
5386
|
+
if (attribute && filterOperator && filterOperator !== "none") {
|
|
5387
|
+
const isNested = attribute.includes(".");
|
|
5388
|
+
if (filterOperator === "empty") {
|
|
5389
|
+
if (isNested) {
|
|
5390
|
+
const [parent, child] = attribute.split(".");
|
|
5391
|
+
filters[parent] = { [child]: { $null: true } };
|
|
5392
|
+
} else {
|
|
5393
|
+
filters[attribute] = { $null: true };
|
|
5394
|
+
}
|
|
5395
|
+
} else if (filterOperator === "not_empty") {
|
|
5396
|
+
if (isNested) {
|
|
5397
|
+
const [parent, child] = attribute.split(".");
|
|
5398
|
+
filters[parent] = { [child]: { $notNull: true } };
|
|
5399
|
+
} else {
|
|
5400
|
+
filters[attribute] = { $notNull: true };
|
|
4656
5401
|
}
|
|
4657
5402
|
} else if (filterQuery !== void 0 && filterQuery !== "") {
|
|
4658
5403
|
const opMap = {
|
|
@@ -4690,40 +5435,64 @@ const csvService = ({ strapi }) => ({
|
|
|
4690
5435
|
}
|
|
4691
5436
|
}
|
|
4692
5437
|
const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
|
|
4693
|
-
|
|
5438
|
+
const lines = [];
|
|
5439
|
+
lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
|
|
5440
|
+
let totalExported = 0;
|
|
5441
|
+
const maxLimit = options.limit && options.limit > 0 ? options.limit : Infinity;
|
|
5442
|
+
const CHUNK_SIZE = 1e3;
|
|
4694
5443
|
if (ct.kind === "singleType") {
|
|
4695
5444
|
const findFirstArgs = {};
|
|
4696
5445
|
if (deepPopulate) {
|
|
4697
5446
|
findFirstArgs.populate = deepPopulate;
|
|
4698
5447
|
}
|
|
4699
5448
|
const doc = await strapi.documents(uid).findFirst(findFirstArgs);
|
|
4700
|
-
|
|
5449
|
+
if (doc) {
|
|
5450
|
+
const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(doc, f.key)));
|
|
5451
|
+
lines.push(rowValues.join(","));
|
|
5452
|
+
totalExported = 1;
|
|
5453
|
+
}
|
|
4701
5454
|
} else {
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
5455
|
+
let currentPage = 1;
|
|
5456
|
+
let hasMore = true;
|
|
5457
|
+
while (hasMore && totalExported < maxLimit) {
|
|
5458
|
+
const remaining = maxLimit - totalExported;
|
|
5459
|
+
const currentBatchLimit = Math.min(CHUNK_SIZE, remaining);
|
|
5460
|
+
const findPageArgs = {
|
|
5461
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0,
|
|
5462
|
+
page: currentPage,
|
|
5463
|
+
pageSize: currentBatchLimit,
|
|
5464
|
+
sort: { createdAt: "desc" }
|
|
5465
|
+
};
|
|
5466
|
+
if (deepPopulate) {
|
|
5467
|
+
findPageArgs.populate = deepPopulate;
|
|
5468
|
+
}
|
|
5469
|
+
const chunkEntries = await strapi.documents(uid).findMany(findPageArgs);
|
|
5470
|
+
if (!chunkEntries || chunkEntries.length === 0) {
|
|
5471
|
+
hasMore = false;
|
|
5472
|
+
break;
|
|
5473
|
+
}
|
|
5474
|
+
for (const entry of chunkEntries) {
|
|
5475
|
+
const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(entry, f.key)));
|
|
5476
|
+
lines.push(rowValues.join(","));
|
|
5477
|
+
totalExported++;
|
|
5478
|
+
if (totalExported >= maxLimit) {
|
|
5479
|
+
hasMore = false;
|
|
5480
|
+
break;
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
if (chunkEntries.length < currentBatchLimit) {
|
|
5484
|
+
hasMore = false;
|
|
5485
|
+
} else {
|
|
5486
|
+
currentPage++;
|
|
5487
|
+
}
|
|
4709
5488
|
}
|
|
4710
|
-
entries = await strapi.documents(uid).findMany(findManyArgs);
|
|
4711
|
-
}
|
|
4712
|
-
const lines = [];
|
|
4713
|
-
lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
|
|
4714
|
-
for (const entry of entries || []) {
|
|
4715
|
-
const rowValues = fields.map((f) => {
|
|
4716
|
-
const val = getNestedExportValue(entry, f.key);
|
|
4717
|
-
return escapeCsvValue(val);
|
|
4718
|
-
});
|
|
4719
|
-
lines.push(rowValues.join(","));
|
|
4720
5489
|
}
|
|
4721
5490
|
const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
|
|
4722
5491
|
const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
4723
5492
|
return {
|
|
4724
5493
|
filename,
|
|
4725
5494
|
csvContent,
|
|
4726
|
-
count:
|
|
5495
|
+
count: totalExported,
|
|
4727
5496
|
fields: fields.map((f) => f.key)
|
|
4728
5497
|
};
|
|
4729
5498
|
},
|
|
@@ -4784,7 +5553,7 @@ const csvService = ({ strapi }) => ({
|
|
|
4784
5553
|
const uids = Object.keys(byUid);
|
|
4785
5554
|
if (uids.length === 1) {
|
|
4786
5555
|
const uid = uids[0];
|
|
4787
|
-
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
|
|
5556
|
+
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid], limit: 0 });
|
|
4788
5557
|
}
|
|
4789
5558
|
const rows = [];
|
|
4790
5559
|
const headers = [
|
|
@@ -4835,7 +5604,77 @@ const csvService = ({ strapi }) => ({
|
|
|
4835
5604
|
};
|
|
4836
5605
|
},
|
|
4837
5606
|
/**
|
|
4838
|
-
* Imports
|
|
5607
|
+
* Imports a single chunk of CSV rows (for large dataset streaming/batching).
|
|
5608
|
+
* Aggregates stats across chunks and writes unified audit trail on the final chunk.
|
|
5609
|
+
*/
|
|
5610
|
+
async importChunk(uid, rows, options) {
|
|
5611
|
+
const ct = strapi.contentTypes[uid];
|
|
5612
|
+
if (!ct) {
|
|
5613
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5614
|
+
}
|
|
5615
|
+
cleanupStaleSessions();
|
|
5616
|
+
const batchId = options.batchId || `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5617
|
+
let session = csvImportSessions.get(batchId);
|
|
5618
|
+
if (!session || options.isFirstChunk) {
|
|
5619
|
+
session = {
|
|
5620
|
+
batchId,
|
|
5621
|
+
uid,
|
|
5622
|
+
createdAt: Date.now(),
|
|
5623
|
+
total: 0,
|
|
5624
|
+
created: 0,
|
|
5625
|
+
updated: 0,
|
|
5626
|
+
skipped: 0,
|
|
5627
|
+
newWithoutDocId: 0,
|
|
5628
|
+
errors: [],
|
|
5629
|
+
diffs: []
|
|
5630
|
+
};
|
|
5631
|
+
csvImportSessions.set(batchId, session);
|
|
5632
|
+
}
|
|
5633
|
+
const startRowNumber = options.startRowNumber || session.total + 2;
|
|
5634
|
+
const chunkStats = await processCsvRows(strapi, ct, uid, rows || [], startRowNumber, options);
|
|
5635
|
+
session.total += chunkStats.total;
|
|
5636
|
+
session.created += chunkStats.created;
|
|
5637
|
+
session.updated += chunkStats.updated;
|
|
5638
|
+
session.skipped += chunkStats.skipped;
|
|
5639
|
+
session.newWithoutDocId += chunkStats.newWithoutDocId;
|
|
5640
|
+
session.errors.push(...chunkStats.errors);
|
|
5641
|
+
session.diffs.push(...chunkStats.diffs);
|
|
5642
|
+
if (options.isLastChunk) {
|
|
5643
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, session, options.user);
|
|
5644
|
+
try {
|
|
5645
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5646
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5647
|
+
bulkService.invalidateCollectionCache(uid);
|
|
5648
|
+
}
|
|
5649
|
+
} catch {
|
|
5650
|
+
}
|
|
5651
|
+
const aggregateStats = {
|
|
5652
|
+
total: session.total,
|
|
5653
|
+
created: session.created,
|
|
5654
|
+
updated: session.updated,
|
|
5655
|
+
skipped: session.skipped,
|
|
5656
|
+
newWithoutDocId: session.newWithoutDocId,
|
|
5657
|
+
errors: session.errors,
|
|
5658
|
+
diffs: session.diffs
|
|
5659
|
+
};
|
|
5660
|
+
csvImportSessions.delete(batchId);
|
|
5661
|
+
return {
|
|
5662
|
+
success: true,
|
|
5663
|
+
batchId,
|
|
5664
|
+
chunkStats,
|
|
5665
|
+
stats: aggregateStats,
|
|
5666
|
+
isFinished: true
|
|
5667
|
+
};
|
|
5668
|
+
}
|
|
5669
|
+
return {
|
|
5670
|
+
success: true,
|
|
5671
|
+
batchId,
|
|
5672
|
+
chunkStats,
|
|
5673
|
+
isFinished: false
|
|
5674
|
+
};
|
|
5675
|
+
},
|
|
5676
|
+
/**
|
|
5677
|
+
* Imports CSV records to update existing records or insert/override records (single payload).
|
|
4839
5678
|
*/
|
|
4840
5679
|
async importCsv(uid, csvText, options) {
|
|
4841
5680
|
const ct = strapi.contentTypes[uid];
|
|
@@ -4846,560 +5685,433 @@ const csvService = ({ strapi }) => ({
|
|
|
4846
5685
|
if (!parsedRows || parsedRows.length === 0) {
|
|
4847
5686
|
throw new Error("CSV is empty or could not be parsed.");
|
|
4848
5687
|
}
|
|
4849
|
-
const attrs = ct.attributes || {};
|
|
4850
|
-
const { mode = "auto", publish = true, user } = options;
|
|
4851
|
-
const stats = {
|
|
4852
|
-
total: parsedRows.length,
|
|
4853
|
-
created: 0,
|
|
4854
|
-
updated: 0,
|
|
4855
|
-
skipped: 0,
|
|
4856
|
-
newWithoutDocId: 0,
|
|
4857
|
-
errors: [],
|
|
4858
|
-
diffs: []
|
|
4859
|
-
};
|
|
4860
5688
|
const batchId = `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
if (
|
|
4865
|
-
|
|
4866
|
-
const queryArgs = { documentId: targetDocId };
|
|
4867
|
-
if (docLocale) queryArgs.locale = docLocale;
|
|
4868
|
-
const found = await strapi.documents(uid).findOne(queryArgs);
|
|
4869
|
-
if (found) return found;
|
|
4870
|
-
} catch {
|
|
4871
|
-
}
|
|
4872
|
-
if (/^\d+$/.test(targetDocId)) {
|
|
4873
|
-
try {
|
|
4874
|
-
const queryArgs = {
|
|
4875
|
-
filters: { id: parseInt(targetDocId, 10) },
|
|
4876
|
-
limit: 1
|
|
4877
|
-
};
|
|
4878
|
-
if (docLocale) queryArgs.locale = docLocale;
|
|
4879
|
-
const list = await strapi.documents(uid).findMany(queryArgs);
|
|
4880
|
-
if (list && list.length > 0) return list[0];
|
|
4881
|
-
} catch {
|
|
4882
|
-
}
|
|
5689
|
+
const stats = await processCsvRows(strapi, ct, uid, parsedRows, 2, options);
|
|
5690
|
+
try {
|
|
5691
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5692
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5693
|
+
bulkService.invalidateCollectionCache(uid);
|
|
4883
5694
|
}
|
|
4884
|
-
|
|
5695
|
+
} catch {
|
|
5696
|
+
}
|
|
5697
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, stats, options.user);
|
|
5698
|
+
return {
|
|
5699
|
+
success: true,
|
|
5700
|
+
batchId,
|
|
5701
|
+
stats
|
|
4885
5702
|
};
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
const
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
if (ct.kind === "singleType" && !docId) {
|
|
4906
|
-
try {
|
|
4907
|
-
const findSingleArgs = {};
|
|
4908
|
-
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
4909
|
-
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
4910
|
-
if (singleDoc?.documentId) {
|
|
4911
|
-
docId = singleDoc.documentId;
|
|
4912
|
-
}
|
|
4913
|
-
} catch {
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
const payload = {};
|
|
4917
|
-
for (const [key, rawVal] of Object.entries(row)) {
|
|
4918
|
-
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
4919
|
-
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
4920
|
-
if (key.includes(".")) {
|
|
4921
|
-
const [parent, child] = key.split(".");
|
|
4922
|
-
const parentDef = attrs[parent];
|
|
4923
|
-
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
4924
|
-
const comp = strapi.components[parentDef.component];
|
|
4925
|
-
const childDef = comp?.attributes?.[child];
|
|
4926
|
-
if (childDef) {
|
|
4927
|
-
if (!payload[parent]) payload[parent] = {};
|
|
4928
|
-
let val = rawVal;
|
|
4929
|
-
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4930
|
-
val = null;
|
|
4931
|
-
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
4932
|
-
const parsedInt = parseInt(rawVal, 10);
|
|
4933
|
-
val = isNaN(parsedInt) ? null : parsedInt;
|
|
4934
|
-
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
4935
|
-
const parsedFloat = parseFloat(rawVal);
|
|
4936
|
-
val = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4937
|
-
} else if (childDef.type === "boolean") {
|
|
4938
|
-
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4939
|
-
} else if (childDef.type === "json") {
|
|
4940
|
-
try {
|
|
4941
|
-
val = JSON.parse(rawVal);
|
|
4942
|
-
} catch {
|
|
4943
|
-
val = rawVal;
|
|
4944
|
-
}
|
|
4945
|
-
}
|
|
4946
|
-
payload[parent][child] = val;
|
|
4947
|
-
}
|
|
4948
|
-
}
|
|
4949
|
-
continue;
|
|
4950
|
-
}
|
|
4951
|
-
let attrDef = attrs[key];
|
|
4952
|
-
if (!attrDef) {
|
|
4953
|
-
const matchKey = Object.keys(attrs).find(
|
|
4954
|
-
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
4955
|
-
);
|
|
4956
|
-
if (matchKey) {
|
|
4957
|
-
attrDef = attrs[matchKey];
|
|
4958
|
-
}
|
|
4959
|
-
}
|
|
4960
|
-
if (!attrDef) continue;
|
|
4961
|
-
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4962
|
-
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
4963
|
-
payload[key] = null;
|
|
4964
|
-
}
|
|
4965
|
-
continue;
|
|
4966
|
-
}
|
|
4967
|
-
switch (attrDef.type) {
|
|
4968
|
-
case "integer":
|
|
4969
|
-
case "biginteger": {
|
|
4970
|
-
const parsedInt = parseInt(rawVal, 10);
|
|
4971
|
-
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
4972
|
-
break;
|
|
4973
|
-
}
|
|
4974
|
-
case "float":
|
|
4975
|
-
case "decimal": {
|
|
4976
|
-
const parsedFloat = parseFloat(rawVal);
|
|
4977
|
-
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4978
|
-
break;
|
|
4979
|
-
}
|
|
4980
|
-
case "boolean":
|
|
4981
|
-
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4982
|
-
break;
|
|
4983
|
-
case "json":
|
|
4984
|
-
try {
|
|
4985
|
-
payload[key] = JSON.parse(rawVal);
|
|
4986
|
-
} catch {
|
|
4987
|
-
payload[key] = rawVal;
|
|
4988
|
-
}
|
|
4989
|
-
break;
|
|
4990
|
-
case "dynamiczone": {
|
|
4991
|
-
try {
|
|
4992
|
-
const parsed = JSON.parse(rawVal);
|
|
4993
|
-
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
4994
|
-
payload[key] = parsed;
|
|
4995
|
-
}
|
|
4996
|
-
} catch {
|
|
4997
|
-
}
|
|
4998
|
-
break;
|
|
4999
|
-
}
|
|
5000
|
-
case "component": {
|
|
5001
|
-
if (attrDef.repeatable) {
|
|
5002
|
-
try {
|
|
5003
|
-
const parsed = JSON.parse(rawVal);
|
|
5004
|
-
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
5005
|
-
payload[key] = parsed;
|
|
5006
|
-
}
|
|
5007
|
-
} catch {
|
|
5008
|
-
}
|
|
5009
|
-
} else {
|
|
5010
|
-
try {
|
|
5011
|
-
const parsed = JSON.parse(rawVal);
|
|
5012
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5013
|
-
payload[key] = parsed;
|
|
5014
|
-
}
|
|
5015
|
-
} catch {
|
|
5016
|
-
}
|
|
5017
|
-
}
|
|
5018
|
-
break;
|
|
5019
|
-
}
|
|
5020
|
-
case "relation": {
|
|
5021
|
-
try {
|
|
5022
|
-
const parsed = JSON.parse(rawVal);
|
|
5023
|
-
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5024
|
-
payload[key] = parsed;
|
|
5025
|
-
break;
|
|
5026
|
-
}
|
|
5027
|
-
} catch {
|
|
5028
|
-
}
|
|
5029
|
-
if (isValidDocId(rawVal)) {
|
|
5030
|
-
payload[key] = rawVal;
|
|
5031
|
-
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5032
|
-
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5033
|
-
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5034
|
-
payload[key] = parts;
|
|
5035
|
-
}
|
|
5036
|
-
}
|
|
5037
|
-
break;
|
|
5038
|
-
}
|
|
5039
|
-
case "media": {
|
|
5040
|
-
if (/^\d+$/.test(rawVal.trim())) {
|
|
5041
|
-
payload[key] = parseInt(rawVal.trim(), 10);
|
|
5042
|
-
} else {
|
|
5043
|
-
try {
|
|
5044
|
-
const parsed = JSON.parse(rawVal);
|
|
5045
|
-
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
5046
|
-
payload[key] = parsed;
|
|
5047
|
-
}
|
|
5048
|
-
} catch {
|
|
5049
|
-
}
|
|
5050
|
-
}
|
|
5051
|
-
break;
|
|
5052
|
-
}
|
|
5053
|
-
default:
|
|
5054
|
-
payload[key] = rawVal;
|
|
5055
|
-
}
|
|
5703
|
+
},
|
|
5704
|
+
/**
|
|
5705
|
+
* Executes a pending CSV batch approved by Checker or queued for off-peak execution
|
|
5706
|
+
*/
|
|
5707
|
+
async executeCsvPendingBatch(uid, rows, batchId, options = {}, checkerUser) {
|
|
5708
|
+
const ct = strapi.contentTypes[uid];
|
|
5709
|
+
if (!ct) {
|
|
5710
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5711
|
+
}
|
|
5712
|
+
const effectiveUser = checkerUser || options.user;
|
|
5713
|
+
const stats = await processCsvRows(strapi, ct, uid, rows || [], 2, {
|
|
5714
|
+
...options,
|
|
5715
|
+
user: effectiveUser
|
|
5716
|
+
});
|
|
5717
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, stats, effectiveUser);
|
|
5718
|
+
try {
|
|
5719
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5720
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5721
|
+
bulkService.invalidateCollectionCache(uid);
|
|
5056
5722
|
}
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5723
|
+
} catch {
|
|
5724
|
+
}
|
|
5725
|
+
return {
|
|
5726
|
+
success: true,
|
|
5727
|
+
batchId,
|
|
5728
|
+
stats
|
|
5729
|
+
};
|
|
5730
|
+
},
|
|
5731
|
+
/**
|
|
5732
|
+
* Pre-Flight Dry-Run Validation: Non-mutating analysis verifying document existence,
|
|
5733
|
+
* data types, schema integrity, and quota rule violations before execution.
|
|
5734
|
+
*/
|
|
5735
|
+
async validatePreflight(uid, rows, options = {}) {
|
|
5736
|
+
const ct = strapi.contentTypes[uid];
|
|
5737
|
+
if (!ct) {
|
|
5738
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5739
|
+
}
|
|
5740
|
+
const { mode = "auto" } = options;
|
|
5741
|
+
const attrs = ct.attributes || {};
|
|
5742
|
+
let validationRulesService2 = null;
|
|
5743
|
+
let activeRules = [];
|
|
5744
|
+
try {
|
|
5745
|
+
validationRulesService2 = strapi.plugin("bulk-updater").service("validationRules");
|
|
5746
|
+
if (validationRulesService2) {
|
|
5747
|
+
const rulesRes = await validationRulesService2.getRules();
|
|
5748
|
+
activeRules = (rulesRes || []).filter((r) => r.enabled);
|
|
5749
|
+
}
|
|
5750
|
+
} catch {
|
|
5751
|
+
}
|
|
5752
|
+
let validRows = 0;
|
|
5753
|
+
let estimatedUpdates = 0;
|
|
5754
|
+
let estimatedCreates = 0;
|
|
5755
|
+
const errors2 = [];
|
|
5756
|
+
const warnings = [];
|
|
5757
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5758
|
+
const rowNumber = i + 2;
|
|
5759
|
+
const row = rows[i];
|
|
5760
|
+
const docId = extractDocId(row);
|
|
5761
|
+
const rowLocale = extractLocale(row);
|
|
5762
|
+
let rowHasError = false;
|
|
5763
|
+
let existing = null;
|
|
5764
|
+
if (docId) {
|
|
5765
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5766
|
+
}
|
|
5767
|
+
if (mode === "update") {
|
|
5768
|
+
if (!docId) {
|
|
5769
|
+
errors2.push({
|
|
5770
|
+
row: rowNumber,
|
|
5771
|
+
error: "Missing or invalid documentId for Update Only mode."
|
|
5772
|
+
});
|
|
5773
|
+
rowHasError = true;
|
|
5774
|
+
} else if (!existing) {
|
|
5775
|
+
errors2.push({
|
|
5776
|
+
row: rowNumber,
|
|
5777
|
+
documentId: docId,
|
|
5778
|
+
error: `Document with ID "${docId}" not found in database.`
|
|
5779
|
+
});
|
|
5780
|
+
rowHasError = true;
|
|
5781
|
+
} else {
|
|
5782
|
+
estimatedUpdates++;
|
|
5783
|
+
}
|
|
5784
|
+
} else if (mode === "create_or_override") {
|
|
5785
|
+
if (existing) {
|
|
5786
|
+
estimatedUpdates++;
|
|
5787
|
+
} else {
|
|
5788
|
+
estimatedCreates++;
|
|
5789
|
+
}
|
|
5790
|
+
} else {
|
|
5791
|
+
if (existing) {
|
|
5792
|
+
estimatedUpdates++;
|
|
5793
|
+
} else {
|
|
5794
|
+
estimatedCreates++;
|
|
5795
|
+
if (docId) {
|
|
5796
|
+
warnings.push({
|
|
5071
5797
|
row: rowNumber,
|
|
5072
5798
|
documentId: docId,
|
|
5073
|
-
|
|
5074
|
-
});
|
|
5075
|
-
continue;
|
|
5076
|
-
}
|
|
5077
|
-
const resolvedDocId = existing.documentId || docId;
|
|
5078
|
-
const mergedPayload = { ...payload };
|
|
5079
|
-
for (const key of Object.keys(payload)) {
|
|
5080
|
-
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5081
|
-
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5082
|
-
}
|
|
5083
|
-
}
|
|
5084
|
-
if (Object.keys(mergedPayload).length === 0) {
|
|
5085
|
-
stats.skipped++;
|
|
5086
|
-
stats.diffs.push({
|
|
5087
|
-
uid,
|
|
5088
|
-
collectionName: ct.info?.displayName || uid,
|
|
5089
|
-
documentId: resolvedDocId,
|
|
5090
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5091
|
-
attribute: "",
|
|
5092
|
-
oldValue: existing,
|
|
5093
|
-
newValue: {},
|
|
5094
|
-
status: "skipped",
|
|
5095
|
-
message: `Skipped (no editable attributes in row ${rowNumber})`
|
|
5096
|
-
});
|
|
5097
|
-
continue;
|
|
5098
|
-
}
|
|
5099
|
-
const updateArgs = {
|
|
5100
|
-
documentId: resolvedDocId,
|
|
5101
|
-
data: mergedPayload
|
|
5102
|
-
};
|
|
5103
|
-
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5104
|
-
await strapi.documents(uid).update(updateArgs);
|
|
5105
|
-
if (publish && ct.options?.draftAndPublish) {
|
|
5106
|
-
try {
|
|
5107
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5108
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5109
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5110
|
-
} catch (pubErr) {
|
|
5111
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5112
|
-
}
|
|
5113
|
-
}
|
|
5114
|
-
try {
|
|
5115
|
-
await recordContentHistoryVersion(strapi, {
|
|
5116
|
-
uid,
|
|
5117
|
-
documentId: resolvedDocId,
|
|
5118
|
-
userId: user?.id,
|
|
5119
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5120
|
-
});
|
|
5121
|
-
} catch {
|
|
5122
|
-
}
|
|
5123
|
-
stats.updated++;
|
|
5124
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5125
|
-
const oldVal = existing?.[attrKey] ?? null;
|
|
5126
|
-
const newVal = payload[attrKey];
|
|
5127
|
-
stats.diffs.push({
|
|
5128
|
-
uid,
|
|
5129
|
-
collectionName: ct.info?.displayName || uid,
|
|
5130
|
-
documentId: resolvedDocId,
|
|
5131
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5132
|
-
attribute: attrKey,
|
|
5133
|
-
oldValue: oldVal,
|
|
5134
|
-
newValue: newVal,
|
|
5135
|
-
status: "updated",
|
|
5136
|
-
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5799
|
+
warning: `Document ID "${docId}" not found in DB; will create as a new entry.`
|
|
5137
5800
|
});
|
|
5138
5801
|
}
|
|
5139
|
-
}
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5161
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5162
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5163
|
-
} catch (pubErr) {
|
|
5164
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
try {
|
|
5168
|
-
await recordContentHistoryVersion(strapi, {
|
|
5169
|
-
uid,
|
|
5170
|
-
documentId: resolvedDocId,
|
|
5171
|
-
userId: user?.id,
|
|
5172
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
for (const [colKey, rawVal] of Object.entries(row)) {
|
|
5805
|
+
if (!rawVal || rawVal.trim() === "") continue;
|
|
5806
|
+
const normalizedKey = colKey.toLowerCase().replace(/[\s_-]/g, "");
|
|
5807
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5808
|
+
let attrDef = attrs[colKey];
|
|
5809
|
+
if (!attrDef && !colKey.includes(".")) {
|
|
5810
|
+
const matchKey = Object.keys(attrs).find(
|
|
5811
|
+
(k) => k.toLowerCase() === colKey.toLowerCase() || k.toLowerCase() === colKey.toLowerCase().replace(/\s+/g, "")
|
|
5812
|
+
);
|
|
5813
|
+
if (matchKey) attrDef = attrs[matchKey];
|
|
5814
|
+
}
|
|
5815
|
+
if (attrDef) {
|
|
5816
|
+
if (attrDef.type === "integer" || attrDef.type === "biginteger") {
|
|
5817
|
+
const num = parseInt(rawVal, 10);
|
|
5818
|
+
if (isNaN(num)) {
|
|
5819
|
+
errors2.push({
|
|
5820
|
+
row: rowNumber,
|
|
5821
|
+
documentId: docId || void 0,
|
|
5822
|
+
error: `Column "${colKey}" expects an integer, received: "${rawVal}"`
|
|
5173
5823
|
});
|
|
5174
|
-
|
|
5824
|
+
rowHasError = true;
|
|
5175
5825
|
}
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
documentId: resolvedDocId,
|
|
5184
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5185
|
-
attribute: attrKey,
|
|
5186
|
-
oldValue: oldVal,
|
|
5187
|
-
newValue: newVal,
|
|
5188
|
-
status: "updated",
|
|
5189
|
-
message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5826
|
+
} else if (attrDef.type === "float" || attrDef.type === "decimal") {
|
|
5827
|
+
const num = parseFloat(rawVal);
|
|
5828
|
+
if (isNaN(num)) {
|
|
5829
|
+
errors2.push({
|
|
5830
|
+
row: rowNumber,
|
|
5831
|
+
documentId: docId || void 0,
|
|
5832
|
+
error: `Column "${colKey}" expects a numeric float/decimal, received: "${rawVal}"`
|
|
5190
5833
|
});
|
|
5834
|
+
rowHasError = true;
|
|
5191
5835
|
}
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
try {
|
|
5200
|
-
const pubArgs = { documentId: newDocId };
|
|
5201
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5202
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5203
|
-
} catch (pubErr) {
|
|
5204
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5207
|
-
try {
|
|
5208
|
-
await recordContentHistoryVersion(strapi, {
|
|
5209
|
-
uid,
|
|
5210
|
-
documentId: newDocId,
|
|
5211
|
-
userId: user?.id,
|
|
5212
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5213
|
-
});
|
|
5214
|
-
} catch {
|
|
5215
|
-
}
|
|
5216
|
-
stats.created++;
|
|
5217
|
-
if (!docId) {
|
|
5218
|
-
stats.newWithoutDocId++;
|
|
5219
|
-
}
|
|
5220
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5221
|
-
stats.diffs.push({
|
|
5222
|
-
uid,
|
|
5223
|
-
collectionName: ct.info?.displayName || uid,
|
|
5224
|
-
documentId: newDocId,
|
|
5225
|
-
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5226
|
-
attribute: attrKey,
|
|
5227
|
-
oldValue: null,
|
|
5228
|
-
newValue: payload[attrKey],
|
|
5229
|
-
status: "created",
|
|
5230
|
-
message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
|
|
5231
|
-
});
|
|
5232
|
-
}
|
|
5233
|
-
} else {
|
|
5234
|
-
let existing = null;
|
|
5235
|
-
if (docId) {
|
|
5236
|
-
existing = await findExistingDocument(docId, rowLocale);
|
|
5237
|
-
}
|
|
5238
|
-
if (existing) {
|
|
5239
|
-
const resolvedDocId = existing.documentId || docId;
|
|
5240
|
-
const mergedPayload = { ...payload };
|
|
5241
|
-
for (const key of Object.keys(payload)) {
|
|
5242
|
-
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5243
|
-
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5244
|
-
}
|
|
5245
|
-
}
|
|
5246
|
-
const updateArgs = {
|
|
5247
|
-
documentId: resolvedDocId,
|
|
5248
|
-
data: mergedPayload
|
|
5249
|
-
};
|
|
5250
|
-
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5251
|
-
await strapi.documents(uid).update(updateArgs);
|
|
5252
|
-
if (publish && ct.options?.draftAndPublish) {
|
|
5253
|
-
try {
|
|
5254
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5255
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5256
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5257
|
-
} catch (pubErr) {
|
|
5258
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5259
|
-
}
|
|
5836
|
+
} else if (attrDef.type === "enumeration" && Array.isArray(attrDef.enum)) {
|
|
5837
|
+
if (!attrDef.enum.includes(rawVal.trim())) {
|
|
5838
|
+
warnings.push({
|
|
5839
|
+
row: rowNumber,
|
|
5840
|
+
documentId: docId || void 0,
|
|
5841
|
+
warning: `Column "${colKey}" value "${rawVal}" is not in defined enum options: [${attrDef.enum.join(", ")}]`
|
|
5842
|
+
});
|
|
5260
5843
|
}
|
|
5844
|
+
} else if (attrDef.type === "json") {
|
|
5261
5845
|
try {
|
|
5262
|
-
|
|
5263
|
-
uid,
|
|
5264
|
-
documentId: resolvedDocId,
|
|
5265
|
-
userId: user?.id,
|
|
5266
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5267
|
-
});
|
|
5846
|
+
JSON.parse(rawVal);
|
|
5268
5847
|
} catch {
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
const newVal = payload[attrKey];
|
|
5274
|
-
stats.diffs.push({
|
|
5275
|
-
uid,
|
|
5276
|
-
collectionName: ct.info?.displayName || uid,
|
|
5277
|
-
documentId: resolvedDocId,
|
|
5278
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5279
|
-
attribute: attrKey,
|
|
5280
|
-
oldValue: oldVal,
|
|
5281
|
-
newValue: newVal,
|
|
5282
|
-
status: "updated",
|
|
5283
|
-
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5848
|
+
warnings.push({
|
|
5849
|
+
row: rowNumber,
|
|
5850
|
+
documentId: docId || void 0,
|
|
5851
|
+
warning: `Column "${colKey}" is type JSON but contains non-JSON text.`
|
|
5284
5852
|
});
|
|
5285
5853
|
}
|
|
5286
|
-
continue;
|
|
5287
5854
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
const
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
const
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5855
|
+
}
|
|
5856
|
+
if (activeRules.length > 0) {
|
|
5857
|
+
for (const rule of activeRules) {
|
|
5858
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) continue;
|
|
5859
|
+
if (rule.includeRichTextHtml && (rawVal.includes("<h") || rawVal.includes("# "))) {
|
|
5860
|
+
const headings = extractHeadingsFromRichText(rawVal);
|
|
5861
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
5862
|
+
const max = rule.quotas?.[tag.toLowerCase()];
|
|
5863
|
+
if (max !== void 0 && count > max) {
|
|
5864
|
+
warnings.push({
|
|
5865
|
+
row: rowNumber,
|
|
5866
|
+
documentId: docId || void 0,
|
|
5867
|
+
warning: `Rule "${rule.name}" quota warning: column "${colKey}" contains ${count} "${tag.toUpperCase()}" headings (maximum allowed: ${max}).`
|
|
5868
|
+
});
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5299
5871
|
}
|
|
5300
5872
|
}
|
|
5301
|
-
try {
|
|
5302
|
-
await recordContentHistoryVersion(strapi, {
|
|
5303
|
-
uid,
|
|
5304
|
-
documentId: newDocId,
|
|
5305
|
-
userId: user?.id,
|
|
5306
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5307
|
-
});
|
|
5308
|
-
} catch {
|
|
5309
|
-
}
|
|
5310
|
-
stats.created++;
|
|
5311
|
-
if (!docId) {
|
|
5312
|
-
stats.newWithoutDocId++;
|
|
5313
|
-
}
|
|
5314
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5315
|
-
stats.diffs.push({
|
|
5316
|
-
uid,
|
|
5317
|
-
collectionName: ct.info?.displayName || uid,
|
|
5318
|
-
documentId: newDocId,
|
|
5319
|
-
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5320
|
-
attribute: attrKey,
|
|
5321
|
-
oldValue: null,
|
|
5322
|
-
newValue: payload[attrKey],
|
|
5323
|
-
status: "created",
|
|
5324
|
-
message: docId ? `Created "${attrKey}" as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created "${attrKey}" as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
5325
|
-
});
|
|
5326
|
-
}
|
|
5327
5873
|
}
|
|
5328
|
-
} catch (err) {
|
|
5329
|
-
stats.errors.push({
|
|
5330
|
-
row: rowNumber,
|
|
5331
|
-
documentId: docId || void 0,
|
|
5332
|
-
error: extractDetailedErrorMessage(err)
|
|
5333
|
-
});
|
|
5334
5874
|
}
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5338
|
-
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5339
|
-
bulkService.invalidateCollectionCache(uid);
|
|
5875
|
+
if (!rowHasError) {
|
|
5876
|
+
validRows++;
|
|
5340
5877
|
}
|
|
5341
|
-
} catch {
|
|
5342
5878
|
}
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5879
|
+
return {
|
|
5880
|
+
success: true,
|
|
5881
|
+
totalRows: rows.length,
|
|
5882
|
+
validRows,
|
|
5883
|
+
estimatedUpdates,
|
|
5884
|
+
estimatedCreates,
|
|
5885
|
+
hasErrors: errors2.length > 0,
|
|
5886
|
+
hasWarnings: warnings.length > 0,
|
|
5887
|
+
errors: errors2.slice(0, 100),
|
|
5888
|
+
warnings: warnings.slice(0, 100),
|
|
5889
|
+
totalErrorCount: errors2.length,
|
|
5890
|
+
totalWarningCount: warnings.length
|
|
5891
|
+
};
|
|
5892
|
+
},
|
|
5893
|
+
/**
|
|
5894
|
+
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
5895
|
+
*/
|
|
5896
|
+
async submitBatch(uid, rows, options = {}) {
|
|
5897
|
+
const ct = strapi.contentTypes[uid];
|
|
5898
|
+
if (!ct) {
|
|
5899
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5900
|
+
}
|
|
5901
|
+
if (!rows || rows.length === 0) {
|
|
5902
|
+
throw new Error("No CSV records provided for batch submission.");
|
|
5903
|
+
}
|
|
5904
|
+
const {
|
|
5905
|
+
executionMode = "immediate",
|
|
5906
|
+
makerNotes = "",
|
|
5907
|
+
forceImmediate = false,
|
|
5908
|
+
mode = "auto",
|
|
5909
|
+
publish = true,
|
|
5910
|
+
user
|
|
5911
|
+
} = options;
|
|
5912
|
+
const executionConfig = getExecutionWindowConfig(strapi);
|
|
5913
|
+
const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
|
|
5914
|
+
const collectionDisplayName = ct.info?.displayName || uid;
|
|
5915
|
+
if (executionMode === "maker_checker") {
|
|
5916
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5917
|
+
const batch = {
|
|
5918
|
+
id: batchId,
|
|
5919
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5920
|
+
createdBy: user ? {
|
|
5921
|
+
id: user.id,
|
|
5922
|
+
email: user.email,
|
|
5923
|
+
firstname: user.firstname,
|
|
5924
|
+
lastname: user.lastname,
|
|
5925
|
+
username: user.username
|
|
5926
|
+
} : { email: "admin@system.local" },
|
|
5927
|
+
payload: {
|
|
5928
|
+
action: "csv_import",
|
|
5929
|
+
uid,
|
|
5930
|
+
rows,
|
|
5931
|
+
options: {
|
|
5932
|
+
mode,
|
|
5933
|
+
publish,
|
|
5934
|
+
user
|
|
5935
|
+
},
|
|
5936
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5937
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5938
|
+
publishOnApproval: publish
|
|
5939
|
+
},
|
|
5940
|
+
summary: {
|
|
5941
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5942
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5943
|
+
totalTargets: rows.length,
|
|
5944
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
5945
|
+
publishOnApproval: publish
|
|
5946
|
+
},
|
|
5947
|
+
status: "pending_approval",
|
|
5948
|
+
executionMode: "maker_checker",
|
|
5949
|
+
makerNotes
|
|
5950
|
+
};
|
|
5951
|
+
registerPendingBatch(batch, {
|
|
5952
|
+
id: batchId,
|
|
5953
|
+
createdAt: batch.createdAt,
|
|
5954
|
+
createdBy: batch.createdBy,
|
|
5955
|
+
status: "pending_approval",
|
|
5956
|
+
executionMode: "maker_checker",
|
|
5957
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5958
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5959
|
+
totalTargets: rows.length,
|
|
5960
|
+
processedCount: 0,
|
|
5961
|
+
updatedCount: 0,
|
|
5962
|
+
skippedCount: 0,
|
|
5963
|
+
failedCount: 0,
|
|
5964
|
+
isLive: false,
|
|
5965
|
+
publishOnApproval: publish,
|
|
5966
|
+
makerNotes,
|
|
5967
|
+
results: []
|
|
5968
|
+
});
|
|
5969
|
+
return {
|
|
5970
|
+
success: true,
|
|
5971
|
+
pending: true,
|
|
5972
|
+
batchId,
|
|
5973
|
+
status: "pending_approval",
|
|
5974
|
+
message: "CSV batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
|
|
5975
|
+
batch
|
|
5976
|
+
};
|
|
5977
|
+
}
|
|
5978
|
+
if (executionMode === "queue_offpeak") {
|
|
5979
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5980
|
+
const batch = {
|
|
5981
|
+
id: batchId,
|
|
5982
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5983
|
+
createdBy: user ? {
|
|
5984
|
+
id: user.id,
|
|
5985
|
+
email: user.email,
|
|
5986
|
+
firstname: user.firstname,
|
|
5987
|
+
lastname: user.lastname,
|
|
5988
|
+
username: user.username
|
|
5989
|
+
} : { email: "admin@system.local" },
|
|
5990
|
+
payload: {
|
|
5991
|
+
action: "csv_import",
|
|
5992
|
+
uid,
|
|
5993
|
+
rows,
|
|
5994
|
+
options: {
|
|
5995
|
+
mode,
|
|
5996
|
+
publish,
|
|
5997
|
+
user
|
|
5998
|
+
},
|
|
5999
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6000
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6001
|
+
publishOnApproval: publish
|
|
6002
|
+
},
|
|
6003
|
+
summary: {
|
|
6004
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6005
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6006
|
+
totalTargets: rows.length,
|
|
6007
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
6008
|
+
publishOnApproval: publish
|
|
6009
|
+
},
|
|
6010
|
+
status: "queued_offpeak",
|
|
6011
|
+
executionMode: "queue_offpeak",
|
|
6012
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6013
|
+
makerNotes
|
|
6014
|
+
};
|
|
6015
|
+
registerPendingBatch(batch, {
|
|
6016
|
+
id: batchId,
|
|
6017
|
+
createdAt: batch.createdAt,
|
|
6018
|
+
createdBy: batch.createdBy,
|
|
6019
|
+
status: "queued_offpeak",
|
|
6020
|
+
executionMode: "queue_offpeak",
|
|
6021
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6022
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6023
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6024
|
+
totalTargets: rows.length,
|
|
6025
|
+
processedCount: 0,
|
|
6026
|
+
updatedCount: 0,
|
|
6027
|
+
skippedCount: 0,
|
|
6028
|
+
failedCount: 0,
|
|
6029
|
+
isLive: false,
|
|
6030
|
+
publishOnApproval: publish,
|
|
6031
|
+
makerNotes,
|
|
6032
|
+
results: []
|
|
6033
|
+
});
|
|
6034
|
+
return {
|
|
6035
|
+
success: true,
|
|
6036
|
+
pending: true,
|
|
6037
|
+
batchId,
|
|
6038
|
+
status: "queued_offpeak",
|
|
6039
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6040
|
+
message: `CSV batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
|
|
6041
|
+
batch
|
|
6042
|
+
};
|
|
6043
|
+
}
|
|
6044
|
+
if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
|
|
6045
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
6046
|
+
const batch = {
|
|
6047
|
+
id: batchId,
|
|
6048
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6049
|
+
createdBy: user ? {
|
|
6050
|
+
id: user.id,
|
|
6051
|
+
email: user.email,
|
|
6052
|
+
firstname: user.firstname,
|
|
6053
|
+
lastname: user.lastname,
|
|
6054
|
+
username: user.username
|
|
6055
|
+
} : { email: "admin@system.local" },
|
|
6056
|
+
payload: {
|
|
6057
|
+
action: "csv_import",
|
|
6058
|
+
uid,
|
|
6059
|
+
rows,
|
|
6060
|
+
options: {
|
|
6061
|
+
mode,
|
|
6062
|
+
publish,
|
|
6063
|
+
user
|
|
6064
|
+
},
|
|
6065
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6066
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6067
|
+
publishOnApproval: publish
|
|
6068
|
+
},
|
|
6069
|
+
summary: {
|
|
6070
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6071
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6072
|
+
totalTargets: rows.length,
|
|
6073
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
6074
|
+
publishOnApproval: publish
|
|
6075
|
+
},
|
|
6076
|
+
status: "queued_offpeak",
|
|
6077
|
+
executionMode: "queue_offpeak",
|
|
6078
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6079
|
+
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
6080
|
+
};
|
|
6081
|
+
registerPendingBatch(batch, {
|
|
6082
|
+
id: batchId,
|
|
6083
|
+
createdAt: batch.createdAt,
|
|
6084
|
+
createdBy: batch.createdBy,
|
|
6085
|
+
status: "queued_offpeak",
|
|
6086
|
+
executionMode: "queue_offpeak",
|
|
6087
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6088
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6089
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6090
|
+
totalTargets: rows.length,
|
|
6091
|
+
processedCount: 0,
|
|
6092
|
+
updatedCount: 0,
|
|
6093
|
+
skippedCount: 0,
|
|
6094
|
+
failedCount: 0,
|
|
6095
|
+
isLive: false,
|
|
6096
|
+
publishOnApproval: publish,
|
|
6097
|
+
policyMessage,
|
|
6098
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
6099
|
+
results: []
|
|
6100
|
+
});
|
|
6101
|
+
return {
|
|
6102
|
+
success: true,
|
|
6103
|
+
pending: true,
|
|
6104
|
+
batchId,
|
|
6105
|
+
status: "queued_offpeak",
|
|
6106
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6107
|
+
message: policyMessage,
|
|
6108
|
+
batch
|
|
6109
|
+
};
|
|
5398
6110
|
}
|
|
5399
6111
|
return {
|
|
5400
6112
|
success: true,
|
|
5401
|
-
|
|
5402
|
-
|
|
6113
|
+
pending: false,
|
|
6114
|
+
executeNow: true
|
|
5403
6115
|
};
|
|
5404
6116
|
}
|
|
5405
6117
|
});
|