strapi-plugin-bulk-updater 1.2.9 → 1.2.11

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-DS6gm_qd.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-B_COVQmo.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",
@@ -286,10 +294,6 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
286
294
  if (data?.etag) {
287
295
  ctx.set("ETag", data.etag);
288
296
  ctx.set("Cache-Control", "private, no-cache");
289
- if (ctx.request.headers["if-none-match"] === data.etag) {
290
- ctx.status = 304;
291
- return;
292
- }
293
297
  }
294
298
  ctx.body = data;
295
299
  } catch (err) {
@@ -315,10 +319,6 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
315
319
  if (data?.etag) {
316
320
  ctx.set("ETag", data.etag);
317
321
  ctx.set("Cache-Control", "private, no-cache");
318
- if (ctx.request.headers["if-none-match"] === data.etag) {
319
- ctx.status = 304;
320
- return;
321
- }
322
322
  }
323
323
  ctx.body = { success: true, ...data };
324
324
  } catch (err) {
@@ -353,6 +353,16 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
353
353
  ctx.body = { success: false, error: err.message };
354
354
  }
355
355
  },
356
+ async previewBatch(ctx) {
357
+ try {
358
+ const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
359
+ const result = await service.previewBatch(ctx.request.body || {});
360
+ ctx.body = result;
361
+ } catch (err) {
362
+ ctx.status = 400;
363
+ ctx.body = { success: false, error: err.message };
364
+ }
365
+ },
356
366
  async getPendingBatches(ctx) {
357
367
  try {
358
368
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
@@ -1385,10 +1395,12 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1385
1395
  }
1386
1396
  let condition = void 0;
1387
1397
  const q = (filterQuery || "").trim();
1398
+ const targetAttrSchema = isNestedAttr ? strapi2.components[attrs[parentKey]?.component]?.attributes?.[childKey] : attrs[attribute];
1399
+ const isStringLike = ["string", "text", "email", "richtext", "uid"].includes(targetAttrSchema?.type);
1388
1400
  if (normOp === "empty") {
1389
- condition = { $or: [{ $null: true }, { $eq: "" }] };
1401
+ condition = isStringLike ? { $or: [{ $null: true }, { $eq: "" }] } : { $null: true };
1390
1402
  } else if (normOp === "notempty") {
1391
- condition = { $and: [{ $notNull: true }, { $ne: "" }] };
1403
+ condition = isStringLike ? { $and: [{ $notNull: true }, { $ne: "" }] } : { $notNull: true };
1392
1404
  } else if (q !== "") {
1393
1405
  switch (normOp) {
1394
1406
  case "equals":
@@ -1417,12 +1429,12 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1417
1429
  [childKey]: condition
1418
1430
  };
1419
1431
  } else {
1420
- if (normOp === "empty") {
1432
+ if (normOp === "empty" && isStringLike) {
1421
1433
  filters2.$or = [
1422
1434
  { [attribute]: { $null: true } },
1423
1435
  { [attribute]: { $eq: "" } }
1424
1436
  ];
1425
- } else if (normOp === "notempty") {
1437
+ } else if (normOp === "notempty" && isStringLike) {
1426
1438
  filters2[attribute] = { $notNull: true, $ne: "" };
1427
1439
  } else {
1428
1440
  filters2[attribute] = condition;
@@ -1557,6 +1569,252 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1557
1569
  queryCache.set(cacheKey, { signature, data: result, timestamp: Date.now() });
1558
1570
  return result;
1559
1571
  },
1572
+ /**
1573
+ * Preview bulk update changes before execution:
1574
+ * Reads current values and computes proposed new values without mutating data.
1575
+ */
1576
+ async previewBatch({
1577
+ attribute,
1578
+ value,
1579
+ selectedEntries = [],
1580
+ selectedCollections = [],
1581
+ isRegex = false,
1582
+ regexPattern = ""
1583
+ }) {
1584
+ if (!attribute) {
1585
+ throw new Error("Attribute name is required.");
1586
+ }
1587
+ const targetsMap = /* @__PURE__ */ new Map();
1588
+ for (const colUid of selectedCollections) {
1589
+ if (!strapi2.contentTypes[colUid]) continue;
1590
+ const entries = await strapi2.documents(colUid).findMany();
1591
+ for (const entry of entries || []) {
1592
+ const key = `${colUid}@@@${entry.documentId}`;
1593
+ targetsMap.set(key, { uid: colUid, documentId: entry.documentId });
1594
+ }
1595
+ }
1596
+ for (const item of selectedEntries) {
1597
+ if (!item.uid || !item.documentId) continue;
1598
+ const key = `${item.uid}@@@${item.documentId}`;
1599
+ targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
1600
+ }
1601
+ if (targetsMap.size === 0) {
1602
+ throw new Error("No collections or entries were selected for preview.");
1603
+ }
1604
+ let regexObj = null;
1605
+ if (isRegex && regexPattern) {
1606
+ try {
1607
+ regexObj = new RegExp(regexPattern, "g");
1608
+ } catch (e) {
1609
+ throw new Error(`Invalid regular expression: ${e.message}`);
1610
+ }
1611
+ }
1612
+ const targets = Array.from(targetsMap.values());
1613
+ const diffs = [];
1614
+ let willUpdateCount = 0;
1615
+ let unchangedCount = 0;
1616
+ let skippedCount = 0;
1617
+ for (const target of targets) {
1618
+ const contentType = strapi2.contentTypes[target.uid];
1619
+ const displayName = contentType?.info?.displayName || target.uid;
1620
+ try {
1621
+ const isNested = attribute.includes(".");
1622
+ let isComponentNested = false;
1623
+ let parentKey = "";
1624
+ let childKey = "";
1625
+ if (isNested) {
1626
+ const parts = attribute.split(".");
1627
+ parentKey = parts[0];
1628
+ childKey = parts[1];
1629
+ const parentAttrDef = contentType?.attributes?.[parentKey];
1630
+ if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
1631
+ const compSchema = strapi2.components[parentAttrDef.component];
1632
+ if (compSchema?.attributes?.[childKey]) {
1633
+ isComponentNested = true;
1634
+ }
1635
+ }
1636
+ }
1637
+ const attrDef = contentType?.attributes?.[attribute];
1638
+ const hasAttribute = Boolean(attrDef) || isComponentNested;
1639
+ if (!hasAttribute) {
1640
+ skippedCount++;
1641
+ diffs.push({
1642
+ uid: target.uid,
1643
+ collectionName: displayName,
1644
+ documentId: target.documentId,
1645
+ label: `Entry #${target.documentId}`,
1646
+ attribute,
1647
+ oldValue: null,
1648
+ newValue: null,
1649
+ oldDisplay: "—",
1650
+ newDisplay: "—",
1651
+ hasChange: false,
1652
+ status: "will_skip",
1653
+ message: `Attribute "${attribute}" does not exist in schema for ${displayName}`
1654
+ });
1655
+ continue;
1656
+ }
1657
+ let currentEntry;
1658
+ if (isComponentNested) {
1659
+ currentEntry = await strapi2.documents(target.uid).findOne({
1660
+ documentId: target.documentId,
1661
+ populate: [parentKey]
1662
+ });
1663
+ } else if (attrDef?.type === "relation") {
1664
+ currentEntry = await strapi2.documents(target.uid).findOne({
1665
+ documentId: target.documentId,
1666
+ populate: [attribute]
1667
+ });
1668
+ } else {
1669
+ currentEntry = await strapi2.documents(target.uid).findOne({
1670
+ documentId: target.documentId
1671
+ });
1672
+ }
1673
+ const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
1674
+ if (isComponentNested) {
1675
+ const existingParent = currentEntry?.[parentKey] || {};
1676
+ const currentVal2 = existingParent?.[childKey];
1677
+ let newValue2 = value;
1678
+ if (regexObj && typeof currentVal2 === "string") {
1679
+ newValue2 = currentVal2.replace(regexObj, value);
1680
+ }
1681
+ const hasChange2 = String(currentVal2 ?? "") !== String(newValue2 ?? "");
1682
+ if (hasChange2) {
1683
+ willUpdateCount++;
1684
+ } else {
1685
+ unchangedCount++;
1686
+ }
1687
+ diffs.push({
1688
+ uid: target.uid,
1689
+ collectionName: displayName,
1690
+ documentId: target.documentId,
1691
+ label: entryLabel,
1692
+ attribute,
1693
+ oldValue: currentVal2 ?? null,
1694
+ newValue: newValue2,
1695
+ oldDisplay: currentVal2 !== void 0 && currentVal2 !== null ? String(currentVal2) : "(empty)",
1696
+ newDisplay: String(newValue2 ?? ""),
1697
+ hasChange: hasChange2,
1698
+ status: hasChange2 ? "will_update" : "unchanged",
1699
+ message: hasChange2 ? "Value will be updated" : "Already has target value"
1700
+ });
1701
+ continue;
1702
+ }
1703
+ if (attrDef?.type === "relation") {
1704
+ const isMulti = attrDef.relation === "oneToMany" || attrDef.relation === "manyToMany";
1705
+ const currentRel = currentEntry?.[attribute];
1706
+ let oldDocIds = [];
1707
+ let oldDisplay = "";
1708
+ if (isMulti && Array.isArray(currentRel)) {
1709
+ oldDocIds = currentRel.map((r) => r.documentId).filter(Boolean);
1710
+ oldDisplay = currentRel.map((r) => r.name || r.title || r.email || r.documentId).join(", ") || "(empty)";
1711
+ } else if (currentRel) {
1712
+ oldDocIds = currentRel.documentId ? [currentRel.documentId] : [];
1713
+ oldDisplay = currentRel.name || currentRel.title || currentRel.email || currentRel.documentId || "(empty)";
1714
+ } else {
1715
+ oldDisplay = "(unlinked / none)";
1716
+ }
1717
+ let mode2 = "set";
1718
+ let targetDocIds = [];
1719
+ let newDisplay = "";
1720
+ if (value === "__clear__" || value === "") {
1721
+ mode2 = "clear";
1722
+ newDisplay = "(cleared / unlinked)";
1723
+ } else {
1724
+ try {
1725
+ const parsed = JSON.parse(value);
1726
+ if (parsed && typeof parsed === "object" && parsed.mode) {
1727
+ mode2 = parsed.mode;
1728
+ targetDocIds = Array.isArray(parsed.documentIds) ? parsed.documentIds : parsed.documentId ? [parsed.documentId] : [];
1729
+ newDisplay = parsed.label || targetDocIds.join(", ") || `[${mode2}]`;
1730
+ } else if (Array.isArray(parsed)) {
1731
+ targetDocIds = parsed;
1732
+ newDisplay = targetDocIds.join(", ");
1733
+ } else {
1734
+ targetDocIds = [value];
1735
+ newDisplay = value;
1736
+ }
1737
+ } catch {
1738
+ targetDocIds = [value];
1739
+ newDisplay = value;
1740
+ }
1741
+ }
1742
+ const hasChange2 = oldDisplay !== newDisplay;
1743
+ if (hasChange2) {
1744
+ willUpdateCount++;
1745
+ } else {
1746
+ unchangedCount++;
1747
+ }
1748
+ diffs.push({
1749
+ uid: target.uid,
1750
+ collectionName: displayName,
1751
+ documentId: target.documentId,
1752
+ label: entryLabel,
1753
+ attribute,
1754
+ oldValue: { isRelation: true, documentIds: oldDocIds, display: oldDisplay },
1755
+ newValue: { isRelation: true, mode: mode2, documentIds: targetDocIds, display: newDisplay },
1756
+ oldDisplay,
1757
+ newDisplay,
1758
+ hasChange: hasChange2,
1759
+ status: hasChange2 ? "will_update" : "unchanged",
1760
+ message: hasChange2 ? `Relation will be modified (${mode2})` : "Relation already set to this target"
1761
+ });
1762
+ continue;
1763
+ }
1764
+ let newValue = value;
1765
+ const currentVal = currentEntry?.[attribute];
1766
+ if (regexObj && typeof currentVal === "string") {
1767
+ newValue = currentVal.replace(regexObj, value);
1768
+ }
1769
+ const hasChange = String(currentVal ?? "") !== String(newValue ?? "");
1770
+ if (hasChange) {
1771
+ willUpdateCount++;
1772
+ } else {
1773
+ unchangedCount++;
1774
+ }
1775
+ diffs.push({
1776
+ uid: target.uid,
1777
+ collectionName: displayName,
1778
+ documentId: target.documentId,
1779
+ label: entryLabel,
1780
+ attribute,
1781
+ oldValue: currentVal ?? null,
1782
+ newValue,
1783
+ oldDisplay: currentVal !== void 0 && currentVal !== null ? String(currentVal) : "(empty)",
1784
+ newDisplay: String(newValue ?? ""),
1785
+ hasChange,
1786
+ status: hasChange ? "will_update" : "unchanged",
1787
+ message: hasChange ? "Value will be updated" : "Already matches target value"
1788
+ });
1789
+ } catch (err) {
1790
+ skippedCount++;
1791
+ diffs.push({
1792
+ uid: target.uid,
1793
+ collectionName: displayName,
1794
+ documentId: target.documentId,
1795
+ label: `Entry #${target.documentId}`,
1796
+ attribute,
1797
+ oldValue: null,
1798
+ newValue: null,
1799
+ oldDisplay: "—",
1800
+ newDisplay: "—",
1801
+ hasChange: false,
1802
+ status: "will_skip",
1803
+ message: `Error inspecting entry: ${err.message}`
1804
+ });
1805
+ }
1806
+ }
1807
+ return {
1808
+ success: true,
1809
+ summary: {
1810
+ total: targets.length,
1811
+ willUpdate: willUpdateCount,
1812
+ unchanged: unchangedCount,
1813
+ skipped: skippedCount
1814
+ },
1815
+ diffs
1816
+ };
1817
+ },
1560
1818
  /**
1561
1819
  * Batch Update API with Maker-Checker and Off-Peak Scheduling:
1562
1820
  */