strapi-plugin-bulk-updater 1.2.0 → 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/README.md +8 -5
- package/dist/_chunks/{BulkUpdater-DCrCPKSe.js → BulkUpdater-2TTEWMXI.js} +1211 -190
- package/dist/_chunks/BulkUpdater-2TTEWMXI.js.map +1 -0
- package/dist/_chunks/{BulkUpdater-BDszahRM.mjs → BulkUpdater-DR8Z9z95.mjs} +1213 -192
- 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 +817 -129
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +817 -129
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/_chunks/BulkUpdater-BDszahRM.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-DCrCPKSe.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",
|
|
@@ -182,6 +206,78 @@ const routes = {
|
|
|
182
206
|
]
|
|
183
207
|
}
|
|
184
208
|
};
|
|
209
|
+
const auditLog = {
|
|
210
|
+
collectionName: "bulk_updater_audit_logs",
|
|
211
|
+
info: {
|
|
212
|
+
singularName: "audit-log",
|
|
213
|
+
pluralName: "audit-logs",
|
|
214
|
+
displayName: "Bulk Updater Audit Log",
|
|
215
|
+
description: "Internal audit trail for bulk attribute and relation updates"
|
|
216
|
+
},
|
|
217
|
+
options: {
|
|
218
|
+
draftAndPublish: false
|
|
219
|
+
},
|
|
220
|
+
pluginOptions: {
|
|
221
|
+
"content-manager": {
|
|
222
|
+
visible: true
|
|
223
|
+
},
|
|
224
|
+
"content-type-builder": {
|
|
225
|
+
visible: false
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
attributes: {
|
|
229
|
+
batchId: {
|
|
230
|
+
type: "string",
|
|
231
|
+
required: true
|
|
232
|
+
},
|
|
233
|
+
attribute: {
|
|
234
|
+
type: "string",
|
|
235
|
+
required: true
|
|
236
|
+
},
|
|
237
|
+
value: {
|
|
238
|
+
type: "text"
|
|
239
|
+
},
|
|
240
|
+
userEmail: {
|
|
241
|
+
type: "string"
|
|
242
|
+
},
|
|
243
|
+
userName: {
|
|
244
|
+
type: "string"
|
|
245
|
+
},
|
|
246
|
+
updatedCount: {
|
|
247
|
+
type: "integer",
|
|
248
|
+
default: 0
|
|
249
|
+
},
|
|
250
|
+
skippedCount: {
|
|
251
|
+
type: "integer",
|
|
252
|
+
default: 0
|
|
253
|
+
},
|
|
254
|
+
totalTargets: {
|
|
255
|
+
type: "integer",
|
|
256
|
+
default: 0
|
|
257
|
+
},
|
|
258
|
+
diffs: {
|
|
259
|
+
type: "json"
|
|
260
|
+
},
|
|
261
|
+
isRolledBack: {
|
|
262
|
+
type: "boolean",
|
|
263
|
+
default: false
|
|
264
|
+
},
|
|
265
|
+
rolledBackAt: {
|
|
266
|
+
type: "datetime"
|
|
267
|
+
},
|
|
268
|
+
rolledBackBy: {
|
|
269
|
+
type: "string"
|
|
270
|
+
},
|
|
271
|
+
executedAt: {
|
|
272
|
+
type: "datetime"
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const contentTypes = {
|
|
277
|
+
"audit-log": {
|
|
278
|
+
schema: auditLog
|
|
279
|
+
}
|
|
280
|
+
};
|
|
185
281
|
const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
186
282
|
async getModels(ctx) {
|
|
187
283
|
try {
|
|
@@ -304,6 +400,39 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
304
400
|
ctx.status = 400;
|
|
305
401
|
ctx.body = { success: false, error: err.message };
|
|
306
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
|
+
}
|
|
307
436
|
}
|
|
308
437
|
});
|
|
309
438
|
const auditController = ({ strapi: strapi2 }) => ({
|
|
@@ -476,7 +605,7 @@ const csvController = ({ strapi: strapi2 }) => ({
|
|
|
476
605
|
}
|
|
477
606
|
const csvService2 = strapi2.plugin("bulk-updater").service("csv");
|
|
478
607
|
const result = await csvService2.importCsv(uid, csvText, {
|
|
479
|
-
mode: mode2 === "create_or_override" ? "create_or_override" : "
|
|
608
|
+
mode: mode2 === "update" ? "update" : mode2 === "create_or_override" ? "create_or_override" : "auto",
|
|
480
609
|
publish: publish !== false,
|
|
481
610
|
user: ctx.state.user
|
|
482
611
|
});
|
|
@@ -549,6 +678,7 @@ function checkBusinessHours(config, testDate) {
|
|
|
549
678
|
};
|
|
550
679
|
}
|
|
551
680
|
const pendingBatches = /* @__PURE__ */ new Map();
|
|
681
|
+
const batchJobsMap = /* @__PURE__ */ new Map();
|
|
552
682
|
let isRunnerRunning = false;
|
|
553
683
|
let runnerInterval = null;
|
|
554
684
|
function invalidateCollectionCache(uid) {
|
|
@@ -648,6 +778,31 @@ async function executeBatchInternal(strapi2, {
|
|
|
648
778
|
const results = [];
|
|
649
779
|
let updatedCount = 0;
|
|
650
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);
|
|
651
806
|
let regexObj = null;
|
|
652
807
|
if (isRegex && regexPattern) {
|
|
653
808
|
regexObj = new RegExp(regexPattern, "g");
|
|
@@ -882,11 +1037,24 @@ async function executeBatchInternal(strapi2, {
|
|
|
882
1037
|
else if (res.status === "skipped") skippedCount++;
|
|
883
1038
|
results.push(res);
|
|
884
1039
|
}
|
|
1040
|
+
jobState.processedCount = results.length;
|
|
1041
|
+
jobState.updatedCount = updatedCount;
|
|
1042
|
+
jobState.skippedCount = skippedCount;
|
|
1043
|
+
jobState.failedCount = results.filter((r) => r.status === "error").length;
|
|
885
1044
|
}
|
|
886
1045
|
const affectedUids = new Set(targets.map((t) => t.uid));
|
|
887
1046
|
affectedUids.forEach((uid) => {
|
|
888
1047
|
invalidateCollectionCache(uid);
|
|
889
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
|
+
}));
|
|
890
1058
|
const batchSummary = {
|
|
891
1059
|
batchId,
|
|
892
1060
|
attribute,
|
|
@@ -908,7 +1076,10 @@ async function executeBatchInternal(strapi2, {
|
|
|
908
1076
|
value,
|
|
909
1077
|
updatedCount,
|
|
910
1078
|
skippedCount,
|
|
1079
|
+
failedCount,
|
|
911
1080
|
totalTargets: targetsMap.size,
|
|
1081
|
+
isLive: jobState.isLive,
|
|
1082
|
+
status: jobState.status,
|
|
912
1083
|
results
|
|
913
1084
|
};
|
|
914
1085
|
}
|
|
@@ -1036,69 +1207,87 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1036
1207
|
}
|
|
1037
1208
|
const collections = [];
|
|
1038
1209
|
const signatures = [];
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
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;
|
|
1069
1239
|
});
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
totalCount = 1;
|
|
1073
|
-
}
|
|
1074
|
-
} else {
|
|
1075
|
-
totalCount = await strapi2.documents(uid).count();
|
|
1076
|
-
entries = await strapi2.documents(uid).findMany({
|
|
1077
|
-
limit: 10,
|
|
1078
|
-
sort: { createdAt: "desc" },
|
|
1079
|
-
populate: populateOption
|
|
1240
|
+
compKeys.forEach((k) => {
|
|
1241
|
+
populateOptionObj[k] = true;
|
|
1080
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;
|
|
1081
1264
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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);
|
|
1102
1291
|
}
|
|
1103
1292
|
}
|
|
1104
1293
|
const executionConfig = getExecutionWindowConfig(strapi2);
|
|
@@ -1417,6 +1606,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1417
1606
|
makerNotes
|
|
1418
1607
|
};
|
|
1419
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
|
+
});
|
|
1420
1627
|
return {
|
|
1421
1628
|
success: true,
|
|
1422
1629
|
pending: true,
|
|
@@ -1456,6 +1663,25 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1456
1663
|
makerNotes
|
|
1457
1664
|
};
|
|
1458
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
|
+
});
|
|
1459
1685
|
return {
|
|
1460
1686
|
success: true,
|
|
1461
1687
|
pending: true,
|
|
@@ -1496,6 +1722,26 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1496
1722
|
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
1497
1723
|
};
|
|
1498
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
|
+
});
|
|
1499
1745
|
return {
|
|
1500
1746
|
success: true,
|
|
1501
1747
|
pending: true,
|
|
@@ -1628,6 +1874,226 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1628
1874
|
result
|
|
1629
1875
|
};
|
|
1630
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
|
+
},
|
|
1631
2097
|
invalidateCache(uid) {
|
|
1632
2098
|
if (uid) {
|
|
1633
2099
|
invalidateCollectionCache(uid);
|
|
@@ -1658,6 +2124,7 @@ const DEFAULT_FIELDS = {
|
|
|
1658
2124
|
executedAt: "executedAt"
|
|
1659
2125
|
};
|
|
1660
2126
|
const DEFAULT_CANDIDATE_TYPES = [
|
|
2127
|
+
"plugin::bulk-updater.audit-log",
|
|
1661
2128
|
"api::audit-log.audit-log",
|
|
1662
2129
|
"api::audit.audit",
|
|
1663
2130
|
"api::bulk-audit-log.bulk-audit-log"
|
|
@@ -1679,6 +2146,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1679
2146
|
* 1. Explicit consumer custom schema (full declarative control)
|
|
1680
2147
|
* 2. Zero-config convention auto-detection (api::audit-log.audit-log)
|
|
1681
2148
|
* 3. Strapi Enterprise native audit log bridge
|
|
2149
|
+
* 4. Zero-config plugin core store (works out of the box in 100% of Strapi installations)
|
|
1682
2150
|
*/
|
|
1683
2151
|
getAuditConfig() {
|
|
1684
2152
|
const rawConfig = strapi2.config.get("plugin::bulk-updater.audit");
|
|
@@ -1732,10 +2200,9 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1732
2200
|
};
|
|
1733
2201
|
}
|
|
1734
2202
|
return {
|
|
1735
|
-
enabled:
|
|
1736
|
-
mode: "
|
|
1737
|
-
isEnterpriseNativeAvailable: false
|
|
1738
|
-
error: "No audit collection configured or detected. To enable rollback, provide a collection or use Strapi Enterprise."
|
|
2203
|
+
enabled: true,
|
|
2204
|
+
mode: "store",
|
|
2205
|
+
isEnterpriseNativeAvailable: false
|
|
1739
2206
|
};
|
|
1740
2207
|
},
|
|
1741
2208
|
/**
|
|
@@ -1780,7 +2247,40 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1780
2247
|
},
|
|
1781
2248
|
user
|
|
1782
2249
|
);
|
|
1783
|
-
if (!config.enabled
|
|
2250
|
+
if (!config.enabled) {
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
if (config.mode === "store") {
|
|
2254
|
+
try {
|
|
2255
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
2256
|
+
const stored = await store.get({}) || { history: [] };
|
|
2257
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
2258
|
+
const newRecord = {
|
|
2259
|
+
id: batchSummary.batchId,
|
|
2260
|
+
documentId: batchSummary.batchId,
|
|
2261
|
+
batchId: batchSummary.batchId,
|
|
2262
|
+
attribute: batchSummary.attribute,
|
|
2263
|
+
value: batchSummary.value,
|
|
2264
|
+
userEmail: user?.email || "admin@strapi.io",
|
|
2265
|
+
userName: user?.firstname ? `${user.firstname} ${user.lastname || ""}`.trim() : user?.email || "Admin",
|
|
2266
|
+
updatedCount: batchSummary.updatedCount,
|
|
2267
|
+
skippedCount: batchSummary.skippedCount,
|
|
2268
|
+
totalTargets: batchSummary.totalTargets,
|
|
2269
|
+
diffs: results,
|
|
2270
|
+
isRolledBack: false,
|
|
2271
|
+
rolledBackAt: null,
|
|
2272
|
+
rolledBackBy: null,
|
|
2273
|
+
executedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2274
|
+
};
|
|
2275
|
+
const updatedHistory = [newRecord, ...history].slice(0, 200);
|
|
2276
|
+
await store.set({ value: { history: updatedHistory } });
|
|
2277
|
+
return newRecord;
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
strapi2.log.error(`[bulk-updater] Failed to record audit log in core store: ${err.message}`);
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
if (!config.contentType || !config.fields) {
|
|
1784
2284
|
return null;
|
|
1785
2285
|
}
|
|
1786
2286
|
const f = config.fields;
|
|
@@ -1821,6 +2321,25 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1821
2321
|
history: []
|
|
1822
2322
|
};
|
|
1823
2323
|
}
|
|
2324
|
+
if (config.mode === "store") {
|
|
2325
|
+
try {
|
|
2326
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
2327
|
+
const stored = await store.get({}) || { history: [] };
|
|
2328
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
2329
|
+
return {
|
|
2330
|
+
enabled: true,
|
|
2331
|
+
mode: "store",
|
|
2332
|
+
isEnterpriseNativeAvailable: config.isEnterpriseNativeAvailable,
|
|
2333
|
+
history
|
|
2334
|
+
};
|
|
2335
|
+
} catch (err) {
|
|
2336
|
+
return {
|
|
2337
|
+
enabled: false,
|
|
2338
|
+
message: `Failed to read audit history from core store: ${err.message}`,
|
|
2339
|
+
history: []
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
1824
2343
|
if (!config.enabled || !config.contentType || !config.fields) {
|
|
1825
2344
|
return {
|
|
1826
2345
|
enabled: false,
|
|
@@ -1866,27 +2385,9 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1866
2385
|
}
|
|
1867
2386
|
},
|
|
1868
2387
|
/**
|
|
1869
|
-
*
|
|
2388
|
+
* Helper to revert diff items back to original values
|
|
1870
2389
|
*/
|
|
1871
|
-
async
|
|
1872
|
-
const config = this.getAuditConfig();
|
|
1873
|
-
if (!config.enabled || !config.contentType || !config.fields) {
|
|
1874
|
-
throw new Error("Audit trail is not configured or disabled.");
|
|
1875
|
-
}
|
|
1876
|
-
const f = config.fields;
|
|
1877
|
-
const records = await strapi2.documents(config.contentType).findMany({
|
|
1878
|
-
filters: {
|
|
1879
|
-
[f.batchId]: { $eq: batchId }
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
const record = records?.[0];
|
|
1883
|
-
if (!record) {
|
|
1884
|
-
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
1885
|
-
}
|
|
1886
|
-
if (record[f.isRolledBack]) {
|
|
1887
|
-
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
1888
|
-
}
|
|
1889
|
-
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
2390
|
+
async revertDiffItems(diffs) {
|
|
1890
2391
|
let revertedCount = 0;
|
|
1891
2392
|
const rollbackDetails = [];
|
|
1892
2393
|
for (const item of diffs) {
|
|
@@ -1971,6 +2472,78 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1971
2472
|
}
|
|
1972
2473
|
}
|
|
1973
2474
|
}
|
|
2475
|
+
return { revertedCount, rollbackDetails };
|
|
2476
|
+
},
|
|
2477
|
+
/**
|
|
2478
|
+
* Performs deterministic rollback of a batch
|
|
2479
|
+
*/
|
|
2480
|
+
async rollbackBatch(batchId, currentUser) {
|
|
2481
|
+
const config = this.getAuditConfig();
|
|
2482
|
+
if (!config.enabled) {
|
|
2483
|
+
throw new Error("Audit trail is not configured or disabled.");
|
|
2484
|
+
}
|
|
2485
|
+
if (config.mode === "store") {
|
|
2486
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
2487
|
+
const stored = await store.get({}) || { history: [] };
|
|
2488
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
2489
|
+
const recordIndex = history.findIndex((r) => r.batchId === batchId);
|
|
2490
|
+
if (recordIndex === -1) {
|
|
2491
|
+
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
2492
|
+
}
|
|
2493
|
+
const record2 = history[recordIndex];
|
|
2494
|
+
if (record2.isRolledBack) {
|
|
2495
|
+
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
2496
|
+
}
|
|
2497
|
+
const diffs2 = Array.isArray(record2.diffs) ? record2.diffs : [];
|
|
2498
|
+
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2);
|
|
2499
|
+
record2.isRolledBack = true;
|
|
2500
|
+
record2.rolledBackAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2501
|
+
record2.rolledBackBy = currentUser?.email || "Admin";
|
|
2502
|
+
history[recordIndex] = record2;
|
|
2503
|
+
await store.set({ value: { history } });
|
|
2504
|
+
try {
|
|
2505
|
+
const affectedUids = new Set(rollbackDetails2.map((d) => d.uid));
|
|
2506
|
+
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
2507
|
+
affectedUids.forEach((uid) => {
|
|
2508
|
+
if (uid && bulkService?.invalidateCache) {
|
|
2509
|
+
bulkService.invalidateCache(uid);
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
await this.recordToEnterpriseNative(
|
|
2515
|
+
"bulk-updater.rollback",
|
|
2516
|
+
{
|
|
2517
|
+
batchId,
|
|
2518
|
+
revertedCount: revertedCount2
|
|
2519
|
+
},
|
|
2520
|
+
currentUser
|
|
2521
|
+
);
|
|
2522
|
+
return {
|
|
2523
|
+
success: true,
|
|
2524
|
+
batchId,
|
|
2525
|
+
revertedCount: revertedCount2,
|
|
2526
|
+
details: rollbackDetails2
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
if (!config.contentType || !config.fields) {
|
|
2530
|
+
throw new Error("Audit trail is not configured or disabled.");
|
|
2531
|
+
}
|
|
2532
|
+
const f = config.fields;
|
|
2533
|
+
const records = await strapi2.documents(config.contentType).findMany({
|
|
2534
|
+
filters: {
|
|
2535
|
+
[f.batchId]: { $eq: batchId }
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
const record = records?.[0];
|
|
2539
|
+
if (!record) {
|
|
2540
|
+
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
2541
|
+
}
|
|
2542
|
+
if (record[f.isRolledBack]) {
|
|
2543
|
+
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
2544
|
+
}
|
|
2545
|
+
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
2546
|
+
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs);
|
|
1974
2547
|
const updateData = {};
|
|
1975
2548
|
if (f.isRolledBack) updateData[f.isRolledBack] = true;
|
|
1976
2549
|
if (f.rolledBackAt) updateData[f.rolledBackAt] = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2123,56 +2696,102 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
|
|
|
2123
2696
|
const violations = [];
|
|
2124
2697
|
let totalScanned = 0;
|
|
2125
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
|
+
}
|
|
2126
2724
|
const candidateUids = Object.keys(allContentTypes).filter(
|
|
2127
2725
|
(uid) => uid.startsWith("api::") && !uid.includes("bulk-updater")
|
|
2128
2726
|
);
|
|
2727
|
+
const relevantCollections = [];
|
|
2129
2728
|
for (const uid of candidateUids) {
|
|
2130
2729
|
const ct = allContentTypes[uid];
|
|
2131
2730
|
if (!ct) continue;
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
|
2136
2737
|
});
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
+
}
|
|
2148
2785
|
}
|
|
2149
2786
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
const counts = {};
|
|
2153
|
-
traverse(entry);
|
|
2154
|
-
for (const [optKey, max] of Object.entries(quotas)) {
|
|
2155
|
-
const lowerOpt = optKey.toLowerCase();
|
|
2156
|
-
const actualCount = counts[lowerOpt] || 0;
|
|
2157
|
-
if (actualCount > max) {
|
|
2158
|
-
const entryLabel = entry.title || entry.name || entry.email || `Entry #${entry.documentId}`;
|
|
2159
|
-
violations.push({
|
|
2160
|
-
uid,
|
|
2161
|
-
collectionDisplayName: displayName,
|
|
2162
|
-
documentId: entry.documentId,
|
|
2163
|
-
entryLabel,
|
|
2164
|
-
violatingOption: optKey.toUpperCase(),
|
|
2165
|
-
foundCount: actualCount,
|
|
2166
|
-
maxAllowed: max,
|
|
2167
|
-
editUrl: `/admin/content-manager/collection-types/${uid}/${entry.documentId}`
|
|
2168
|
-
});
|
|
2169
|
-
}
|
|
2787
|
+
if (entries.length < BATCH_SIZE) break;
|
|
2788
|
+
page++;
|
|
2170
2789
|
}
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
strapi2.log.warn(`[bulk-updater] Failed scanning ${uid} for rule ${ruleId}:`, err);
|
|
2171
2792
|
}
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2793
|
+
})
|
|
2794
|
+
);
|
|
2176
2795
|
return {
|
|
2177
2796
|
rule,
|
|
2178
2797
|
totalScanned,
|
|
@@ -2474,12 +3093,13 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2474
3093
|
throw new Error("CSV is empty or could not be parsed.");
|
|
2475
3094
|
}
|
|
2476
3095
|
const attrs = ct.attributes || {};
|
|
2477
|
-
const { mode: mode2, publish = true, user } = options;
|
|
3096
|
+
const { mode: mode2 = "auto", publish = true, user } = options;
|
|
2478
3097
|
const stats = {
|
|
2479
3098
|
total: parsedRows.length,
|
|
2480
3099
|
created: 0,
|
|
2481
3100
|
updated: 0,
|
|
2482
3101
|
skipped: 0,
|
|
3102
|
+
newWithoutDocId: 0,
|
|
2483
3103
|
errors: [],
|
|
2484
3104
|
diffs: []
|
|
2485
3105
|
};
|
|
@@ -2551,7 +3171,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2551
3171
|
stats.skipped++;
|
|
2552
3172
|
stats.errors.push({
|
|
2553
3173
|
row: rowNumber,
|
|
2554
|
-
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)."
|
|
2555
3175
|
});
|
|
2556
3176
|
continue;
|
|
2557
3177
|
}
|
|
@@ -2596,7 +3216,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2596
3216
|
status: "updated",
|
|
2597
3217
|
message: `Updated via CSV import (row ${rowNumber})`
|
|
2598
3218
|
});
|
|
2599
|
-
} else {
|
|
3219
|
+
} else if (mode2 === "create_or_override") {
|
|
2600
3220
|
if (docId) {
|
|
2601
3221
|
const existing = await strapi2.documents(uid).findOne({
|
|
2602
3222
|
documentId: docId
|
|
@@ -2646,6 +3266,73 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2646
3266
|
}
|
|
2647
3267
|
}
|
|
2648
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
|
+
}
|
|
2649
3336
|
stats.diffs.push({
|
|
2650
3337
|
uid,
|
|
2651
3338
|
collectionName: ct.info?.displayName || uid,
|
|
@@ -2655,7 +3342,7 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2655
3342
|
oldValue: null,
|
|
2656
3343
|
newValue: payload,
|
|
2657
3344
|
status: "updated",
|
|
2658
|
-
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})`
|
|
2659
3346
|
});
|
|
2660
3347
|
}
|
|
2661
3348
|
} catch (err) {
|
|
@@ -32213,6 +32900,7 @@ const index = {
|
|
|
32213
32900
|
});
|
|
32214
32901
|
},
|
|
32215
32902
|
routes,
|
|
32903
|
+
contentTypes,
|
|
32216
32904
|
controllers: {
|
|
32217
32905
|
bulkUpdater: bulkUpdaterController,
|
|
32218
32906
|
audit: auditController,
|