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.
- package/dist/_chunks/{BulkUpdater-NpSxrH3b.mjs → BulkUpdater-B_COVQmo.mjs} +473 -34
- package/dist/_chunks/BulkUpdater-B_COVQmo.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-Q4rX3L5w.js → BulkUpdater-DS6gm_qd.js} +472 -33
- package/dist/_chunks/BulkUpdater-DS6gm_qd.js.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +270 -12
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +270 -12
- 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/admin/index.js
CHANGED
|
@@ -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-
|
|
13
|
+
Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-DS6gm_qd.js")),
|
|
14
14
|
permissions: [],
|
|
15
15
|
position: 3
|
|
16
16
|
});
|
package/dist/admin/index.mjs
CHANGED
|
@@ -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-
|
|
12
|
+
Component: () => import("../_chunks/BulkUpdater-B_COVQmo.mjs"),
|
|
13
13
|
permissions: [],
|
|
14
14
|
position: 3
|
|
15
15
|
});
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
*/
|