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