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