strapi-plugin-bulk-updater 1.2.9 → 1.2.10

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.
@@ -10,7 +10,7 @@ const index = {
10
10
  id: `${PLUGIN_ID}.plugin.name`,
11
11
  defaultMessage: "Bulk Attribute Updater"
12
12
  },
13
- Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-Q4rX3L5w.js")),
13
+ Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-EI4sKaLN.js")),
14
14
  permissions: [],
15
15
  position: 3
16
16
  });
@@ -9,7 +9,7 @@ const index = {
9
9
  id: `${PLUGIN_ID}.plugin.name`,
10
10
  defaultMessage: "Bulk Attribute Updater"
11
11
  },
12
- Component: () => import("../_chunks/BulkUpdater-NpSxrH3b.mjs"),
12
+ Component: () => import("../_chunks/BulkUpdater-Cg9utqzm.mjs"),
13
13
  permissions: [],
14
14
  position: 3
15
15
  });
@@ -59,6 +59,14 @@ const routes = {
59
59
  policies: []
60
60
  }
61
61
  },
62
+ {
63
+ method: "POST",
64
+ path: "/preview",
65
+ handler: "bulkUpdater.previewBatch",
66
+ config: {
67
+ policies: []
68
+ }
69
+ },
62
70
  {
63
71
  method: "GET",
64
72
  path: "/pending-batches",
@@ -353,6 +361,16 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
353
361
  ctx.body = { success: false, error: err.message };
354
362
  }
355
363
  },
