strapi-plugin-bulk-updater 1.4.9 → 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 +646 -152
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +646 -152
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- 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
|
*/
|
|
@@ -4990,176 +5229,263 @@ async function findExistingDocument(strapi, uid, targetDocId, docLocale) {
|
|
|
4990
5229
|
}
|
|
4991
5230
|
return null;
|
|
4992
5231
|
}
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
for (let i = 0; i < rows.length; i++) {
|
|
5007
|
-
const rowNumber = startRowNumber + i;
|
|
5008
|
-
const row = rows[i];
|
|
5009
|
-
let docId = extractDocId(row);
|
|
5010
|
-
const rowLocale = extractLocale(row);
|
|
5011
|
-
if (ct.kind === "singleType" && !docId) {
|
|
5012
|
-
try {
|
|
5013
|
-
const findSingleArgs = {};
|
|
5014
|
-
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
5015
|
-
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
5016
|
-
if (singleDoc?.documentId) {
|
|
5017
|
-
docId = singleDoc.documentId;
|
|
5018
|
-
}
|
|
5019
|
-
} catch {
|
|
5020
|
-
}
|
|
5232
|
+
function deepMergeComponent(existingVal, newVal) {
|
|
5233
|
+
if (!existingVal || typeof existingVal !== "object" || Array.isArray(existingVal)) {
|
|
5234
|
+
return newVal;
|
|
5235
|
+
}
|
|
5236
|
+
if (!newVal || typeof newVal !== "object" || Array.isArray(newVal)) {
|
|
5237
|
+
return newVal;
|
|
5238
|
+
}
|
|
5239
|
+
const merged = { ...existingVal };
|
|
5240
|
+
for (const k of Object.keys(newVal)) {
|
|
5241
|
+
if (newVal[k] && typeof newVal[k] === "object" && !Array.isArray(newVal[k])) {
|
|
5242
|
+
merged[k] = deepMergeComponent(existingVal[k], newVal[k]);
|
|
5243
|
+
} else if (newVal[k] !== void 0) {
|
|
5244
|
+
merged[k] = newVal[k];
|
|
5021
5245
|
}
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5246
|
+
}
|
|
5247
|
+
return merged;
|
|
5248
|
+
}
|
|
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;
|
|
5275
|
+
}
|
|
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;
|
|
5289
|
+
}
|
|
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;
|
|
5295
|
+
}
|
|
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)) {
|
|
5304
|
+
currentTarget[part] = rawVal;
|
|
5305
|
+
}
|
|
5306
|
+
} catch {
|
|
5307
|
+
if (isValidDocId(rawVal)) {
|
|
5308
|
+
currentTarget[part] = rawVal;
|
|
5050
5309
|
}
|
|
5051
5310
|
}
|
|
5052
|
-
|
|
5311
|
+
} 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) {
|
|
5325
|
+
break;
|
|
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;
|
|
5053
5334
|
}
|
|
5054
5335
|
}
|
|
5055
|
-
continue;
|
|
5056
5336
|
}
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
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];
|
|
5065
5346
|
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
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;
|
|
5072
5352
|
}
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
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;
|
|
5079
5376
|
}
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
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 {
|
|
5085
5385
|
}
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5386
|
+
break;
|
|
5387
|
+
}
|
|
5388
|
+
case "component": {
|
|
5389
|
+
if (attrDef.repeatable) {
|
|
5090
5390
|
try {
|
|
5091
|
-
|
|
5391
|
+
const parsed = JSON.parse(rawVal);
|
|
5392
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
5393
|
+
payload[key] = parsed;
|
|
5394
|
+
}
|
|
5092
5395
|
} catch {
|
|
5093
|
-
payload[key] = rawVal;
|
|
5094
5396
|
}
|
|
5095
|
-
|
|
5096
|
-
case "dynamiczone": {
|
|
5397
|
+
} else {
|
|
5097
5398
|
try {
|
|
5098
5399
|
const parsed = JSON.parse(rawVal);
|
|
5099
|
-
if (
|
|
5400
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5100
5401
|
payload[key] = parsed;
|
|
5101
5402
|
}
|
|
5102
5403
|
} catch {
|
|
5103
5404
|
}
|
|
5104
|
-
break;
|
|
5105
5405
|
}
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
}
|
|
5115
|
-
} else {
|
|
5116
|
-
try {
|
|
5117
|
-
const parsed = JSON.parse(rawVal);
|
|
5118
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5119
|
-
payload[key] = parsed;
|
|
5120
|
-
}
|
|
5121
|
-
} catch {
|
|
5122
|
-
}
|
|
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;
|
|
5123
5414
|
}
|
|
5124
|
-
|
|
5415
|
+
} catch {
|
|
5125
5416
|
}
|
|
5126
|
-
|
|
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;
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
break;
|
|
5426
|
+
}
|
|
5427
|
+
case "media": {
|
|
5428
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
5429
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
5430
|
+
} else {
|
|
5127
5431
|
try {
|
|
5128
5432
|
const parsed = JSON.parse(rawVal);
|
|
5129
|
-
if (
|
|
5433
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
5130
5434
|
payload[key] = parsed;
|
|
5131
|
-
break;
|
|
5132
5435
|
}
|
|
5133
5436
|
} catch {
|
|
5134
5437
|
}
|
|
5135
|
-
if (isValidDocId(rawVal)) {
|
|
5136
|
-
payload[key] = rawVal;
|
|
5137
|
-
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5138
|
-
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5139
|
-
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5140
|
-
payload[key] = parts;
|
|
5141
|
-
}
|
|
5142
|
-
}
|
|
5143
|
-
break;
|
|
5144
5438
|
}
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
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;
|
|
5158
5484
|
}
|
|
5159
|
-
|
|
5160
|
-
payload[key] = rawVal;
|
|
5485
|
+
} catch {
|
|
5161
5486
|
}
|
|
5162
5487
|
}
|
|
5488
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
5163
5489
|
try {
|
|
5164
5490
|
if (mode === "update") {
|
|
5165
5491
|
if (!docId) {
|
|
@@ -5184,7 +5510,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5184
5510
|
const mergedPayload = { ...payload };
|
|
5185
5511
|
for (const key of Object.keys(payload)) {
|
|
5186
5512
|
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5187
|
-
mergedPayload[key] =
|
|
5513
|
+
mergedPayload[key] = deepMergeComponent(existing[key], payload[key]);
|
|
5188
5514
|
}
|
|
5189
5515
|
}
|
|
5190
5516
|
if (Object.keys(mergedPayload).length === 0) {
|
|
@@ -5209,7 +5535,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5209
5535
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5210
5536
|
await strapi.documents(uid).update(updateArgs);
|
|
5211
5537
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5212
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5538
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5213
5539
|
if (shouldPublish) {
|
|
5214
5540
|
try {
|
|
5215
5541
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5254,7 +5580,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5254
5580
|
const mergedPayload = { ...payload };
|
|
5255
5581
|
for (const key of Object.keys(payload)) {
|
|
5256
5582
|
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5257
|
-
mergedPayload[key] =
|
|
5583
|
+
mergedPayload[key] = deepMergeComponent(existing[key], payload[key]);
|
|
5258
5584
|
}
|
|
5259
5585
|
}
|
|
5260
5586
|
const updateArgs = {
|
|
@@ -5264,7 +5590,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5264
5590
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5265
5591
|
await strapi.documents(uid).update(updateArgs);
|
|
5266
5592
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5267
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5593
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5268
5594
|
if (shouldPublish) {
|
|
5269
5595
|
try {
|
|
5270
5596
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5305,7 +5631,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5305
5631
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5306
5632
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5307
5633
|
const newDocId = created?.documentId || docId;
|
|
5308
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5634
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5309
5635
|
if (shouldPublishNew && newDocId) {
|
|
5310
5636
|
try {
|
|
5311
5637
|
const pubArgs = { documentId: newDocId };
|
|
@@ -5351,7 +5677,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5351
5677
|
const mergedPayload = { ...payload };
|
|
5352
5678
|
for (const key of Object.keys(payload)) {
|
|
5353
5679
|
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5354
|
-
mergedPayload[key] =
|
|
5680
|
+
mergedPayload[key] = deepMergeComponent(existing[key], payload[key]);
|
|
5355
5681
|
}
|
|
5356
5682
|
}
|
|
5357
5683
|
const updateArgs = {
|
|
@@ -5361,7 +5687,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5361
5687
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5362
5688
|
await strapi.documents(uid).update(updateArgs);
|
|
5363
5689
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5364
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5690
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5365
5691
|
if (shouldPublish) {
|
|
5366
5692
|
try {
|
|
5367
5693
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5402,7 +5728,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5402
5728
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5403
5729
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5404
5730
|
const newDocId = created?.documentId || docId;
|
|
5405
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5731
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5406
5732
|
if (shouldPublishNew && newDocId) {
|
|
5407
5733
|
try {
|
|
5408
5734
|
const pubArgs = { documentId: newDocId };
|
|
@@ -6146,6 +6472,174 @@ const csvService = ({ strapi }) => ({
|
|
|
6146
6472
|
totalWarningCount: warnings.length
|
|
6147
6473
|
};
|
|
6148
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
|
+
},
|
|
6149
6643
|
/**
|
|
6150
6644
|
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
6151
6645
|
*/
|