strapi-plugin-bulk-updater 1.4.10 → 1.5.2
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/README.md +3 -0
- package/dist/_chunks/{BulkUpdater-CHdA0546.mjs → BulkUpdater-BiSkkAHF.mjs} +673 -147
- package/dist/_chunks/BulkUpdater-BiSkkAHF.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-C0uPoso2.js → BulkUpdater-JZW74XM4.js} +672 -146
- package/dist/_chunks/BulkUpdater-JZW74XM4.js.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +687 -231
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +687 -231
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/_chunks/BulkUpdater-C0uPoso2.js.map +0 -1
- package/dist/_chunks/BulkUpdater-CHdA0546.mjs.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -111,6 +111,22 @@ const routes = {
|
|
|
111
111
|
policies: []
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
|
+
{
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: "/batches/:batchId/diffs",
|
|
117
|
+
handler: "bulkUpdater.getPendingBatchDiffs",
|
|
118
|
+
config: {
|
|
119
|
+
policies: []
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
method: "GET",
|
|
124
|
+
path: "/batches/:batchId/diffs/export",
|
|
125
|
+
handler: "bulkUpdater.exportPendingBatchDiffs",
|
|
126
|
+
config: {
|
|
127
|
+
policies: []
|
|
128
|
+
}
|
|
129
|
+
},
|
|
114
130
|
{
|
|
115
131
|
method: "GET",
|
|
116
132
|
path: "/audit/history",
|
|
@@ -474,6 +490,28 @@ const bulkUpdaterController = ({ strapi }) => ({
|
|
|
474
490
|
ctx.body = { success: false, error: err.message };
|
|
475
491
|
}
|
|
476
492
|
},
|
|
493
|
+
async getPendingBatchDiffs(ctx) {
|
|
494
|
+
try {
|
|
495
|
+
const { batchId } = ctx.params;
|
|
496
|
+
const service = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
497
|
+
const result = await service.getPendingBatchDiffs(batchId);
|
|
498
|
+
ctx.body = result;
|
|
499
|
+
} catch (err) {
|
|
500
|
+
ctx.status = err.message?.includes("not found") ? 404 : 500;
|
|
501
|
+
ctx.body = { success: false, error: err.message };
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
async exportPendingBatchDiffs(ctx) {
|
|
505
|
+
try {
|
|
506
|
+
const { batchId } = ctx.params;
|
|
507
|
+
const service = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
508
|
+
const result = await service.getPendingBatchDiffsCsv(batchId);
|
|
509
|
+
ctx.body = result;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
ctx.status = err.message?.includes("not found") ? 404 : 500;
|
|
512
|
+
ctx.body = { success: false, error: err.message };
|
|
513
|
+
}
|
|
514
|
+
},
|
|
477
515
|
async getRecentBatches(ctx) {
|
|
478
516
|
try {
|
|
479
517
|
const service = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
@@ -1078,14 +1116,15 @@ function discoverComponentAttributes(strapi, compUid, prefix, currentDepth, maxD
|
|
|
1078
1116
|
}
|
|
1079
1117
|
function parseDynamicZonePath(path) {
|
|
1080
1118
|
if (!path) return { cleanPath: "", rootKey: "", subPath: "" };
|
|
1081
|
-
const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\]
|
|
1119
|
+
const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\](?:\.(.+))?$/);
|
|
1082
1120
|
if (bracketMatch) {
|
|
1083
1121
|
const rootKey = bracketMatch[1];
|
|
1084
|
-
const selector = bracketMatch[2];
|
|
1085
|
-
const subPath = bracketMatch[3];
|
|
1086
|
-
const cleanPath = `${rootKey}.${subPath}
|
|
1087
|
-
|
|
1088
|
-
|
|
1122
|
+
const selector = bracketMatch[2].trim();
|
|
1123
|
+
const subPath = bracketMatch[3] || "";
|
|
1124
|
+
const cleanPath = subPath ? `${rootKey}.${subPath}` : rootKey;
|
|
1125
|
+
const numMatch = selector.match(/^#?(\d+)/);
|
|
1126
|
+
if (numMatch) {
|
|
1127
|
+
return { cleanPath, rootKey, subPath, targetIndex: parseInt(numMatch[1], 10) };
|
|
1089
1128
|
}
|
|
1090
1129
|
return { cleanPath, rootKey, subPath, targetComponentUid: selector };
|
|
1091
1130
|
}
|
|
@@ -2374,7 +2413,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2374
2413
|
} else if (normOp === "endswith") {
|
|
2375
2414
|
orConditions.push({ [k]: { $endsWithi: q } });
|
|
2376
2415
|
} else if (normOp === "notequals") {
|
|
2377
|
-
orConditions.push({ [k]: { $
|
|
2416
|
+
orConditions.push({ [k]: { $nei: q } });
|
|
2378
2417
|
} else if (normOp === "notcontains") {
|
|
2379
2418
|
orConditions.push({ [k]: { $notContainsi: q } });
|
|
2380
2419
|
} else {
|
|
@@ -2392,12 +2431,21 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2392
2431
|
} else if (normOp === "notempty") {
|
|
2393
2432
|
condition = isStringLike ? { $and: [{ $notNull: true }, { $ne: "" }] } : { $notNull: true };
|
|
2394
2433
|
} else if (q !== "") {
|
|
2434
|
+
let typedVal = q;
|
|
2435
|
+
const isNumeric = ["integer", "biginteger", "float", "decimal", "number"].includes(targetAttrSchema?.type);
|
|
2436
|
+
const isBoolean = targetAttrSchema?.type === "boolean";
|
|
2437
|
+
if (isNumeric && /^-?\d+(\.\d+)?$/.test(q)) {
|
|
2438
|
+
typedVal = Number(q);
|
|
2439
|
+
} else if (isBoolean) {
|
|
2440
|
+
if (q.toLowerCase() === "true") typedVal = true;
|
|
2441
|
+
else if (q.toLowerCase() === "false") typedVal = false;
|
|
2442
|
+
}
|
|
2395
2443
|
switch (normOp) {
|
|
2396
2444
|
case "equals":
|
|
2397
|
-
condition = { $
|
|
2445
|
+
condition = isStringLike ? { $eqi: q } : { $eq: typedVal };
|
|
2398
2446
|
break;
|
|
2399
2447
|
case "notequals":
|
|
2400
|
-
condition = { $
|
|
2448
|
+
condition = isStringLike ? { $nei: q } : { $ne: typedVal };
|
|
2401
2449
|
break;
|
|
2402
2450
|
case "contains":
|
|
2403
2451
|
condition = { $containsi: q };
|
|
@@ -2406,10 +2454,10 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2406
2454
|
condition = { $notContainsi: q };
|
|
2407
2455
|
break;
|
|
2408
2456
|
case "startswith":
|
|
2409
|
-
condition = { $
|
|
2457
|
+
condition = { $startsWithi: q };
|
|
2410
2458
|
break;
|
|
2411
2459
|
case "endswith":
|
|
2412
|
-
condition = { $
|
|
2460
|
+
condition = { $endsWithi: q };
|
|
2413
2461
|
break;
|
|
2414
2462
|
}
|
|
2415
2463
|
}
|
|
@@ -2535,7 +2583,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2535
2583
|
if (typeof v === "object") {
|
|
2536
2584
|
return testSingleValue(v.name) || testSingleValue(v.title) || testSingleValue(v.email) || testSingleValue(v.username) || testSingleValue(v.documentId) || testSingleValue(v.id) || testSingleValue(v.slug);
|
|
2537
2585
|
}
|
|
2538
|
-
const valStr = String(v).toLowerCase();
|
|
2586
|
+
const valStr = String(v).trim().toLowerCase();
|
|
2539
2587
|
switch (normOp) {
|
|
2540
2588
|
case "equals":
|
|
2541
2589
|
return valStr === qVal;
|
|
@@ -3619,6 +3667,198 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3619
3667
|
batch
|
|
3620
3668
|
};
|
|
3621
3669
|
},
|
|
3670
|
+
/**
|
|
3671
|
+
* Inspect batch changes item-by-item for Maker-Checker 4-Eyes review
|
|
3672
|
+
*/
|
|
3673
|
+
async getPendingBatchDiffs(batchId) {
|
|
3674
|
+
if (!batchId) throw new Error("batchId is required");
|
|
3675
|
+
const batch = pendingBatches.get(batchId);
|
|
3676
|
+
if (!batch) {
|
|
3677
|
+
throw new Error(`Pending batch "${batchId}" not found.`);
|
|
3678
|
+
}
|
|
3679
|
+
let previewRes = null;
|
|
3680
|
+
if (batch.payload?.action === "csv_import" || batch.id.startsWith("csv_")) {
|
|
3681
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
3682
|
+
previewRes = await csvService2.previewCsvBatch(
|
|
3683
|
+
batch.payload.uid,
|
|
3684
|
+
batch.payload.rows || [],
|
|
3685
|
+
batch.payload.options || {}
|
|
3686
|
+
);
|
|
3687
|
+
} else if (batch.payload?.action === "publish") {
|
|
3688
|
+
const selectedEntries = batch.payload.selectedEntries || [];
|
|
3689
|
+
const selectedCollections = batch.payload.selectedCollections || [];
|
|
3690
|
+
const targetsMap = /* @__PURE__ */ new Map();
|
|
3691
|
+
for (const colUid of selectedCollections) {
|
|
3692
|
+
if (!strapi.contentTypes[colUid]) continue;
|
|
3693
|
+
const entries = await strapi.documents(colUid).findMany();
|
|
3694
|
+
for (const entry of entries || []) {
|
|
3695
|
+
targetsMap.set(`${colUid}@@@${entry.documentId}`, { uid: colUid, documentId: entry.documentId });
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
for (const item of selectedEntries) {
|
|
3699
|
+
if (!item.uid || !item.documentId) continue;
|
|
3700
|
+
targetsMap.set(`${item.uid}@@@${item.documentId}`, { uid: item.uid, documentId: item.documentId });
|
|
3701
|
+
}
|
|
3702
|
+
const diffs = [];
|
|
3703
|
+
let willUpdate = 0;
|
|
3704
|
+
let unchanged = 0;
|
|
3705
|
+
for (const target of targetsMap.values()) {
|
|
3706
|
+
const ct = strapi.contentTypes[target.uid];
|
|
3707
|
+
const displayName = ct?.info?.displayName || target.uid;
|
|
3708
|
+
try {
|
|
3709
|
+
const doc = await strapi.documents(target.uid).findOne({
|
|
3710
|
+
documentId: target.documentId
|
|
3711
|
+
});
|
|
3712
|
+
const label = doc ? doc.title || doc.name || doc.slug || doc.documentId : target.documentId;
|
|
3713
|
+
const isLive = Boolean(doc?.publishedAt) && doc?.status !== "modified";
|
|
3714
|
+
const currentStatus = isLive ? "published" : doc?.status === "modified" ? "modified draft" : "draft";
|
|
3715
|
+
const hasChange = !isLive;
|
|
3716
|
+
if (hasChange) willUpdate++;
|
|
3717
|
+
else unchanged++;
|
|
3718
|
+
diffs.push({
|
|
3719
|
+
uid: target.uid,
|
|
3720
|
+
collectionName: displayName,
|
|
3721
|
+
documentId: target.documentId,
|
|
3722
|
+
label,
|
|
3723
|
+
attribute: "status",
|
|
3724
|
+
oldValue: currentStatus,
|
|
3725
|
+
newValue: "published",
|
|
3726
|
+
oldDisplay: currentStatus,
|
|
3727
|
+
newDisplay: "published live",
|
|
3728
|
+
hasChange,
|
|
3729
|
+
status: hasChange ? "will_update" : "unchanged",
|
|
3730
|
+
action: "publish",
|
|
3731
|
+
message: hasChange ? "Record will be published live" : "Already published live"
|
|
3732
|
+
});
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
diffs.push({
|
|
3735
|
+
uid: target.uid,
|
|
3736
|
+
collectionName: displayName,
|
|
3737
|
+
documentId: target.documentId,
|
|
3738
|
+
label: target.documentId,
|
|
3739
|
+
attribute: "status",
|
|
3740
|
+
oldValue: null,
|
|
3741
|
+
newValue: null,
|
|
3742
|
+
oldDisplay: "—",
|
|
3743
|
+
newDisplay: "—",
|
|
3744
|
+
hasChange: false,
|
|
3745
|
+
status: "will_skip",
|
|
3746
|
+
action: "skip",
|
|
3747
|
+
message: `Error inspecting document: ${err.message}`
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
previewRes = {
|
|
3752
|
+
success: true,
|
|
3753
|
+
summary: {
|
|
3754
|
+
total: targetsMap.size,
|
|
3755
|
+
willUpdate,
|
|
3756
|
+
willCreate: 0,
|
|
3757
|
+
unchanged,
|
|
3758
|
+
skipped: 0
|
|
3759
|
+
},
|
|
3760
|
+
diffs
|
|
3761
|
+
};
|
|
3762
|
+
} else {
|
|
3763
|
+
const preview = await this.previewBatch({
|
|
3764
|
+
attribute: batch.payload.attribute,
|
|
3765
|
+
value: batch.payload.value,
|
|
3766
|
+
selectedEntries: batch.payload.selectedEntries,
|
|
3767
|
+
selectedCollections: batch.payload.selectedCollections,
|
|
3768
|
+
isRegex: batch.payload.isRegex,
|
|
3769
|
+
regexPattern: batch.payload.regexPattern
|
|
3770
|
+
});
|
|
3771
|
+
previewRes = {
|
|
3772
|
+
success: true,
|
|
3773
|
+
summary: {
|
|
3774
|
+
total: preview.summary?.total ?? 0,
|
|
3775
|
+
willUpdate: preview.summary?.willUpdate ?? 0,
|
|
3776
|
+
willCreate: 0,
|
|
3777
|
+
unchanged: preview.summary?.unchanged ?? 0,
|
|
3778
|
+
skipped: preview.summary?.skipped ?? 0
|
|
3779
|
+
},
|
|
3780
|
+
diffs: (preview.diffs || []).map((d) => ({
|
|
3781
|
+
...d,
|
|
3782
|
+
action: "update"
|
|
3783
|
+
}))
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
return {
|
|
3787
|
+
success: true,
|
|
3788
|
+
batchId: batch.id,
|
|
3789
|
+
createdAt: batch.createdAt,
|
|
3790
|
+
createdBy: batch.createdBy,
|
|
3791
|
+
makerNotes: batch.makerNotes,
|
|
3792
|
+
batchSummary: batch.summary,
|
|
3793
|
+
payload: {
|
|
3794
|
+
action: batch.payload?.action || "bulk_update",
|
|
3795
|
+
attribute: batch.payload?.attribute,
|
|
3796
|
+
value: batch.payload?.value,
|
|
3797
|
+
publishOnApproval: batch.summary?.publishOnApproval,
|
|
3798
|
+
publishStrategy: batch.summary?.publishStrategy
|
|
3799
|
+
},
|
|
3800
|
+
summary: previewRes.summary,
|
|
3801
|
+
diffs: previewRes.diffs
|
|
3802
|
+
};
|
|
3803
|
+
},
|
|
3804
|
+
/**
|
|
3805
|
+
* Export batch diffs to CSV for offline audit review
|
|
3806
|
+
*/
|
|
3807
|
+
async getPendingBatchDiffsCsv(batchId) {
|
|
3808
|
+
const data = await this.getPendingBatchDiffs(batchId);
|
|
3809
|
+
function escapeVal(val) {
|
|
3810
|
+
if (val === null || val === void 0) return "";
|
|
3811
|
+
let str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
3812
|
+
str = str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3813
|
+
if (/^[=+@-]$/.test(str.charAt(0))) {
|
|
3814
|
+
str = `'${str}`;
|
|
3815
|
+
}
|
|
3816
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
3817
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
3818
|
+
}
|
|
3819
|
+
return str;
|
|
3820
|
+
}
|
|
3821
|
+
const headers = [
|
|
3822
|
+
"Batch ID",
|
|
3823
|
+
"Action",
|
|
3824
|
+
"Collection UID",
|
|
3825
|
+
"Collection Name",
|
|
3826
|
+
"Document ID",
|
|
3827
|
+
"Record Identifier",
|
|
3828
|
+
"Attribute Name",
|
|
3829
|
+
"Original Value (Before)",
|
|
3830
|
+
"Proposed Value (After)",
|
|
3831
|
+
"Change Status",
|
|
3832
|
+
"Details / Message"
|
|
3833
|
+
];
|
|
3834
|
+
const rows = [headers.join(",")];
|
|
3835
|
+
for (const diff of data.diffs || []) {
|
|
3836
|
+
const row = [
|
|
3837
|
+
escapeVal(data.batchId),
|
|
3838
|
+
escapeVal(diff.action || "update"),
|
|
3839
|
+
escapeVal(diff.uid),
|
|
3840
|
+
escapeVal(diff.collectionName),
|
|
3841
|
+
escapeVal(diff.documentId),
|
|
3842
|
+
escapeVal(diff.label),
|
|
3843
|
+
escapeVal(diff.attribute),
|
|
3844
|
+
escapeVal(diff.oldDisplay ?? diff.oldValue),
|
|
3845
|
+
escapeVal(diff.newDisplay ?? diff.newValue),
|
|
3846
|
+
escapeVal(
|
|
3847
|
+
diff.status === "will_update" ? "Will Update" : diff.status === "will_create" ? "Will Create" : diff.status === "unchanged" ? "Unchanged" : "Skipped"
|
|
3848
|
+
),
|
|
3849
|
+
escapeVal(diff.message)
|
|
3850
|
+
];
|
|
3851
|
+
rows.push(row.join(","));
|
|
3852
|
+
}
|
|
3853
|
+
const safeBatchId = batchId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3854
|
+
const filename = `pending_batch_review_${safeBatchId}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3855
|
+
return {
|
|
3856
|
+
success: true,
|
|
3857
|
+
filename,
|
|
3858
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
3859
|
+
count: data.diffs?.length || 0
|
|
3860
|
+
};
|
|
3861
|
+
},
|
|
3622
3862
|
/**
|
|
3623
3863
|
* Get recent batch jobs for the processing dashboard
|
|
3624
3864
|
*/
|
|
@@ -4037,42 +4277,74 @@ const auditService = ({ strapi }) => ({
|
|
|
4037
4277
|
for (const item of diffs) {
|
|
4038
4278
|
if (item.status === "updated" && item.uid && item.documentId && item.attribute) {
|
|
4039
4279
|
try {
|
|
4280
|
+
const ct = strapi.contentTypes[item.uid];
|
|
4281
|
+
if (!ct) {
|
|
4282
|
+
throw new Error(`Content type "${item.uid}" does not exist in schema.`);
|
|
4283
|
+
}
|
|
4040
4284
|
const isRel = item.oldValue && typeof item.oldValue === "object" && item.oldValue.isRelation;
|
|
4041
4285
|
let rollbackPayload;
|
|
4042
4286
|
if (isRel) {
|
|
4043
4287
|
const oldDocIds = Array.isArray(item.oldValue.documentIds) ? item.oldValue.documentIds : [];
|
|
4044
4288
|
rollbackPayload = { set: oldDocIds };
|
|
4045
4289
|
} else {
|
|
4046
|
-
rollbackPayload = item.oldValue;
|
|
4290
|
+
rollbackPayload = item.oldValue ?? null;
|
|
4047
4291
|
}
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4292
|
+
const parsed = parseDynamicZonePath(item.attribute);
|
|
4293
|
+
const rootKey = parsed.rootKey;
|
|
4294
|
+
const rootAttrDef = ct.attributes?.[rootKey];
|
|
4295
|
+
const isComponent = rootAttrDef?.type === "component";
|
|
4296
|
+
const isDynamicZone = rootAttrDef?.type === "dynamiczone";
|
|
4297
|
+
const isNested = item.attribute.includes(".") || item.attribute.includes("[") || isComponent || isDynamicZone || isDeepComponentField(ct, strapi.components, item.attribute);
|
|
4298
|
+
let wasPublished = false;
|
|
4299
|
+
if (isNested && (isComponent || isDynamicZone || rootAttrDef)) {
|
|
4300
|
+
const populateOption = buildDeepPopulate$1(ct.attributes, strapi.components);
|
|
4301
|
+
const findOneArgs = { documentId: item.documentId };
|
|
4302
|
+
if (populateOption) {
|
|
4303
|
+
findOneArgs.populate = populateOption;
|
|
4304
|
+
}
|
|
4305
|
+
const currentDoc = await strapi.documents(item.uid).findOne(findOneArgs);
|
|
4306
|
+
if (!currentDoc) {
|
|
4307
|
+
throw new Error(`Document "${item.documentId}" not found in collection "${item.uid}".`);
|
|
4308
|
+
}
|
|
4309
|
+
wasPublished = Boolean(currentDoc?.publishedAt);
|
|
4310
|
+
const isRootDz = isDynamicZone;
|
|
4311
|
+
const existingRoot = currentDoc?.[rootKey] || (isRootDz ? [] : {});
|
|
4312
|
+
let restoredRoot;
|
|
4313
|
+
if (Array.isArray(item.dynamicZoneDiffs) && item.dynamicZoneDiffs.length > 0) {
|
|
4314
|
+
let workingRoot = existingRoot;
|
|
4315
|
+
for (const dzDiff of item.dynamicZoneDiffs) {
|
|
4316
|
+
const parsedDz = parseDynamicZonePath(dzDiff.attribute || item.attribute);
|
|
4317
|
+
const dzParts = parsedDz.cleanPath.split(".").slice(1);
|
|
4318
|
+
const targetIndexes = dzDiff.index !== void 0 ? [dzDiff.index] : parsedDz.targetIndex !== void 0 ? [parsedDz.targetIndex] : void 0;
|
|
4319
|
+
workingRoot = setDeepProperty(workingRoot, dzParts, dzDiff.oldValue ?? null, {
|
|
4320
|
+
targetIndexes,
|
|
4321
|
+
targetComponentUid: dzDiff.componentUid || parsedDz.targetComponentUid,
|
|
4322
|
+
strapi
|
|
4323
|
+
});
|
|
4324
|
+
}
|
|
4325
|
+
restoredRoot = workingRoot;
|
|
4067
4326
|
} else {
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4327
|
+
const parts = parsed.cleanPath.split(".");
|
|
4328
|
+
const pathParts = parts.slice(1);
|
|
4329
|
+
const targetIndexes = parsed.targetIndex !== void 0 ? [parsed.targetIndex] : void 0;
|
|
4330
|
+
const targetComponentUid = parsed.targetComponentUid;
|
|
4331
|
+
restoredRoot = setDeepProperty(existingRoot, pathParts, rollbackPayload, {
|
|
4332
|
+
targetIndexes,
|
|
4333
|
+
targetComponentUid,
|
|
4334
|
+
strapi
|
|
4073
4335
|
});
|
|
4074
4336
|
}
|
|
4337
|
+
await strapi.documents(item.uid).update({
|
|
4338
|
+
documentId: item.documentId,
|
|
4339
|
+
data: {
|
|
4340
|
+
[rootKey]: restoredRoot
|
|
4341
|
+
}
|
|
4342
|
+
});
|
|
4075
4343
|
} else {
|
|
4344
|
+
const currentDoc = await strapi.documents(item.uid).findOne({
|
|
4345
|
+
documentId: item.documentId
|
|
4346
|
+
});
|
|
4347
|
+
wasPublished = Boolean(currentDoc?.publishedAt);
|
|
4076
4348
|
await strapi.documents(item.uid).update({
|
|
4077
4349
|
documentId: item.documentId,
|
|
4078
4350
|
data: {
|
|
@@ -4080,13 +4352,13 @@ const auditService = ({ strapi }) => ({
|
|
|
4080
4352
|
}
|
|
4081
4353
|
});
|
|
4082
4354
|
}
|
|
4083
|
-
|
|
4084
|
-
if (ct?.options?.draftAndPublish) {
|
|
4355
|
+
if (ct?.options?.draftAndPublish && wasPublished) {
|
|
4085
4356
|
try {
|
|
4086
4357
|
await strapi.documents(item.uid).publish({
|
|
4087
4358
|
documentId: item.documentId
|
|
4088
4359
|
});
|
|
4089
|
-
} catch {
|
|
4360
|
+
} catch (publishErr) {
|
|
4361
|
+
strapi.log.warn(`[bulk-updater] Failed to auto-publish rollback for ${item.uid}/${item.documentId}:`, publishErr?.message);
|
|
4090
4362
|
}
|
|
4091
4363
|
}
|
|
4092
4364
|
try {
|
|
@@ -4094,7 +4366,7 @@ const auditService = ({ strapi }) => ({
|
|
|
4094
4366
|
uid: item.uid,
|
|
4095
4367
|
documentId: item.documentId,
|
|
4096
4368
|
userId,
|
|
4097
|
-
statusOverride: ct?.options?.draftAndPublish ? "published" : void 0
|
|
4369
|
+
statusOverride: ct?.options?.draftAndPublish ? wasPublished ? "published" : "draft" : void 0
|
|
4098
4370
|
});
|
|
4099
4371
|
} catch {
|
|
4100
4372
|
}
|
|
@@ -5007,230 +5279,246 @@ function deepMergeComponent(existingVal, newVal) {
|
|
|
5007
5279
|
}
|
|
5008
5280
|
return merged;
|
|
5009
5281
|
}
|
|
5010
|
-
|
|
5011
|
-
const
|
|
5012
|
-
const
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
} catch {
|
|
5037
|
-
}
|
|
5038
|
-
}
|
|
5039
|
-
const payload = {};
|
|
5040
|
-
for (const [key, rawVal] of Object.entries(row)) {
|
|
5041
|
-
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
5042
|
-
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5043
|
-
if (key.includes(".")) {
|
|
5044
|
-
const parts = key.split(".");
|
|
5045
|
-
let currentAttrs = attrs;
|
|
5046
|
-
let currentTarget = payload;
|
|
5047
|
-
for (let pIdx = 0; pIdx < parts.length; pIdx++) {
|
|
5048
|
-
const part = parts[pIdx];
|
|
5049
|
-
const isLeaf = pIdx === parts.length - 1;
|
|
5050
|
-
if (isLeaf) {
|
|
5051
|
-
let leafDef = currentAttrs[part];
|
|
5052
|
-
if (!leafDef) {
|
|
5053
|
-
const match = Object.keys(currentAttrs).find(
|
|
5054
|
-
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5055
|
-
);
|
|
5056
|
-
if (match) leafDef = currentAttrs[match];
|
|
5282
|
+
function parseRowToPayload(strapi, attrs, row) {
|
|
5283
|
+
const payload = {};
|
|
5284
|
+
for (const [key, rawVal] of Object.entries(row)) {
|
|
5285
|
+
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
5286
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5287
|
+
if (key.includes(".")) {
|
|
5288
|
+
const parts = key.split(".");
|
|
5289
|
+
let currentAttrs = attrs;
|
|
5290
|
+
let currentTarget = payload;
|
|
5291
|
+
for (let pIdx = 0; pIdx < parts.length; pIdx++) {
|
|
5292
|
+
const part = parts[pIdx];
|
|
5293
|
+
const isLeaf = pIdx === parts.length - 1;
|
|
5294
|
+
if (isLeaf) {
|
|
5295
|
+
let leafDef = currentAttrs[part];
|
|
5296
|
+
if (!leafDef) {
|
|
5297
|
+
const match = Object.keys(currentAttrs).find(
|
|
5298
|
+
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5299
|
+
);
|
|
5300
|
+
if (match) leafDef = currentAttrs[match];
|
|
5301
|
+
}
|
|
5302
|
+
if (!leafDef) {
|
|
5303
|
+
break;
|
|
5304
|
+
}
|
|
5305
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
5306
|
+
if (leafDef.type !== "dynamiczone" && !(leafDef.type === "component" && leafDef.repeatable) && leafDef.type !== "relation") {
|
|
5307
|
+
currentTarget[part] = null;
|
|
5057
5308
|
}
|
|
5058
|
-
|
|
5059
|
-
|
|
5309
|
+
} else if (leafDef.type === "integer" || leafDef.type === "biginteger") {
|
|
5310
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
5311
|
+
currentTarget[part] = isNaN(parsedInt) ? null : parsedInt;
|
|
5312
|
+
} else if (leafDef.type === "float" || leafDef.type === "decimal") {
|
|
5313
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5314
|
+
currentTarget[part] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
5315
|
+
} else if (leafDef.type === "boolean") {
|
|
5316
|
+
currentTarget[part] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
5317
|
+
} else if (leafDef.type === "json") {
|
|
5318
|
+
try {
|
|
5319
|
+
currentTarget[part] = JSON.parse(rawVal);
|
|
5320
|
+
} catch {
|
|
5321
|
+
currentTarget[part] = rawVal;
|
|
5060
5322
|
}
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5323
|
+
} else if (leafDef.type === "component" && leafDef.repeatable) {
|
|
5324
|
+
try {
|
|
5325
|
+
const parsed = JSON.parse(rawVal);
|
|
5326
|
+
if (Array.isArray(parsed)) {
|
|
5327
|
+
currentTarget[part] = parsed;
|
|
5064
5328
|
}
|
|
5065
|
-
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
const
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
} else if (leafDef.type === "json") {
|
|
5074
|
-
try {
|
|
5075
|
-
currentTarget[part] = JSON.parse(rawVal);
|
|
5076
|
-
} catch {
|
|
5329
|
+
} catch {
|
|
5330
|
+
}
|
|
5331
|
+
} else if (leafDef.type === "relation") {
|
|
5332
|
+
try {
|
|
5333
|
+
const parsed = JSON.parse(rawVal);
|
|
5334
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5335
|
+
currentTarget[part] = parsed;
|
|
5336
|
+
} else if (isValidDocId(rawVal)) {
|
|
5077
5337
|
currentTarget[part] = rawVal;
|
|
5078
5338
|
}
|
|
5079
|
-
}
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
if (Array.isArray(parsed)) {
|
|
5083
|
-
currentTarget[part] = parsed;
|
|
5084
|
-
}
|
|
5085
|
-
} catch {
|
|
5086
|
-
}
|
|
5087
|
-
} else if (leafDef.type === "relation") {
|
|
5088
|
-
try {
|
|
5089
|
-
const parsed = JSON.parse(rawVal);
|
|
5090
|
-
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5091
|
-
currentTarget[part] = parsed;
|
|
5092
|
-
} else if (isValidDocId(rawVal)) {
|
|
5093
|
-
currentTarget[part] = rawVal;
|
|
5094
|
-
}
|
|
5095
|
-
} catch {
|
|
5096
|
-
if (isValidDocId(rawVal)) {
|
|
5097
|
-
currentTarget[part] = rawVal;
|
|
5098
|
-
}
|
|
5339
|
+
} catch {
|
|
5340
|
+
if (isValidDocId(rawVal)) {
|
|
5341
|
+
currentTarget[part] = rawVal;
|
|
5099
5342
|
}
|
|
5100
|
-
} else {
|
|
5101
|
-
currentTarget[part] = rawVal;
|
|
5102
5343
|
}
|
|
5103
5344
|
} else {
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
currentTarget[part] = {};
|
|
5118
|
-
}
|
|
5119
|
-
currentTarget = currentTarget[part];
|
|
5120
|
-
currentAttrs = comp.attributes;
|
|
5121
|
-
} else {
|
|
5345
|
+
currentTarget[part] = rawVal;
|
|
5346
|
+
}
|
|
5347
|
+
} else {
|
|
5348
|
+
let compDef = currentAttrs[part];
|
|
5349
|
+
if (!compDef) {
|
|
5350
|
+
const match = Object.keys(currentAttrs).find(
|
|
5351
|
+
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5352
|
+
);
|
|
5353
|
+
if (match) compDef = currentAttrs[match];
|
|
5354
|
+
}
|
|
5355
|
+
if (compDef?.type === "component" && !compDef.repeatable && compDef.component) {
|
|
5356
|
+
const comp = strapi.components[compDef.component];
|
|
5357
|
+
if (!comp?.attributes) {
|
|
5122
5358
|
break;
|
|
5123
5359
|
}
|
|
5360
|
+
if (!currentTarget[part] || typeof currentTarget[part] !== "object" || Array.isArray(currentTarget[part])) {
|
|
5361
|
+
currentTarget[part] = {};
|
|
5362
|
+
}
|
|
5363
|
+
currentTarget = currentTarget[part];
|
|
5364
|
+
currentAttrs = comp.attributes;
|
|
5365
|
+
} else {
|
|
5366
|
+
break;
|
|
5124
5367
|
}
|
|
5125
5368
|
}
|
|
5126
|
-
continue;
|
|
5127
5369
|
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5370
|
+
continue;
|
|
5371
|
+
}
|
|
5372
|
+
let attrDef = attrs[key];
|
|
5373
|
+
if (!attrDef) {
|
|
5374
|
+
const matchKey = Object.keys(attrs).find(
|
|
5375
|
+
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
5376
|
+
);
|
|
5377
|
+
if (matchKey) {
|
|
5378
|
+
attrDef = attrs[matchKey];
|
|
5136
5379
|
}
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
continue;
|
|
5380
|
+
}
|
|
5381
|
+
if (!attrDef) continue;
|
|
5382
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
5383
|
+
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
5384
|
+
payload[key] = null;
|
|
5143
5385
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5388
|
+
switch (attrDef.type) {
|
|
5389
|
+
case "integer":
|
|
5390
|
+
case "biginteger": {
|
|
5391
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
5392
|
+
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
5393
|
+
break;
|
|
5394
|
+
}
|
|
5395
|
+
case "float":
|
|
5396
|
+
case "decimal": {
|
|
5397
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5398
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
5399
|
+
break;
|
|
5400
|
+
}
|
|
5401
|
+
case "boolean":
|
|
5402
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
5403
|
+
break;
|
|
5404
|
+
case "json":
|
|
5405
|
+
try {
|
|
5406
|
+
payload[key] = JSON.parse(rawVal);
|
|
5407
|
+
} catch {
|
|
5408
|
+
payload[key] = rawVal;
|
|
5150
5409
|
}
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5410
|
+
break;
|
|
5411
|
+
case "dynamiczone": {
|
|
5412
|
+
try {
|
|
5413
|
+
const parsed = JSON.parse(rawVal);
|
|
5414
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
5415
|
+
payload[key] = parsed;
|
|
5416
|
+
}
|
|
5417
|
+
} catch {
|
|
5156
5418
|
}
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5419
|
+
break;
|
|
5420
|
+
}
|
|
5421
|
+
case "component": {
|
|
5422
|
+
if (attrDef.repeatable) {
|
|
5161
5423
|
try {
|
|
5162
|
-
|
|
5424
|
+
const parsed = JSON.parse(rawVal);
|
|
5425
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
5426
|
+
payload[key] = parsed;
|
|
5427
|
+
}
|
|
5163
5428
|
} catch {
|
|
5164
|
-
payload[key] = rawVal;
|
|
5165
5429
|
}
|
|
5166
|
-
|
|
5167
|
-
case "dynamiczone": {
|
|
5430
|
+
} else {
|
|
5168
5431
|
try {
|
|
5169
5432
|
const parsed = JSON.parse(rawVal);
|
|
5170
|
-
if (
|
|
5433
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5171
5434
|
payload[key] = parsed;
|
|
5172
5435
|
}
|
|
5173
5436
|
} catch {
|
|
5174
5437
|
}
|
|
5175
|
-
break;
|
|
5176
5438
|
}
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
}
|
|
5186
|
-
} else {
|
|
5187
|
-
try {
|
|
5188
|
-
const parsed = JSON.parse(rawVal);
|
|
5189
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5190
|
-
payload[key] = parsed;
|
|
5191
|
-
}
|
|
5192
|
-
} catch {
|
|
5193
|
-
}
|
|
5439
|
+
break;
|
|
5440
|
+
}
|
|
5441
|
+
case "relation": {
|
|
5442
|
+
try {
|
|
5443
|
+
const parsed = JSON.parse(rawVal);
|
|
5444
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5445
|
+
payload[key] = parsed;
|
|
5446
|
+
break;
|
|
5194
5447
|
}
|
|
5195
|
-
|
|
5448
|
+
} catch {
|
|
5196
5449
|
}
|
|
5197
|
-
|
|
5450
|
+
if (isValidDocId(rawVal)) {
|
|
5451
|
+
payload[key] = rawVal;
|
|
5452
|
+
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5453
|
+
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5454
|
+
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5455
|
+
payload[key] = parts;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
break;
|
|
5459
|
+
}
|
|
5460
|
+
case "media": {
|
|
5461
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
5462
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
5463
|
+
} else {
|
|
5198
5464
|
try {
|
|
5199
5465
|
const parsed = JSON.parse(rawVal);
|
|
5200
|
-
if (
|
|
5466
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
5201
5467
|
payload[key] = parsed;
|
|
5202
|
-
break;
|
|
5203
5468
|
}
|
|
5204
5469
|
} catch {
|
|
5205
5470
|
}
|
|
5206
|
-
if (isValidDocId(rawVal)) {
|
|
5207
|
-
payload[key] = rawVal;
|
|
5208
|
-
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5209
|
-
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5210
|
-
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5211
|
-
payload[key] = parts;
|
|
5212
|
-
}
|
|
5213
|
-
}
|
|
5214
|
-
break;
|
|
5215
5471
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5472
|
+
break;
|
|
5473
|
+
}
|
|
5474
|
+
default:
|
|
5475
|
+
payload[key] = rawVal;
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
return payload;
|
|
5479
|
+
}
|
|
5480
|
+
function formatValueDisplay(val) {
|
|
5481
|
+
if (val === null || val === void 0) return "(empty)";
|
|
5482
|
+
if (typeof val === "object") {
|
|
5483
|
+
try {
|
|
5484
|
+
return JSON.stringify(val);
|
|
5485
|
+
} catch {
|
|
5486
|
+
return String(val);
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
const s = String(val).trim();
|
|
5490
|
+
return s === "" ? "(empty)" : s;
|
|
5491
|
+
}
|
|
5492
|
+
async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
5493
|
+
const attrs = ct.attributes || {};
|
|
5494
|
+
const { mode = "auto", publish = true, publishStrategy = "safe", user } = options;
|
|
5495
|
+
const isDraftAndPublish = Boolean(ct.options?.draftAndPublish);
|
|
5496
|
+
const stats = {
|
|
5497
|
+
total: rows.length,
|
|
5498
|
+
created: 0,
|
|
5499
|
+
updated: 0,
|
|
5500
|
+
skipped: 0,
|
|
5501
|
+
newWithoutDocId: 0,
|
|
5502
|
+
errors: [],
|
|
5503
|
+
diffs: []
|
|
5504
|
+
};
|
|
5505
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5506
|
+
const rowNumber = startRowNumber + i;
|
|
5507
|
+
const row = rows[i];
|
|
5508
|
+
let docId = extractDocId(row);
|
|
5509
|
+
const rowLocale = extractLocale(row);
|
|
5510
|
+
if (ct.kind === "singleType" && !docId) {
|
|
5511
|
+
try {
|
|
5512
|
+
const findSingleArgs = {};
|
|
5513
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
5514
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
5515
|
+
if (singleDoc?.documentId) {
|
|
5516
|
+
docId = singleDoc.documentId;
|
|
5229
5517
|
}
|
|
5230
|
-
|
|
5231
|
-
payload[key] = rawVal;
|
|
5518
|
+
} catch {
|
|
5232
5519
|
}
|
|
5233
5520
|
}
|
|
5521
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
5234
5522
|
try {
|
|
5235
5523
|
if (mode === "update") {
|
|
5236
5524
|
if (!docId) {
|
|
@@ -5280,7 +5568,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5280
5568
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5281
5569
|
await strapi.documents(uid).update(updateArgs);
|
|
5282
5570
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5283
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5571
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5284
5572
|
if (shouldPublish) {
|
|
5285
5573
|
try {
|
|
5286
5574
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5335,7 +5623,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5335
5623
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5336
5624
|
await strapi.documents(uid).update(updateArgs);
|
|
5337
5625
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5338
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5626
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5339
5627
|
if (shouldPublish) {
|
|
5340
5628
|
try {
|
|
5341
5629
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5376,7 +5664,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5376
5664
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5377
5665
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5378
5666
|
const newDocId = created?.documentId || docId;
|
|
5379
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5667
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5380
5668
|
if (shouldPublishNew && newDocId) {
|
|
5381
5669
|
try {
|
|
5382
5670
|
const pubArgs = { documentId: newDocId };
|
|
@@ -5432,7 +5720,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5432
5720
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5433
5721
|
await strapi.documents(uid).update(updateArgs);
|
|
5434
5722
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5435
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5723
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5436
5724
|
if (shouldPublish) {
|
|
5437
5725
|
try {
|
|
5438
5726
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5473,7 +5761,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5473
5761
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5474
5762
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5475
5763
|
const newDocId = created?.documentId || docId;
|
|
5476
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5764
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5477
5765
|
if (shouldPublishNew && newDocId) {
|
|
5478
5766
|
try {
|
|
5479
5767
|
const pubArgs = { documentId: newDocId };
|
|
@@ -6217,6 +6505,174 @@ const csvService = ({ strapi }) => ({
|
|
|
6217
6505
|
totalWarningCount: warnings.length
|
|
6218
6506
|
};
|
|
6219
6507
|
},
|
|
6508
|
+
/**
|
|
6509
|
+
* Previews CSV batch changes item-by-item without mutating the database.
|
|
6510
|
+
* Compares proposed CSV rows against existing documents to compute exact diffs for Maker-Checker review.
|
|
6511
|
+
*/
|
|
6512
|
+
async previewCsvBatch(uid, rows, options = {}) {
|
|
6513
|
+
const ct = strapi.contentTypes[uid];
|
|
6514
|
+
if (!ct) {
|
|
6515
|
+
throw new Error(`Content type "${uid}" not found`);
|
|
6516
|
+
}
|
|
6517
|
+
const attrs = ct.attributes || {};
|
|
6518
|
+
const { mode = "auto" } = options;
|
|
6519
|
+
const diffs = [];
|
|
6520
|
+
let willUpdateCount = 0;
|
|
6521
|
+
let willCreateCount = 0;
|
|
6522
|
+
let unchangedCount = 0;
|
|
6523
|
+
let skippedCount = 0;
|
|
6524
|
+
for (let i = 0; i < rows.length; i++) {
|
|
6525
|
+
const rowNumber = i + 2;
|
|
6526
|
+
const row = rows[i];
|
|
6527
|
+
let docId = extractDocId(row);
|
|
6528
|
+
const rowLocale = extractLocale(row);
|
|
6529
|
+
if (ct.kind === "singleType" && !docId) {
|
|
6530
|
+
try {
|
|
6531
|
+
const findSingleArgs = {};
|
|
6532
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
6533
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
6534
|
+
if (singleDoc?.documentId) {
|
|
6535
|
+
docId = singleDoc.documentId;
|
|
6536
|
+
}
|
|
6537
|
+
} catch {
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
let existing = null;
|
|
6541
|
+
if (docId) {
|
|
6542
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
6543
|
+
}
|
|
6544
|
+
if (mode === "update" && (!docId || !existing)) {
|
|
6545
|
+
skippedCount++;
|
|
6546
|
+
diffs.push({
|
|
6547
|
+
uid,
|
|
6548
|
+
collectionName: ct.info?.displayName || uid,
|
|
6549
|
+
documentId: docId || `(Row #${rowNumber})`,
|
|
6550
|
+
label: `Row #${rowNumber}`,
|
|
6551
|
+
attribute: "(record)",
|
|
6552
|
+
oldValue: null,
|
|
6553
|
+
newValue: null,
|
|
6554
|
+
oldDisplay: "—",
|
|
6555
|
+
newDisplay: "—",
|
|
6556
|
+
hasChange: false,
|
|
6557
|
+
status: "will_skip",
|
|
6558
|
+
action: "skip",
|
|
6559
|
+
message: !docId ? "Missing documentId in update mode" : `Document "${docId}" not found in database`
|
|
6560
|
+
});
|
|
6561
|
+
continue;
|
|
6562
|
+
}
|
|
6563
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
6564
|
+
if (existing) {
|
|
6565
|
+
const resolvedDocId = existing.documentId || docId;
|
|
6566
|
+
const entryLabel = existing.title || existing.name || existing.slug || `Entry #${resolvedDocId}`;
|
|
6567
|
+
let rowHasChanges = false;
|
|
6568
|
+
const payloadKeys = Object.keys(payload);
|
|
6569
|
+
if (payloadKeys.length === 0) {
|
|
6570
|
+
unchangedCount++;
|
|
6571
|
+
diffs.push({
|
|
6572
|
+
uid,
|
|
6573
|
+
collectionName: ct.info?.displayName || uid,
|
|
6574
|
+
documentId: resolvedDocId,
|
|
6575
|
+
label: entryLabel,
|
|
6576
|
+
attribute: "(no fields)",
|
|
6577
|
+
oldValue: null,
|
|
6578
|
+
newValue: null,
|
|
6579
|
+
oldDisplay: "—",
|
|
6580
|
+
newDisplay: "—",
|
|
6581
|
+
hasChange: false,
|
|
6582
|
+
status: "unchanged",
|
|
6583
|
+
action: "update",
|
|
6584
|
+
message: "No modified attributes in CSV row"
|
|
6585
|
+
});
|
|
6586
|
+
} else {
|
|
6587
|
+
for (const attrKey of payloadKeys) {
|
|
6588
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
6589
|
+
let newVal = payload[attrKey];
|
|
6590
|
+
if (attrs[attrKey]?.type === "component" && typeof newVal === "object" && newVal !== null) {
|
|
6591
|
+
newVal = deepMergeComponent(existing[attrKey], newVal);
|
|
6592
|
+
}
|
|
6593
|
+
const oldDisplay = formatValueDisplay(oldVal);
|
|
6594
|
+
const newDisplay = formatValueDisplay(newVal);
|
|
6595
|
+
const hasChange = String(oldVal ?? "") !== String(newVal ?? "");
|
|
6596
|
+
if (hasChange) {
|
|
6597
|
+
rowHasChanges = true;
|
|
6598
|
+
}
|
|
6599
|
+
diffs.push({
|
|
6600
|
+
uid,
|
|
6601
|
+
collectionName: ct.info?.displayName || uid,
|
|
6602
|
+
documentId: resolvedDocId,
|
|
6603
|
+
label: entryLabel,
|
|
6604
|
+
attribute: attrKey,
|
|
6605
|
+
oldValue: oldVal,
|
|
6606
|
+
newValue: newVal,
|
|
6607
|
+
oldDisplay,
|
|
6608
|
+
newDisplay,
|
|
6609
|
+
hasChange,
|
|
6610
|
+
status: hasChange ? "will_update" : "unchanged",
|
|
6611
|
+
action: "update",
|
|
6612
|
+
message: hasChange ? `Will update "${attrKey}" from CSV` : `Already matches CSV value`
|
|
6613
|
+
});
|
|
6614
|
+
}
|
|
6615
|
+
if (rowHasChanges) {
|
|
6616
|
+
willUpdateCount++;
|
|
6617
|
+
} else {
|
|
6618
|
+
unchangedCount++;
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
} else {
|
|
6622
|
+
willCreateCount++;
|
|
6623
|
+
const resolvedDocId = docId || `(Auto-generated)`;
|
|
6624
|
+
const entryLabel = payload.title || payload.name || payload.slug || `New Entry (Row #${rowNumber})`;
|
|
6625
|
+
const payloadKeys = Object.keys(payload);
|
|
6626
|
+
if (payloadKeys.length === 0) {
|
|
6627
|
+
diffs.push({
|
|
6628
|
+
uid,
|
|
6629
|
+
collectionName: ct.info?.displayName || uid,
|
|
6630
|
+
documentId: resolvedDocId,
|
|
6631
|
+
label: entryLabel,
|
|
6632
|
+
attribute: "(all fields)",
|
|
6633
|
+
oldValue: null,
|
|
6634
|
+
newValue: null,
|
|
6635
|
+
oldDisplay: "(new entry)",
|
|
6636
|
+
newDisplay: "(empty record)",
|
|
6637
|
+
hasChange: true,
|
|
6638
|
+
status: "will_create",
|
|
6639
|
+
action: "create",
|
|
6640
|
+
message: "Will create new record"
|
|
6641
|
+
});
|
|
6642
|
+
} else {
|
|
6643
|
+
for (const attrKey of payloadKeys) {
|
|
6644
|
+
const newVal = payload[attrKey];
|
|
6645
|
+
diffs.push({
|
|
6646
|
+
uid,
|
|
6647
|
+
collectionName: ct.info?.displayName || uid,
|
|
6648
|
+
documentId: resolvedDocId,
|
|
6649
|
+
label: entryLabel,
|
|
6650
|
+
attribute: attrKey,
|
|
6651
|
+
oldValue: null,
|
|
6652
|
+
newValue: newVal,
|
|
6653
|
+
oldDisplay: "(new entry)",
|
|
6654
|
+
newDisplay: formatValueDisplay(newVal),
|
|
6655
|
+
hasChange: true,
|
|
6656
|
+
status: "will_create",
|
|
6657
|
+
action: "create",
|
|
6658
|
+
message: `Will create new record with attribute "${attrKey}"`
|
|
6659
|
+
});
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
return {
|
|
6665
|
+
success: true,
|
|
6666
|
+
summary: {
|
|
6667
|
+
total: rows.length,
|
|
6668
|
+
willUpdate: willUpdateCount,
|
|
6669
|
+
willCreate: willCreateCount,
|
|
6670
|
+
unchanged: unchangedCount,
|
|
6671
|
+
skipped: skippedCount
|
|
6672
|
+
},
|
|
6673
|
+
diffs
|
|
6674
|
+
};
|
|
6675
|
+
},
|
|
6220
6676
|
/**
|
|
6221
6677
|
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
6222
6678
|
*/
|