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