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.
- package/dist/_chunks/{BulkUpdater-mSvmiLwc.js → BulkUpdater-C0uPoso2.js} +1899 -481
- package/dist/_chunks/BulkUpdater-C0uPoso2.js.map +1 -0
- package/dist/_chunks/{BulkUpdater-CbQk_OQG.mjs → BulkUpdater-CHdA0546.mjs} +1902 -484
- package/dist/_chunks/BulkUpdater-CHdA0546.mjs.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +1687 -677
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1687 -677
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/_chunks/BulkUpdater-CbQk_OQG.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-mSvmiLwc.js.map +0 -1
package/dist/server/index.mjs
CHANGED
|
@@ -211,6 +211,30 @@ const routes = {
|
|
|
211
211
|
config: {
|
|
212
212
|
policies: []
|
|
213
213
|
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
method: "POST",
|
|
217
|
+
path: "/csv/import-chunk",
|
|
218
|
+
handler: "csv.importChunk",
|
|
219
|
+
config: {
|
|
220
|
+
policies: []
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
method: "POST",
|
|
225
|
+
path: "/csv/validate-preflight",
|
|
226
|
+
handler: "csv.validatePreflight",
|
|
227
|
+
config: {
|
|
228
|
+
policies: []
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
method: "POST",
|
|
233
|
+
path: "/csv/submit-batch",
|
|
234
|
+
handler: "csv.submitBatch",
|
|
235
|
+
config: {
|
|
236
|
+
policies: []
|
|
237
|
+
}
|
|
214
238
|
}
|
|
215
239
|
]
|
|
216
240
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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(
|
|
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
|
|
2107
|
-
if (
|
|
2108
|
-
|
|
2309
|
+
const resolvedField = resolveContentTypeField(contentType, targetFilterAttr);
|
|
2310
|
+
if (resolvedField) {
|
|
2311
|
+
resolvedFilterAttr = resolvedField;
|
|
2109
2312
|
colHasAttribute = true;
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
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[
|
|
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 (
|
|
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
|
-
{ [
|
|
2182
|
-
{ [
|
|
2423
|
+
{ [resolvedFilterAttr]: { $null: true } },
|
|
2424
|
+
{ [resolvedFilterAttr]: { $eq: "" } }
|
|
2183
2425
|
];
|
|
2184
2426
|
} else if (normOp === "notempty" && isStringLike) {
|
|
2185
|
-
filters[
|
|
2427
|
+
filters[resolvedFilterAttr] = { $notNull: true, $ne: "" };
|
|
2186
2428
|
} else {
|
|
2187
|
-
filters[
|
|
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
|
|
2247
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
2271
|
-
if (
|
|
2272
|
-
|
|
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(
|
|
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(
|
|
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, {
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
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
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
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
|
-
|
|
5078
|
+
case "decimal": {
|
|
5079
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5080
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4600
5081
|
break;
|
|
5082
|
+
}
|
|
4601
5083
|
case "boolean":
|
|
4602
|
-
|
|
5084
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4603
5085
|
break;
|
|
4604
|
-
case "
|
|
4605
|
-
|
|
5086
|
+
case "json":
|
|
5087
|
+
try {
|
|
5088
|
+
payload[key] = JSON.parse(rawVal);
|
|
5089
|
+
} catch {
|
|
5090
|
+
payload[key] = rawVal;
|
|
5091
|
+
}
|
|
4606
5092
|
break;
|
|
4607
|
-
case "
|
|
4608
|
-
|
|
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
|
-
|
|
4611
|
-
|
|
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
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
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:
|
|
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
|
|
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
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
if (
|
|
4865
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
}
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
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
|
-
|
|
5058
|
-
if (
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
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
|
-
|
|
6076
|
+
rowHasError = true;
|
|
5175
6077
|
}
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
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
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
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
|
-
|
|
5289
|
-
|
|
5290
|
-
const
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
const
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
}
|
|
5395
|
-
|
|
5396
|
-
|
|
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
|
-
|
|
5402
|
-
|
|
6411
|
+
pending: false,
|
|
6412
|
+
executeNow: true
|
|
5403
6413
|
};
|
|
5404
6414
|
}
|
|
5405
6415
|
});
|