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