strapi-plugin-bulk-updater 1.4.2 → 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-D4v5fP-1.mjs → BulkUpdater-CMkPdA9_.mjs} +1365 -401
- package/dist/_chunks/BulkUpdater-CMkPdA9_.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-L2FdlJfh.js → BulkUpdater-DtBH2c0-.js} +1361 -397
- 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 +1570 -643
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1570 -643
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/_chunks/BulkUpdater-D4v5fP-1.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-L2FdlJfh.js.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -214,6 +214,30 @@ const routes = {
|
|
|
214
214
|
config: {
|
|
215
215
|
policies: []
|
|
216
216
|
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
method: "POST",
|
|
220
|
+
path: "/csv/import-chunk",
|
|
221
|
+
handler: "csv.importChunk",
|
|
222
|
+
config: {
|
|
223
|
+
policies: []
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
method: "POST",
|
|
228
|
+
path: "/csv/validate-preflight",
|
|
229
|
+
handler: "csv.validatePreflight",
|
|
230
|
+
config: {
|
|
231
|
+
policies: []
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
method: "POST",
|
|
236
|
+
path: "/csv/submit-batch",
|
|
237
|
+
handler: "csv.submitBatch",
|
|
238
|
+
config: {
|
|
239
|
+
policies: []
|
|
240
|
+
}
|
|
217
241
|
}
|
|
218
242
|
]
|
|
219
243
|
}
|
|
@@ -666,6 +690,86 @@ const csvController = ({ strapi }) => ({
|
|
|
666
690
|
ctx.status = 500;
|
|
667
691
|
ctx.body = { success: false, error: err.message };
|
|
668
692
|
}
|
|
693
|
+
},
|
|
694
|
+
async importChunk(ctx) {
|
|
695
|
+
try {
|
|
696
|
+
const { uid, rows, batchId, isFirstChunk, isLastChunk, startRowNumber, mode, publish } = ctx.request.body;
|
|
697
|
+
if (!uid) {
|
|
698
|
+
ctx.status = 400;
|
|
699
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!Array.isArray(rows)) {
|
|
703
|
+
ctx.status = 400;
|
|
704
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
708
|
+
const result = await csvService2.importChunk(uid, rows, {
|
|
709
|
+
batchId: batchId ? String(batchId) : void 0,
|
|
710
|
+
isFirstChunk: Boolean(isFirstChunk),
|
|
711
|
+
isLastChunk: Boolean(isLastChunk),
|
|
712
|
+
startRowNumber: typeof startRowNumber === "number" ? startRowNumber : void 0,
|
|
713
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
|
|
714
|
+
publish: publish !== false,
|
|
715
|
+
user: ctx.state.user
|
|
716
|
+
});
|
|
717
|
+
ctx.body = result;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
ctx.status = 500;
|
|
720
|
+
ctx.body = { success: false, error: err.message };
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
async validatePreflight(ctx) {
|
|
724
|
+
try {
|
|
725
|
+
const { uid, rows, mode } = ctx.request.body;
|
|
726
|
+
if (!uid) {
|
|
727
|
+
ctx.status = 400;
|
|
728
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!Array.isArray(rows)) {
|
|
732
|
+
ctx.status = 400;
|
|
733
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
737
|
+
const result = await csvService2.validatePreflight(uid, rows, {
|
|
738
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto"
|
|
739
|
+
});
|
|
740
|
+
ctx.body = result;
|
|
741
|
+
} catch (err) {
|
|
742
|
+
ctx.status = 500;
|
|
743
|
+
ctx.body = { success: false, error: err.message };
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
async submitBatch(ctx) {
|
|
747
|
+
try {
|
|
748
|
+
const { uid, rows, executionMode, makerNotes, forceImmediate, mode, publish } = ctx.request.body;
|
|
749
|
+
if (!uid) {
|
|
750
|
+
ctx.status = 400;
|
|
751
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (!Array.isArray(rows)) {
|
|
755
|
+
ctx.status = 400;
|
|
756
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
760
|
+
const result = await csvService2.submitBatch(uid, rows, {
|
|
761
|
+
executionMode,
|
|
762
|
+
makerNotes,
|
|
763
|
+
forceImmediate: Boolean(forceImmediate),
|
|
764
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
|
|
765
|
+
publish: publish !== false,
|
|
766
|
+
user: ctx.state.user
|
|
767
|
+
});
|
|
768
|
+
ctx.body = result;
|
|
769
|
+
} catch (err) {
|
|
770
|
+
ctx.status = 500;
|
|
771
|
+
ctx.body = { success: false, error: err.message };
|
|
772
|
+
}
|
|
669
773
|
}
|
|
670
774
|
});
|
|
671
775
|
const FIELDS_TO_IGNORE = [
|
|
@@ -911,9 +1015,74 @@ function discoverComponentAttributes(strapi, compUid, prefix, currentDepth, maxD
|
|
|
911
1015
|
}
|
|
912
1016
|
return Array.from(new Set(discoveredAttrs));
|
|
913
1017
|
}
|
|
1018
|
+
function parseDynamicZonePath(path) {
|
|
1019
|
+
if (!path) return { cleanPath: "", rootKey: "", subPath: "" };
|
|
1020
|
+
const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\]\.(.+)$/);
|
|
1021
|
+
if (bracketMatch) {
|
|
1022
|
+
const rootKey = bracketMatch[1];
|
|
1023
|
+
const selector = bracketMatch[2];
|
|
1024
|
+
const subPath = bracketMatch[3];
|
|
1025
|
+
const cleanPath = `${rootKey}.${subPath}`;
|
|
1026
|
+
if (/^\d+$/.test(selector)) {
|
|
1027
|
+
return { cleanPath, rootKey, subPath, targetIndex: parseInt(selector, 10) };
|
|
1028
|
+
}
|
|
1029
|
+
return { cleanPath, rootKey, subPath, targetComponentUid: selector };
|
|
1030
|
+
}
|
|
1031
|
+
const parts = path.split(".");
|
|
1032
|
+
return {
|
|
1033
|
+
cleanPath: path,
|
|
1034
|
+
rootKey: parts[0] || "",
|
|
1035
|
+
subPath: parts.slice(1).join(".")
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function formatComponentName(compUid) {
|
|
1039
|
+
if (!compUid) return "Component";
|
|
1040
|
+
const name = compUid.includes(".") ? compUid.split(".")[1] : compUid;
|
|
1041
|
+
return name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1042
|
+
}
|
|
1043
|
+
function getAllNestedValues(obj, path) {
|
|
1044
|
+
if (!obj || !path) return [];
|
|
1045
|
+
const parsed = parseDynamicZonePath(path);
|
|
1046
|
+
const parts = parsed.cleanPath.split(".");
|
|
1047
|
+
const [head, ...tail] = parts;
|
|
1048
|
+
const rootTarget = obj[head];
|
|
1049
|
+
if (Array.isArray(rootTarget)) {
|
|
1050
|
+
const results = [];
|
|
1051
|
+
rootTarget.forEach((item, idx) => {
|
|
1052
|
+
if (!item || typeof item !== "object") return;
|
|
1053
|
+
if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) return;
|
|
1054
|
+
if (parsed.targetComponentUid && item.__component !== parsed.targetComponentUid) return;
|
|
1055
|
+
const compSchema = global.strapi?.components?.[item.__component];
|
|
1056
|
+
const fieldInSchema = tail.length > 0 && compSchema?.attributes ? tail[0] in compSchema.attributes : false;
|
|
1057
|
+
const subVal = tail.length > 0 ? getDeepProperty(item, tail.join(".")) : item;
|
|
1058
|
+
if (subVal !== void 0 || fieldInSchema) {
|
|
1059
|
+
results.push({
|
|
1060
|
+
index: idx,
|
|
1061
|
+
componentUid: item.__component,
|
|
1062
|
+
componentName: formatComponentName(item.__component),
|
|
1063
|
+
value: subVal !== void 0 ? subVal : null,
|
|
1064
|
+
path: `${head}[#${idx}${item.__component ? ` ${formatComponentName(item.__component)}` : ""}].${tail.join(".")}`
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
return results;
|
|
1069
|
+
}
|
|
1070
|
+
const singleVal = getDeepProperty(obj, parsed.cleanPath);
|
|
1071
|
+
if (singleVal !== void 0) {
|
|
1072
|
+
return [
|
|
1073
|
+
{
|
|
1074
|
+
index: 0,
|
|
1075
|
+
value: singleVal,
|
|
1076
|
+
path: parsed.cleanPath
|
|
1077
|
+
}
|
|
1078
|
+
];
|
|
1079
|
+
}
|
|
1080
|
+
return [];
|
|
1081
|
+
}
|
|
914
1082
|
function isDeepComponentField(contentType, allComponents, attribute) {
|
|
915
|
-
if (!attribute || !attribute.includes(".")) return false;
|
|
916
|
-
const
|
|
1083
|
+
if (!attribute || !attribute.includes(".") && !attribute.includes("[")) return false;
|
|
1084
|
+
const parsed = parseDynamicZonePath(attribute);
|
|
1085
|
+
const parts = parsed.cleanPath.split(".");
|
|
917
1086
|
function checkPath(currentSchema, pathParts) {
|
|
918
1087
|
if (pathParts.length === 0) return true;
|
|
919
1088
|
if (!currentSchema || !currentSchema.attributes) return false;
|
|
@@ -929,6 +1098,10 @@ function isDeepComponentField(contentType, allComponents, attribute) {
|
|
|
929
1098
|
}
|
|
930
1099
|
if (attrDef.type === "dynamiczone") {
|
|
931
1100
|
const compUids = Array.isArray(attrDef.components) ? attrDef.components : [];
|
|
1101
|
+
if (parsed.targetComponentUid) {
|
|
1102
|
+
const comp = allComponents[parsed.targetComponentUid];
|
|
1103
|
+
return comp ? checkPath(comp, tail) : false;
|
|
1104
|
+
}
|
|
932
1105
|
return compUids.some((compUid) => {
|
|
933
1106
|
const comp = allComponents[compUid];
|
|
934
1107
|
return checkPath(comp, tail);
|
|
@@ -940,8 +1113,9 @@ function isDeepComponentField(contentType, allComponents, attribute) {
|
|
|
940
1113
|
}
|
|
941
1114
|
function getLeafAttributeSchema(contentType, allComponents, attribute) {
|
|
942
1115
|
if (!attribute) return null;
|
|
943
|
-
|
|
944
|
-
|
|
1116
|
+
const parsed = parseDynamicZonePath(attribute);
|
|
1117
|
+
if (!parsed.cleanPath.includes(".")) return contentType?.attributes?.[parsed.cleanPath] || null;
|
|
1118
|
+
const parts = parsed.cleanPath.split(".");
|
|
945
1119
|
function resolveLeaf(currentSchema, pathParts) {
|
|
946
1120
|
if (!currentSchema || !currentSchema.attributes) return null;
|
|
947
1121
|
const [head, ...tail] = pathParts;
|
|
@@ -955,6 +1129,9 @@ function getLeafAttributeSchema(contentType, allComponents, attribute) {
|
|
|
955
1129
|
}
|
|
956
1130
|
if (attrDef.type === "dynamiczone") {
|
|
957
1131
|
const compUids = Array.isArray(attrDef.components) ? attrDef.components : [];
|
|
1132
|
+
if (parsed.targetComponentUid) {
|
|
1133
|
+
return resolveLeaf(allComponents[parsed.targetComponentUid], tail);
|
|
1134
|
+
}
|
|
958
1135
|
for (const compUid of compUids) {
|
|
959
1136
|
const found = resolveLeaf(allComponents[compUid], tail);
|
|
960
1137
|
if (found) return found;
|
|
@@ -964,22 +1141,30 @@ function getLeafAttributeSchema(contentType, allComponents, attribute) {
|
|
|
964
1141
|
}
|
|
965
1142
|
return resolveLeaf(contentType, parts);
|
|
966
1143
|
}
|
|
967
|
-
function setDeepProperty(obj, pathParts, value) {
|
|
1144
|
+
function setDeepProperty(obj, pathParts, value, options) {
|
|
968
1145
|
if (pathParts.length === 0) return value;
|
|
969
1146
|
const [head, ...tail] = pathParts;
|
|
970
1147
|
if (Array.isArray(obj)) {
|
|
971
1148
|
if (/^\d+$/.test(head)) {
|
|
972
1149
|
const idx = parseInt(head, 10);
|
|
973
1150
|
const copy = [...obj];
|
|
974
|
-
copy[idx] = setDeepProperty(copy[idx] || {}, tail, value);
|
|
1151
|
+
copy[idx] = setDeepProperty(copy[idx] || {}, tail, value, options);
|
|
975
1152
|
return copy;
|
|
976
1153
|
}
|
|
977
|
-
return obj.map((item) => {
|
|
1154
|
+
return obj.map((item, idx) => {
|
|
978
1155
|
if (!item || typeof item !== "object") return item;
|
|
979
|
-
if (
|
|
1156
|
+
if (options?.targetIndexes && options.targetIndexes.length > 0) {
|
|
1157
|
+
if (!options.targetIndexes.includes(idx)) return item;
|
|
1158
|
+
}
|
|
1159
|
+
if (options?.targetComponentUid) {
|
|
1160
|
+
if (item.__component !== options.targetComponentUid) return item;
|
|
1161
|
+
}
|
|
1162
|
+
const compSchema = global.strapi?.components?.[item.__component];
|
|
1163
|
+
const fieldExistsInSchema = compSchema?.attributes ? head in compSchema.attributes : true;
|
|
1164
|
+
if (head in item || fieldExistsInSchema || tail.length === 0) {
|
|
980
1165
|
return {
|
|
981
1166
|
...item,
|
|
982
|
-
[head]: setDeepProperty(item[head], tail, value)
|
|
1167
|
+
[head]: setDeepProperty(item[head], tail, value, options)
|
|
983
1168
|
};
|
|
984
1169
|
}
|
|
985
1170
|
return item;
|
|
@@ -991,16 +1176,17 @@ function setDeepProperty(obj, pathParts, value) {
|
|
|
991
1176
|
} else {
|
|
992
1177
|
const subTarget = current[head];
|
|
993
1178
|
if (Array.isArray(subTarget)) {
|
|
994
|
-
current[head] = setDeepProperty(subTarget, tail, value);
|
|
1179
|
+
current[head] = setDeepProperty(subTarget, tail, value, options);
|
|
995
1180
|
} else {
|
|
996
|
-
current[head] = setDeepProperty(subTarget || {}, tail, value);
|
|
1181
|
+
current[head] = setDeepProperty(subTarget || {}, tail, value, options);
|
|
997
1182
|
}
|
|
998
1183
|
}
|
|
999
1184
|
return current;
|
|
1000
1185
|
}
|
|
1001
1186
|
function getDeepProperty(obj, path) {
|
|
1002
1187
|
if (!obj || !path) return void 0;
|
|
1003
|
-
const
|
|
1188
|
+
const parsed = parseDynamicZonePath(path);
|
|
1189
|
+
const parts = parsed.cleanPath.split(".");
|
|
1004
1190
|
function extract(current, remainingParts) {
|
|
1005
1191
|
if (current === void 0 || current === null) return void 0;
|
|
1006
1192
|
if (remainingParts.length === 0) return current;
|
|
@@ -1009,7 +1195,10 @@ function getDeepProperty(obj, path) {
|
|
|
1009
1195
|
if (/^\d+$/.test(head)) {
|
|
1010
1196
|
return extract(current[parseInt(head, 10)], tail);
|
|
1011
1197
|
}
|
|
1012
|
-
for (
|
|
1198
|
+
for (let idx = 0; idx < current.length; idx++) {
|
|
1199
|
+
const item = current[idx];
|
|
1200
|
+
if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) continue;
|
|
1201
|
+
if (parsed.targetComponentUid && item?.__component !== parsed.targetComponentUid) continue;
|
|
1013
1202
|
const found = extract(item?.[head], tail);
|
|
1014
1203
|
if (found !== void 0 && found !== null) return found;
|
|
1015
1204
|
}
|
|
@@ -1086,6 +1275,12 @@ const pendingBatches = /* @__PURE__ */ new Map();
|
|
|
1086
1275
|
const batchJobsMap = /* @__PURE__ */ new Map();
|
|
1087
1276
|
let isRunnerRunning = false;
|
|
1088
1277
|
let runnerInterval = null;
|
|
1278
|
+
function registerPendingBatch(batch, jobState) {
|
|
1279
|
+
pendingBatches.set(batch.id, batch);
|
|
1280
|
+
if (jobState) {
|
|
1281
|
+
batchJobsMap.set(batch.id, jobState);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1089
1284
|
function invalidateCollectionCache(uid) {
|
|
1090
1285
|
modelsCache.delete(uid);
|
|
1091
1286
|
for (const key of queryCache.keys()) {
|
|
@@ -1174,7 +1369,12 @@ async function executeBatchInternal(strapi, {
|
|
|
1174
1369
|
for (const item of selectedEntries) {
|
|
1175
1370
|
if (!item.uid || !item.documentId) continue;
|
|
1176
1371
|
const key = `${item.uid}@@@${item.documentId}`;
|
|
1177
|
-
targetsMap.set(key, {
|
|
1372
|
+
targetsMap.set(key, {
|
|
1373
|
+
uid: item.uid,
|
|
1374
|
+
documentId: item.documentId,
|
|
1375
|
+
dynamicZoneIndexes: item.dynamicZoneIndexes,
|
|
1376
|
+
targetComponentUid: item.targetComponentUid
|
|
1377
|
+
});
|
|
1178
1378
|
}
|
|
1179
1379
|
if (targetsMap.size === 0) {
|
|
1180
1380
|
throw new Error("No collections or entries were selected for update.");
|
|
@@ -1233,10 +1433,13 @@ async function executeBatchInternal(strapi, {
|
|
|
1233
1433
|
try {
|
|
1234
1434
|
const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
|
|
1235
1435
|
const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
|
|
1236
|
-
const
|
|
1436
|
+
const parsed = parseDynamicZonePath(attribute);
|
|
1437
|
+
const isNested = attribute.includes(".") || attribute.includes("[");
|
|
1237
1438
|
const isComponentNested = isNested && isDeepComponentField(contentType, strapi.components, attribute);
|
|
1238
|
-
const parts = isNested ?
|
|
1239
|
-
const rootKey =
|
|
1439
|
+
const parts = isNested ? parsed.cleanPath.split(".") : [];
|
|
1440
|
+
const rootKey = parsed.rootKey;
|
|
1441
|
+
const targetComponentUid = target.targetComponentUid || parsed.targetComponentUid;
|
|
1442
|
+
const targetIndexes = target.dynamicZoneIndexes;
|
|
1240
1443
|
const attrDef = contentType?.attributes?.[attribute];
|
|
1241
1444
|
const hasAttribute = Boolean(attrDef) || isComponentNested;
|
|
1242
1445
|
if (hasAttribute) {
|
|
@@ -1249,12 +1452,25 @@ async function executeBatchInternal(strapi, {
|
|
|
1249
1452
|
const currentEntryWithPopulate = await strapi.documents(target.uid).findOne(findOneArgs);
|
|
1250
1453
|
const isRootDz = contentType?.attributes?.[rootKey]?.type === "dynamiczone";
|
|
1251
1454
|
const existingRoot = currentEntryWithPopulate?.[rootKey] || (isRootDz ? [] : {});
|
|
1252
|
-
|
|
1455
|
+
let affectedMatches = [];
|
|
1456
|
+
if (isRootDz && Array.isArray(existingRoot)) {
|
|
1457
|
+
affectedMatches = getAllNestedValues({ [rootKey]: existingRoot }, attribute);
|
|
1458
|
+
if (targetIndexes && targetIndexes.length > 0) {
|
|
1459
|
+
affectedMatches = affectedMatches.filter((m) => targetIndexes.includes(m.index));
|
|
1460
|
+
}
|
|
1461
|
+
if (targetComponentUid) {
|
|
1462
|
+
affectedMatches = affectedMatches.filter((m) => m.componentUid === targetComponentUid);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const currentVal2 = isRootDz ? affectedMatches.length > 0 ? affectedMatches.map((m) => m.value).join(", ") : getDeepProperty(existingRoot, parts.slice(1).join(".")) : getDeepProperty(existingRoot, parts.slice(1).join("."));
|
|
1253
1466
|
let newValue2 = value;
|
|
1254
1467
|
if (regexObj && typeof currentVal2 === "string") {
|
|
1255
1468
|
newValue2 = currentVal2.replace(regexObj, value);
|
|
1256
1469
|
}
|
|
1257
|
-
const updatedRoot = setDeepProperty(existingRoot, parts.slice(1), newValue2
|
|
1470
|
+
const updatedRoot = setDeepProperty(existingRoot, parts.slice(1), newValue2, {
|
|
1471
|
+
targetIndexes,
|
|
1472
|
+
targetComponentUid
|
|
1473
|
+
});
|
|
1258
1474
|
await strapi.documents(target.uid).update({
|
|
1259
1475
|
documentId: target.documentId,
|
|
1260
1476
|
data: {
|
|
@@ -1278,6 +1494,14 @@ async function executeBatchInternal(strapi, {
|
|
|
1278
1494
|
userId: currentUser?.id,
|
|
1279
1495
|
statusOverride: publishOnApproval && isDraftAndPublishComp ? "published" : void 0
|
|
1280
1496
|
});
|
|
1497
|
+
const dzDiffs = isRootDz && affectedMatches.length > 0 ? affectedMatches.map((m) => ({
|
|
1498
|
+
index: m.index,
|
|
1499
|
+
componentUid: m.componentUid,
|
|
1500
|
+
componentName: m.componentName,
|
|
1501
|
+
attribute: `${rootKey}[#${m.index} ${m.componentName}].${parts.slice(1).join(".")}`,
|
|
1502
|
+
oldValue: m.value,
|
|
1503
|
+
newValue: regexObj && typeof m.value === "string" ? m.value.replace(regexObj, value) : value
|
|
1504
|
+
})) : [];
|
|
1281
1505
|
return {
|
|
1282
1506
|
uid: target.uid,
|
|
1283
1507
|
collectionName: displayName,
|
|
@@ -1287,7 +1511,8 @@ async function executeBatchInternal(strapi, {
|
|
|
1287
1511
|
oldValue: currentVal2 ?? null,
|
|
1288
1512
|
newValue: newValue2,
|
|
1289
1513
|
status: "updated",
|
|
1290
|
-
message: `Successfully updated nested field ${attribute}
|
|
1514
|
+
message: isRootDz ? `Successfully updated ${affectedMatches.length} dynamic zone item(s) in ${attribute}` : `Successfully updated nested field ${attribute}`,
|
|
1515
|
+
dynamicZoneDiffs: dzDiffs
|
|
1291
1516
|
};
|
|
1292
1517
|
}
|
|
1293
1518
|
if (attrDef.type === "relation") {
|
|
@@ -1316,13 +1541,13 @@ async function executeBatchInternal(strapi, {
|
|
|
1316
1541
|
newDisplay = "(cleared / unlinked)";
|
|
1317
1542
|
} else {
|
|
1318
1543
|
try {
|
|
1319
|
-
const
|
|
1320
|
-
if (
|
|
1321
|
-
mode =
|
|
1322
|
-
targetDocIds = Array.isArray(
|
|
1323
|
-
newDisplay =
|
|
1324
|
-
} else if (Array.isArray(
|
|
1325
|
-
targetDocIds =
|
|
1544
|
+
const parsed2 = JSON.parse(value);
|
|
1545
|
+
if (parsed2 && typeof parsed2 === "object" && parsed2.mode) {
|
|
1546
|
+
mode = parsed2.mode;
|
|
1547
|
+
targetDocIds = Array.isArray(parsed2.documentIds) ? parsed2.documentIds : parsed2.documentId ? [parsed2.documentId] : [];
|
|
1548
|
+
newDisplay = parsed2.label || targetDocIds.join(", ") || `[${mode}]`;
|
|
1549
|
+
} else if (Array.isArray(parsed2)) {
|
|
1550
|
+
targetDocIds = parsed2;
|
|
1326
1551
|
newDisplay = targetDocIds.join(", ");
|
|
1327
1552
|
} else {
|
|
1328
1553
|
targetDocIds = [value];
|
|
@@ -1466,6 +1691,26 @@ async function executeBatchInternal(strapi, {
|
|
|
1466
1691
|
...r,
|
|
1467
1692
|
isLive: r.status === "updated"
|
|
1468
1693
|
}));
|
|
1694
|
+
const flattenedDiffs = [];
|
|
1695
|
+
for (const r of results) {
|
|
1696
|
+
if (r.dynamicZoneDiffs && r.dynamicZoneDiffs.length > 0) {
|
|
1697
|
+
for (const dz of r.dynamicZoneDiffs) {
|
|
1698
|
+
flattenedDiffs.push({
|
|
1699
|
+
uid: r.uid,
|
|
1700
|
+
collectionName: r.collectionName,
|
|
1701
|
+
documentId: r.documentId,
|
|
1702
|
+
label: `${r.label} [Block #${dz.index} (${dz.componentName})]`,
|
|
1703
|
+
attribute: dz.attribute,
|
|
1704
|
+
oldValue: dz.oldValue,
|
|
1705
|
+
newValue: dz.newValue,
|
|
1706
|
+
status: "updated",
|
|
1707
|
+
message: `Updated ${dz.attribute}`
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
} else {
|
|
1711
|
+
flattenedDiffs.push(r);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1469
1714
|
const batchSummary = {
|
|
1470
1715
|
batchId,
|
|
1471
1716
|
attribute,
|
|
@@ -1477,7 +1722,7 @@ async function executeBatchInternal(strapi, {
|
|
|
1477
1722
|
};
|
|
1478
1723
|
await strapi.plugin("bulk-updater").service("audit").recordBatch(
|
|
1479
1724
|
batchSummary,
|
|
1480
|
-
|
|
1725
|
+
flattenedDiffs,
|
|
1481
1726
|
currentUser
|
|
1482
1727
|
);
|
|
1483
1728
|
return {
|
|
@@ -1658,6 +1903,22 @@ async function executePublishBatchInternal(strapi, {
|
|
|
1658
1903
|
results
|
|
1659
1904
|
};
|
|
1660
1905
|
}
|
|
1906
|
+
async function executeBatchByPayload(strapi, batch, currentUser) {
|
|
1907
|
+
if (batch.payload?.action === "csv_import") {
|
|
1908
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
1909
|
+
return await csvService2.executeCsvPendingBatch(
|
|
1910
|
+
batch.payload.uid,
|
|
1911
|
+
batch.payload.rows,
|
|
1912
|
+
batch.id,
|
|
1913
|
+
batch.payload.options,
|
|
1914
|
+
currentUser
|
|
1915
|
+
);
|
|
1916
|
+
} else if (batch.payload?.action === "publish") {
|
|
1917
|
+
return await executePublishBatchInternal(strapi, batch.payload, currentUser, batch.id);
|
|
1918
|
+
} else {
|
|
1919
|
+
return await executeBatchInternal(strapi, batch.payload, currentUser, batch.id);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1661
1922
|
const bulkUpdaterService = ({ strapi }) => {
|
|
1662
1923
|
if (!runnerInterval) {
|
|
1663
1924
|
runnerInterval = setInterval(async () => {
|
|
@@ -1669,7 +1930,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
1669
1930
|
for (const [id, batch] of pendingBatches.entries()) {
|
|
1670
1931
|
if (batch.status === "queued_offpeak" && !isBusinessHours) {
|
|
1671
1932
|
strapi.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
|
|
1672
|
-
const result =
|
|
1933
|
+
const result = await executeBatchByPayload(strapi, batch, batch.createdBy);
|
|
1673
1934
|
batch.status = "executed";
|
|
1674
1935
|
batch.executionResult = result;
|
|
1675
1936
|
}
|
|
@@ -1745,6 +2006,19 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
1745
2006
|
colAttributesMeta
|
|
1746
2007
|
);
|
|
1747
2008
|
nestedComponentAttrs.push(...compAttrs);
|
|
2009
|
+
const scopedAttrs = discoverComponentAttributes(
|
|
2010
|
+
strapi,
|
|
2011
|
+
compUid,
|
|
2012
|
+
`${attrKey}[${compUid}]`,
|
|
2013
|
+
1,
|
|
2014
|
+
5,
|
|
2015
|
+
systemFields,
|
|
2016
|
+
allContentTypes,
|
|
2017
|
+
relationsByAttr,
|
|
2018
|
+
allRelationsMeta,
|
|
2019
|
+
colAttributesMeta
|
|
2020
|
+
);
|
|
2021
|
+
nestedComponentAttrs.push(...scopedAttrs);
|
|
1748
2022
|
}
|
|
1749
2023
|
return false;
|
|
1750
2024
|
}
|
|
@@ -1959,13 +2233,20 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
1959
2233
|
const attrs = contentType.attributes || {};
|
|
1960
2234
|
let colHasAttribute = false;
|
|
1961
2235
|
let isNestedAttr = false;
|
|
2236
|
+
let isRootDz = false;
|
|
1962
2237
|
let parts = [];
|
|
1963
2238
|
if (targetFilterAttr) {
|
|
1964
|
-
|
|
2239
|
+
const parsed = parseDynamicZonePath(targetFilterAttr);
|
|
2240
|
+
if (parsed.rootKey && contentType.attributes?.[parsed.rootKey]?.type === "dynamiczone") {
|
|
2241
|
+
isRootDz = true;
|
|
2242
|
+
colHasAttribute = true;
|
|
2243
|
+
isNestedAttr = true;
|
|
2244
|
+
parts = parsed.cleanPath.split(".");
|
|
2245
|
+
} else if (targetFilterAttr.includes(".") || targetFilterAttr.includes("[")) {
|
|
1965
2246
|
isNestedAttr = isDeepComponentField(contentType, strapi.components, targetFilterAttr);
|
|
1966
2247
|
if (isNestedAttr) {
|
|
1967
2248
|
colHasAttribute = true;
|
|
1968
|
-
parts =
|
|
2249
|
+
parts = parsed.cleanPath.split(".");
|
|
1969
2250
|
}
|
|
1970
2251
|
} else if (attrs[targetFilterAttr]) {
|
|
1971
2252
|
colHasAttribute = true;
|
|
@@ -2022,8 +2303,11 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2022
2303
|
}
|
|
2023
2304
|
if (condition !== void 0) {
|
|
2024
2305
|
if (isNestedAttr) {
|
|
2025
|
-
|
|
2026
|
-
|
|
2306
|
+
if (isRootDz) ;
|
|
2307
|
+
else {
|
|
2308
|
+
const deepFilter = buildDeepFilter(parts, condition);
|
|
2309
|
+
Object.assign(filters, deepFilter);
|
|
2310
|
+
}
|
|
2027
2311
|
} else {
|
|
2028
2312
|
if (normOp === "empty" && isStringLike) {
|
|
2029
2313
|
filters.$or = [
|
|
@@ -2097,39 +2381,100 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2097
2381
|
} else {
|
|
2098
2382
|
sortClause["createdAt"] = "desc";
|
|
2099
2383
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
}
|
|
2109
|
-
const [entries, totalEntries] = await Promise.all([
|
|
2110
|
-
strapi.documents(uid).findMany(findManyArgs),
|
|
2111
|
-
strapi.documents(uid).count({
|
|
2112
|
-
filters: Object.keys(filters).length > 0 ? filters : void 0
|
|
2113
|
-
})
|
|
2114
|
-
]);
|
|
2115
|
-
const totalPages = Math.ceil(totalEntries / limit);
|
|
2116
|
-
result = {
|
|
2117
|
-
uid,
|
|
2118
|
-
displayName: contentType.info?.displayName || uid,
|
|
2119
|
-
isSingleType: false,
|
|
2120
|
-
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
2121
|
-
entries: (entries || []).map((entry) => ({
|
|
2122
|
-
id: entry.id,
|
|
2123
|
-
documentId: entry.documentId,
|
|
2124
|
-
...entry
|
|
2125
|
-
})),
|
|
2126
|
-
pagination: {
|
|
2127
|
-
page: currentPage,
|
|
2128
|
-
pageSize: limit,
|
|
2129
|
-
total: totalEntries,
|
|
2130
|
-
pageCount: totalPages
|
|
2384
|
+
if (isRootDz && normOp && normOp !== "none") {
|
|
2385
|
+
const findManyArgs = {
|
|
2386
|
+
limit: 500,
|
|
2387
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0,
|
|
2388
|
+
sort: sortClause
|
|
2389
|
+
};
|
|
2390
|
+
if (populateOption) {
|
|
2391
|
+
findManyArgs.populate = populateOption;
|
|
2131
2392
|
}
|
|
2132
|
-
|
|
2393
|
+
const rawEntries = await strapi.documents(uid).findMany(findManyArgs);
|
|
2394
|
+
const qVal = (filterQuery || "").trim().toLowerCase();
|
|
2395
|
+
const passesDzFilter = (entry) => {
|
|
2396
|
+
const matches = getAllNestedValues(entry, targetFilterAttr);
|
|
2397
|
+
if (normOp === "empty") {
|
|
2398
|
+
return matches.length === 0 || matches.every((m) => m.value === void 0 || m.value === null || String(m.value).trim() === "");
|
|
2399
|
+
}
|
|
2400
|
+
if (normOp === "notempty") {
|
|
2401
|
+
return matches.some((m) => m.value !== void 0 && m.value !== null && String(m.value).trim() !== "");
|
|
2402
|
+
}
|
|
2403
|
+
return matches.some((m) => {
|
|
2404
|
+
if (m.value === void 0 || m.value === null) return false;
|
|
2405
|
+
const valStr = String(m.value).toLowerCase();
|
|
2406
|
+
switch (normOp) {
|
|
2407
|
+
case "equals":
|
|
2408
|
+
return valStr === qVal;
|
|
2409
|
+
case "notequals":
|
|
2410
|
+
return valStr !== qVal;
|
|
2411
|
+
case "contains":
|
|
2412
|
+
return valStr.includes(qVal);
|
|
2413
|
+
case "notcontains":
|
|
2414
|
+
return !valStr.includes(qVal);
|
|
2415
|
+
case "startswith":
|
|
2416
|
+
return valStr.startsWith(qVal);
|
|
2417
|
+
case "endswith":
|
|
2418
|
+
return valStr.endsWith(qVal);
|
|
2419
|
+
default:
|
|
2420
|
+
return true;
|
|
2421
|
+
}
|
|
2422
|
+
});
|
|
2423
|
+
};
|
|
2424
|
+
const matchedEntries = (rawEntries || []).filter(passesDzFilter);
|
|
2425
|
+
const totalEntries = matchedEntries.length;
|
|
2426
|
+
const paginatedEntries = matchedEntries.slice(start, start + limit);
|
|
2427
|
+
result = {
|
|
2428
|
+
uid,
|
|
2429
|
+
displayName: contentType.info?.displayName || uid,
|
|
2430
|
+
isSingleType: false,
|
|
2431
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
2432
|
+
entries: paginatedEntries.map((entry) => ({
|
|
2433
|
+
id: entry.id,
|
|
2434
|
+
documentId: entry.documentId,
|
|
2435
|
+
...entry
|
|
2436
|
+
})),
|
|
2437
|
+
pagination: {
|
|
2438
|
+
page: currentPage,
|
|
2439
|
+
pageSize: limit,
|
|
2440
|
+
total: totalEntries,
|
|
2441
|
+
pageCount: Math.ceil(totalEntries / limit) || 1
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
} else {
|
|
2445
|
+
const findManyArgs = {
|
|
2446
|
+
start,
|
|
2447
|
+
limit,
|
|
2448
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0,
|
|
2449
|
+
sort: sortClause
|
|
2450
|
+
};
|
|
2451
|
+
if (populateOption) {
|
|
2452
|
+
findManyArgs.populate = populateOption;
|
|
2453
|
+
}
|
|
2454
|
+
const [entries, totalEntries] = await Promise.all([
|
|
2455
|
+
strapi.documents(uid).findMany(findManyArgs),
|
|
2456
|
+
strapi.documents(uid).count({
|
|
2457
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0
|
|
2458
|
+
})
|
|
2459
|
+
]);
|
|
2460
|
+
result = {
|
|
2461
|
+
uid,
|
|
2462
|
+
displayName: contentType.info?.displayName || uid,
|
|
2463
|
+
isSingleType: false,
|
|
2464
|
+
draftAndPublish: Boolean(contentType.options?.draftAndPublish),
|
|
2465
|
+
entries: (entries || []).map((entry) => ({
|
|
2466
|
+
id: entry.id,
|
|
2467
|
+
documentId: entry.documentId,
|
|
2468
|
+
...entry
|
|
2469
|
+
})),
|
|
2470
|
+
pagination: {
|
|
2471
|
+
page: currentPage,
|
|
2472
|
+
pageSize: limit,
|
|
2473
|
+
total: totalEntries,
|
|
2474
|
+
pageCount: Math.ceil(totalEntries / limit) || 1
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2133
2478
|
}
|
|
2134
2479
|
if (queryCache.size >= MAX_QUERY_CACHE_SIZE) {
|
|
2135
2480
|
const toDelete = Array.from(queryCache.keys()).slice(0, 100);
|
|
@@ -2198,7 +2543,12 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2198
2543
|
for (const item of selectedEntries) {
|
|
2199
2544
|
if (!item.uid || !item.documentId) continue;
|
|
2200
2545
|
const key = `${item.uid}@@@${item.documentId}`;
|
|
2201
|
-
targetsMap.set(key, {
|
|
2546
|
+
targetsMap.set(key, {
|
|
2547
|
+
uid: item.uid,
|
|
2548
|
+
documentId: item.documentId,
|
|
2549
|
+
dynamicZoneIndexes: item.dynamicZoneIndexes,
|
|
2550
|
+
targetComponentUid: item.targetComponentUid
|
|
2551
|
+
});
|
|
2202
2552
|
}
|
|
2203
2553
|
if (targetsMap.size === 0) {
|
|
2204
2554
|
throw new Error("No collections or entries were selected for preview.");
|
|
@@ -2262,6 +2612,49 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2262
2612
|
}
|
|
2263
2613
|
const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
|
|
2264
2614
|
if (isComponentNested) {
|
|
2615
|
+
const parsed = parseDynamicZonePath(attribute);
|
|
2616
|
+
const rootKey = parsed.rootKey;
|
|
2617
|
+
const isRootDz = contentType?.attributes?.[rootKey]?.type === "dynamiczone";
|
|
2618
|
+
const targetIndexes = target.dynamicZoneIndexes;
|
|
2619
|
+
const targetComponentUid = target.targetComponentUid || parsed.targetComponentUid;
|
|
2620
|
+
if (isRootDz && currentEntry?.[rootKey] && Array.isArray(currentEntry[rootKey])) {
|
|
2621
|
+
let affectedMatches = getAllNestedValues({ [rootKey]: currentEntry[rootKey] }, attribute);
|
|
2622
|
+
if (targetIndexes && targetIndexes.length > 0) {
|
|
2623
|
+
affectedMatches = affectedMatches.filter((m) => targetIndexes.includes(m.index));
|
|
2624
|
+
}
|
|
2625
|
+
if (targetComponentUid) {
|
|
2626
|
+
affectedMatches = affectedMatches.filter((m) => m.componentUid === targetComponentUid);
|
|
2627
|
+
}
|
|
2628
|
+
if (affectedMatches.length > 0) {
|
|
2629
|
+
for (const match of affectedMatches) {
|
|
2630
|
+
let blockNewVal = value;
|
|
2631
|
+
if (regexObj && typeof match.value === "string") {
|
|
2632
|
+
blockNewVal = match.value.replace(regexObj, value);
|
|
2633
|
+
}
|
|
2634
|
+
const hasChange3 = String(match.value ?? "") !== String(blockNewVal ?? "");
|
|
2635
|
+
if (hasChange3) {
|
|
2636
|
+
willUpdateCount++;
|
|
2637
|
+
} else {
|
|
2638
|
+
unchangedCount++;
|
|
2639
|
+
}
|
|
2640
|
+
diffs.push({
|
|
2641
|
+
uid: target.uid,
|
|
2642
|
+
collectionName: displayName,
|
|
2643
|
+
documentId: target.documentId,
|
|
2644
|
+
label: `${entryLabel} [Block #${match.index} (${match.componentName})]`,
|
|
2645
|
+
attribute: `${rootKey}[#${match.index} ${match.componentName}].${parsed.subPath}`,
|
|
2646
|
+
oldValue: match.value ?? null,
|
|
2647
|
+
newValue: blockNewVal,
|
|
2648
|
+
oldDisplay: match.value !== void 0 && match.value !== null && String(match.value).trim() !== "" ? String(match.value) : "(empty)",
|
|
2649
|
+
newDisplay: String(blockNewVal ?? ""),
|
|
2650
|
+
hasChange: hasChange3,
|
|
2651
|
+
status: hasChange3 ? "will_update" : "unchanged",
|
|
2652
|
+
message: hasChange3 ? "Block value will be updated" : "Block already has target value"
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2265
2658
|
const currentVal2 = getDeepProperty(currentEntry, attribute);
|
|
2266
2659
|
let newValue2 = value;
|
|
2267
2660
|
if (regexObj && typeof currentVal2 === "string") {
|
|
@@ -2841,7 +3234,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2841
3234
|
const { isBusinessHours } = checkBusinessHours(config);
|
|
2842
3235
|
if (options.executeImmediately || !isBusinessHours) {
|
|
2843
3236
|
batch.status = "approved";
|
|
2844
|
-
const result =
|
|
3237
|
+
const result = await executeBatchByPayload(strapi, batch, checkerUser);
|
|
2845
3238
|
batch.status = "executed";
|
|
2846
3239
|
batch.executionResult = result;
|
|
2847
3240
|
return {
|
|
@@ -2902,7 +3295,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2902
3295
|
if (batch.status === "executed") {
|
|
2903
3296
|
throw new Error(`Batch ${batchId} has already been executed.`);
|
|
2904
3297
|
}
|
|
2905
|
-
const result =
|
|
3298
|
+
const result = await executeBatchByPayload(strapi, batch, currentUser);
|
|
2906
3299
|
batch.status = "executed";
|
|
2907
3300
|
batch.executionResult = result;
|
|
2908
3301
|
return {
|
|
@@ -3149,6 +3542,20 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3149
3542
|
},
|
|
3150
3543
|
invalidateCollectionCache(uid) {
|
|
3151
3544
|
invalidateCollectionCache(uid);
|
|
3545
|
+
},
|
|
3546
|
+
registerPendingBatch(batch, jobState) {
|
|
3547
|
+
pendingBatches.set(batch.id, batch);
|
|
3548
|
+
if (jobState) {
|
|
3549
|
+
batchJobsMap.set(batch.id, jobState);
|
|
3550
|
+
}
|
|
3551
|
+
return batch;
|
|
3552
|
+
},
|
|
3553
|
+
getExecutionWindowConfig() {
|
|
3554
|
+
return getExecutionWindowConfig(strapi);
|
|
3555
|
+
},
|
|
3556
|
+
checkBusinessHours(config) {
|
|
3557
|
+
const cfg = config || getExecutionWindowConfig(strapi);
|
|
3558
|
+
return checkBusinessHours(cfg);
|
|
3152
3559
|
}
|
|
3153
3560
|
};
|
|
3154
3561
|
};
|
|
@@ -4355,36 +4762,589 @@ function getNestedExportValue(entry, path) {
|
|
|
4355
4762
|
}
|
|
4356
4763
|
return current;
|
|
4357
4764
|
}
|
|
4358
|
-
const
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
if (!ct) {
|
|
4365
|
-
throw new Error(`Content type ${uid} not found`);
|
|
4765
|
+
const csvImportSessions = /* @__PURE__ */ new Map();
|
|
4766
|
+
function cleanupStaleSessions() {
|
|
4767
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
4768
|
+
for (const [id, session] of csvImportSessions.entries()) {
|
|
4769
|
+
if (session.createdAt < oneHourAgo) {
|
|
4770
|
+
csvImportSessions.delete(id);
|
|
4366
4771
|
}
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
const SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
4775
|
+
"documentid",
|
|
4776
|
+
"document_id",
|
|
4777
|
+
"docid",
|
|
4778
|
+
"id",
|
|
4779
|
+
"createdat",
|
|
4780
|
+
"updatedat",
|
|
4781
|
+
"publishedat",
|
|
4782
|
+
"createdby",
|
|
4783
|
+
"updatedby",
|
|
4784
|
+
"localizations",
|
|
4785
|
+
"locale",
|
|
4786
|
+
"status"
|
|
4787
|
+
]);
|
|
4788
|
+
async function findExistingDocument(strapi, uid, targetDocId, docLocale) {
|
|
4789
|
+
if (!targetDocId) return null;
|
|
4790
|
+
try {
|
|
4791
|
+
const queryArgs = { documentId: targetDocId };
|
|
4792
|
+
if (docLocale) queryArgs.locale = docLocale;
|
|
4793
|
+
const found = await strapi.documents(uid).findOne(queryArgs);
|
|
4794
|
+
if (found) return found;
|
|
4795
|
+
} catch {
|
|
4796
|
+
}
|
|
4797
|
+
if (/^\d+$/.test(targetDocId)) {
|
|
4798
|
+
try {
|
|
4799
|
+
const queryArgs = {
|
|
4800
|
+
filters: { id: parseInt(targetDocId, 10) },
|
|
4801
|
+
limit: 1
|
|
4802
|
+
};
|
|
4803
|
+
if (docLocale) queryArgs.locale = docLocale;
|
|
4804
|
+
const list = await strapi.documents(uid).findMany(queryArgs);
|
|
4805
|
+
if (list && list.length > 0) return list[0];
|
|
4806
|
+
} catch {
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
return null;
|
|
4810
|
+
}
|
|
4811
|
+
async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
4812
|
+
const attrs = ct.attributes || {};
|
|
4813
|
+
const { mode = "auto", publish = true, user } = options;
|
|
4814
|
+
const stats = {
|
|
4815
|
+
total: rows.length,
|
|
4816
|
+
created: 0,
|
|
4817
|
+
updated: 0,
|
|
4818
|
+
skipped: 0,
|
|
4819
|
+
newWithoutDocId: 0,
|
|
4820
|
+
errors: [],
|
|
4821
|
+
diffs: []
|
|
4822
|
+
};
|
|
4823
|
+
for (let i = 0; i < rows.length; i++) {
|
|
4824
|
+
const rowNumber = startRowNumber + i;
|
|
4825
|
+
const row = rows[i];
|
|
4826
|
+
let docId = extractDocId(row);
|
|
4827
|
+
const rowLocale = extractLocale(row);
|
|
4828
|
+
if (ct.kind === "singleType" && !docId) {
|
|
4829
|
+
try {
|
|
4830
|
+
const findSingleArgs = {};
|
|
4831
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
4832
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
4833
|
+
if (singleDoc?.documentId) {
|
|
4834
|
+
docId = singleDoc.documentId;
|
|
4835
|
+
}
|
|
4836
|
+
} catch {
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
const payload = {};
|
|
4840
|
+
for (const [key, rawVal] of Object.entries(row)) {
|
|
4841
|
+
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
4842
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
4843
|
+
if (key.includes(".")) {
|
|
4844
|
+
const [parent, child] = key.split(".");
|
|
4845
|
+
const parentDef = attrs[parent];
|
|
4846
|
+
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
4847
|
+
const comp = strapi.components[parentDef.component];
|
|
4848
|
+
const childDef = comp?.attributes?.[child];
|
|
4849
|
+
if (childDef) {
|
|
4850
|
+
if (!payload[parent]) payload[parent] = {};
|
|
4851
|
+
let val = rawVal;
|
|
4852
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4853
|
+
val = null;
|
|
4854
|
+
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
4855
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
4856
|
+
val = isNaN(parsedInt) ? null : parsedInt;
|
|
4857
|
+
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
4858
|
+
const parsedFloat = parseFloat(rawVal);
|
|
4859
|
+
val = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4860
|
+
} else if (childDef.type === "boolean") {
|
|
4861
|
+
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4862
|
+
} else if (childDef.type === "json") {
|
|
4863
|
+
try {
|
|
4864
|
+
val = JSON.parse(rawVal);
|
|
4865
|
+
} catch {
|
|
4866
|
+
val = rawVal;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
payload[parent][child] = val;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
let attrDef = attrs[key];
|
|
4875
|
+
if (!attrDef) {
|
|
4876
|
+
const matchKey = Object.keys(attrs).find(
|
|
4877
|
+
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
4878
|
+
);
|
|
4879
|
+
if (matchKey) {
|
|
4880
|
+
attrDef = attrs[matchKey];
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
if (!attrDef) continue;
|
|
4884
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4885
|
+
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
4886
|
+
payload[key] = null;
|
|
4887
|
+
}
|
|
4888
|
+
continue;
|
|
4889
|
+
}
|
|
4377
4890
|
switch (attrDef.type) {
|
|
4378
|
-
case "string":
|
|
4379
|
-
case "text":
|
|
4380
|
-
case "richtext":
|
|
4381
|
-
sampleVal = `Sample ${attrName}`;
|
|
4382
|
-
break;
|
|
4383
4891
|
case "integer":
|
|
4384
|
-
case "biginteger":
|
|
4892
|
+
case "biginteger": {
|
|
4893
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
4894
|
+
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
4895
|
+
break;
|
|
4896
|
+
}
|
|
4385
4897
|
case "float":
|
|
4386
|
-
case "decimal":
|
|
4387
|
-
|
|
4898
|
+
case "decimal": {
|
|
4899
|
+
const parsedFloat = parseFloat(rawVal);
|
|
4900
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4901
|
+
break;
|
|
4902
|
+
}
|
|
4903
|
+
case "boolean":
|
|
4904
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4905
|
+
break;
|
|
4906
|
+
case "json":
|
|
4907
|
+
try {
|
|
4908
|
+
payload[key] = JSON.parse(rawVal);
|
|
4909
|
+
} catch {
|
|
4910
|
+
payload[key] = rawVal;
|
|
4911
|
+
}
|
|
4912
|
+
break;
|
|
4913
|
+
case "dynamiczone": {
|
|
4914
|
+
try {
|
|
4915
|
+
const parsed = JSON.parse(rawVal);
|
|
4916
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
4917
|
+
payload[key] = parsed;
|
|
4918
|
+
}
|
|
4919
|
+
} catch {
|
|
4920
|
+
}
|
|
4921
|
+
break;
|
|
4922
|
+
}
|
|
4923
|
+
case "component": {
|
|
4924
|
+
if (attrDef.repeatable) {
|
|
4925
|
+
try {
|
|
4926
|
+
const parsed = JSON.parse(rawVal);
|
|
4927
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
4928
|
+
payload[key] = parsed;
|
|
4929
|
+
}
|
|
4930
|
+
} catch {
|
|
4931
|
+
}
|
|
4932
|
+
} else {
|
|
4933
|
+
try {
|
|
4934
|
+
const parsed = JSON.parse(rawVal);
|
|
4935
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
4936
|
+
payload[key] = parsed;
|
|
4937
|
+
}
|
|
4938
|
+
} catch {
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
break;
|
|
4942
|
+
}
|
|
4943
|
+
case "relation": {
|
|
4944
|
+
try {
|
|
4945
|
+
const parsed = JSON.parse(rawVal);
|
|
4946
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
4947
|
+
payload[key] = parsed;
|
|
4948
|
+
break;
|
|
4949
|
+
}
|
|
4950
|
+
} catch {
|
|
4951
|
+
}
|
|
4952
|
+
if (isValidDocId(rawVal)) {
|
|
4953
|
+
payload[key] = rawVal;
|
|
4954
|
+
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
4955
|
+
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
4956
|
+
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
4957
|
+
payload[key] = parts;
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
break;
|
|
4961
|
+
}
|
|
4962
|
+
case "media": {
|
|
4963
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
4964
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
4965
|
+
} else {
|
|
4966
|
+
try {
|
|
4967
|
+
const parsed = JSON.parse(rawVal);
|
|
4968
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
4969
|
+
payload[key] = parsed;
|
|
4970
|
+
}
|
|
4971
|
+
} catch {
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
break;
|
|
4975
|
+
}
|
|
4976
|
+
default:
|
|
4977
|
+
payload[key] = rawVal;
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
try {
|
|
4981
|
+
if (mode === "update") {
|
|
4982
|
+
if (!docId) {
|
|
4983
|
+
stats.skipped++;
|
|
4984
|
+
stats.errors.push({
|
|
4985
|
+
row: rowNumber,
|
|
4986
|
+
error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
|
|
4987
|
+
});
|
|
4988
|
+
continue;
|
|
4989
|
+
}
|
|
4990
|
+
const existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
4991
|
+
if (!existing) {
|
|
4992
|
+
stats.skipped++;
|
|
4993
|
+
stats.errors.push({
|
|
4994
|
+
row: rowNumber,
|
|
4995
|
+
documentId: docId,
|
|
4996
|
+
error: `Document with ID "${docId}" not found in database.`
|
|
4997
|
+
});
|
|
4998
|
+
continue;
|
|
4999
|
+
}
|
|
5000
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5001
|
+
const mergedPayload = { ...payload };
|
|
5002
|
+
for (const key of Object.keys(payload)) {
|
|
5003
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5004
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
if (Object.keys(mergedPayload).length === 0) {
|
|
5008
|
+
stats.skipped++;
|
|
5009
|
+
stats.diffs.push({
|
|
5010
|
+
uid,
|
|
5011
|
+
collectionName: ct.info?.displayName || uid,
|
|
5012
|
+
documentId: resolvedDocId,
|
|
5013
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5014
|
+
attribute: "",
|
|
5015
|
+
oldValue: existing,
|
|
5016
|
+
newValue: {},
|
|
5017
|
+
status: "skipped",
|
|
5018
|
+
message: `Skipped (no editable attributes in row ${rowNumber})`
|
|
5019
|
+
});
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
const updateArgs = {
|
|
5023
|
+
documentId: resolvedDocId,
|
|
5024
|
+
data: mergedPayload
|
|
5025
|
+
};
|
|
5026
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5027
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5028
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5029
|
+
try {
|
|
5030
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5031
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5032
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5033
|
+
} catch (pubErr) {
|
|
5034
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
try {
|
|
5038
|
+
await recordContentHistoryVersion(strapi, {
|
|
5039
|
+
uid,
|
|
5040
|
+
documentId: resolvedDocId,
|
|
5041
|
+
userId: user?.id,
|
|
5042
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5043
|
+
});
|
|
5044
|
+
} catch {
|
|
5045
|
+
}
|
|
5046
|
+
stats.updated++;
|
|
5047
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5048
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5049
|
+
const newVal = payload[attrKey];
|
|
5050
|
+
stats.diffs.push({
|
|
5051
|
+
uid,
|
|
5052
|
+
collectionName: ct.info?.displayName || uid,
|
|
5053
|
+
documentId: resolvedDocId,
|
|
5054
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5055
|
+
attribute: attrKey,
|
|
5056
|
+
oldValue: oldVal,
|
|
5057
|
+
newValue: newVal,
|
|
5058
|
+
status: "updated",
|
|
5059
|
+
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5060
|
+
});
|
|
5061
|
+
}
|
|
5062
|
+
} else if (mode === "create_or_override") {
|
|
5063
|
+
let existing = null;
|
|
5064
|
+
if (docId) {
|
|
5065
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5066
|
+
}
|
|
5067
|
+
if (existing) {
|
|
5068
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5069
|
+
const mergedPayload = { ...payload };
|
|
5070
|
+
for (const key of Object.keys(payload)) {
|
|
5071
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5072
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
const updateArgs = {
|
|
5076
|
+
documentId: resolvedDocId,
|
|
5077
|
+
data: mergedPayload
|
|
5078
|
+
};
|
|
5079
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5080
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5081
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5082
|
+
try {
|
|
5083
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5084
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5085
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5086
|
+
} catch (pubErr) {
|
|
5087
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
try {
|
|
5091
|
+
await recordContentHistoryVersion(strapi, {
|
|
5092
|
+
uid,
|
|
5093
|
+
documentId: resolvedDocId,
|
|
5094
|
+
userId: user?.id,
|
|
5095
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5096
|
+
});
|
|
5097
|
+
} catch {
|
|
5098
|
+
}
|
|
5099
|
+
stats.updated++;
|
|
5100
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5101
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5102
|
+
const newVal = payload[attrKey];
|
|
5103
|
+
stats.diffs.push({
|
|
5104
|
+
uid,
|
|
5105
|
+
collectionName: ct.info?.displayName || uid,
|
|
5106
|
+
documentId: resolvedDocId,
|
|
5107
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5108
|
+
attribute: attrKey,
|
|
5109
|
+
oldValue: oldVal,
|
|
5110
|
+
newValue: newVal,
|
|
5111
|
+
status: "updated",
|
|
5112
|
+
message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
continue;
|
|
5116
|
+
}
|
|
5117
|
+
const createArgs = { data: payload };
|
|
5118
|
+
if (rowLocale) createArgs.locale = rowLocale;
|
|
5119
|
+
const created = await strapi.documents(uid).create(createArgs);
|
|
5120
|
+
const newDocId = created?.documentId || docId;
|
|
5121
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
5122
|
+
try {
|
|
5123
|
+
const pubArgs = { documentId: newDocId };
|
|
5124
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5125
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5126
|
+
} catch (pubErr) {
|
|
5127
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
try {
|
|
5131
|
+
await recordContentHistoryVersion(strapi, {
|
|
5132
|
+
uid,
|
|
5133
|
+
documentId: newDocId,
|
|
5134
|
+
userId: user?.id,
|
|
5135
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5136
|
+
});
|
|
5137
|
+
} catch {
|
|
5138
|
+
}
|
|
5139
|
+
stats.created++;
|
|
5140
|
+
if (!docId) {
|
|
5141
|
+
stats.newWithoutDocId++;
|
|
5142
|
+
}
|
|
5143
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5144
|
+
stats.diffs.push({
|
|
5145
|
+
uid,
|
|
5146
|
+
collectionName: ct.info?.displayName || uid,
|
|
5147
|
+
documentId: newDocId,
|
|
5148
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5149
|
+
attribute: attrKey,
|
|
5150
|
+
oldValue: null,
|
|
5151
|
+
newValue: payload[attrKey],
|
|
5152
|
+
status: "created",
|
|
5153
|
+
message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
} else {
|
|
5157
|
+
let existing = null;
|
|
5158
|
+
if (docId) {
|
|
5159
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5160
|
+
}
|
|
5161
|
+
if (existing) {
|
|
5162
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5163
|
+
const mergedPayload = { ...payload };
|
|
5164
|
+
for (const key of Object.keys(payload)) {
|
|
5165
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5166
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
const updateArgs = {
|
|
5170
|
+
documentId: resolvedDocId,
|
|
5171
|
+
data: mergedPayload
|
|
5172
|
+
};
|
|
5173
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5174
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5175
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5176
|
+
try {
|
|
5177
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5178
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5179
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5180
|
+
} catch (pubErr) {
|
|
5181
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
try {
|
|
5185
|
+
await recordContentHistoryVersion(strapi, {
|
|
5186
|
+
uid,
|
|
5187
|
+
documentId: resolvedDocId,
|
|
5188
|
+
userId: user?.id,
|
|
5189
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5190
|
+
});
|
|
5191
|
+
} catch {
|
|
5192
|
+
}
|
|
5193
|
+
stats.updated++;
|
|
5194
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5195
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5196
|
+
const newVal = payload[attrKey];
|
|
5197
|
+
stats.diffs.push({
|
|
5198
|
+
uid,
|
|
5199
|
+
collectionName: ct.info?.displayName || uid,
|
|
5200
|
+
documentId: resolvedDocId,
|
|
5201
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5202
|
+
attribute: attrKey,
|
|
5203
|
+
oldValue: oldVal,
|
|
5204
|
+
newValue: newVal,
|
|
5205
|
+
status: "updated",
|
|
5206
|
+
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
continue;
|
|
5210
|
+
}
|
|
5211
|
+
const createArgs = { data: payload };
|
|
5212
|
+
if (rowLocale) createArgs.locale = rowLocale;
|
|
5213
|
+
const created = await strapi.documents(uid).create(createArgs);
|
|
5214
|
+
const newDocId = created?.documentId || docId;
|
|
5215
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
5216
|
+
try {
|
|
5217
|
+
const pubArgs = { documentId: newDocId };
|
|
5218
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5219
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5220
|
+
} catch (pubErr) {
|
|
5221
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
try {
|
|
5225
|
+
await recordContentHistoryVersion(strapi, {
|
|
5226
|
+
uid,
|
|
5227
|
+
documentId: newDocId,
|
|
5228
|
+
userId: user?.id,
|
|
5229
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5230
|
+
});
|
|
5231
|
+
} catch {
|
|
5232
|
+
}
|
|
5233
|
+
stats.created++;
|
|
5234
|
+
if (!docId) {
|
|
5235
|
+
stats.newWithoutDocId++;
|
|
5236
|
+
}
|
|
5237
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5238
|
+
stats.diffs.push({
|
|
5239
|
+
uid,
|
|
5240
|
+
collectionName: ct.info?.displayName || uid,
|
|
5241
|
+
documentId: newDocId,
|
|
5242
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5243
|
+
attribute: attrKey,
|
|
5244
|
+
oldValue: null,
|
|
5245
|
+
newValue: payload[attrKey],
|
|
5246
|
+
status: "created",
|
|
5247
|
+
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})`
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
stats.errors.push({
|
|
5253
|
+
row: rowNumber,
|
|
5254
|
+
documentId: docId || void 0,
|
|
5255
|
+
error: extractDetailedErrorMessage(err)
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
return stats;
|
|
5260
|
+
}
|
|
5261
|
+
async function recordCsvAuditLog(strapi, ct, uid, batchId, stats, user) {
|
|
5262
|
+
if (stats.updated === 0 && stats.created === 0) return;
|
|
5263
|
+
try {
|
|
5264
|
+
const auditService2 = strapi.plugin("bulk-updater").service("audit");
|
|
5265
|
+
if (!auditService2) return;
|
|
5266
|
+
const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
|
|
5267
|
+
if (!isEnabled) return;
|
|
5268
|
+
const userEmail = user?.email || "admin@strapi.io";
|
|
5269
|
+
const userName = user ? `${user.firstname || ""} ${user.lastname || ""}`.trim() : "CSV Importer";
|
|
5270
|
+
const collectionDisplayName = ct.info?.displayName || uid;
|
|
5271
|
+
const updatedDiffs = stats.diffs.filter(
|
|
5272
|
+
(d) => d.status === "updated" || d.status === "created"
|
|
5273
|
+
);
|
|
5274
|
+
const uniqueAttributes = Array.from(
|
|
5275
|
+
new Set(updatedDiffs.map((d) => d.attribute))
|
|
5276
|
+
).filter(Boolean);
|
|
5277
|
+
const attrListStr = uniqueAttributes.length > 0 ? uniqueAttributes.join(", ") : "fields";
|
|
5278
|
+
const batchAttribute = `CSV [${collectionDisplayName}]: ${attrListStr}`;
|
|
5279
|
+
let batchValue = "";
|
|
5280
|
+
if (updatedDiffs.length === 1) {
|
|
5281
|
+
const d = updatedDiffs[0];
|
|
5282
|
+
const newStr = d.newValue !== null && d.newValue !== void 0 ? String(d.newValue) : "(empty)";
|
|
5283
|
+
batchValue = `"${d.label}": ${d.attribute} → "${newStr}"`;
|
|
5284
|
+
} else if (updatedDiffs.length > 1 && stats.updated === 1 && stats.created === 0) {
|
|
5285
|
+
const d = updatedDiffs[0];
|
|
5286
|
+
const changes = updatedDiffs.map((ud) => `${ud.attribute}: "${ud.newValue ?? ""}"`).join(", ");
|
|
5287
|
+
batchValue = `"${d.label}": ${changes}`;
|
|
5288
|
+
} else {
|
|
5289
|
+
const sampleVal = updatedDiffs[0] ? ` (e.g. ${updatedDiffs[0].attribute}: "${updatedDiffs[0].newValue ?? ""}")` : "";
|
|
5290
|
+
batchValue = `${stats.updated} updated, ${stats.created} created in ${collectionDisplayName}${sampleVal}`;
|
|
5291
|
+
}
|
|
5292
|
+
const batchSummary = {
|
|
5293
|
+
batchId,
|
|
5294
|
+
attribute: batchAttribute,
|
|
5295
|
+
value: batchValue,
|
|
5296
|
+
updatedCount: stats.updated + stats.created,
|
|
5297
|
+
skippedCount: stats.skipped,
|
|
5298
|
+
totalTargets: stats.total
|
|
5299
|
+
};
|
|
5300
|
+
const userObj = {
|
|
5301
|
+
email: userEmail || "admin@strapi.io",
|
|
5302
|
+
firstname: userName || "Admin"
|
|
5303
|
+
};
|
|
5304
|
+
if (typeof auditService2.recordBatch === "function") {
|
|
5305
|
+
await auditService2.recordBatch(batchSummary, stats.diffs, userObj);
|
|
5306
|
+
} else if (typeof auditService2.logOperation === "function") {
|
|
5307
|
+
await auditService2.logOperation({
|
|
5308
|
+
...batchSummary,
|
|
5309
|
+
userEmail,
|
|
5310
|
+
userName,
|
|
5311
|
+
diffs: stats.diffs
|
|
5312
|
+
});
|
|
5313
|
+
}
|
|
5314
|
+
} catch (auditErr) {
|
|
5315
|
+
strapi.log.warn("[bulk-updater] Failed logging CSV import audit:", auditErr);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
const csvService = ({ strapi }) => ({
|
|
5319
|
+
/**
|
|
5320
|
+
* Generates a sample template CSV with schema attribute headers and sample values
|
|
5321
|
+
*/
|
|
5322
|
+
async getTemplateCsv(uid) {
|
|
5323
|
+
const ct = strapi.contentTypes[uid];
|
|
5324
|
+
if (!ct) {
|
|
5325
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5326
|
+
}
|
|
5327
|
+
const attrs = ct.attributes || {};
|
|
5328
|
+
const excluded = ["createdAt", "updatedAt", "publishedAt", "createdBy", "updatedBy", "localizations"];
|
|
5329
|
+
const headers = ["documentId"];
|
|
5330
|
+
const sampleRow = ["(Leave empty to create new, or enter existing docId)"];
|
|
5331
|
+
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
5332
|
+
if (excluded.includes(attrName)) continue;
|
|
5333
|
+
if (attrDef.type === "relation") continue;
|
|
5334
|
+
if (attrDef.type === "component" || attrDef.type === "dynamiczone") continue;
|
|
5335
|
+
headers.push(attrName);
|
|
5336
|
+
let sampleVal = "";
|
|
5337
|
+
switch (attrDef.type) {
|
|
5338
|
+
case "string":
|
|
5339
|
+
case "text":
|
|
5340
|
+
case "richtext":
|
|
5341
|
+
sampleVal = `Sample ${attrName}`;
|
|
5342
|
+
break;
|
|
5343
|
+
case "integer":
|
|
5344
|
+
case "biginteger":
|
|
5345
|
+
case "float":
|
|
5346
|
+
case "decimal":
|
|
5347
|
+
sampleVal = "10";
|
|
4388
5348
|
break;
|
|
4389
5349
|
case "boolean":
|
|
4390
5350
|
sampleVal = "true";
|
|
@@ -4478,40 +5438,64 @@ const csvService = ({ strapi }) => ({
|
|
|
4478
5438
|
}
|
|
4479
5439
|
}
|
|
4480
5440
|
const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
|
|
4481
|
-
|
|
5441
|
+
const lines = [];
|
|
5442
|
+
lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
|
|
5443
|
+
let totalExported = 0;
|
|
5444
|
+
const maxLimit = options.limit && options.limit > 0 ? options.limit : Infinity;
|
|
5445
|
+
const CHUNK_SIZE = 1e3;
|
|
4482
5446
|
if (ct.kind === "singleType") {
|
|
4483
5447
|
const findFirstArgs = {};
|
|
4484
5448
|
if (deepPopulate) {
|
|
4485
5449
|
findFirstArgs.populate = deepPopulate;
|
|
4486
5450
|
}
|
|
4487
5451
|
const doc = await strapi.documents(uid).findFirst(findFirstArgs);
|
|
4488
|
-
|
|
5452
|
+
if (doc) {
|
|
5453
|
+
const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(doc, f.key)));
|
|
5454
|
+
lines.push(rowValues.join(","));
|
|
5455
|
+
totalExported = 1;
|
|
5456
|
+
}
|
|
4489
5457
|
} else {
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
5458
|
+
let currentPage = 1;
|
|
5459
|
+
let hasMore = true;
|
|
5460
|
+
while (hasMore && totalExported < maxLimit) {
|
|
5461
|
+
const remaining = maxLimit - totalExported;
|
|
5462
|
+
const currentBatchLimit = Math.min(CHUNK_SIZE, remaining);
|
|
5463
|
+
const findPageArgs = {
|
|
5464
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0,
|
|
5465
|
+
page: currentPage,
|
|
5466
|
+
pageSize: currentBatchLimit,
|
|
5467
|
+
sort: { createdAt: "desc" }
|
|
5468
|
+
};
|
|
5469
|
+
if (deepPopulate) {
|
|
5470
|
+
findPageArgs.populate = deepPopulate;
|
|
5471
|
+
}
|
|
5472
|
+
const chunkEntries = await strapi.documents(uid).findMany(findPageArgs);
|
|
5473
|
+
if (!chunkEntries || chunkEntries.length === 0) {
|
|
5474
|
+
hasMore = false;
|
|
5475
|
+
break;
|
|
5476
|
+
}
|
|
5477
|
+
for (const entry of chunkEntries) {
|
|
5478
|
+
const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(entry, f.key)));
|
|
5479
|
+
lines.push(rowValues.join(","));
|
|
5480
|
+
totalExported++;
|
|
5481
|
+
if (totalExported >= maxLimit) {
|
|
5482
|
+
hasMore = false;
|
|
5483
|
+
break;
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
if (chunkEntries.length < currentBatchLimit) {
|
|
5487
|
+
hasMore = false;
|
|
5488
|
+
} else {
|
|
5489
|
+
currentPage++;
|
|
5490
|
+
}
|
|
4497
5491
|
}
|
|
4498
|
-
entries = await strapi.documents(uid).findMany(findManyArgs);
|
|
4499
|
-
}
|
|
4500
|
-
const lines = [];
|
|
4501
|
-
lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
|
|
4502
|
-
for (const entry of entries || []) {
|
|
4503
|
-
const rowValues = fields.map((f) => {
|
|
4504
|
-
const val = getNestedExportValue(entry, f.key);
|
|
4505
|
-
return escapeCsvValue(val);
|
|
4506
|
-
});
|
|
4507
|
-
lines.push(rowValues.join(","));
|
|
4508
5492
|
}
|
|
4509
5493
|
const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
|
|
4510
5494
|
const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
4511
5495
|
return {
|
|
4512
5496
|
filename,
|
|
4513
5497
|
csvContent,
|
|
4514
|
-
count:
|
|
5498
|
+
count: totalExported,
|
|
4515
5499
|
fields: fields.map((f) => f.key)
|
|
4516
5500
|
};
|
|
4517
5501
|
},
|
|
@@ -4572,7 +5556,7 @@ const csvService = ({ strapi }) => ({
|
|
|
4572
5556
|
const uids = Object.keys(byUid);
|
|
4573
5557
|
if (uids.length === 1) {
|
|
4574
5558
|
const uid = uids[0];
|
|
4575
|
-
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
|
|
5559
|
+
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid], limit: 0 });
|
|
4576
5560
|
}
|
|
4577
5561
|
const rows = [];
|
|
4578
5562
|
const headers = [
|
|
@@ -4623,7 +5607,77 @@ const csvService = ({ strapi }) => ({
|
|
|
4623
5607
|
};
|
|
4624
5608
|
},
|
|
4625
5609
|
/**
|
|
4626
|
-
* Imports
|
|
5610
|
+
* Imports a single chunk of CSV rows (for large dataset streaming/batching).
|
|
5611
|
+
* Aggregates stats across chunks and writes unified audit trail on the final chunk.
|
|
5612
|
+
*/
|
|
5613
|
+
async importChunk(uid, rows, options) {
|
|
5614
|
+
const ct = strapi.contentTypes[uid];
|
|
5615
|
+
if (!ct) {
|
|
5616
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5617
|
+
}
|
|
5618
|
+
cleanupStaleSessions();
|
|
5619
|
+
const batchId = options.batchId || `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5620
|
+
let session = csvImportSessions.get(batchId);
|
|
5621
|
+
if (!session || options.isFirstChunk) {
|
|
5622
|
+
session = {
|
|
5623
|
+
batchId,
|
|
5624
|
+
uid,
|
|
5625
|
+
createdAt: Date.now(),
|
|
5626
|
+
total: 0,
|
|
5627
|
+
created: 0,
|
|
5628
|
+
updated: 0,
|
|
5629
|
+
skipped: 0,
|
|
5630
|
+
newWithoutDocId: 0,
|
|
5631
|
+
errors: [],
|
|
5632
|
+
diffs: []
|
|
5633
|
+
};
|
|
5634
|
+
csvImportSessions.set(batchId, session);
|
|
5635
|
+
}
|
|
5636
|
+
const startRowNumber = options.startRowNumber || session.total + 2;
|
|
5637
|
+
const chunkStats = await processCsvRows(strapi, ct, uid, rows || [], startRowNumber, options);
|
|
5638
|
+
session.total += chunkStats.total;
|
|
5639
|
+
session.created += chunkStats.created;
|
|
5640
|
+
session.updated += chunkStats.updated;
|
|
5641
|
+
session.skipped += chunkStats.skipped;
|
|
5642
|
+
session.newWithoutDocId += chunkStats.newWithoutDocId;
|
|
5643
|
+
session.errors.push(...chunkStats.errors);
|
|
5644
|
+
session.diffs.push(...chunkStats.diffs);
|
|
5645
|
+
if (options.isLastChunk) {
|
|
5646
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, session, options.user);
|
|
5647
|
+
try {
|
|
5648
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5649
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5650
|
+
bulkService.invalidateCollectionCache(uid);
|
|
5651
|
+
}
|
|
5652
|
+
} catch {
|
|
5653
|
+
}
|
|
5654
|
+
const aggregateStats = {
|
|
5655
|
+
total: session.total,
|
|
5656
|
+
created: session.created,
|
|
5657
|
+
updated: session.updated,
|
|
5658
|
+
skipped: session.skipped,
|
|
5659
|
+
newWithoutDocId: session.newWithoutDocId,
|
|
5660
|
+
errors: session.errors,
|
|
5661
|
+
diffs: session.diffs
|
|
5662
|
+
};
|
|
5663
|
+
csvImportSessions.delete(batchId);
|
|
5664
|
+
return {
|
|
5665
|
+
success: true,
|
|
5666
|
+
batchId,
|
|
5667
|
+
chunkStats,
|
|
5668
|
+
stats: aggregateStats,
|
|
5669
|
+
isFinished: true
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
return {
|
|
5673
|
+
success: true,
|
|
5674
|
+
batchId,
|
|
5675
|
+
chunkStats,
|
|
5676
|
+
isFinished: false
|
|
5677
|
+
};
|
|
5678
|
+
},
|
|
5679
|
+
/**
|
|
5680
|
+
* Imports CSV records to update existing records or insert/override records (single payload).
|
|
4627
5681
|
*/
|
|
4628
5682
|
async importCsv(uid, csvText, options) {
|
|
4629
5683
|
const ct = strapi.contentTypes[uid];
|
|
@@ -4634,560 +5688,433 @@ const csvService = ({ strapi }) => ({
|
|
|
4634
5688
|
if (!parsedRows || parsedRows.length === 0) {
|
|
4635
5689
|
throw new Error("CSV is empty or could not be parsed.");
|
|
4636
5690
|
}
|
|
4637
|
-
const attrs = ct.attributes || {};
|
|
4638
|
-
const { mode = "auto", publish = true, user } = options;
|
|
4639
|
-
const stats = {
|
|
4640
|
-
total: parsedRows.length,
|
|
4641
|
-
created: 0,
|
|
4642
|
-
updated: 0,
|
|
4643
|
-
skipped: 0,
|
|
4644
|
-
newWithoutDocId: 0,
|
|
4645
|
-
errors: [],
|
|
4646
|
-
diffs: []
|
|
4647
|
-
};
|
|
4648
5691
|
const batchId = `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
4649
|
-
const
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
if (
|
|
4653
|
-
|
|
4654
|
-
const queryArgs = { documentId: targetDocId };
|
|
4655
|
-
if (docLocale) queryArgs.locale = docLocale;
|
|
4656
|
-
const found = await strapi.documents(uid).findOne(queryArgs);
|
|
4657
|
-
if (found) return found;
|
|
4658
|
-
} catch {
|
|
4659
|
-
}
|
|
4660
|
-
if (/^\d+$/.test(targetDocId)) {
|
|
4661
|
-
try {
|
|
4662
|
-
const queryArgs = {
|
|
4663
|
-
filters: { id: parseInt(targetDocId, 10) },
|
|
4664
|
-
limit: 1
|
|
4665
|
-
};
|
|
4666
|
-
if (docLocale) queryArgs.locale = docLocale;
|
|
4667
|
-
const list = await strapi.documents(uid).findMany(queryArgs);
|
|
4668
|
-
if (list && list.length > 0) return list[0];
|
|
4669
|
-
} catch {
|
|
4670
|
-
}
|
|
5692
|
+
const stats = await processCsvRows(strapi, ct, uid, parsedRows, 2, options);
|
|
5693
|
+
try {
|
|
5694
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5695
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5696
|
+
bulkService.invalidateCollectionCache(uid);
|
|
4671
5697
|
}
|
|
4672
|
-
|
|
5698
|
+
} catch {
|
|
5699
|
+
}
|
|
5700
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, stats, options.user);
|
|
5701
|
+
return {
|
|
5702
|
+
success: true,
|
|
5703
|
+
batchId,
|
|
5704
|
+
stats
|
|
4673
5705
|
};
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
const
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
if (ct.kind === "singleType" && !docId) {
|
|
4694
|
-
try {
|
|
4695
|
-
const findSingleArgs = {};
|
|
4696
|
-
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
4697
|
-
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
4698
|
-
if (singleDoc?.documentId) {
|
|
4699
|
-
docId = singleDoc.documentId;
|
|
4700
|
-
}
|
|
4701
|
-
} catch {
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
const payload = {};
|
|
4705
|
-
for (const [key, rawVal] of Object.entries(row)) {
|
|
4706
|
-
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
4707
|
-
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
4708
|
-
if (key.includes(".")) {
|
|
4709
|
-
const [parent, child] = key.split(".");
|
|
4710
|
-
const parentDef = attrs[parent];
|
|
4711
|
-
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
4712
|
-
const comp = strapi.components[parentDef.component];
|
|
4713
|
-
const childDef = comp?.attributes?.[child];
|
|
4714
|
-
if (childDef) {
|
|
4715
|
-
if (!payload[parent]) payload[parent] = {};
|
|
4716
|
-
let val = rawVal;
|
|
4717
|
-
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4718
|
-
val = null;
|
|
4719
|
-
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
4720
|
-
const parsedInt = parseInt(rawVal, 10);
|
|
4721
|
-
val = isNaN(parsedInt) ? null : parsedInt;
|
|
4722
|
-
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
4723
|
-
const parsedFloat = parseFloat(rawVal);
|
|
4724
|
-
val = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4725
|
-
} else if (childDef.type === "boolean") {
|
|
4726
|
-
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4727
|
-
} else if (childDef.type === "json") {
|
|
4728
|
-
try {
|
|
4729
|
-
val = JSON.parse(rawVal);
|
|
4730
|
-
} catch {
|
|
4731
|
-
val = rawVal;
|
|
4732
|
-
}
|
|
4733
|
-
}
|
|
4734
|
-
payload[parent][child] = val;
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
|
-
continue;
|
|
4738
|
-
}
|
|
4739
|
-
let attrDef = attrs[key];
|
|
4740
|
-
if (!attrDef) {
|
|
4741
|
-
const matchKey = Object.keys(attrs).find(
|
|
4742
|
-
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
4743
|
-
);
|
|
4744
|
-
if (matchKey) {
|
|
4745
|
-
attrDef = attrs[matchKey];
|
|
4746
|
-
}
|
|
4747
|
-
}
|
|
4748
|
-
if (!attrDef) continue;
|
|
4749
|
-
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4750
|
-
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
4751
|
-
payload[key] = null;
|
|
4752
|
-
}
|
|
4753
|
-
continue;
|
|
4754
|
-
}
|
|
4755
|
-
switch (attrDef.type) {
|
|
4756
|
-
case "integer":
|
|
4757
|
-
case "biginteger": {
|
|
4758
|
-
const parsedInt = parseInt(rawVal, 10);
|
|
4759
|
-
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
4760
|
-
break;
|
|
4761
|
-
}
|
|
4762
|
-
case "float":
|
|
4763
|
-
case "decimal": {
|
|
4764
|
-
const parsedFloat = parseFloat(rawVal);
|
|
4765
|
-
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4766
|
-
break;
|
|
4767
|
-
}
|
|
4768
|
-
case "boolean":
|
|
4769
|
-
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4770
|
-
break;
|
|
4771
|
-
case "json":
|
|
4772
|
-
try {
|
|
4773
|
-
payload[key] = JSON.parse(rawVal);
|
|
4774
|
-
} catch {
|
|
4775
|
-
payload[key] = rawVal;
|
|
4776
|
-
}
|
|
4777
|
-
break;
|
|
4778
|
-
case "dynamiczone": {
|
|
4779
|
-
try {
|
|
4780
|
-
const parsed = JSON.parse(rawVal);
|
|
4781
|
-
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
4782
|
-
payload[key] = parsed;
|
|
4783
|
-
}
|
|
4784
|
-
} catch {
|
|
4785
|
-
}
|
|
4786
|
-
break;
|
|
4787
|
-
}
|
|
4788
|
-
case "component": {
|
|
4789
|
-
if (attrDef.repeatable) {
|
|
4790
|
-
try {
|
|
4791
|
-
const parsed = JSON.parse(rawVal);
|
|
4792
|
-
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
4793
|
-
payload[key] = parsed;
|
|
4794
|
-
}
|
|
4795
|
-
} catch {
|
|
4796
|
-
}
|
|
4797
|
-
} else {
|
|
4798
|
-
try {
|
|
4799
|
-
const parsed = JSON.parse(rawVal);
|
|
4800
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
4801
|
-
payload[key] = parsed;
|
|
4802
|
-
}
|
|
4803
|
-
} catch {
|
|
4804
|
-
}
|
|
4805
|
-
}
|
|
4806
|
-
break;
|
|
4807
|
-
}
|
|
4808
|
-
case "relation": {
|
|
4809
|
-
try {
|
|
4810
|
-
const parsed = JSON.parse(rawVal);
|
|
4811
|
-
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
4812
|
-
payload[key] = parsed;
|
|
4813
|
-
break;
|
|
4814
|
-
}
|
|
4815
|
-
} catch {
|
|
4816
|
-
}
|
|
4817
|
-
if (isValidDocId(rawVal)) {
|
|
4818
|
-
payload[key] = rawVal;
|
|
4819
|
-
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
4820
|
-
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
4821
|
-
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
4822
|
-
payload[key] = parts;
|
|
4823
|
-
}
|
|
4824
|
-
}
|
|
4825
|
-
break;
|
|
4826
|
-
}
|
|
4827
|
-
case "media": {
|
|
4828
|
-
if (/^\d+$/.test(rawVal.trim())) {
|
|
4829
|
-
payload[key] = parseInt(rawVal.trim(), 10);
|
|
4830
|
-
} else {
|
|
4831
|
-
try {
|
|
4832
|
-
const parsed = JSON.parse(rawVal);
|
|
4833
|
-
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
4834
|
-
payload[key] = parsed;
|
|
4835
|
-
}
|
|
4836
|
-
} catch {
|
|
4837
|
-
}
|
|
4838
|
-
}
|
|
4839
|
-
break;
|
|
4840
|
-
}
|
|
4841
|
-
default:
|
|
4842
|
-
payload[key] = rawVal;
|
|
4843
|
-
}
|
|
5706
|
+
},
|
|
5707
|
+
/**
|
|
5708
|
+
* Executes a pending CSV batch approved by Checker or queued for off-peak execution
|
|
5709
|
+
*/
|
|
5710
|
+
async executeCsvPendingBatch(uid, rows, batchId, options = {}, checkerUser) {
|
|
5711
|
+
const ct = strapi.contentTypes[uid];
|
|
5712
|
+
if (!ct) {
|
|
5713
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5714
|
+
}
|
|
5715
|
+
const effectiveUser = checkerUser || options.user;
|
|
5716
|
+
const stats = await processCsvRows(strapi, ct, uid, rows || [], 2, {
|
|
5717
|
+
...options,
|
|
5718
|
+
user: effectiveUser
|
|
5719
|
+
});
|
|
5720
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, stats, effectiveUser);
|
|
5721
|
+
try {
|
|
5722
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5723
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5724
|
+
bulkService.invalidateCollectionCache(uid);
|
|
4844
5725
|
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
5726
|
+
} catch {
|
|
5727
|
+
}
|
|
5728
|
+
return {
|
|
5729
|
+
success: true,
|
|
5730
|
+
batchId,
|
|
5731
|
+
stats
|
|
5732
|
+
};
|
|
5733
|
+
},
|
|
5734
|
+
/**
|
|
5735
|
+
* Pre-Flight Dry-Run Validation: Non-mutating analysis verifying document existence,
|
|
5736
|
+
* data types, schema integrity, and quota rule violations before execution.
|
|
5737
|
+
*/
|
|
5738
|
+
async validatePreflight(uid, rows, options = {}) {
|
|
5739
|
+
const ct = strapi.contentTypes[uid];
|
|
5740
|
+
if (!ct) {
|
|
5741
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5742
|
+
}
|
|
5743
|
+
const { mode = "auto" } = options;
|
|
5744
|
+
const attrs = ct.attributes || {};
|
|
5745
|
+
let validationRulesService2 = null;
|
|
5746
|
+
let activeRules = [];
|
|
5747
|
+
try {
|
|
5748
|
+
validationRulesService2 = strapi.plugin("bulk-updater").service("validationRules");
|
|
5749
|
+
if (validationRulesService2) {
|
|
5750
|
+
const rulesRes = await validationRulesService2.getRules();
|
|
5751
|
+
activeRules = (rulesRes || []).filter((r) => r.enabled);
|
|
5752
|
+
}
|
|
5753
|
+
} catch {
|
|
5754
|
+
}
|
|
5755
|
+
let validRows = 0;
|
|
5756
|
+
let estimatedUpdates = 0;
|
|
5757
|
+
let estimatedCreates = 0;
|
|
5758
|
+
const errors = [];
|
|
5759
|
+
const warnings = [];
|
|
5760
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5761
|
+
const rowNumber = i + 2;
|
|
5762
|
+
const row = rows[i];
|
|
5763
|
+
const docId = extractDocId(row);
|
|
5764
|
+
const rowLocale = extractLocale(row);
|
|
5765
|
+
let rowHasError = false;
|
|
5766
|
+
let existing = null;
|
|
5767
|
+
if (docId) {
|
|
5768
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5769
|
+
}
|
|
5770
|
+
if (mode === "update") {
|
|
5771
|
+
if (!docId) {
|
|
5772
|
+
errors.push({
|
|
5773
|
+
row: rowNumber,
|
|
5774
|
+
error: "Missing or invalid documentId for Update Only mode."
|
|
5775
|
+
});
|
|
5776
|
+
rowHasError = true;
|
|
5777
|
+
} else if (!existing) {
|
|
5778
|
+
errors.push({
|
|
5779
|
+
row: rowNumber,
|
|
5780
|
+
documentId: docId,
|
|
5781
|
+
error: `Document with ID "${docId}" not found in database.`
|
|
5782
|
+
});
|
|
5783
|
+
rowHasError = true;
|
|
5784
|
+
} else {
|
|
5785
|
+
estimatedUpdates++;
|
|
5786
|
+
}
|
|
5787
|
+
} else if (mode === "create_or_override") {
|
|
5788
|
+
if (existing) {
|
|
5789
|
+
estimatedUpdates++;
|
|
5790
|
+
} else {
|
|
5791
|
+
estimatedCreates++;
|
|
5792
|
+
}
|
|
5793
|
+
} else {
|
|
5794
|
+
if (existing) {
|
|
5795
|
+
estimatedUpdates++;
|
|
5796
|
+
} else {
|
|
5797
|
+
estimatedCreates++;
|
|
5798
|
+
if (docId) {
|
|
5799
|
+
warnings.push({
|
|
4859
5800
|
row: rowNumber,
|
|
4860
5801
|
documentId: docId,
|
|
4861
|
-
|
|
4862
|
-
});
|
|
4863
|
-
continue;
|
|
4864
|
-
}
|
|
4865
|
-
const resolvedDocId = existing.documentId || docId;
|
|
4866
|
-
const mergedPayload = { ...payload };
|
|
4867
|
-
for (const key of Object.keys(payload)) {
|
|
4868
|
-
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
4869
|
-
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
if (Object.keys(mergedPayload).length === 0) {
|
|
4873
|
-
stats.skipped++;
|
|
4874
|
-
stats.diffs.push({
|
|
4875
|
-
uid,
|
|
4876
|
-
collectionName: ct.info?.displayName || uid,
|
|
4877
|
-
documentId: resolvedDocId,
|
|
4878
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
4879
|
-
attribute: "",
|
|
4880
|
-
oldValue: existing,
|
|
4881
|
-
newValue: {},
|
|
4882
|
-
status: "skipped",
|
|
4883
|
-
message: `Skipped (no editable attributes in row ${rowNumber})`
|
|
4884
|
-
});
|
|
4885
|
-
continue;
|
|
4886
|
-
}
|
|
4887
|
-
const updateArgs = {
|
|
4888
|
-
documentId: resolvedDocId,
|
|
4889
|
-
data: mergedPayload
|
|
4890
|
-
};
|
|
4891
|
-
if (rowLocale) updateArgs.locale = rowLocale;
|
|
4892
|
-
await strapi.documents(uid).update(updateArgs);
|
|
4893
|
-
if (publish && ct.options?.draftAndPublish) {
|
|
4894
|
-
try {
|
|
4895
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
4896
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
4897
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
4898
|
-
} catch (pubErr) {
|
|
4899
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
4900
|
-
}
|
|
4901
|
-
}
|
|
4902
|
-
try {
|
|
4903
|
-
await recordContentHistoryVersion(strapi, {
|
|
4904
|
-
uid,
|
|
4905
|
-
documentId: resolvedDocId,
|
|
4906
|
-
userId: user?.id,
|
|
4907
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
4908
|
-
});
|
|
4909
|
-
} catch {
|
|
4910
|
-
}
|
|
4911
|
-
stats.updated++;
|
|
4912
|
-
for (const attrKey of Object.keys(payload)) {
|
|
4913
|
-
const oldVal = existing?.[attrKey] ?? null;
|
|
4914
|
-
const newVal = payload[attrKey];
|
|
4915
|
-
stats.diffs.push({
|
|
4916
|
-
uid,
|
|
4917
|
-
collectionName: ct.info?.displayName || uid,
|
|
4918
|
-
documentId: resolvedDocId,
|
|
4919
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
4920
|
-
attribute: attrKey,
|
|
4921
|
-
oldValue: oldVal,
|
|
4922
|
-
newValue: newVal,
|
|
4923
|
-
status: "updated",
|
|
4924
|
-
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5802
|
+
warning: `Document ID "${docId}" not found in DB; will create as a new entry.`
|
|
4925
5803
|
});
|
|
4926
5804
|
}
|
|
4927
|
-
}
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
4949
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
4950
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
4951
|
-
} catch (pubErr) {
|
|
4952
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
4953
|
-
}
|
|
4954
|
-
}
|
|
4955
|
-
try {
|
|
4956
|
-
await recordContentHistoryVersion(strapi, {
|
|
4957
|
-
uid,
|
|
4958
|
-
documentId: resolvedDocId,
|
|
4959
|
-
userId: user?.id,
|
|
4960
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
for (const [colKey, rawVal] of Object.entries(row)) {
|
|
5808
|
+
if (!rawVal || rawVal.trim() === "") continue;
|
|
5809
|
+
const normalizedKey = colKey.toLowerCase().replace(/[\s_-]/g, "");
|
|
5810
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5811
|
+
let attrDef = attrs[colKey];
|
|
5812
|
+
if (!attrDef && !colKey.includes(".")) {
|
|
5813
|
+
const matchKey = Object.keys(attrs).find(
|
|
5814
|
+
(k) => k.toLowerCase() === colKey.toLowerCase() || k.toLowerCase() === colKey.toLowerCase().replace(/\s+/g, "")
|
|
5815
|
+
);
|
|
5816
|
+
if (matchKey) attrDef = attrs[matchKey];
|
|
5817
|
+
}
|
|
5818
|
+
if (attrDef) {
|
|
5819
|
+
if (attrDef.type === "integer" || attrDef.type === "biginteger") {
|
|
5820
|
+
const num = parseInt(rawVal, 10);
|
|
5821
|
+
if (isNaN(num)) {
|
|
5822
|
+
errors.push({
|
|
5823
|
+
row: rowNumber,
|
|
5824
|
+
documentId: docId || void 0,
|
|
5825
|
+
error: `Column "${colKey}" expects an integer, received: "${rawVal}"`
|
|
4961
5826
|
});
|
|
4962
|
-
|
|
5827
|
+
rowHasError = true;
|
|
4963
5828
|
}
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
documentId: resolvedDocId,
|
|
4972
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
4973
|
-
attribute: attrKey,
|
|
4974
|
-
oldValue: oldVal,
|
|
4975
|
-
newValue: newVal,
|
|
4976
|
-
status: "updated",
|
|
4977
|
-
message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5829
|
+
} else if (attrDef.type === "float" || attrDef.type === "decimal") {
|
|
5830
|
+
const num = parseFloat(rawVal);
|
|
5831
|
+
if (isNaN(num)) {
|
|
5832
|
+
errors.push({
|
|
5833
|
+
row: rowNumber,
|
|
5834
|
+
documentId: docId || void 0,
|
|
5835
|
+
error: `Column "${colKey}" expects a numeric float/decimal, received: "${rawVal}"`
|
|
4978
5836
|
});
|
|
5837
|
+
rowHasError = true;
|
|
4979
5838
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
try {
|
|
4988
|
-
const pubArgs = { documentId: newDocId };
|
|
4989
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
4990
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
4991
|
-
} catch (pubErr) {
|
|
4992
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
4993
|
-
}
|
|
4994
|
-
}
|
|
4995
|
-
try {
|
|
4996
|
-
await recordContentHistoryVersion(strapi, {
|
|
4997
|
-
uid,
|
|
4998
|
-
documentId: newDocId,
|
|
4999
|
-
userId: user?.id,
|
|
5000
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5001
|
-
});
|
|
5002
|
-
} catch {
|
|
5003
|
-
}
|
|
5004
|
-
stats.created++;
|
|
5005
|
-
if (!docId) {
|
|
5006
|
-
stats.newWithoutDocId++;
|
|
5007
|
-
}
|
|
5008
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5009
|
-
stats.diffs.push({
|
|
5010
|
-
uid,
|
|
5011
|
-
collectionName: ct.info?.displayName || uid,
|
|
5012
|
-
documentId: newDocId,
|
|
5013
|
-
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5014
|
-
attribute: attrKey,
|
|
5015
|
-
oldValue: null,
|
|
5016
|
-
newValue: payload[attrKey],
|
|
5017
|
-
status: "created",
|
|
5018
|
-
message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
|
|
5019
|
-
});
|
|
5020
|
-
}
|
|
5021
|
-
} else {
|
|
5022
|
-
let existing = null;
|
|
5023
|
-
if (docId) {
|
|
5024
|
-
existing = await findExistingDocument(docId, rowLocale);
|
|
5025
|
-
}
|
|
5026
|
-
if (existing) {
|
|
5027
|
-
const resolvedDocId = existing.documentId || docId;
|
|
5028
|
-
const mergedPayload = { ...payload };
|
|
5029
|
-
for (const key of Object.keys(payload)) {
|
|
5030
|
-
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5031
|
-
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5032
|
-
}
|
|
5033
|
-
}
|
|
5034
|
-
const updateArgs = {
|
|
5035
|
-
documentId: resolvedDocId,
|
|
5036
|
-
data: mergedPayload
|
|
5037
|
-
};
|
|
5038
|
-
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5039
|
-
await strapi.documents(uid).update(updateArgs);
|
|
5040
|
-
if (publish && ct.options?.draftAndPublish) {
|
|
5041
|
-
try {
|
|
5042
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5043
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5044
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5045
|
-
} catch (pubErr) {
|
|
5046
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5047
|
-
}
|
|
5839
|
+
} else if (attrDef.type === "enumeration" && Array.isArray(attrDef.enum)) {
|
|
5840
|
+
if (!attrDef.enum.includes(rawVal.trim())) {
|
|
5841
|
+
warnings.push({
|
|
5842
|
+
row: rowNumber,
|
|
5843
|
+
documentId: docId || void 0,
|
|
5844
|
+
warning: `Column "${colKey}" value "${rawVal}" is not in defined enum options: [${attrDef.enum.join(", ")}]`
|
|
5845
|
+
});
|
|
5048
5846
|
}
|
|
5847
|
+
} else if (attrDef.type === "json") {
|
|
5049
5848
|
try {
|
|
5050
|
-
|
|
5051
|
-
uid,
|
|
5052
|
-
documentId: resolvedDocId,
|
|
5053
|
-
userId: user?.id,
|
|
5054
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5055
|
-
});
|
|
5849
|
+
JSON.parse(rawVal);
|
|
5056
5850
|
} catch {
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
const newVal = payload[attrKey];
|
|
5062
|
-
stats.diffs.push({
|
|
5063
|
-
uid,
|
|
5064
|
-
collectionName: ct.info?.displayName || uid,
|
|
5065
|
-
documentId: resolvedDocId,
|
|
5066
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5067
|
-
attribute: attrKey,
|
|
5068
|
-
oldValue: oldVal,
|
|
5069
|
-
newValue: newVal,
|
|
5070
|
-
status: "updated",
|
|
5071
|
-
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5851
|
+
warnings.push({
|
|
5852
|
+
row: rowNumber,
|
|
5853
|
+
documentId: docId || void 0,
|
|
5854
|
+
warning: `Column "${colKey}" is type JSON but contains non-JSON text.`
|
|
5072
5855
|
});
|
|
5073
5856
|
}
|
|
5074
|
-
continue;
|
|
5075
5857
|
}
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
const
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
const
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5858
|
+
}
|
|
5859
|
+
if (activeRules.length > 0) {
|
|
5860
|
+
for (const rule of activeRules) {
|
|
5861
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) continue;
|
|
5862
|
+
if (rule.includeRichTextHtml && (rawVal.includes("<h") || rawVal.includes("# "))) {
|
|
5863
|
+
const headings = extractHeadingsFromRichText(rawVal);
|
|
5864
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
5865
|
+
const max = rule.quotas?.[tag.toLowerCase()];
|
|
5866
|
+
if (max !== void 0 && count > max) {
|
|
5867
|
+
warnings.push({
|
|
5868
|
+
row: rowNumber,
|
|
5869
|
+
documentId: docId || void 0,
|
|
5870
|
+
warning: `Rule "${rule.name}" quota warning: column "${colKey}" contains ${count} "${tag.toUpperCase()}" headings (maximum allowed: ${max}).`
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5087
5874
|
}
|
|
5088
5875
|
}
|
|
5089
|
-
try {
|
|
5090
|
-
await recordContentHistoryVersion(strapi, {
|
|
5091
|
-
uid,
|
|
5092
|
-
documentId: newDocId,
|
|
5093
|
-
userId: user?.id,
|
|
5094
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5095
|
-
});
|
|
5096
|
-
} catch {
|
|
5097
|
-
}
|
|
5098
|
-
stats.created++;
|
|
5099
|
-
if (!docId) {
|
|
5100
|
-
stats.newWithoutDocId++;
|
|
5101
|
-
}
|
|
5102
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5103
|
-
stats.diffs.push({
|
|
5104
|
-
uid,
|
|
5105
|
-
collectionName: ct.info?.displayName || uid,
|
|
5106
|
-
documentId: newDocId,
|
|
5107
|
-
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5108
|
-
attribute: attrKey,
|
|
5109
|
-
oldValue: null,
|
|
5110
|
-
newValue: payload[attrKey],
|
|
5111
|
-
status: "created",
|
|
5112
|
-
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})`
|
|
5113
|
-
});
|
|
5114
|
-
}
|
|
5115
5876
|
}
|
|
5116
|
-
} catch (err) {
|
|
5117
|
-
stats.errors.push({
|
|
5118
|
-
row: rowNumber,
|
|
5119
|
-
documentId: docId || void 0,
|
|
5120
|
-
error: extractDetailedErrorMessage(err)
|
|
5121
|
-
});
|
|
5122
5877
|
}
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5126
|
-
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5127
|
-
bulkService.invalidateCollectionCache(uid);
|
|
5878
|
+
if (!rowHasError) {
|
|
5879
|
+
validRows++;
|
|
5128
5880
|
}
|
|
5129
|
-
} catch {
|
|
5130
5881
|
}
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5882
|
+
return {
|
|
5883
|
+
success: true,
|
|
5884
|
+
totalRows: rows.length,
|
|
5885
|
+
validRows,
|
|
5886
|
+
estimatedUpdates,
|
|
5887
|
+
estimatedCreates,
|
|
5888
|
+
hasErrors: errors.length > 0,
|
|
5889
|
+
hasWarnings: warnings.length > 0,
|
|
5890
|
+
errors: errors.slice(0, 100),
|
|
5891
|
+
warnings: warnings.slice(0, 100),
|
|
5892
|
+
totalErrorCount: errors.length,
|
|
5893
|
+
totalWarningCount: warnings.length
|
|
5894
|
+
};
|
|
5895
|
+
},
|
|
5896
|
+
/**
|
|
5897
|
+
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
5898
|
+
*/
|
|
5899
|
+
async submitBatch(uid, rows, options = {}) {
|
|
5900
|
+
const ct = strapi.contentTypes[uid];
|
|
5901
|
+
if (!ct) {
|
|
5902
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5903
|
+
}
|
|
5904
|
+
if (!rows || rows.length === 0) {
|
|
5905
|
+
throw new Error("No CSV records provided for batch submission.");
|
|
5906
|
+
}
|
|
5907
|
+
const {
|
|
5908
|
+
executionMode = "immediate",
|
|
5909
|
+
makerNotes = "",
|
|
5910
|
+
forceImmediate = false,
|
|
5911
|
+
mode = "auto",
|
|
5912
|
+
publish = true,
|
|
5913
|
+
user
|
|
5914
|
+
} = options;
|
|
5915
|
+
const executionConfig = getExecutionWindowConfig(strapi);
|
|
5916
|
+
const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
|
|
5917
|
+
const collectionDisplayName = ct.info?.displayName || uid;
|
|
5918
|
+
if (executionMode === "maker_checker") {
|
|
5919
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5920
|
+
const batch = {
|
|
5921
|
+
id: batchId,
|
|
5922
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5923
|
+
createdBy: user ? {
|
|
5924
|
+
id: user.id,
|
|
5925
|
+
email: user.email,
|
|
5926
|
+
firstname: user.firstname,
|
|
5927
|
+
lastname: user.lastname,
|
|
5928
|
+
username: user.username
|
|
5929
|
+
} : { email: "admin@system.local" },
|
|
5930
|
+
payload: {
|
|
5931
|
+
action: "csv_import",
|
|
5932
|
+
uid,
|
|
5933
|
+
rows,
|
|
5934
|
+
options: {
|
|
5935
|
+
mode,
|
|
5936
|
+
publish,
|
|
5937
|
+
user
|
|
5938
|
+
},
|
|
5939
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5940
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5941
|
+
publishOnApproval: publish
|
|
5942
|
+
},
|
|
5943
|
+
summary: {
|
|
5944
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5945
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5946
|
+
totalTargets: rows.length,
|
|
5947
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
5948
|
+
publishOnApproval: publish
|
|
5949
|
+
},
|
|
5950
|
+
status: "pending_approval",
|
|
5951
|
+
executionMode: "maker_checker",
|
|
5952
|
+
makerNotes
|
|
5953
|
+
};
|
|
5954
|
+
registerPendingBatch(batch, {
|
|
5955
|
+
id: batchId,
|
|
5956
|
+
createdAt: batch.createdAt,
|
|
5957
|
+
createdBy: batch.createdBy,
|
|
5958
|
+
status: "pending_approval",
|
|
5959
|
+
executionMode: "maker_checker",
|
|
5960
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5961
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5962
|
+
totalTargets: rows.length,
|
|
5963
|
+
processedCount: 0,
|
|
5964
|
+
updatedCount: 0,
|
|
5965
|
+
skippedCount: 0,
|
|
5966
|
+
failedCount: 0,
|
|
5967
|
+
isLive: false,
|
|
5968
|
+
publishOnApproval: publish,
|
|
5969
|
+
makerNotes,
|
|
5970
|
+
results: []
|
|
5971
|
+
});
|
|
5972
|
+
return {
|
|
5973
|
+
success: true,
|
|
5974
|
+
pending: true,
|
|
5975
|
+
batchId,
|
|
5976
|
+
status: "pending_approval",
|
|
5977
|
+
message: "CSV batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
|
|
5978
|
+
batch
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5981
|
+
if (executionMode === "queue_offpeak") {
|
|
5982
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5983
|
+
const batch = {
|
|
5984
|
+
id: batchId,
|
|
5985
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5986
|
+
createdBy: user ? {
|
|
5987
|
+
id: user.id,
|
|
5988
|
+
email: user.email,
|
|
5989
|
+
firstname: user.firstname,
|
|
5990
|
+
lastname: user.lastname,
|
|
5991
|
+
username: user.username
|
|
5992
|
+
} : { email: "admin@system.local" },
|
|
5993
|
+
payload: {
|
|
5994
|
+
action: "csv_import",
|
|
5995
|
+
uid,
|
|
5996
|
+
rows,
|
|
5997
|
+
options: {
|
|
5998
|
+
mode,
|
|
5999
|
+
publish,
|
|
6000
|
+
user
|
|
6001
|
+
},
|
|
6002
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6003
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6004
|
+
publishOnApproval: publish
|
|
6005
|
+
},
|
|
6006
|
+
summary: {
|
|
6007
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6008
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6009
|
+
totalTargets: rows.length,
|
|
6010
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
6011
|
+
publishOnApproval: publish
|
|
6012
|
+
},
|
|
6013
|
+
status: "queued_offpeak",
|
|
6014
|
+
executionMode: "queue_offpeak",
|
|
6015
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6016
|
+
makerNotes
|
|
6017
|
+
};
|
|
6018
|
+
registerPendingBatch(batch, {
|
|
6019
|
+
id: batchId,
|
|
6020
|
+
createdAt: batch.createdAt,
|
|
6021
|
+
createdBy: batch.createdBy,
|
|
6022
|
+
status: "queued_offpeak",
|
|
6023
|
+
executionMode: "queue_offpeak",
|
|
6024
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6025
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6026
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6027
|
+
totalTargets: rows.length,
|
|
6028
|
+
processedCount: 0,
|
|
6029
|
+
updatedCount: 0,
|
|
6030
|
+
skippedCount: 0,
|
|
6031
|
+
failedCount: 0,
|
|
6032
|
+
isLive: false,
|
|
6033
|
+
publishOnApproval: publish,
|
|
6034
|
+
makerNotes,
|
|
6035
|
+
results: []
|
|
6036
|
+
});
|
|
6037
|
+
return {
|
|
6038
|
+
success: true,
|
|
6039
|
+
pending: true,
|
|
6040
|
+
batchId,
|
|
6041
|
+
status: "queued_offpeak",
|
|
6042
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6043
|
+
message: `CSV batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
|
|
6044
|
+
batch
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
|
|
6048
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
6049
|
+
const batch = {
|
|
6050
|
+
id: batchId,
|
|
6051
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6052
|
+
createdBy: user ? {
|
|
6053
|
+
id: user.id,
|
|
6054
|
+
email: user.email,
|
|
6055
|
+
firstname: user.firstname,
|
|
6056
|
+
lastname: user.lastname,
|
|
6057
|
+
username: user.username
|
|
6058
|
+
} : { email: "admin@system.local" },
|
|
6059
|
+
payload: {
|
|
6060
|
+
action: "csv_import",
|
|
6061
|
+
uid,
|
|
6062
|
+
rows,
|
|
6063
|
+
options: {
|
|
6064
|
+
mode,
|
|
6065
|
+
publish,
|
|
6066
|
+
user
|
|
6067
|
+
},
|
|
6068
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6069
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6070
|
+
publishOnApproval: publish
|
|
6071
|
+
},
|
|
6072
|
+
summary: {
|
|
6073
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6074
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6075
|
+
totalTargets: rows.length,
|
|
6076
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
6077
|
+
publishOnApproval: publish
|
|
6078
|
+
},
|
|
6079
|
+
status: "queued_offpeak",
|
|
6080
|
+
executionMode: "queue_offpeak",
|
|
6081
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6082
|
+
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
6083
|
+
};
|
|
6084
|
+
registerPendingBatch(batch, {
|
|
6085
|
+
id: batchId,
|
|
6086
|
+
createdAt: batch.createdAt,
|
|
6087
|
+
createdBy: batch.createdBy,
|
|
6088
|
+
status: "queued_offpeak",
|
|
6089
|
+
executionMode: "queue_offpeak",
|
|
6090
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6091
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6092
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6093
|
+
totalTargets: rows.length,
|
|
6094
|
+
processedCount: 0,
|
|
6095
|
+
updatedCount: 0,
|
|
6096
|
+
skippedCount: 0,
|
|
6097
|
+
failedCount: 0,
|
|
6098
|
+
isLive: false,
|
|
6099
|
+
publishOnApproval: publish,
|
|
6100
|
+
policyMessage,
|
|
6101
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
6102
|
+
results: []
|
|
6103
|
+
});
|
|
6104
|
+
return {
|
|
6105
|
+
success: true,
|
|
6106
|
+
pending: true,
|
|
6107
|
+
batchId,
|
|
6108
|
+
status: "queued_offpeak",
|
|
6109
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6110
|
+
message: policyMessage,
|
|
6111
|
+
batch
|
|
6112
|
+
};
|
|
5186
6113
|
}
|
|
5187
6114
|
return {
|
|
5188
6115
|
success: true,
|
|
5189
|
-
|
|
5190
|
-
|
|
6116
|
+
pending: false,
|
|
6117
|
+
executeNow: true
|
|
5191
6118
|
};
|
|
5192
6119
|
}
|
|
5193
6120
|
});
|