364
+ async previewBatch(ctx) {
365
+ try {
366
+ const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
367
+ const result = await service.previewBatch(ctx.request.body || {});
368
+ ctx.body = result;
369
+ } catch (err) {
370
+ ctx.status = 400;
371
+ ctx.body = { success: false, error: err.message };
372
+ }
373
+ },
356
374
  async getPendingBatches(ctx) {
357
375
  try {
358
376
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
@@ -1557,6 +1575,252 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1557
1575
  queryCache.set(cacheKey, { signature, data: result, timestamp: Date.now() });
1558
1576
  return result;
1559
1577
  },
1578
+ /**
1579
+ * Preview bulk update changes before execution:
1580
+ * Reads current values and computes proposed new values without mutating data.
1581
+ */
1582
+ async previewBatch({
1583
+ attribute,
1584
+ value,
1585
+ selectedEntries = [],
1586
+ selectedCollections = [],
1587
+ isRegex = false,
1588
+ regexPattern = ""
1589
+ }) {
1590
+ if (!attribute) {
1591
+ throw new Error("Attribute name is required.");
1592
+ }
1593
+ const targetsMap = /* @__PURE__ */ new Map();
1594
+ for (const colUid of selectedCollections) {
1595
+ if (!strapi2.contentTypes[colUid]) continue;
1596
+ const entries = await strapi2.documents(colUid).findMany();
1597
+ for (const entry of entries || []) {
1598
+ const key = `${colUid}@@@${entry.documentId}`;
1599
+ targetsMap.set(key, { uid: colUid, documentId: entry.documentId });
1600
+ }
1601
+ }
1602
+ for (const item of selectedEntries) {
1603
+ if (!item.uid || !item.documentId) continue;
1604
+ const key = `${item.uid}@@@${item.documentId}`;
1605
+ targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
1606
+ }
1607
+ if (targetsMap.size === 0) {
1608
+ throw new Error("No collections or entries were selected for preview.");
1609
+ }
1610
+ let regexObj = null;
1611
+ if (isRegex && regexPattern) {
1612
+ try {
1613
+ regexObj = new RegExp(regexPattern, "g");
1614
+ } catch (e) {
1615
+ throw new Error(`Invalid regular expression: ${e.message}`);
1616
+ }
1617
+ }
1618
+ const targets = Array.from(targetsMap.values());
1619
+ const diffs = [];
1620
+ let willUpdateCount = 0;
1621
+ let unchangedCount = 0;
1622
+ let skippedCount = 0;
1623
+ for (const target of targets) {
1624
+ const contentType = strapi2.contentTypes[target.uid];
1625
+ const displayName = contentType?.info?.displayName || target.uid;
1626
+ try {
1627
+ const isNested = attribute.includes(".");
1628
+ let isComponentNested = false;
1629
+ let parentKey = "";
1630
+ let childKey = "";
1631
+ if (isNested) {
1632
+ const parts = attribute.split(".");
1633
+ parentKey = parts[0];
1634
+ childKey = parts[1];
1635
+ const parentAttrDef = contentType?.attributes?.[parentKey];
1636
+ if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
1637
+ const compSchema = strapi2.components[parentAttrDef.component];
1638
+ if (compSchema?.attributes?.[childKey]) {
1639
+ isComponentNested = true;
1640
+ }
1641
+ }
1642
+ }
1643
+ const attrDef = contentType?.attributes?.[attribute];
1644
+ const hasAttribute = Boolean(attrDef) || isComponentNested;
1645
+ if (!hasAttribute) {
1646
+ skippedCount++;
1647
+ diffs.push({
1648
+ uid: target.uid,
1649
+ collectionName: displayName,
1650
+ documentId: target.documentId,
1651
+ label: `Entry #${target.documentId}`,
1652
+ attribute,
1653
+ oldValue: null,
1654
+ newValue: null,
1655
+ oldDisplay: "—",
1656
+ newDisplay: "—",
1657
+ hasChange: false,
1658
+ status: "will_skip",
1659
+ message: `Attribute "${attribute}" does not exist in schema for ${displayName}`
1660
+ });
1661
+ continue;
1662
+ }
1663
+ let currentEntry;
1664
+ if (isComponentNested) {
1665
+ currentEntry = await strapi2.documents(target.uid).findOne({
1666
+ documentId: target.documentId,
1667
+ populate: [parentKey]
1668
+ });
1669
+ } else if (attrDef?.type === "relation") {
1670
+ currentEntry = await strapi2.documents(target.uid).findOne({
1671
+ documentId: target.documentId,
1672
+ populate: [attribute]
1673
+ });
1674
+ } else {
1675
+ currentEntry = await strapi2.documents(target.uid).findOne({
1676
+ documentId: target.documentId
1677
+ });
1678
+ }
1679
+ const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
1680
+ if (isComponentNested) {
1681
+ const existingParent = currentEntry?.[parentKey] || {};
1682
+ const currentVal2 = existingParent?.[childKey];
1683
+ let newValue2 = value;
1684
+ if (regexObj && typeof currentVal2 === "string") {
1685
+ newValue2 = currentVal2.replace(regexObj, value);
1686
+ }
1687
+ const hasChange2 = String(currentVal2 ?? "") !== String(newValue2 ?? "");
1688
+ if (hasChange2) {
1689
+ willUpdateCount++;
1690
+ } else {
1691
+ unchangedCount++;
1692
+ }
1693
+ diffs.push({
1694
+ uid: target.uid,
1695
+ collectionName: displayName,
1696
+ documentId: target.documentId,
1697
+ label: entryLabel,
1698
+ attribute,
1699
+ oldValue: currentVal2 ?? null,
1700
+ newValue: newValue2,
1701
+ oldDisplay: currentVal2 !== void 0 && currentVal2 !== null ? String(currentVal2) : "(empty)",
1702
+ newDisplay: String(newValue2 ?? ""),
1703
+ hasChange: hasChange2,
1704
+ status: hasChange2 ? "will_update" : "unchanged",
1705
+ message: hasChange2 ? "Value will be updated" : "Already has target value"
1706
+ });
1707
+ continue;
1708
+ }
1709
+ if (attrDef?.type === "relation") {
1710
+ const isMulti = attrDef.relation === "oneToMany" || attrDef.relation === "manyToMany";
1711
+ const currentRel = currentEntry?.[attribute];
1712
+ let oldDocIds = [];
1713
+ let oldDisplay = "";
1714
+ if (isMulti && Array.isArray(currentRel)) {
1715
+ oldDocIds = currentRel.map((r) => r.documentId).filter(Boolean);
1716
+ oldDisplay = currentRel.map((r) => r.name || r.title || r.email || r.documentId).join(", ") || "(empty)";
1717
+ } else if (currentRel) {
1718
+ oldDocIds = currentRel.documentId ? [currentRel.documentId] : [];
1719
+ oldDisplay = currentRel.name || currentRel.title || currentRel.email || currentRel.documentId || "(empty)";
1720
+ } else {
1721
+ oldDisplay = "(unlinked / none)";
1722
+ }
1723
+ let mode2 = "set";
1724
+ let targetDocIds = [];
1725
+ let newDisplay = "";
1726
+ if (value === "__clear__" || value === "") {
1727
+ mode2 = "clear";
1728
+ newDisplay = "(cleared / unlinked)";
1729
+ } else {
1730
+ try {
1731
+ const parsed = JSON.parse(value);
1732
+ if (parsed && typeof parsed === "object" && parsed.mode) {
1733
+ mode2 = parsed.mode;
1734
+ targetDocIds = Array.isArray(parsed.documentIds) ? parsed.documentIds : parsed.documentId ? [parsed.documentId] : [];
1735
+ newDisplay = parsed.label || targetDocIds.join(", ") || `[${mode2}]`;
1736
+ } else if (Array.isArray(parsed)) {
1737
+ targetDocIds = parsed;
1738
+ newDisplay = targetDocIds.join(", ");
1739
+ } else {
1740
+ targetDocIds = [value];
1741
+ newDisplay = value;
1742
+ }
1743
+ } catch {
1744
+ targetDocIds = [value];
1745
+ newDisplay = value;
1746
+ }
1747
+ }
1748
+ const hasChange2 = oldDisplay !== newDisplay;
1749
+ if (hasChange2) {
1750
+ willUpdateCount++;
1751
+ } else {
1752
+ unchangedCount++;
1753
+ }
1754
+ diffs.push({
1755
+ uid: target.uid,
1756
+ collectionName: displayName,
1757
+ documentId: target.documentId,
1758
+ label: entryLabel,
1759
+ attribute,
1760
+ oldValue: { isRelation: true, documentIds: oldDocIds, display: oldDisplay },
1761
+ newValue: { isRelation: true, mode: mode2, documentIds: targetDocIds, display: newDisplay },
1762
+ oldDisplay,
1763
+ newDisplay,
1764
+ hasChange: hasChange2,
1765
+ status: hasChange2 ? "will_update" : "unchanged",
1766
+ message: hasChange2 ? `Relation will be modified (${mode2})` : "Relation already set to this target"
1767
+ });
1768
+ continue;
1769
+ }
1770
+ let newValue = value;
1771
+ const currentVal = currentEntry?.[attribute];
1772
+ if (regexObj && typeof currentVal === "string") {
1773
+ newValue = currentVal.replace(regexObj, value);
1774
+ }
1775
+ const hasChange = String(currentVal ?? "") !== String(newValue ?? "");
1776
+ if (hasChange) {
1777
+ willUpdateCount++;
1778
+ } else {
1779
+ unchangedCount++;
1780
+ }
1781
+ diffs.push({
1782
+ uid: target.uid,
1783
+ collectionName: displayName,
1784
+ documentId: target.documentId,
1785
+ label: entryLabel,
1786
+ attribute,
1787
+ oldValue: currentVal ?? null,
1788
+ newValue,
1789
+ oldDisplay: currentVal !== void 0 && currentVal !== null ? String(currentVal) : "(empty)",
1790
+ newDisplay: String(newValue ?? ""),
1791
+ hasChange,
1792
+ status: hasChange ? "will_update" : "unchanged",
1793
+ message: hasChange ? "Value will be updated" : "Already matches target value"
1794
+ });
1795
+ } catch (err) {
1796
+ skippedCount++;
1797
+ diffs.push({
1798
+ uid: target.uid,
1799
+ collectionName: displayName,
1800
+ documentId: target.documentId,
1801
+ label: `Entry #${target.documentId}`,
1802
+ attribute,
1803
+ oldValue: null,
1804
+ newValue: null,
1805
+ oldDisplay: "—",
1806
+ newDisplay: "—",
1807
+ hasChange: false,
1808
+ status: "will_skip",
1809
+ message: `Error inspecting entry: ${err.message}`
1810
+ });
1811
+ }
1812
+ }
1813
+ return {
1814
+ success: true,
1815
+ summary: {
1816
+ total: targets.length,
1817
+ willUpdate: willUpdateCount,
1818
+ unchanged: unchangedCount,
1819
+ skipped: skippedCount
1820
+ },
1821
+ diffs
1822
+ };
1823
+ },
1560
1824
  /**
1561
1825
  * Batch Update API with Maker-Checker and Off-Peak Scheduling:
1562
1826
  */