strapi-plugin-bulk-updater 1.2.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.
@@ -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 {
@@ -1645,6 +1717,7 @@ const DEFAULT_FIELDS = {
1645
1717
  executedAt: "executedAt"
1646
1718
  };
1647
1719
  const DEFAULT_CANDIDATE_TYPES = [
1720
+ "plugin::bulk-updater.audit-log",
1648
1721
  "api::audit-log.audit-log",
1649
1722
  "api::audit.audit",
1650
1723
  "api::bulk-audit-log.bulk-audit-log"
@@ -1666,6 +1739,7 @@ const auditService = ({ strapi: strapi2 }) => ({
1666
1739
  * 1. Explicit consumer custom schema (full declarative control)
1667
1740
  * 2. Zero-config convention auto-detection (api::audit-log.audit-log)
1668
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)
1669
1743
  */
1670
1744
  getAuditConfig() {
1671
1745
  const rawConfig = strapi2.config.get("plugin::bulk-updater.audit");
@@ -1719,10 +1793,9 @@ const auditService = ({ strapi: strapi2 }) => ({
1719
1793
  };
1720
1794
  }
1721
1795
  return {
1722
- enabled: false,
1723
- mode: "disabled",
1724
- isEnterpriseNativeAvailable: false,
1725
- 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
1726
1799
  };
1727
1800
  },
1728
1801
  /**
@@ -1767,7 +1840,40 @@ const auditService = ({ strapi: strapi2 }) => ({
1767
1840
  },
1768
1841
  user
1769
1842
  );
1770
- if (!config.enabled || !config.contentType || !config.fields) {
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) {
1771
1877
  return null;
1772
1878
  }
1773
1879
  const f = config.fields;
@@ -1808,6 +1914,25 @@ const auditService = ({ strapi: strapi2 }) => ({
1808
1914
  history: []
1809
1915
  };
1810
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
+ }
1811
1936
  if (!config.enabled || !config.contentType || !config.fields) {
1812
1937
  return {
1813
1938
  enabled: false,
@@ -1853,27 +1978,9 @@ const auditService = ({ strapi: strapi2 }) => ({
1853
1978
  }
1854
1979
  },
1855
1980
  /**
1856
- * Performs deterministic rollback of a batch
1981
+ * Helper to revert diff items back to original values
1857
1982
  */
1858
- async rollbackBatch(batchId, currentUser) {
1859
- const config = this.getAuditConfig();
1860
- if (!config.enabled || !config.contentType || !config.fields) {
1861
- throw new Error("Audit trail is not configured or disabled.");
1862
- }
1863
- const f = config.fields;
1864
- const records = await strapi2.documents(config.contentType).findMany({
1865
- filters: {
1866
- [f.batchId]: { $eq: batchId }
1867
- }
1868
- });
1869
- const record = records?.[0];
1870
- if (!record) {
1871
- throw new Error(`Batch record with ID "${batchId}" was not found in audit trail.`);
1872
- }
1873
- if (record[f.isRolledBack]) {
1874
- throw new Error(`Batch "${batchId}" has already been rolled back.`);
1875
- }
1876
- const diffs = typeof record[f.diffs] === "string" ? JSON.parse(record[f.diffs]) : record[f.diffs] || [];
1983
+ async revertDiffItems(diffs) {
1877
1984
  let revertedCount = 0;
1878
1985
  const rollbackDetails = [];
1879
1986
  for (const item of diffs) {
@@ -1958,6 +2065,78 @@ const auditService = ({ strapi: strapi2 }) => ({
1958
2065
  }
1959
2066
  }
1960
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);
1961
2140
  const updateData = {};
1962
2141
  if (f.isRolledBack) updateData[f.isRolledBack] = true;
1963
2142
  if (f.rolledBackAt) updateData[f.rolledBackAt] = (/* @__PURE__ */ new Date()).toISOString();
@@ -32200,6 +32379,7 @@ const index = {
32200
32379
  });
32201
32380
  },
32202
32381
  routes,
32382
+ contentTypes,
32203
32383
  controllers: {
32204
32384
  bulkUpdater: bulkUpdaterController,
32205
32385
  audit: auditController,