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