strapi-plugin-bulk-updater 1.2.3 → 1.2.7
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-BdRJhVFh.js → BulkUpdater-2TTEWMXI.js} +1170 -163
- package/dist/_chunks/BulkUpdater-2TTEWMXI.js.map +1 -0
- package/dist/_chunks/{BulkUpdater-BYtO2SHc.mjs → BulkUpdater-DR8Z9z95.mjs} +1172 -165
- package/dist/_chunks/BulkUpdater-DR8Z9z95.mjs.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +612 -104
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +612 -104
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/_chunks/BulkUpdater-BYtO2SHc.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-BdRJhVFh.js.map +0 -1
package/dist/server/index.mjs
CHANGED
|
@@ -78,6 +78,30 @@ const routes = {
|
|
|
78
78
|
policies: []
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
method: "GET",
|
|
83
|
+
path: "/batches/recent",
|
|
84
|
+
handler: "bulkUpdater.getRecentBatches",
|
|
85
|
+
config: {
|
|
86
|
+
policies: []
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
method: "GET",
|
|
91
|
+
path: "/batches/:batchId/status",
|
|
92
|
+
handler: "bulkUpdater.getBatchStatus",
|
|
93
|
+
config: {
|
|
94
|
+
policies: []
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
method: "GET",
|
|
99
|
+
path: "/batches/:batchId/report",
|
|
100
|
+
handler: "bulkUpdater.getBatchReport",
|
|
101
|
+
config: {
|
|
102
|
+
policies: []
|
|
103
|
+
}
|
|
104
|
+
},
|
|
81
105
|
{
|
|
82
106
|
method: "GET",
|
|
83
107
|
path: "/audit/history",
|
|
@@ -363,6 +387,39 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
363
387
|
ctx.status = 400;
|
|
364
388
|
ctx.body = { success: false, error: err.message };
|
|
365
389
|
}
|
|
390
|
+
},
|
|
391
|
+
async getBatchStatus(ctx) {
|
|
392
|
+
try {
|
|
393
|
+
const { batchId } = ctx.params;
|
|
394
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
395
|
+
const result = await service.getBatchStatus(batchId);
|
|
396
|
+
ctx.body = result;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
ctx.status = 404;
|
|
399
|
+
ctx.body = { success: false, error: err.message };
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
async getBatchReport(ctx) {
|
|
403
|
+
try {
|
|
404
|
+
const { batchId } = ctx.params;
|
|
405
|
+
const { format = "csv" } = ctx.query;
|
|
406
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
407
|
+
const result = await service.getBatchReport(batchId, format);
|
|
408
|
+
ctx.body = result;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
ctx.status = 500;
|
|
411
|
+
ctx.body = { success: false, error: err.message };
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
async getRecentBatches(ctx) {
|
|
415
|
+
try {
|
|
416
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
417
|
+
const result = await service.getRecentBatches();
|
|
418
|
+
ctx.body = result;
|
|
419
|
+
} catch (err) {
|
|
420
|
+
ctx.status = 500;
|
|
421
|
+
ctx.body = { success: false, error: err.message };
|
|
422
|
+
}
|
|
366
423
|
}
|
|
367
424
|
});
|
|
368
425
|
const auditController = ({ strapi: strapi2 }) => ({
|
|
@@ -535,7 +592,7 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
535
592
|
}
|
|
536
593
|
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
537
594
|
const result = await csvService2.importCsv(uid, csvText, {
|
|
538
|
-
mode: mode2 === "create_or_override" ? "create_or_override" : "
|
|
595
|
+
mode: mode2 === "update" ? "update" : mode2 === "create_or_override" ? "create_or_override" : "auto",
|
|
539
596
|
publish: publish !== false,
|
|
540
597
|
user: ctx.state.user
|
|
541
598
|
});
|
|
@@ -608,6 +665,7 @@ function checkBusinessHours(config, testDate) {
|
|
|
608
665
|
};
|
|
609
666
|
}
|
|
610
667
|
const pendingBatches = /* @__PURE__ */ new Map();
|
|
668
|
+
const batchJobsMap = /* @__PURE__ */ new Map();
|
|
611
669
|
let isRunnerRunning = false;
|
|
612
670
|
let runnerInterval = null;
|
|
613
671
|
function invalidateCollectionCache(uid) {
|
|
@@ -707,6 +765,31 @@ async function executeBatchInternal(strapi2, {
|
|
|
707
765
|
const results = [];
|
|
708
766
|
let updatedCount = 0;
|
|
709
767
|
let skippedCount = 0;
|
|
768
|
+
const jobState = {
|
|
769
|
+
id: batchId,
|
|
770
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
771
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
772
|
+
createdBy: currentUser ? {
|
|
773
|
+
id: currentUser.id,
|
|
774
|
+
email: currentUser.email,
|
|
775
|
+
firstname: currentUser.firstname,
|
|
776
|
+
lastname: currentUser.lastname,
|
|
777
|
+
username: currentUser.username
|
|
778
|
+
} : { email: "admin@system.local" },
|
|
779
|
+
status: "processing",
|
|
780
|
+
executionMode: "immediate",
|
|
781
|
+
attribute,
|
|
782
|
+
value,
|
|
783
|
+
totalTargets: targetsMap.size,
|
|
784
|
+
processedCount: 0,
|
|
785
|
+
updatedCount: 0,
|
|
786
|
+
skippedCount: 0,
|
|
787
|
+
failedCount: 0,
|
|
788
|
+
isLive: false,
|
|
789
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
790
|
+
results: []
|
|
791
|
+
};
|
|
792
|
+
batchJobsMap.set(batchId, jobState);
|
|
710
793
|
let regexObj = null;
|
|
711
794
|
if (isRegex && regexPattern) {
|
|
712
795
|
regexObj = new RegExp(regexPattern, "g");
|
|
@@ -941,11 +1024,24 @@ async function executeBatchInternal(strapi2, {
|
|
|
941
1024
|
else if (res.status === "skipped") skippedCount++;
|
|
942
1025
|
results.push(res);
|
|
943
1026
|
}
|
|
1027
|
+
jobState.processedCount = results.length;
|
|
1028
|
+
jobState.updatedCount = updatedCount;
|
|
1029
|
+
jobState.skippedCount = skippedCount;
|
|
1030
|
+
jobState.failedCount = results.filter((r) => r.status === "error").length;
|
|
944
1031
|
}
|
|
945
1032
|
const affectedUids = new Set(targets.map((t) => t.uid));
|
|
946
1033
|
affectedUids.forEach((uid) => {
|
|
947
1034
|
invalidateCollectionCache(uid);
|
|
948
1035
|
});
|
|
1036
|
+
const failedCount = results.filter((r) => r.status === "error").length;
|
|
1037
|
+
jobState.failedCount = failedCount;
|
|
1038
|
+
jobState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1039
|
+
jobState.isLive = updatedCount > 0;
|
|
1040
|
+
jobState.status = failedCount === 0 && updatedCount > 0 ? "successful" : failedCount > 0 && updatedCount > 0 ? "partial_failure" : failedCount > 0 ? "failed" : "successful";
|
|
1041
|
+
jobState.results = results.map((r) => ({
|
|
1042
|
+
...r,
|
|
1043
|
+
isLive: r.status === "updated"
|
|
1044
|
+
}));
|
|
949
1045
|
const batchSummary = {
|
|
950
1046
|
batchId,
|
|
951
1047
|
attribute,
|
|
@@ -967,7 +1063,10 @@ async function executeBatchInternal(strapi2, {
|
|
|
967
1063
|
value,
|
|
968
1064
|
updatedCount,
|
|
969
1065
|
skippedCount,
|
|
1066
|
+
failedCount,
|
|
970
1067
|
totalTargets: targetsMap.size,
|
|
1068
|
+
isLive: jobState.isLive,
|
|
1069
|
+
status: jobState.status,
|
|
971
1070
|
results
|
|
972
1071
|
};
|
|
973
1072
|
}
|
|
@@ -1095,69 +1194,87 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1095
1194
|
}
|
|
1096
1195
|
const collections = [];
|
|
1097
1196
|
const signatures = [];
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
populate: populateOption
|
|
1197
|
+
const BATCH_CONCURRENCY = 10;
|
|
1198
|
+
const collectionResults = [];
|
|
1199
|
+
for (let i = 0; i < staticSchemaCache.targetUids.length; i += BATCH_CONCURRENCY) {
|
|
1200
|
+
const batchUids = staticSchemaCache.targetUids.slice(i, i + BATCH_CONCURRENCY);
|
|
1201
|
+
const batchRes = await Promise.all(
|
|
1202
|
+
batchUids.map(async (uid) => {
|
|
1203
|
+
const meta = staticSchemaCache.metaByUid[uid];
|
|
1204
|
+
if (!meta) return null;
|
|
1205
|
+
const signature = await getCollectionSignature(strapi2, uid);
|
|
1206
|
+
const cached = modelsCache.get(uid);
|
|
1207
|
+
if (cached && cached.signature === signature) {
|
|
1208
|
+
return {
|
|
1209
|
+
collection: {
|
|
1210
|
+
...meta,
|
|
1211
|
+
totalEntries: cached.totalCount,
|
|
1212
|
+
entries: cached.entries
|
|
1213
|
+
},
|
|
1214
|
+
signature: `${uid}=${signature}`
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
let entries = [];
|
|
1218
|
+
let totalCount = 0;
|
|
1219
|
+
try {
|
|
1220
|
+
const ctAttrs = allContentTypes[uid]?.attributes || {};
|
|
1221
|
+
const relKeys = Object.keys(meta.relations || {});
|
|
1222
|
+
const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
|
|
1223
|
+
const populateOptionObj = {};
|
|
1224
|
+
relKeys.forEach((k) => {
|
|
1225
|
+
populateOptionObj[k] = true;
|
|
1128
1226
|
});
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
totalCount = 1;
|
|
1132
|
-
}
|
|
1133
|
-
} else {
|
|
1134
|
-
totalCount = await strapi2.documents(uid).count();
|
|
1135
|
-
entries = await strapi2.documents(uid).findMany({
|
|
1136
|
-
limit: 10,
|
|
1137
|
-
sort: { createdAt: "desc" },
|
|
1138
|
-
populate: populateOption
|
|
1227
|
+
compKeys.forEach((k) => {
|
|
1228
|
+
populateOptionObj[k] = true;
|
|
1139
1229
|
});
|
|
1230
|
+
const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
|
|
1231
|
+
if (meta.isSingleType) {
|
|
1232
|
+
const singleDoc = await strapi2.documents(uid).findFirst({
|
|
1233
|
+
populate: populateOption
|
|
1234
|
+
});
|
|
1235
|
+
if (singleDoc) {
|
|
1236
|
+
entries = [singleDoc];
|
|
1237
|
+
totalCount = 1;
|
|
1238
|
+
}
|
|
1239
|
+
} else {
|
|
1240
|
+
totalCount = await strapi2.documents(uid).count();
|
|
1241
|
+
entries = await strapi2.documents(uid).findMany({
|
|
1242
|
+
limit: 10,
|
|
1243
|
+
sort: { createdAt: "desc" },
|
|
1244
|
+
populate: populateOption
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
strapi2.log.error(`[bulk-updater] Failed to fetch entries for ${uid}:`, err);
|
|
1249
|
+
entries = [];
|
|
1250
|
+
totalCount = 0;
|
|
1140
1251
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1252
|
+
const formattedEntries = (entries || []).map((entry) => ({
|
|
1253
|
+
id: entry.id,
|
|
1254
|
+
documentId: entry.documentId,
|
|
1255
|
+
...entry
|
|
1256
|
+
}));
|
|
1257
|
+
modelsCache.set(uid, {
|
|
1258
|
+
signature,
|
|
1259
|
+
totalCount,
|
|
1260
|
+
entries: formattedEntries
|
|
1261
|
+
});
|
|
1262
|
+
return {
|
|
1263
|
+
collection: {
|
|
1264
|
+
...meta,
|
|
1265
|
+
totalEntries: totalCount,
|
|
1266
|
+
entries: formattedEntries
|
|
1267
|
+
},
|
|
1268
|
+
signature: `${uid}=${signature}`
|
|
1269
|
+
};
|
|
1270
|
+
})
|
|
1271
|
+
);
|
|
1272
|
+
collectionResults.push(...batchRes);
|
|
1273
|
+
}
|
|
1274
|
+
for (const res of collectionResults) {
|
|
1275
|
+
if (res) {
|
|
1276
|
+
collections.push(res.collection);
|
|
1277
|
+
signatures.push(res.signature);
|
|
1161
1278
|
}
|
|
1162
1279
|
}
|
|
1163
1280
|
const executionConfig = getExecutionWindowConfig(strapi2);
|
|
@@ -1476,6 +1593,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1476
1593
|
makerNotes
|
|
1477
1594
|
};
|
|
1478
1595
|
pendingBatches.set(batchId, batch);
|
|
1596
|
+
batchJobsMap.set(batchId, {
|
|
1597
|
+
id: batchId,
|
|
1598
|
+
createdAt: batch.createdAt,
|
|
1599
|
+
createdBy: batch.createdBy,
|
|
1600
|
+
status: "pending_approval",
|
|
1601
|
+
executionMode: "maker_checker",
|
|
1602
|
+
attribute,
|
|
1603
|
+
value,
|
|
1604
|
+
totalTargets: summary.totalTargets,
|
|
1605
|
+
processedCount: 0,
|
|
1606
|
+
updatedCount: 0,
|
|
1607
|
+
skippedCount: 0,
|
|
1608
|
+
failedCount: 0,
|
|
1609
|
+
isLive: false,
|
|
1610
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
1611
|
+
makerNotes,
|
|
1612
|
+
results: []
|
|
1613
|
+
});
|
|
1479
1614
|
return {
|
|
1480
1615
|
success: true,
|
|
1481
1616
|
pending: true,
|
|
@@ -1515,6 +1650,25 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1515
1650
|
makerNotes
|
|
1516
1651
|
};
|
|
1517
1652
|
pendingBatches.set(batchId, batch);
|
|
1653
|
+
batchJobsMap.set(batchId, {
|
|
1654
|
+
id: batchId,
|
|
1655
|
+
createdAt: batch.createdAt,
|
|
1656
|
+
createdBy: batch.createdBy,
|
|
1657
|
+
status: "queued_offpeak",
|
|
1658
|
+
executionMode: "queue_offpeak",
|
|
1659
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
1660
|
+
attribute,
|
|
1661
|
+
value,
|
|
1662
|
+
totalTargets: summary.totalTargets,
|
|
1663
|
+
processedCount: 0,
|
|
1664
|
+
updatedCount: 0,
|
|
1665
|
+
skippedCount: 0,
|
|
1666
|
+
failedCount: 0,
|
|
1667
|
+
isLive: false,
|
|
1668
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
1669
|
+
makerNotes,
|
|
1670
|
+
results: []
|
|
1671
|
+
});
|
|
1518
1672
|
return {
|
|
1519
1673
|
success: true,
|
|
1520
1674
|
pending: true,
|
|
@@ -1555,6 +1709,26 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1555
1709
|
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
1556
1710
|
};
|
|
1557
1711
|
pendingBatches.set(batchId, batch);
|
|
1712
|
+
batchJobsMap.set(batchId, {
|
|
1713
|
+
id: batchId,
|
|
1714
|
+
createdAt: batch.createdAt,
|
|
1715
|
+
createdBy: batch.createdBy,
|
|
1716
|
+
status: "queued_offpeak",
|
|
1717
|
+
executionMode: "queue_offpeak",
|
|
1718
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
1719
|
+
attribute,
|
|
1720
|
+
value,
|
|
1721
|
+
totalTargets: summary.totalTargets,
|
|
1722
|
+
processedCount: 0,
|
|
1723
|
+
updatedCount: 0,
|
|
1724
|
+
skippedCount: 0,
|
|
1725
|
+
failedCount: 0,
|
|
1726
|
+
isLive: false,
|
|
1727
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
1728
|
+
policyMessage,
|
|
1729
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
1730
|
+
results: []
|
|
1731
|
+
});
|
|
1558
1732
|
return {
|
|
1559
1733
|
success: true,
|
|
1560
1734
|
pending: true,
|
|
@@ -1687,6 +1861,226 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1687
1861
|
result
|
|
1688
1862
|
};
|
|
1689
1863
|
},
|
|
1864
|
+
/**
|
|
1865
|
+
* Get real-time or historical processing status of a batch job
|
|
1866
|
+
*/
|
|
1867
|
+
async getBatchStatus(batchId) {
|
|
1868
|
+
if (!batchId) throw new Error("batchId is required");
|
|
1869
|
+
let job = batchJobsMap.get(batchId);
|
|
1870
|
+
if (job) {
|
|
1871
|
+
return { success: true, batch: job };
|
|
1872
|
+
}
|
|
1873
|
+
const pending = pendingBatches.get(batchId);
|
|
1874
|
+
if (pending) {
|
|
1875
|
+
const pendingJob = {
|
|
1876
|
+
id: pending.id,
|
|
1877
|
+
createdAt: pending.createdAt,
|
|
1878
|
+
createdBy: pending.createdBy,
|
|
1879
|
+
status: pending.status,
|
|
1880
|
+
executionMode: pending.executionMode,
|
|
1881
|
+
scheduledFor: pending.scheduledFor,
|
|
1882
|
+
attribute: pending.payload.attribute,
|
|
1883
|
+
value: pending.payload.value,
|
|
1884
|
+
totalTargets: pending.summary.totalTargets,
|
|
1885
|
+
processedCount: 0,
|
|
1886
|
+
updatedCount: 0,
|
|
1887
|
+
skippedCount: 0,
|
|
1888
|
+
failedCount: 0,
|
|
1889
|
+
isLive: false,
|
|
1890
|
+
publishOnApproval: Boolean(pending.payload.publishOnApproval),
|
|
1891
|
+
policyMessage: pending.makerNotes,
|
|
1892
|
+
results: []
|
|
1893
|
+
};
|
|
1894
|
+
return { success: true, batch: pendingJob };
|
|
1895
|
+
}
|
|
1896
|
+
try {
|
|
1897
|
+
const auditService2 = strapi2.plugin("bulk-updater").service("audit");
|
|
1898
|
+
const historyData = await auditService2.getHistory();
|
|
1899
|
+
const records = Array.isArray(historyData?.history) ? historyData.history : [];
|
|
1900
|
+
const found = records.find((r) => r.batchId === batchId);
|
|
1901
|
+
if (found) {
|
|
1902
|
+
const total = found.totalTargets || found.updatedCount + found.skippedCount || 1;
|
|
1903
|
+
const historicalJob = {
|
|
1904
|
+
id: found.batchId,
|
|
1905
|
+
createdAt: found.executedAt || found.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1906
|
+
startedAt: found.executedAt || found.createdAt,
|
|
1907
|
+
completedAt: found.executedAt || found.createdAt,
|
|
1908
|
+
createdBy: {
|
|
1909
|
+
email: found.userEmail || "admin@strapi.io",
|
|
1910
|
+
firstname: found.userName || "Admin"
|
|
1911
|
+
},
|
|
1912
|
+
status: found.isRolledBack ? "failed" : "successful",
|
|
1913
|
+
executionMode: "immediate",
|
|
1914
|
+
attribute: found.attribute,
|
|
1915
|
+
value: found.value,
|
|
1916
|
+
totalTargets: total,
|
|
1917
|
+
processedCount: total,
|
|
1918
|
+
updatedCount: found.updatedCount || 0,
|
|
1919
|
+
skippedCount: found.skippedCount || 0,
|
|
1920
|
+
failedCount: 0,
|
|
1921
|
+
isLive: !found.isRolledBack && found.updatedCount > 0,
|
|
1922
|
+
publishOnApproval: true,
|
|
1923
|
+
results: (found.diffs || []).map((d) => ({
|
|
1924
|
+
uid: d.uid,
|
|
1925
|
+
collectionName: d.collectionName || d.uid,
|
|
1926
|
+
documentId: d.documentId,
|
|
1927
|
+
label: d.label || `Entry #${d.documentId}`,
|
|
1928
|
+
attribute: d.attribute || found.attribute,
|
|
1929
|
+
oldValue: d.oldValue,
|
|
1930
|
+
newValue: d.newValue,
|
|
1931
|
+
status: d.status || "updated",
|
|
1932
|
+
isLive: !found.isRolledBack,
|
|
1933
|
+
message: d.message || "Updated via bulk update"
|
|
1934
|
+
}))
|
|
1935
|
+
};
|
|
1936
|
+
batchJobsMap.set(batchId, historicalJob);
|
|
1937
|
+
return { success: true, batch: historicalJob };
|
|
1938
|
+
}
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
}
|
|
1941
|
+
throw new Error(`Batch with ID "${batchId}" was not found.`);
|
|
1942
|
+
},
|
|
1943
|
+
/**
|
|
1944
|
+
* Downloadable execution report (CSV or JSON)
|
|
1945
|
+
*/
|
|
1946
|
+
async getBatchReport(batchId, format = "csv") {
|
|
1947
|
+
const statusRes = await this.getBatchStatus(batchId);
|
|
1948
|
+
const batch = statusRes.batch;
|
|
1949
|
+
function escapeCsvVal(val) {
|
|
1950
|
+
if (val === null || val === void 0) return "";
|
|
1951
|
+
let str2 = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
1952
|
+
if (str2.includes(",") || str2.includes('"') || str2.includes("\n") || str2.includes("\r")) {
|
|
1953
|
+
return `"${str2.replace(/"/g, '""')}"`;
|
|
1954
|
+
}
|
|
1955
|
+
return str2;
|
|
1956
|
+
}
|
|
1957
|
+
const headers = [
|
|
1958
|
+
"Batch ID",
|
|
1959
|
+
"Collection UID",
|
|
1960
|
+
"Collection Name",
|
|
1961
|
+
"Document ID",
|
|
1962
|
+
"Record Title / Identifier",
|
|
1963
|
+
"Attribute Name",
|
|
1964
|
+
"Previous Value",
|
|
1965
|
+
"New Live Value",
|
|
1966
|
+
"Processing Status",
|
|
1967
|
+
"Live On Server",
|
|
1968
|
+
"Details / Message",
|
|
1969
|
+
"Executed At",
|
|
1970
|
+
"Operator"
|
|
1971
|
+
];
|
|
1972
|
+
const rows = [headers.join(",")];
|
|
1973
|
+
for (const item of batch.results || []) {
|
|
1974
|
+
const row = [
|
|
1975
|
+
escapeCsvVal(batch.id),
|
|
1976
|
+
escapeCsvVal(item.uid),
|
|
1977
|
+
escapeCsvVal(item.collectionName),
|
|
1978
|
+
escapeCsvVal(item.documentId),
|
|
1979
|
+
escapeCsvVal(item.label),
|
|
1980
|
+
escapeCsvVal(item.attribute),
|
|
1981
|
+
escapeCsvVal(item.oldValue),
|
|
1982
|
+
escapeCsvVal(item.newValue),
|
|
1983
|
+
escapeCsvVal(item.status === "updated" ? "Updated" : item.status === "error" ? "Failed" : "Skipped"),
|
|
1984
|
+
escapeCsvVal(item.isLive ? "YES (Live on Site)" : "NO"),
|
|
1985
|
+
escapeCsvVal(item.message),
|
|
1986
|
+
escapeCsvVal(batch.completedAt || batch.createdAt),
|
|
1987
|
+
escapeCsvVal(batch.createdBy?.email || "admin@strapi.io")
|
|
1988
|
+
];
|
|
1989
|
+
rows.push(row.join(","));
|
|
1990
|
+
}
|
|
1991
|
+
const safeBatchId = batch.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1992
|
+
const filename = `bulk_update_report_${safeBatchId}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
1993
|
+
return {
|
|
1994
|
+
success: true,
|
|
1995
|
+
filename,
|
|
1996
|
+
csvContent: rows.join("\n"),
|
|
1997
|
+
summary: {
|
|
1998
|
+
batchId: batch.id,
|
|
1999
|
+
status: batch.status,
|
|
2000
|
+
isLive: batch.isLive,
|
|
2001
|
+
totalTargets: batch.totalTargets,
|
|
2002
|
+
updatedCount: batch.updatedCount,
|
|
2003
|
+
skippedCount: batch.skippedCount,
|
|
2004
|
+
failedCount: batch.failedCount,
|
|
2005
|
+
executedAt: batch.completedAt || batch.createdAt,
|
|
2006
|
+
operator: batch.createdBy?.email || "admin@strapi.io"
|
|
2007
|
+
},
|
|
2008
|
+
batch
|
|
2009
|
+
};
|
|
2010
|
+
},
|
|
2011
|
+
/**
|
|
2012
|
+
* Get recent batch jobs for the processing dashboard
|
|
2013
|
+
*/
|
|
2014
|
+
async getRecentBatches() {
|
|
2015
|
+
const mergedMap = /* @__PURE__ */ new Map();
|
|
2016
|
+
for (const [id, job] of batchJobsMap.entries()) {
|
|
2017
|
+
mergedMap.set(id, {
|
|
2018
|
+
id: job.id,
|
|
2019
|
+
createdAt: job.createdAt,
|
|
2020
|
+
completedAt: job.completedAt,
|
|
2021
|
+
status: job.status,
|
|
2022
|
+
isLive: job.isLive,
|
|
2023
|
+
executionMode: job.executionMode,
|
|
2024
|
+
attribute: job.attribute,
|
|
2025
|
+
value: job.value,
|
|
2026
|
+
totalTargets: job.totalTargets,
|
|
2027
|
+
updatedCount: job.updatedCount,
|
|
2028
|
+
skippedCount: job.skippedCount,
|
|
2029
|
+
failedCount: job.failedCount,
|
|
2030
|
+
operator: job.createdBy?.email || "admin@strapi.io"
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
for (const [id, p] of pendingBatches.entries()) {
|
|
2034
|
+
if (!mergedMap.has(id)) {
|
|
2035
|
+
mergedMap.set(id, {
|
|
2036
|
+
id: p.id,
|
|
2037
|
+
createdAt: p.createdAt,
|
|
2038
|
+
status: p.status,
|
|
2039
|
+
isLive: false,
|
|
2040
|
+
executionMode: p.executionMode,
|
|
2041
|
+
attribute: p.payload.attribute,
|
|
2042
|
+
value: p.payload.value,
|
|
2043
|
+
totalTargets: p.summary.totalTargets,
|
|
2044
|
+
updatedCount: 0,
|
|
2045
|
+
skippedCount: 0,
|
|
2046
|
+
failedCount: 0,
|
|
2047
|
+
operator: p.createdBy?.email || "admin@strapi.io"
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
try {
|
|
2052
|
+
const auditService2 = strapi2.plugin("bulk-updater").service("audit");
|
|
2053
|
+
const historyData = await auditService2.getHistory();
|
|
2054
|
+
const records = Array.isArray(historyData?.history) ? historyData.history : [];
|
|
2055
|
+
for (const r of records) {
|
|
2056
|
+
if (!mergedMap.has(r.batchId)) {
|
|
2057
|
+
mergedMap.set(r.batchId, {
|
|
2058
|
+
id: r.batchId,
|
|
2059
|
+
createdAt: r.executedAt || r.createdAt,
|
|
2060
|
+
completedAt: r.executedAt,
|
|
2061
|
+
status: r.isRolledBack ? "failed" : "successful",
|
|
2062
|
+
isLive: !r.isRolledBack && r.updatedCount > 0,
|
|
2063
|
+
executionMode: "immediate",
|
|
2064
|
+
attribute: r.attribute,
|
|
2065
|
+
value: r.value,
|
|
2066
|
+
totalTargets: r.totalTargets || r.updatedCount + r.skippedCount || 1,
|
|
2067
|
+
updatedCount: r.updatedCount || 0,
|
|
2068
|
+
skippedCount: r.skippedCount || 0,
|
|
2069
|
+
failedCount: 0,
|
|
2070
|
+
operator: r.userEmail || "admin@strapi.io"
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
} catch {
|
|
2075
|
+
}
|
|
2076
|
+
const batches = Array.from(mergedMap.values()).sort(
|
|
2077
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
2078
|
+
);
|
|
2079
|
+
return {
|
|
2080
|
+
success: true,
|
|
2081
|
+
batches: batches.slice(0, 50)
|
|
2082
|
+
};
|
|
2083
|
+
},
|
|
1690
2084
|
invalidateCache(uid) {
|
|
1691
2085
|
if (uid) {
|
|
1692
2086
|
invalidateCollectionCache(uid);
|
|
@@ -2289,56 +2683,102 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
2289
2683
|
const violations = [];
|
|
2290
2684
|
let totalScanned = 0;
|
|
2291
2685
|
const allContentTypes = strapi2.contentTypes || {};
|
|
2686
|
+
const allComponents = strapi2.components || {};
|
|
2687
|
+
function schemaHasField(ctSchema) {
|
|
2688
|
+
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
2689
|
+
const rawAttrs = ctSchema.attributes;
|
|
2690
|
+
const populateAttrs = {};
|
|
2691
|
+
let hasField = Boolean(rawAttrs[targetField]);
|
|
2692
|
+
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
2693
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
2694
|
+
const comp = allComponents[attrDef.component];
|
|
2695
|
+
if (comp?.attributes?.[targetField]) {
|
|
2696
|
+
hasField = true;
|
|
2697
|
+
populateAttrs[attrKey] = true;
|
|
2698
|
+
}
|
|
2699
|
+
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
2700
|
+
const matchingComp = attrDef.components.some(
|
|
2701
|
+
(compUid) => allComponents[compUid]?.attributes?.[targetField]
|
|
2702
|
+
);
|
|
2703
|
+
if (matchingComp) {
|
|
2704
|
+
hasField = true;
|
|
2705
|
+
populateAttrs[attrKey] = true;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return { hasField, populateAttrs };
|
|
2710
|
+
}
|
|
2292
2711
|
const candidateUids = Object.keys(allContentTypes).filter(
|
|
2293
2712
|
(uid) => uid.startsWith("api::") && !uid.includes("bulk-updater")
|
|
2294
2713
|
);
|
|
2714
|
+
const relevantCollections = [];
|
|
2295
2715
|
for (const uid of candidateUids) {
|
|
2296
2716
|
const ct = allContentTypes[uid];
|
|
2297
2717
|
if (!ct) continue;
|
|
2298
|
-
const
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2718
|
+
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
2719
|
+
if (hasField) {
|
|
2720
|
+
relevantCollections.push({
|
|
2721
|
+
uid,
|
|
2722
|
+
displayName: ct.info?.displayName || uid,
|
|
2723
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : void 0
|
|
2302
2724
|
});
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
const BATCH_SIZE = 250;
|
|
2728
|
+
await Promise.all(
|
|
2729
|
+
relevantCollections.map(async ({ uid, displayName, populateOption }) => {
|
|
2730
|
+
try {
|
|
2731
|
+
let page = 1;
|
|
2732
|
+
while (true) {
|
|
2733
|
+
const entries = await strapi2.documents(uid).findMany({
|
|
2734
|
+
page,
|
|
2735
|
+
pageSize: BATCH_SIZE,
|
|
2736
|
+
populate: populateOption
|
|
2737
|
+
});
|
|
2738
|
+
if (!entries || entries.length === 0) break;
|
|
2739
|
+
for (const entry of entries) {
|
|
2740
|
+
let traverse = function(obj) {
|
|
2741
|
+
if (!obj || typeof obj !== "object") return;
|
|
2742
|
+
for (const key of Object.keys(obj)) {
|
|
2743
|
+
const val = obj[key];
|
|
2744
|
+
if (key === targetField && typeof val === "string" && val) {
|
|
2745
|
+
const lower = val.toLowerCase();
|
|
2746
|
+
counts[lower] = (counts[lower] || 0) + 1;
|
|
2747
|
+
}
|
|
2748
|
+
if (typeof val === "object" && val !== null) {
|
|
2749
|
+
traverse(val);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
};
|
|
2753
|
+
totalScanned++;
|
|
2754
|
+
const counts = {};
|
|
2755
|
+
traverse(entry);
|
|
2756
|
+
for (const [optKey, max] of Object.entries(quotas)) {
|
|
2757
|
+
const lowerOpt = optKey.toLowerCase();
|
|
2758
|
+
const actualCount = counts[lowerOpt] || 0;
|
|
2759
|
+
if (actualCount > max) {
|
|
2760
|
+
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
2761
|
+
violations.push({
|
|
2762
|
+
uid,
|
|
2763
|
+
collectionDisplayName: displayName,
|
|
2764
|
+
documentId: entry.documentId,
|
|
2765
|
+
entryLabel,
|
|
2766
|
+
violatingOption: optKey.toUpperCase(),
|
|
2767
|
+
foundCount: actualCount,
|
|
2768
|
+
maxAllowed: max,
|
|
2769
|
+
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2314
2772
|
}
|
|
2315
2773
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
const counts = {};
|
|
2319
|
-
traverse(entry);
|
|
2320
|
-
for (const [optKey, max] of Object.entries(quotas)) {
|
|
2321
|
-
const lowerOpt = optKey.toLowerCase();
|
|
2322
|
-
const actualCount = counts[lowerOpt] || 0;
|
|
2323
|
-
if (actualCount > max) {
|
|
2324
|
-
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
2325
|
-
violations.push({
|
|
2326
|
-
uid,
|
|
2327
|
-
collectionDisplayName: displayName,
|
|
2328
|
-
documentId: entry.documentId,
|
|
2329
|
-
entryLabel,
|
|
2330
|
-
violatingOption: optKey.toUpperCase(),
|
|
2331
|
-
foundCount: actualCount,
|
|
2332
|
-
maxAllowed: max,
|
|
2333
|
-
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2774
|
+
if (entries.length < BATCH_SIZE) break;
|
|
2775
|
+
page++;
|
|
2336
2776
|
}
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
strapi2.log.warn(`[bulk-updater] Failed scanning ${uid} for rule ${ruleId}:`, err);
|
|
2337
2779
|
}
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2780
|
+
})
|
|
2781
|
+
);
|
|
2342
2782
|
return {
|
|
2343
2783
|
rule,
|
|
2344
2784
|
totalScanned,
|
|
@@ -2640,12 +3080,13 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2640
3080
|
throw new Error("CSV is empty or could not be parsed.");
|
|
2641
3081
|
}
|
|
2642
3082
|
const attrs = ct.attributes || {};
|
|
2643
|
-
const { mode: mode2, publish = true, user } = options;
|
|
3083
|
+
const { mode: mode2 = "auto", publish = true, user } = options;
|
|
2644
3084
|
const stats = {
|
|
2645
3085
|
total: parsedRows.length,
|
|
2646
3086
|
created: 0,
|
|
2647
3087
|
updated: 0,
|
|
2648
3088
|
skipped: 0,
|
|
3089
|
+
newWithoutDocId: 0,
|
|
2649
3090
|
errors: [],
|
|
2650
3091
|
diffs: []
|
|
2651
3092
|
};
|
|
@@ -2717,7 +3158,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2717
3158
|
stats.skipped++;
|
|
2718
3159
|
stats.errors.push({
|
|
2719
3160
|
row: rowNumber,
|
|
2720
|
-
error: "Missing documentId for Update mode."
|
|
3161
|
+
error: "Missing documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
|
|
2721
3162
|
});
|
|
2722
3163
|
continue;
|
|
2723
3164
|
}
|
|
@@ -2762,7 +3203,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2762
3203
|
status: "updated",
|
|
2763
3204
|
message: `Updated via CSV import (row ${rowNumber})`
|
|
2764
3205
|
});
|
|
2765
|
-
} else {
|
|
3206
|
+
} else if (mode2 === "create_or_override") {
|
|
2766
3207
|
if (docId) {
|
|
2767
3208
|
const existing = await strapi2.documents(uid).findOne({
|
|
2768
3209
|
documentId: docId
|
|
@@ -2812,6 +3253,73 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2812
3253
|
}
|
|
2813
3254
|
}
|
|
2814
3255
|
stats.created++;
|
|
3256
|
+
if (!docId) {
|
|
3257
|
+
stats.newWithoutDocId++;
|
|
3258
|
+
}
|
|
3259
|
+
stats.diffs.push({
|
|
3260
|
+
uid,
|
|
3261
|
+
collectionName: ct.info?.displayName || uid,
|
|
3262
|
+
documentId: newDocId,
|
|
3263
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
3264
|
+
attribute: Object.keys(payload).join(", "),
|
|
3265
|
+
oldValue: null,
|
|
3266
|
+
newValue: payload,
|
|
3267
|
+
status: "updated",
|
|
3268
|
+
message: docId ? `Created via CSV import (row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
3269
|
+
});
|
|
3270
|
+
} else {
|
|
3271
|
+
if (docId) {
|
|
3272
|
+
const existing = await strapi2.documents(uid).findOne({
|
|
3273
|
+
documentId: docId
|
|
3274
|
+
});
|
|
3275
|
+
if (existing) {
|
|
3276
|
+
const mergedPayload = { ...payload };
|
|
3277
|
+
for (const key of Object.keys(payload)) {
|
|
3278
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
3279
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
await strapi2.documents(uid).update({
|
|
3283
|
+
documentId: docId,
|
|
3284
|
+
data: mergedPayload
|
|
3285
|
+
});
|
|
3286
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
3287
|
+
try {
|
|
3288
|
+
await strapi2.documents(uid).publish({ documentId: docId });
|
|
3289
|
+
} catch (pubErr) {
|
|
3290
|
+
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
stats.updated++;
|
|
3294
|
+
stats.diffs.push({
|
|
3295
|
+
uid,
|
|
3296
|
+
collectionName: ct.info?.displayName || uid,
|
|
3297
|
+
documentId: docId,
|
|
3298
|
+
label: existing.title || existing.name || `Entry #${docId}`,
|
|
3299
|
+
attribute: Object.keys(payload).join(", "),
|
|
3300
|
+
oldValue: existing,
|
|
3301
|
+
newValue: payload,
|
|
3302
|
+
status: "updated",
|
|
3303
|
+
message: `Updated existing entry via smart match (row ${rowNumber})`
|
|
3304
|
+
});
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
const created = await strapi2.documents(uid).create({
|
|
3309
|
+
data: payload
|
|
3310
|
+
});
|
|
3311
|
+
const newDocId = created?.documentId || docId;
|
|
3312
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
3313
|
+
try {
|
|
3314
|
+
await strapi2.documents(uid).publish({ documentId: newDocId });
|
|
3315
|
+
} catch (pubErr) {
|
|
3316
|
+
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
stats.created++;
|
|
3320
|
+
if (!docId) {
|
|
3321
|
+
stats.newWithoutDocId++;
|
|
3322
|
+
}
|
|
2815
3323
|
stats.diffs.push({
|
|
2816
3324
|
uid,
|
|
2817
3325
|
collectionName: ct.info?.displayName || uid,
|
|
@@ -2821,7 +3329,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2821
3329
|
oldValue: null,
|
|
2822
3330
|
newValue: payload,
|
|
2823
3331
|
status: "updated",
|
|
2824
|
-
message: `Created
|
|
3332
|
+
message: docId ? `Created as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
2825
3333
|
});
|
|
2826
3334
|
}
|
|
2827
3335
|
} catch (err) {
|