strapi-plugin-bulk-updater 1.1.0 → 1.2.3
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 +15 -6
- package/dist/_chunks/{BulkUpdater-BSZP0U18.mjs → BulkUpdater-BYtO2SHc.mjs} +199 -127
- package/dist/_chunks/BulkUpdater-BYtO2SHc.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-BRcMQGP3.js → BulkUpdater-BdRJhVFh.js} +196 -124
- package/dist/_chunks/BulkUpdater-BdRJhVFh.js.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +524 -88
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +524 -88
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/_chunks/BulkUpdater-BRcMQGP3.js.map +0 -1
- package/dist/_chunks/BulkUpdater-BSZP0U18.mjs.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -182,6 +182,78 @@ const routes = {
|
|
|
182
182
|
]
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
|
+
const auditLog = {
|
|
186
|
+
collectionName: "bulk_updater_audit_logs",
|
|
187
|
+
info: {
|
|
188
|
+
singularName: "audit-log",
|
|
189
|
+
pluralName: "audit-logs",
|
|
190
|
+
displayName: "Bulk Updater Audit Log",
|
|
191
|
+
description: "Internal audit trail for bulk attribute and relation updates"
|
|
192
|
+
},
|
|
193
|
+
options: {
|
|
194
|
+
draftAndPublish: false
|
|
195
|
+
},
|
|
196
|
+
pluginOptions: {
|
|
197
|
+
"content-manager": {
|
|
198
|
+
visible: true
|
|
199
|
+
},
|
|
200
|
+
"content-type-builder": {
|
|
201
|
+
visible: false
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
attributes: {
|
|
205
|
+
batchId: {
|
|
206
|
+
type: "string",
|
|
207
|
+
required: true
|
|
208
|
+
},
|
|
209
|
+
attribute: {
|
|
210
|
+
type: "string",
|
|
211
|
+
required: true
|
|
212
|
+
},
|
|
213
|
+
value: {
|
|
214
|
+
type: "text"
|
|
215
|
+
},
|
|
216
|
+
userEmail: {
|
|
217
|
+
type: "string"
|
|
218
|
+
},
|
|
219
|
+
userName: {
|
|
220
|
+
type: "string"
|
|
221
|
+
},
|
|
222
|
+
updatedCount: {
|
|
223
|
+
type: "integer",
|
|
224
|
+
default: 0
|
|
225
|
+
},
|
|
226
|
+
skippedCount: {
|
|
227
|
+
type: "integer",
|
|
228
|
+
default: 0
|
|
229
|
+
},
|
|
230
|
+
totalTargets: {
|
|
231
|
+
type: "integer",
|
|
232
|
+
default: 0
|
|
233
|
+
},
|
|
234
|
+
diffs: {
|
|
235
|
+
type: "json"
|
|
236
|
+
},
|
|
237
|
+
isRolledBack: {
|
|
238
|
+
type: "boolean",
|
|
239
|
+
default: false
|
|
240
|
+
},
|
|
241
|
+
rolledBackAt: {
|
|
242
|
+
type: "datetime"
|
|
243
|
+
},
|
|
244
|
+
rolledBackBy: {
|
|
245
|
+
type: "string"
|
|
246
|
+
},
|
|
247
|
+
executedAt: {
|
|
248
|
+
type: "datetime"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const contentTypes = {
|
|
253
|
+
"audit-log": {
|
|
254
|
+
schema: auditLog
|
|
255
|
+
}
|
|
256
|
+
};
|
|
185
257
|
const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
186
258
|
async getModels(ctx) {
|
|
187
259
|
try {
|
|
@@ -224,7 +296,7 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
224
296
|
return;
|
|
225
297
|
}
|
|
226
298
|
}
|
|
227
|
-
ctx.body = data;
|
|
299
|
+
ctx.body = { success: true, ...data };
|
|
228
300
|
} catch (err) {
|
|
229
301
|
ctx.status = 500;
|
|
230
302
|
ctx.body = { success: false, error: err.message };
|
|
@@ -673,9 +745,69 @@ async function executeBatchInternal(strapi2, {
|
|
|
673
745
|
try {
|
|
674
746
|
const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
|
|
675
747
|
const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
|
|
748
|
+
const isNested = attribute.includes(".");
|
|
749
|
+
let isComponentNested = false;
|
|
750
|
+
let parentKey = "";
|
|
751
|
+
let childKey = "";
|
|
752
|
+
if (isNested) {
|
|
753
|
+
const parts = attribute.split(".");
|
|
754
|
+
parentKey = parts[0];
|
|
755
|
+
childKey = parts[1];
|
|
756
|
+
const parentAttrDef = contentType?.attributes?.[parentKey];
|
|
757
|
+
if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
|
|
758
|
+
const compSchema = strapi2.components[parentAttrDef.component];
|
|
759
|
+
if (compSchema?.attributes?.[childKey]) {
|
|
760
|
+
isComponentNested = true;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
676
764
|
const attrDef = contentType?.attributes?.[attribute];
|
|
677
|
-
const hasAttribute = Boolean(attrDef);
|
|
765
|
+
const hasAttribute = Boolean(attrDef) || isComponentNested;
|
|
678
766
|
if (hasAttribute) {
|
|
767
|
+
if (isComponentNested) {
|
|
768
|
+
const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
|
|
769
|
+
documentId: target.documentId,
|
|
770
|
+
populate: [parentKey]
|
|
771
|
+
});
|
|
772
|
+
const existingParent = currentEntryWithPopulate?.[parentKey] || {};
|
|
773
|
+
const currentVal2 = existingParent?.[childKey];
|
|
774
|
+
let newValue2 = value;
|
|
775
|
+
if (regexObj && typeof currentVal2 === "string") {
|
|
776
|
+
newValue2 = currentVal2.replace(regexObj, value);
|
|
777
|
+
}
|
|
778
|
+
const updatedParent = {
|
|
779
|
+
...existingParent,
|
|
780
|
+
[childKey]: newValue2
|
|
781
|
+
};
|
|
782
|
+
await strapi2.documents(target.uid).update({
|
|
783
|
+
documentId: target.documentId,
|
|
784
|
+
data: {
|
|
785
|
+
[parentKey]: updatedParent
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
|
|
789
|
+
if (publishOnApproval && isDraftAndPublishComp) {
|
|
790
|
+
try {
|
|
791
|
+
await strapi2.documents(target.uid).publish({
|
|
792
|
+
documentId: target.documentId
|
|
793
|
+
});
|
|
794
|
+
} catch (publishErr) {
|
|
795
|
+
strapi2.log.warn(`[bulk-updater] Failed to auto-publish ${target.uid}/${target.documentId}:`, publishErr);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
invalidateCollectionCache(target.uid);
|
|
799
|
+
return {
|
|
800
|
+
uid: target.uid,
|
|
801
|
+
collectionName: displayName,
|
|
802
|
+
documentId: target.documentId,
|
|
803
|
+
label: entryLabel,
|
|
804
|
+
attribute,
|
|
805
|
+
oldValue: currentVal2 ?? null,
|
|
806
|
+
newValue: newValue2,
|
|
807
|
+
status: "updated",
|
|
808
|
+
message: `Successfully updated nested field ${attribute}`
|
|
809
|
+
};
|
|
810
|
+
}
|
|
679
811
|
if (attrDef.type === "relation") {
|
|
680
812
|
const isMulti = attrDef.relation === "oneToMany" || attrDef.relation === "manyToMany";
|
|
681
813
|
const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
|
|
@@ -917,9 +1049,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
917
1049
|
const isSingleType = contentType.kind === "singleType";
|
|
918
1050
|
const rawAttrs = contentType.attributes || {};
|
|
919
1051
|
const relationsByAttr = {};
|
|
1052
|
+
const nestedComponentAttrs = [];
|
|
920
1053
|
const validAttrs = Object.keys(rawAttrs).filter((attrKey) => {
|
|
921
1054
|
if (systemFields.includes(attrKey)) return false;
|
|
922
|
-
if (rawAttrs[attrKey].type === "
|
|
1055
|
+
if (rawAttrs[attrKey].type === "dynamiczone") return false;
|
|
1056
|
+
if (rawAttrs[attrKey].type === "component") {
|
|
1057
|
+
const compDef = rawAttrs[attrKey];
|
|
1058
|
+
if (!compDef.repeatable && compDef.component) {
|
|
1059
|
+
const componentSchema = strapi2.components[compDef.component];
|
|
1060
|
+
if (componentSchema && componentSchema.attributes) {
|
|
1061
|
+
for (const [subKey, subAttr] of Object.entries(componentSchema.attributes)) {
|
|
1062
|
+
if (systemFields.includes(subKey)) continue;
|
|
1063
|
+
if (["component", "dynamiczone", "relation"].includes(subAttr.type)) continue;
|
|
1064
|
+
nestedComponentAttrs.push(`${attrKey}.${subKey}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
923
1070
|
if (rawAttrs[attrKey].type === "relation") {
|
|
924
1071
|
const rel = rawAttrs[attrKey];
|
|
925
1072
|
const targetUid = rel.target;
|
|
@@ -939,7 +1086,8 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
939
1086
|
}
|
|
940
1087
|
return true;
|
|
941
1088
|
});
|
|
942
|
-
validAttrs
|
|
1089
|
+
const allColAttrs = [...validAttrs, ...nestedComponentAttrs];
|
|
1090
|
+
allColAttrs.forEach((attr) => allAttributesSet.add(attr));
|
|
943
1091
|
metaByUid[uid] = {
|
|
944
1092
|
uid,
|
|
945
1093
|
kind: contentType.kind,
|
|
@@ -947,7 +1095,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
947
1095
|
singularName: contentType.info?.singularName || uid,
|
|
948
1096
|
pluralName: contentType.info?.pluralName || uid,
|
|
949
1097
|
displayName: contentType.info?.displayName || uid,
|
|
950
|
-
attributes:
|
|
1098
|
+
attributes: allColAttrs,
|
|
951
1099
|
relations: relationsByAttr
|
|
952
1100
|
};
|
|
953
1101
|
}
|
|
@@ -976,8 +1124,17 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
976
1124
|
let entries = [];
|
|
977
1125
|
let totalCount = 0;
|
|
978
1126
|
try {
|
|
1127
|
+
const ctAttrs = allContentTypes[uid]?.attributes || {};
|
|
979
1128
|
const relKeys = Object.keys(meta.relations || {});
|
|
980
|
-
const
|
|
1129
|
+
const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
|
|
1130
|
+
const populateOptionObj = {};
|
|
1131
|
+
relKeys.forEach((k) => {
|
|
1132
|
+
populateOptionObj[k] = true;
|
|
1133
|
+
});
|
|
1134
|
+
compKeys.forEach((k) => {
|
|
1135
|
+
populateOptionObj[k] = true;
|
|
1136
|
+
});
|
|
1137
|
+
const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
|
|
981
1138
|
if (meta.isSingleType) {
|
|
982
1139
|
const singleDoc = await strapi2.documents(uid).findFirst({
|
|
983
1140
|
populate: populateOption
|
|
@@ -1055,7 +1212,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1055
1212
|
if (!contentType) {
|
|
1056
1213
|
throw new Error(`Collection with UID "${uid}" does not exist.`);
|
|
1057
1214
|
}
|
|
1058
|
-
const limit = Math.min(Math.max(Number(pageSize) || 10, 1),
|
|
1215
|
+
const limit = Math.min(Math.max(Number(pageSize) || 10, 1), 500);
|
|
1059
1216
|
const currentPage = Math.max(Number(page) || 1, 1);
|
|
1060
1217
|
const start = (currentPage - 1) * limit;
|
|
1061
1218
|
const isSingleType = contentType.kind === "singleType";
|
|
@@ -1071,8 +1228,118 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1071
1228
|
};
|
|
1072
1229
|
}
|
|
1073
1230
|
let result;
|
|
1231
|
+
const filters2 = {};
|
|
1232
|
+
const attrs = contentType.attributes || {};
|
|
1233
|
+
let colHasAttribute = false;
|
|
1234
|
+
let isNestedAttr = false;
|
|
1235
|
+
let parentKey = "";
|
|
1236
|
+
let childKey = "";
|
|
1237
|
+
if (attribute) {
|
|
1238
|
+
if (attribute.includes(".")) {
|
|
1239
|
+
[parentKey, childKey] = attribute.split(".");
|
|
1240
|
+
if (attrs[parentKey]?.type === "component" && !attrs[parentKey].repeatable && attrs[parentKey].component) {
|
|
1241
|
+
const compSchema = strapi2.components[attrs[parentKey].component];
|
|
1242
|
+
if (compSchema?.attributes?.[childKey]) {
|
|
1243
|
+
colHasAttribute = true;
|
|
1244
|
+
isNestedAttr = true;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
} else if (attrs[attribute]) {
|
|
1248
|
+
colHasAttribute = true;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const normOp = (filterOperator || "").toLowerCase().replace(/_/g, "");
|
|
1252
|
+
if (normOp && normOp !== "none") {
|
|
1253
|
+
if (!colHasAttribute) {
|
|
1254
|
+
result = {
|
|
1255
|
+
uid,
|
|
1256
|
+
displayName: contentType.info?.displayName || uid,
|
|
1257
|
+
isSingleType,
|
|
1258
|
+
entries: [],
|
|
1259
|
+
pagination: {
|
|
1260
|
+
page: 1,
|
|
1261
|
+
pageSize: limit,
|
|
1262
|
+
total: 0,
|
|
1263
|
+
pageCount: 0
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
queryCache.set(cacheKey, { signature, data: result, timestamp: Date.now() });
|
|
1267
|
+
return { ...result, cached: false, etag };
|
|
1268
|
+
}
|
|
1269
|
+
let condition = void 0;
|
|
1270
|
+
const q = (filterQuery || "").trim();
|
|
1271
|
+
if (normOp === "empty") {
|
|
1272
|
+
condition = { $or: [{ $null: true }, { $eq: "" }] };
|
|
1273
|
+
} else if (normOp === "notempty") {
|
|
1274
|
+
condition = { $and: [{ $notNull: true }, { $ne: "" }] };
|
|
1275
|
+
} else if (q !== "") {
|
|
1276
|
+
switch (normOp) {
|
|
1277
|
+
case "equals":
|
|
1278
|
+
condition = { $containsi: q };
|
|
1279
|
+
break;
|
|
1280
|
+
case "notequals":
|
|
1281
|
+
condition = { $notContainsi: q };
|
|
1282
|
+
break;
|
|
1283
|
+
case "contains":
|
|
1284
|
+
condition = { $containsi: q };
|
|
1285
|
+
break;
|
|
1286
|
+
case "notcontains":
|
|
1287
|
+
condition = { $notContainsi: q };
|
|
1288
|
+
break;
|
|
1289
|
+
case "startswith":
|
|
1290
|
+
condition = { $startsWith: q };
|
|
1291
|
+
break;
|
|
1292
|
+
case "endswith":
|
|
1293
|
+
condition = { $endsWith: q };
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (condition !== void 0) {
|
|
1298
|
+
if (isNestedAttr) {
|
|
1299
|
+
filters2[parentKey] = {
|
|
1300
|
+
[childKey]: condition
|
|
1301
|
+
};
|
|
1302
|
+
} else {
|
|
1303
|
+
if (normOp === "empty") {
|
|
1304
|
+
filters2.$or = [
|
|
1305
|
+
{ [attribute]: { $null: true } },
|
|
1306
|
+
{ [attribute]: { $eq: "" } }
|
|
1307
|
+
];
|
|
1308
|
+
} else if (normOp === "notempty") {
|
|
1309
|
+
filters2[attribute] = { $notNull: true, $ne: "" };
|
|
1310
|
+
} else {
|
|
1311
|
+
filters2[attribute] = condition;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (search && search.trim() !== "") {
|
|
1317
|
+
const q = search.trim();
|
|
1318
|
+
const searchableAttrs = Object.keys(attrs).filter(
|
|
1319
|
+
(k) => ["string", "text", "email", "uid"].includes(attrs[k]?.type)
|
|
1320
|
+
);
|
|
1321
|
+
if (searchableAttrs.length > 0) {
|
|
1322
|
+
const orList = searchableAttrs.slice(0, 6).map((k) => ({
|
|
1323
|
+
[k]: { $containsi: q }
|
|
1324
|
+
}));
|
|
1325
|
+
if (Object.keys(filters2).length > 0) {
|
|
1326
|
+
const existingFilters = { ...filters2 };
|
|
1327
|
+
Object.keys(filters2).forEach((k) => delete filters2[k]);
|
|
1328
|
+
filters2.$and = [existingFilters, { $or: orList }];
|
|
1329
|
+
} else {
|
|
1330
|
+
filters2.$or = orList;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1074
1334
|
if (isSingleType) {
|
|
1075
|
-
|
|
1335
|
+
let singleDoc = null;
|
|
1336
|
+
try {
|
|
1337
|
+
singleDoc = await strapi2.documents(uid).findFirst({
|
|
1338
|
+
filters: Object.keys(filters2).length > 0 ? filters2 : void 0
|
|
1339
|
+
});
|
|
1340
|
+
} catch {
|
|
1341
|
+
singleDoc = null;
|
|
1342
|
+
}
|
|
1076
1343
|
result = {
|
|
1077
1344
|
uid,
|
|
1078
1345
|
displayName: contentType.info?.displayName || uid,
|
|
@@ -1086,55 +1353,23 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1086
1353
|
}
|
|
1087
1354
|
};
|
|
1088
1355
|
} else {
|
|
1089
|
-
const filters2 = {};
|
|
1090
|
-
if (attribute && filterOperator && filterQuery !== void 0 && filterQuery !== "") {
|
|
1091
|
-
switch (filterOperator) {
|
|
1092
|
-
case "equals":
|
|
1093
|
-
filters2[attribute] = { $eq: filterQuery };
|
|
1094
|
-
break;
|
|
1095
|
-
case "notEquals":
|
|
1096
|
-
filters2[attribute] = { $ne: filterQuery };
|
|
1097
|
-
break;
|
|
1098
|
-
case "contains":
|
|
1099
|
-
filters2[attribute] = { $containsi: filterQuery };
|
|
1100
|
-
break;
|
|
1101
|
-
case "notContains":
|
|
1102
|
-
filters2[attribute] = { $notContainsi: filterQuery };
|
|
1103
|
-
break;
|
|
1104
|
-
case "startsWith":
|
|
1105
|
-
filters2[attribute] = { $startsWith: filterQuery };
|
|
1106
|
-
break;
|
|
1107
|
-
case "endsWith":
|
|
1108
|
-
filters2[attribute] = { $endsWith: filterQuery };
|
|
1109
|
-
break;
|
|
1110
|
-
case "empty":
|
|
1111
|
-
filters2[attribute] = { $null: true };
|
|
1112
|
-
break;
|
|
1113
|
-
case "notEmpty":
|
|
1114
|
-
filters2[attribute] = { $notNull: true };
|
|
1115
|
-
break;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
if (search && search.trim() !== "") {
|
|
1119
|
-
const stringAttrs = Object.keys(contentType.attributes || {}).filter(
|
|
1120
|
-
(k) => contentType.attributes[k].type === "string" || contentType.attributes[k].type === "text" || contentType.attributes[k].type === "email"
|
|
1121
|
-
);
|
|
1122
|
-
if (stringAttrs.length > 0) {
|
|
1123
|
-
filters2.$or = stringAttrs.slice(0, 5).map((attr) => ({
|
|
1124
|
-
[attr]: { $containsi: search.trim() }
|
|
1125
|
-
}));
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
1356
|
const sortClause = {};
|
|
1129
|
-
if (sortField &&
|
|
1357
|
+
if (sortField && sortField.includes(".")) {
|
|
1358
|
+
const [parent, child] = sortField.split(".");
|
|
1359
|
+
if (contentType.attributes?.[parent]?.type === "component") {
|
|
1360
|
+
sortClause[parent] = { [child]: sortOrder };
|
|
1361
|
+
} else {
|
|
1362
|
+
sortClause["createdAt"] = "desc";
|
|
1363
|
+
}
|
|
1364
|
+
} else if (sortField && contentType.attributes?.[sortField]) {
|
|
1130
1365
|
sortClause[sortField] = sortOrder;
|
|
1131
1366
|
} else {
|
|
1132
1367
|
sortClause["createdAt"] = "desc";
|
|
1133
1368
|
}
|
|
1134
|
-
const
|
|
1135
|
-
(k) => contentType.attributes[k].type === "relation"
|
|
1369
|
+
const populateAttrs = Object.keys(contentType.attributes || {}).filter(
|
|
1370
|
+
(k) => contentType.attributes[k].type === "relation" || contentType.attributes[k].type === "component"
|
|
1136
1371
|
);
|
|
1137
|
-
const populateOption =
|
|
1372
|
+
const populateOption = populateAttrs.length > 0 ? populateAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
|
|
1138
1373
|
const [entries, totalEntries] = await Promise.all([
|
|
1139
1374
|
strapi2.documents(uid).findMany({
|
|
1140
1375
|
start,
|
|
@@ -1473,6 +1708,9 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1473
1708
|
modelsCache.clear();
|
|
1474
1709
|
staticSchemaCache = null;
|
|
1475
1710
|
}
|
|
1711
|
+
},
|
|
1712
|
+
invalidateCollectionCache(uid) {
|
|
1713
|
+
invalidateCollectionCache(uid);
|
|
1476
1714
|
}
|
|
1477
1715
|
};
|
|
1478
1716
|
};
|
|
@@ -1492,6 +1730,7 @@ const DEFAULT_FIELDS = {
|
|
|
1492
1730
|
executedAt: "executedAt"
|
|
1493
1731
|
};
|
|
1494
1732
|
const DEFAULT_CANDIDATE_TYPES = [
|
|
1733
|
+
"plugin::bulk-updater.audit-log",
|
|
1495
1734
|
"api::audit-log.audit-log",
|
|
1496
1735
|
"api::audit.audit",
|
|
1497
1736
|
"api::bulk-audit-log.bulk-audit-log"
|
|
@@ -1513,6 +1752,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1513
1752
|
* 1. Explicit consumer custom schema (full declarative control)
|
|
1514
1753
|
* 2. Zero-config convention auto-detection (api::audit-log.audit-log)
|
|
1515
1754
|
* 3. Strapi Enterprise native audit log bridge
|
|
1755
|
+
* 4. Zero-config plugin core store (works out of the box in 100% of Strapi installations)
|
|
1516
1756
|
*/
|
|
1517
1757
|
getAuditConfig() {
|
|
1518
1758
|
const rawConfig = strapi2.config.get("plugin::bulk-updater.audit");
|
|
@@ -1566,10 +1806,9 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1566
1806
|
};
|
|
1567
1807
|
}
|
|
1568
1808
|
return {
|
|
1569
|
-
enabled:
|
|
1570
|
-
mode: "
|
|
1571
|
-
isEnterpriseNativeAvailable: false
|
|
1572
|
-
error: "No audit collection configured or detected. To enable rollback, provide a collection or use Strapi Enterprise."
|
|
1809
|
+
enabled: true,
|
|
1810
|
+
mode: "store",
|
|
1811
|
+
isEnterpriseNativeAvailable: false
|
|
1573
1812
|
};
|
|
1574
1813
|
},
|
|
1575
1814
|
/**
|
|
@@ -1614,7 +1853,40 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1614
1853
|
},
|
|
1615
1854
|
user
|
|
1616
1855
|
);
|
|
1617
|
-
if (!config.enabled
|
|
1856
|
+
if (!config.enabled) {
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
if (config.mode === "store") {
|
|
1860
|
+
try {
|
|
1861
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
1862
|
+
const stored = await store.get({}) || { history: [] };
|
|
1863
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
1864
|
+
const newRecord = {
|
|
1865
|
+
id: batchSummary.batchId,
|
|
1866
|
+
documentId: batchSummary.batchId,
|
|
1867
|
+
batchId: batchSummary.batchId,
|
|
1868
|
+
attribute: batchSummary.attribute,
|
|
1869
|
+
value: batchSummary.value,
|
|
1870
|
+
userEmail: user?.email || "admin@strapi.io",
|
|
1871
|
+
userName: user?.firstname ? `${user.firstname} ${user.lastname || ""}`.trim() : user?.email || "Admin",
|
|
1872
|
+
updatedCount: batchSummary.updatedCount,
|
|
1873
|
+
skippedCount: batchSummary.skippedCount,
|
|
1874
|
+
totalTargets: batchSummary.totalTargets,
|
|
1875
|
+
diffs: results,
|
|
1876
|
+
isRolledBack: false,
|
|
1877
|
+
rolledBackAt: null,
|
|
1878
|
+
rolledBackBy: null,
|
|
1879
|
+
executedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1880
|
+
};
|
|
1881
|
+
const updatedHistory = [newRecord, ...history].slice(0, 200);
|
|
1882
|
+
await store.set({ value: { history: updatedHistory } });
|
|
1883
|
+
return newRecord;
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
strapi2.log.error(`[bulk-updater] Failed to record audit log in core store: ${err.message}`);
|
|
1886
|
+
return null;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (!config.contentType || !config.fields) {
|
|
1618
1890
|
return null;
|
|
1619
1891
|
}
|
|
1620
1892
|
const f = config.fields;
|
|
@@ -1655,6 +1927,25 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1655
1927
|
history: []
|
|
1656
1928
|
};
|
|
1657
1929
|
}
|
|
1930
|
+
if (config.mode === "store") {
|
|
1931
|
+
try {
|
|
1932
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
1933
|
+
const stored = await store.get({}) || { history: [] };
|
|
1934
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
1935
|
+
return {
|
|
1936
|
+
enabled: true,
|
|
1937
|
+
mode: "store",
|
|
1938
|
+
isEnterpriseNativeAvailable: config.isEnterpriseNativeAvailable,
|
|
1939
|
+
history
|
|
1940
|
+
};
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
return {
|
|
1943
|
+
enabled: false,
|
|
1944
|
+
message: `Failed to read audit history from core store: ${err.message}`,
|
|
1945
|
+
history: []
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1658
1949
|
if (!config.enabled || !config.contentType || !config.fields) {
|
|
1659
1950
|
return {
|
|
1660
1951
|
enabled: false,
|
|
@@ -1700,27 +1991,9 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1700
1991
|
}
|
|
1701
1992
|
},
|
|
1702
1993
|
/**
|
|
1703
|
-
*
|
|
1994
|
+
* Helper to revert diff items back to original values
|
|
1704
1995
|
*/
|
|
1705
|
-
async
|
|
1706
|
-
const config = this.getAuditConfig();
|
|
1707
|
-
if (!config.enabled || !config.contentType || !config.fields) {
|
|
1708
|
-
throw new Error("Audit trail is not configured or disabled.");
|
|
1709
|
-
}
|
|
1710
|
-
const f = config.fields;
|
|
1711
|
-
const records = await strapi2.documents(config.contentType).findMany({
|
|
1712
|
-
filters: {
|
|
1713
|
-
[f.batchId]: { $eq: batchId }
|
|
1714
|
-
}
|
|
1715
|
-
});
|
|
1716
|
-
const record = records?.[0];
|
|
1717
|
-
if (!record) {
|
|
1718
|
-
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
1719
|
-
}
|
|
1720
|
-
if (record[f.isRolledBack]) {
|
|
1721
|
-
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
1722
|
-
}
|
|
1723
|
-
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
1996
|
+
async revertDiffItems(diffs) {
|
|
1724
1997
|
let revertedCount = 0;
|
|
1725
1998
|
const rollbackDetails = [];
|
|
1726
1999
|
for (const item of diffs) {
|
|
@@ -1734,12 +2007,41 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1734
2007
|
} else {
|
|
1735
2008
|
rollbackPayload = item.oldValue;
|
|
1736
2009
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2010
|
+
if (item.attribute.includes(".")) {
|
|
2011
|
+
const [parentKey, childKey] = item.attribute.split(".");
|
|
2012
|
+
const ct2 = strapi2.contentTypes[item.uid];
|
|
2013
|
+
if (ct2?.attributes?.[parentKey]?.type === "component") {
|
|
2014
|
+
const currentDoc = await strapi2.documents(item.uid).findOne({
|
|
2015
|
+
documentId: item.documentId,
|
|
2016
|
+
populate: [parentKey]
|
|
2017
|
+
});
|
|
2018
|
+
const currentParent = currentDoc?.[parentKey] || {};
|
|
2019
|
+
const restoredParent = {
|
|
2020
|
+
...currentParent,
|
|
2021
|
+
[childKey]: rollbackPayload
|
|
2022
|
+
};
|
|
2023
|
+
await strapi2.documents(item.uid).update({
|
|
2024
|
+
documentId: item.documentId,
|
|
2025
|
+
data: {
|
|
2026
|
+
[parentKey]: restoredParent
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
} else {
|
|
2030
|
+
await strapi2.documents(item.uid).update({
|
|
2031
|
+
documentId: item.documentId,
|
|
2032
|
+
data: {
|
|
2033
|
+
[item.attribute]: rollbackPayload
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
1741
2036
|
}
|
|
1742
|
-
}
|
|
2037
|
+
} else {
|
|
2038
|
+
await strapi2.documents(item.uid).update({
|
|
2039
|
+
documentId: item.documentId,
|
|
2040
|
+
data: {
|
|
2041
|
+
[item.attribute]: rollbackPayload
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
1743
2045
|
const ct = strapi2.contentTypes[item.uid];
|
|
1744
2046
|
if (ct?.options?.draftAndPublish) {
|
|
1745
2047
|
try {
|
|
@@ -1750,7 +2052,10 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1750
2052
|
}
|
|
1751
2053
|
}
|
|
1752
2054
|
try {
|
|
1753
|
-
strapi2.plugin("bulk-updater").service("bulkUpdater")
|
|
2055
|
+
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
2056
|
+
if (bulkService?.invalidateCache) {
|
|
2057
|
+
bulkService.invalidateCache(item.uid);
|
|
2058
|
+
}
|
|
1754
2059
|
} catch {
|
|
1755
2060
|
}
|
|
1756
2061
|
revertedCount++;
|
|
@@ -1773,6 +2078,78 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1773
2078
|
}
|
|
1774
2079
|
}
|
|
1775
2080
|
}
|
|
2081
|
+
return { revertedCount, rollbackDetails };
|
|
2082
|
+
},
|
|
2083
|
+
/**
|
|
2084
|
+
* Performs deterministic rollback of a batch
|
|
2085
|
+
*/
|
|
2086
|
+
async rollbackBatch(batchId, currentUser) {
|
|
2087
|
+
const config = this.getAuditConfig();
|
|
2088
|
+
if (!config.enabled) {
|
|
2089
|
+
throw new Error("Audit trail is not configured or disabled.");
|
|
2090
|
+
}
|
|
2091
|
+
if (config.mode === "store") {
|
|
2092
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
2093
|
+
const stored = await store.get({}) || { history: [] };
|
|
2094
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
2095
|
+
const recordIndex = history.findIndex((r) => r.batchId === batchId);
|
|
2096
|
+
if (recordIndex === -1) {
|
|
2097
|
+
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
2098
|
+
}
|
|
2099
|
+
const record2 = history[recordIndex];
|
|
2100
|
+
if (record2.isRolledBack) {
|
|
2101
|
+
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
2102
|
+
}
|
|
2103
|
+
const diffs2 = Array.isArray(record2.diffs) ? record2.diffs : [];
|
|
2104
|
+
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2);
|
|
2105
|
+
record2.isRolledBack = true;
|
|
2106
|
+
record2.rolledBackAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2107
|
+
record2.rolledBackBy = currentUser?.email || "Admin";
|
|
2108
|
+
history[recordIndex] = record2;
|
|
2109
|
+
await store.set({ value: { history } });
|
|
2110
|
+
try {
|
|
2111
|
+
const affectedUids = new Set(rollbackDetails2.map((d) => d.uid));
|
|
2112
|
+
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
2113
|
+
affectedUids.forEach((uid) => {
|
|
2114
|
+
if (uid && bulkService?.invalidateCache) {
|
|
2115
|
+
bulkService.invalidateCache(uid);
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
} catch {
|
|
2119
|
+
}
|
|
2120
|
+
await this.recordToEnterpriseNative(
|
|
2121
|
+
"bulk-updater.rollback",
|
|
2122
|
+
{
|
|
2123
|
+
batchId,
|
|
2124
|
+
revertedCount: revertedCount2
|
|
2125
|
+
},
|
|
2126
|
+
currentUser
|
|
2127
|
+
);
|
|
2128
|
+
return {
|
|
2129
|
+
success: true,
|
|
2130
|
+
batchId,
|
|
2131
|
+
revertedCount: revertedCount2,
|
|
2132
|
+
details: rollbackDetails2
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
if (!config.contentType || !config.fields) {
|
|
2136
|
+
throw new Error("Audit trail is not configured or disabled.");
|
|
2137
|
+
}
|
|
2138
|
+
const f = config.fields;
|
|
2139
|
+
const records = await strapi2.documents(config.contentType).findMany({
|
|
2140
|
+
filters: {
|
|
2141
|
+
[f.batchId]: { $eq: batchId }
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
const record = records?.[0];
|
|
2145
|
+
if (!record) {
|
|
2146
|
+
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
2147
|
+
}
|
|
2148
|
+
if (record[f.isRolledBack]) {
|
|
2149
|
+
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
2150
|
+
}
|
|
2151
|
+
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
2152
|
+
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs);
|
|
1776
2153
|
const updateData = {};
|
|
1777
2154
|
if (f.isRolledBack) updateData[f.isRolledBack] = true;
|
|
1778
2155
|
if (f.rolledBackAt) updateData[f.rolledBackAt] = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2130,10 +2507,24 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2130
2507
|
const attrs = ct.attributes || {};
|
|
2131
2508
|
const excluded = ["createdBy", "updatedBy", "localizations"];
|
|
2132
2509
|
const exportFields = ["documentId"];
|
|
2510
|
+
const compFieldsToPopulate = [];
|
|
2133
2511
|
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
2134
2512
|
if (excluded.includes(attrName)) continue;
|
|
2135
|
-
if (["
|
|
2136
|
-
if (attrDef.type === "
|
|
2513
|
+
if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
|
|
2514
|
+
if (attrDef.type === "component") {
|
|
2515
|
+
if (!attrDef.repeatable && attrDef.component) {
|
|
2516
|
+
const comp = strapi2.components[attrDef.component];
|
|
2517
|
+
if (comp?.attributes) {
|
|
2518
|
+
compFieldsToPopulate.push(attrName);
|
|
2519
|
+
for (const [subKey, subDef] of Object.entries(comp.attributes)) {
|
|
2520
|
+
if (excluded.includes(subKey)) continue;
|
|
2521
|
+
if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
|
|
2522
|
+
exportFields.push(`${attrName}.${subKey}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2137
2528
|
exportFields.push(attrName);
|
|
2138
2529
|
}
|
|
2139
2530
|
const filters2 = {};
|
|
@@ -2177,16 +2568,25 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2177
2568
|
}
|
|
2178
2569
|
}
|
|
2179
2570
|
}
|
|
2571
|
+
const populateOption = compFieldsToPopulate.length > 0 ? compFieldsToPopulate.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
|
|
2180
2572
|
const entries = await strapi2.documents(uid).findMany({
|
|
2181
2573
|
filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
|
|
2182
2574
|
limit: options.limit || 5e3,
|
|
2183
|
-
sort: { createdAt: "desc" }
|
|
2575
|
+
sort: { createdAt: "desc" },
|
|
2576
|
+
populate: populateOption
|
|
2184
2577
|
});
|
|
2185
2578
|
const lines = [];
|
|
2186
2579
|
lines.push(exportFields.map(escapeCsvValue).join(","));
|
|
2187
2580
|
for (const entry of entries || []) {
|
|
2188
2581
|
const rowValues = exportFields.map((field) => {
|
|
2189
|
-
|
|
2582
|
+
let val;
|
|
2583
|
+
if (field.includes(".")) {
|
|
2584
|
+
const [parent, child] = field.split(".");
|
|
2585
|
+
val = entry[parent]?.[child];
|
|
2586
|
+
} else {
|
|
2587
|
+
val = entry[field];
|
|
2588
|
+
}
|
|
2589
|
+
return escapeCsvValue(val);
|
|
2190
2590
|
});
|
|
2191
2591
|
lines.push(rowValues.join(","));
|
|
2192
2592
|
}
|
|
@@ -2272,6 +2672,29 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2272
2672
|
const payload = {};
|
|
2273
2673
|
for (const [key, rawVal] of Object.entries(row)) {
|
|
2274
2674
|
if (key === "documentId" || key === "id") continue;
|
|
2675
|
+
if (key.includes(".")) {
|
|
2676
|
+
const [parent, child] = key.split(".");
|
|
2677
|
+
const parentDef = attrs[parent];
|
|
2678
|
+
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
2679
|
+
const comp = strapi2.components[parentDef.component];
|
|
2680
|
+
const childDef = comp?.attributes?.[child];
|
|
2681
|
+
if (childDef) {
|
|
2682
|
+
if (!payload[parent]) payload[parent] = {};
|
|
2683
|
+
let val = rawVal;
|
|
2684
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
2685
|
+
val = null;
|
|
2686
|
+
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
2687
|
+
val = parseInt(rawVal, 10);
|
|
2688
|
+
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
2689
|
+
val = parseFloat(rawVal);
|
|
2690
|
+
} else if (childDef.type === "boolean") {
|
|
2691
|
+
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
2692
|
+
}
|
|
2693
|
+
payload[parent][child] = val;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2275
2698
|
const attrDef = attrs[key];
|
|
2276
2699
|
if (!attrDef) continue;
|
|
2277
2700
|
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
@@ -2323,9 +2746,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2323
2746
|
});
|
|
2324
2747
|
continue;
|
|
2325
2748
|
}
|
|
2749
|
+
const mergedPayload = { ...payload };
|
|
2750
|
+
for (const key of Object.keys(payload)) {
|
|
2751
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
2752
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2326
2755
|
await strapi2.documents(uid).update({
|
|
2327
2756
|
documentId: docId,
|
|
2328
|
-
data:
|
|
2757
|
+
data: mergedPayload
|
|
2329
2758
|
});
|
|
2330
2759
|
if (publish && ct.options?.draftAndPublish) {
|
|
2331
2760
|
try {
|
|
@@ -2352,9 +2781,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2352
2781
|
documentId: docId
|
|
2353
2782
|
});
|
|
2354
2783
|
if (existing) {
|
|
2784
|
+
const mergedPayload = { ...payload };
|
|
2785
|
+
for (const key of Object.keys(payload)) {
|
|
2786
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
2787
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2355
2790
|
await strapi2.documents(uid).update({
|
|
2356
2791
|
documentId: docId,
|
|
2357
|
-
data:
|
|
2792
|
+
data: mergedPayload
|
|
2358
2793
|
});
|
|
2359
2794
|
if (publish && ct.options?.draftAndPublish) {
|
|
2360
2795
|
try {
|
|
@@ -31957,6 +32392,7 @@ const index = {
|
|
|
31957
32392
|
});
|
|
31958
32393
|
},
|
|
31959
32394
|
routes,
|
|
32395
|
+
contentTypes,
|
|
31960
32396
|
controllers: {
|
|
31961
32397
|
bulkUpdater: bulkUpdaterController,
|
|
31962
32398
|
audit: auditController,
|