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.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");
|
|
@@ -2371,7 +2409,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2371
2409
|
} else if (normOp === "endswith") {
|
|
2372
2410
|
orConditions.push({ [k]: { $endsWithi: q } });
|
|
2373
2411
|
} else if (normOp === "notequals") {
|
|
2374
|
-
orConditions.push({ [k]: { $
|
|
2412
|
+
orConditions.push({ [k]: { $nei: q } });
|
|
2375
2413
|
} else if (normOp === "notcontains") {
|
|
2376
2414
|
orConditions.push({ [k]: { $notContainsi: q } });
|
|
2377
2415
|
} else {
|
|
@@ -2389,12 +2427,21 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2389
2427
|
} else if (normOp === "notempty") {
|
|
2390
2428
|
condition = isStringLike ? { $and: [{ $notNull: true }, { $ne: "" }] } : { $notNull: true };
|
|
2391
2429
|
} else if (q !== "") {
|
|
2430
|
+
let typedVal = q;
|
|
2431
|
+
const isNumeric = ["integer", "biginteger", "float", "decimal", "number"].includes(targetAttrSchema?.type);
|
|
2432
|
+
const isBoolean = targetAttrSchema?.type === "boolean";
|
|
2433
|
+
if (isNumeric && /^-?\d+(\.\d+)?$/.test(q)) {
|
|
2434
|
+
typedVal = Number(q);
|
|
2435
|
+
} else if (isBoolean) {
|
|
2436
|
+
if (q.toLowerCase() === "true") typedVal = true;
|
|
2437
|
+
else if (q.toLowerCase() === "false") typedVal = false;
|
|
2438
|
+
}
|
|
2392
2439
|
switch (normOp) {
|
|
2393
2440
|
case "equals":
|
|
2394
|
-
condition = { $
|
|
2441
|
+
condition = isStringLike ? { $eqi: q } : { $eq: typedVal };
|
|
2395
2442
|
break;
|
|
2396
2443
|
case "notequals":
|
|
2397
|
-
condition = { $
|
|
2444
|
+
condition = isStringLike ? { $nei: q } : { $ne: typedVal };
|
|
2398
2445
|
break;
|
|
2399
2446
|
case "contains":
|
|
2400
2447
|
condition = { $containsi: q };
|
|
@@ -2403,10 +2450,10 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2403
2450
|
condition = { $notContainsi: q };
|
|
2404
2451
|
break;
|
|
2405
2452
|
case "startswith":
|
|
2406
|
-
condition = { $
|
|
2453
|
+
condition = { $startsWithi: q };
|
|
2407
2454
|
break;
|
|
2408
2455
|
case "endswith":
|
|
2409
|
-
condition = { $
|
|
2456
|
+
condition = { $endsWithi: q };
|
|
2410
2457
|
break;
|
|
2411
2458
|
}
|
|
2412
2459
|
}
|
|
@@ -2532,7 +2579,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2532
2579
|
if (typeof v === "object") {
|
|
2533
2580
|
return testSingleValue(v.name) || testSingleValue(v.title) || testSingleValue(v.email) || testSingleValue(v.username) || testSingleValue(v.documentId) || testSingleValue(v.id) || testSingleValue(v.slug);
|
|
2534
2581
|
}
|
|
2535
|
-
const valStr = String(v).toLowerCase();
|
|
2582
|
+
const valStr = String(v).trim().toLowerCase();
|
|
2536
2583
|
switch (normOp) {
|
|
2537
2584
|
case "equals":
|
|
2538
2585
|
return valStr === qVal;
|
|
@@ -3616,6 +3663,198 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3616
3663
|
batch
|
|
3617
3664
|
};
|
|
3618
3665
|
},
|
|
3666
|
+
/**
|
|
3667
|
+
* Inspect batch changes item-by-item for Maker-Checker 4-Eyes review
|
|
3668
|
+
*/
|
|
3669
|
+
async getPendingBatchDiffs(batchId) {
|
|
3670
|
+
if (!batchId) throw new Error("batchId is required");
|
|
3671
|
+
const batch = pendingBatches.get(batchId);
|
|
3672
|
+
if (!batch) {
|
|
3673
|
+
throw new Error(`Pending batch "${batchId}" not found.`);
|
|
3674
|
+
}
|
|
3675
|
+
let previewRes = null;
|
|
3676
|
+
if (batch.payload?.action === "csv_import" || batch.id.startsWith("csv_")) {
|
|
3677
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
3678
|
+
previewRes = await csvService2.previewCsvBatch(
|
|
3679
|
+
batch.payload.uid,
|
|
3680
|
+
batch.payload.rows || [],
|
|
3681
|
+
batch.payload.options || {}
|
|
3682
|
+
);
|
|
3683
|
+
} else if (batch.payload?.action === "publish") {
|
|
3684
|
+
const selectedEntries = batch.payload.selectedEntries || [];
|
|
3685
|
+
const selectedCollections = batch.payload.selectedCollections || [];
|
|
3686
|
+
const targetsMap = /* @__PURE__ */ new Map();
|
|
3687
|
+
for (const colUid of selectedCollections) {
|
|
3688
|
+
if (!strapi.contentTypes[colUid]) continue;
|
|
3689
|
+
const entries = await strapi.documents(colUid).findMany();
|
|
3690
|
+
for (const entry of entries || []) {
|
|
3691
|
+
targetsMap.set(`${colUid}@@@${entry.documentId}`, { uid: colUid, documentId: entry.documentId });
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
for (const item of selectedEntries) {
|
|
3695
|
+
if (!item.uid || !item.documentId) continue;
|
|
3696
|
+
targetsMap.set(`${item.uid}@@@${item.documentId}`, { uid: item.uid, documentId: item.documentId });
|
|
3697
|
+
}
|
|
3698
|
+
const diffs = [];
|
|
3699
|
+
let willUpdate = 0;
|
|
3700
|
+
let unchanged = 0;
|
|
3701
|
+
for (const target of targetsMap.values()) {
|
|
3702
|
+
const ct = strapi.contentTypes[target.uid];
|
|
3703
|
+
const displayName = ct?.info?.displayName || target.uid;
|
|
3704
|
+
try {
|
|
3705
|
+
const doc = await strapi.documents(target.uid).findOne({
|
|
3706
|
+
documentId: target.documentId
|
|
3707
|
+
});
|
|
3708
|
+
const label = doc ? doc.title || doc.name || doc.slug || doc.documentId : target.documentId;
|
|
3709
|
+
const isLive = Boolean(doc?.publishedAt) && doc?.status !== "modified";
|
|
3710
|
+
const currentStatus = isLive ? "published" : doc?.status === "modified" ? "modified draft" : "draft";
|
|
3711
|
+
const hasChange = !isLive;
|
|
3712
|
+
if (hasChange) willUpdate++;
|
|
3713
|
+
else unchanged++;
|
|
3714
|
+
diffs.push({
|
|
3715
|
+
uid: target.uid,
|
|
3716
|
+
collectionName: displayName,
|
|
3717
|
+
documentId: target.documentId,
|
|
3718
|
+
label,
|
|
3719
|
+
attribute: "status",
|
|
3720
|
+
oldValue: currentStatus,
|
|
3721
|
+
newValue: "published",
|
|
3722
|
+
oldDisplay: currentStatus,
|
|
3723
|
+
newDisplay: "published live",
|
|
3724
|
+
hasChange,
|
|
3725
|
+
status: hasChange ? "will_update" : "unchanged",
|
|
3726
|
+
action: "publish",
|
|
3727
|
+
message: hasChange ? "Record will be published live" : "Already published live"
|
|
3728
|
+
});
|
|
3729
|
+
} catch (err) {
|
|
3730
|
+
diffs.push({
|
|
3731
|
+
uid: target.uid,
|
|
3732
|
+
collectionName: displayName,
|
|
3733
|
+
documentId: target.documentId,
|
|
3734
|
+
label: target.documentId,
|
|
3735
|
+
attribute: "status",
|
|
3736
|
+
oldValue: null,
|
|
3737
|
+
newValue: null,
|
|
3738
|
+
oldDisplay: "—",
|
|
3739
|
+
newDisplay: "—",
|
|
3740
|
+
hasChange: false,
|
|
3741
|
+
status: "will_skip",
|
|
3742
|
+
action: "skip",
|
|
3743
|
+
message: `Error inspecting document: ${err.message}`
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
previewRes = {
|
|
3748
|
+
success: true,
|
|
3749
|
+
summary: {
|
|
3750
|
+
total: targetsMap.size,
|
|
3751
|
+
willUpdate,
|
|
3752
|
+
willCreate: 0,
|
|
3753
|
+
unchanged,
|
|
3754
|
+
skipped: 0
|
|
3755
|
+
},
|
|
3756
|
+
diffs
|
|
3757
|
+
};
|
|
3758
|
+
} else {
|
|
3759
|
+
const preview = await this.previewBatch({
|
|
3760
|
+
attribute: batch.payload.attribute,
|
|
3761
|
+
value: batch.payload.value,
|
|
3762
|
+
selectedEntries: batch.payload.selectedEntries,
|
|
3763
|
+
selectedCollections: batch.payload.selectedCollections,
|
|
3764
|
+
isRegex: batch.payload.isRegex,
|
|
3765
|
+
regexPattern: batch.payload.regexPattern
|
|
3766
|
+
});
|
|
3767
|
+
previewRes = {
|
|
3768
|
+
success: true,
|
|
3769
|
+
summary: {
|
|
3770
|
+
total: preview.summary?.total ?? 0,
|
|
3771
|
+
willUpdate: preview.summary?.willUpdate ?? 0,
|
|
3772
|
+
willCreate: 0,
|
|
3773
|
+
unchanged: preview.summary?.unchanged ?? 0,
|
|
3774
|
+
skipped: preview.summary?.skipped ?? 0
|
|
3775
|
+
},
|
|
3776
|
+
diffs: (preview.diffs || []).map((d) => ({
|
|
3777
|
+
...d,
|
|
3778
|
+
action: "update"
|
|
3779
|
+
}))
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
return {
|
|
3783
|
+
success: true,
|
|
3784
|
+
batchId: batch.id,
|
|
3785
|
+
createdAt: batch.createdAt,
|
|
3786
|
+
createdBy: batch.createdBy,
|
|
3787
|
+
makerNotes: batch.makerNotes,
|
|
3788
|
+
batchSummary: batch.summary,
|
|
3789
|
+
payload: {
|
|
3790
|
+
action: batch.payload?.action || "bulk_update",
|
|
3791
|
+
attribute: batch.payload?.attribute,
|
|
3792
|
+
value: batch.payload?.value,
|
|
3793
|
+
publishOnApproval: batch.summary?.publishOnApproval,
|
|
3794
|
+
publishStrategy: batch.summary?.publishStrategy
|
|
3795
|
+
},
|
|
3796
|
+
summary: previewRes.summary,
|
|
3797
|
+
diffs: previewRes.diffs
|
|
3798
|
+
};
|
|
3799
|
+
},
|
|
3800
|
+
/**
|
|
3801
|
+
* Export batch diffs to CSV for offline audit review
|
|
3802
|
+
*/
|
|
3803
|
+
async getPendingBatchDiffsCsv(batchId) {
|
|
3804
|
+
const data = await this.getPendingBatchDiffs(batchId);
|
|
3805
|
+
function escapeVal(val) {
|
|
3806
|
+
if (val === null || val === void 0) return "";
|
|
3807
|
+
let str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
3808
|
+
str = str.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3809
|
+
if (/^[=+@-]$/.test(str.charAt(0))) {
|
|
3810
|
+
str = `'${str}`;
|
|
3811
|
+
}
|
|
3812
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
3813
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
3814
|
+
}
|
|
3815
|
+
return str;
|
|
3816
|
+
}
|
|
3817
|
+
const headers = [
|
|
3818
|
+
"Batch ID",
|
|
3819
|
+
"Action",
|
|
3820
|
+
"Collection UID",
|
|
3821
|
+
"Collection Name",
|
|
3822
|
+
"Document ID",
|
|
3823
|
+
"Record Identifier",
|
|
3824
|
+
"Attribute Name",
|
|
3825
|
+
"Original Value (Before)",
|
|
3826
|
+
"Proposed Value (After)",
|
|
3827
|
+
"Change Status",
|
|
3828
|
+
"Details / Message"
|
|
3829
|
+
];
|
|
3830
|
+
const rows = [headers.join(",")];
|
|
3831
|
+
for (const diff of data.diffs || []) {
|
|
3832
|
+
const row = [
|
|
3833
|
+
escapeVal(data.batchId),
|
|
3834
|
+
escapeVal(diff.action || "update"),
|
|
3835
|
+
escapeVal(diff.uid),
|
|
3836
|
+
escapeVal(diff.collectionName),
|
|
3837
|
+
escapeVal(diff.documentId),
|
|
3838
|
+
escapeVal(diff.label),
|
|
3839
|
+
escapeVal(diff.attribute),
|
|
3840
|
+
escapeVal(diff.oldDisplay ?? diff.oldValue),
|
|
3841
|
+
escapeVal(diff.newDisplay ?? diff.newValue),
|
|
3842
|
+
escapeVal(
|
|
3843
|
+
diff.status === "will_update" ? "Will Update" : diff.status === "will_create" ? "Will Create" : diff.status === "unchanged" ? "Unchanged" : "Skipped"
|
|
3844
|
+
),
|
|
3845
|
+
escapeVal(diff.message)
|
|
3846
|
+
];
|
|
3847
|
+
rows.push(row.join(","));
|
|
3848
|
+
}
|
|
3849
|
+
const safeBatchId = batchId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
3850
|
+
const filename = `pending_batch_review_${safeBatchId}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
3851
|
+
return {
|
|
3852
|
+
success: true,
|
|
3853
|
+
filename,
|
|
3854
|
+
csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
|
|
3855
|
+
count: data.diffs?.length || 0
|
|
3856
|
+
};
|
|
3857
|
+
},
|
|
3619
3858
|
/**
|
|
3620
3859
|
* Get recent batch jobs for the processing dashboard
|
|
3621
3860
|
*/
|
|
@@ -4987,176 +5226,263 @@ async function findExistingDocument(strapi, uid, targetDocId, docLocale) {
|
|
|
4987
5226
|
}
|
|
4988
5227
|
return null;
|
|
4989
5228
|
}
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
for (let i = 0; i < rows.length; i++) {
|
|
5004
|
-
const rowNumber = startRowNumber + i;
|
|
5005
|
-
const row = rows[i];
|
|
5006
|
-
let docId = extractDocId(row);
|
|
5007
|
-
const rowLocale = extractLocale(row);
|
|
5008
|
-
if (ct.kind === "singleType" && !docId) {
|
|
5009
|
-
try {
|
|
5010
|
-
const findSingleArgs = {};
|
|
5011
|
-
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
5012
|
-
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
5013
|
-
if (singleDoc?.documentId) {
|
|
5014
|
-
docId = singleDoc.documentId;
|
|
5015
|
-
}
|
|
5016
|
-
} catch {
|
|
5017
|
-
}
|
|
5229
|
+
function deepMergeComponent(existingVal, newVal) {
|
|
5230
|
+
if (!existingVal || typeof existingVal !== "object" || Array.isArray(existingVal)) {
|
|
5231
|
+
return newVal;
|
|
5232
|
+
}
|
|
5233
|
+
if (!newVal || typeof newVal !== "object" || Array.isArray(newVal)) {
|
|
5234
|
+
return newVal;
|
|
5235
|
+
}
|
|
5236
|
+
const merged = { ...existingVal };
|
|
5237
|
+
for (const k of Object.keys(newVal)) {
|
|
5238
|
+
if (newVal[k] && typeof newVal[k] === "object" && !Array.isArray(newVal[k])) {
|
|
5239
|
+
merged[k] = deepMergeComponent(existingVal[k], newVal[k]);
|
|
5240
|
+
} else if (newVal[k] !== void 0) {
|
|
5241
|
+
merged[k] = newVal[k];
|
|
5018
5242
|
}
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
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
|
-
|
|
5243
|
+
}
|
|
5244
|
+
return merged;
|
|
5245
|
+
}
|
|
5246
|
+
function parseRowToPayload(strapi, attrs, row) {
|
|
5247
|
+
const payload = {};
|
|
5248
|
+
for (const [key, rawVal] of Object.entries(row)) {
|
|
5249
|
+
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
5250
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5251
|
+
if (key.includes(".")) {
|
|
5252
|
+
const parts = key.split(".");
|
|
5253
|
+
let currentAttrs = attrs;
|
|
5254
|
+
let currentTarget = payload;
|
|
5255
|
+
for (let pIdx = 0; pIdx < parts.length; pIdx++) {
|
|
5256
|
+
const part = parts[pIdx];
|
|
5257
|
+
const isLeaf = pIdx === parts.length - 1;
|
|
5258
|
+
if (isLeaf) {
|
|
5259
|
+
let leafDef = currentAttrs[part];
|
|
5260
|
+
if (!leafDef) {
|
|
5261
|
+
const match = Object.keys(currentAttrs).find(
|
|
5262
|
+
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5263
|
+
);
|
|
5264
|
+
if (match) leafDef = currentAttrs[match];
|
|
5265
|
+
}
|
|
5266
|
+
if (!leafDef) {
|
|
5267
|
+
break;
|
|
5268
|
+
}
|
|
5269
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
5270
|
+
if (leafDef.type !== "dynamiczone" && !(leafDef.type === "component" && leafDef.repeatable) && leafDef.type !== "relation") {
|
|
5271
|
+
currentTarget[part] = null;
|
|
5272
|
+
}
|
|
5273
|
+
} else if (leafDef.type === "integer" || leafDef.type === "biginteger") {
|
|
5274
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
5275
|
+
currentTarget[part] = isNaN(parsedInt) ? null : parsedInt;
|
|
5276
|
+
} else if (leafDef.type === "float" || leafDef.type === "decimal") {
|
|
5277
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5278
|
+
currentTarget[part] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
5279
|
+
} else if (leafDef.type === "boolean") {
|
|
5280
|
+
currentTarget[part] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
5281
|
+
} else if (leafDef.type === "json") {
|
|
5282
|
+
try {
|
|
5283
|
+
currentTarget[part] = JSON.parse(rawVal);
|
|
5284
|
+
} catch {
|
|
5285
|
+
currentTarget[part] = rawVal;
|
|
5286
|
+
}
|
|
5287
|
+
} else if (leafDef.type === "component" && leafDef.repeatable) {
|
|
5288
|
+
try {
|
|
5289
|
+
const parsed = JSON.parse(rawVal);
|
|
5290
|
+
if (Array.isArray(parsed)) {
|
|
5291
|
+
currentTarget[part] = parsed;
|
|
5292
|
+
}
|
|
5293
|
+
} catch {
|
|
5294
|
+
}
|
|
5295
|
+
} else if (leafDef.type === "relation") {
|
|
5296
|
+
try {
|
|
5297
|
+
const parsed = JSON.parse(rawVal);
|
|
5298
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5299
|
+
currentTarget[part] = parsed;
|
|
5300
|
+
} else if (isValidDocId(rawVal)) {
|
|
5301
|
+
currentTarget[part] = rawVal;
|
|
5302
|
+
}
|
|
5303
|
+
} catch {
|
|
5304
|
+
if (isValidDocId(rawVal)) {
|
|
5305
|
+
currentTarget[part] = rawVal;
|
|
5047
5306
|
}
|
|
5048
5307
|
}
|
|
5049
|
-
|
|
5308
|
+
} else {
|
|
5309
|
+
currentTarget[part] = rawVal;
|
|
5310
|
+
}
|
|
5311
|
+
} else {
|
|
5312
|
+
let compDef = currentAttrs[part];
|
|
5313
|
+
if (!compDef) {
|
|
5314
|
+
const match = Object.keys(currentAttrs).find(
|
|
5315
|
+
(k) => k.toLowerCase() === part.toLowerCase() || k.toLowerCase() === part.toLowerCase().replace(/\s+/g, "")
|
|
5316
|
+
);
|
|
5317
|
+
if (match) compDef = currentAttrs[match];
|
|
5318
|
+
}
|
|
5319
|
+
if (compDef?.type === "component" && !compDef.repeatable && compDef.component) {
|
|
5320
|
+
const comp = strapi.components[compDef.component];
|
|
5321
|
+
if (!comp?.attributes) {
|
|
5322
|
+
break;
|
|
5323
|
+
}
|
|
5324
|
+
if (!currentTarget[part] || typeof currentTarget[part] !== "object" || Array.isArray(currentTarget[part])) {
|
|
5325
|
+
currentTarget[part] = {};
|
|
5326
|
+
}
|
|
5327
|
+
currentTarget = currentTarget[part];
|
|
5328
|
+
currentAttrs = comp.attributes;
|
|
5329
|
+
} else {
|
|
5330
|
+
break;
|
|
5050
5331
|
}
|
|
5051
5332
|
}
|
|
5052
|
-
continue;
|
|
5053
5333
|
}
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5334
|
+
continue;
|
|
5335
|
+
}
|
|
5336
|
+
let attrDef = attrs[key];
|
|
5337
|
+
if (!attrDef) {
|
|
5338
|
+
const matchKey = Object.keys(attrs).find(
|
|
5339
|
+
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
5340
|
+
);
|
|
5341
|
+
if (matchKey) {
|
|
5342
|
+
attrDef = attrs[matchKey];
|
|
5062
5343
|
}
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
continue;
|
|
5344
|
+
}
|
|
5345
|
+
if (!attrDef) continue;
|
|
5346
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
5347
|
+
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
5348
|
+
payload[key] = null;
|
|
5069
5349
|
}
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5350
|
+
continue;
|
|
5351
|
+
}
|
|
5352
|
+
switch (attrDef.type) {
|
|
5353
|
+
case "integer":
|
|
5354
|
+
case "biginteger": {
|
|
5355
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
5356
|
+
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
5357
|
+
break;
|
|
5358
|
+
}
|
|
5359
|
+
case "float":
|
|
5360
|
+
case "decimal": {
|
|
5361
|
+
const parsedFloat = parseFloat(rawVal);
|
|
5362
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
5363
|
+
break;
|
|
5364
|
+
}
|
|
5365
|
+
case "boolean":
|
|
5366
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
5367
|
+
break;
|
|
5368
|
+
case "json":
|
|
5369
|
+
try {
|
|
5370
|
+
payload[key] = JSON.parse(rawVal);
|
|
5371
|
+
} catch {
|
|
5372
|
+
payload[key] = rawVal;
|
|
5076
5373
|
}
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5374
|
+
break;
|
|
5375
|
+
case "dynamiczone": {
|
|
5376
|
+
try {
|
|
5377
|
+
const parsed = JSON.parse(rawVal);
|
|
5378
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
5379
|
+
payload[key] = parsed;
|
|
5380
|
+
}
|
|
5381
|
+
} catch {
|
|
5082
5382
|
}
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5383
|
+
break;
|
|
5384
|
+
}
|
|
5385
|
+
case "component": {
|
|
5386
|
+
if (attrDef.repeatable) {
|
|
5087
5387
|
try {
|
|
5088
|
-
|
|
5388
|
+
const parsed = JSON.parse(rawVal);
|
|
5389
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
5390
|
+
payload[key] = parsed;
|
|
5391
|
+
}
|
|
5089
5392
|
} catch {
|
|
5090
|
-
payload[key] = rawVal;
|
|
5091
5393
|
}
|
|
5092
|
-
|
|
5093
|
-
case "dynamiczone": {
|
|
5394
|
+
} else {
|
|
5094
5395
|
try {
|
|
5095
5396
|
const parsed = JSON.parse(rawVal);
|
|
5096
|
-
if (
|
|
5397
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5097
5398
|
payload[key] = parsed;
|
|
5098
5399
|
}
|
|
5099
5400
|
} catch {
|
|
5100
5401
|
}
|
|
5101
|
-
break;
|
|
5102
5402
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
}
|
|
5112
|
-
} else {
|
|
5113
|
-
try {
|
|
5114
|
-
const parsed = JSON.parse(rawVal);
|
|
5115
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5116
|
-
payload[key] = parsed;
|
|
5117
|
-
}
|
|
5118
|
-
} catch {
|
|
5119
|
-
}
|
|
5403
|
+
break;
|
|
5404
|
+
}
|
|
5405
|
+
case "relation": {
|
|
5406
|
+
try {
|
|
5407
|
+
const parsed = JSON.parse(rawVal);
|
|
5408
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5409
|
+
payload[key] = parsed;
|
|
5410
|
+
break;
|
|
5120
5411
|
}
|
|
5121
|
-
|
|
5412
|
+
} catch {
|
|
5122
5413
|
}
|
|
5123
|
-
|
|
5414
|
+
if (isValidDocId(rawVal)) {
|
|
5415
|
+
payload[key] = rawVal;
|
|
5416
|
+
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5417
|
+
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5418
|
+
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5419
|
+
payload[key] = parts;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
break;
|
|
5423
|
+
}
|
|
5424
|
+
case "media": {
|
|
5425
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
5426
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
5427
|
+
} else {
|
|
5124
5428
|
try {
|
|
5125
5429
|
const parsed = JSON.parse(rawVal);
|
|
5126
|
-
if (
|
|
5430
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
5127
5431
|
payload[key] = parsed;
|
|
5128
|
-
break;
|
|
5129
5432
|
}
|
|
5130
5433
|
} catch {
|
|
5131
5434
|
}
|
|
5132
|
-
if (isValidDocId(rawVal)) {
|
|
5133
|
-
payload[key] = rawVal;
|
|
5134
|
-
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5135
|
-
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5136
|
-
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5137
|
-
payload[key] = parts;
|
|
5138
|
-
}
|
|
5139
|
-
}
|
|
5140
|
-
break;
|
|
5141
5435
|
}
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5436
|
+
break;
|
|
5437
|
+
}
|
|
5438
|
+
default:
|
|
5439
|
+
payload[key] = rawVal;
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
return payload;
|
|
5443
|
+
}
|
|
5444
|
+
function formatValueDisplay(val) {
|
|
5445
|
+
if (val === null || val === void 0) return "(empty)";
|
|
5446
|
+
if (typeof val === "object") {
|
|
5447
|
+
try {
|
|
5448
|
+
return JSON.stringify(val);
|
|
5449
|
+
} catch {
|
|
5450
|
+
return String(val);
|
|
5451
|
+
}
|
|
5452
|
+
}
|
|
5453
|
+
const s = String(val).trim();
|
|
5454
|
+
return s === "" ? "(empty)" : s;
|
|
5455
|
+
}
|
|
5456
|
+
async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
5457
|
+
const attrs = ct.attributes || {};
|
|
5458
|
+
const { mode = "auto", publish = true, publishStrategy = "safe", user } = options;
|
|
5459
|
+
const isDraftAndPublish = Boolean(ct.options?.draftAndPublish);
|
|
5460
|
+
const stats = {
|
|
5461
|
+
total: rows.length,
|
|
5462
|
+
created: 0,
|
|
5463
|
+
updated: 0,
|
|
5464
|
+
skipped: 0,
|
|
5465
|
+
newWithoutDocId: 0,
|
|
5466
|
+
errors: [],
|
|
5467
|
+
diffs: []
|
|
5468
|
+
};
|
|
5469
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5470
|
+
const rowNumber = startRowNumber + i;
|
|
5471
|
+
const row = rows[i];
|
|
5472
|
+
let docId = extractDocId(row);
|
|
5473
|
+
const rowLocale = extractLocale(row);
|
|
5474
|
+
if (ct.kind === "singleType" && !docId) {
|
|
5475
|
+
try {
|
|
5476
|
+
const findSingleArgs = {};
|
|
5477
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
5478
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
5479
|
+
if (singleDoc?.documentId) {
|
|
5480
|
+
docId = singleDoc.documentId;
|
|
5155
5481
|
}
|
|
5156
|
-
|
|
5157
|
-
payload[key] = rawVal;
|
|
5482
|
+
} catch {
|
|
5158
5483
|
}
|
|
5159
5484
|
}
|
|
5485
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
5160
5486
|
try {
|
|
5161
5487
|
if (mode === "update") {
|
|
5162
5488
|
if (!docId) {
|
|
@@ -5181,7 +5507,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5181
5507
|
const mergedPayload = { ...payload };
|
|
5182
5508
|
for (const key of Object.keys(payload)) {
|
|
5183
5509
|
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5184
|
-
mergedPayload[key] =
|
|
5510
|
+
mergedPayload[key] = deepMergeComponent(existing[key], payload[key]);
|
|
5185
5511
|
}
|
|
5186
5512
|
}
|
|
5187
5513
|
if (Object.keys(mergedPayload).length === 0) {
|
|
@@ -5206,7 +5532,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5206
5532
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5207
5533
|
await strapi.documents(uid).update(updateArgs);
|
|
5208
5534
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5209
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5535
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5210
5536
|
if (shouldPublish) {
|
|
5211
5537
|
try {
|
|
5212
5538
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5251,7 +5577,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5251
5577
|
const mergedPayload = { ...payload };
|
|
5252
5578
|
for (const key of Object.keys(payload)) {
|
|
5253
5579
|
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5254
|
-
mergedPayload[key] =
|
|
5580
|
+
mergedPayload[key] = deepMergeComponent(existing[key], payload[key]);
|
|
5255
5581
|
}
|
|
5256
5582
|
}
|
|
5257
5583
|
const updateArgs = {
|
|
@@ -5261,7 +5587,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5261
5587
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5262
5588
|
await strapi.documents(uid).update(updateArgs);
|
|
5263
5589
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5264
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5590
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5265
5591
|
if (shouldPublish) {
|
|
5266
5592
|
try {
|
|
5267
5593
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5302,7 +5628,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5302
5628
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5303
5629
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5304
5630
|
const newDocId = created?.documentId || docId;
|
|
5305
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5631
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5306
5632
|
if (shouldPublishNew && newDocId) {
|
|
5307
5633
|
try {
|
|
5308
5634
|
const pubArgs = { documentId: newDocId };
|
|
@@ -5348,7 +5674,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5348
5674
|
const mergedPayload = { ...payload };
|
|
5349
5675
|
for (const key of Object.keys(payload)) {
|
|
5350
5676
|
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5351
|
-
mergedPayload[key] =
|
|
5677
|
+
mergedPayload[key] = deepMergeComponent(existing[key], payload[key]);
|
|
5352
5678
|
}
|
|
5353
5679
|
}
|
|
5354
5680
|
const updateArgs = {
|
|
@@ -5358,7 +5684,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5358
5684
|
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5359
5685
|
await strapi.documents(uid).update(updateArgs);
|
|
5360
5686
|
const isApproved = isDraftAndPublish && Boolean(existing?.publishedAt) && existing?.status !== "modified";
|
|
5361
|
-
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : isApproved);
|
|
5687
|
+
const shouldPublish = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : isApproved);
|
|
5362
5688
|
if (shouldPublish) {
|
|
5363
5689
|
try {
|
|
5364
5690
|
const pubArgs = { documentId: resolvedDocId };
|
|
@@ -5399,7 +5725,7 @@ async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
|
5399
5725
|
if (rowLocale) createArgs.locale = rowLocale;
|
|
5400
5726
|
const created = await strapi.documents(uid).create(createArgs);
|
|
5401
5727
|
const newDocId = created?.documentId || docId;
|
|
5402
|
-
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" ? true : false);
|
|
5728
|
+
const shouldPublishNew = isDraftAndPublish && Boolean(publish) && (publishStrategy === "all" || options.unreviewedConsent ? true : false);
|
|
5403
5729
|
if (shouldPublishNew && newDocId) {
|
|
5404
5730
|
try {
|
|
5405
5731
|
const pubArgs = { documentId: newDocId };
|
|
@@ -6143,6 +6469,174 @@ const csvService = ({ strapi }) => ({
|
|
|
6143
6469
|
totalWarningCount: warnings.length
|
|
6144
6470
|
};
|
|
6145
6471
|
},
|
|
6472
|
+
/**
|
|
6473
|
+
* Previews CSV batch changes item-by-item without mutating the database.
|
|
6474
|
+
* Compares proposed CSV rows against existing documents to compute exact diffs for Maker-Checker review.
|
|
6475
|
+
*/
|
|
6476
|
+
async previewCsvBatch(uid, rows, options = {}) {
|
|
6477
|
+
const ct = strapi.contentTypes[uid];
|
|
6478
|
+
if (!ct) {
|
|
6479
|
+
throw new Error(`Content type "${uid}" not found`);
|
|
6480
|
+
}
|
|
6481
|
+
const attrs = ct.attributes || {};
|
|
6482
|
+
const { mode = "auto" } = options;
|
|
6483
|
+
const diffs = [];
|
|
6484
|
+
let willUpdateCount = 0;
|
|
6485
|
+
let willCreateCount = 0;
|
|
6486
|
+
let unchangedCount = 0;
|
|
6487
|
+
let skippedCount = 0;
|
|
6488
|
+
for (let i = 0; i < rows.length; i++) {
|
|
6489
|
+
const rowNumber = i + 2;
|
|
6490
|
+
const row = rows[i];
|
|
6491
|
+
let docId = extractDocId(row);
|
|
6492
|
+
const rowLocale = extractLocale(row);
|
|
6493
|
+
if (ct.kind === "singleType" && !docId) {
|
|
6494
|
+
try {
|
|
6495
|
+
const findSingleArgs = {};
|
|
6496
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
6497
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
6498
|
+
if (singleDoc?.documentId) {
|
|
6499
|
+
docId = singleDoc.documentId;
|
|
6500
|
+
}
|
|
6501
|
+
} catch {
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
let existing = null;
|
|
6505
|
+
if (docId) {
|
|
6506
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
6507
|
+
}
|
|
6508
|
+
if (mode === "update" && (!docId || !existing)) {
|
|
6509
|
+
skippedCount++;
|
|
6510
|
+
diffs.push({
|
|
6511
|
+
uid,
|
|
6512
|
+
collectionName: ct.info?.displayName || uid,
|
|
6513
|
+
documentId: docId || `(Row #${rowNumber})`,
|
|
6514
|
+
label: `Row #${rowNumber}`,
|
|
6515
|
+
attribute: "(record)",
|
|
6516
|
+
oldValue: null,
|
|
6517
|
+
newValue: null,
|
|
6518
|
+
oldDisplay: "—",
|
|
6519
|
+
newDisplay: "—",
|
|
6520
|
+
hasChange: false,
|
|
6521
|
+
status: "will_skip",
|
|
6522
|
+
action: "skip",
|
|
6523
|
+
message: !docId ? "Missing documentId in update mode" : `Document "${docId}" not found in database`
|
|
6524
|
+
});
|
|
6525
|
+
continue;
|
|
6526
|
+
}
|
|
6527
|
+
const payload = parseRowToPayload(strapi, attrs, row);
|
|
6528
|
+
if (existing) {
|
|
6529
|
+
const resolvedDocId = existing.documentId || docId;
|
|
6530
|
+
const entryLabel = existing.title || existing.name || existing.slug || `Entry #${resolvedDocId}`;
|
|
6531
|
+
let rowHasChanges = false;
|
|
6532
|
+
const payloadKeys = Object.keys(payload);
|
|
6533
|
+
if (payloadKeys.length === 0) {
|
|
6534
|
+
unchangedCount++;
|
|
6535
|
+
diffs.push({
|
|
6536
|
+
uid,
|
|
6537
|
+
collectionName: ct.info?.displayName || uid,
|
|
6538
|
+
documentId: resolvedDocId,
|
|
6539
|
+
label: entryLabel,
|
|
6540
|
+
attribute: "(no fields)",
|
|
6541
|
+
oldValue: null,
|
|
6542
|
+
newValue: null,
|
|
6543
|
+
oldDisplay: "—",
|
|
6544
|
+
newDisplay: "—",
|
|
6545
|
+
hasChange: false,
|
|
6546
|
+
status: "unchanged",
|
|
6547
|
+
action: "update",
|
|
6548
|
+
message: "No modified attributes in CSV row"
|
|
6549
|
+
});
|
|
6550
|
+
} else {
|
|
6551
|
+
for (const attrKey of payloadKeys) {
|
|
6552
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
6553
|
+
let newVal = payload[attrKey];
|
|
6554
|
+
if (attrs[attrKey]?.type === "component" && typeof newVal === "object" && newVal !== null) {
|
|
6555
|
+
newVal = deepMergeComponent(existing[attrKey], newVal);
|
|
6556
|
+
}
|
|
6557
|
+
const oldDisplay = formatValueDisplay(oldVal);
|
|
6558
|
+
const newDisplay = formatValueDisplay(newVal);
|
|
6559
|
+
const hasChange = String(oldVal ?? "") !== String(newVal ?? "");
|
|
6560
|
+
if (hasChange) {
|
|
6561
|
+
rowHasChanges = true;
|
|
6562
|
+
}
|
|
6563
|
+
diffs.push({
|
|
6564
|
+
uid,
|
|
6565
|
+
collectionName: ct.info?.displayName || uid,
|
|
6566
|
+
documentId: resolvedDocId,
|
|
6567
|
+
label: entryLabel,
|
|
6568
|
+
attribute: attrKey,
|
|
6569
|
+
oldValue: oldVal,
|
|
6570
|
+
newValue: newVal,
|
|
6571
|
+
oldDisplay,
|
|
6572
|
+
newDisplay,
|
|
6573
|
+
hasChange,
|
|
6574
|
+
status: hasChange ? "will_update" : "unchanged",
|
|
6575
|
+
action: "update",
|
|
6576
|
+
message: hasChange ? `Will update "${attrKey}" from CSV` : `Already matches CSV value`
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
if (rowHasChanges) {
|
|
6580
|
+
willUpdateCount++;
|
|
6581
|
+
} else {
|
|
6582
|
+
unchangedCount++;
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
} else {
|
|
6586
|
+
willCreateCount++;
|
|
6587
|
+
const resolvedDocId = docId || `(Auto-generated)`;
|
|
6588
|
+
const entryLabel = payload.title || payload.name || payload.slug || `New Entry (Row #${rowNumber})`;
|
|
6589
|
+
const payloadKeys = Object.keys(payload);
|
|
6590
|
+
if (payloadKeys.length === 0) {
|
|
6591
|
+
diffs.push({
|
|
6592
|
+
uid,
|
|
6593
|
+
collectionName: ct.info?.displayName || uid,
|
|
6594
|
+
documentId: resolvedDocId,
|
|
6595
|
+
label: entryLabel,
|
|
6596
|
+
attribute: "(all fields)",
|
|
6597
|
+
oldValue: null,
|
|
6598
|
+
newValue: null,
|
|
6599
|
+
oldDisplay: "(new entry)",
|
|
6600
|
+
newDisplay: "(empty record)",
|
|
6601
|
+
hasChange: true,
|
|
6602
|
+
status: "will_create",
|
|
6603
|
+
action: "create",
|
|
6604
|
+
message: "Will create new record"
|
|
6605
|
+
});
|
|
6606
|
+
} else {
|
|
6607
|
+
for (const attrKey of payloadKeys) {
|
|
6608
|
+
const newVal = payload[attrKey];
|
|
6609
|
+
diffs.push({
|
|
6610
|
+
uid,
|
|
6611
|
+
collectionName: ct.info?.displayName || uid,
|
|
6612
|
+
documentId: resolvedDocId,
|
|
6613
|
+
label: entryLabel,
|
|
6614
|
+
attribute: attrKey,
|
|
6615
|
+
oldValue: null,
|
|
6616
|
+
newValue: newVal,
|
|
6617
|
+
oldDisplay: "(new entry)",
|
|
6618
|
+
newDisplay: formatValueDisplay(newVal),
|
|
6619
|
+
hasChange: true,
|
|
6620
|
+
status: "will_create",
|
|
6621
|
+
action: "create",
|
|
6622
|
+
message: `Will create new record with attribute "${attrKey}"`
|
|
6623
|
+
});
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6628
|
+
return {
|
|
6629
|
+
success: true,
|
|
6630
|
+
summary: {
|
|
6631
|
+
total: rows.length,
|
|
6632
|
+
willUpdate: willUpdateCount,
|
|
6633
|
+
willCreate: willCreateCount,
|
|
6634
|
+
unchanged: unchangedCount,
|
|
6635
|
+
skipped: skippedCount
|
|
6636
|
+
},
|
|
6637
|
+
diffs
|
|
6638
|
+
};
|
|
6639
|
+
},
|
|
6146
6640
|
/**
|
|
6147
6641
|
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
6148
6642
|
*/
|