strapi-plugin-bulk-updater 1.3.11 → 1.4.3

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.
@@ -911,9 +911,72 @@ function discoverComponentAttributes(strapi, compUid, prefix, currentDepth, maxD
911
911
  }
912
912
  return Array.from(new Set(discoveredAttrs));
913
913
  }
914
+ function parseDynamicZonePath(path) {
915
+ if (!path) return { cleanPath: "", rootKey: "", subPath: "" };
916
+ const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\]\.(.+)$/);
917
+ if (bracketMatch) {
918
+ const rootKey = bracketMatch[1];
919
+ const selector = bracketMatch[2];
920
+ const subPath = bracketMatch[3];
921
+ const cleanPath = `${rootKey}.${subPath}`;
922
+ if (/^\d+$/.test(selector)) {
923
+ return { cleanPath, rootKey, subPath, targetIndex: parseInt(selector, 10) };
924
+ }
925
+ return { cleanPath, rootKey, subPath, targetComponentUid: selector };
926
+ }
927
+ const parts = path.split(".");
928
+ return {
929
+ cleanPath: path,
930
+ rootKey: parts[0] || "",
931
+ subPath: parts.slice(1).join(".")
932
+ };
933
+ }
934
+ function formatComponentName(compUid) {
935
+ if (!compUid) return "Component";
936
+ const name = compUid.includes(".") ? compUid.split(".")[1] : compUid;
937
+ return name.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
938
+ }
939
+ function getAllNestedValues(obj, path) {
940
+ if (!obj || !path) return [];
941
+ const parsed = parseDynamicZonePath(path);
942
+ const parts = parsed.cleanPath.split(".");
943
+ const [head, ...tail] = parts;
944
+ const rootTarget = obj[head];
945
+ if (Array.isArray(rootTarget)) {
946
+ const results = [];
947
+ rootTarget.forEach((item, idx) => {
948
+ if (!item || typeof item !== "object") return;
949
+ if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) return;
950
+ if (parsed.targetComponentUid && item.__component !== parsed.targetComponentUid) return;
951
+ const subVal = tail.length > 0 ? getDeepProperty(item, tail.join(".")) : item;
952
+ if (subVal !== void 0) {
953
+ results.push({
954
+ index: idx,
955
+ componentUid: item.__component,
956
+ componentName: formatComponentName(item.__component),
957
+ value: subVal,
958
+ path: `${head}[#${idx}${item.__component ? ` ${formatComponentName(item.__component)}` : ""}].${tail.join(".")}`
959
+ });
960
+ }
961
+ });
962
+ return results;
963
+ }
964
+ const singleVal = getDeepProperty(obj, parsed.cleanPath);
965
+ if (singleVal !== void 0) {
966
+ return [
967
+ {
968
+ index: 0,
969
+ value: singleVal,
970
+ path: parsed.cleanPath
971
+ }
972
+ ];
973
+ }
974
+ return [];
975
+ }
914
976
  function isDeepComponentField(contentType, allComponents, attribute) {
915
- if (!attribute || !attribute.includes(".")) return false;
916
- const parts = attribute.split(".");
977
+ if (!attribute || !attribute.includes(".") && !attribute.includes("[")) return false;
978
+ const parsed = parseDynamicZonePath(attribute);
979
+ const parts = parsed.cleanPath.split(".");
917
980
  function checkPath(currentSchema, pathParts) {
918
981
  if (pathParts.length === 0) return true;
919
982
  if (!currentSchema || !currentSchema.attributes) return false;
@@ -929,6 +992,10 @@ function isDeepComponentField(contentType, allComponents, attribute) {
929
992
  }
930
993
  if (attrDef.type === "dynamiczone") {
931
994
  const compUids = Array.isArray(attrDef.components) ? attrDef.components : [];
995
+ if (parsed.targetComponentUid) {
996
+ const comp = allComponents[parsed.targetComponentUid];
997
+ return comp ? checkPath(comp, tail) : false;
998
+ }
932
999
  return compUids.some((compUid) => {
933
1000
  const comp = allComponents[compUid];
934
1001
  return checkPath(comp, tail);
@@ -940,8 +1007,9 @@ function isDeepComponentField(contentType, allComponents, attribute) {
940
1007
  }
941
1008
  function getLeafAttributeSchema(contentType, allComponents, attribute) {
942
1009
  if (!attribute) return null;
943
- if (!attribute.includes(".")) return contentType?.attributes?.[attribute] || null;
944
- const parts = attribute.split(".");
1010
+ const parsed = parseDynamicZonePath(attribute);
1011
+ if (!parsed.cleanPath.includes(".")) return contentType?.attributes?.[parsed.cleanPath] || null;
1012
+ const parts = parsed.cleanPath.split(".");
945
1013
  function resolveLeaf(currentSchema, pathParts) {
946
1014
  if (!currentSchema || !currentSchema.attributes) return null;
947
1015
  const [head, ...tail] = pathParts;
@@ -955,6 +1023,9 @@ function getLeafAttributeSchema(contentType, allComponents, attribute) {
955
1023
  }
956
1024
  if (attrDef.type === "dynamiczone") {
957
1025
  const compUids = Array.isArray(attrDef.components) ? attrDef.components : [];
1026
+ if (parsed.targetComponentUid) {
1027
+ return resolveLeaf(allComponents[parsed.targetComponentUid], tail);
1028
+ }
958
1029
  for (const compUid of compUids) {
959
1030
  const found = resolveLeaf(allComponents[compUid], tail);
960
1031
  if (found) return found;
@@ -964,22 +1035,28 @@ function getLeafAttributeSchema(contentType, allComponents, attribute) {
964
1035
  }
965
1036
  return resolveLeaf(contentType, parts);
966
1037
  }
967
- function setDeepProperty(obj, pathParts, value) {
1038
+ function setDeepProperty(obj, pathParts, value, options) {
968
1039
  if (pathParts.length === 0) return value;
969
1040
  const [head, ...tail] = pathParts;
970
1041
  if (Array.isArray(obj)) {
971
1042
  if (/^\d+$/.test(head)) {
972
1043
  const idx = parseInt(head, 10);
973
1044
  const copy = [...obj];
974
- copy[idx] = setDeepProperty(copy[idx] || {}, tail, value);
1045
+ copy[idx] = setDeepProperty(copy[idx] || {}, tail, value, options);
975
1046
  return copy;
976
1047
  }
977
- return obj.map((item) => {
1048
+ return obj.map((item, idx) => {
978
1049
  if (!item || typeof item !== "object") return item;
1050
+ if (options?.targetIndexes && options.targetIndexes.length > 0) {
1051
+ if (!options.targetIndexes.includes(idx)) return item;
1052
+ }
1053
+ if (options?.targetComponentUid) {
1054
+ if (item.__component !== options.targetComponentUid) return item;
1055
+ }
979
1056
  if (head in item || tail.length > 0 && typeof item[head] === "object") {
980
1057
  return {
981
1058
  ...item,
982
- [head]: setDeepProperty(item[head], tail, value)
1059
+ [head]: setDeepProperty(item[head], tail, value, options)
983
1060
  };
984
1061
  }
985
1062
  return item;
@@ -991,16 +1068,17 @@ function setDeepProperty(obj, pathParts, value) {
991
1068
  } else {
992
1069
  const subTarget = current[head];
993
1070
  if (Array.isArray(subTarget)) {
994
- current[head] = setDeepProperty(subTarget, tail, value);
1071
+ current[head] = setDeepProperty(subTarget, tail, value, options);
995
1072
  } else {
996
- current[head] = setDeepProperty(subTarget || {}, tail, value);
1073
+ current[head] = setDeepProperty(subTarget || {}, tail, value, options);
997
1074
  }
998
1075
  }
999
1076
  return current;
1000
1077
  }
1001
1078
  function getDeepProperty(obj, path) {
1002
1079
  if (!obj || !path) return void 0;
1003
- const parts = path.split(".");
1080
+ const parsed = parseDynamicZonePath(path);
1081
+ const parts = parsed.cleanPath.split(".");
1004
1082
  function extract(current, remainingParts) {
1005
1083
  if (current === void 0 || current === null) return void 0;
1006
1084
  if (remainingParts.length === 0) return current;
@@ -1009,7 +1087,10 @@ function getDeepProperty(obj, path) {
1009
1087
  if (/^\d+$/.test(head)) {
1010
1088
  return extract(current[parseInt(head, 10)], tail);
1011
1089
  }
1012
- for (const item of current) {
1090
+ for (let idx = 0; idx < current.length; idx++) {
1091
+ const item = current[idx];
1092
+ if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) continue;
1093
+ if (parsed.targetComponentUid && item?.__component !== parsed.targetComponentUid) continue;
1013
1094
  const found = extract(item?.[head], tail);
1014
1095
  if (found !== void 0 && found !== null) return found;
1015
1096
  }
@@ -1174,7 +1255,12 @@ async function executeBatchInternal(strapi, {
1174
1255
  for (const item of selectedEntries) {
1175
1256
  if (!item.uid || !item.documentId) continue;
1176
1257
  const key = `${item.uid}@@@${item.documentId}`;
1177
- targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
1258
+ targetsMap.set(key, {
1259
+ uid: item.uid,
1260
+ documentId: item.documentId,
1261
+ dynamicZoneIndexes: item.dynamicZoneIndexes,
1262
+ targetComponentUid: item.targetComponentUid
1263
+ });
1178
1264
  }
1179
1265
  if (targetsMap.size === 0) {
1180
1266
  throw new Error("No collections or entries were selected for update.");
@@ -1233,10 +1319,13 @@ async function executeBatchInternal(strapi, {
1233
1319
  try {
1234
1320
  const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
1235
1321
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
1236
- const isNested = attribute.includes(".");
1322
+ const parsed = parseDynamicZonePath(attribute);
1323
+ const isNested = attribute.includes(".") || attribute.includes("[");
1237
1324
  const isComponentNested = isNested && isDeepComponentField(contentType, strapi.components, attribute);
1238
- const parts = isNested ? attribute.split(".") : [];
1239
- const rootKey = isNested ? parts[0] : "";
1325
+ const parts = isNested ? parsed.cleanPath.split(".") : [];
1326
+ const rootKey = parsed.rootKey;
1327
+ const targetComponentUid = target.targetComponentUid || parsed.targetComponentUid;
1328
+ const targetIndexes = target.dynamicZoneIndexes;
1240
1329
  const attrDef = contentType?.attributes?.[attribute];
1241
1330
  const hasAttribute = Boolean(attrDef) || isComponentNested;
1242
1331
  if (hasAttribute) {
@@ -1249,12 +1338,25 @@ async function executeBatchInternal(strapi, {
1249
1338
  const currentEntryWithPopulate = await strapi.documents(target.uid).findOne(findOneArgs);
1250
1339
  const isRootDz = contentType?.attributes?.[rootKey]?.type === "dynamiczone";
1251
1340
  const existingRoot = currentEntryWithPopulate?.[rootKey] || (isRootDz ? [] : {});
1252
- const currentVal2 = getDeepProperty(existingRoot, parts.slice(1).join("."));
1341
+ let affectedMatches = [];
1342
+ if (isRootDz && Array.isArray(existingRoot)) {
1343
+ affectedMatches = getAllNestedValues({ [rootKey]: existingRoot }, attribute);
1344
+ if (targetIndexes && targetIndexes.length > 0) {
1345
+ affectedMatches = affectedMatches.filter((m) => targetIndexes.includes(m.index));
1346
+ }
1347
+ if (targetComponentUid) {
1348
+ affectedMatches = affectedMatches.filter((m) => m.componentUid === targetComponentUid);
1349
+ }
1350
+ }
1351
+ const currentVal2 = isRootDz ? affectedMatches.length > 0 ? affectedMatches.map((m) => m.value).join(", ") : getDeepProperty(existingRoot, parts.slice(1).join(".")) : getDeepProperty(existingRoot, parts.slice(1).join("."));
1253
1352
  let newValue2 = value;
1254
1353
  if (regexObj && typeof currentVal2 === "string") {
1255
1354
  newValue2 = currentVal2.replace(regexObj, value);
1256
1355
  }
1257
- const updatedRoot = setDeepProperty(existingRoot, parts.slice(1), newValue2);
1356
+ const updatedRoot = setDeepProperty(existingRoot, parts.slice(1), newValue2, {
1357
+ targetIndexes,
1358
+ targetComponentUid
1359
+ });
1258
1360
  await strapi.documents(target.uid).update({
1259
1361
  documentId: target.documentId,
1260
1362
  data: {
@@ -1278,6 +1380,14 @@ async function executeBatchInternal(strapi, {
1278
1380
  userId: currentUser?.id,
1279
1381
  statusOverride: publishOnApproval && isDraftAndPublishComp ? "published" : void 0
1280
1382
  });
1383
+ const dzDiffs = isRootDz && affectedMatches.length > 0 ? affectedMatches.map((m) => ({
1384
+ index: m.index,
1385
+ componentUid: m.componentUid,
1386
+ componentName: m.componentName,
1387
+ attribute: `${rootKey}[#${m.index} ${m.componentName}].${parts.slice(1).join(".")}`,
1388
+ oldValue: m.value,
1389
+ newValue: regexObj && typeof m.value === "string" ? m.value.replace(regexObj, value) : value
1390
+ })) : [];
1281
1391
  return {
1282
1392
  uid: target.uid,
1283
1393
  collectionName: displayName,
@@ -1287,7 +1397,8 @@ async function executeBatchInternal(strapi, {
1287
1397
  oldValue: currentVal2 ?? null,
1288
1398
  newValue: newValue2,
1289
1399
  status: "updated",
1290
- message: `Successfully updated nested field ${attribute}`
1400
+ message: isRootDz ? `Successfully updated ${affectedMatches.length} dynamic zone item(s) in ${attribute}` : `Successfully updated nested field ${attribute}`,
1401
+ dynamicZoneDiffs: dzDiffs
1291
1402
  };
1292
1403
  }
1293
1404
  if (attrDef.type === "relation") {
@@ -1316,13 +1427,13 @@ async function executeBatchInternal(strapi, {
1316
1427
  newDisplay = "(cleared / unlinked)";
1317
1428
  } else {
1318
1429
  try {
1319
- const parsed = JSON.parse(value);
1320
- if (parsed && typeof parsed === "object" && parsed.mode) {
1321
- mode = parsed.mode;
1322
- targetDocIds = Array.isArray(parsed.documentIds) ? parsed.documentIds : parsed.documentId ? [parsed.documentId] : [];
1323
- newDisplay = parsed.label || targetDocIds.join(", ") || `[${mode}]`;
1324
- } else if (Array.isArray(parsed)) {
1325
- targetDocIds = parsed;
1430
+ const parsed2 = JSON.parse(value);
1431
+ if (parsed2 && typeof parsed2 === "object" && parsed2.mode) {
1432
+ mode = parsed2.mode;
1433
+ targetDocIds = Array.isArray(parsed2.documentIds) ? parsed2.documentIds : parsed2.documentId ? [parsed2.documentId] : [];
1434
+ newDisplay = parsed2.label || targetDocIds.join(", ") || `[${mode}]`;
1435
+ } else if (Array.isArray(parsed2)) {
1436
+ targetDocIds = parsed2;
1326
1437
  newDisplay = targetDocIds.join(", ");
1327
1438
  } else {
1328
1439
  targetDocIds = [value];
@@ -1466,6 +1577,26 @@ async function executeBatchInternal(strapi, {
1466
1577
  ...r,
1467
1578
  isLive: r.status === "updated"
1468
1579
  }));
1580
+ const flattenedDiffs = [];
1581
+ for (const r of results) {
1582
+ if (r.dynamicZoneDiffs && r.dynamicZoneDiffs.length > 0) {
1583
+ for (const dz of r.dynamicZoneDiffs) {
1584
+ flattenedDiffs.push({
1585
+ uid: r.uid,
1586
+ collectionName: r.collectionName,
1587
+ documentId: r.documentId,
1588
+ label: `${r.label} [Block #${dz.index} (${dz.componentName})]`,
1589
+ attribute: dz.attribute,
1590
+ oldValue: dz.oldValue,
1591
+ newValue: dz.newValue,
1592
+ status: "updated",
1593
+ message: `Updated ${dz.attribute}`
1594
+ });
1595
+ }
1596
+ } else {
1597
+ flattenedDiffs.push(r);
1598
+ }
1599
+ }
1469
1600
  const batchSummary = {
1470
1601
  batchId,
1471
1602
  attribute,
@@ -1477,7 +1608,7 @@ async function executeBatchInternal(strapi, {
1477
1608
  };
1478
1609
  await strapi.plugin("bulk-updater").service("audit").recordBatch(
1479
1610
  batchSummary,
1480
- results,
1611
+ flattenedDiffs,
1481
1612
  currentUser
1482
1613
  );
1483
1614
  return {
@@ -1745,6 +1876,19 @@ const bulkUpdaterService = ({ strapi }) => {
1745
1876
  colAttributesMeta
1746
1877
  );
1747
1878
  nestedComponentAttrs.push(...compAttrs);
1879
+ const scopedAttrs = discoverComponentAttributes(
1880
+ strapi,
1881
+ compUid,
1882
+ `${attrKey}[${compUid}]`,
1883
+ 1,
1884
+ 5,
1885
+ systemFields,
1886
+ allContentTypes,
1887
+ relationsByAttr,
1888
+ allRelationsMeta,
1889
+ colAttributesMeta
1890
+ );
1891
+ nestedComponentAttrs.push(...scopedAttrs);
1748
1892
  }
1749
1893
  return false;
1750
1894
  }
@@ -1959,13 +2103,20 @@ const bulkUpdaterService = ({ strapi }) => {
1959
2103
  const attrs = contentType.attributes || {};
1960
2104
  let colHasAttribute = false;
1961
2105
  let isNestedAttr = false;
2106
+ let isRootDz = false;
1962
2107
  let parts = [];
1963
2108
  if (targetFilterAttr) {
1964
- if (targetFilterAttr.includes(".")) {
2109
+ const parsed = parseDynamicZonePath(targetFilterAttr);
2110
+ if (parsed.rootKey && contentType.attributes?.[parsed.rootKey]?.type === "dynamiczone") {
2111
+ isRootDz = true;
2112
+ colHasAttribute = true;
2113
+ isNestedAttr = true;
2114
+ parts = parsed.cleanPath.split(".");
2115
+ } else if (targetFilterAttr.includes(".") || targetFilterAttr.includes("[")) {
1965
2116
  isNestedAttr = isDeepComponentField(contentType, strapi.components, targetFilterAttr);
1966
2117
  if (isNestedAttr) {
1967
2118
  colHasAttribute = true;
1968
- parts = targetFilterAttr.split(".");
2119
+ parts = parsed.cleanPath.split(".");
1969
2120
  }
1970
2121
  } else if (attrs[targetFilterAttr]) {
1971
2122
  colHasAttribute = true;
@@ -2022,8 +2173,11 @@ const bulkUpdaterService = ({ strapi }) => {
2022
2173
  }
2023
2174
  if (condition !== void 0) {
2024
2175
  if (isNestedAttr) {
2025
- const deepFilter = buildDeepFilter(parts, condition);
2026
- Object.assign(filters, deepFilter);
2176
+ if (isRootDz) ;
2177
+ else {
2178
+ const deepFilter = buildDeepFilter(parts, condition);
2179
+ Object.assign(filters, deepFilter);
2180
+ }
2027
2181
  } else {
2028
2182
  if (normOp === "empty" && isStringLike) {
2029
2183
  filters.$or = [
@@ -2097,39 +2251,100 @@ const bulkUpdaterService = ({ strapi }) => {
2097
2251
  } else {
2098
2252
  sortClause["createdAt"] = "desc";
2099
2253
  }
2100
- const findManyArgs = {
2101
- start,
2102
- limit,
2103
- filters: Object.keys(filters).length > 0 ? filters : void 0,
2104
- sort: sortClause
2105
- };
2106
- if (populateOption) {
2107
- findManyArgs.populate = populateOption;
2108
- }
2109
- const [entries, totalEntries] = await Promise.all([
2110
- strapi.documents(uid).findMany(findManyArgs),
2111
- strapi.documents(uid).count({
2112
- filters: Object.keys(filters).length > 0 ? filters : void 0
2113
- })
2114
- ]);
2115
- const totalPages = Math.ceil(totalEntries / limit);
2116
- result = {
2117
- uid,
2118
- displayName: contentType.info?.displayName || uid,
2119
- isSingleType: false,
2120
- draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2121
- entries: (entries || []).map((entry) => ({
2122
- id: entry.id,
2123
- documentId: entry.documentId,
2124
- ...entry
2125
- })),
2126
- pagination: {
2127
- page: currentPage,
2128
- pageSize: limit,
2129
- total: totalEntries,
2130
- pageCount: totalPages
2254
+ if (isRootDz && normOp && normOp !== "none") {
2255
+ const findManyArgs = {
2256
+ limit: 500,
2257
+ filters: Object.keys(filters).length > 0 ? filters : void 0,
2258
+ sort: sortClause
2259
+ };
2260
+ if (populateOption) {
2261
+ findManyArgs.populate = populateOption;
2131
2262
  }
2132
- };
2263
+ const rawEntries = await strapi.documents(uid).findMany(findManyArgs);
2264
+ const qVal = (filterQuery || "").trim().toLowerCase();
2265
+ const passesDzFilter = (entry) => {
2266
+ const matches = getAllNestedValues(entry, targetFilterAttr);
2267
+ if (normOp === "empty") {
2268
+ return matches.length === 0 || matches.every((m) => m.value === void 0 || m.value === null || String(m.value).trim() === "");
2269
+ }
2270
+ if (normOp === "notempty") {
2271
+ return matches.some((m) => m.value !== void 0 && m.value !== null && String(m.value).trim() !== "");
2272
+ }
2273
+ return matches.some((m) => {
2274
+ if (m.value === void 0 || m.value === null) return false;
2275
+ const valStr = String(m.value).toLowerCase();
2276
+ switch (normOp) {
2277
+ case "equals":
2278
+ return valStr === qVal;
2279
+ case "notequals":
2280
+ return valStr !== qVal;
2281
+ case "contains":
2282
+ return valStr.includes(qVal);
2283
+ case "notcontains":
2284
+ return !valStr.includes(qVal);
2285
+ case "startswith":
2286
+ return valStr.startsWith(qVal);
2287
+ case "endswith":
2288
+ return valStr.endsWith(qVal);
2289
+ default:
2290
+ return true;
2291
+ }
2292
+ });
2293
+ };
2294
+ const matchedEntries = (rawEntries || []).filter(passesDzFilter);
2295
+ const totalEntries = matchedEntries.length;
2296
+ const paginatedEntries = matchedEntries.slice(start, start + limit);
2297
+ result = {
2298
+ uid,
2299
+ displayName: contentType.info?.displayName || uid,
2300
+ isSingleType: false,
2301
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2302
+ entries: paginatedEntries.map((entry) => ({
2303
+ id: entry.id,
2304
+ documentId: entry.documentId,
2305
+ ...entry
2306
+ })),
2307
+ pagination: {
2308
+ page: currentPage,
2309
+ pageSize: limit,
2310
+ total: totalEntries,
2311
+ pageCount: Math.ceil(totalEntries / limit) || 1
2312
+ }
2313
+ };
2314
+ } else {
2315
+ const findManyArgs = {
2316
+ start,
2317
+ limit,
2318
+ filters: Object.keys(filters).length > 0 ? filters : void 0,
2319
+ sort: sortClause
2320
+ };
2321
+ if (populateOption) {
2322
+ findManyArgs.populate = populateOption;
2323
+ }
2324
+ const [entries, totalEntries] = await Promise.all([
2325
+ strapi.documents(uid).findMany(findManyArgs),
2326
+ strapi.documents(uid).count({
2327
+ filters: Object.keys(filters).length > 0 ? filters : void 0
2328
+ })
2329
+ ]);
2330
+ result = {
2331
+ uid,
2332
+ displayName: contentType.info?.displayName || uid,
2333
+ isSingleType: false,
2334
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2335
+ entries: (entries || []).map((entry) => ({
2336
+ id: entry.id,
2337
+ documentId: entry.documentId,
2338
+ ...entry
2339
+ })),
2340
+ pagination: {
2341
+ page: currentPage,
2342
+ pageSize: limit,
2343
+ total: totalEntries,
2344
+ pageCount: Math.ceil(totalEntries / limit) || 1
2345
+ }
2346
+ };
2347
+ }
2133
2348
  }
2134
2349
  if (queryCache.size >= MAX_QUERY_CACHE_SIZE) {
2135
2350
  const toDelete = Array.from(queryCache.keys()).slice(0, 100);
@@ -4160,6 +4375,99 @@ function parseCsv(csvText) {
4160
4375
  }
4161
4376
  return records;
4162
4377
  }
4378
+ const LIFECYCLE_STATUS_WORDS = /* @__PURE__ */ new Set([
4379
+ "pending review",
4380
+ "pending_review",
4381
+ "pending approval",
4382
+ "pending_approval",
4383
+ "approved",
4384
+ "rejected",
4385
+ "draft",
4386
+ "published",
4387
+ "modified",
4388
+ "supported",
4389
+ "ignored"
4390
+ ]);
4391
+ function isValidDocId(val) {
4392
+ if (!val || typeof val !== "string") return false;
4393
+ const trimmed = val.trim();
4394
+ if (!trimmed) return false;
4395
+ if (LIFECYCLE_STATUS_WORDS.has(trimmed.toLowerCase())) return false;
4396
+ if (/\s/.test(trimmed)) return false;
4397
+ return /^[a-zA-Z0-9_-]+$/.test(trimmed);
4398
+ }
4399
+ function extractDocId(row) {
4400
+ if (!row || typeof row !== "object") return "";
4401
+ const candidates = [
4402
+ row.documentId,
4403
+ row.document_id,
4404
+ row["Document ID"],
4405
+ row["Document Id"],
4406
+ row["document_id"],
4407
+ row.docId,
4408
+ row["docId"],
4409
+ row.id,
4410
+ row.ID,
4411
+ row.Id
4412
+ ];
4413
+ for (const c of candidates) {
4414
+ if (c !== void 0 && c !== null) {
4415
+ const str = String(c).trim();
4416
+ if (isValidDocId(str)) {
4417
+ return str;
4418
+ }
4419
+ }
4420
+ }
4421
+ return "";
4422
+ }
4423
+ function extractLocale(row) {
4424
+ if (!row || typeof row !== "object") return void 0;
4425
+ const candidates = [row.locale, row.Locale, row["locale"], row["Locale"]];
4426
+ for (const c of candidates) {
4427
+ if (c !== void 0 && c !== null) {
4428
+ const str = String(c).trim();
4429
+ if (str && str.toLowerCase() !== "null" && str.toLowerCase() !== "undefined") {
4430
+ return str;
4431
+ }
4432
+ }
4433
+ }
4434
+ return void 0;
4435
+ }
4436
+ function extractDetailedErrorMessage(err) {
4437
+ if (!err) return "Error processing row";
4438
+ const detailsErrors = err.details?.errors || err.error?.details?.errors;
4439
+ if (Array.isArray(detailsErrors) && detailsErrors.length > 0) {
4440
+ const messages = detailsErrors.map((e) => {
4441
+ if (!e) return "";
4442
+ const path = Array.isArray(e.path) ? e.path.join(".") : e.path;
4443
+ const msg = e.message || (typeof e === "string" ? e : JSON.stringify(e));
4444
+ return path ? `${path}: ${msg}` : msg;
4445
+ }).filter(Boolean);
4446
+ if (messages.length > 0) return messages.join("; ");
4447
+ }
4448
+ if (Array.isArray(err.inner) && err.inner.length > 0) {
4449
+ const messages = err.inner.map((e) => {
4450
+ if (!e) return "";
4451
+ const path = e.path;
4452
+ const msg = e.message || (typeof e === "string" ? e : JSON.stringify(e));
4453
+ return path ? `${path}: ${msg}` : msg;
4454
+ }).filter(Boolean);
4455
+ if (messages.length > 0) return messages.join("; ");
4456
+ }
4457
+ if (Array.isArray(err.errors) && err.errors.length > 0) {
4458
+ return err.errors.join("; ");
4459
+ }
4460
+ if (typeof err.details === "string" && err.details.trim()) {
4461
+ return err.details;
4462
+ }
4463
+ if (err.details?.message) {
4464
+ return err.details.message;
4465
+ }
4466
+ if (err.message && err.message !== "[object Object]") {
4467
+ return err.message;
4468
+ }
4469
+ return "Error processing row";
4470
+ }
4163
4471
  function discoverExportFields(ct, allComponents, maxDepth = 4) {
4164
4472
  const fields = [{ key: "documentId", label: "documentId", type: "string" }];
4165
4473
  const excluded = ["createdBy", "updatedBy", "localizations"];
@@ -4555,13 +4863,63 @@ const csvService = ({ strapi }) => ({
4555
4863
  const batchId = `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
4556
4864
  const userEmail = user?.email || "admin@strapi.io";
4557
4865
  const userName = user ? `${user.firstname || ""} ${user.lastname || ""}`.trim() : "CSV Importer";
4866
+ const findExistingDocument = async (targetDocId, docLocale) => {
4867
+ if (!targetDocId) return null;
4868
+ try {
4869
+ const queryArgs = { documentId: targetDocId };
4870
+ if (docLocale) queryArgs.locale = docLocale;
4871
+ const found = await strapi.documents(uid).findOne(queryArgs);
4872
+ if (found) return found;
4873
+ } catch {
4874
+ }
4875
+ if (/^\d+$/.test(targetDocId)) {
4876
+ try {
4877
+ const queryArgs = {
4878
+ filters: { id: parseInt(targetDocId, 10) },
4879
+ limit: 1
4880
+ };
4881
+ if (docLocale) queryArgs.locale = docLocale;
4882
+ const list = await strapi.documents(uid).findMany(queryArgs);
4883
+ if (list && list.length > 0) return list[0];
4884
+ } catch {
4885
+ }
4886
+ }
4887
+ return null;
4888
+ };
4889
+ const SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
4890
+ "documentid",
4891
+ "document_id",
4892
+ "docid",
4893
+ "id",
4894
+ "createdat",
4895
+ "updatedat",
4896
+ "publishedat",
4897
+ "createdby",
4898
+ "updatedby",
4899
+ "localizations",
4900
+ "locale",
4901
+ "status"
4902
+ ]);
4558
4903
  for (let i = 0; i < parsedRows.length; i++) {
4559
4904
  const rowNumber = i + 2;
4560
4905
  const row = parsedRows[i];
4561
- const docId = (row.documentId || row.id || "").trim();
4906
+ let docId = extractDocId(row);
4907
+ const rowLocale = extractLocale(row);
4908
+ if (ct.kind === "singleType" && !docId) {
4909
+ try {
4910
+ const findSingleArgs = {};
4911
+ if (rowLocale) findSingleArgs.locale = rowLocale;
4912
+ const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
4913
+ if (singleDoc?.documentId) {
4914
+ docId = singleDoc.documentId;
4915
+ }
4916
+ } catch {
4917
+ }
4918
+ }
4562
4919
  const payload = {};
4563
4920
  for (const [key, rawVal] of Object.entries(row)) {
4564
- if (key === "documentId" || key === "id") continue;
4921
+ const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
4922
+ if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
4565
4923
  if (key.includes(".")) {
4566
4924
  const [parent, child] = key.split(".");
4567
4925
  const parentDef = attrs[parent];
@@ -4574,32 +4932,54 @@ const csvService = ({ strapi }) => ({
4574
4932
  if (rawVal === "" || rawVal === null || rawVal === void 0) {
4575
4933
  val = null;
4576
4934
  } else if (childDef.type === "integer" || childDef.type === "biginteger") {
4577
- val = parseInt(rawVal, 10);
4935
+ const parsedInt = parseInt(rawVal, 10);
4936
+ val = isNaN(parsedInt) ? null : parsedInt;
4578
4937
  } else if (childDef.type === "float" || childDef.type === "decimal") {
4579
- val = parseFloat(rawVal);
4938
+ const parsedFloat = parseFloat(rawVal);
4939
+ val = isNaN(parsedFloat) ? null : parsedFloat;
4580
4940
  } else if (childDef.type === "boolean") {
4581
4941
  val = rawVal.toLowerCase() === "true" || rawVal === "1";
4942
+ } else if (childDef.type === "json") {
4943
+ try {
4944
+ val = JSON.parse(rawVal);
4945
+ } catch {
4946
+ val = rawVal;
4947
+ }
4582
4948
  }
4583
4949
  payload[parent][child] = val;
4584
4950
  }
4585
4951
  }
4586
4952
  continue;
4587
4953
  }
4588
- const attrDef = attrs[key];
4954
+ let attrDef = attrs[key];
4955
+ if (!attrDef) {
4956
+ const matchKey = Object.keys(attrs).find(
4957
+ (k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
4958
+ );
4959
+ if (matchKey) {
4960
+ attrDef = attrs[matchKey];
4961
+ }
4962
+ }
4589
4963
  if (!attrDef) continue;
4590
4964
  if (rawVal === "" || rawVal === null || rawVal === void 0) {
4591
- payload[key] = null;
4965
+ if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
4966
+ payload[key] = null;
4967
+ }
4592
4968
  continue;
4593
4969
  }
4594
4970
  switch (attrDef.type) {
4595
4971
  case "integer":
4596
- case "biginteger":
4597
- payload[key] = parseInt(rawVal, 10);
4972
+ case "biginteger": {
4973
+ const parsedInt = parseInt(rawVal, 10);
4974
+ payload[key] = isNaN(parsedInt) ? null : parsedInt;
4598
4975
  break;
4976
+ }
4599
4977
  case "float":
4600
- case "decimal":
4601
- payload[key] = parseFloat(rawVal);
4978
+ case "decimal": {
4979
+ const parsedFloat = parseFloat(rawVal);
4980
+ payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
4602
4981
  break;
4982
+ }
4603
4983
  case "boolean":
4604
4984
  payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
4605
4985
  break;
@@ -4610,6 +4990,69 @@ const csvService = ({ strapi }) => ({
4610
4990
  payload[key] = rawVal;
4611
4991
  }
4612
4992
  break;
4993
+ case "dynamiczone": {
4994
+ try {
4995
+ const parsed = JSON.parse(rawVal);
4996
+ if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
4997
+ payload[key] = parsed;
4998
+ }
4999
+ } catch {
5000
+ }
5001
+ break;
5002
+ }
5003
+ case "component": {
5004
+ if (attrDef.repeatable) {
5005
+ try {
5006
+ const parsed = JSON.parse(rawVal);
5007
+ if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
5008
+ payload[key] = parsed;
5009
+ }
5010
+ } catch {
5011
+ }
5012
+ } else {
5013
+ try {
5014
+ const parsed = JSON.parse(rawVal);
5015
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
5016
+ payload[key] = parsed;
5017
+ }
5018
+ } catch {
5019
+ }
5020
+ }
5021
+ break;
5022
+ }
5023
+ case "relation": {
5024
+ try {
5025
+ const parsed = JSON.parse(rawVal);
5026
+ if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
5027
+ payload[key] = parsed;
5028
+ break;
5029
+ }
5030
+ } catch {
5031
+ }
5032
+ if (isValidDocId(rawVal)) {
5033
+ payload[key] = rawVal;
5034
+ } else if (rawVal.includes(";") || rawVal.includes(",")) {
5035
+ const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
5036
+ if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
5037
+ payload[key] = parts;
5038
+ }
5039
+ }
5040
+ break;
5041
+ }
5042
+ case "media": {
5043
+ if (/^\d+$/.test(rawVal.trim())) {
5044
+ payload[key] = parseInt(rawVal.trim(), 10);
5045
+ } else {
5046
+ try {
5047
+ const parsed = JSON.parse(rawVal);
5048
+ if (typeof parsed === "number" || typeof parsed === "object") {
5049
+ payload[key] = parsed;
5050
+ }
5051
+ } catch {
5052
+ }
5053
+ }
5054
+ break;
5055
+ }
4613
5056
  default:
4614
5057
  payload[key] = rawVal;
4615
5058
  }
@@ -4620,114 +5063,146 @@ const csvService = ({ strapi }) => ({
4620
5063
  stats.skipped++;
4621
5064
  stats.errors.push({
4622
5065
  row: rowNumber,
4623
- error: "Missing documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
5066
+ error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
4624
5067
  });
4625
5068
  continue;
4626
5069
  }
4627
- const existing = await strapi.documents(uid).findOne({
4628
- documentId: docId
4629
- });
5070
+ const existing = await findExistingDocument(docId, rowLocale);
4630
5071
  if (!existing) {
4631
5072
  stats.skipped++;
4632
5073
  stats.errors.push({
4633
5074
  row: rowNumber,
4634
5075
  documentId: docId,
4635
- error: `Document with documentId "${docId}" not found.`
5076
+ error: `Document with ID "${docId}" not found in database.`
4636
5077
  });
4637
5078
  continue;
4638
5079
  }
5080
+ const resolvedDocId = existing.documentId || docId;
4639
5081
  const mergedPayload = { ...payload };
4640
5082
  for (const key of Object.keys(payload)) {
4641
5083
  if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4642
5084
  mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4643
5085
  }
4644
5086
  }
4645
- await strapi.documents(uid).update({
4646
- documentId: docId,
5087
+ if (Object.keys(mergedPayload).length === 0) {
5088
+ stats.skipped++;
5089
+ stats.diffs.push({
5090
+ uid,
5091
+ collectionName: ct.info?.displayName || uid,
5092
+ documentId: resolvedDocId,
5093
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5094
+ attribute: "",
5095
+ oldValue: existing,
5096
+ newValue: {},
5097
+ status: "skipped",
5098
+ message: `Skipped (no editable attributes in row ${rowNumber})`
5099
+ });
5100
+ continue;
5101
+ }
5102
+ const updateArgs = {
5103
+ documentId: resolvedDocId,
4647
5104
  data: mergedPayload
4648
- });
5105
+ };
5106
+ if (rowLocale) updateArgs.locale = rowLocale;
5107
+ await strapi.documents(uid).update(updateArgs);
4649
5108
  if (publish && ct.options?.draftAndPublish) {
4650
5109
  try {
4651
- await strapi.documents(uid).publish({ documentId: docId });
5110
+ const pubArgs = { documentId: resolvedDocId };
5111
+ if (rowLocale) pubArgs.locale = rowLocale;
5112
+ await strapi.documents(uid).publish(pubArgs);
4652
5113
  } catch (pubErr) {
4653
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
5114
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
4654
5115
  }
4655
5116
  }
4656
5117
  try {
4657
5118
  await recordContentHistoryVersion(strapi, {
4658
5119
  uid,
4659
- documentId: docId,
5120
+ documentId: resolvedDocId,
4660
5121
  userId: user?.id,
4661
5122
  statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4662
5123
  });
4663
5124
  } catch {
4664
5125
  }
4665
5126
  stats.updated++;
4666
- stats.diffs.push({
4667
- uid,
4668
- collectionName: ct.info?.displayName || uid,
4669
- documentId: docId,
4670
- label: existing.title || existing.name || `Entry #${docId}`,
4671
- attribute: Object.keys(payload).join(", "),
4672
- oldValue: existing,
4673
- newValue: payload,
4674
- status: "updated",
4675
- message: `Updated via CSV import (row ${rowNumber})`
4676
- });
5127
+ for (const attrKey of Object.keys(payload)) {
5128
+ const oldVal = existing?.[attrKey] ?? null;
5129
+ const newVal = payload[attrKey];
5130
+ stats.diffs.push({
5131
+ uid,
5132
+ collectionName: ct.info?.displayName || uid,
5133
+ documentId: resolvedDocId,
5134
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5135
+ attribute: attrKey,
5136
+ oldValue: oldVal,
5137
+ newValue: newVal,
5138
+ status: "updated",
5139
+ message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
5140
+ });
5141
+ }
4677
5142
  } else if (mode === "create_or_override") {
5143
+ let existing = null;
4678
5144
  if (docId) {
4679
- const existing = await strapi.documents(uid).findOne({
4680
- documentId: docId
4681
- });
4682
- if (existing) {
4683
- const mergedPayload = { ...payload };
4684
- for (const key of Object.keys(payload)) {
4685
- if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4686
- mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4687
- }
4688
- }
4689
- await strapi.documents(uid).update({
4690
- documentId: docId,
4691
- data: mergedPayload
4692
- });
4693
- if (publish && ct.options?.draftAndPublish) {
4694
- try {
4695
- await strapi.documents(uid).publish({ documentId: docId });
4696
- } catch (pubErr) {
4697
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
4698
- }
5145
+ existing = await findExistingDocument(docId, rowLocale);
5146
+ }
5147
+ if (existing) {
5148
+ const resolvedDocId = existing.documentId || docId;
5149
+ const mergedPayload = { ...payload };
5150
+ for (const key of Object.keys(payload)) {
5151
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5152
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4699
5153
  }
5154
+ }
5155
+ const updateArgs = {
5156
+ documentId: resolvedDocId,
5157
+ data: mergedPayload
5158
+ };
5159
+ if (rowLocale) updateArgs.locale = rowLocale;
5160
+ await strapi.documents(uid).update(updateArgs);
5161
+ if (publish && ct.options?.draftAndPublish) {
4700
5162
  try {
4701
- await recordContentHistoryVersion(strapi, {
4702
- uid,
4703
- documentId: docId,
4704
- userId: user?.id,
4705
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4706
- });
4707
- } catch {
5163
+ const pubArgs = { documentId: resolvedDocId };
5164
+ if (rowLocale) pubArgs.locale = rowLocale;
5165
+ await strapi.documents(uid).publish(pubArgs);
5166
+ } catch (pubErr) {
5167
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
4708
5168
  }
4709
- stats.updated++;
5169
+ }
5170
+ try {
5171
+ await recordContentHistoryVersion(strapi, {
5172
+ uid,
5173
+ documentId: resolvedDocId,
5174
+ userId: user?.id,
5175
+ statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5176
+ });
5177
+ } catch {
5178
+ }
5179
+ stats.updated++;
5180
+ for (const attrKey of Object.keys(payload)) {
5181
+ const oldVal = existing?.[attrKey] ?? null;
5182
+ const newVal = payload[attrKey];
4710
5183
  stats.diffs.push({
4711
5184
  uid,
4712
5185
  collectionName: ct.info?.displayName || uid,
4713
- documentId: docId,
4714
- label: existing.title || existing.name || `Entry #${docId}`,
4715
- attribute: Object.keys(payload).join(", "),
4716
- oldValue: existing,
4717
- newValue: payload,
5186
+ documentId: resolvedDocId,
5187
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5188
+ attribute: attrKey,
5189
+ oldValue: oldVal,
5190
+ newValue: newVal,
4718
5191
  status: "updated",
4719
- message: `Overridden via CSV import (row ${rowNumber})`
5192
+ message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
4720
5193
  });
4721
- continue;
4722
5194
  }
5195
+ continue;
4723
5196
  }
4724
- const created = await strapi.documents(uid).create({
4725
- data: payload
4726
- });
5197
+ const createArgs = { data: payload };
5198
+ if (rowLocale) createArgs.locale = rowLocale;
5199
+ const created = await strapi.documents(uid).create(createArgs);
4727
5200
  const newDocId = created?.documentId || docId;
4728
5201
  if (publish && ct.options?.draftAndPublish && newDocId) {
4729
5202
  try {
4730
- await strapi.documents(uid).publish({ documentId: newDocId });
5203
+ const pubArgs = { documentId: newDocId };
5204
+ if (rowLocale) pubArgs.locale = rowLocale;
5205
+ await strapi.documents(uid).publish(pubArgs);
4731
5206
  } catch (pubErr) {
4732
5207
  strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
4733
5208
  }
@@ -4745,71 +5220,83 @@ const csvService = ({ strapi }) => ({
4745
5220
  if (!docId) {
4746
5221
  stats.newWithoutDocId++;
4747
5222
  }
4748
- stats.diffs.push({
4749
- uid,
4750
- collectionName: ct.info?.displayName || uid,
4751
- documentId: newDocId,
4752
- label: created?.title || created?.name || `New Entry #${newDocId}`,
4753
- attribute: Object.keys(payload).join(", "),
4754
- oldValue: null,
4755
- newValue: payload,
4756
- status: "updated",
4757
- message: docId ? `Created via CSV import (row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
4758
- });
5223
+ for (const attrKey of Object.keys(payload)) {
5224
+ stats.diffs.push({
5225
+ uid,
5226
+ collectionName: ct.info?.displayName || uid,
5227
+ documentId: newDocId,
5228
+ label: created?.title || created?.name || `New Entry #${newDocId}`,
5229
+ attribute: attrKey,
5230
+ oldValue: null,
5231
+ newValue: payload[attrKey],
5232
+ status: "created",
5233
+ message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
5234
+ });
5235
+ }
4759
5236
  } else {
5237
+ let existing = null;
4760
5238
  if (docId) {
4761
- const existing = await strapi.documents(uid).findOne({
4762
- documentId: docId
4763
- });
4764
- if (existing) {
4765
- const mergedPayload = { ...payload };
4766
- for (const key of Object.keys(payload)) {
4767
- if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4768
- mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4769
- }
4770
- }
4771
- await strapi.documents(uid).update({
4772
- documentId: docId,
4773
- data: mergedPayload
4774
- });
4775
- if (publish && ct.options?.draftAndPublish) {
4776
- try {
4777
- await strapi.documents(uid).publish({ documentId: docId });
4778
- } catch (pubErr) {
4779
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
4780
- }
5239
+ existing = await findExistingDocument(docId, rowLocale);
5240
+ }
5241
+ if (existing) {
5242
+ const resolvedDocId = existing.documentId || docId;
5243
+ const mergedPayload = { ...payload };
5244
+ for (const key of Object.keys(payload)) {
5245
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5246
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4781
5247
  }
5248
+ }
5249
+ const updateArgs = {
5250
+ documentId: resolvedDocId,
5251
+ data: mergedPayload
5252
+ };
5253
+ if (rowLocale) updateArgs.locale = rowLocale;
5254
+ await strapi.documents(uid).update(updateArgs);
5255
+ if (publish && ct.options?.draftAndPublish) {
4782
5256
  try {
4783
- await recordContentHistoryVersion(strapi, {
4784
- uid,
4785
- documentId: docId,
4786
- userId: user?.id,
4787
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4788
- });
4789
- } catch {
5257
+ const pubArgs = { documentId: resolvedDocId };
5258
+ if (rowLocale) pubArgs.locale = rowLocale;
5259
+ await strapi.documents(uid).publish(pubArgs);
5260
+ } catch (pubErr) {
5261
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
4790
5262
  }
4791
- stats.updated++;
5263
+ }
5264
+ try {
5265
+ await recordContentHistoryVersion(strapi, {
5266
+ uid,
5267
+ documentId: resolvedDocId,
5268
+ userId: user?.id,
5269
+ statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5270
+ });
5271
+ } catch {
5272
+ }
5273
+ stats.updated++;
5274
+ for (const attrKey of Object.keys(payload)) {
5275
+ const oldVal = existing?.[attrKey] ?? null;
5276
+ const newVal = payload[attrKey];
4792
5277
  stats.diffs.push({
4793
5278
  uid,
4794
5279
  collectionName: ct.info?.displayName || uid,
4795
- documentId: docId,
4796
- label: existing.title || existing.name || `Entry #${docId}`,
4797
- attribute: Object.keys(payload).join(", "),
4798
- oldValue: existing,
4799
- newValue: payload,
5280
+ documentId: resolvedDocId,
5281
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5282
+ attribute: attrKey,
5283
+ oldValue: oldVal,
5284
+ newValue: newVal,
4800
5285
  status: "updated",
4801
- message: `Updated existing entry via smart match (row ${rowNumber})`
5286
+ message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
4802
5287
  });
4803
- continue;
4804
5288
  }
5289
+ continue;
4805
5290
  }
4806
- const created = await strapi.documents(uid).create({
4807
- data: payload
4808
- });
5291
+ const createArgs = { data: payload };
5292
+ if (rowLocale) createArgs.locale = rowLocale;
5293
+ const created = await strapi.documents(uid).create(createArgs);
4809
5294
  const newDocId = created?.documentId || docId;
4810
5295
  if (publish && ct.options?.draftAndPublish && newDocId) {
4811
5296
  try {
4812
- await strapi.documents(uid).publish({ documentId: newDocId });
5297
+ const pubArgs = { documentId: newDocId };
5298
+ if (rowLocale) pubArgs.locale = rowLocale;
5299
+ await strapi.documents(uid).publish(pubArgs);
4813
5300
  } catch (pubErr) {
4814
5301
  strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
4815
5302
  }
@@ -4827,23 +5314,25 @@ const csvService = ({ strapi }) => ({
4827
5314
  if (!docId) {
4828
5315
  stats.newWithoutDocId++;
4829
5316
  }
4830
- stats.diffs.push({
4831
- uid,
4832
- collectionName: ct.info?.displayName || uid,
4833
- documentId: newDocId,
4834
- label: created?.title || created?.name || `New Entry #${newDocId}`,
4835
- attribute: Object.keys(payload).join(", "),
4836
- oldValue: null,
4837
- newValue: payload,
4838
- status: "updated",
4839
- message: docId ? `Created as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
4840
- });
5317
+ for (const attrKey of Object.keys(payload)) {
5318
+ stats.diffs.push({
5319
+ uid,
5320
+ collectionName: ct.info?.displayName || uid,
5321
+ documentId: newDocId,
5322
+ label: created?.title || created?.name || `New Entry #${newDocId}`,
5323
+ attribute: attrKey,
5324
+ oldValue: null,
5325
+ newValue: payload[attrKey],
5326
+ status: "created",
5327
+ 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})`
5328
+ });
5329
+ }
4841
5330
  }
4842
5331
  } catch (err) {
4843
5332
  stats.errors.push({
4844
5333
  row: rowNumber,
4845
5334
  documentId: docId || void 0,
4846
- error: err.message || "Error processing row"
5335
+ error: extractDetailedErrorMessage(err)
4847
5336
  });
4848
5337
  }
4849
5338
  }
@@ -4860,10 +5349,32 @@ const csvService = ({ strapi }) => ({
4860
5349
  if (auditService2) {
4861
5350
  const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
4862
5351
  if (isEnabled) {
5352
+ const collectionDisplayName = ct.info?.displayName || uid;
5353
+ const updatedDiffs = stats.diffs.filter(
5354
+ (d) => d.status === "updated" || d.status === "created"
5355
+ );
5356
+ const uniqueAttributes = Array.from(
5357
+ new Set(updatedDiffs.map((d) => d.attribute))
5358
+ ).filter(Boolean);
5359
+ const attrListStr = uniqueAttributes.length > 0 ? uniqueAttributes.join(", ") : "fields";
5360
+ const batchAttribute = `CSV [${collectionDisplayName}]: ${attrListStr}`;
5361
+ let batchValue = "";
5362
+ if (updatedDiffs.length === 1) {
5363
+ const d = updatedDiffs[0];
5364
+ const newStr = d.newValue !== null && d.newValue !== void 0 ? String(d.newValue) : "(empty)";
5365
+ batchValue = `"${d.label}": ${d.attribute} → "${newStr}"`;
5366
+ } else if (updatedDiffs.length > 1 && stats.updated === 1 && stats.created === 0) {
5367
+ const d = updatedDiffs[0];
5368
+ const changes = updatedDiffs.map((ud) => `${ud.attribute}: "${ud.newValue ?? ""}"`).join(", ");
5369
+ batchValue = `"${d.label}": ${changes}`;
5370
+ } else {
5371
+ const sampleVal = updatedDiffs[0] ? ` (e.g. ${updatedDiffs[0].attribute}: "${updatedDiffs[0].newValue ?? ""}")` : "";
5372
+ batchValue = `${stats.updated} updated, ${stats.created} created in ${collectionDisplayName}${sampleVal}`;
5373
+ }
4863
5374
  const batchSummary = {
4864
5375
  batchId,
4865
- attribute: "CSV_IMPORT",
4866
- value: `Mode: ${mode} (${stats.created} created, ${stats.updated} updated)`,
5376
+ attribute: batchAttribute,
5377
+ value: batchValue,
4867
5378
  updatedCount: stats.updated + stats.created,
4868
5379
  skippedCount: stats.skipped,
4869
5380
  totalTargets: stats.total