strapi-plugin-bulk-updater 1.4.3 → 1.4.9

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.
@@ -214,6 +214,30 @@ const routes = {
214
214
  config: {
215
215
  policies: []
216
216
  }
217
+ },
218
+ {
219
+ method: "POST",
220
+ path: "/csv/import-chunk",
221
+ handler: "csv.importChunk",
222
+ config: {
223
+ policies: []
224
+ }
225
+ },
226
+ {
227
+ method: "POST",
228
+ path: "/csv/validate-preflight",
229
+ handler: "csv.validatePreflight",
230
+ config: {
231
+ policies: []
232
+ }
233
+ },
234
+ {
235
+ method: "POST",
236
+ path: "/csv/submit-batch",
237
+ handler: "csv.submitBatch",
238
+ config: {
239
+ policies: []
240
+ }
217
241
  }
218
242
  ]
219
243
  }
@@ -644,7 +668,7 @@ const csvController = ({ strapi }) => ({
644
668
  },
645
669
  async importCsv(ctx) {
646
670
  try {
647
- const { uid, csvText, mode, publish } = ctx.request.body;
671
+ const { uid, csvText, mode, publish, publishStrategy, unreviewedConsent } = ctx.request.body;
648
672
  if (!uid) {
649
673
  ctx.status = 400;
650
674
  ctx.body = { success: false, error: "Target collection UID is required." };
@@ -659,6 +683,93 @@ const csvController = ({ strapi }) => ({
659
683
  const result = await csvService2.importCsv(uid, csvText, {
660
684
  mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
661
685
  publish: publish !== false,
686
+ publishStrategy: publishStrategy === "all" ? "all" : "safe",
687
+ unreviewedConsent: Boolean(unreviewedConsent),
688
+ user: ctx.state.user
689
+ });
690
+ ctx.body = result;
691
+ } catch (err) {
692
+ ctx.status = 500;
693
+ ctx.body = { success: false, error: err.message };
694
+ }
695
+ },
696
+ async importChunk(ctx) {
697
+ try {
698
+ const { uid, rows, batchId, isFirstChunk, isLastChunk, startRowNumber, mode, publish, publishStrategy, unreviewedConsent } = ctx.request.body;
699
+ if (!uid) {
700
+ ctx.status = 400;
701
+ ctx.body = { success: false, error: "Target collection UID is required." };
702
+ return;
703
+ }
704
+ if (!Array.isArray(rows)) {
705
+ ctx.status = 400;
706
+ ctx.body = { success: false, error: "Rows array is required." };
707
+ return;
708
+ }
709
+ const csvService2 = strapi.plugin("bulk-updater").service("csv");
710
+ const result = await csvService2.importChunk(uid, rows, {
711
+ batchId: batchId ? String(batchId) : void 0,
712
+ isFirstChunk: Boolean(isFirstChunk),
713
+ isLastChunk: Boolean(isLastChunk),
714
+ startRowNumber: typeof startRowNumber === "number" ? startRowNumber : void 0,
715
+ mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
716
+ publish: publish !== false,
717
+ publishStrategy: publishStrategy === "all" ? "all" : "safe",
718
+ unreviewedConsent: Boolean(unreviewedConsent),
719
+ user: ctx.state.user
720
+ });
721
+ ctx.body = result;
722
+ } catch (err) {
723
+ ctx.status = 500;
724
+ ctx.body = { success: false, error: err.message };
725
+ }
726
+ },
727
+ async validatePreflight(ctx) {
728
+ try {
729
+ const { uid, rows, mode } = ctx.request.body;
730
+ if (!uid) {
731
+ ctx.status = 400;
732
+ ctx.body = { success: false, error: "Target collection UID is required." };
733
+ return;
734
+ }
735
+ if (!Array.isArray(rows)) {
736
+ ctx.status = 400;
737
+ ctx.body = { success: false, error: "Rows array is required." };
738
+ return;
739
+ }
740
+ const csvService2 = strapi.plugin("bulk-updater").service("csv");
741
+ const result = await csvService2.validatePreflight(uid, rows, {
742
+ mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto"
743
+ });
744
+ ctx.body = result;
745
+ } catch (err) {
746
+ ctx.status = 500;
747
+ ctx.body = { success: false, error: err.message };
748
+ }
749
+ },
750
+ async submitBatch(ctx) {
751
+ try {
752
+ const { uid, rows, executionMode, makerNotes, forceImmediate, mode, publish, publishStrategy, unreviewedConsent, unreviewedCount } = ctx.request.body;
753
+ if (!uid) {
754
+ ctx.status = 400;
755
+ ctx.body = { success: false, error: "Target collection UID is required." };
756
+ return;
757
+ }
758
+ if (!Array.isArray(rows)) {
759
+ ctx.status = 400;
760
+ ctx.body = { success: false, error: "Rows array is required." };
761
+ return;
762
+ }
763
+ const csvService2 = strapi.plugin("bulk-updater").service("csv");
764
+ const result = await csvService2.submitBatch(uid, rows, {
765
+ executionMode,
766
+ makerNotes,
767
+ forceImmediate: Boolean(forceImmediate),
768
+ mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
769
+ publish: publish !== false,
770
+ publishStrategy: publishStrategy === "all" ? "all" : "safe",
771
+ unreviewedConsent: Boolean(unreviewedConsent),
772
+ unreviewedCount: typeof unreviewedCount === "number" ? unreviewedCount : 0,
662
773
  user: ctx.state.user
663
774
  });
664
775
  ctx.body = result;
@@ -816,6 +927,60 @@ const modelsCache = /* @__PURE__ */ new Map();
816
927
  const MAX_QUERY_CACHE_SIZE = 500;
817
928
  const CACHE_TTL_MS = 60 * 1e3;
818
929
  let staticSchemaCache = null;
930
+ function formatEntryRecord(entry) {
931
+ if (!entry || typeof entry !== "object") return entry;
932
+ const formatted = {
933
+ id: entry.id,
934
+ documentId: entry.documentId,
935
+ ...entry
936
+ };
937
+ if (entry.updatedAt !== void 0 && formatted.updated_at === void 0) {
938
+ formatted.updated_at = entry.updatedAt;
939
+ }
940
+ if (entry.updated_at !== void 0 && formatted.updatedAt === void 0) {
941
+ formatted.updatedAt = entry.updated_at;
942
+ }
943
+ if (entry.createdAt !== void 0 && formatted.created_at === void 0) {
944
+ formatted.created_at = entry.createdAt;
945
+ }
946
+ if (entry.created_at !== void 0 && formatted.createdAt === void 0) {
947
+ formatted.createdAt = entry.created_at;
948
+ }
949
+ if (entry.publishedAt !== void 0 && formatted.published_at === void 0) {
950
+ formatted.published_at = entry.publishedAt;
951
+ }
952
+ if (entry.published_at !== void 0 && formatted.publishedAt === void 0) {
953
+ formatted.publishedAt = entry.published_at;
954
+ }
955
+ return formatted;
956
+ }
957
+ function resolveContentTypeField(contentType, field) {
958
+ if (!field) return null;
959
+ const attrs = contentType?.attributes || {};
960
+ if (attrs[field]) return field;
961
+ if (field === "updated_at" || field === "updatedAt") {
962
+ return attrs["updated_at"] ? "updated_at" : "updatedAt";
963
+ }
964
+ if (field === "created_at" || field === "createdAt") {
965
+ return attrs["created_at"] ? "created_at" : "createdAt";
966
+ }
967
+ if (field === "published_at" || field === "publishedAt") {
968
+ return attrs["published_at"] ? "published_at" : "publishedAt";
969
+ }
970
+ if (field === "id" || field === "documentId") {
971
+ return field;
972
+ }
973
+ const camel = field.replace(/_([a-z0-9])/g, (_, l) => l.toUpperCase());
974
+ if (attrs[camel]) return camel;
975
+ const snake = field.replace(/([A-Z])/g, "_$1").toLowerCase();
976
+ if (attrs[snake]) return snake;
977
+ const lower = field.toLowerCase().replace(/_/g, "");
978
+ const matched = Object.keys(attrs).find(
979
+ (k) => k.toLowerCase().replace(/_/g, "") === lower
980
+ );
981
+ if (matched) return matched;
982
+ return null;
983
+ }
819
984
  function buildDeepPopulate$1(attributes, allComponents, depth = 0) {
820
985
  if (depth > 5) return void 0;
821
986
  const populate = {};
@@ -948,13 +1113,15 @@ function getAllNestedValues(obj, path) {
948
1113
  if (!item || typeof item !== "object") return;
949
1114
  if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) return;
950
1115
  if (parsed.targetComponentUid && item.__component !== parsed.targetComponentUid) return;
1116
+ const compSchema = global.strapi?.components?.[item.__component];
1117
+ const fieldInSchema = tail.length > 0 && compSchema?.attributes ? tail[0] in compSchema.attributes : false;
951
1118
  const subVal = tail.length > 0 ? getDeepProperty(item, tail.join(".")) : item;
952
- if (subVal !== void 0) {
1119
+ if (subVal !== void 0 || fieldInSchema) {
953
1120
  results.push({
954
1121
  index: idx,
955
1122
  componentUid: item.__component,
956
1123
  componentName: formatComponentName(item.__component),
957
- value: subVal,
1124
+ value: subVal !== void 0 ? subVal : null,
958
1125
  path: `${head}[#${idx}${item.__component ? ` ${formatComponentName(item.__component)}` : ""}].${tail.join(".")}`
959
1126
  });
960
1127
  }
@@ -1053,7 +1220,9 @@ function setDeepProperty(obj, pathParts, value, options) {
1053
1220
  if (options?.targetComponentUid) {
1054
1221
  if (item.__component !== options.targetComponentUid) return item;
1055
1222
  }
1056
- if (head in item || tail.length > 0 && typeof item[head] === "object") {
1223
+ const compSchema = global.strapi?.components?.[item.__component];
1224
+ const fieldExistsInSchema = compSchema?.attributes ? head in compSchema.attributes : true;
1225
+ if (head in item || fieldExistsInSchema || tail.length === 0) {
1057
1226
  return {
1058
1227
  ...item,
1059
1228
  [head]: setDeepProperty(item[head], tail, value, options)
@@ -1167,6 +1336,12 @@ const pendingBatches = /* @__PURE__ */ new Map();
1167
1336
  const batchJobsMap = /* @__PURE__ */ new Map();
1168
1337
  let isRunnerRunning = false;
1169
1338
  let runnerInterval = null;
1339
+ function registerPendingBatch(batch, jobState) {
1340
+ pendingBatches.set(batch.id, batch);
1341
+ if (jobState) {
1342
+ batchJobsMap.set(batch.id, jobState);
1343
+ }
1344
+ }
1170
1345
  function invalidateCollectionCache(uid) {
1171
1346
  modelsCache.delete(uid);
1172
1347
  for (const key of queryCache.keys()) {
@@ -1238,7 +1413,9 @@ async function executeBatchInternal(strapi, {
1238
1413
  selectedCollections = [],
1239
1414
  isRegex = false,
1240
1415
  regexPattern = "",
1241
- publishOnApproval = true
1416
+ publishOnApproval = true,
1417
+ publishStrategy = "safe",
1418
+ unreviewedConsent = false
1242
1419
  }, currentUser, existingBatchId) {
1243
1420
  if (!attribute) {
1244
1421
  throw new Error("Attribute name is required.");
@@ -1291,6 +1468,8 @@ async function executeBatchInternal(strapi, {
1291
1468
  failedCount: 0,
1292
1469
  isLive: false,
1293
1470
  publishOnApproval: Boolean(publishOnApproval),
1471
+ publishStrategy,
1472
+ unreviewedConsent,
1294
1473
  results: []
1295
1474
  };
1296
1475
  batchJobsMap.set(batchId, jobState);
@@ -1364,7 +1543,9 @@ async function executeBatchInternal(strapi, {
1364
1543
  }
1365
1544
  });
1366
1545
  const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
1367
- if (publishOnApproval && isDraftAndPublishComp) {
1546
+ const isApprovedComp = isDraftAndPublishComp && Boolean(currentEntry?.publishedAt) && currentEntry?.status !== "modified";
1547
+ const shouldPublishComp = isDraftAndPublishComp && Boolean(publishOnApproval) && (publishStrategy === "all" ? true : isApprovedComp);
1548
+ if (shouldPublishComp) {
1368
1549
  try {
1369
1550
  await strapi.documents(target.uid).publish({
1370
1551
  documentId: target.documentId
@@ -1378,7 +1559,7 @@ async function executeBatchInternal(strapi, {
1378
1559
  uid: target.uid,
1379
1560
  documentId: target.documentId,
1380
1561
  userId: currentUser?.id,
1381
- statusOverride: publishOnApproval && isDraftAndPublishComp ? "published" : void 0
1562
+ statusOverride: shouldPublishComp ? "published" : isDraftAndPublishComp ? "draft" : void 0
1382
1563
  });
1383
1564
  const dzDiffs = isRootDz && affectedMatches.length > 0 ? affectedMatches.map((m) => ({
1384
1565
  index: m.index,
@@ -1461,7 +1642,9 @@ async function executeBatchInternal(strapi, {
1461
1642
  }
1462
1643
  });
1463
1644
  const isDraftAndPublishRel = Boolean(contentType?.options?.draftAndPublish);
1464
- if (publishOnApproval && isDraftAndPublishRel) {
1645
+ const isApprovedRel = isDraftAndPublishRel && Boolean(currentEntry?.publishedAt) && currentEntry?.status !== "modified";
1646
+ const shouldPublishRel = isDraftAndPublishRel && Boolean(publishOnApproval) && (publishStrategy === "all" ? true : isApprovedRel);
1647
+ if (shouldPublishRel) {
1465
1648
  try {
1466
1649
  await strapi.documents(target.uid).publish({
1467
1650
  documentId: target.documentId
@@ -1478,7 +1661,7 @@ async function executeBatchInternal(strapi, {
1478
1661
  uid: target.uid,
1479
1662
  documentId: target.documentId,
1480
1663
  userId: currentUser?.id,
1481
- statusOverride: publishOnApproval && isDraftAndPublishRel ? "published" : void 0
1664
+ statusOverride: shouldPublishRel ? "published" : isDraftAndPublishRel ? "draft" : void 0
1482
1665
  });
1483
1666
  return {
1484
1667
  uid: target.uid,
@@ -1504,7 +1687,9 @@ async function executeBatchInternal(strapi, {
1504
1687
  }
1505
1688
  });
1506
1689
  const isDraftAndPublishScalar = Boolean(contentType?.options?.draftAndPublish);
1507
- if (publishOnApproval && isDraftAndPublishScalar) {
1690
+ const isApprovedScalar = isDraftAndPublishScalar && Boolean(currentEntry?.publishedAt) && currentEntry?.status !== "modified";
1691
+ const shouldPublishScalar = isDraftAndPublishScalar && Boolean(publishOnApproval) && (publishStrategy === "all" ? true : isApprovedScalar);
1692
+ if (shouldPublishScalar) {
1508
1693
  try {
1509
1694
  await strapi.documents(target.uid).publish({
1510
1695
  documentId: target.documentId
@@ -1517,7 +1702,7 @@ async function executeBatchInternal(strapi, {
1517
1702
  uid: target.uid,
1518
1703
  documentId: target.documentId,
1519
1704
  userId: currentUser?.id,
1520
- statusOverride: publishOnApproval && isDraftAndPublishScalar ? "published" : void 0
1705
+ statusOverride: shouldPublishScalar ? "published" : isDraftAndPublishScalar ? "draft" : void 0
1521
1706
  });
1522
1707
  return {
1523
1708
  uid: target.uid,
@@ -1604,7 +1789,9 @@ async function executeBatchInternal(strapi, {
1604
1789
  updatedCount,
1605
1790
  skippedCount,
1606
1791
  totalTargets: targetsMap.size,
1607
- publishOnApproval
1792
+ publishOnApproval,
1793
+ publishStrategy,
1794
+ unreviewedConsent
1608
1795
  };
1609
1796
  await strapi.plugin("bulk-updater").service("audit").recordBatch(
1610
1797
  batchSummary,
@@ -1627,7 +1814,8 @@ async function executeBatchInternal(strapi, {
1627
1814
  }
1628
1815
  async function executePublishBatchInternal(strapi, {
1629
1816
  selectedEntries = [],
1630
- selectedCollections = []
1817
+ selectedCollections = [],
1818
+ unreviewedConsent = false
1631
1819
  }, currentUser, existingBatchId) {
1632
1820
  const targetsMap = /* @__PURE__ */ new Map();
1633
1821
  for (const colUid of selectedCollections) {
@@ -1672,6 +1860,7 @@ async function executePublishBatchInternal(strapi, {
1672
1860
  failedCount: 0,
1673
1861
  isLive: false,
1674
1862
  publishOnApproval: true,
1863
+ unreviewedConsent,
1675
1864
  results: []
1676
1865
  };
1677
1866
  batchJobsMap.set(batchId, jobState);
@@ -1769,7 +1958,8 @@ async function executePublishBatchInternal(strapi, {
1769
1958
  updatedCount: publishedCount,
1770
1959
  skippedCount,
1771
1960
  totalTargets: targetsMap.size,
1772
- publishOnApproval: true
1961
+ publishOnApproval: true,
1962
+ unreviewedConsent
1773
1963
  };
1774
1964
  await strapi.plugin("bulk-updater").service("audit").recordBatch(
1775
1965
  batchSummary,
@@ -1789,6 +1979,22 @@ async function executePublishBatchInternal(strapi, {
1789
1979
  results
1790
1980
  };
1791
1981
  }
1982
+ async function executeBatchByPayload(strapi, batch, currentUser) {
1983
+ if (batch.payload?.action === "csv_import") {
1984
+ const csvService2 = strapi.plugin("bulk-updater").service("csv");
1985
+ return await csvService2.executeCsvPendingBatch(
1986
+ batch.payload.uid,
1987
+ batch.payload.rows,
1988
+ batch.id,
1989
+ batch.payload.options,
1990
+ currentUser
1991
+ );
1992
+ } else if (batch.payload?.action === "publish") {
1993
+ return await executePublishBatchInternal(strapi, batch.payload, currentUser, batch.id);
1994
+ } else {
1995
+ return await executeBatchInternal(strapi, batch.payload, currentUser, batch.id);
1996
+ }
1997
+ }
1792
1998
  const bulkUpdaterService = ({ strapi }) => {
1793
1999
  if (!runnerInterval) {
1794
2000
  runnerInterval = setInterval(async () => {
@@ -1800,7 +2006,7 @@ const bulkUpdaterService = ({ strapi }) => {
1800
2006
  for (const [id, batch] of pendingBatches.entries()) {
1801
2007
  if (batch.status === "queued_offpeak" && !isBusinessHours) {
1802
2008
  strapi.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
1803
- const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi, batch.payload, batch.createdBy, id) : await executeBatchInternal(strapi, batch.payload, batch.createdBy, id);
2009
+ const result = await executeBatchByPayload(strapi, batch, batch.createdBy);
1804
2010
  batch.status = "executed";
1805
2011
  batch.executionResult = result;
1806
2012
  }
@@ -2012,11 +2218,7 @@ const bulkUpdaterService = ({ strapi }) => {
2012
2218
  entries = [];
2013
2219
  totalCount = 0;
2014
2220
  }
2015
- const formattedEntries = (entries || []).map((entry) => ({
2016
- id: entry.id,
2017
- documentId: entry.documentId,
2018
- ...entry
2019
- }));
2221
+ const formattedEntries = (entries || []).map(formatEntryRecord);
2020
2222
  modelsCache.set(uid, {
2021
2223
  signature,
2022
2224
  totalCount,
@@ -2105,21 +2307,26 @@ const bulkUpdaterService = ({ strapi }) => {
2105
2307
  let isNestedAttr = false;
2106
2308
  let isRootDz = false;
2107
2309
  let parts = [];
2310
+ let resolvedFilterAttr = targetFilterAttr;
2108
2311
  if (targetFilterAttr) {
2109
- const parsed = parseDynamicZonePath(targetFilterAttr);
2110
- if (parsed.rootKey && contentType.attributes?.[parsed.rootKey]?.type === "dynamiczone") {
2111
- isRootDz = true;
2312
+ const resolvedField = resolveContentTypeField(contentType, targetFilterAttr);
2313
+ if (resolvedField) {
2314
+ resolvedFilterAttr = resolvedField;
2112
2315
  colHasAttribute = true;
2113
- isNestedAttr = true;
2114
- parts = parsed.cleanPath.split(".");
2115
- } else if (targetFilterAttr.includes(".") || targetFilterAttr.includes("[")) {
2116
- isNestedAttr = isDeepComponentField(contentType, strapi.components, targetFilterAttr);
2117
- if (isNestedAttr) {
2316
+ } else {
2317
+ const parsed = parseDynamicZonePath(targetFilterAttr);
2318
+ if (parsed.rootKey && contentType.attributes?.[parsed.rootKey]?.type === "dynamiczone") {
2319
+ isRootDz = true;
2118
2320
  colHasAttribute = true;
2321
+ isNestedAttr = true;
2119
2322
  parts = parsed.cleanPath.split(".");
2323
+ } else if (targetFilterAttr.includes(".") || targetFilterAttr.includes("[")) {
2324
+ isNestedAttr = isDeepComponentField(contentType, strapi.components, targetFilterAttr);
2325
+ if (isNestedAttr) {
2326
+ colHasAttribute = true;
2327
+ parts = parsed.cleanPath.split(".");
2328
+ }
2120
2329
  }
2121
- } else if (attrs[targetFilterAttr]) {
2122
- colHasAttribute = true;
2123
2330
  }
2124
2331
  }
2125
2332
  const normOp = (filterOperator || "").toLowerCase().replace(/_/g, "");
@@ -2143,9 +2350,44 @@ const bulkUpdaterService = ({ strapi }) => {
2143
2350
  }
2144
2351
  let condition = void 0;
2145
2352
  const q = (filterQuery || "").trim();
2146
- const targetAttrSchema = isNestedAttr ? getLeafAttributeSchema(contentType, strapi.components, targetFilterAttr) : attrs[targetFilterAttr];
2353
+ const targetAttrSchema = isNestedAttr ? getLeafAttributeSchema(contentType, strapi.components, targetFilterAttr) : attrs[resolvedFilterAttr];
2354
+ const isRelation = targetAttrSchema?.type === "relation";
2147
2355
  const isStringLike = ["string", "text", "email", "richtext", "uid"].includes(targetAttrSchema?.type);
2148
- if (normOp === "empty") {
2356
+ if (isRelation) {
2357
+ const targetUid = targetAttrSchema?.target;
2358
+ const targetContentType = targetUid ? strapi.contentTypes[targetUid] : void 0;
2359
+ const targetAttrs = targetContentType?.attributes || {};
2360
+ const targetStringAttrs = Object.keys(targetAttrs).filter(
2361
+ (k) => ["string", "text", "email", "uid"].includes(targetAttrs[k]?.type)
2362
+ );
2363
+ if (normOp === "empty") {
2364
+ condition = { documentId: { $null: true } };
2365
+ } else if (normOp === "notempty") {
2366
+ condition = { documentId: { $notNull: true } };
2367
+ } else if (q !== "") {
2368
+ const orConditions = [];
2369
+ targetStringAttrs.slice(0, 5).forEach((k) => {
2370
+ if (normOp === "equals") {
2371
+ orConditions.push({ [k]: { $eqi: q } });
2372
+ } else if (normOp === "startswith") {
2373
+ orConditions.push({ [k]: { $startsWithi: q } });
2374
+ } else if (normOp === "endswith") {
2375
+ orConditions.push({ [k]: { $endsWithi: q } });
2376
+ } else if (normOp === "notequals") {
2377
+ orConditions.push({ [k]: { $ne: q } });
2378
+ } else if (normOp === "notcontains") {
2379
+ orConditions.push({ [k]: { $notContainsi: q } });
2380
+ } else {
2381
+ orConditions.push({ [k]: { $containsi: q } });
2382
+ }
2383
+ });
2384
+ orConditions.push({ documentId: { $eq: q } });
2385
+ if (/^\d+$/.test(q)) {
2386
+ orConditions.push({ id: { $eq: Number(q) } });
2387
+ }
2388
+ condition = orConditions.length === 1 ? orConditions[0] : { $or: orConditions };
2389
+ }
2390
+ } else if (normOp === "empty") {
2149
2391
  condition = isStringLike ? { $or: [{ $null: true }, { $eq: "" }] } : { $null: true };
2150
2392
  } else if (normOp === "notempty") {
2151
2393
  condition = isStringLike ? { $and: [{ $notNull: true }, { $ne: "" }] } : { $notNull: true };
@@ -2181,13 +2423,13 @@ const bulkUpdaterService = ({ strapi }) => {
2181
2423
  } else {
2182
2424
  if (normOp === "empty" && isStringLike) {
2183
2425
  filters.$or = [
2184
- { [targetFilterAttr]: { $null: true } },
2185
- { [targetFilterAttr]: { $eq: "" } }
2426
+ { [resolvedFilterAttr]: { $null: true } },
2427
+ { [resolvedFilterAttr]: { $eq: "" } }
2186
2428
  ];
2187
2429
  } else if (normOp === "notempty" && isStringLike) {
2188
- filters[targetFilterAttr] = { $notNull: true, $ne: "" };
2430
+ filters[resolvedFilterAttr] = { $notNull: true, $ne: "" };
2189
2431
  } else {
2190
- filters[targetFilterAttr] = condition;
2432
+ filters[resolvedFilterAttr] = condition;
2191
2433
  }
2192
2434
  }
2193
2435
  }
@@ -2229,7 +2471,7 @@ const bulkUpdaterService = ({ strapi }) => {
2229
2471
  displayName: contentType.info?.displayName || uid,
2230
2472
  isSingleType: true,
2231
2473
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2232
- entries: singleDoc ? [singleDoc] : [],
2474
+ entries: singleDoc ? [formatEntryRecord(singleDoc)] : [],
2233
2475
  pagination: {
2234
2476
  page: 1,
2235
2477
  pageSize: 1,
@@ -2246,8 +2488,13 @@ const bulkUpdaterService = ({ strapi }) => {
2246
2488
  } else {
2247
2489
  sortClause["createdAt"] = "desc";
2248
2490
  }
2249
- } else if (sortField && contentType.attributes?.[sortField]) {
2250
- sortClause[sortField] = sortOrder;
2491
+ } else if (sortField) {
2492
+ const resolvedField = resolveContentTypeField(contentType, sortField);
2493
+ if (resolvedField) {
2494
+ sortClause[resolvedField] = sortOrder;
2495
+ } else {
2496
+ sortClause["createdAt"] = "desc";
2497
+ }
2251
2498
  } else {
2252
2499
  sortClause["createdAt"] = "desc";
2253
2500
  }
@@ -2265,14 +2512,30 @@ const bulkUpdaterService = ({ strapi }) => {
2265
2512
  const passesDzFilter = (entry) => {
2266
2513
  const matches = getAllNestedValues(entry, targetFilterAttr);
2267
2514
  if (normOp === "empty") {
2268
- return matches.length === 0 || matches.every((m) => m.value === void 0 || m.value === null || String(m.value).trim() === "");
2515
+ return matches.length === 0 || matches.every((m) => {
2516
+ const v = m.value;
2517
+ if (v === void 0 || v === null) return true;
2518
+ if (Array.isArray(v)) return v.length === 0;
2519
+ if (typeof v === "object") return Object.keys(v).length === 0;
2520
+ return String(v).trim() === "";
2521
+ });
2269
2522
  }
2270
2523
  if (normOp === "notempty") {
2271
- return matches.some((m) => m.value !== void 0 && m.value !== null && String(m.value).trim() !== "");
2524
+ return matches.some((m) => {
2525
+ const v = m.value;
2526
+ if (v === void 0 || v === null) return false;
2527
+ if (Array.isArray(v)) return v.length > 0;
2528
+ if (typeof v === "object") return Object.keys(v).length > 0;
2529
+ return String(v).trim() !== "";
2530
+ });
2272
2531
  }
2273
- return matches.some((m) => {
2274
- if (m.value === void 0 || m.value === null) return false;
2275
- const valStr = String(m.value).toLowerCase();
2532
+ const testSingleValue = (v) => {
2533
+ if (v === void 0 || v === null) return false;
2534
+ if (Array.isArray(v)) return v.some(testSingleValue);
2535
+ if (typeof v === "object") {
2536
+ return testSingleValue(v.name) || testSingleValue(v.title) || testSingleValue(v.email) || testSingleValue(v.username) || testSingleValue(v.documentId) || testSingleValue(v.id) || testSingleValue(v.slug);
2537
+ }
2538
+ const valStr = String(v).toLowerCase();
2276
2539
  switch (normOp) {
2277
2540
  case "equals":
2278
2541
  return valStr === qVal;
@@ -2289,7 +2552,8 @@ const bulkUpdaterService = ({ strapi }) => {
2289
2552
  default:
2290
2553
  return true;
2291
2554
  }
2292
- });
2555
+ };
2556
+ return matches.some((m) => testSingleValue(m.value));
2293
2557
  };
2294
2558
  const matchedEntries = (rawEntries || []).filter(passesDzFilter);
2295
2559
  const totalEntries = matchedEntries.length;
@@ -2299,11 +2563,7 @@ const bulkUpdaterService = ({ strapi }) => {
2299
2563
  displayName: contentType.info?.displayName || uid,
2300
2564
  isSingleType: false,
2301
2565
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2302
- entries: paginatedEntries.map((entry) => ({
2303
- id: entry.id,
2304
- documentId: entry.documentId,
2305
- ...entry
2306
- })),
2566
+ entries: paginatedEntries.map(formatEntryRecord),
2307
2567
  pagination: {
2308
2568
  page: currentPage,
2309
2569
  pageSize: limit,
@@ -2332,11 +2592,7 @@ const bulkUpdaterService = ({ strapi }) => {
2332
2592
  displayName: contentType.info?.displayName || uid,
2333
2593
  isSingleType: false,
2334
2594
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2335
- entries: (entries || []).map((entry) => ({
2336
- id: entry.id,
2337
- documentId: entry.documentId,
2338
- ...entry
2339
- })),
2595
+ entries: (entries || []).map(formatEntryRecord),
2340
2596
  pagination: {
2341
2597
  page: currentPage,
2342
2598
  pageSize: limit,
@@ -2413,7 +2669,12 @@ const bulkUpdaterService = ({ strapi }) => {
2413
2669
  for (const item of selectedEntries) {
2414
2670
  if (!item.uid || !item.documentId) continue;
2415
2671
  const key = `${item.uid}@@@${item.documentId}`;
2416
- targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
2672
+ targetsMap.set(key, {
2673
+ uid: item.uid,
2674
+ documentId: item.documentId,
2675
+ dynamicZoneIndexes: item.dynamicZoneIndexes,
2676
+ targetComponentUid: item.targetComponentUid
2677
+ });
2417
2678
  }
2418
2679
  if (targetsMap.size === 0) {
2419
2680
  throw new Error("No collections or entries were selected for preview.");
@@ -2477,6 +2738,49 @@ const bulkUpdaterService = ({ strapi }) => {
2477
2738
  }
2478
2739
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
2479
2740
  if (isComponentNested) {
2741
+ const parsed = parseDynamicZonePath(attribute);
2742
+ const rootKey = parsed.rootKey;
2743
+ const isRootDz = contentType?.attributes?.[rootKey]?.type === "dynamiczone";
2744
+ const targetIndexes = target.dynamicZoneIndexes;
2745
+ const targetComponentUid = target.targetComponentUid || parsed.targetComponentUid;
2746
+ if (isRootDz && currentEntry?.[rootKey] && Array.isArray(currentEntry[rootKey])) {
2747
+ let affectedMatches = getAllNestedValues({ [rootKey]: currentEntry[rootKey] }, attribute);
2748
+ if (targetIndexes && targetIndexes.length > 0) {
2749
+ affectedMatches = affectedMatches.filter((m) => targetIndexes.includes(m.index));
2750
+ }
2751
+ if (targetComponentUid) {
2752
+ affectedMatches = affectedMatches.filter((m) => m.componentUid === targetComponentUid);
2753
+ }
2754
+ if (affectedMatches.length > 0) {
2755
+ for (const match of affectedMatches) {
2756
+ let blockNewVal = value;
2757
+ if (regexObj && typeof match.value === "string") {
2758
+ blockNewVal = match.value.replace(regexObj, value);
2759
+ }
2760
+ const hasChange3 = String(match.value ?? "") !== String(blockNewVal ?? "");
2761
+ if (hasChange3) {
2762
+ willUpdateCount++;
2763
+ } else {
2764
+ unchangedCount++;
2765
+ }
2766
+ diffs.push({
2767
+ uid: target.uid,
2768
+ collectionName: displayName,
2769
+ documentId: target.documentId,
2770
+ label: `${entryLabel} [Block #${match.index} (${match.componentName})]`,
2771
+ attribute: `${rootKey}[#${match.index} ${match.componentName}].${parsed.subPath}`,
2772
+ oldValue: match.value ?? null,
2773
+ newValue: blockNewVal,
2774
+ oldDisplay: match.value !== void 0 && match.value !== null && String(match.value).trim() !== "" ? String(match.value) : "(empty)",
2775
+ newDisplay: String(blockNewVal ?? ""),
2776
+ hasChange: hasChange3,
2777
+ status: hasChange3 ? "will_update" : "unchanged",
2778
+ message: hasChange3 ? "Block value will be updated" : "Block already has target value"
2779
+ });
2780
+ }
2781
+ continue;
2782
+ }
2783
+ }
2480
2784
  const currentVal2 = getDeepProperty(currentEntry, attribute);
2481
2785
  let newValue2 = value;
2482
2786
  if (regexObj && typeof currentVal2 === "string") {
@@ -2632,7 +2936,10 @@ const bulkUpdaterService = ({ strapi }) => {
2632
2936
  executionMode = "immediate",
2633
2937
  makerNotes = "",
2634
2938
  forceImmediate = false,
2635
- publishOnApproval = true
2939
+ publishOnApproval = true,
2940
+ publishStrategy = "safe",
2941
+ unreviewedConsent = false,
2942
+ unreviewedCount = 0
2636
2943
  }, currentUser) {
2637
2944
  if (!attribute) {
2638
2945
  throw new Error("Attribute name is required.");
@@ -2655,13 +2962,16 @@ const bulkUpdaterService = ({ strapi }) => {
2655
2962
  lastname: currentUser.lastname,
2656
2963
  username: currentUser.username
2657
2964
  } : { email: "admin@system.local" },
2658
- payload: { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval },
2965
+ payload: { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval, publishStrategy, unreviewedConsent, unreviewedCount },
2659
2966
  summary: {
2660
2967
  attribute,
2661
2968
  value,
2662
2969
  totalTargets: summary.totalTargets,
2663
2970
  collectionBreakdown: summary.collectionBreakdown,
2664
- publishOnApproval
2971
+ publishOnApproval,
2972
+ publishStrategy,
2973
+ unreviewedConsent,
2974
+ unreviewedCount
2665
2975
  },
2666
2976
  status: "pending_approval",
2667
2977
  executionMode: "maker_checker",
@@ -2683,6 +2993,9 @@ const bulkUpdaterService = ({ strapi }) => {
2683
2993
  failedCount: 0,
2684
2994
  isLive: false,
2685
2995
  publishOnApproval: Boolean(publishOnApproval),
2996
+ publishStrategy,
2997
+ unreviewedConsent,
2998
+ unreviewedCount,
2686
2999
  makerNotes,
2687
3000
  results: []
2688
3001
  });
@@ -2711,13 +3024,16 @@ const bulkUpdaterService = ({ strapi }) => {
2711
3024
  lastname: currentUser.lastname,
2712
3025
  username: currentUser.username
2713
3026
  } : { email: "admin@system.local" },
2714
- payload: { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval },
3027
+ payload: { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval, publishStrategy, unreviewedConsent, unreviewedCount },
2715
3028
  summary: {
2716
3029
  attribute,
2717
3030
  value,
2718
3031
  totalTargets: summary.totalTargets,
2719
3032
  collectionBreakdown: summary.collectionBreakdown,
2720
- publishOnApproval
3033
+ publishOnApproval,
3034
+ publishStrategy,
3035
+ unreviewedConsent,
3036
+ unreviewedCount
2721
3037
  },
2722
3038
  status: "queued_offpeak",
2723
3039
  executionMode: "queue_offpeak",
@@ -2741,6 +3057,9 @@ const bulkUpdaterService = ({ strapi }) => {
2741
3057
  failedCount: 0,
2742
3058
  isLive: false,
2743
3059
  publishOnApproval: Boolean(publishOnApproval),
3060
+ publishStrategy,
3061
+ unreviewedConsent,
3062
+ unreviewedCount,
2744
3063
  makerNotes,
2745
3064
  results: []
2746
3065
  });
@@ -2770,13 +3089,16 @@ const bulkUpdaterService = ({ strapi }) => {
2770
3089
  lastname: currentUser.lastname,
2771
3090
  username: currentUser.username
2772
3091
  } : { email: "admin@system.local" },
2773
- payload: { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval },
3092
+ payload: { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval, publishStrategy, unreviewedConsent, unreviewedCount },
2774
3093
  summary: {
2775
3094
  attribute,
2776
3095
  value,
2777
3096
  totalTargets: summary.totalTargets,
2778
3097
  collectionBreakdown: summary.collectionBreakdown,
2779
- publishOnApproval
3098
+ publishOnApproval,
3099
+ publishStrategy,
3100
+ unreviewedConsent,
3101
+ unreviewedCount
2780
3102
  },
2781
3103
  status: "queued_offpeak",
2782
3104
  executionMode: "queue_offpeak",
@@ -2800,6 +3122,9 @@ const bulkUpdaterService = ({ strapi }) => {
2800
3122
  failedCount: 0,
2801
3123
  isLive: false,
2802
3124
  publishOnApproval: Boolean(publishOnApproval),
3125
+ publishStrategy,
3126
+ unreviewedConsent,
3127
+ unreviewedCount,
2803
3128
  policyMessage,
2804
3129
  makerNotes: "Automatically queued due to peak business hours restriction.",
2805
3130
  results: []
@@ -2816,7 +3141,7 @@ const bulkUpdaterService = ({ strapi }) => {
2816
3141
  }
2817
3142
  return await executeBatchInternal(
2818
3143
  strapi,
2819
- { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval },
3144
+ { attribute, value, selectedEntries, selectedCollections, isRegex, regexPattern, publishOnApproval, publishStrategy, unreviewedConsent },
2820
3145
  currentUser
2821
3146
  );
2822
3147
  },
@@ -2828,7 +3153,9 @@ const bulkUpdaterService = ({ strapi }) => {
2828
3153
  selectedCollections = [],
2829
3154
  executionMode = "immediate",
2830
3155
  makerNotes = "",
2831
- forceImmediate = false
3156
+ forceImmediate = false,
3157
+ unreviewedConsent = false,
3158
+ unreviewedCount = 0
2832
3159
  }, currentUser) {
2833
3160
  const executionConfig = getExecutionWindowConfig(strapi);
2834
3161
  const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
@@ -2848,13 +3175,15 @@ const bulkUpdaterService = ({ strapi }) => {
2848
3175
  lastname: currentUser.lastname,
2849
3176
  username: currentUser.username
2850
3177
  } : { email: "admin@system.local" },
2851
- payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
3178
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true, unreviewedConsent, unreviewedCount },
2852
3179
  summary: {
2853
3180
  attribute: "(publish)",
2854
3181
  value: "published",
2855
3182
  totalTargets: summary.totalTargets,
2856
3183
  collectionBreakdown: summary.collectionBreakdown,
2857
- publishOnApproval: true
3184
+ publishOnApproval: true,
3185
+ unreviewedConsent,
3186
+ unreviewedCount
2858
3187
  },
2859
3188
  status: "pending_approval",
2860
3189
  executionMode: "maker_checker",
@@ -2876,6 +3205,8 @@ const bulkUpdaterService = ({ strapi }) => {
2876
3205
  failedCount: 0,
2877
3206
  isLive: false,
2878
3207
  publishOnApproval: true,
3208
+ unreviewedConsent,
3209
+ unreviewedCount,
2879
3210
  makerNotes,
2880
3211
  results: []
2881
3212
  });
@@ -2904,13 +3235,15 @@ const bulkUpdaterService = ({ strapi }) => {
2904
3235
  lastname: currentUser.lastname,
2905
3236
  username: currentUser.username
2906
3237
  } : { email: "admin@system.local" },
2907
- payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
3238
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true, unreviewedConsent, unreviewedCount },
2908
3239
  summary: {
2909
3240
  attribute: "(publish)",
2910
3241
  value: "published",
2911
3242
  totalTargets: summary.totalTargets,
2912
3243
  collectionBreakdown: summary.collectionBreakdown,
2913
- publishOnApproval: true
3244
+ publishOnApproval: true,
3245
+ unreviewedConsent,
3246
+ unreviewedCount
2914
3247
  },
2915
3248
  status: "queued_offpeak",
2916
3249
  executionMode: "queue_offpeak",
@@ -2934,6 +3267,8 @@ const bulkUpdaterService = ({ strapi }) => {
2934
3267
  failedCount: 0,
2935
3268
  isLive: false,
2936
3269
  publishOnApproval: true,
3270
+ unreviewedConsent,
3271
+ unreviewedCount,
2937
3272
  makerNotes,
2938
3273
  results: []
2939
3274
  });
@@ -2963,13 +3298,15 @@ const bulkUpdaterService = ({ strapi }) => {
2963
3298
  lastname: currentUser.lastname,
2964
3299
  username: currentUser.username
2965
3300
  } : { email: "admin@system.local" },
2966
- payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
3301
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true, unreviewedConsent, unreviewedCount },
2967
3302
  summary: {
2968
3303
  attribute: "(publish)",
2969
3304
  value: "published",
2970
3305
  totalTargets: summary.totalTargets,
2971
3306
  collectionBreakdown: summary.collectionBreakdown,
2972
- publishOnApproval: true
3307
+ publishOnApproval: true,
3308
+ unreviewedConsent,
3309
+ unreviewedCount
2973
3310
  },
2974
3311
  status: "queued_offpeak",
2975
3312
  executionMode: "queue_offpeak",
@@ -2993,6 +3330,8 @@ const bulkUpdaterService = ({ strapi }) => {
2993
3330
  failedCount: 0,
2994
3331
  isLive: false,
2995
3332
  publishOnApproval: true,
3333
+ unreviewedConsent,
3334
+ unreviewedCount,
2996
3335
  policyMessage,
2997
3336
  makerNotes: "Automatically queued due to peak business hours restriction.",
2998
3337
  results: []
@@ -3009,7 +3348,7 @@ const bulkUpdaterService = ({ strapi }) => {
3009
3348
  }
3010
3349
  return await executePublishBatchInternal(
3011
3350
  strapi,
3012
- { selectedEntries, selectedCollections },
3351
+ { selectedEntries, selectedCollections, unreviewedConsent },
3013
3352
  currentUser
3014
3353
  );
3015
3354
  },
@@ -3056,7 +3395,7 @@ const bulkUpdaterService = ({ strapi }) => {
3056
3395
  const { isBusinessHours } = checkBusinessHours(config);
3057
3396
  if (options.executeImmediately || !isBusinessHours) {
3058
3397
  batch.status = "approved";
3059
- const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi, batch.payload, checkerUser, batch.id) : await executeBatchInternal(strapi, batch.payload, checkerUser, batch.id);
3398
+ const result = await executeBatchByPayload(strapi, batch, checkerUser);
3060
3399
  batch.status = "executed";
3061
3400
  batch.executionResult = result;
3062
3401
  return {
@@ -3117,7 +3456,7 @@ const bulkUpdaterService = ({ strapi }) => {
3117
3456
  if (batch.status === "executed") {
3118
3457
  throw new Error(`Batch ${batchId} has already been executed.`);
3119
3458
  }
3120
- const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi, batch.payload, currentUser, batch.id) : await executeBatchInternal(strapi, batch.payload, currentUser, batch.id);
3459
+ const result = await executeBatchByPayload(strapi, batch, currentUser);
3121
3460
  batch.status = "executed";
3122
3461
  batch.executionResult = result;
3123
3462
  return {
@@ -3364,6 +3703,20 @@ const bulkUpdaterService = ({ strapi }) => {
3364
3703
  },
3365
3704
  invalidateCollectionCache(uid) {
3366
3705
  invalidateCollectionCache(uid);
3706
+ },
3707
+ registerPendingBatch(batch, jobState) {
3708
+ pendingBatches.set(batch.id, batch);
3709
+ if (jobState) {
3710
+ batchJobsMap.set(batch.id, jobState);
3711
+ }
3712
+ return batch;
3713
+ },
3714
+ getExecutionWindowConfig() {
3715
+ return getExecutionWindowConfig(strapi);
3716
+ },
3717
+ checkBusinessHours(config) {
3718
+ const cfg = config || getExecutionWindowConfig(strapi);
3719
+ return checkBusinessHours(cfg);
3367
3720
  }
3368
3721
  };
3369
3722
  };
@@ -3529,7 +3882,9 @@ const auditService = ({ strapi }) => ({
3529
3882
  value: batchSummary.value,
3530
3883
  updatedCount: batchSummary.updatedCount,
3531
3884
  skippedCount: batchSummary.skippedCount,
3532
- totalTargets: batchSummary.totalTargets
3885
+ totalTargets: batchSummary.totalTargets,
3886
+ publishStrategy: batchSummary.publishStrategy,
3887
+ unreviewedConsent: batchSummary.unreviewedConsent
3533
3888
  },
3534
3889
  user
3535
3890
  );
@@ -3552,6 +3907,9 @@ const auditService = ({ strapi }) => ({
3552
3907
  updatedCount: batchSummary.updatedCount,
3553
3908
  skippedCount: batchSummary.skippedCount,
3554
3909
  totalTargets: batchSummary.totalTargets,
3910
+ publishOnApproval: batchSummary.publishOnApproval,
3911
+ publishStrategy: batchSummary.publishStrategy,
3912
+ unreviewedConsent: batchSummary.unreviewedConsent,
3555
3913
  diffs: results,
3556
3914
  isRolledBack: false,
3557
3915
  rolledBackAt: null,
@@ -4548,7 +4906,23 @@ function getNestedExportValue(entry, path) {
4548
4906
  return String(item);
4549
4907
  }).filter(Boolean).join("; ");
4550
4908
  }
4551
- current = current[part];
4909
+ if (current && typeof current === "object") {
4910
+ if (current[part] !== void 0) {
4911
+ current = current[part];
4912
+ } else {
4913
+ const camel = part.replace(/_([a-z0-9])/g, (_, l) => l.toUpperCase());
4914
+ const snake = part.replace(/([A-Z])/g, "_$1").toLowerCase();
4915
+ if (current[camel] !== void 0) {
4916
+ current = current[camel];
4917
+ } else if (current[snake] !== void 0) {
4918
+ current = current[snake];
4919
+ } else {
4920
+ current = void 0;
4921
+ }
4922
+ }
4923
+ } else {
4924
+ current = void 0;
4925
+ }
4552
4926
  }
4553
4927
  if (current === null || current === void 0) return "";
4554
4928
  if (typeof current === "object" && current.url) {
@@ -4570,69 +4944,638 @@ function getNestedExportValue(entry, path) {
4570
4944
  }
4571
4945
  return current;
4572
4946
  }
4573
- const csvService = ({ strapi }) => ({
4574
- /**
4575
- * Generates a sample template CSV with schema attribute headers and sample values
4576
- */
4577
- async getTemplateCsv(uid) {
4578
- const ct = strapi.contentTypes[uid];
4579
- if (!ct) {
4580
- throw new Error(`Content type ${uid} not found`);
4947
+ const csvImportSessions = /* @__PURE__ */ new Map();
4948
+ function cleanupStaleSessions() {
4949
+ const oneHourAgo = Date.now() - 36e5;
4950
+ for (const [id, session] of csvImportSessions.entries()) {
4951
+ if (session.createdAt < oneHourAgo) {
4952
+ csvImportSessions.delete(id);
4581
4953
  }
4582
- const attrs = ct.attributes || {};
4583
- const excluded = ["createdAt", "updatedAt", "publishedAt", "createdBy", "updatedBy", "localizations"];
4584
- const headers = ["documentId"];
4585
- const sampleRow = ["(Leave empty to create new, or enter existing docId)"];
4586
- for (const [attrName, attrDef] of Object.entries(attrs)) {
4587
- if (excluded.includes(attrName)) continue;
4588
- if (attrDef.type === "relation") continue;
4589
- if (attrDef.type === "component" || attrDef.type === "dynamiczone") continue;
4590
- headers.push(attrName);
4591
- let sampleVal = "";
4954
+ }
4955
+ }
4956
+ const SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
4957
+ "documentid",
4958
+ "document_id",
4959
+ "docid",
4960
+ "id",
4961
+ "createdat",
4962
+ "updatedat",
4963
+ "publishedat",
4964
+ "createdby",
4965
+ "updatedby",
4966
+ "localizations",
4967
+ "locale",
4968
+ "status"
4969
+ ]);
4970
+ async function findExistingDocument(strapi, uid, targetDocId, docLocale) {
4971
+ if (!targetDocId) return null;
4972
+ try {
4973
+ const queryArgs = { documentId: targetDocId };
4974
+ if (docLocale) queryArgs.locale = docLocale;
4975
+ const found = await strapi.documents(uid).findOne(queryArgs);
4976
+ if (found) return found;
4977
+ } catch {
4978
+ }
4979
+ if (/^\d+$/.test(targetDocId)) {
4980
+ try {
4981
+ const queryArgs = {
4982
+ filters: { id: parseInt(targetDocId, 10) },
4983
+ limit: 1
4984
+ };
4985
+ if (docLocale) queryArgs.locale = docLocale;
4986
+ const list = await strapi.documents(uid).findMany(queryArgs);
4987
+ if (list && list.length > 0) return list[0];
4988
+ } catch {
4989
+ }
4990
+ }
4991
+ return null;
4992
+ }
4993
+ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
4994
+ const attrs = ct.attributes || {};
4995
+ const { mode = "auto", publish = true, publishStrategy = "safe", user } = options;
4996
+ const isDraftAndPublish = Boolean(ct.options?.draftAndPublish);
4997
+ const stats = {
4998
+ total: rows.length,
4999
+ created: 0,
5000
+ updated: 0,
5001
+ skipped: 0,
5002
+ newWithoutDocId: 0,
5003
+ errors: [],
5004
+ diffs: []
5005
+ };
5006
+ for (let i = 0; i < rows.length; i++) {
5007
+ const rowNumber = startRowNumber + i;
5008
+ const row = rows[i];
5009
+ let docId = extractDocId(row);
5010
+ const rowLocale = extractLocale(row);
5011
+ if (ct.kind === "singleType" && !docId) {
5012
+ try {
5013
+ const findSingleArgs = {};
5014
+ if (rowLocale) findSingleArgs.locale = rowLocale;
5015
+ const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
5016
+ if (singleDoc?.documentId) {
5017
+ docId = singleDoc.documentId;
5018
+ }
5019
+ } catch {
5020
+ }
5021
+ }
5022
+ const payload = {};
5023
+ for (const [key, rawVal] of Object.entries(row)) {
5024
+ const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
5025
+ if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
5026
+ if (key.includes(".")) {
5027
+ const [parent, child] = key.split(".");
5028
+ const parentDef = attrs[parent];
5029
+ if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
5030
+ const comp = strapi.components[parentDef.component];
5031
+ const childDef = comp?.attributes?.[child];
5032
+ if (childDef) {
5033
+ if (!payload[parent]) payload[parent] = {};
5034
+ let val = rawVal;
5035
+ if (rawVal === "" || rawVal === null || rawVal === void 0) {
5036
+ val = null;
5037
+ } else if (childDef.type === "integer" || childDef.type === "biginteger") {
5038
+ const parsedInt = parseInt(rawVal, 10);
5039
+ val = isNaN(parsedInt) ? null : parsedInt;
5040
+ } else if (childDef.type === "float" || childDef.type === "decimal") {
5041
+ const parsedFloat = parseFloat(rawVal);
5042
+ val = isNaN(parsedFloat) ? null : parsedFloat;
5043
+ } else if (childDef.type === "boolean") {
5044
+ val = rawVal.toLowerCase() === "true" || rawVal === "1";
5045
+ } else if (childDef.type === "json") {
5046
+ try {
5047
+ val = JSON.parse(rawVal);
5048
+ } catch {
5049
+ val = rawVal;
5050
+ }
5051
+ }
5052
+ payload[parent][child] = val;
5053
+ }
5054
+ }
5055
+ continue;
5056
+ }
5057
+ let attrDef = attrs[key];
5058
+ if (!attrDef) {
5059
+ const matchKey = Object.keys(attrs).find(
5060
+ (k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
5061
+ );
5062
+ if (matchKey) {
5063
+ attrDef = attrs[matchKey];
5064
+ }
5065
+ }
5066
+ if (!attrDef) continue;
5067
+ if (rawVal === "" || rawVal === null || rawVal === void 0) {
5068
+ if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
5069
+ payload[key] = null;
5070
+ }
5071
+ continue;
5072
+ }
4592
5073
  switch (attrDef.type) {
4593
- case "string":
4594
- case "text":
4595
- case "richtext":
4596
- sampleVal = `Sample ${attrName}`;
4597
- break;
4598
5074
  case "integer":
4599
- case "biginteger":
5075
+ case "biginteger": {
5076
+ const parsedInt = parseInt(rawVal, 10);
5077
+ payload[key] = isNaN(parsedInt) ? null : parsedInt;
5078
+ break;
5079
+ }
4600
5080
  case "float":
4601
- case "decimal":
4602
- sampleVal = "10";
5081
+ case "decimal": {
5082
+ const parsedFloat = parseFloat(rawVal);
5083
+ payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
4603
5084
  break;
5085
+ }
4604
5086
  case "boolean":
4605
- sampleVal = "true";
5087
+ payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
4606
5088
  break;
4607
- case "email":
4608
- sampleVal = "sample@example.com";
5089
+ case "json":
5090
+ try {
5091
+ payload[key] = JSON.parse(rawVal);
5092
+ } catch {
5093
+ payload[key] = rawVal;
5094
+ }
4609
5095
  break;
4610
- case "enumeration":
4611
- sampleVal = attrDef.enum?.[0] || "option1";
5096
+ case "dynamiczone": {
5097
+ try {
5098
+ const parsed = JSON.parse(rawVal);
5099
+ if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
5100
+ payload[key] = parsed;
5101
+ }
5102
+ } catch {
5103
+ }
4612
5104
  break;
4613
- case "customField":
4614
- sampleVal = "value";
5105
+ }
5106
+ case "component": {
5107
+ if (attrDef.repeatable) {
5108
+ try {
5109
+ const parsed = JSON.parse(rawVal);
5110
+ if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
5111
+ payload[key] = parsed;
5112
+ }
5113
+ } catch {
5114
+ }
5115
+ } else {
5116
+ try {
5117
+ const parsed = JSON.parse(rawVal);
5118
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
5119
+ payload[key] = parsed;
5120
+ }
5121
+ } catch {
5122
+ }
5123
+ }
4615
5124
  break;
4616
- default:
4617
- sampleVal = "sample";
4618
- }
4619
- sampleRow.push(sampleVal);
4620
- }
4621
- const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
4622
- return {
4623
- filename: `${ct.info?.singularName || "template"}_template.csv`,
4624
- csvContent,
4625
- headers
4626
- };
4627
- },
4628
- /**
4629
- * Exports entries of a collection to CSV with deep nested component support,
4630
- * RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
4631
- */
4632
- async exportCollectionCsv(uid, options = {}) {
4633
- const ct = strapi.contentTypes[uid];
4634
- if (!ct) {
4635
- throw new Error(`Content type ${uid} not found`);
5125
+ }
5126
+ case "relation": {
5127
+ try {
5128
+ const parsed = JSON.parse(rawVal);
5129
+ if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
5130
+ payload[key] = parsed;
5131
+ break;
5132
+ }
5133
+ } catch {
5134
+ }
5135
+ if (isValidDocId(rawVal)) {
5136
+ payload[key] = rawVal;
5137
+ } else if (rawVal.includes(";") || rawVal.includes(",")) {
5138
+ const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
5139
+ if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
5140
+ payload[key] = parts;
5141
+ }
5142
+ }
5143
+ break;
5144
+ }
5145
+ case "media": {
5146
+ if (/^\d+$/.test(rawVal.trim())) {
5147
+ payload[key] = parseInt(rawVal.trim(), 10);
5148
+ } else {
5149
+ try {
5150
+ const parsed = JSON.parse(rawVal);
5151
+ if (typeof parsed === "number" || typeof parsed === "object") {
5152
+ payload[key] = parsed;
5153
+ }
5154
+ } catch {
5155
+ }
5156
+ }
5157
+ break;
5158
+ }
5159
+ default:
5160
+ payload[key] = rawVal;
5161
+ }
5162
+ }
5163
+ try {
5164
+ if (mode === "update") {
5165
+ if (!docId) {
5166
+ stats.skipped++;
5167
+ stats.errors.push({
5168
+ row: rowNumber,
5169
+ error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
5170
+ });
5171
+ continue;
5172
+ }
5173
+ const existing = await findExistingDocument(strapi, uid, docId, rowLocale);
5174
+ if (!existing) {
5175
+ stats.skipped++;
5176
+ stats.errors.push({
5177
+ row: rowNumber,
5178
+ documentId: docId,
5179
+ error: `Document with ID "${docId}" not found in database.`
5180
+ });
5181
+ continue;
5182
+ }
5183
+ const resolvedDocId = existing.documentId || docId;
5184
+ const mergedPayload = { ...payload };
5185
+ for (const key of Object.keys(payload)) {
5186
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5187
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
5188
+ }
5189
+ }
5190
+ if (Object.keys(mergedPayload).length === 0) {
5191
+ stats.skipped++;
5192
+ stats.diffs.push({
5193
+ uid,
5194
+ collectionName: ct.info?.displayName || uid,
5195
+ documentId: resolvedDocId,
5196
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5197
+ attribute: "",
5198
+ oldValue: existing,
5199
+ newValue: {},
5200
+ status: "skipped",
5201
+ message: `Skipped (no editable attributes in row ${rowNumber})`
5202
+ });
5203
+ continue;
5204
+ }
5205
+ const updateArgs = {
5206
+ documentId: resolvedDocId,
5207
+ data: mergedPayload
5208
+ };
5209
+ if (rowLocale) updateArgs.locale = rowLocale;
5210
+ await strapi.documents(uid).update(updateArgs);
5211
+ const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
5212
+ const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
5213
+ if (shouldPublish) {
5214
+ try {
5215
+ const pubArgs = { documentId: resolvedDocId };
5216
+ if (rowLocale) pubArgs.locale = rowLocale;
5217
+ await strapi.documents(uid).publish(pubArgs);
5218
+ } catch (pubErr) {
5219
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
5220
+ }
5221
+ }
5222
+ try {
5223
+ await recordContentHistoryVersion(strapi, {
5224
+ uid,
5225
+ documentId: resolvedDocId,
5226
+ userId: user?.id,
5227
+ statusOverride: shouldPublish ? "published" : isDraftAndPublish ? "draft" : void 0
5228
+ });
5229
+ } catch {
5230
+ }
5231
+ stats.updated++;
5232
+ for (const attrKey of Object.keys(payload)) {
5233
+ const oldVal = existing?.[attrKey] ?? null;
5234
+ const newVal = payload[attrKey];
5235
+ stats.diffs.push({
5236
+ uid,
5237
+ collectionName: ct.info?.displayName || uid,
5238
+ documentId: resolvedDocId,
5239
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5240
+ attribute: attrKey,
5241
+ oldValue: oldVal,
5242
+ newValue: newVal,
5243
+ status: "updated",
5244
+ message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
5245
+ });
5246
+ }
5247
+ } else if (mode === "create_or_override") {
5248
+ let existing = null;
5249
+ if (docId) {
5250
+ existing = await findExistingDocument(strapi, uid, docId, rowLocale);
5251
+ }
5252
+ if (existing) {
5253
+ const resolvedDocId = existing.documentId || docId;
5254
+ const mergedPayload = { ...payload };
5255
+ for (const key of Object.keys(payload)) {
5256
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5257
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
5258
+ }
5259
+ }
5260
+ const updateArgs = {
5261
+ documentId: resolvedDocId,
5262
+ data: mergedPayload
5263
+ };
5264
+ if (rowLocale) updateArgs.locale = rowLocale;
5265
+ await strapi.documents(uid).update(updateArgs);
5266
+ const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
5267
+ const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
5268
+ if (shouldPublish) {
5269
+ try {
5270
+ const pubArgs = { documentId: resolvedDocId };
5271
+ if (rowLocale) pubArgs.locale = rowLocale;
5272
+ await strapi.documents(uid).publish(pubArgs);
5273
+ } catch (pubErr) {
5274
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
5275
+ }
5276
+ }
5277
+ try {
5278
+ await recordContentHistoryVersion(strapi, {
5279
+ uid,
5280
+ documentId: resolvedDocId,
5281
+ userId: user?.id,
5282
+ statusOverride: shouldPublish ? "published" : isDraftAndPublish ? "draft" : void 0
5283
+ });
5284
+ } catch {
5285
+ }
5286
+ stats.updated++;
5287
+ for (const attrKey of Object.keys(payload)) {
5288
+ const oldVal = existing?.[attrKey] ?? null;
5289
+ const newVal = payload[attrKey];
5290
+ stats.diffs.push({
5291
+ uid,
5292
+ collectionName: ct.info?.displayName || uid,
5293
+ documentId: resolvedDocId,
5294
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5295
+ attribute: attrKey,
5296
+ oldValue: oldVal,
5297
+ newValue: newVal,
5298
+ status: "updated",
5299
+ message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
5300
+ });
5301
+ }
5302
+ continue;
5303
+ }
5304
+ const createArgs = { data: payload };
5305
+ if (rowLocale) createArgs.locale = rowLocale;
5306
+ const created = await strapi.documents(uid).create(createArgs);
5307
+ const newDocId = created?.documentId || docId;
5308
+ const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
5309
+ if (shouldPublishNew && newDocId) {
5310
+ try {
5311
+ const pubArgs = { documentId: newDocId };
5312
+ if (rowLocale) pubArgs.locale = rowLocale;
5313
+ await strapi.documents(uid).publish(pubArgs);
5314
+ } catch (pubErr) {
5315
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
5316
+ }
5317
+ }
5318
+ try {
5319
+ await recordContentHistoryVersion(strapi, {
5320
+ uid,
5321
+ documentId: newDocId,
5322
+ userId: user?.id,
5323
+ statusOverride: shouldPublishNew ? "published" : isDraftAndPublish ? "draft" : void 0
5324
+ });
5325
+ } catch {
5326
+ }
5327
+ stats.created++;
5328
+ if (!docId) {
5329
+ stats.newWithoutDocId++;
5330
+ }
5331
+ for (const attrKey of Object.keys(payload)) {
5332
+ stats.diffs.push({
5333
+ uid,
5334
+ collectionName: ct.info?.displayName || uid,
5335
+ documentId: newDocId,
5336
+ label: created?.title || created?.name || `New Entry #${newDocId}`,
5337
+ attribute: attrKey,
5338
+ oldValue: null,
5339
+ newValue: payload[attrKey],
5340
+ status: "created",
5341
+ message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
5342
+ });
5343
+ }
5344
+ } else {
5345
+ let existing = null;
5346
+ if (docId) {
5347
+ existing = await findExistingDocument(strapi, uid, docId, rowLocale);
5348
+ }
5349
+ if (existing) {
5350
+ const resolvedDocId = existing.documentId || docId;
5351
+ const mergedPayload = { ...payload };
5352
+ for (const key of Object.keys(payload)) {
5353
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5354
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
5355
+ }
5356
+ }
5357
+ const updateArgs = {
5358
+ documentId: resolvedDocId,
5359
+ data: mergedPayload
5360
+ };
5361
+ if (rowLocale) updateArgs.locale = rowLocale;
5362
+ await strapi.documents(uid).update(updateArgs);
5363
+ const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
5364
+ const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
5365
+ if (shouldPublish) {
5366
+ try {
5367
+ const pubArgs = { documentId: resolvedDocId };
5368
+ if (rowLocale) pubArgs.locale = rowLocale;
5369
+ await strapi.documents(uid).publish(pubArgs);
5370
+ } catch (pubErr) {
5371
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
5372
+ }
5373
+ }
5374
+ try {
5375
+ await recordContentHistoryVersion(strapi, {
5376
+ uid,
5377
+ documentId: resolvedDocId,
5378
+ userId: user?.id,
5379
+ statusOverride: shouldPublish ? "published" : isDraftAndPublish ? "draft" : void 0
5380
+ });
5381
+ } catch {
5382
+ }
5383
+ stats.updated++;
5384
+ for (const attrKey of Object.keys(payload)) {
5385
+ const oldVal = existing?.[attrKey] ?? null;
5386
+ const newVal = payload[attrKey];
5387
+ stats.diffs.push({
5388
+ uid,
5389
+ collectionName: ct.info?.displayName || uid,
5390
+ documentId: resolvedDocId,
5391
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5392
+ attribute: attrKey,
5393
+ oldValue: oldVal,
5394
+ newValue: newVal,
5395
+ status: "updated",
5396
+ message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
5397
+ });
5398
+ }
5399
+ continue;
5400
+ }
5401
+ const createArgs = { data: payload };
5402
+ if (rowLocale) createArgs.locale = rowLocale;
5403
+ const created = await strapi.documents(uid).create(createArgs);
5404
+ const newDocId = created?.documentId || docId;
5405
+ const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
5406
+ if (shouldPublishNew && newDocId) {
5407
+ try {
5408
+ const pubArgs = { documentId: newDocId };
5409
+ if (rowLocale) pubArgs.locale = rowLocale;
5410
+ await strapi.documents(uid).publish(pubArgs);
5411
+ } catch (pubErr) {
5412
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
5413
+ }
5414
+ }
5415
+ try {
5416
+ await recordContentHistoryVersion(strapi, {
5417
+ uid,
5418
+ documentId: newDocId,
5419
+ userId: user?.id,
5420
+ statusOverride: shouldPublishNew ? "published" : isDraftAndPublish ? "draft" : void 0
5421
+ });
5422
+ } catch {
5423
+ }
5424
+ stats.created++;
5425
+ if (!docId) {
5426
+ stats.newWithoutDocId++;
5427
+ }
5428
+ for (const attrKey of Object.keys(payload)) {
5429
+ stats.diffs.push({
5430
+ uid,
5431
+ collectionName: ct.info?.displayName || uid,
5432
+ documentId: newDocId,
5433
+ label: created?.title || created?.name || `New Entry #${newDocId}`,
5434
+ attribute: attrKey,
5435
+ oldValue: null,
5436
+ newValue: payload[attrKey],
5437
+ status: "created",
5438
+ 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})`
5439
+ });
5440
+ }
5441
+ }
5442
+ } catch (err) {
5443
+ stats.errors.push({
5444
+ row: rowNumber,
5445
+ documentId: docId || void 0,
5446
+ error: extractDetailedErrorMessage(err)
5447
+ });
5448
+ }
5449
+ }
5450
+ return stats;
5451
+ }
5452
+ async function recordCsvAuditLog(strapi, ct, uid, batchId, stats, user, publishOnApproval, publishStrategy, unreviewedConsent) {
5453
+ if (stats.updated === 0 && stats.created === 0) return;
5454
+ try {
5455
+ const auditService2 = strapi.plugin("bulk-updater").service("audit");
5456
+ if (!auditService2) return;
5457
+ const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
5458
+ if (!isEnabled) return;
5459
+ const userEmail = user?.email || "admin@strapi.io";
5460
+ const userName = user ? `${user.firstname || ""} ${user.lastname || ""}`.trim() : "CSV Importer";
5461
+ const collectionDisplayName = ct.info?.displayName || uid;
5462
+ const updatedDiffs = stats.diffs.filter(
5463
+ (d) => d.status === "updated" || d.status === "created"
5464
+ );
5465
+ const uniqueAttributes = Array.from(
5466
+ new Set(updatedDiffs.map((d) => d.attribute))
5467
+ ).filter(Boolean);
5468
+ const attrListStr = uniqueAttributes.length > 0 ? uniqueAttributes.join(", ") : "fields";
5469
+ const batchAttribute = `CSV [${collectionDisplayName}]: ${attrListStr}`;
5470
+ let batchValue = "";
5471
+ if (updatedDiffs.length === 1) {
5472
+ const d = updatedDiffs[0];
5473
+ const newStr = d.newValue !== null && d.newValue !== void 0 ? String(d.newValue) : "(empty)";
5474
+ batchValue = `"${d.label}": ${d.attribute} → "${newStr}"`;
5475
+ } else if (updatedDiffs.length > 1 && stats.updated === 1 && stats.created === 0) {
5476
+ const d = updatedDiffs[0];
5477
+ const changes = updatedDiffs.map((ud) => `${ud.attribute}: "${ud.newValue ?? ""}"`).join(", ");
5478
+ batchValue = `"${d.label}": ${changes}`;
5479
+ } else {
5480
+ const sampleVal = updatedDiffs[0] ? ` (e.g. ${updatedDiffs[0].attribute}: "${updatedDiffs[0].newValue ?? ""}")` : "";
5481
+ batchValue = `${stats.updated} updated, ${stats.created} created in ${collectionDisplayName}${sampleVal}`;
5482
+ }
5483
+ const batchSummary = {
5484
+ batchId,
5485
+ attribute: batchAttribute,
5486
+ value: batchValue,
5487
+ updatedCount: stats.updated + stats.created,
5488
+ skippedCount: stats.skipped,
5489
+ totalTargets: stats.total
5490
+ };
5491
+ const userObj = {
5492
+ email: userEmail || "admin@strapi.io",
5493
+ firstname: userName || "Admin"
5494
+ };
5495
+ if (typeof auditService2.recordBatch === "function") {
5496
+ await auditService2.recordBatch(
5497
+ batchSummary,
5498
+ stats.diffs,
5499
+ userObj,
5500
+ publishOnApproval,
5501
+ publishStrategy,
5502
+ unreviewedConsent
5503
+ );
5504
+ } else if (typeof auditService2.logOperation === "function") {
5505
+ await auditService2.logOperation({
5506
+ ...batchSummary,
5507
+ userEmail,
5508
+ userName,
5509
+ diffs: stats.diffs
5510
+ });
5511
+ }
5512
+ } catch (auditErr) {
5513
+ strapi.log.warn("[bulk-updater] Failed logging CSV import audit:", auditErr);
5514
+ }
5515
+ }
5516
+ const csvService = ({ strapi }) => ({
5517
+ /**
5518
+ * Generates a sample template CSV with schema attribute headers and sample values
5519
+ */
5520
+ async getTemplateCsv(uid) {
5521
+ const ct = strapi.contentTypes[uid];
5522
+ if (!ct) {
5523
+ throw new Error(`Content type ${uid} not found`);
5524
+ }
5525
+ const attrs = ct.attributes || {};
5526
+ const excluded = ["createdAt", "updatedAt", "publishedAt", "createdBy", "updatedBy", "localizations"];
5527
+ const headers = ["documentId"];
5528
+ const sampleRow = ["(Leave empty to create new, or enter existing docId)"];
5529
+ for (const [attrName, attrDef] of Object.entries(attrs)) {
5530
+ if (excluded.includes(attrName)) continue;
5531
+ if (attrDef.type === "relation") continue;
5532
+ if (attrDef.type === "component" || attrDef.type === "dynamiczone") continue;
5533
+ headers.push(attrName);
5534
+ let sampleVal = "";
5535
+ switch (attrDef.type) {
5536
+ case "string":
5537
+ case "text":
5538
+ case "richtext":
5539
+ sampleVal = `Sample ${attrName}`;
5540
+ break;
5541
+ case "integer":
5542
+ case "biginteger":
5543
+ case "float":
5544
+ case "decimal":
5545
+ sampleVal = "10";
5546
+ break;
5547
+ case "boolean":
5548
+ sampleVal = "true";
5549
+ break;
5550
+ case "email":
5551
+ sampleVal = "sample@example.com";
5552
+ break;
5553
+ case "enumeration":
5554
+ sampleVal = attrDef.enum?.[0] || "option1";
5555
+ break;
5556
+ case "customField":
5557
+ sampleVal = "value";
5558
+ break;
5559
+ default:
5560
+ sampleVal = "sample";
5561
+ }
5562
+ sampleRow.push(sampleVal);
5563
+ }
5564
+ const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
5565
+ return {
5566
+ filename: `${ct.info?.singularName || "template"}_template.csv`,
5567
+ csvContent,
5568
+ headers
5569
+ };
5570
+ },
5571
+ /**
5572
+ * Exports entries of a collection to CSV with deep nested component support,
5573
+ * RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
5574
+ */
5575
+ async exportCollectionCsv(uid, options = {}) {
5576
+ const ct = strapi.contentTypes[uid];
5577
+ if (!ct) {
5578
+ throw new Error(`Content type ${uid} not found`);
4636
5579
  }
4637
5580
  const allComponents = strapi.components || {};
4638
5581
  const fields = discoverExportFields(ct, allComponents);
@@ -4693,40 +5636,64 @@ const csvService = ({ strapi }) => ({
4693
5636
  }
4694
5637
  }
4695
5638
  const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
4696
- let entries = [];
5639
+ const lines = [];
5640
+ lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
5641
+ let totalExported = 0;
5642
+ const maxLimit = options.limit && options.limit > 0 ? options.limit : Infinity;
5643
+ const CHUNK_SIZE = 1e3;
4697
5644
  if (ct.kind === "singleType") {
4698
5645
  const findFirstArgs = {};
4699
5646
  if (deepPopulate) {
4700
5647
  findFirstArgs.populate = deepPopulate;
4701
5648
  }
4702
5649
  const doc = await strapi.documents(uid).findFirst(findFirstArgs);
4703
- entries = doc ? [doc] : [];
5650
+ if (doc) {
5651
+ const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(doc, f.key)));
5652
+ lines.push(rowValues.join(","));
5653
+ totalExported = 1;
5654
+ }
4704
5655
  } else {
4705
- const findManyArgs = {
4706
- filters: Object.keys(filters).length > 0 ? filters : void 0,
4707
- limit: options.limit || 5e3,
4708
- sort: { createdAt: "desc" }
4709
- };
4710
- if (deepPopulate) {
4711
- findManyArgs.populate = deepPopulate;
5656
+ let currentPage = 1;
5657
+ let hasMore = true;
5658
+ while (hasMore && totalExported < maxLimit) {
5659
+ const remaining = maxLimit - totalExported;
5660
+ const currentBatchLimit = Math.min(CHUNK_SIZE, remaining);
5661
+ const findPageArgs = {
5662
+ filters: Object.keys(filters).length > 0 ? filters : void 0,
5663
+ page: currentPage,
5664
+ pageSize: currentBatchLimit,
5665
+ sort: { createdAt: "desc" }
5666
+ };
5667
+ if (deepPopulate) {
5668
+ findPageArgs.populate = deepPopulate;
5669
+ }
5670
+ const chunkEntries = await strapi.documents(uid).findMany(findPageArgs);
5671
+ if (!chunkEntries || chunkEntries.length === 0) {
5672
+ hasMore = false;
5673
+ break;
5674
+ }
5675
+ for (const entry of chunkEntries) {
5676
+ const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(entry, f.key)));
5677
+ lines.push(rowValues.join(","));
5678
+ totalExported++;
5679
+ if (totalExported >= maxLimit) {
5680
+ hasMore = false;
5681
+ break;
5682
+ }
5683
+ }
5684
+ if (chunkEntries.length < currentBatchLimit) {
5685
+ hasMore = false;
5686
+ } else {
5687
+ currentPage++;
5688
+ }
4712
5689
  }
4713
- entries = await strapi.documents(uid).findMany(findManyArgs);
4714
- }
4715
- const lines = [];
4716
- lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
4717
- for (const entry of entries || []) {
4718
- const rowValues = fields.map((f) => {
4719
- const val = getNestedExportValue(entry, f.key);
4720
- return escapeCsvValue(val);
4721
- });
4722
- lines.push(rowValues.join(","));
4723
5690
  }
4724
5691
  const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
4725
5692
  const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
4726
5693
  return {
4727
5694
  filename,
4728
5695
  csvContent,
4729
- count: entries ? entries.length : 0,
5696
+ count: totalExported,
4730
5697
  fields: fields.map((f) => f.key)
4731
5698
  };
4732
5699
  },
@@ -4787,7 +5754,7 @@ const csvService = ({ strapi }) => ({
4787
5754
  const uids = Object.keys(byUid);
4788
5755
  if (uids.length === 1) {
4789
5756
  const uid = uids[0];
4790
- return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
5757
+ return await this.exportCollectionCsv(uid, { documentIds: byUid[uid], limit: 0 });
4791
5758
  }
4792
5759
  const rows = [];
4793
5760
  const headers = [
@@ -4838,7 +5805,89 @@ const csvService = ({ strapi }) => ({
4838
5805
  };
4839
5806
  },
4840
5807
  /**
4841
- * Imports CSV records to update existing records or insert/override records
5808
+ * Imports a single chunk of CSV rows (for large dataset streaming/batching).
5809
+ * Aggregates stats across chunks and writes unified audit trail on the final chunk.
5810
+ */
5811
+ async importChunk(uid, rows, options) {
5812
+ const ct = strapi.contentTypes[uid];
5813
+ if (!ct) {
5814
+ throw new Error(`Content type ${uid} not found`);
5815
+ }
5816
+ cleanupStaleSessions();
5817
+ const batchId = options.batchId || `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
5818
+ let session = csvImportSessions.get(batchId);
5819
+ if (!session || options.isFirstChunk) {
5820
+ session = {
5821
+ batchId,
5822
+ uid,
5823
+ createdAt: Date.now(),
5824
+ total: 0,
5825
+ created: 0,
5826
+ updated: 0,
5827
+ skipped: 0,
5828
+ newWithoutDocId: 0,
5829
+ errors: [],
5830
+ diffs: [],
5831
+ publishStrategy: options.publishStrategy,
5832
+ unreviewedConsent: options.unreviewedConsent
5833
+ };
5834
+ csvImportSessions.set(batchId, session);
5835
+ }
5836
+ const startRowNumber = options.startRowNumber || session.total + 2;
5837
+ const chunkStats = await processCsvRows(strapi, ct, uid, rows || [], startRowNumber, options);
5838
+ session.total += chunkStats.total;
5839
+ session.created += chunkStats.created;
5840
+ session.updated += chunkStats.updated;
5841
+ session.skipped += chunkStats.skipped;
5842
+ session.newWithoutDocId += chunkStats.newWithoutDocId;
5843
+ session.errors.push(...chunkStats.errors);
5844
+ session.diffs.push(...chunkStats.diffs);
5845
+ if (options.isLastChunk) {
5846
+ await recordCsvAuditLog(
5847
+ strapi,
5848
+ ct,
5849
+ uid,
5850
+ batchId,
5851
+ session,
5852
+ options.user,
5853
+ options.publish !== false,
5854
+ session.publishStrategy || options.publishStrategy,
5855
+ session.unreviewedConsent ?? options.unreviewedConsent
5856
+ );
5857
+ try {
5858
+ const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
5859
+ if (bulkService && bulkService.invalidateCollectionCache) {
5860
+ bulkService.invalidateCollectionCache(uid);
5861
+ }
5862
+ } catch {
5863
+ }
5864
+ const aggregateStats = {
5865
+ total: session.total,
5866
+ created: session.created,
5867
+ updated: session.updated,
5868
+ skipped: session.skipped,
5869
+ newWithoutDocId: session.newWithoutDocId,
5870
+ errors: session.errors,
5871
+ diffs: session.diffs
5872
+ };
5873
+ csvImportSessions.delete(batchId);
5874
+ return {
5875
+ success: true,
5876
+ batchId,
5877
+ chunkStats,
5878
+ stats: aggregateStats,
5879
+ isFinished: true
5880
+ };
5881
+ }
5882
+ return {
5883
+ success: true,
5884
+ batchId,
5885
+ chunkStats,
5886
+ isFinished: false
5887
+ };
5888
+ },
5889
+ /**
5890
+ * Imports CSV records to update existing records or insert/override records (single payload).
4842
5891
  */
4843
5892
  async importCsv(uid, csvText, options) {
4844
5893
  const ct = strapi.contentTypes[uid];
@@ -4849,560 +5898,521 @@ const csvService = ({ strapi }) => ({
4849
5898
  if (!parsedRows || parsedRows.length === 0) {
4850
5899
  throw new Error("CSV is empty or could not be parsed.");
4851
5900
  }
4852
- const attrs = ct.attributes || {};
4853
- const { mode = "auto", publish = true, user } = options;
4854
- const stats = {
4855
- total: parsedRows.length,
4856
- created: 0,
4857
- updated: 0,
4858
- skipped: 0,
4859
- newWithoutDocId: 0,
4860
- errors: [],
4861
- diffs: []
4862
- };
4863
5901
  const batchId = `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
4864
- const userEmail = user?.email || "admin@strapi.io";
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
- }
5902
+ const stats = await processCsvRows(strapi, ct, uid, parsedRows, 2, options);
5903
+ try {
5904
+ const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
5905
+ if (bulkService && bulkService.invalidateCollectionCache) {
5906
+ bulkService.invalidateCollectionCache(uid);
4886
5907
  }
4887
- return null;
5908
+ } catch {
5909
+ }
5910
+ await recordCsvAuditLog(
5911
+ strapi,
5912
+ ct,
5913
+ uid,
5914
+ batchId,
5915
+ stats,
5916
+ options.user,
5917
+ options.publish !== false,
5918
+ options.publishStrategy,
5919
+ options.unreviewedConsent
5920
+ );
5921
+ return {
5922
+ success: true,
5923
+ batchId,
5924
+ stats
4888
5925
  };
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
- ]);
4903
- for (let i = 0; i < parsedRows.length; i++) {
4904
- const rowNumber = i + 2;
4905
- const row = parsedRows[i];
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
- }
4919
- const payload = {};
4920
- for (const [key, rawVal] of Object.entries(row)) {
4921
- const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
4922
- if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
4923
- if (key.includes(".")) {
4924
- const [parent, child] = key.split(".");
4925
- const parentDef = attrs[parent];
4926
- if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
4927
- const comp = strapi.components[parentDef.component];
4928
- const childDef = comp?.attributes?.[child];
4929
- if (childDef) {
4930
- if (!payload[parent]) payload[parent] = {};
4931
- let val = rawVal;
4932
- if (rawVal === "" || rawVal === null || rawVal === void 0) {
4933
- val = null;
4934
- } else if (childDef.type === "integer" || childDef.type === "biginteger") {
4935
- const parsedInt = parseInt(rawVal, 10);
4936
- val = isNaN(parsedInt) ? null : parsedInt;
4937
- } else if (childDef.type === "float" || childDef.type === "decimal") {
4938
- const parsedFloat = parseFloat(rawVal);
4939
- val = isNaN(parsedFloat) ? null : parsedFloat;
4940
- } else if (childDef.type === "boolean") {
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
- }
4948
- }
4949
- payload[parent][child] = val;
4950
- }
4951
- }
4952
- continue;
4953
- }
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
- }
4963
- if (!attrDef) continue;
4964
- if (rawVal === "" || rawVal === null || rawVal === void 0) {
4965
- if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
4966
- payload[key] = null;
4967
- }
4968
- continue;
4969
- }
4970
- switch (attrDef.type) {
4971
- case "integer":
4972
- case "biginteger": {
4973
- const parsedInt = parseInt(rawVal, 10);
4974
- payload[key] = isNaN(parsedInt) ? null : parsedInt;
4975
- break;
4976
- }
4977
- case "float":
4978
- case "decimal": {
4979
- const parsedFloat = parseFloat(rawVal);
4980
- payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
4981
- break;
4982
- }
4983
- case "boolean":
4984
- payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
4985
- break;
4986
- case "json":
4987
- try {
4988
- payload[key] = JSON.parse(rawVal);
4989
- } catch {
4990
- payload[key] = rawVal;
4991
- }
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
- }
5056
- default:
5057
- payload[key] = rawVal;
5926
+ },
5927
+ /**
5928
+ * Executes a pending CSV batch approved by Checker or queued for off-peak execution
5929
+ */
5930
+ async executeCsvPendingBatch(uid, rows, batchId, options = {}, checkerUser) {
5931
+ const ct = strapi.contentTypes[uid];
5932
+ if (!ct) {
5933
+ throw new Error(`Content type ${uid} not found`);
5934
+ }
5935
+ const effectiveUser = checkerUser || options.user;
5936
+ const stats = await processCsvRows(strapi, ct, uid, rows || [], 2, {
5937
+ ...options,
5938
+ user: effectiveUser
5939
+ });
5940
+ await recordCsvAuditLog(
5941
+ strapi,
5942
+ ct,
5943
+ uid,
5944
+ batchId,
5945
+ stats,
5946
+ effectiveUser,
5947
+ options.publish !== false,
5948
+ options.publishStrategy,
5949
+ options.unreviewedConsent
5950
+ );
5951
+ try {
5952
+ const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
5953
+ if (bulkService && bulkService.invalidateCollectionCache) {
5954
+ bulkService.invalidateCollectionCache(uid);
5955
+ }
5956
+ } catch {
5957
+ }
5958
+ return {
5959
+ success: true,
5960
+ batchId,
5961
+ stats
5962
+ };
5963
+ },
5964
+ /**
5965
+ * Pre-Flight Dry-Run Validation: Non-mutating analysis verifying document existence,
5966
+ * data types, schema integrity, and quota rule violations before execution.
5967
+ */
5968
+ async validatePreflight(uid, rows, options = {}) {
5969
+ const ct = strapi.contentTypes[uid];
5970
+ if (!ct) {
5971
+ throw new Error(`Content type ${uid} not found`);
5972
+ }
5973
+ const { mode = "auto" } = options;
5974
+ const attrs = ct.attributes || {};
5975
+ let validationRulesService2 = null;
5976
+ let activeRules = [];
5977
+ try {
5978
+ validationRulesService2 = strapi.plugin("bulk-updater").service("validationRules");
5979
+ if (validationRulesService2) {
5980
+ const rulesRes = await validationRulesService2.getRules();
5981
+ activeRules = (rulesRes || []).filter((r) => r.enabled);
5982
+ }
5983
+ } catch {
5984
+ }
5985
+ let validRows = 0;
5986
+ let estimatedUpdates = 0;
5987
+ let estimatedCreates = 0;
5988
+ const isDraftAndPublish = Boolean(ct.options?.draftAndPublish);
5989
+ const lifecycleStats = {
5990
+ isDraftAndPublish,
5991
+ totalChecked: 0,
5992
+ published: 0,
5993
+ modified: 0,
5994
+ draft: 0,
5995
+ unreviewedCount: 0
5996
+ };
5997
+ const errors = [];
5998
+ const warnings = [];
5999
+ for (let i = 0; i < rows.length; i++) {
6000
+ const rowNumber = i + 2;
6001
+ const row = rows[i];
6002
+ const docId = extractDocId(row);
6003
+ const rowLocale = extractLocale(row);
6004
+ let rowHasError = false;
6005
+ let existing = null;
6006
+ if (docId) {
6007
+ existing = await findExistingDocument(strapi, uid, docId, rowLocale);
6008
+ }
6009
+ if (existing && isDraftAndPublish) {
6010
+ lifecycleStats.totalChecked++;
6011
+ const isApproved = Boolean(existing.publishedAt) && existing.status !== "modified";
6012
+ if (isApproved) {
6013
+ lifecycleStats.published++;
6014
+ } else if (existing.status === "modified") {
6015
+ lifecycleStats.modified++;
6016
+ lifecycleStats.unreviewedCount++;
6017
+ } else {
6018
+ lifecycleStats.draft++;
6019
+ lifecycleStats.unreviewedCount++;
5058
6020
  }
5059
6021
  }
5060
- try {
5061
- if (mode === "update") {
5062
- if (!docId) {
5063
- stats.skipped++;
5064
- stats.errors.push({
5065
- row: rowNumber,
5066
- error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
5067
- });
5068
- continue;
5069
- }
5070
- const existing = await findExistingDocument(docId, rowLocale);
5071
- if (!existing) {
5072
- stats.skipped++;
5073
- stats.errors.push({
6022
+ if (mode === "update") {
6023
+ if (!docId) {
6024
+ errors.push({
6025
+ row: rowNumber,
6026
+ error: "Missing or invalid documentId for Update Only mode."
6027
+ });
6028
+ rowHasError = true;
6029
+ } else if (!existing) {
6030
+ errors.push({
6031
+ row: rowNumber,
6032
+ documentId: docId,
6033
+ error: `Document with ID "${docId}" not found in database.`
6034
+ });
6035
+ rowHasError = true;
6036
+ } else {
6037
+ estimatedUpdates++;
6038
+ }
6039
+ } else if (mode === "create_or_override") {
6040
+ if (existing) {
6041
+ estimatedUpdates++;
6042
+ } else {
6043
+ estimatedCreates++;
6044
+ }
6045
+ } else {
6046
+ if (existing) {
6047
+ estimatedUpdates++;
6048
+ } else {
6049
+ estimatedCreates++;
6050
+ if (docId) {
6051
+ warnings.push({
5074
6052
  row: rowNumber,
5075
6053
  documentId: docId,
5076
- error: `Document with ID "${docId}" not found in database.`
5077
- });
5078
- continue;
5079
- }
5080
- const resolvedDocId = existing.documentId || docId;
5081
- const mergedPayload = { ...payload };
5082
- for (const key of Object.keys(payload)) {
5083
- if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5084
- mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
5085
- }
5086
- }
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,
5104
- data: mergedPayload
5105
- };
5106
- if (rowLocale) updateArgs.locale = rowLocale;
5107
- await strapi.documents(uid).update(updateArgs);
5108
- if (publish && ct.options?.draftAndPublish) {
5109
- try {
5110
- const pubArgs = { documentId: resolvedDocId };
5111
- if (rowLocale) pubArgs.locale = rowLocale;
5112
- await strapi.documents(uid).publish(pubArgs);
5113
- } catch (pubErr) {
5114
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
5115
- }
5116
- }
5117
- try {
5118
- await recordContentHistoryVersion(strapi, {
5119
- uid,
5120
- documentId: resolvedDocId,
5121
- userId: user?.id,
5122
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5123
- });
5124
- } catch {
5125
- }
5126
- stats.updated++;
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})`
6054
+ warning: `Document ID "${docId}" not found in DB; will create as a new entry.`
5140
6055
  });
5141
6056
  }
5142
- } else if (mode === "create_or_override") {
5143
- let existing = null;
5144
- if (docId) {
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] };
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) {
5162
- try {
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);
5168
- }
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
6057
+ }
6058
+ }
6059
+ for (const [colKey, rawVal] of Object.entries(row)) {
6060
+ if (!rawVal || rawVal.trim() === "") continue;
6061
+ const normalizedKey = colKey.toLowerCase().replace(/[\s_-]/g, "");
6062
+ if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
6063
+ let attrDef = attrs[colKey];
6064
+ if (!attrDef && !colKey.includes(".")) {
6065
+ const matchKey = Object.keys(attrs).find(
6066
+ (k) => k.toLowerCase() === colKey.toLowerCase() || k.toLowerCase() === colKey.toLowerCase().replace(/\s+/g, "")
6067
+ );
6068
+ if (matchKey) attrDef = attrs[matchKey];
6069
+ }
6070
+ if (attrDef) {
6071
+ if (attrDef.type === "integer" || attrDef.type === "biginteger") {
6072
+ const num = parseInt(rawVal, 10);
6073
+ if (isNaN(num)) {
6074
+ errors.push({
6075
+ row: rowNumber,
6076
+ documentId: docId || void 0,
6077
+ error: `Column "${colKey}" expects an integer, received: "${rawVal}"`
5176
6078
  });
5177
- } catch {
6079
+ rowHasError = true;
5178
6080
  }
5179
- stats.updated++;
5180
- for (const attrKey of Object.keys(payload)) {
5181
- const oldVal = existing?.[attrKey] ?? null;
5182
- const newVal = payload[attrKey];
5183
- stats.diffs.push({
5184
- uid,
5185
- collectionName: ct.info?.displayName || uid,
5186
- documentId: resolvedDocId,
5187
- label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5188
- attribute: attrKey,
5189
- oldValue: oldVal,
5190
- newValue: newVal,
5191
- status: "updated",
5192
- message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
6081
+ } else if (attrDef.type === "float" || attrDef.type === "decimal") {
6082
+ const num = parseFloat(rawVal);
6083
+ if (isNaN(num)) {
6084
+ errors.push({
6085
+ row: rowNumber,
6086
+ documentId: docId || void 0,
6087
+ error: `Column "${colKey}" expects a numeric float/decimal, received: "${rawVal}"`
5193
6088
  });
6089
+ rowHasError = true;
5194
6090
  }
5195
- continue;
5196
- }
5197
- const createArgs = { data: payload };
5198
- if (rowLocale) createArgs.locale = rowLocale;
5199
- const created = await strapi.documents(uid).create(createArgs);
5200
- const newDocId = created?.documentId || docId;
5201
- if (publish && ct.options?.draftAndPublish && newDocId) {
5202
- try {
5203
- const pubArgs = { documentId: newDocId };
5204
- if (rowLocale) pubArgs.locale = rowLocale;
5205
- await strapi.documents(uid).publish(pubArgs);
5206
- } catch (pubErr) {
5207
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
5208
- }
5209
- }
5210
- try {
5211
- await recordContentHistoryVersion(strapi, {
5212
- uid,
5213
- documentId: newDocId,
5214
- userId: user?.id,
5215
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5216
- });
5217
- } catch {
5218
- }
5219
- stats.created++;
5220
- if (!docId) {
5221
- stats.newWithoutDocId++;
5222
- }
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
- }
5236
- } else {
5237
- let existing = null;
5238
- if (docId) {
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] };
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) {
5256
- try {
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);
5262
- }
6091
+ } else if (attrDef.type === "enumeration" && Array.isArray(attrDef.enum)) {
6092
+ if (!attrDef.enum.includes(rawVal.trim())) {
6093
+ warnings.push({
6094
+ row: rowNumber,
6095
+ documentId: docId || void 0,
6096
+ warning: `Column "${colKey}" value "${rawVal}" is not in defined enum options: [${attrDef.enum.join(", ")}]`
6097
+ });
5263
6098
  }
6099
+ } else if (attrDef.type === "json") {
5264
6100
  try {
5265
- await recordContentHistoryVersion(strapi, {
5266
- uid,
5267
- documentId: resolvedDocId,
5268
- userId: user?.id,
5269
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5270
- });
6101
+ JSON.parse(rawVal);
5271
6102
  } catch {
5272
- }
5273
- stats.updated++;
5274
- for (const attrKey of Object.keys(payload)) {
5275
- const oldVal = existing?.[attrKey] ?? null;
5276
- const newVal = payload[attrKey];
5277
- stats.diffs.push({
5278
- uid,
5279
- collectionName: ct.info?.displayName || uid,
5280
- documentId: resolvedDocId,
5281
- label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5282
- attribute: attrKey,
5283
- oldValue: oldVal,
5284
- newValue: newVal,
5285
- status: "updated",
5286
- message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
6103
+ warnings.push({
6104
+ row: rowNumber,
6105
+ documentId: docId || void 0,
6106
+ warning: `Column "${colKey}" is type JSON but contains non-JSON text.`
5287
6107
  });
5288
6108
  }
5289
- continue;
5290
6109
  }
5291
- const createArgs = { data: payload };
5292
- if (rowLocale) createArgs.locale = rowLocale;
5293
- const created = await strapi.documents(uid).create(createArgs);
5294
- const newDocId = created?.documentId || docId;
5295
- if (publish && ct.options?.draftAndPublish && newDocId) {
5296
- try {
5297
- const pubArgs = { documentId: newDocId };
5298
- if (rowLocale) pubArgs.locale = rowLocale;
5299
- await strapi.documents(uid).publish(pubArgs);
5300
- } catch (pubErr) {
5301
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
6110
+ }
6111
+ if (activeRules.length > 0) {
6112
+ for (const rule of activeRules) {
6113
+ if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) continue;
6114
+ if (rule.includeRichTextHtml && (rawVal.includes("<h") || rawVal.includes("# "))) {
6115
+ const headings = extractHeadingsFromRichText(rawVal);
6116
+ for (const [tag, count] of Object.entries(headings)) {
6117
+ const max = rule.quotas?.[tag.toLowerCase()];
6118
+ if (max !== void 0 && count > max) {
6119
+ warnings.push({
6120
+ row: rowNumber,
6121
+ documentId: docId || void 0,
6122
+ warning: `Rule "${rule.name}" quota warning: column "${colKey}" contains ${count} "${tag.toUpperCase()}" headings (maximum allowed: ${max}).`
6123
+ });
6124
+ }
6125
+ }
5302
6126
  }
5303
6127
  }
5304
- try {
5305
- await recordContentHistoryVersion(strapi, {
5306
- uid,
5307
- documentId: newDocId,
5308
- userId: user?.id,
5309
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5310
- });
5311
- } catch {
5312
- }
5313
- stats.created++;
5314
- if (!docId) {
5315
- stats.newWithoutDocId++;
5316
- }
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
- }
5330
6128
  }
5331
- } catch (err) {
5332
- stats.errors.push({
5333
- row: rowNumber,
5334
- documentId: docId || void 0,
5335
- error: extractDetailedErrorMessage(err)
5336
- });
5337
6129
  }
5338
- }
5339
- try {
5340
- const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
5341
- if (bulkService && bulkService.invalidateCollectionCache) {
5342
- bulkService.invalidateCollectionCache(uid);
6130
+ if (!rowHasError) {
6131
+ validRows++;
5343
6132
  }
5344
- } catch {
5345
6133
  }
5346
- if (stats.updated > 0 || stats.created > 0) {
5347
- try {
5348
- const auditService2 = strapi.plugin("bulk-updater").service("audit");
5349
- if (auditService2) {
5350
- const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
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
- }
5374
- const batchSummary = {
5375
- batchId,
5376
- attribute: batchAttribute,
5377
- value: batchValue,
5378
- updatedCount: stats.updated + stats.created,
5379
- skippedCount: stats.skipped,
5380
- totalTargets: stats.total
5381
- };
5382
- const userObj = {
5383
- email: userEmail || "admin@strapi.io",
5384
- firstname: userName || "Admin"
5385
- };
5386
- if (typeof auditService2.recordBatch === "function") {
5387
- await auditService2.recordBatch(batchSummary, stats.diffs, userObj);
5388
- } else if (typeof auditService2.logOperation === "function") {
5389
- await auditService2.logOperation({
5390
- ...batchSummary,
5391
- userEmail,
5392
- userName,
5393
- diffs: stats.diffs
5394
- });
5395
- }
5396
- }
5397
- }
5398
- } catch (auditErr) {
5399
- strapi.log.warn("[bulk-updater] Failed logging CSV import audit:", auditErr);
5400
- }
6134
+ return {
6135
+ success: true,
6136
+ totalRows: rows.length,
6137
+ validRows,
6138
+ estimatedUpdates,
6139
+ estimatedCreates,
6140
+ lifecycleStats,
6141
+ hasErrors: errors.length > 0,
6142
+ hasWarnings: warnings.length > 0,
6143
+ errors: errors.slice(0, 100),
6144
+ warnings: warnings.slice(0, 100),
6145
+ totalErrorCount: errors.length,
6146
+ totalWarningCount: warnings.length
6147
+ };
6148
+ },
6149
+ /**
6150
+ * Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
6151
+ */
6152
+ async submitBatch(uid, rows, options = {}) {
6153
+ const ct = strapi.contentTypes[uid];
6154
+ if (!ct) {
6155
+ throw new Error(`Content type ${uid} not found`);
6156
+ }
6157
+ if (!rows || rows.length === 0) {
6158
+ throw new Error("No CSV records provided for batch submission.");
6159
+ }
6160
+ const {
6161
+ executionMode = "immediate",
6162
+ makerNotes = "",
6163
+ forceImmediate = false,
6164
+ mode = "auto",
6165
+ publish = true,
6166
+ publishStrategy = "safe",
6167
+ unreviewedConsent = false,
6168
+ unreviewedCount = 0,
6169
+ user
6170
+ } = options;
6171
+ const executionConfig = getExecutionWindowConfig(strapi);
6172
+ const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
6173
+ const collectionDisplayName = ct.info?.displayName || uid;
6174
+ if (executionMode === "maker_checker") {
6175
+ const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
6176
+ const batch = {
6177
+ id: batchId,
6178
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6179
+ createdBy: user ? {
6180
+ id: user.id,
6181
+ email: user.email,
6182
+ firstname: user.firstname,
6183
+ lastname: user.lastname,
6184
+ username: user.username
6185
+ } : { email: "admin@system.local" },
6186
+ payload: {
6187
+ action: "csv_import",
6188
+ uid,
6189
+ rows,
6190
+ options: {
6191
+ mode,
6192
+ publish,
6193
+ publishStrategy,
6194
+ unreviewedConsent,
6195
+ user
6196
+ },
6197
+ attribute: `CSV Import: ${collectionDisplayName}`,
6198
+ value: `${rows.length} rows (${mode} mode)`,
6199
+ publishOnApproval: publish,
6200
+ publishStrategy,
6201
+ unreviewedConsent,
6202
+ unreviewedCount
6203
+ },
6204
+ summary: {
6205
+ attribute: `CSV Import: ${collectionDisplayName}`,
6206
+ value: `${rows.length} rows (${mode} mode)`,
6207
+ totalTargets: rows.length,
6208
+ collectionBreakdown: { [collectionDisplayName]: rows.length },
6209
+ publishOnApproval: publish,
6210
+ publishStrategy,
6211
+ unreviewedConsent,
6212
+ unreviewedCount
6213
+ },
6214
+ status: "pending_approval",
6215
+ executionMode: "maker_checker",
6216
+ makerNotes,
6217
+ publishStrategy,
6218
+ unreviewedConsent,
6219
+ unreviewedCount
6220
+ };
6221
+ registerPendingBatch(batch, {
6222
+ id: batchId,
6223
+ createdAt: batch.createdAt,
6224
+ createdBy: batch.createdBy,
6225
+ status: "pending_approval",
6226
+ executionMode: "maker_checker",
6227
+ attribute: `CSV Import: ${collectionDisplayName}`,
6228
+ value: `${rows.length} rows (${mode} mode)`,
6229
+ totalTargets: rows.length,
6230
+ processedCount: 0,
6231
+ updatedCount: 0,
6232
+ skippedCount: 0,
6233
+ failedCount: 0,
6234
+ isLive: false,
6235
+ publishOnApproval: publish,
6236
+ publishStrategy,
6237
+ unreviewedConsent,
6238
+ unreviewedCount,
6239
+ makerNotes,
6240
+ results: []
6241
+ });
6242
+ return {
6243
+ success: true,
6244
+ pending: true,
6245
+ batchId,
6246
+ status: "pending_approval",
6247
+ message: "CSV batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
6248
+ batch
6249
+ };
6250
+ }
6251
+ if (executionMode === "queue_offpeak") {
6252
+ const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
6253
+ const batch = {
6254
+ id: batchId,
6255
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6256
+ createdBy: user ? {
6257
+ id: user.id,
6258
+ email: user.email,
6259
+ firstname: user.firstname,
6260
+ lastname: user.lastname,
6261
+ username: user.username
6262
+ } : { email: "admin@system.local" },
6263
+ payload: {
6264
+ action: "csv_import",
6265
+ uid,
6266
+ rows,
6267
+ options: {
6268
+ mode,
6269
+ publish,
6270
+ publishStrategy,
6271
+ unreviewedConsent,
6272
+ user
6273
+ },
6274
+ attribute: `CSV Import: ${collectionDisplayName}`,
6275
+ value: `${rows.length} rows (${mode} mode)`,
6276
+ publishOnApproval: publish,
6277
+ publishStrategy,
6278
+ unreviewedConsent,
6279
+ unreviewedCount
6280
+ },
6281
+ summary: {
6282
+ attribute: `CSV Import: ${collectionDisplayName}`,
6283
+ value: `${rows.length} rows (${mode} mode)`,
6284
+ totalTargets: rows.length,
6285
+ collectionBreakdown: { [collectionDisplayName]: rows.length },
6286
+ publishOnApproval: publish,
6287
+ publishStrategy,
6288
+ unreviewedConsent,
6289
+ unreviewedCount
6290
+ },
6291
+ status: "queued_offpeak",
6292
+ executionMode: "queue_offpeak",
6293
+ scheduledFor: executionConfig.offPeakStartTime,
6294
+ makerNotes,
6295
+ publishStrategy,
6296
+ unreviewedConsent,
6297
+ unreviewedCount
6298
+ };
6299
+ registerPendingBatch(batch, {
6300
+ id: batchId,
6301
+ createdAt: batch.createdAt,
6302
+ createdBy: batch.createdBy,
6303
+ status: "queued_offpeak",
6304
+ executionMode: "queue_offpeak",
6305
+ scheduledFor: executionConfig.offPeakStartTime,
6306
+ attribute: `CSV Import: ${collectionDisplayName}`,
6307
+ value: `${rows.length} rows (${mode} mode)`,
6308
+ totalTargets: rows.length,
6309
+ processedCount: 0,
6310
+ updatedCount: 0,
6311
+ skippedCount: 0,
6312
+ failedCount: 0,
6313
+ isLive: false,
6314
+ publishOnApproval: publish,
6315
+ publishStrategy,
6316
+ unreviewedConsent,
6317
+ unreviewedCount,
6318
+ makerNotes,
6319
+ results: []
6320
+ });
6321
+ return {
6322
+ success: true,
6323
+ pending: true,
6324
+ batchId,
6325
+ status: "queued_offpeak",
6326
+ scheduledFor: executionConfig.offPeakStartTime,
6327
+ message: `CSV batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
6328
+ batch
6329
+ };
6330
+ }
6331
+ if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
6332
+ const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
6333
+ const batch = {
6334
+ id: batchId,
6335
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6336
+ createdBy: user ? {
6337
+ id: user.id,
6338
+ email: user.email,
6339
+ firstname: user.firstname,
6340
+ lastname: user.lastname,
6341
+ username: user.username
6342
+ } : { email: "admin@system.local" },
6343
+ payload: {
6344
+ action: "csv_import",
6345
+ uid,
6346
+ rows,
6347
+ options: {
6348
+ mode,
6349
+ publish,
6350
+ publishStrategy,
6351
+ unreviewedConsent,
6352
+ user
6353
+ },
6354
+ attribute: `CSV Import: ${collectionDisplayName}`,
6355
+ value: `${rows.length} rows (${mode} mode)`,
6356
+ publishOnApproval: publish,
6357
+ publishStrategy,
6358
+ unreviewedConsent,
6359
+ unreviewedCount
6360
+ },
6361
+ summary: {
6362
+ attribute: `CSV Import: ${collectionDisplayName}`,
6363
+ value: `${rows.length} rows (${mode} mode)`,
6364
+ totalTargets: rows.length,
6365
+ collectionBreakdown: { [collectionDisplayName]: rows.length },
6366
+ publishOnApproval: publish,
6367
+ publishStrategy,
6368
+ unreviewedConsent,
6369
+ unreviewedCount
6370
+ },
6371
+ status: "queued_offpeak",
6372
+ executionMode: "queue_offpeak",
6373
+ scheduledFor: executionConfig.offPeakStartTime,
6374
+ makerNotes: "Automatically queued due to peak business hours restriction.",
6375
+ publishStrategy,
6376
+ unreviewedConsent,
6377
+ unreviewedCount
6378
+ };
6379
+ registerPendingBatch(batch, {
6380
+ id: batchId,
6381
+ createdAt: batch.createdAt,
6382
+ createdBy: batch.createdBy,
6383
+ status: "queued_offpeak",
6384
+ executionMode: "queue_offpeak",
6385
+ scheduledFor: executionConfig.offPeakStartTime,
6386
+ attribute: `CSV Import: ${collectionDisplayName}`,
6387
+ value: `${rows.length} rows (${mode} mode)`,
6388
+ totalTargets: rows.length,
6389
+ processedCount: 0,
6390
+ updatedCount: 0,
6391
+ skippedCount: 0,
6392
+ failedCount: 0,
6393
+ isLive: false,
6394
+ publishOnApproval: publish,
6395
+ publishStrategy,
6396
+ unreviewedConsent,
6397
+ unreviewedCount,
6398
+ policyMessage,
6399
+ makerNotes: "Automatically queued due to peak business hours restriction.",
6400
+ results: []
6401
+ });
6402
+ return {
6403
+ success: true,
6404
+ pending: true,
6405
+ batchId,
6406
+ status: "queued_offpeak",
6407
+ scheduledFor: executionConfig.offPeakStartTime,
6408
+ message: policyMessage,
6409
+ batch
6410
+ };
5401
6411
  }
5402
6412
  return {
5403
6413
  success: true,
5404
- batchId,
5405
- stats
6414
+ pending: false,
6415
+ executeNow: true
5406
6416
  };
5407
6417
  }
5408
6418
  });