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