strapi-plugin-bulk-updater 1.4.10 → 1.5.0
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-By9Dd_YF.mjs} +659 -80
- package/dist/_chunks/BulkUpdater-By9Dd_YF.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-C0uPoso2.js → BulkUpdater-CSmtXjbM.js} +658 -79
- package/dist/_chunks/BulkUpdater-CSmtXjbM.js.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +619 -196
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +619 -196
- 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");
|
|
@@ -2374,7 +2412,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2374
2412
|
} else if (normOp === "endswith") {
|
|
2375
2413
|
orConditions.push({ [k]: { $endsWithi: q } });
|
|
2376
2414
|
} else if (normOp === "notequals") {
|
|
2377
|
-
orConditions.push({ [k]: { $
|
|
2415
|
+
orConditions.push({ [k]: { $nei: q } });
|
|
2378
2416
|
} else if (normOp === "notcontains") {
|
|
2379
2417
|
orConditions.push({ [k]: { $notContainsi: q } });
|
|
2380
2418
|
} else {
|
|
@@ -2392,12 +2430,21 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2392
2430
|
} else if (normOp === "notempty") {
|
|
2393
2431
|
condition = isStringLike ? { $and: [{ $notNull: true }, { $ne: "" }] } : { $notNull: true };
|
|
2394
2432
|
} else if (q !== "") {
|
|
2433
|
+
let typedVal = q;
|
|
2434
|
+
const isNumeric = ["integer", "biginteger", "float", "decimal", "number"].includes(targetAttrSchema?.type);
|
|
2435
|
+
const isBoolean = targetAttrSchema?.type === "boolean";
|
|
2436
|
+
if (isNumeric && /^-?\d+(\.\d+)?$/.test(q)) {
|
|
2437
|
+
typedVal = Number(q);
|
|
2438
|
+
} else if (isBoolean) {
|
|
2439
|
+
if (q.toLowerCase() === "true") typedVal = true;
|
|
2440
|
+
else if (q.toLowerCase() === "false") typedVal = false;
|
|
2441
|
+
}
|
|
2395
2442
|
switch (normOp) {
|
|
2396
2443
|
case "equals":
|
|
2397
|
-
condition = { $
|
|
2444
|
+
condition = isStringLike ? { $eqi: q } : { $eq: typedVal };
|
|
2398
2445
|
break;
|
|
2399
2446
|
case "notequals":
|
|
2400
|
-
condition = { $
|
|
2447
|
+
condition = isStringLike ? { $nei: q } : { $ne: typedVal };
|
|
2401
2448
|
break;
|
|
2402
2449
|
case "contains":
|
|
2403
2450
|
condition = { $containsi: q };
|
|
@@ -2406,10 +2453,10 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2406
2453
|
condition = { $notContainsi: q };
|
|
2407
2454
|
break;
|
|
2408
2455
|
case "startswith":
|
|
2409
|
-
condition = { $
|
|
2456
|
+
condition = { $startsWithi: q };
|
|
2410
2457
|
break;
|
|
2411
2458
|
case "endswith":
|
|
2412
|
-
condition = { $
|
|
2459
|
+
condition = { $endsWithi: q };
|
|
2413
2460
|
break;
|
|
2414
2461
|
}
|
|
2415
2462
|
}
|
|
@@ -2535,7 +2582,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2535
2582
|
if (typeof v === "object") {
|
|
2536
2583
|
return testSingleValue(v.name) || testSingleValue(v.title) || testSingleValue(v.email) || testSingleValue(v.username) || testSingleValue(v.documentId) || testSingleValue(v.id) || testSingleValue(v.slug);
|
|
2537
2584
|
}
|
|
2538
|
-
const valStr = String(v).toLowerCase();
|
|
2585
|
+
const valStr = String(v).trim().toLowerCase();
|
|
2539
2586
|
switch (normOp) {
|
|
2540
2587
|
case "equals":
|
|
2541
2588
|
return valStr === qVal;
|
|
@@ -3619,6 +3666,198 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3619
3666
|
batch
|
|
3620
3667
|
};
|
|
3621
3668
|
},
|
|
3669
|
+
/**
|
|
3670
|
+
* Inspect batch changes item-by-item for Maker-Checker 4-Eyes review
|
|
3671
|
+
*/
|
|
3672
|
+
async getPendingBatchDiffs(batchId) {
|
|
3673
|
+
if (!batchId) throw new Error("batchId is required");
|
|
3674
|
+
const batch = pendingBatches.get(batchId);
|
|
3675
|
+
if (!batch) {
|
|
3676
|
+
throw new Error(`Pending batch "${batchId}" not found.`);
|
|
3677
|
+
}
|
|
3678
|
+
let previewRes = null;
|
|
3679
|
+
if (batch.payload?.action === "csv_import" || batch.id.startsWith("csv_")) {
|
|
3680
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
3681
|
+
previewRes = await csvService2.previewCsvBatch(
|
|
3682
|
+
batch.payload.uid,
|
|
3683
|
+
batch.payload.rows || [],
|
|
3684
|
+
batch.payload.options || {}
|
|
3685
|
+
);
|
|
3686
|
+
} else if (batch.payload?.action === "publish") {
|
|
3687
|
+
const selectedEntries = batch.payload.selectedEntries || [];
|
|
3688
|
+
const selectedCollections = batch.payload.selectedCollections || [];
|
|
3689
|
+
const targetsMap = /* @__PURE__ */ new Map();
|
|
3690
|
+
for (const colUid of selectedCollections) {
|
|
3691
|
+
if (!strapi.contentTypes[colUid]) continue;
|
|
3692
|
+
const entries = await strapi.documents(colUid).findMany();
|
|
3693
|
+
for (const entry of entries || []) {
|
|
3694
|
+
targetsMap.set(`${colUid}@@@${entry.documentId}`, { uid: colUid, documentId: entry.documentId });
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
for (const item of selectedEntries) {
|
|
3698
|
+
if (!item.uid || !item.documentId) continue;
|
|
3699
|
+
targetsMap.set(`${item.uid}@@@${item.documentId}`, { uid: item.uid, documentId: item.documentId });
|
|
3700
|
+
}
|
|
3701
|
+
const diffs = [];
|
|
3702
|
+
let willUpdate = 0;
|
|
3703
|
+
let unchanged = 0;
|
|
3704
|
+
for (const target of targetsMap.values()) {
|
|
3705
|
+
const ct = strapi.contentTypes[target.uid];
|
|
3706
|
+
const displayName = ct?.info?.displayName || target.uid;
|
|
3707
|
+
try {
|
|
3708
|
+
const doc = await strapi.documents(target.uid).findOne({
|
|
3709
|
+
documentId: target.documentId
|
|
3710
|
+
});
|
|
3711
|
+
const label = doc ? doc.title || doc.name || doc.slug || doc.documentId : target.documentId;
|
|
3712
|
+
const isLive = Boolean(doc?.publishedAt) && doc?.status !== "modified";
|
|
3713
|
+
const currentStatus = isLive ? "published" : doc?.status === "modified" ? "modified draft" : "draft";
|
|
3714
|
+
const hasChange = !isLive;
|
|
3715
|
+
if (hasChange) willUpdate++;
|
|
3716
|
+
else unchanged++;
|
|
3717
|
+
diffs.push({
|
|
3718
|
+
uid: target.uid,
|
|
3719
|
+
collectionName: displayName,
|
|
3720
|
+
documentId: target.documentId,
|
|
3721
|
+
label,
|
|
3722
|
+
attribute: "status",
|
|
3723
|
+
oldValue: currentStatus,
|
|
3724
|
+
newValue: "published",
|
|
3725
|
+
oldDisplay: currentStatus,
|
|
3726
|
+
newDisplay: "published live",
|
|
3727
|
+
hasChange,
|
|
3728
|
+
status: hasChange ? "will_update" : "unchanged",
|
|
3729
|
+
action: "publish",
|
|
3730
|
+
message: hasChange ? "Record will be published live" : "Already published live"
|
|
3731
|
+
});
|
|
3732
|
+
} catch (err) {
|
|
3733
|
+
diffs.push({
|
|
3734
|
+
uid: target.uid,
|
|
3735
|
+
collectionName: displayName,
|
|
3736
|
+
documentId: target.documentId,
|
|
3737
|
+
label: target.documentId,
|
|
3738
|
+
attribute: "status",
|
|
3739
|
+
oldValue: null,
|
|
3740
|
+
newValue: null,
|
|
3741
|
+
oldDisplay: "—",
|
|
3742
|
+
newDisplay: "—",
|
|
3743
|
+
hasChange: false,
|
|
3744
|
+
status: "will_skip",
|
|
3745
|
+
action: "skip",
|
|
3746
|
+
message: `Error inspecting document: ${err.message}`
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
previewRes = {
|
|
3751
|
+
success: true,
|
|
3752
|
+
summary: {
|
|
3753
|
+
total: targetsMap.size,
|
|
3754
|
+
willUpdate,
|
|
3755
|
+
willCreate: 0,
|
|
3756
|
+
unchanged,
|
|
3757
|
+
skipped: 0
|
|
3758
|
+
},
|
|
3759
|
+
diffs
|
|
3760
|
+
};
|
|
3761
|
+
} else {
|
|
3762
|
+
const preview = await this.previewBatch({
|
|
3763
|
+
attribute: batch.payload.attribute,
|
|
3764
|
+
value: batch.payload.value,
|
|
3765
|
+
selectedEntries: batch.payload.selectedEntries,
|
|
3766
|
+
selectedCollections: batch.payload.selectedCollections,
|
|
3767
|
+
isRegex: batch.payload.isRegex,
|
|
3768
|
+
regexPattern: batch.payload.regexPattern
|
|
3769
|
+
});
|
|
3770
|
+
previewRes = {
|
|
3771
|
+
success: true,
|
|
3772
|
+
summary: {
|
|
3773
|
+
total: preview.summary?.total ?? 0,
|
|
3774
|
+
willUpdate: preview.summary?.willUpdate ?? 0,
|
|
3775
|
+
willCreate: 0,
|
|
3776
|
+
unchanged: preview.summary?.unchanged ?? 0,
|
|
3777
|
+
skipped: preview.summary?.skipped ?? 0
|
|
3778
|
+
},
|
|
3779
|
+
diffs: (preview.diffs || []).map((d) => ({
|
|
3780
|
+
...d,
|
|
3781
|
+
action: "update"
|
|
3782
|
+
}))
|
|
3783
|
+
};
|
|
3784
|
+
}
|
|
3785
|
+
return {
|
|
3786
|
+
success: true,
|
|
3787
|
+
batchId: batch.id,
|
|
3788
|
+
createdAt: batch.createdAt,
|
|
3789
|
+
createdBy: batch.createdBy,
|
|
3790
|
+
makerNotes: batch.makerNotes,
|
|
3791
|
+
batchSummary: batch.summary,
|
|
3792
|
+
payload: {
|
|
3793
|
+
action: batch.payload?.action || "bulk_update",
|
|
3794
|
+
attribute: batch.payload?.attribute,
|
|
3795
|
+
value: batch.payload?.value,
|
|
3796
|
+
publishOnApproval: batch.summary?.publishOnApproval,
|
|
3797
|
+
publishStrategy: batch.summary?.publishStrategy
|
|
3798
|
+
},
|
|
3799
|
+
summary: previewRes.summary,
|
|
3800
|
+
diffs: previewRes.diffs
|
|
3801
|
+
};
|
|
3802
|
+
},
|
|
3803
|
+
/**
|
|
3804
|
+
* Export batch diffs to CSV for offline audit review
|
|
3805
|
+
*/
|
|
3806
|
+
async getPendingBatchDiffsCsv(batchId) {
|
|
3807
|
+
const data = await this.getPendingBatchDiffs(batchId);
|
|
3808
|
+
function escapeVal(val) {
|
|
3809
|
+
if (val === null || val === void 0) return "";
|
|
3810
|
+
let str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
3811
|
+
str = str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3812
|
+
if (/^[=+@-]$/.test(str.charAt(0))) {
|
|
3813
|
+
str = `'${str}`;
|
|
3814
|
+
}
|
|
3815
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
3816
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
3817
|
+
}
|
|
3818
|
+
return str;
|
|
3819
|
+
}
|
|
3820
|
+
const headers = [
|
|
3821
|
+
"Batch ID",
|
|
3822
|
+
"Action",
|
|
3823
|
+
"Collection UID",
|
|
3824
|
+
"Collection Name",
|
|
3825
|
+
"Document ID",
|
|
3826
|
+
"Record Identifier",
|
|
3827
|
+
"Attribute Name",
|
|
3828
|
+
"Original Value (Before)",
|
|
3829
|
+
"Proposed Value (After)",
|
|
3830
|
+
"Change Status",
|
|
3831
|
+
"Details / Message"
|
|
3832
|
+
];
|
|
3833
|
+
const rows = [headers.join(",")];
|
|
3834
|
+
for (const diff of data.diffs || []) {
|
|
3835
|
+
const row = [
|
|
3836
|
+
escapeVal(data.batchId),
|
|
3837
|
+
escapeVal(diff.action || "update"),
|
|
3838
|
+
escapeVal(diff.uid),
|
|
3839
|
+
escapeVal(diff.collectionName),
|
|
3840
|
+
escapeVal(diff.documentId),
|
|
3841
|
+
escapeVal(diff.label),
|
|
3842
|
+
escapeVal(diff.attribute),
|
|
3843
|
+
escapeVal(diff.oldDisplay ?? diff.oldValue),
|
|
3844
|
+
escapeVal(diff.newDisplay ?? diff.newValue),
|
|
3845
|
+
escapeVal(
|
|
3846
|
+
diff.status === "will_update" ? "Will Update" : diff.status === "will_create" ? "Will Create" : diff.status === "unchanged" ? "Unchanged" : "Skipped"
|
|
3847
|
+
),
|
|
3848
|
+
escapeVal(diff.message)
|
|
3849
|
+
];
|
|
3850
|
+
rows.push(row.join(","));
|
|
3851
|
+
}
|
|
3852
|
+
const safeBatchId = batchId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3853
|
+
const filename = `pending_batch_review_${safeBatchId}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3854
|
+
return {
|
|
3855
|
+
success: true,
|
|
3856
|
+
filename,
|
|
3857
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
3858
|
+
count: data.diffs?.length || 0
|
|
3859
|
+
};
|
|
3860
|
+
},
|
|
3622
3861
|
/**
|
|
3623
3862
|
* Get recent batch jobs for the processing dashboard
|
|
3624
3863
|
*/
|
|
@@ -5007,230 +5246,246 @@ function deepMergeComponent(existingVal, newVal) {
|
|
|
5007
5246
|
}
|
|
5008
5247
|
return merged;
|
|
5009
5248
|
}
|
|
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];
|
|
5249
|
+
function parseRowToPayload(strapi, attrs, row) {
|
|
5250
|
+
const payload = {};
|
|
5251
|
+
for (const [key, rawVal] of Object.entries(row)) {
|
|
5252
|
+
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
5253
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5254
|
+
if (key.includes(".")) {
|
|
5255
|
+
const parts = key.split(".");
|
|
5256
|
+
let currentAttrs = attrs;
|
|
5257
|
+
let currentTarget = payload;
|
|
5258
|
+
for (let pIdx = 0; pIdx < parts.length; pIdx++) {
|
|
5259
|
+
const part = parts[pIdx];
|
|
5260
|
+
const isLeaf = pIdx === parts.length - 1;
|
|
5261
|
+
if (isLeaf) {
|
|
5262
|
+
let leafDef = currentAttrs[part];
|
|
5263
|
+
if (!leafDef) {
|
|
5264
|
+
const match = Object.keys(currentAttrs).find(
|
|
5265
|
+
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5266
|
+
);
|
|
5267
|
+
if (match) leafDef = currentAttrs[match];
|
|
5268
|
+
}
|
|
5269
|
+
if (!leafDef) {
|
|
5270
|
+
break;
|
|
5271
|
+
}
|
|
5272
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
5273
|
+
if (leafDef.type !== "dynamiczone" && !(leafDef.type === "component" && leafDef.repeatable) && leafDef.type !== "relation") {
|
|
5274
|
+
currentTarget[part] = null;
|
|
5057
5275
|
}
|
|
5058
|
-
|
|
5059
|
-
|
|
5276
|
+
} else if (leafDef.type === "integer" || leafDef.type === "biginteger") {
|
|
5277
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
5278
|
+
currentTarget[part] = isNaN(parsedInt) ? null : parsedInt;
|
|
5279
|
+
} else if (leafDef.type === "float" || leafDef.type === "decimal") {
|
|
5280
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5281
|
+
currentTarget[part] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
5282
|
+
} else if (leafDef.type === "boolean") {
|
|
5283
|
+
currentTarget[part] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
5284
|
+
} else if (leafDef.type === "json") {
|
|
5285
|
+
try {
|
|
5286
|
+
currentTarget[part] = JSON.parse(rawVal);
|
|
5287
|
+
} catch {
|
|
5288
|
+
currentTarget[part] = rawVal;
|
|
5060
5289
|
}
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5290
|
+
} else if (leafDef.type === "component" && leafDef.repeatable) {
|
|
5291
|
+
try {
|
|
5292
|
+
const parsed = JSON.parse(rawVal);
|
|
5293
|
+
if (Array.isArray(parsed)) {
|
|
5294
|
+
currentTarget[part] = parsed;
|
|
5064
5295
|
}
|
|
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 {
|
|
5296
|
+
} catch {
|
|
5297
|
+
}
|
|
5298
|
+
} else if (leafDef.type === "relation") {
|
|
5299
|
+
try {
|
|
5300
|
+
const parsed = JSON.parse(rawVal);
|
|
5301
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5302
|
+
currentTarget[part] = parsed;
|
|
5303
|
+
} else if (isValidDocId(rawVal)) {
|
|
5077
5304
|
currentTarget[part] = rawVal;
|
|
5078
5305
|
}
|
|
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
|
-
}
|
|
5306
|
+
} catch {
|
|
5307
|
+
if (isValidDocId(rawVal)) {
|
|
5308
|
+
currentTarget[part] = rawVal;
|
|
5099
5309
|
}
|
|
5100
|
-
} else {
|
|
5101
|
-
currentTarget[part] = rawVal;
|
|
5102
5310
|
}
|
|
5103
5311
|
} 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 {
|
|
5312
|
+
currentTarget[part] = rawVal;
|
|
5313
|
+
}
|
|
5314
|
+
} else {
|
|
5315
|
+
let compDef = currentAttrs[part];
|
|
5316
|
+
if (!compDef) {
|
|
5317
|
+
const match = Object.keys(currentAttrs).find(
|
|
5318
|
+
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5319
|
+
);
|
|
5320
|
+
if (match) compDef = currentAttrs[match];
|
|
5321
|
+
}
|
|
5322
|
+
if (compDef?.type === "component" && !compDef.repeatable && compDef.component) {
|
|
5323
|
+
const comp = strapi.components[compDef.component];
|
|
5324
|
+
if (!comp?.attributes) {
|
|
5122
5325
|
break;
|
|
5123
5326
|
}
|
|
5327
|
+
if (!currentTarget[part] || typeof currentTarget[part] !== "object" || Array.isArray(currentTarget[part])) {
|
|
5328
|
+
currentTarget[part] = {};
|
|
5329
|
+
}
|
|
5330
|
+
currentTarget = currentTarget[part];
|
|
5331
|
+
currentAttrs = comp.attributes;
|
|
5332
|
+
} else {
|
|
5333
|
+
break;
|
|
5124
5334
|
}
|
|
5125
5335
|
}
|
|
5126
|
-
continue;
|
|
5127
5336
|
}
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5337
|
+
continue;
|
|
5338
|
+
}
|
|
5339
|
+
let attrDef = attrs[key];
|
|
5340
|
+
if (!attrDef) {
|
|
5341
|
+
const matchKey = Object.keys(attrs).find(
|
|
5342
|
+
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
5343
|
+
);
|
|
5344
|
+
if (matchKey) {
|
|
5345
|
+
attrDef = attrs[matchKey];
|
|
5136
5346
|
}
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
continue;
|
|
5347
|
+
}
|
|
5348
|
+
if (!attrDef) continue;
|
|
5349
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
5350
|
+
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
5351
|
+
payload[key] = null;
|
|
5143
5352
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5353
|
+
continue;
|
|
5354
|
+
}
|
|
5355
|
+
switch (attrDef.type) {
|
|
5356
|
+
case "integer":
|
|
5357
|
+
case "biginteger": {
|
|
5358
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
5359
|
+
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
5360
|
+
break;
|
|
5361
|
+
}
|
|
5362
|
+
case "float":
|
|
5363
|
+
case "decimal": {
|
|
5364
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5365
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
5366
|
+
break;
|
|
5367
|
+
}
|
|
5368
|
+
case "boolean":
|
|
5369
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
5370
|
+
break;
|
|
5371
|
+
case "json":
|
|
5372
|
+
try {
|
|
5373
|
+
payload[key] = JSON.parse(rawVal);
|
|
5374
|
+
} catch {
|
|
5375
|
+
payload[key] = rawVal;
|
|
5150
5376
|
}
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5377
|
+
break;
|
|
5378
|
+
case "dynamiczone": {
|
|
5379
|
+
try {
|
|
5380
|
+
const parsed = JSON.parse(rawVal);
|
|
5381
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
5382
|
+
payload[key] = parsed;
|
|
5383
|
+
}
|
|
5384
|
+
} catch {
|
|
5156
5385
|
}
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5386
|
+
break;
|
|
5387
|
+
}
|
|
5388
|
+
case "component": {
|
|
5389
|
+
if (attrDef.repeatable) {
|
|
5161
5390
|
try {
|
|
5162
|
-
|
|
5391
|
+
const parsed = JSON.parse(rawVal);
|
|
5392
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
5393
|
+
payload[key] = parsed;
|
|
5394
|
+
}
|
|
5163
5395
|
} catch {
|
|
5164
|
-
payload[key] = rawVal;
|
|
5165
5396
|
}
|
|
5166
|
-
|
|
5167
|
-
case "dynamiczone": {
|
|
5397
|
+
} else {
|
|
5168
5398
|
try {
|
|
5169
5399
|
const parsed = JSON.parse(rawVal);
|
|
5170
|
-
if (
|
|
5400
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5171
5401
|
payload[key] = parsed;
|
|
5172
5402
|
}
|
|
5173
5403
|
} catch {
|
|
5174
5404
|
}
|
|
5175
|
-
break;
|
|
5176
5405
|
}
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5406
|
+
break;
|
|
5407
|
+
}
|
|
5408
|
+
case "relation": {
|
|
5409
|
+
try {
|
|
5410
|
+
const parsed = JSON.parse(rawVal);
|
|
5411
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5412
|
+
payload[key] = parsed;
|
|
5413
|
+
break;
|
|
5414
|
+
}
|
|
5415
|
+
} catch {
|
|
5416
|
+
}
|
|
5417
|
+
if (isValidDocId(rawVal)) {
|
|
5418
|
+
payload[key] = rawVal;
|
|
5419
|
+
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5420
|
+
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5421
|
+
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5422
|
+
payload[key] = parts;
|
|
5194
5423
|
}
|
|
5195
|
-
break;
|
|
5196
5424
|
}
|
|
5197
|
-
|
|
5425
|
+
break;
|
|
5426
|
+
}
|
|
5427
|
+
case "media": {
|
|
5428
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
5429
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
5430
|
+
} else {
|
|
5198
5431
|
try {
|
|
5199
5432
|
const parsed = JSON.parse(rawVal);
|
|
5200
|
-
if (
|
|
5433
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
5201
5434
|
payload[key] = parsed;
|
|
5202
|
-
break;
|
|
5203
5435
|
}
|
|
5204
5436
|
} catch {
|
|
5205
5437
|
}
|
|
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
5438
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5439
|
+
break;
|
|
5440
|
+
}
|
|
5441
|
+
default:
|
|
5442
|
+
payload[key] = rawVal;
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
return payload;
|
|
5446
|
+
}
|
|
5447
|
+
function formatValueDisplay(val) {
|
|
5448
|
+
if (val === null || val === void 0) return "(empty)";
|
|
5449
|
+
if (typeof val === "object") {
|
|
5450
|
+
try {
|
|
5451
|
+
return JSON.stringify(val);
|
|
5452
|
+
} catch {
|
|
5453
|
+
return String(val);
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
const s = String(val).trim();
|
|
5457
|
+
return s === "" ? "(empty)" : s;
|
|
5458
|
+
}
|
|
5459
|
+
async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
5460
|
+
const attrs = ct.attributes || {};
|
|
5461
|
+
const { mode = "auto", publish = true, publishStrategy = "safe", user } = options;
|
|
5462
|
+
const isDraftAndPublish = Boolean(ct.options?.draftAndPublish);
|
|
5463
|
+
const stats = {
|
|
5464
|
+
total: rows.length,
|
|
5465
|
+
created: 0,
|
|
5466
|
+
updated: 0,
|
|
5467
|
+
skipped: 0,
|
|
5468
|
+
newWithoutDocId: 0,
|
|
5469
|
+
errors: [],
|
|
5470
|
+
diffs: []
|
|
5471
|
+
};
|
|
5472
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5473
|
+
const rowNumber = startRowNumber + i;
|
|
5474
|
+
const row = rows[i];
|
|
5475
|
+
let docId = extractDocId(row);
|
|
5476
|
+
const rowLocale = extractLocale(row);
|
|
5477
|
+
if (ct.kind === "singleType" && !docId) {
|
|
5478
|
+
try {
|
|
5479
|
+
const findSingleArgs = {};
|
|
5480
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
5481
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
5482
|
+
if (singleDoc?.documentId) {
|
|
5483
|
+
docId = singleDoc.documentId;
|
|
5229
5484
|
}
|
|
5230
|
-
|
|
5231
|
-
payload[key] = rawVal;
|
|
5485
|
+
} catch {
|
|
5232
5486
|
}
|
|
5233
5487
|
}
|
|
5488
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
5234
5489
|
try {
|
|
5235
5490
|
if (mode === "update") {
|
|
5236
5491
|
if (!docId) {
|
|
@@ -5280,7 +5535,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5280
5535
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5281
5536
|
await strapi.documents(uid).update(updateArgs);
|
|
5282
5537
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5283
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5538
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5284
5539
|
if (shouldPublish) {
|
|
5285
5540
|
try {
|
|
5286
5541
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5335,7 +5590,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5335
5590
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5336
5591
|
await strapi.documents(uid).update(updateArgs);
|
|
5337
5592
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5338
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5593
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5339
5594
|
if (shouldPublish) {
|
|
5340
5595
|
try {
|
|
5341
5596
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5376,7 +5631,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5376
5631
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5377
5632
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5378
5633
|
const newDocId = created?.documentId || docId;
|
|
5379
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5634
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5380
5635
|
if (shouldPublishNew && newDocId) {
|
|
5381
5636
|
try {
|
|
5382
5637
|
const pubArgs = { documentId: newDocId };
|
|
@@ -5432,7 +5687,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5432
5687
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5433
5688
|
await strapi.documents(uid).update(updateArgs);
|
|
5434
5689
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5435
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5690
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5436
5691
|
if (shouldPublish) {
|
|
5437
5692
|
try {
|
|
5438
5693
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5473,7 +5728,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5473
5728
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5474
5729
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5475
5730
|
const newDocId = created?.documentId || docId;
|
|
5476
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5731
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5477
5732
|
if (shouldPublishNew && newDocId) {
|
|
5478
5733
|
try {
|
|
5479
5734
|
const pubArgs = { documentId: newDocId };
|
|
@@ -6217,6 +6472,174 @@ const csvService = ({ strapi }) => ({
|
|
|
6217
6472
|
totalWarningCount: warnings.length
|
|
6218
6473
|
};
|
|
6219
6474
|
},
|
|
6475
|
+
/**
|
|
6476
|
+
* Previews CSV batch changes item-by-item without mutating the database.
|
|
6477
|
+
* Compares proposed CSV rows against existing documents to compute exact diffs for Maker-Checker review.
|
|
6478
|
+
*/
|
|
6479
|
+
async previewCsvBatch(uid, rows, options = {}) {
|
|
6480
|
+
const ct = strapi.contentTypes[uid];
|
|
6481
|
+
if (!ct) {
|
|
6482
|
+
throw new Error(`Content type "${uid}" not found`);
|
|
6483
|
+
}
|
|
6484
|
+
const attrs = ct.attributes || {};
|
|
6485
|
+
const { mode = "auto" } = options;
|
|
6486
|
+
const diffs = [];
|
|
6487
|
+
let willUpdateCount = 0;
|
|
6488
|
+
let willCreateCount = 0;
|
|
6489
|
+
let unchangedCount = 0;
|
|
6490
|
+
let skippedCount = 0;
|
|
6491
|
+
for (let i = 0; i < rows.length; i++) {
|
|
6492
|
+
const rowNumber = i + 2;
|
|
6493
|
+
const row = rows[i];
|
|
6494
|
+
let docId = extractDocId(row);
|
|
6495
|
+
const rowLocale = extractLocale(row);
|
|
6496
|
+
if (ct.kind === "singleType" && !docId) {
|
|
6497
|
+
try {
|
|
6498
|
+
const findSingleArgs = {};
|
|
6499
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
6500
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
6501
|
+
if (singleDoc?.documentId) {
|
|
6502
|
+
docId = singleDoc.documentId;
|
|
6503
|
+
}
|
|
6504
|
+
} catch {
|
|
6505
|
+
}
|
|
6506
|
+
}
|
|
6507
|
+
let existing = null;
|
|
6508
|
+
if (docId) {
|
|
6509
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
6510
|
+
}
|
|
6511
|
+
if (mode === "update" && (!docId || !existing)) {
|
|
6512
|
+
skippedCount++;
|
|
6513
|
+
diffs.push({
|
|
6514
|
+
uid,
|
|
6515
|
+
collectionName: ct.info?.displayName || uid,
|
|
6516
|
+
documentId: docId || `(Row #${rowNumber})`,
|
|
6517
|
+
label: `Row #${rowNumber}`,
|
|
6518
|
+
attribute: "(record)",
|
|
6519
|
+
oldValue: null,
|
|
6520
|
+
newValue: null,
|
|
6521
|
+
oldDisplay: "—",
|
|
6522
|
+
newDisplay: "—",
|
|
6523
|
+
hasChange: false,
|
|
6524
|
+
status: "will_skip",
|
|
6525
|
+
action: "skip",
|
|
6526
|
+
message: !docId ? "Missing documentId in update mode" : `Document "${docId}" not found in database`
|
|
6527
|
+
});
|
|
6528
|
+
continue;
|
|
6529
|
+
}
|
|
6530
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
6531
|
+
if (existing) {
|
|
6532
|
+
const resolvedDocId = existing.documentId || docId;
|
|
6533
|
+
const entryLabel = existing.title || existing.name || existing.slug || `Entry #${resolvedDocId}`;
|
|
6534
|
+
let rowHasChanges = false;
|
|
6535
|
+
const payloadKeys = Object.keys(payload);
|
|
6536
|
+
if (payloadKeys.length === 0) {
|
|
6537
|
+
unchangedCount++;
|
|
6538
|
+
diffs.push({
|
|
6539
|
+
uid,
|
|
6540
|
+
collectionName: ct.info?.displayName || uid,
|
|
6541
|
+
documentId: resolvedDocId,
|
|
6542
|
+
label: entryLabel,
|
|
6543
|
+
attribute: "(no fields)",
|
|
6544
|
+
oldValue: null,
|
|
6545
|
+
newValue: null,
|
|
6546
|
+
oldDisplay: "—",
|
|
6547
|
+
newDisplay: "—",
|
|
6548
|
+
hasChange: false,
|
|
6549
|
+
status: "unchanged",
|
|
6550
|
+
action: "update",
|
|
6551
|
+
message: "No modified attributes in CSV row"
|
|
6552
|
+
});
|
|
6553
|
+
} else {
|
|
6554
|
+
for (const attrKey of payloadKeys) {
|
|
6555
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
6556
|
+
let newVal = payload[attrKey];
|
|
6557
|
+
if (attrs[attrKey]?.type === "component" && typeof newVal === "object" && newVal !== null) {
|
|
6558
|
+
newVal = deepMergeComponent(existing[attrKey], newVal);
|
|
6559
|
+
}
|
|
6560
|
+
const oldDisplay = formatValueDisplay(oldVal);
|
|
6561
|
+
const newDisplay = formatValueDisplay(newVal);
|
|
6562
|
+
const hasChange = String(oldVal ?? "") !== String(newVal ?? "");
|
|
6563
|
+
if (hasChange) {
|
|
6564
|
+
rowHasChanges = true;
|
|
6565
|
+
}
|
|
6566
|
+
diffs.push({
|
|
6567
|
+
uid,
|
|
6568
|
+
collectionName: ct.info?.displayName || uid,
|
|
6569
|
+
documentId: resolvedDocId,
|
|
6570
|
+
label: entryLabel,
|
|
6571
|
+
attribute: attrKey,
|
|
6572
|
+
oldValue: oldVal,
|
|
6573
|
+
newValue: newVal,
|
|
6574
|
+
oldDisplay,
|
|
6575
|
+
newDisplay,
|
|
6576
|
+
hasChange,
|
|
6577
|
+
status: hasChange ? "will_update" : "unchanged",
|
|
6578
|
+
action: "update",
|
|
6579
|
+
message: hasChange ? `Will update "${attrKey}" from CSV` : `Already matches CSV value`
|
|
6580
|
+
});
|
|
6581
|
+
}
|
|
6582
|
+
if (rowHasChanges) {
|
|
6583
|
+
willUpdateCount++;
|
|
6584
|
+
} else {
|
|
6585
|
+
unchangedCount++;
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
} else {
|
|
6589
|
+
willCreateCount++;
|
|
6590
|
+
const resolvedDocId = docId || `(Auto-generated)`;
|
|
6591
|
+
const entryLabel = payload.title || payload.name || payload.slug || `New Entry (Row #${rowNumber})`;
|
|
6592
|
+
const payloadKeys = Object.keys(payload);
|
|
6593
|
+
if (payloadKeys.length === 0) {
|
|
6594
|
+
diffs.push({
|
|
6595
|
+
uid,
|
|
6596
|
+
collectionName: ct.info?.displayName || uid,
|
|
6597
|
+
documentId: resolvedDocId,
|
|
6598
|
+
label: entryLabel,
|
|
6599
|
+
attribute: "(all fields)",
|
|
6600
|
+
oldValue: null,
|
|
6601
|
+
newValue: null,
|
|
6602
|
+
oldDisplay: "(new entry)",
|
|
6603
|
+
newDisplay: "(empty record)",
|
|
6604
|
+
hasChange: true,
|
|
6605
|
+
status: "will_create",
|
|
6606
|
+
action: "create",
|
|
6607
|
+
message: "Will create new record"
|
|
6608
|
+
});
|
|
6609
|
+
} else {
|
|
6610
|
+
for (const attrKey of payloadKeys) {
|
|
6611
|
+
const newVal = payload[attrKey];
|
|
6612
|
+
diffs.push({
|
|
6613
|
+
uid,
|
|
6614
|
+
collectionName: ct.info?.displayName || uid,
|
|
6615
|
+
documentId: resolvedDocId,
|
|
6616
|
+
label: entryLabel,
|
|
6617
|
+
attribute: attrKey,
|
|
6618
|
+
oldValue: null,
|
|
6619
|
+
newValue: newVal,
|
|
6620
|
+
oldDisplay: "(new entry)",
|
|
6621
|
+
newDisplay: formatValueDisplay(newVal),
|
|
6622
|
+
hasChange: true,
|
|
6623
|
+
status: "will_create",
|
|
6624
|
+
action: "create",
|
|
6625
|
+
message: `Will create new record with attribute "${attrKey}"`
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
return {
|
|
6632
|
+
success: true,
|
|
6633
|
+
summary: {
|
|
6634
|
+
total: rows.length,
|
|
6635
|
+
willUpdate: willUpdateCount,
|
|
6636
|
+
willCreate: willCreateCount,
|
|
6637
|
+
unchanged: unchangedCount,
|
|
6638
|
+
skipped: skippedCount
|
|
6639
|
+
},
|
|
6640
|
+
diffs
|
|
6641
|
+
};
|
|
6642
|
+
},
|
|
6220
6643
|
/**
|
|
6221
6644
|
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
6222
6645
|
*/
|