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.mjs
CHANGED
|
@@ -169,6 +169,78 @@ const routes = {
|
|
|
169
169
|
]
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
|
+
const auditLog = {
|
|
173
|
+
collectionName: "bulk_updater_audit_logs",
|
|
174
|
+
info: {
|
|
175
|
+
singularName: "audit-log",
|
|
176
|
+
pluralName: "audit-logs",
|
|
177
|
+
displayName: "Bulk Updater Audit Log",
|
|
178
|
+
description: "Internal audit trail for bulk attribute and relation updates"
|
|
179
|
+
},
|
|
180
|
+
options: {
|
|
181
|
+
draftAndPublish: false
|
|
182
|
+
},
|
|
183
|
+
pluginOptions: {
|
|
184
|
+
"content-manager": {
|
|
185
|
+
visible: true
|
|
186
|
+
},
|
|
187
|
+
"content-type-builder": {
|
|
188
|
+
visible: false
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
attributes: {
|
|
192
|
+
batchId: {
|
|
193
|
+
type: "string",
|
|
194
|
+
required: true
|
|
195
|
+
},
|
|
196
|
+
attribute: {
|
|
197
|
+
type: "string",
|
|
198
|
+
required: true
|
|
199
|
+
},
|
|
200
|
+
value: {
|
|
201
|
+
type: "text"
|
|
202
|
+
},
|
|
203
|
+
userEmail: {
|
|
204
|
+
type: "string"
|
|
205
|
+
},
|
|
206
|
+
userName: {
|
|
207
|
+
type: "string"
|
|
208
|
+
},
|
|
209
|
+
updatedCount: {
|
|
210
|
+
type: "integer",
|
|
211
|
+
default: 0
|
|
212
|
+
},
|
|
213
|
+
skippedCount: {
|
|
214
|
+
type: "integer",
|
|
215
|
+
default: 0
|
|
216
|
+
},
|
|
217
|
+
totalTargets: {
|
|
218
|
+
type: "integer",
|
|
219
|
+
default: 0
|
|
220
|
+
},
|
|
221
|
+
diffs: {
|
|
222
|
+
type: "json"
|
|
223
|
+
},
|
|
224
|
+
isRolledBack: {
|
|
225
|
+
type: "boolean",
|
|
226
|
+
default: false
|
|
227
|
+
},
|
|
228
|
+
rolledBackAt: {
|
|
229
|
+
type: "datetime"
|
|
230
|
+
},
|
|
231
|
+
rolledBackBy: {
|
|
232
|
+
type: "string"
|
|
233
|
+
},
|
|
234
|
+
executedAt: {
|
|
235
|
+
type: "datetime"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const contentTypes = {
|
|
240
|
+
"audit-log": {
|
|
241
|
+
schema: auditLog
|
|
242
|
+
}
|
|
243
|
+
};
|
|
172
244
|
const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
173
245
|
async getModels(ctx) {
|
|
174
246
|
try {
|
|
@@ -211,7 +283,7 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
|
|
|
211
283
|
return;
|
|
212
284
|
}
|
|
213
285
|
}
|
|
214
|
-
ctx.body = data;
|
|
286
|
+
ctx.body = { success: true, ...data };
|
|
215
287
|
} catch (err) {
|
|
216
288
|
ctx.status = 500;
|
|
217
289
|
ctx.body = { success: false, error: err.message };
|
|
@@ -660,9 +732,69 @@ async function executeBatchInternal(strapi2, {
|
|
|
660
732
|
try {
|
|
661
733
|
const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
|
|
662
734
|
const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
|
|
735
|
+
const isNested = attribute.includes(".");
|
|
736
|
+
let isComponentNested = false;
|
|
737
|
+
let parentKey = "";
|
|
738
|
+
let childKey = "";
|
|
739
|
+
if (isNested) {
|
|
740
|
+
const parts = attribute.split(".");
|
|
741
|
+
parentKey = parts[0];
|
|
742
|
+
childKey = parts[1];
|
|
743
|
+
const parentAttrDef = contentType?.attributes?.[parentKey];
|
|
744
|
+
if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
|
|
745
|
+
const compSchema = strapi2.components[parentAttrDef.component];
|
|
746
|
+
if (compSchema?.attributes?.[childKey]) {
|
|
747
|
+
isComponentNested = true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
663
751
|
const attrDef = contentType?.attributes?.[attribute];
|
|
664
|
-
const hasAttribute = Boolean(attrDef);
|
|
752
|
+
const hasAttribute = Boolean(attrDef) || isComponentNested;
|
|
665
753
|
if (hasAttribute) {
|
|
754
|
+
if (isComponentNested) {
|
|
755
|
+
const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
|
|
756
|
+
documentId: target.documentId,
|
|
757
|
+
populate: [parentKey]
|
|
758
|
+
});
|
|
759
|
+
const existingParent = currentEntryWithPopulate?.[parentKey] || {};
|
|
760
|
+
const currentVal2 = existingParent?.[childKey];
|
|
761
|
+
let newValue2 = value;
|
|
762
|
+
if (regexObj && typeof currentVal2 === "string") {
|
|
763
|
+
newValue2 = currentVal2.replace(regexObj, value);
|
|
764
|
+
}
|
|
765
|
+
const updatedParent = {
|
|
766
|
+
...existingParent,
|
|
767
|
+
[childKey]: newValue2
|
|
768
|
+
};
|
|
769
|
+
await strapi2.documents(target.uid).update({
|
|
770
|
+
documentId: target.documentId,
|
|
771
|
+
data: {
|
|
772
|
+
[parentKey]: updatedParent
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
|
|
776
|
+
if (publishOnApproval && isDraftAndPublishComp) {
|
|
777
|
+
try {
|
|
778
|
+
await strapi2.documents(target.uid).publish({
|
|
779
|
+
documentId: target.documentId
|
|
780
|
+
});
|
|
781
|
+
} catch (publishErr) {
|
|
782
|
+
strapi2.log.warn(`[bulk-updater] Failed to auto-publish ${target.uid}/${target.documentId}:`, publishErr);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
invalidateCollectionCache(target.uid);
|
|
786
|
+
return {
|
|
787
|
+
uid: target.uid,
|
|
788
|
+
collectionName: displayName,
|
|
789
|
+
documentId: target.documentId,
|
|
790
|
+
label: entryLabel,
|
|
791
|
+
attribute,
|
|
792
|
+
oldValue: currentVal2 ?? null,
|
|
793
|
+
newValue: newValue2,
|
|
794
|
+
status: "updated",
|
|
795
|
+
message: `Successfully updated nested field ${attribute}`
|
|
796
|
+
};
|
|
797
|
+
}
|
|
666
798
|
if (attrDef.type === "relation") {
|
|
667
799
|
const isMulti = attrDef.relation === "oneToMany" || attrDef.relation === "manyToMany";
|
|
668
800
|
const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
|
|
@@ -904,9 +1036,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
904
1036
|
const isSingleType = contentType.kind === "singleType";
|
|
905
1037
|
const rawAttrs = contentType.attributes || {};
|
|
906
1038
|
const relationsByAttr = {};
|
|
1039
|
+
const nestedComponentAttrs = [];
|
|
907
1040
|
const validAttrs = Object.keys(rawAttrs).filter((attrKey) => {
|
|
908
1041
|
if (systemFields.includes(attrKey)) return false;
|
|
909
|
-
if (rawAttrs[attrKey].type === "
|
|
1042
|
+
if (rawAttrs[attrKey].type === "dynamiczone") return false;
|
|
1043
|
+
if (rawAttrs[attrKey].type === "component") {
|
|
1044
|
+
const compDef = rawAttrs[attrKey];
|
|
1045
|
+
if (!compDef.repeatable && compDef.component) {
|
|
1046
|
+
const componentSchema = strapi2.components[compDef.component];
|
|
1047
|
+
if (componentSchema && componentSchema.attributes) {
|
|
1048
|
+
for (const [subKey, subAttr] of Object.entries(componentSchema.attributes)) {
|
|
1049
|
+
if (systemFields.includes(subKey)) continue;
|
|
1050
|
+
if (["component", "dynamiczone", "relation"].includes(subAttr.type)) continue;
|
|
1051
|
+
nestedComponentAttrs.push(`${attrKey}.${subKey}`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
910
1057
|
if (rawAttrs[attrKey].type === "relation") {
|
|
911
1058
|
const rel = rawAttrs[attrKey];
|
|
912
1059
|
const targetUid = rel.target;
|
|
@@ -926,7 +1073,8 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
926
1073
|
}
|
|
927
1074
|
return true;
|
|
928
1075
|
});
|
|
929
|
-
validAttrs
|
|
1076
|
+
const allColAttrs = [...validAttrs, ...nestedComponentAttrs];
|
|
1077
|
+
allColAttrs.forEach((attr) => allAttributesSet.add(attr));
|
|
930
1078
|
metaByUid[uid] = {
|
|
931
1079
|
uid,
|
|
932
1080
|
kind: contentType.kind,
|
|
@@ -934,7 +1082,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
934
1082
|
singularName: contentType.info?.singularName || uid,
|
|
935
1083
|
pluralName: contentType.info?.pluralName || uid,
|
|
936
1084
|
displayName: contentType.info?.displayName || uid,
|
|
937
|
-
attributes:
|
|
1085
|
+
attributes: allColAttrs,
|
|
938
1086
|
relations: relationsByAttr
|
|
939
1087
|
};
|
|
940
1088
|
}
|
|
@@ -963,8 +1111,17 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
963
1111
|
let entries = [];
|
|
964
1112
|
let totalCount = 0;
|
|
965
1113
|
try {
|
|
1114
|
+
const ctAttrs = allContentTypes[uid]?.attributes || {};
|
|
966
1115
|
const relKeys = Object.keys(meta.relations || {});
|
|
967
|
-
const
|
|
1116
|
+
const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
|
|
1117
|
+
const populateOptionObj = {};
|
|
1118
|
+
relKeys.forEach((k) => {
|
|
1119
|
+
populateOptionObj[k] = true;
|
|
1120
|
+
});
|
|
1121
|
+
compKeys.forEach((k) => {
|
|
1122
|
+
populateOptionObj[k] = true;
|
|
1123
|
+
});
|
|
1124
|
+
const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
|
|
968
1125
|
if (meta.isSingleType) {
|
|
969
1126
|
const singleDoc = await strapi2.documents(uid).findFirst({
|
|
970
1127
|
populate: populateOption
|
|
@@ -1042,7 +1199,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1042
1199
|
if (!contentType) {
|
|
1043
1200
|
throw new Error(`Collection with UID "${uid}" does not exist.`);
|
|
1044
1201
|
}
|
|
1045
|
-
const limit = Math.min(Math.max(Number(pageSize) || 10, 1),
|
|
1202
|
+
const limit = Math.min(Math.max(Number(pageSize) || 10, 1), 500);
|
|
1046
1203
|
const currentPage = Math.max(Number(page) || 1, 1);
|
|
1047
1204
|
const start = (currentPage - 1) * limit;
|
|
1048
1205
|
const isSingleType = contentType.kind === "singleType";
|
|
@@ -1058,8 +1215,118 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1058
1215
|
};
|
|
1059
1216
|
}
|
|
1060
1217
|
let result;
|
|
1218
|
+
const filters2 = {};
|
|
1219
|
+
const attrs = contentType.attributes || {};
|
|
1220
|
+
let colHasAttribute = false;
|
|
1221
|
+
let isNestedAttr = false;
|
|
1222
|
+
let parentKey = "";
|
|
1223
|
+
let childKey = "";
|
|
1224
|
+
if (attribute) {
|
|
1225
|
+
if (attribute.includes(".")) {
|
|
1226
|
+
[parentKey, childKey] = attribute.split(".");
|
|
1227
|
+
if (attrs[parentKey]?.type === "component" && !attrs[parentKey].repeatable && attrs[parentKey].component) {
|
|
1228
|
+
const compSchema = strapi2.components[attrs[parentKey].component];
|
|
1229
|
+
if (compSchema?.attributes?.[childKey]) {
|
|
1230
|
+
colHasAttribute = true;
|
|
1231
|
+
isNestedAttr = true;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} else if (attrs[attribute]) {
|
|
1235
|
+
colHasAttribute = true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const normOp = (filterOperator || "").toLowerCase().replace(/_/g, "");
|
|
1239
|
+
if (normOp && normOp !== "none") {
|
|
1240
|
+
if (!colHasAttribute) {
|
|
1241
|
+
result = {
|
|
1242
|
+
uid,
|
|
1243
|
+
displayName: contentType.info?.displayName || uid,
|
|
1244
|
+
isSingleType,
|
|
1245
|
+
entries: [],
|
|
1246
|
+
pagination: {
|
|
1247
|
+
page: 1,
|
|
1248
|
+
pageSize: limit,
|
|
1249
|
+
total: 0,
|
|
1250
|
+
pageCount: 0
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
queryCache.set(cacheKey, { signature, data: result, timestamp: Date.now() });
|
|
1254
|
+
return { ...result, cached: false, etag };
|
|
1255
|
+
}
|
|
1256
|
+
let condition = void 0;
|
|
1257
|
+
const q = (filterQuery || "").trim();
|
|
1258
|
+
if (normOp === "empty") {
|
|
1259
|
+
condition = { $or: [{ $null: true }, { $eq: "" }] };
|
|
1260
|
+
} else if (normOp === "notempty") {
|
|
1261
|
+
condition = { $and: [{ $notNull: true }, { $ne: "" }] };
|
|
1262
|
+
} else if (q !== "") {
|
|
1263
|
+
switch (normOp) {
|
|
1264
|
+
case "equals":
|
|
1265
|
+
condition = { $containsi: q };
|
|
1266
|
+
break;
|
|
1267
|
+
case "notequals":
|
|
1268
|
+
condition = { $notContainsi: q };
|
|
1269
|
+
break;
|
|
1270
|
+
case "contains":
|
|
1271
|
+
condition = { $containsi: q };
|
|
1272
|
+
break;
|
|
1273
|
+
case "notcontains":
|
|
1274
|
+
condition = { $notContainsi: q };
|
|
1275
|
+
break;
|
|
1276
|
+
case "startswith":
|
|
1277
|
+
condition = { $startsWith: q };
|
|
1278
|
+
break;
|
|
1279
|
+
case "endswith":
|
|
1280
|
+
condition = { $endsWith: q };
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (condition !== void 0) {
|
|
1285
|
+
if (isNestedAttr) {
|
|
1286
|
+
filters2[parentKey] = {
|
|
1287
|
+
[childKey]: condition
|
|
1288
|
+
};
|
|
1289
|
+
} else {
|
|
1290
|
+
if (normOp === "empty") {
|
|
1291
|
+
filters2.$or = [
|
|
1292
|
+
{ [attribute]: { $null: true } },
|
|
1293
|
+
{ [attribute]: { $eq: "" } }
|
|
1294
|
+
];
|
|
1295
|
+
} else if (normOp === "notempty") {
|
|
1296
|
+
filters2[attribute] = { $notNull: true, $ne: "" };
|
|
1297
|
+
} else {
|
|
1298
|
+
filters2[attribute] = condition;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (search && search.trim() !== "") {
|
|
1304
|
+
const q = search.trim();
|
|
1305
|
+
const searchableAttrs = Object.keys(attrs).filter(
|
|
1306
|
+
(k) => ["string", "text", "email", "uid"].includes(attrs[k]?.type)
|
|
1307
|
+
);
|
|
1308
|
+
if (searchableAttrs.length > 0) {
|
|
1309
|
+
const orList = searchableAttrs.slice(0, 6).map((k) => ({
|
|
1310
|
+
[k]: { $containsi: q }
|
|
1311
|
+
}));
|
|
1312
|
+
if (Object.keys(filters2).length > 0) {
|
|
1313
|
+
const existingFilters = { ...filters2 };
|
|
1314
|
+
Object.keys(filters2).forEach((k) => delete filters2[k]);
|
|
1315
|
+
filters2.$and = [existingFilters, { $or: orList }];
|
|
1316
|
+
} else {
|
|
1317
|
+
filters2.$or = orList;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1061
1321
|
if (isSingleType) {
|
|
1062
|
-
|
|
1322
|
+
let singleDoc = null;
|
|
1323
|
+
try {
|
|
1324
|
+
singleDoc = await strapi2.documents(uid).findFirst({
|
|
1325
|
+
filters: Object.keys(filters2).length > 0 ? filters2 : void 0
|
|
1326
|
+
});
|
|
1327
|
+
} catch {
|
|
1328
|
+
singleDoc = null;
|
|
1329
|
+
}
|
|
1063
1330
|
result = {
|
|
1064
1331
|
uid,
|
|
1065
1332
|
displayName: contentType.info?.displayName || uid,
|
|
@@ -1073,55 +1340,23 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1073
1340
|
}
|
|
1074
1341
|
};
|
|
1075
1342
|
} else {
|
|
1076
|
-
const filters2 = {};
|
|
1077
|
-
if (attribute && filterOperator && filterQuery !== void 0 && filterQuery !== "") {
|
|
1078
|
-
switch (filterOperator) {
|
|
1079
|
-
case "equals":
|
|
1080
|
-
filters2[attribute] = { $eq: filterQuery };
|
|
1081
|
-
break;
|
|
1082
|
-
case "notEquals":
|
|
1083
|
-
filters2[attribute] = { $ne: filterQuery };
|
|
1084
|
-
break;
|
|
1085
|
-
case "contains":
|
|
1086
|
-
filters2[attribute] = { $containsi: filterQuery };
|
|
1087
|
-
break;
|
|
1088
|
-
case "notContains":
|
|
1089
|
-
filters2[attribute] = { $notContainsi: filterQuery };
|
|
1090
|
-
break;
|
|
1091
|
-
case "startsWith":
|
|
1092
|
-
filters2[attribute] = { $startsWith: filterQuery };
|
|
1093
|
-
break;
|
|
1094
|
-
case "endsWith":
|
|
1095
|
-
filters2[attribute] = { $endsWith: filterQuery };
|
|
1096
|
-
break;
|
|
1097
|
-
case "empty":
|
|
1098
|
-
filters2[attribute] = { $null: true };
|
|
1099
|
-
break;
|
|
1100
|
-
case "notEmpty":
|
|
1101
|
-
filters2[attribute] = { $notNull: true };
|
|
1102
|
-
break;
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
if (search && search.trim() !== "") {
|
|
1106
|
-
const stringAttrs = Object.keys(contentType.attributes || {}).filter(
|
|
1107
|
-
(k) => contentType.attributes[k].type === "string" || contentType.attributes[k].type === "text" || contentType.attributes[k].type === "email"
|
|
1108
|
-
);
|
|
1109
|
-
if (stringAttrs.length > 0) {
|
|
1110
|
-
filters2.$or = stringAttrs.slice(0, 5).map((attr) => ({
|
|
1111
|
-
[attr]: { $containsi: search.trim() }
|
|
1112
|
-
}));
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
1343
|
const sortClause = {};
|
|
1116
|
-
if (sortField &&
|
|
1344
|
+
if (sortField && sortField.includes(".")) {
|
|
1345
|
+
const [parent, child] = sortField.split(".");
|
|
1346
|
+
if (contentType.attributes?.[parent]?.type === "component") {
|
|
1347
|
+
sortClause[parent] = { [child]: sortOrder };
|
|
1348
|
+
} else {
|
|
1349
|
+
sortClause["createdAt"] = "desc";
|
|
1350
|
+
}
|
|
1351
|
+
} else if (sortField && contentType.attributes?.[sortField]) {
|
|
1117
1352
|
sortClause[sortField] = sortOrder;
|
|
1118
1353
|
} else {
|
|
1119
1354
|
sortClause["createdAt"] = "desc";
|
|
1120
1355
|
}
|
|
1121
|
-
const
|
|
1122
|
-
(k) => contentType.attributes[k].type === "relation"
|
|
1356
|
+
const populateAttrs = Object.keys(contentType.attributes || {}).filter(
|
|
1357
|
+
(k) => contentType.attributes[k].type === "relation" || contentType.attributes[k].type === "component"
|
|
1123
1358
|
);
|
|
1124
|
-
const populateOption =
|
|
1359
|
+
const populateOption = populateAttrs.length > 0 ? populateAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
|
|
1125
1360
|
const [entries, totalEntries] = await Promise.all([
|
|
1126
1361
|
strapi2.documents(uid).findMany({
|
|
1127
1362
|
start,
|
|
@@ -1460,6 +1695,9 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
|
|
|
1460
1695
|
modelsCache.clear();
|
|
1461
1696
|
staticSchemaCache = null;
|
|
1462
1697
|
}
|
|
1698
|
+
},
|
|
1699
|
+
invalidateCollectionCache(uid) {
|
|
1700
|
+
invalidateCollectionCache(uid);
|
|
1463
1701
|
}
|
|
1464
1702
|
};
|
|
1465
1703
|
};
|
|
@@ -1479,6 +1717,7 @@ const DEFAULT_FIELDS = {
|
|
|
1479
1717
|
executedAt: "executedAt"
|
|
1480
1718
|
};
|
|
1481
1719
|
const DEFAULT_CANDIDATE_TYPES = [
|
|
1720
|
+
"plugin::bulk-updater.audit-log",
|
|
1482
1721
|
"api::audit-log.audit-log",
|
|
1483
1722
|
"api::audit.audit",
|
|
1484
1723
|
"api::bulk-audit-log.bulk-audit-log"
|
|
@@ -1500,6 +1739,7 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1500
1739
|
* 1. Explicit consumer custom schema (full declarative control)
|
|
1501
1740
|
* 2. Zero-config convention auto-detection (api::audit-log.audit-log)
|
|
1502
1741
|
* 3. Strapi Enterprise native audit log bridge
|
|
1742
|
+
* 4. Zero-config plugin core store (works out of the box in 100% of Strapi installations)
|
|
1503
1743
|
*/
|
|
1504
1744
|
getAuditConfig() {
|
|
1505
1745
|
const rawConfig = strapi2.config.get("plugin::bulk-updater.audit");
|
|
@@ -1553,10 +1793,9 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1553
1793
|
};
|
|
1554
1794
|
}
|
|
1555
1795
|
return {
|
|
1556
|
-
enabled:
|
|
1557
|
-
mode: "
|
|
1558
|
-
isEnterpriseNativeAvailable: false
|
|
1559
|
-
error: "No audit collection configured or detected. To enable rollback, provide a collection or use Strapi Enterprise."
|
|
1796
|
+
enabled: true,
|
|
1797
|
+
mode: "store",
|
|
1798
|
+
isEnterpriseNativeAvailable: false
|
|
1560
1799
|
};
|
|
1561
1800
|
},
|
|
1562
1801
|
/**
|
|
@@ -1601,7 +1840,40 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1601
1840
|
},
|
|
1602
1841
|
user
|
|
1603
1842
|
);
|
|
1604
|
-
if (!config.enabled
|
|
1843
|
+
if (!config.enabled) {
|
|
1844
|
+
return null;
|
|
1845
|
+
}
|
|
1846
|
+
if (config.mode === "store") {
|
|
1847
|
+
try {
|
|
1848
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
1849
|
+
const stored = await store.get({}) || { history: [] };
|
|
1850
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
1851
|
+
const newRecord = {
|
|
1852
|
+
id: batchSummary.batchId,
|
|
1853
|
+
documentId: batchSummary.batchId,
|
|
1854
|
+
batchId: batchSummary.batchId,
|
|
1855
|
+
attribute: batchSummary.attribute,
|
|
1856
|
+
value: batchSummary.value,
|
|
1857
|
+
userEmail: user?.email || "admin@strapi.io",
|
|
1858
|
+
userName: user?.firstname ? `${user.firstname} ${user.lastname || ""}`.trim() : user?.email || "Admin",
|
|
1859
|
+
updatedCount: batchSummary.updatedCount,
|
|
1860
|
+
skippedCount: batchSummary.skippedCount,
|
|
1861
|
+
totalTargets: batchSummary.totalTargets,
|
|
1862
|
+
diffs: results,
|
|
1863
|
+
isRolledBack: false,
|
|
1864
|
+
rolledBackAt: null,
|
|
1865
|
+
rolledBackBy: null,
|
|
1866
|
+
executedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1867
|
+
};
|
|
1868
|
+
const updatedHistory = [newRecord, ...history].slice(0, 200);
|
|
1869
|
+
await store.set({ value: { history: updatedHistory } });
|
|
1870
|
+
return newRecord;
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
strapi2.log.error(`[bulk-updater] Failed to record audit log in core store: ${err.message}`);
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (!config.contentType || !config.fields) {
|
|
1605
1877
|
return null;
|
|
1606
1878
|
}
|
|
1607
1879
|
const f = config.fields;
|
|
@@ -1642,6 +1914,25 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1642
1914
|
history: []
|
|
1643
1915
|
};
|
|
1644
1916
|
}
|
|
1917
|
+
if (config.mode === "store") {
|
|
1918
|
+
try {
|
|
1919
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
1920
|
+
const stored = await store.get({}) || { history: [] };
|
|
1921
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
1922
|
+
return {
|
|
1923
|
+
enabled: true,
|
|
1924
|
+
mode: "store",
|
|
1925
|
+
isEnterpriseNativeAvailable: config.isEnterpriseNativeAvailable,
|
|
1926
|
+
history
|
|
1927
|
+
};
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
return {
|
|
1930
|
+
enabled: false,
|
|
1931
|
+
message: `Failed to read audit history from core store: ${err.message}`,
|
|
1932
|
+
history: []
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1645
1936
|
if (!config.enabled || !config.contentType || !config.fields) {
|
|
1646
1937
|
return {
|
|
1647
1938
|
enabled: false,
|
|
@@ -1687,27 +1978,9 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1687
1978
|
}
|
|
1688
1979
|
},
|
|
1689
1980
|
/**
|
|
1690
|
-
*
|
|
1981
|
+
* Helper to revert diff items back to original values
|
|
1691
1982
|
*/
|
|
1692
|
-
async
|
|
1693
|
-
const config = this.getAuditConfig();
|
|
1694
|
-
if (!config.enabled || !config.contentType || !config.fields) {
|
|
1695
|
-
throw new Error("Audit trail is not configured or disabled.");
|
|
1696
|
-
}
|
|
1697
|
-
const f = config.fields;
|
|
1698
|
-
const records = await strapi2.documents(config.contentType).findMany({
|
|
1699
|
-
filters: {
|
|
1700
|
-
[f.batchId]: { $eq: batchId }
|
|
1701
|
-
}
|
|
1702
|
-
});
|
|
1703
|
-
const record = records?.[0];
|
|
1704
|
-
if (!record) {
|
|
1705
|
-
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
1706
|
-
}
|
|
1707
|
-
if (record[f.isRolledBack]) {
|
|
1708
|
-
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
1709
|
-
}
|
|
1710
|
-
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
1983
|
+
async revertDiffItems(diffs) {
|
|
1711
1984
|
let revertedCount = 0;
|
|
1712
1985
|
const rollbackDetails = [];
|
|
1713
1986
|
for (const item of diffs) {
|
|
@@ -1721,12 +1994,41 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1721
1994
|
} else {
|
|
1722
1995
|
rollbackPayload = item.oldValue;
|
|
1723
1996
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1997
|
+
if (item.attribute.includes(".")) {
|
|
1998
|
+
const [parentKey, childKey] = item.attribute.split(".");
|
|
1999
|
+
const ct2 = strapi2.contentTypes[item.uid];
|
|
2000
|
+
if (ct2?.attributes?.[parentKey]?.type === "component") {
|
|
2001
|
+
const currentDoc = await strapi2.documents(item.uid).findOne({
|
|
2002
|
+
documentId: item.documentId,
|
|
2003
|
+
populate: [parentKey]
|
|
2004
|
+
});
|
|
2005
|
+
const currentParent = currentDoc?.[parentKey] || {};
|
|
2006
|
+
const restoredParent = {
|
|
2007
|
+
...currentParent,
|
|
2008
|
+
[childKey]: rollbackPayload
|
|
2009
|
+
};
|
|
2010
|
+
await strapi2.documents(item.uid).update({
|
|
2011
|
+
documentId: item.documentId,
|
|
2012
|
+
data: {
|
|
2013
|
+
[parentKey]: restoredParent
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
} else {
|
|
2017
|
+
await strapi2.documents(item.uid).update({
|
|
2018
|
+
documentId: item.documentId,
|
|
2019
|
+
data: {
|
|
2020
|
+
[item.attribute]: rollbackPayload
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
1728
2023
|
}
|
|
1729
|
-
}
|
|
2024
|
+
} else {
|
|
2025
|
+
await strapi2.documents(item.uid).update({
|
|
2026
|
+
documentId: item.documentId,
|
|
2027
|
+
data: {
|
|
2028
|
+
[item.attribute]: rollbackPayload
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
1730
2032
|
const ct = strapi2.contentTypes[item.uid];
|
|
1731
2033
|
if (ct?.options?.draftAndPublish) {
|
|
1732
2034
|
try {
|
|
@@ -1737,7 +2039,10 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1737
2039
|
}
|
|
1738
2040
|
}
|
|
1739
2041
|
try {
|
|
1740
|
-
strapi2.plugin("bulk-updater").service("bulkUpdater")
|
|
2042
|
+
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
2043
|
+
if (bulkService?.invalidateCache) {
|
|
2044
|
+
bulkService.invalidateCache(item.uid);
|
|
2045
|
+
}
|
|
1741
2046
|
} catch {
|
|
1742
2047
|
}
|
|
1743
2048
|
revertedCount++;
|
|
@@ -1760,6 +2065,78 @@ const auditService = ({ strapi: strapi2 }) => ({
|
|
|
1760
2065
|
}
|
|
1761
2066
|
}
|
|
1762
2067
|
}
|
|
2068
|
+
return { revertedCount, rollbackDetails };
|
|
2069
|
+
},
|
|
2070
|
+
/**
|
|
2071
|
+
* Performs deterministic rollback of a batch
|
|
2072
|
+
*/
|
|
2073
|
+
async rollbackBatch(batchId, currentUser) {
|
|
2074
|
+
const config = this.getAuditConfig();
|
|
2075
|
+
if (!config.enabled) {
|
|
2076
|
+
throw new Error("Audit trail is not configured or disabled.");
|
|
2077
|
+
}
|
|
2078
|
+
if (config.mode === "store") {
|
|
2079
|
+
const store = strapi2.store({ type: "plugin", name: "bulk-updater", key: "audit-history" });
|
|
2080
|
+
const stored = await store.get({}) || { history: [] };
|
|
2081
|
+
const history = Array.isArray(stored.history) ? stored.history : [];
|
|
2082
|
+
const recordIndex = history.findIndex((r) => r.batchId === batchId);
|
|
2083
|
+
if (recordIndex === -1) {
|
|
2084
|
+
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
2085
|
+
}
|
|
2086
|
+
const record2 = history[recordIndex];
|
|
2087
|
+
if (record2.isRolledBack) {
|
|
2088
|
+
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
2089
|
+
}
|
|
2090
|
+
const diffs2 = Array.isArray(record2.diffs) ? record2.diffs : [];
|
|
2091
|
+
const { revertedCount: revertedCount2, rollbackDetails: rollbackDetails2 } = await this.revertDiffItems(diffs2);
|
|
2092
|
+
record2.isRolledBack = true;
|
|
2093
|
+
record2.rolledBackAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2094
|
+
record2.rolledBackBy = currentUser?.email || "Admin";
|
|
2095
|
+
history[recordIndex] = record2;
|
|
2096
|
+
await store.set({ value: { history } });
|
|
2097
|
+
try {
|
|
2098
|
+
const affectedUids = new Set(rollbackDetails2.map((d) => d.uid));
|
|
2099
|
+
const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
|
|
2100
|
+
affectedUids.forEach((uid) => {
|
|
2101
|
+
if (uid && bulkService?.invalidateCache) {
|
|
2102
|
+
bulkService.invalidateCache(uid);
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
} catch {
|
|
2106
|
+
}
|
|
2107
|
+
await this.recordToEnterpriseNative(
|
|
2108
|
+
"bulk-updater.rollback",
|
|
2109
|
+
{
|
|
2110
|
+
batchId,
|
|
2111
|
+
revertedCount: revertedCount2
|
|
2112
|
+
},
|
|
2113
|
+
currentUser
|
|
2114
|
+
);
|
|
2115
|
+
return {
|
|
2116
|
+
success: true,
|
|
2117
|
+
batchId,
|
|
2118
|
+
revertedCount: revertedCount2,
|
|
2119
|
+
details: rollbackDetails2
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
if (!config.contentType || !config.fields) {
|
|
2123
|
+
throw new Error("Audit trail is not configured or disabled.");
|
|
2124
|
+
}
|
|
2125
|
+
const f = config.fields;
|
|
2126
|
+
const records = await strapi2.documents(config.contentType).findMany({
|
|
2127
|
+
filters: {
|
|
2128
|
+
[f.batchId]: { $eq: batchId }
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
const record = records?.[0];
|
|
2132
|
+
if (!record) {
|
|
2133
|
+
throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
|
|
2134
|
+
}
|
|
2135
|
+
if (record[f.isRolledBack]) {
|
|
2136
|
+
throw new Error(`Batch "${batchId}" has already been rolled back.`);
|
|
2137
|
+
}
|
|
2138
|
+
const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
|
|
2139
|
+
const { revertedCount, rollbackDetails } = await this.revertDiffItems(diffs);
|
|
1763
2140
|
const updateData = {};
|
|
1764
2141
|
if (f.isRolledBack) updateData[f.isRolledBack] = true;
|
|
1765
2142
|
if (f.rolledBackAt) updateData[f.rolledBackAt] = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2117,10 +2494,24 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2117
2494
|
const attrs = ct.attributes || {};
|
|
2118
2495
|
const excluded = ["createdBy", "updatedBy", "localizations"];
|
|
2119
2496
|
const exportFields = ["documentId"];
|
|
2497
|
+
const compFieldsToPopulate = [];
|
|
2120
2498
|
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
2121
2499
|
if (excluded.includes(attrName)) continue;
|
|
2122
|
-
if (["
|
|
2123
|
-
if (attrDef.type === "
|
|
2500
|
+
if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
|
|
2501
|
+
if (attrDef.type === "component") {
|
|
2502
|
+
if (!attrDef.repeatable && attrDef.component) {
|
|
2503
|
+
const comp = strapi2.components[attrDef.component];
|
|
2504
|
+
if (comp?.attributes) {
|
|
2505
|
+
compFieldsToPopulate.push(attrName);
|
|
2506
|
+
for (const [subKey, subDef] of Object.entries(comp.attributes)) {
|
|
2507
|
+
if (excluded.includes(subKey)) continue;
|
|
2508
|
+
if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
|
|
2509
|
+
exportFields.push(`${attrName}.${subKey}`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2124
2515
|
exportFields.push(attrName);
|
|
2125
2516
|
}
|
|
2126
2517
|
const filters2 = {};
|
|
@@ -2164,16 +2555,25 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2164
2555
|
}
|
|
2165
2556
|
}
|
|
2166
2557
|
}
|
|
2558
|
+
const populateOption = compFieldsToPopulate.length > 0 ? compFieldsToPopulate.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
|
|
2167
2559
|
const entries = await strapi2.documents(uid).findMany({
|
|
2168
2560
|
filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
|
|
2169
2561
|
limit: options.limit || 5e3,
|
|
2170
|
-
sort: { createdAt: "desc" }
|
|
2562
|
+
sort: { createdAt: "desc" },
|
|
2563
|
+
populate: populateOption
|
|
2171
2564
|
});
|
|
2172
2565
|
const lines = [];
|
|
2173
2566
|
lines.push(exportFields.map(escapeCsvValue).join(","));
|
|
2174
2567
|
for (const entry of entries || []) {
|
|
2175
2568
|
const rowValues = exportFields.map((field) => {
|
|
2176
|
-
|
|
2569
|
+
let val;
|
|
2570
|
+
if (field.includes(".")) {
|
|
2571
|
+
const [parent, child] = field.split(".");
|
|
2572
|
+
val = entry[parent]?.[child];
|
|
2573
|
+
} else {
|
|
2574
|
+
val = entry[field];
|
|
2575
|
+
}
|
|
2576
|
+
return escapeCsvValue(val);
|
|
2177
2577
|
});
|
|
2178
2578
|
lines.push(rowValues.join(","));
|
|
2179
2579
|
}
|
|
@@ -2259,6 +2659,29 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2259
2659
|
const payload = {};
|
|
2260
2660
|
for (const [key, rawVal] of Object.entries(row)) {
|
|
2261
2661
|
if (key === "documentId" || key === "id") continue;
|
|
2662
|
+
if (key.includes(".")) {
|
|
2663
|
+
const [parent, child] = key.split(".");
|
|
2664
|
+
const parentDef = attrs[parent];
|
|
2665
|
+
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
2666
|
+
const comp = strapi2.components[parentDef.component];
|
|
2667
|
+
const childDef = comp?.attributes?.[child];
|
|
2668
|
+
if (childDef) {
|
|
2669
|
+
if (!payload[parent]) payload[parent] = {};
|
|
2670
|
+
let val = rawVal;
|
|
2671
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
2672
|
+
val = null;
|
|
2673
|
+
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
2674
|
+
val = parseInt(rawVal, 10);
|
|
2675
|
+
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
2676
|
+
val = parseFloat(rawVal);
|
|
2677
|
+
} else if (childDef.type === "boolean") {
|
|
2678
|
+
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
2679
|
+
}
|
|
2680
|
+
payload[parent][child] = val;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2262
2685
|
const attrDef = attrs[key];
|
|
2263
2686
|
if (!attrDef) continue;
|
|
2264
2687
|
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
@@ -2310,9 +2733,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2310
2733
|
});
|
|
2311
2734
|
continue;
|
|
2312
2735
|
}
|
|
2736
|
+
const mergedPayload = { ...payload };
|
|
2737
|
+
for (const key of Object.keys(payload)) {
|
|
2738
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
2739
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2313
2742
|
await strapi2.documents(uid).update({
|
|
2314
2743
|
documentId: docId,
|
|
2315
|
-
data:
|
|
2744
|
+
data: mergedPayload
|
|
2316
2745
|
});
|
|
2317
2746
|
if (publish && ct.options?.draftAndPublish) {
|
|
2318
2747
|
try {
|
|
@@ -2339,9 +2768,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
|
|
|
2339
2768
|
documentId: docId
|
|
2340
2769
|
});
|
|
2341
2770
|
if (existing) {
|
|
2771
|
+
const mergedPayload = { ...payload };
|
|
2772
|
+
for (const key of Object.keys(payload)) {
|
|
2773
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
2774
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2342
2777
|
await strapi2.documents(uid).update({
|
|
2343
2778
|
documentId: docId,
|
|
2344
|
-
data:
|
|
2779
|
+
data: mergedPayload
|
|
2345
2780
|
});
|
|
2346
2781
|
if (publish && ct.options?.draftAndPublish) {
|
|
2347
2782
|
try {
|
|
@@ -31944,6 +32379,7 @@ const index = {
|
|
|
31944
32379
|
});
|
|
31945
32380
|
},
|
|
31946
32381
|
routes,
|
|
32382
|
+
contentTypes,
|
|
31947
32383
|
controllers: {
|
|
31948
32384
|
bulkUpdater: bulkUpdaterController,
|
|
31949
32385
|
audit: auditController,
|