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.js
CHANGED
|
@@ -91,6 +91,30 @@ const routes = {
|
|
|
91
91
|
policies: []
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
|
+
{
|
|
95
|
+
method: "GET",
|
|
96
|
+
path: "/batches/recent",
|
|
97
|
+
handler: "bulkUpdater.getRecentBatches",
|
|
98
|
+
config: {
|
|
99
|
+
policies: []
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
method: "GET",
|
|
104
|
+
path: "/batches/:batchId/status",
|
|
105
|
+
handler: "bulkUpdater.getBatchStatus",
|
|
106
|
+
config: {
|
|
107
|
+
policies: []
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
method: "GET",
|
|
112
|
+
path: "/batches/:batchId/report",
|
|
113
|
+
handler: "bulkUpdater.getBatchReport",
|
|
114
|
+
config: {
|
|
115
|
+
policies: []
|
|
116
|
+
}
|
|
117
|
+
},
|
|
94
118
|
{
|
|
95
119
|
method: "GET",
|
|
96
120
|
path: "/audit/history",
|
|
@@ -376,6 +400,39 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
376
400
|
ctx.status = 400;
|
|
377
401
|
ctx.body = { success: false, error: err.message };
|
|
378
402
|
}
|
|
403
|
+
},
|
|
404
|
+
async getBatchStatus(ctx) {
|
|
405
|
+
try {
|
|
406
|
+
const { batchId } = ctx.params;
|
|
407
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
408
|
+
const result = await service.getBatchStatus(batchId);
|
|
409
|
+
ctx.body = result;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
ctx.status = 404;
|
|
412
|
+
ctx.body = { success: false, error: err.message };
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
async getBatchReport(ctx) {
|
|
416
|
+
try {
|
|
417
|
+
const { batchId } = ctx.params;
|
|
418
|
+
const { format = "csv" } = ctx.query;
|
|
419
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
420
|
+
const result = await service.getBatchReport(batchId, format);
|
|
421
|
+
ctx.body = result;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
ctx.status = 500;
|
|
424
|
+
ctx.body = { success: false, error: err.message };
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
async getRecentBatches(ctx) {
|
|
428
|
+
try {
|
|
429
|
+
const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
430
|
+
const result = await service.getRecentBatches();
|
|
431
|
+
ctx.body = result;
|
|
432
|
+
} catch (err) {
|
|
433
|
+
ctx.status = 500;
|
|
434
|
+
ctx.body = { success: false, error: err.message };
|
|
435
|
+
}
|
|
379
436
|
}
|
|
380
437
|
});
|
|
381
438
|
const auditController = ({ strapi: strapi2 }) => ({
|
|
@@ -548,7 +605,7 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
548
605
|
}
|
|
549
606
|
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
550
607
|
const result = await csvService2.importCsv(uid, csvText, {
|
|
551
|
-
mode: mode2 === "create_or_override" ? "create_or_override" : "
|
|
608
|
+
mode: mode2 === "update" ? "update" : mode2 === "create_or_override" ? "create_or_override" : "auto",
|
|
552
609
|
publish: publish !== false,
|
|
553
610
|
user: ctx.state.user
|
|
554
611
|
});
|
|
@@ -621,6 +678,7 @@ function checkBusinessHours(config, testDate) {
|
|
|
621
678
|
};
|
|
622
679
|
}
|
|
623
680
|
const pendingBatches = /* @__PURE__ */ new Map();
|
|
681
|
+
const batchJobsMap = /* @__PURE__ */ new Map();
|
|
624
682
|
let isRunnerRunning = false;
|
|
625
683
|
let runnerInterval = null;
|
|
626
684
|
function invalidateCollectionCache(uid) {
|
|
@@ -720,6 +778,31 @@ async function executeBatchInternal(strapi2, {
|
|
|
720
778
|
const results = [];
|
|
721
779
|
let updatedCount = 0;
|
|
722
780
|
let skippedCount = 0;
|
|
781
|
+
const jobState = {
|
|
782
|
+
id: batchId,
|
|
783
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
784
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
785
|
+
createdBy: currentUser ? {
|
|
786
|
+
id: currentUser.id,
|
|
787
|
+
email: currentUser.email,
|
|
788
|
+
firstname: currentUser.firstname,
|
|
789
|
+
lastname: currentUser.lastname,
|
|
790
|
+
username: currentUser.username
|
|
791
|
+
} : { email: "admin@system.local" },
|
|
792
|
+
status: "processing",
|
|
793
|
+
executionMode: "immediate",
|
|
794
|
+
attribute,
|
|
795
|
+
value,
|
|
796
|
+
totalTargets: targetsMap.size,
|
|
797
|
+
processedCount: 0,
|
|
798
|
+
updatedCount: 0,
|
|
799
|
+
skippedCount: 0,
|
|
800
|
+
failedCount: 0,
|
|
801
|
+
isLive: false,
|
|
802
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
803
|
+
results: []
|
|
804
|
+
};
|
|
805
|
+
batchJobsMap.set(batchId, jobState);
|
|
723
806
|
let regexObj = null;
|
|
724
807
|
if (isRegex && regexPattern) {
|
|
725
808
|
regexObj = new RegExp(regexPattern, "g");
|
|
@@ -954,11 +1037,24 @@ async function executeBatchInternal(strapi2, {
|
|
|
954
1037
|
else if (res.status === "skipped") skippedCount++;
|
|
955
1038
|
results.push(res);
|
|
956
1039
|
}
|
|
1040
|
+
jobState.processedCount = results.length;
|
|
1041
|
+
jobState.updatedCount = updatedCount;
|
|
1042
|
+
jobState.skippedCount = skippedCount;
|
|
1043
|
+
jobState.failedCount = results.filter((r) => r.status === "error").length;
|
|
957
1044
|
}
|
|
958
1045
|
const affectedUids = new Set(targets.map((t) => t.uid));
|
|
959
1046
|
affectedUids.forEach((uid) => {
|
|
960
1047
|
invalidateCollectionCache(uid);
|
|
961
1048
|
});
|
|
1049
|
+
const failedCount = results.filter((r) => r.status === "error").length;
|
|
1050
|
+
jobState.failedCount = failedCount;
|
|
1051
|
+
jobState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1052
|
+
jobState.isLive = updatedCount > 0;
|
|
1053
|
+
jobState.status = failedCount === 0 && updatedCount > 0 ? "successful" : failedCount > 0 && updatedCount > 0 ? "partial_failure" : failedCount > 0 ? "failed" : "successful";
|
|
1054
|
+
jobState.results = results.map((r) => ({
|
|
1055
|
+
...r,
|
|
1056
|
+
isLive: r.status === "updated"
|
|
1057
|
+
}));
|
|
962
1058
|
const batchSummary = {
|
|
963
1059
|
batchId,
|
|
964
1060
|
attribute,
|
|
@@ -980,7 +1076,10 @@ async function executeBatchInternal(strapi2, {
|
|
|
980
1076
|
value,
|
|
981
1077
|
updatedCount,
|
|
982
1078
|
skippedCount,
|
|
1079
|
+
failedCount,
|
|
983
1080
|
totalTargets: targetsMap.size,
|
|
1081
|
+
isLive: jobState.isLive,
|
|
1082
|
+
status: jobState.status,
|
|
984
1083
|
results
|
|
985
1084
|
};
|
|
986
1085
|
}
|
|
@@ -1108,69 +1207,87 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1108
1207
|
}
|
|
1109
1208
|
const collections = [];
|
|
1110
1209
|
const signatures = [];
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
populate: populateOption
|
|
1210
|
+
const BATCH_CONCURRENCY = 10;
|
|
1211
|
+
const collectionResults = [];
|
|
1212
|
+
for (let i = 0; i < staticSchemaCache.targetUids.length; i += BATCH_CONCURRENCY) {
|
|
1213
|
+
const batchUids = staticSchemaCache.targetUids.slice(i, i + BATCH_CONCURRENCY);
|
|
1214
|
+
const batchRes = await Promise.all(
|
|
1215
|
+
batchUids.map(async (uid) => {
|
|
1216
|
+
const meta = staticSchemaCache.metaByUid[uid];
|
|
1217
|
+
if (!meta) return null;
|
|
1218
|
+
const signature = await getCollectionSignature(strapi2, uid);
|
|
1219
|
+
const cached = modelsCache.get(uid);
|
|
1220
|
+
if (cached && cached.signature === signature) {
|
|
1221
|
+
return {
|
|
1222
|
+
collection: {
|
|
1223
|
+
...meta,
|
|
1224
|
+
totalEntries: cached.totalCount,
|
|
1225
|
+
entries: cached.entries
|
|
1226
|
+
},
|
|
1227
|
+
signature: `${uid}=${signature}`
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
let entries = [];
|
|
1231
|
+
let totalCount = 0;
|
|
1232
|
+
try {
|
|
1233
|
+
const ctAttrs = allContentTypes[uid]?.attributes || {};
|
|
1234
|
+
const relKeys = Object.keys(meta.relations || {});
|
|
1235
|
+
const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
|
|
1236
|
+
const populateOptionObj = {};
|
|
1237
|
+
relKeys.forEach((k) => {
|
|
1238
|
+
populateOptionObj[k] = true;
|
|
1141
1239
|
});
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
totalCount = 1;
|
|
1145
|
-
}
|
|
1146
|
-
} else {
|
|
1147
|
-
totalCount = await strapi2.documents(uid).count();
|
|
1148
|
-
entries = await strapi2.documents(uid).findMany({
|
|
1149
|
-
limit: 10,
|
|
1150
|
-
sort: { createdAt: "desc" },
|
|
1151
|
-
populate: populateOption
|
|
1240
|
+
compKeys.forEach((k) => {
|
|
1241
|
+
populateOptionObj[k] = true;
|
|
1152
1242
|
});
|
|
1243
|
+
const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
|
|
1244
|
+
if (meta.isSingleType) {
|
|
1245
|
+
const singleDoc = await strapi2.documents(uid).findFirst({
|
|
1246
|
+
populate: populateOption
|
|
1247
|
+
});
|
|
1248
|
+
if (singleDoc) {
|
|
1249
|
+
entries = [singleDoc];
|
|
1250
|
+
totalCount = 1;
|
|
1251
|
+
}
|
|
1252
|
+
} else {
|
|
1253
|
+
totalCount = await strapi2.documents(uid).count();
|
|
1254
|
+
entries = await strapi2.documents(uid).findMany({
|
|
1255
|
+
limit: 10,
|
|
1256
|
+
sort: { createdAt: "desc" },
|
|
1257
|
+
populate: populateOption
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
strapi2.log.error(`[bulk-updater] Failed to fetch entries for ${uid}:`, err);
|
|
1262
|
+
entries = [];
|
|
1263
|
+
totalCount = 0;
|
|
1153
1264
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1265
|
+
const formattedEntries = (entries || []).map((entry) => ({
|
|
1266
|
+
id: entry.id,
|
|
1267
|
+
documentId: entry.documentId,
|
|
1268
|
+
...entry
|
|
1269
|
+
}));
|
|
1270
|
+
modelsCache.set(uid, {
|
|
1271
|
+
signature,
|
|
1272
|
+
totalCount,
|
|
1273
|
+
entries: formattedEntries
|
|
1274
|
+
});
|
|
1275
|
+
return {
|
|
1276
|
+
collection: {
|
|
1277
|
+
...meta,
|
|
1278
|
+
totalEntries: totalCount,
|
|
1279
|
+
entries: formattedEntries
|
|
1280
|
+
},
|
|
1281
|
+
signature: `${uid}=${signature}`
|
|
1282
|
+
};
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
collectionResults.push(...batchRes);
|
|
1286
|
+
}
|
|
1287
|
+
for (const res of collectionResults) {
|
|
1288
|
+
if (res) {
|
|
1289
|
+
collections.push(res.collection);
|
|
1290
|
+
signatures.push(res.signature);
|
|
1174
1291
|
}
|
|
1175
1292
|
}
|
|
1176
1293
|
const executionConfig = getExecutionWindowConfig(strapi2);
|
|
@@ -1489,6 +1606,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1489
1606
|
makerNotes
|
|
1490
1607
|
};
|
|
1491
1608
|
pendingBatches.set(batchId, batch);
|
|
1609
|
+
batchJobsMap.set(batchId, {
|
|
1610
|
+
id: batchId,
|
|
1611
|
+
createdAt: batch.createdAt,
|
|
1612
|
+
createdBy: batch.createdBy,
|
|
1613
|
+
status: "pending_approval",
|
|
1614
|
+
executionMode: "maker_checker",
|
|
1615
|
+
attribute,
|
|
1616
|
+
value,
|
|
1617
|
+
totalTargets: summary.totalTargets,
|
|
1618
|
+
processedCount: 0,
|
|
1619
|
+
updatedCount: 0,
|
|
1620
|
+
skippedCount: 0,
|
|
1621
|
+
failedCount: 0,
|
|
1622
|
+
isLive: false,
|
|
1623
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
1624
|
+
makerNotes,
|
|
1625
|
+
results: []
|
|
1626
|
+
});
|
|
1492
1627
|
return {
|
|
1493
1628
|
success: true,
|
|
1494
1629
|
pending: true,
|
|
@@ -1528,6 +1663,25 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1528
1663
|
makerNotes
|
|
1529
1664
|
};
|
|
1530
1665
|
pendingBatches.set(batchId, batch);
|
|
1666
|
+
batchJobsMap.set(batchId, {
|
|
1667
|
+
id: batchId,
|
|
1668
|
+
createdAt: batch.createdAt,
|
|
1669
|
+
createdBy: batch.createdBy,
|
|
1670
|
+
status: "queued_offpeak",
|
|
1671
|
+
executionMode: "queue_offpeak",
|
|
1672
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
1673
|
+
attribute,
|
|
1674
|
+
value,
|
|
1675
|
+
totalTargets: summary.totalTargets,
|
|
1676
|
+
processedCount: 0,
|
|
1677
|
+
updatedCount: 0,
|
|
1678
|
+
skippedCount: 0,
|
|
1679
|
+
failedCount: 0,
|
|
1680
|
+
isLive: false,
|
|
1681
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
1682
|
+
makerNotes,
|
|
1683
|
+
results: []
|
|
1684
|
+
});
|
|
1531
1685
|
return {
|
|
1532
1686
|
success: true,
|
|
1533
1687
|
pending: true,
|
|
@@ -1568,6 +1722,26 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1568
1722
|
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
1569
1723
|
};
|
|
1570
1724
|
pendingBatches.set(batchId, batch);
|
|
1725
|
+
batchJobsMap.set(batchId, {
|
|
1726
|
+
id: batchId,
|
|
1727
|
+
createdAt: batch.createdAt,
|
|
1728
|
+
createdBy: batch.createdBy,
|
|
1729
|
+
status: "queued_offpeak",
|
|
1730
|
+
executionMode: "queue_offpeak",
|
|
1731
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
1732
|
+
attribute,
|
|
1733
|
+
value,
|
|
1734
|
+
totalTargets: summary.totalTargets,
|
|
1735
|
+
processedCount: 0,
|
|
1736
|
+
updatedCount: 0,
|
|
1737
|
+
skippedCount: 0,
|
|
1738
|
+
failedCount: 0,
|
|
1739
|
+
isLive: false,
|
|
1740
|
+
publishOnApproval: Boolean(publishOnApproval),
|
|
1741
|
+
policyMessage,
|
|
1742
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
1743
|
+
results: []
|
|
1744
|
+
});
|
|
1571
1745
|
return {
|
|
1572
1746
|
success: true,
|
|
1573
1747
|
pending: true,
|
|
@@ -1700,6 +1874,226 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1700
1874
|
result
|
|
1701
1875
|
};
|
|
1702
1876
|
},
|
|
1877
|
+
/**
|
|
1878
|
+
* Get real-time or historical processing status of a batch job
|
|
1879
|
+
*/
|
|
1880
|
+
async getBatchStatus(batchId) {
|
|
1881
|
+
if (!batchId) throw new Error("batchId is required");
|
|
1882
|
+
let job = batchJobsMap.get(batchId);
|
|
1883
|
+
if (job) {
|
|
1884
|
+
return { success: true, batch: job };
|
|
1885
|
+
}
|
|
1886
|
+
const pending = pendingBatches.get(batchId);
|
|
1887
|
+
if (pending) {
|
|
1888
|
+
const pendingJob = {
|
|
1889
|
+
id: pending.id,
|
|
1890
|
+
createdAt: pending.createdAt,
|
|
1891
|
+
createdBy: pending.createdBy,
|
|
1892
|
+
status: pending.status,
|
|
1893
|
+
executionMode: pending.executionMode,
|
|
1894
|
+
scheduledFor: pending.scheduledFor,
|
|
1895
|
+
attribute: pending.payload.attribute,
|
|
1896
|
+
value: pending.payload.value,
|
|
1897
|
+
totalTargets: pending.summary.totalTargets,
|
|
1898
|
+
processedCount: 0,
|
|
1899
|
+
updatedCount: 0,
|
|
1900
|
+
skippedCount: 0,
|
|
1901
|
+
failedCount: 0,
|
|
1902
|
+
isLive: false,
|
|
1903
|
+
publishOnApproval: Boolean(pending.payload.publishOnApproval),
|
|
1904
|
+
policyMessage: pending.makerNotes,
|
|
1905
|
+
results: []
|
|
1906
|
+
};
|
|
1907
|
+
return { success: true, batch: pendingJob };
|
|
1908
|
+
}
|
|
1909
|
+
try {
|
|
1910
|
+
const auditService2 = strapi2.plugin("bulk-updater").service("audit");
|
|
1911
|
+
const historyData = await auditService2.getHistory();
|
|
1912
|
+
const records = Array.isArray(historyData?.history) ? historyData.history : [];
|
|
1913
|
+
const found = records.find((r) => r.batchId === batchId);
|
|
1914
|
+
if (found) {
|
|
1915
|
+
const total = found.totalTargets || found.updatedCount + found.skippedCount || 1;
|
|
1916
|
+
const historicalJob = {
|
|
1917
|
+
id: found.batchId,
|
|
1918
|
+
createdAt: found.executedAt || found.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1919
|
+
startedAt: found.executedAt || found.createdAt,
|
|
1920
|
+
completedAt: found.executedAt || found.createdAt,
|
|
1921
|
+
createdBy: {
|
|
1922
|
+
email: found.userEmail || "admin@strapi.io",
|
|
1923
|
+
firstname: found.userName || "Admin"
|
|
1924
|
+
},
|
|
1925
|
+
status: found.isRolledBack ? "failed" : "successful",
|
|
1926
|
+
executionMode: "immediate",
|
|
1927
|
+
attribute: found.attribute,
|
|
1928
|
+
value: found.value,
|
|
1929
|
+
totalTargets: total,
|
|
1930
|
+
processedCount: total,
|
|
1931
|
+
updatedCount: found.updatedCount || 0,
|
|
1932
|
+
skippedCount: found.skippedCount || 0,
|
|
1933
|
+
failedCount: 0,
|
|
1934
|
+
isLive: !found.isRolledBack && found.updatedCount > 0,
|
|
1935
|
+
publishOnApproval: true,
|
|
1936
|
+
results: (found.diffs || []).map((d) => ({
|
|
1937
|
+
uid: d.uid,
|
|
1938
|
+
collectionName: d.collectionName || d.uid,
|
|
1939
|
+
documentId: d.documentId,
|
|
1940
|
+
label: d.label || `Entry #${d.documentId}`,
|
|
1941
|
+
attribute: d.attribute || found.attribute,
|
|
1942
|
+
oldValue: d.oldValue,
|
|
1943
|
+
newValue: d.newValue,
|
|
1944
|
+
status: d.status || "updated",
|
|
1945
|
+
isLive: !found.isRolledBack,
|
|
1946
|
+
message: d.message || "Updated via bulk update"
|
|
1947
|
+
}))
|
|
1948
|
+
};
|
|
1949
|
+
batchJobsMap.set(batchId, historicalJob);
|
|
1950
|
+
return { success: true, batch: historicalJob };
|
|
1951
|
+
}
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
}
|
|
1954
|
+
throw new Error(`Batch with ID "${batchId}" was not found.`);
|
|
1955
|
+
},
|
|
1956
|
+
/**
|
|
1957
|
+
* Downloadable execution report (CSV or JSON)
|
|
1958
|
+
*/
|
|
1959
|
+
async getBatchReport(batchId, format = "csv") {
|
|
1960
|
+
const statusRes = await this.getBatchStatus(batchId);
|
|
1961
|
+
const batch = statusRes.batch;
|
|
1962
|
+
function escapeCsvVal(val) {
|
|
1963
|
+
if (val === null || val === void 0) return "";
|
|
1964
|
+
let str2 = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
1965
|
+
if (str2.includes(",") || str2.includes('"') || str2.includes("\n") || str2.includes("\r")) {
|
|
1966
|
+
return `"${str2.replace(/"/g, '""')}"`;
|
|
1967
|
+
}
|
|
1968
|
+
return str2;
|
|
1969
|
+
}
|
|
1970
|
+
const headers = [
|
|
1971
|
+
"Batch ID",
|
|
1972
|
+
"Collection UID",
|
|
1973
|
+
"Collection Name",
|
|
1974
|
+
"Document ID",
|
|
1975
|
+
"Record Title / Identifier",
|
|
1976
|
+
"Attribute Name",
|
|
1977
|
+
"Previous Value",
|
|
1978
|
+
"New Live Value",
|
|
1979
|
+
"Processing Status",
|
|
1980
|
+
"Live On Server",
|
|
1981
|
+
"Details / Message",
|
|
1982
|
+
"Executed At",
|
|
1983
|
+
"Operator"
|
|
1984
|
+
];
|
|
1985
|
+
const rows = [headers.join(",")];
|
|
1986
|
+
for (const item of batch.results || []) {
|
|
1987
|
+
const row = [
|
|
1988
|
+
escapeCsvVal(batch.id),
|
|
1989
|
+
escapeCsvVal(item.uid),
|
|
1990
|
+
escapeCsvVal(item.collectionName),
|
|
1991
|
+
escapeCsvVal(item.documentId),
|
|
1992
|
+
escapeCsvVal(item.label),
|
|
1993
|
+
escapeCsvVal(item.attribute),
|
|
1994
|
+
escapeCsvVal(item.oldValue),
|
|
1995
|
+
escapeCsvVal(item.newValue),
|
|
1996
|
+
escapeCsvVal(item.status === "updated" ? "Updated" : item.status === "error" ? "Failed" : "Skipped"),
|
|
1997
|
+
escapeCsvVal(item.isLive ? "YES (Live on Site)" : "NO"),
|
|
1998
|
+
escapeCsvVal(item.message),
|
|
1999
|
+
escapeCsvVal(batch.completedAt || batch.createdAt),
|
|
2000
|
+
escapeCsvVal(batch.createdBy?.email || "admin@strapi.io")
|
|
2001
|
+
];
|
|
2002
|
+
rows.push(row.join(","));
|
|
2003
|
+
}
|
|
2004
|
+
const safeBatchId = batch.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2005
|
+
const filename = `bulk_update_report_${safeBatchId}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
2006
|
+
return {
|
|
2007
|
+
success: true,
|
|
2008
|
+
filename,
|
|
2009
|
+
csvContent: rows.join("\n"),
|
|
2010
|
+
summary: {
|
|
2011
|
+
batchId: batch.id,
|
|
2012
|
+
status: batch.status,
|
|
2013
|
+
isLive: batch.isLive,
|
|
2014
|
+
totalTargets: batch.totalTargets,
|
|
2015
|
+
updatedCount: batch.updatedCount,
|
|
2016
|
+
skippedCount: batch.skippedCount,
|
|
2017
|
+
failedCount: batch.failedCount,
|
|
2018
|
+
executedAt: batch.completedAt || batch.createdAt,
|
|
2019
|
+
operator: batch.createdBy?.email || "admin@strapi.io"
|
|
2020
|
+
},
|
|
2021
|
+
batch
|
|
2022
|
+
};
|
|
2023
|
+
},
|
|
2024
|
+
/**
|
|
2025
|
+
* Get recent batch jobs for the processing dashboard
|
|
2026
|
+
*/
|
|
2027
|
+
async getRecentBatches() {
|
|
2028
|
+
const mergedMap = /* @__PURE__ */ new Map();
|
|
2029
|
+
for (const [id, job] of batchJobsMap.entries()) {
|
|
2030
|
+
mergedMap.set(id, {
|
|
2031
|
+
id: job.id,
|
|
2032
|
+
createdAt: job.createdAt,
|
|
2033
|
+
completedAt: job.completedAt,
|
|
2034
|
+
status: job.status,
|
|
2035
|
+
isLive: job.isLive,
|
|
2036
|
+
executionMode: job.executionMode,
|
|
2037
|
+
attribute: job.attribute,
|
|
2038
|
+
value: job.value,
|
|
2039
|
+
totalTargets: job.totalTargets,
|
|
2040
|
+
updatedCount: job.updatedCount,
|
|
2041
|
+
skippedCount: job.skippedCount,
|
|
2042
|
+
failedCount: job.failedCount,
|
|
2043
|
+
operator: job.createdBy?.email || "admin@strapi.io"
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
for (const [id, p] of pendingBatches.entries()) {
|
|
2047
|
+
if (!mergedMap.has(id)) {
|
|
2048
|
+
mergedMap.set(id, {
|
|
2049
|
+
id: p.id,
|
|
2050
|
+
createdAt: p.createdAt,
|
|
2051
|
+
status: p.status,
|
|
2052
|
+
isLive: false,
|
|
2053
|
+
executionMode: p.executionMode,
|
|
2054
|
+
attribute: p.payload.attribute,
|
|
2055
|
+
value: p.payload.value,
|
|
2056
|
+
totalTargets: p.summary.totalTargets,
|
|
2057
|
+
updatedCount: 0,
|
|
2058
|
+
skippedCount: 0,
|
|
2059
|
+
failedCount: 0,
|
|
2060
|
+
operator: p.createdBy?.email || "admin@strapi.io"
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const auditService2 = strapi2.plugin("bulk-updater").service("audit");
|
|
2066
|
+
const historyData = await auditService2.getHistory();
|
|
2067
|
+
const records = Array.isArray(historyData?.history) ? historyData.history : [];
|
|
2068
|
+
for (const r of records) {
|
|
2069
|
+
if (!mergedMap.has(r.batchId)) {
|
|
2070
|
+
mergedMap.set(r.batchId, {
|
|
2071
|
+
id: r.batchId,
|
|
2072
|
+
createdAt: r.executedAt || r.createdAt,
|
|
2073
|
+
completedAt: r.executedAt,
|
|
2074
|
+
status: r.isRolledBack ? "failed" : "successful",
|
|
2075
|
+
isLive: !r.isRolledBack && r.updatedCount > 0,
|
|
2076
|
+
executionMode: "immediate",
|
|
2077
|
+
attribute: r.attribute,
|
|
2078
|
+
value: r.value,
|
|
2079
|
+
totalTargets: r.totalTargets || r.updatedCount + r.skippedCount || 1,
|
|
2080
|
+
updatedCount: r.updatedCount || 0,
|
|
2081
|
+
skippedCount: r.skippedCount || 0,
|
|
2082
|
+
failedCount: 0,
|
|
2083
|
+
operator: r.userEmail || "admin@strapi.io"
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
const batches = Array.from(mergedMap.values()).sort(
|
|
2090
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
2091
|
+
);
|
|
2092
|
+
return {
|
|
2093
|
+
success: true,
|
|
2094
|
+
batches: batches.slice(0, 50)
|
|
2095
|
+
};
|
|
2096
|
+
},
|
|
1703
2097
|
invalidateCache(uid) {
|
|
1704
2098
|
if (uid) {
|
|
1705
2099
|
invalidateCollectionCache(uid);
|
|
@@ -2302,56 +2696,102 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
2302
2696
|
const violations = [];
|
|
2303
2697
|
let totalScanned = 0;
|
|
2304
2698
|
const allContentTypes = strapi2.contentTypes || {};
|
|
2699
|
+
const allComponents = strapi2.components || {};
|
|
2700
|
+
function schemaHasField(ctSchema) {
|
|
2701
|
+
if (!ctSchema || !ctSchema.attributes) return { hasField: false, populateAttrs: {} };
|
|
2702
|
+
const rawAttrs = ctSchema.attributes;
|
|
2703
|
+
const populateAttrs = {};
|
|
2704
|
+
let hasField = Boolean(rawAttrs[targetField]);
|
|
2705
|
+
for (const [attrKey, attrDef] of Object.entries(rawAttrs)) {
|
|
2706
|
+
if (attrDef.type === "component" && attrDef.component) {
|
|
2707
|
+
const comp = allComponents[attrDef.component];
|
|
2708
|
+
if (comp?.attributes?.[targetField]) {
|
|
2709
|
+
hasField = true;
|
|
2710
|
+
populateAttrs[attrKey] = true;
|
|
2711
|
+
}
|
|
2712
|
+
} else if (attrDef.type === "dynamiczone" && Array.isArray(attrDef.components)) {
|
|
2713
|
+
const matchingComp = attrDef.components.some(
|
|
2714
|
+
(compUid) => allComponents[compUid]?.attributes?.[targetField]
|
|
2715
|
+
);
|
|
2716
|
+
if (matchingComp) {
|
|
2717
|
+
hasField = true;
|
|
2718
|
+
populateAttrs[attrKey] = true;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return { hasField, populateAttrs };
|
|
2723
|
+
}
|
|
2305
2724
|
const candidateUids = Object.keys(allContentTypes).filter(
|
|
2306
2725
|
(uid) => uid.startsWith("api::") && !uid.includes("bulk-updater")
|
|
2307
2726
|
);
|
|
2727
|
+
const relevantCollections = [];
|
|
2308
2728
|
for (const uid of candidateUids) {
|
|
2309
2729
|
const ct = allContentTypes[uid];
|
|
2310
2730
|
if (!ct) continue;
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2731
|
+
const { hasField, populateAttrs } = schemaHasField(ct);
|
|
2732
|
+
if (hasField) {
|
|
2733
|
+
relevantCollections.push({
|
|
2734
|
+
uid,
|
|
2735
|
+
displayName: ct.info?.displayName || uid,
|
|
2736
|
+
populateOption: Object.keys(populateAttrs).length > 0 ? populateAttrs : void 0
|
|
2315
2737
|
});
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const BATCH_SIZE = 250;
|
|
2741
|
+
await Promise.all(
|
|
2742
|
+
relevantCollections.map(async ({ uid, displayName, populateOption }) => {
|
|
2743
|
+
try {
|
|
2744
|
+
let page = 1;
|
|
2745
|
+
while (true) {
|
|
2746
|
+
const entries = await strapi2.documents(uid).findMany({
|
|
2747
|
+
page,
|
|
2748
|
+
pageSize: BATCH_SIZE,
|
|
2749
|
+
populate: populateOption
|
|
2750
|
+
});
|
|
2751
|
+
if (!entries || entries.length === 0) break;
|
|
2752
|
+
for (const entry of entries) {
|
|
2753
|
+
let traverse = function(obj) {
|
|
2754
|
+
if (!obj || typeof obj !== "object") return;
|
|
2755
|
+
for (const key of Object.keys(obj)) {
|
|
2756
|
+
const val = obj[key];
|
|
2757
|
+
if (key === targetField && typeof val === "string" && val) {
|
|
2758
|
+
const lower = val.toLowerCase();
|
|
2759
|
+
counts[lower] = (counts[lower] || 0) + 1;
|
|
2760
|
+
}
|
|
2761
|
+
if (typeof val === "object" && val !== null) {
|
|
2762
|
+
traverse(val);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
};
|
|
2766
|
+
totalScanned++;
|
|
2767
|
+
const counts = {};
|
|
2768
|
+
traverse(entry);
|
|
2769
|
+
for (const [optKey, max] of Object.entries(quotas)) {
|
|
2770
|
+
const lowerOpt = optKey.toLowerCase();
|
|
2771
|
+
const actualCount = counts[lowerOpt] || 0;
|
|
2772
|
+
if (actualCount > max) {
|
|
2773
|
+
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
2774
|
+
violations.push({
|
|
2775
|
+
uid,
|
|
2776
|
+
collectionDisplayName: displayName,
|
|
2777
|
+
documentId: entry.documentId,
|
|
2778
|
+
entryLabel,
|
|
2779
|
+
violatingOption: optKey.toUpperCase(),
|
|
2780
|
+
foundCount: actualCount,
|
|
2781
|
+
maxAllowed: max,
|
|
2782
|
+
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2327
2785
|
}
|
|
2328
2786
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
const counts = {};
|
|
2332
|
-
traverse(entry);
|
|
2333
|
-
for (const [optKey, max] of Object.entries(quotas)) {
|
|
2334
|
-
const lowerOpt = optKey.toLowerCase();
|
|
2335
|
-
const actualCount = counts[lowerOpt] || 0;
|
|
2336
|
-
if (actualCount > max) {
|
|
2337
|
-
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
2338
|
-
violations.push({
|
|
2339
|
-
uid,
|
|
2340
|
-
collectionDisplayName: displayName,
|
|
2341
|
-
documentId: entry.documentId,
|
|
2342
|
-
entryLabel,
|
|
2343
|
-
violatingOption: optKey.toUpperCase(),
|
|
2344
|
-
foundCount: actualCount,
|
|
2345
|
-
maxAllowed: max,
|
|
2346
|
-
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
2347
|
-
});
|
|
2348
|
-
}
|
|
2787
|
+
if (entries.length < BATCH_SIZE) break;
|
|
2788
|
+
page++;
|
|
2349
2789
|
}
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
strapi2.log.warn(`[bulk-updater] Failed scanning ${uid} for rule ${ruleId}:`, err);
|
|
2350
2792
|
}
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2793
|
+
})
|
|
2794
|
+
);
|
|
2355
2795
|
return {
|
|
2356
2796
|
rule,
|
|
2357
2797
|
totalScanned,
|
|
@@ -2653,12 +3093,13 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2653
3093
|
throw new Error("CSV is empty or could not be parsed.");
|
|
2654
3094
|
}
|
|
2655
3095
|
const attrs = ct.attributes || {};
|
|
2656
|
-
const { mode: mode2, publish = true, user } = options;
|
|
3096
|
+
const { mode: mode2 = "auto", publish = true, user } = options;
|
|
2657
3097
|
const stats = {
|
|
2658
3098
|
total: parsedRows.length,
|
|
2659
3099
|
created: 0,
|
|
2660
3100
|
updated: 0,
|
|
2661
3101
|
skipped: 0,
|
|
3102
|
+
newWithoutDocId: 0,
|
|
2662
3103
|
errors: [],
|
|
2663
3104
|
diffs: []
|
|
2664
3105
|
};
|
|
@@ -2730,7 +3171,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2730
3171
|
stats.skipped++;
|
|
2731
3172
|
stats.errors.push({
|
|
2732
3173
|
row: rowNumber,
|
|
2733
|
-
error: "Missing documentId for Update mode."
|
|
3174
|
+
error: "Missing documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
|
|
2734
3175
|
});
|
|
2735
3176
|
continue;
|
|
2736
3177
|
}
|
|
@@ -2775,7 +3216,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2775
3216
|
status: "updated",
|
|
2776
3217
|
message: `Updated via CSV import (row ${rowNumber})`
|
|
2777
3218
|
});
|
|
2778
|
-
} else {
|
|
3219
|
+
} else if (mode2 === "create_or_override") {
|
|
2779
3220
|
if (docId) {
|
|
2780
3221
|
const existing = await strapi2.documents(uid).findOne({
|
|
2781
3222
|
documentId: docId
|
|
@@ -2825,6 +3266,73 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2825
3266
|
}
|
|
2826
3267
|
}
|
|
2827
3268
|
stats.created++;
|
|
3269
|
+
if (!docId) {
|
|
3270
|
+
stats.newWithoutDocId++;
|
|
3271
|
+
}
|
|
3272
|
+
stats.diffs.push({
|
|
3273
|
+
uid,
|
|
3274
|
+
collectionName: ct.info?.displayName || uid,
|
|
3275
|
+
documentId: newDocId,
|
|
3276
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
3277
|
+
attribute: Object.keys(payload).join(", "),
|
|
3278
|
+
oldValue: null,
|
|
3279
|
+
newValue: payload,
|
|
3280
|
+
status: "updated",
|
|
3281
|
+
message: docId ? `Created via CSV import (row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
3282
|
+
});
|
|
3283
|
+
} else {
|
|
3284
|
+
if (docId) {
|
|
3285
|
+
const existing = await strapi2.documents(uid).findOne({
|
|
3286
|
+
documentId: docId
|
|
3287
|
+
});
|
|
3288
|
+
if (existing) {
|
|
3289
|
+
const mergedPayload = { ...payload };
|
|
3290
|
+
for (const key of Object.keys(payload)) {
|
|
3291
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
3292
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
await strapi2.documents(uid).update({
|
|
3296
|
+
documentId: docId,
|
|
3297
|
+
data: mergedPayload
|
|
3298
|
+
});
|
|
3299
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
3300
|
+
try {
|
|
3301
|
+
await strapi2.documents(uid).publish({ documentId: docId });
|
|
3302
|
+
} catch (pubErr) {
|
|
3303
|
+
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
stats.updated++;
|
|
3307
|
+
stats.diffs.push({
|
|
3308
|
+
uid,
|
|
3309
|
+
collectionName: ct.info?.displayName || uid,
|
|
3310
|
+
documentId: docId,
|
|
3311
|
+
label: existing.title || existing.name || `Entry #${docId}`,
|
|
3312
|
+
attribute: Object.keys(payload).join(", "),
|
|
3313
|
+
oldValue: existing,
|
|
3314
|
+
newValue: payload,
|
|
3315
|
+
status: "updated",
|
|
3316
|
+
message: `Updated existing entry via smart match (row ${rowNumber})`
|
|
3317
|
+
});
|
|
3318
|
+
continue;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
const created = await strapi2.documents(uid).create({
|
|
3322
|
+
data: payload
|
|
3323
|
+
});
|
|
3324
|
+
const newDocId = created?.documentId || docId;
|
|
3325
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
3326
|
+
try {
|
|
3327
|
+
await strapi2.documents(uid).publish({ documentId: newDocId });
|
|
3328
|
+
} catch (pubErr) {
|
|
3329
|
+
strapi2.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
stats.created++;
|
|
3333
|
+
if (!docId) {
|
|
3334
|
+
stats.newWithoutDocId++;
|
|
3335
|
+
}
|
|
2828
3336
|
stats.diffs.push({
|
|
2829
3337
|
uid,
|
|
2830
3338
|
collectionName: ct.info?.displayName || uid,
|
|
@@ -2834,7 +3342,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2834
3342
|
oldValue: null,
|
|
2835
3343
|
newValue: payload,
|
|
2836
3344
|
status: "updated",
|
|
2837
|
-
message: `Created
|
|
3345
|
+
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})`
|
|
2838
3346
|
});
|
|
2839
3347
|
}
|
|
2840
3348
|
} catch (err) {
|