strapi-plugin-bulk-updater 1.4.3 → 1.4.6
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/dist/_chunks/{BulkUpdater-CbQk_OQG.mjs → BulkUpdater-CMkPdA9_.mjs} +942 -328
- package/dist/_chunks/BulkUpdater-CMkPdA9_.mjs.map +1 -0
- package/dist/_chunks/{BulkUpdater-mSvmiLwc.js → BulkUpdater-DtBH2c0-.js} +939 -325
- package/dist/_chunks/BulkUpdater-DtBH2c0-.js.map +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +1341 -629
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1341 -629
- package/dist/server/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/_chunks/BulkUpdater-CbQk_OQG.mjs.map +0 -1
- package/dist/_chunks/BulkUpdater-mSvmiLwc.js.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -214,6 +214,30 @@ const routes = {
|
|
|
214
214
|
config: {
|
|
215
215
|
policies: []
|
|
216
216
|
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
method: "POST",
|
|
220
|
+
path: "/csv/import-chunk",
|
|
221
|
+
handler: "csv.importChunk",
|
|
222
|
+
config: {
|
|
223
|
+
policies: []
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
method: "POST",
|
|
228
|
+
path: "/csv/validate-preflight",
|
|
229
|
+
handler: "csv.validatePreflight",
|
|
230
|
+
config: {
|
|
231
|
+
policies: []
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
method: "POST",
|
|
236
|
+
path: "/csv/submit-batch",
|
|
237
|
+
handler: "csv.submitBatch",
|
|
238
|
+
config: {
|
|
239
|
+
policies: []
|
|
240
|
+
}
|
|
217
241
|
}
|
|
218
242
|
]
|
|
219
243
|
}
|
|
@@ -666,6 +690,86 @@ const csvController = ({ strapi }) => ({
|
|
|
666
690
|
ctx.status = 500;
|
|
667
691
|
ctx.body = { success: false, error: err.message };
|
|
668
692
|
}
|
|
693
|
+
},
|
|
694
|
+
async importChunk(ctx) {
|
|
695
|
+
try {
|
|
696
|
+
const { uid, rows, batchId, isFirstChunk, isLastChunk, startRowNumber, mode, publish } = ctx.request.body;
|
|
697
|
+
if (!uid) {
|
|
698
|
+
ctx.status = 400;
|
|
699
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!Array.isArray(rows)) {
|
|
703
|
+
ctx.status = 400;
|
|
704
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
708
|
+
const result = await csvService2.importChunk(uid, rows, {
|
|
709
|
+
batchId: batchId ? String(batchId) : void 0,
|
|
710
|
+
isFirstChunk: Boolean(isFirstChunk),
|
|
711
|
+
isLastChunk: Boolean(isLastChunk),
|
|
712
|
+
startRowNumber: typeof startRowNumber === "number" ? startRowNumber : void 0,
|
|
713
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
|
|
714
|
+
publish: publish !== false,
|
|
715
|
+
user: ctx.state.user
|
|
716
|
+
});
|
|
717
|
+
ctx.body = result;
|
|
718
|
+
} catch (err) {
|
|
719
|
+
ctx.status = 500;
|
|
720
|
+
ctx.body = { success: false, error: err.message };
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
async validatePreflight(ctx) {
|
|
724
|
+
try {
|
|
725
|
+
const { uid, rows, mode } = ctx.request.body;
|
|
726
|
+
if (!uid) {
|
|
727
|
+
ctx.status = 400;
|
|
728
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!Array.isArray(rows)) {
|
|
732
|
+
ctx.status = 400;
|
|
733
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
737
|
+
const result = await csvService2.validatePreflight(uid, rows, {
|
|
738
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto"
|
|
739
|
+
});
|
|
740
|
+
ctx.body = result;
|
|
741
|
+
} catch (err) {
|
|
742
|
+
ctx.status = 500;
|
|
743
|
+
ctx.body = { success: false, error: err.message };
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
async submitBatch(ctx) {
|
|
747
|
+
try {
|
|
748
|
+
const { uid, rows, executionMode, makerNotes, forceImmediate, mode, publish } = ctx.request.body;
|
|
749
|
+
if (!uid) {
|
|
750
|
+
ctx.status = 400;
|
|
751
|
+
ctx.body = { success: false, error: "Target collection UID is required." };
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (!Array.isArray(rows)) {
|
|
755
|
+
ctx.status = 400;
|
|
756
|
+
ctx.body = { success: false, error: "Rows array is required." };
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
760
|
+
const result = await csvService2.submitBatch(uid, rows, {
|
|
761
|
+
executionMode,
|
|
762
|
+
makerNotes,
|
|
763
|
+
forceImmediate: Boolean(forceImmediate),
|
|
764
|
+
mode: mode === "update" ? "update" : mode === "create_or_override" ? "create_or_override" : "auto",
|
|
765
|
+
publish: publish !== false,
|
|
766
|
+
user: ctx.state.user
|
|
767
|
+
});
|
|
768
|
+
ctx.body = result;
|
|
769
|
+
} catch (err) {
|
|
770
|
+
ctx.status = 500;
|
|
771
|
+
ctx.body = { success: false, error: err.message };
|
|
772
|
+
}
|
|
669
773
|
}
|
|
670
774
|
});
|
|
671
775
|
const FIELDS_TO_IGNORE = [
|
|
@@ -948,13 +1052,15 @@ function getAllNestedValues(obj, path) {
|
|
|
948
1052
|
if (!item || typeof item !== "object") return;
|
|
949
1053
|
if (parsed.targetIndex !== void 0 && idx !== parsed.targetIndex) return;
|
|
950
1054
|
if (parsed.targetComponentUid && item.__component !== parsed.targetComponentUid) return;
|
|
1055
|
+
const compSchema = global.strapi?.components?.[item.__component];
|
|
1056
|
+
const fieldInSchema = tail.length > 0 && compSchema?.attributes ? tail[0] in compSchema.attributes : false;
|
|
951
1057
|
const subVal = tail.length > 0 ? getDeepProperty(item, tail.join(".")) : item;
|
|
952
|
-
if (subVal !== void 0) {
|
|
1058
|
+
if (subVal !== void 0 || fieldInSchema) {
|
|
953
1059
|
results.push({
|
|
954
1060
|
index: idx,
|
|
955
1061
|
componentUid: item.__component,
|
|
956
1062
|
componentName: formatComponentName(item.__component),
|
|
957
|
-
value: subVal,
|
|
1063
|
+
value: subVal !== void 0 ? subVal : null,
|
|
958
1064
|
path: `${head}[#${idx}${item.__component ? ` ${formatComponentName(item.__component)}` : ""}].${tail.join(".")}`
|
|
959
1065
|
});
|
|
960
1066
|
}
|
|
@@ -1053,7 +1159,9 @@ function setDeepProperty(obj, pathParts, value, options) {
|
|
|
1053
1159
|
if (options?.targetComponentUid) {
|
|
1054
1160
|
if (item.__component !== options.targetComponentUid) return item;
|
|
1055
1161
|
}
|
|
1056
|
-
|
|
1162
|
+
const compSchema = global.strapi?.components?.[item.__component];
|
|
1163
|
+
const fieldExistsInSchema = compSchema?.attributes ? head in compSchema.attributes : true;
|
|
1164
|
+
if (head in item || fieldExistsInSchema || tail.length === 0) {
|
|
1057
1165
|
return {
|
|
1058
1166
|
...item,
|
|
1059
1167
|
[head]: setDeepProperty(item[head], tail, value, options)
|
|
@@ -1167,6 +1275,12 @@ const pendingBatches = /* @__PURE__ */ new Map();
|
|
|
1167
1275
|
const batchJobsMap = /* @__PURE__ */ new Map();
|
|
1168
1276
|
let isRunnerRunning = false;
|
|
1169
1277
|
let runnerInterval = null;
|
|
1278
|
+
function registerPendingBatch(batch, jobState) {
|
|
1279
|
+
pendingBatches.set(batch.id, batch);
|
|
1280
|
+
if (jobState) {
|
|
1281
|
+
batchJobsMap.set(batch.id, jobState);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1170
1284
|
function invalidateCollectionCache(uid) {
|
|
1171
1285
|
modelsCache.delete(uid);
|
|
1172
1286
|
for (const key of queryCache.keys()) {
|
|
@@ -1789,6 +1903,22 @@ async function executePublishBatchInternal(strapi, {
|
|
|
1789
1903
|
results
|
|
1790
1904
|
};
|
|
1791
1905
|
}
|
|
1906
|
+
async function executeBatchByPayload(strapi, batch, currentUser) {
|
|
1907
|
+
if (batch.payload?.action === "csv_import") {
|
|
1908
|
+
const csvService2 = strapi.plugin("bulk-updater").service("csv");
|
|
1909
|
+
return await csvService2.executeCsvPendingBatch(
|
|
1910
|
+
batch.payload.uid,
|
|
1911
|
+
batch.payload.rows,
|
|
1912
|
+
batch.id,
|
|
1913
|
+
batch.payload.options,
|
|
1914
|
+
currentUser
|
|
1915
|
+
);
|
|
1916
|
+
} else if (batch.payload?.action === "publish") {
|
|
1917
|
+
return await executePublishBatchInternal(strapi, batch.payload, currentUser, batch.id);
|
|
1918
|
+
} else {
|
|
1919
|
+
return await executeBatchInternal(strapi, batch.payload, currentUser, batch.id);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1792
1922
|
const bulkUpdaterService = ({ strapi }) => {
|
|
1793
1923
|
if (!runnerInterval) {
|
|
1794
1924
|
runnerInterval = setInterval(async () => {
|
|
@@ -1800,7 +1930,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
1800
1930
|
for (const [id, batch] of pendingBatches.entries()) {
|
|
1801
1931
|
if (batch.status === "queued_offpeak" && !isBusinessHours) {
|
|
1802
1932
|
strapi.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
|
|
1803
|
-
const result =
|
|
1933
|
+
const result = await executeBatchByPayload(strapi, batch, batch.createdBy);
|
|
1804
1934
|
batch.status = "executed";
|
|
1805
1935
|
batch.executionResult = result;
|
|
1806
1936
|
}
|
|
@@ -2413,7 +2543,12 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2413
2543
|
for (const item of selectedEntries) {
|
|
2414
2544
|
if (!item.uid || !item.documentId) continue;
|
|
2415
2545
|
const key = `${item.uid}@@@${item.documentId}`;
|
|
2416
|
-
targetsMap.set(key, {
|
|
2546
|
+
targetsMap.set(key, {
|
|
2547
|
+
uid: item.uid,
|
|
2548
|
+
documentId: item.documentId,
|
|
2549
|
+
dynamicZoneIndexes: item.dynamicZoneIndexes,
|
|
2550
|
+
targetComponentUid: item.targetComponentUid
|
|
2551
|
+
});
|
|
2417
2552
|
}
|
|
2418
2553
|
if (targetsMap.size === 0) {
|
|
2419
2554
|
throw new Error("No collections or entries were selected for preview.");
|
|
@@ -2477,6 +2612,49 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
2477
2612
|
}
|
|
2478
2613
|
const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
|
|
2479
2614
|
if (isComponentNested) {
|
|
2615
|
+
const parsed = parseDynamicZonePath(attribute);
|
|
2616
|
+
const rootKey = parsed.rootKey;
|
|
2617
|
+
const isRootDz = contentType?.attributes?.[rootKey]?.type === "dynamiczone";
|
|
2618
|
+
const targetIndexes = target.dynamicZoneIndexes;
|
|
2619
|
+
const targetComponentUid = target.targetComponentUid || parsed.targetComponentUid;
|
|
2620
|
+
if (isRootDz && currentEntry?.[rootKey] && Array.isArray(currentEntry[rootKey])) {
|
|
2621
|
+
let affectedMatches = getAllNestedValues({ [rootKey]: currentEntry[rootKey] }, attribute);
|
|
2622
|
+
if (targetIndexes && targetIndexes.length > 0) {
|
|
2623
|
+
affectedMatches = affectedMatches.filter((m) => targetIndexes.includes(m.index));
|
|
2624
|
+
}
|
|
2625
|
+
if (targetComponentUid) {
|
|
2626
|
+
affectedMatches = affectedMatches.filter((m) => m.componentUid === targetComponentUid);
|
|
2627
|
+
}
|
|
2628
|
+
if (affectedMatches.length > 0) {
|
|
2629
|
+
for (const match of affectedMatches) {
|
|
2630
|
+
let blockNewVal = value;
|
|
2631
|
+
if (regexObj && typeof match.value === "string") {
|
|
2632
|
+
blockNewVal = match.value.replace(regexObj, value);
|
|
2633
|
+
}
|
|
2634
|
+
const hasChange3 = String(match.value ?? "") !== String(blockNewVal ?? "");
|
|
2635
|
+
if (hasChange3) {
|
|
2636
|
+
willUpdateCount++;
|
|
2637
|
+
} else {
|
|
2638
|
+
unchangedCount++;
|
|
2639
|
+
}
|
|
2640
|
+
diffs.push({
|
|
2641
|
+
uid: target.uid,
|
|
2642
|
+
collectionName: displayName,
|
|
2643
|
+
documentId: target.documentId,
|
|
2644
|
+
label: `${entryLabel} [Block #${match.index} (${match.componentName})]`,
|
|
2645
|
+
attribute: `${rootKey}[#${match.index} ${match.componentName}].${parsed.subPath}`,
|
|
2646
|
+
oldValue: match.value ?? null,
|
|
2647
|
+
newValue: blockNewVal,
|
|
2648
|
+
oldDisplay: match.value !== void 0 && match.value !== null && String(match.value).trim() !== "" ? String(match.value) : "(empty)",
|
|
2649
|
+
newDisplay: String(blockNewVal ?? ""),
|
|
2650
|
+
hasChange: hasChange3,
|
|
2651
|
+
status: hasChange3 ? "will_update" : "unchanged",
|
|
2652
|
+
message: hasChange3 ? "Block value will be updated" : "Block already has target value"
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2480
2658
|
const currentVal2 = getDeepProperty(currentEntry, attribute);
|
|
2481
2659
|
let newValue2 = value;
|
|
2482
2660
|
if (regexObj && typeof currentVal2 === "string") {
|
|
@@ -3056,7 +3234,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3056
3234
|
const { isBusinessHours } = checkBusinessHours(config);
|
|
3057
3235
|
if (options.executeImmediately || !isBusinessHours) {
|
|
3058
3236
|
batch.status = "approved";
|
|
3059
|
-
const result =
|
|
3237
|
+
const result = await executeBatchByPayload(strapi, batch, checkerUser);
|
|
3060
3238
|
batch.status = "executed";
|
|
3061
3239
|
batch.executionResult = result;
|
|
3062
3240
|
return {
|
|
@@ -3117,7 +3295,7 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3117
3295
|
if (batch.status === "executed") {
|
|
3118
3296
|
throw new Error(`Batch ${batchId} has already been executed.`);
|
|
3119
3297
|
}
|
|
3120
|
-
const result =
|
|
3298
|
+
const result = await executeBatchByPayload(strapi, batch, currentUser);
|
|
3121
3299
|
batch.status = "executed";
|
|
3122
3300
|
batch.executionResult = result;
|
|
3123
3301
|
return {
|
|
@@ -3364,6 +3542,20 @@ const bulkUpdaterService = ({ strapi }) => {
|
|
|
3364
3542
|
},
|
|
3365
3543
|
invalidateCollectionCache(uid) {
|
|
3366
3544
|
invalidateCollectionCache(uid);
|
|
3545
|
+
},
|
|
3546
|
+
registerPendingBatch(batch, jobState) {
|
|
3547
|
+
pendingBatches.set(batch.id, batch);
|
|
3548
|
+
if (jobState) {
|
|
3549
|
+
batchJobsMap.set(batch.id, jobState);
|
|
3550
|
+
}
|
|
3551
|
+
return batch;
|
|
3552
|
+
},
|
|
3553
|
+
getExecutionWindowConfig() {
|
|
3554
|
+
return getExecutionWindowConfig(strapi);
|
|
3555
|
+
},
|
|
3556
|
+
checkBusinessHours(config) {
|
|
3557
|
+
const cfg = config || getExecutionWindowConfig(strapi);
|
|
3558
|
+
return checkBusinessHours(cfg);
|
|
3367
3559
|
}
|
|
3368
3560
|
};
|
|
3369
3561
|
};
|
|
@@ -4570,92 +4762,645 @@ function getNestedExportValue(entry, path) {
|
|
|
4570
4762
|
}
|
|
4571
4763
|
return current;
|
|
4572
4764
|
}
|
|
4573
|
-
const
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
if (!ct) {
|
|
4580
|
-
throw new Error(`Content type ${uid} not found`);
|
|
4765
|
+
const csvImportSessions = /* @__PURE__ */ new Map();
|
|
4766
|
+
function cleanupStaleSessions() {
|
|
4767
|
+
const oneHourAgo = Date.now() - 36e5;
|
|
4768
|
+
for (const [id, session] of csvImportSessions.entries()) {
|
|
4769
|
+
if (session.createdAt < oneHourAgo) {
|
|
4770
|
+
csvImportSessions.delete(id);
|
|
4581
4771
|
}
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
const SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
|
|
4775
|
+
"documentid",
|
|
4776
|
+
"document_id",
|
|
4777
|
+
"docid",
|
|
4778
|
+
"id",
|
|
4779
|
+
"createdat",
|
|
4780
|
+
"updatedat",
|
|
4781
|
+
"publishedat",
|
|
4782
|
+
"createdby",
|
|
4783
|
+
"updatedby",
|
|
4784
|
+
"localizations",
|
|
4785
|
+
"locale",
|
|
4786
|
+
"status"
|
|
4787
|
+
]);
|
|
4788
|
+
async function findExistingDocument(strapi, uid, targetDocId, docLocale) {
|
|
4789
|
+
if (!targetDocId) return null;
|
|
4790
|
+
try {
|
|
4791
|
+
const queryArgs = { documentId: targetDocId };
|
|
4792
|
+
if (docLocale) queryArgs.locale = docLocale;
|
|
4793
|
+
const found = await strapi.documents(uid).findOne(queryArgs);
|
|
4794
|
+
if (found) return found;
|
|
4795
|
+
} catch {
|
|
4796
|
+
}
|
|
4797
|
+
if (/^\d+$/.test(targetDocId)) {
|
|
4798
|
+
try {
|
|
4799
|
+
const queryArgs = {
|
|
4800
|
+
filters: { id: parseInt(targetDocId, 10) },
|
|
4801
|
+
limit: 1
|
|
4802
|
+
};
|
|
4803
|
+
if (docLocale) queryArgs.locale = docLocale;
|
|
4804
|
+
const list = await strapi.documents(uid).findMany(queryArgs);
|
|
4805
|
+
if (list && list.length > 0) return list[0];
|
|
4806
|
+
} catch {
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
return null;
|
|
4810
|
+
}
|
|
4811
|
+
async function processCsvRows(strapi, ct, uid, rows, startRowNumber, options) {
|
|
4812
|
+
const attrs = ct.attributes || {};
|
|
4813
|
+
const { mode = "auto", publish = true, user } = options;
|
|
4814
|
+
const stats = {
|
|
4815
|
+
total: rows.length,
|
|
4816
|
+
created: 0,
|
|
4817
|
+
updated: 0,
|
|
4818
|
+
skipped: 0,
|
|
4819
|
+
newWithoutDocId: 0,
|
|
4820
|
+
errors: [],
|
|
4821
|
+
diffs: []
|
|
4822
|
+
};
|
|
4823
|
+
for (let i = 0; i < rows.length; i++) {
|
|
4824
|
+
const rowNumber = startRowNumber + i;
|
|
4825
|
+
const row = rows[i];
|
|
4826
|
+
let docId = extractDocId(row);
|
|
4827
|
+
const rowLocale = extractLocale(row);
|
|
4828
|
+
if (ct.kind === "singleType" && !docId) {
|
|
4829
|
+
try {
|
|
4830
|
+
const findSingleArgs = {};
|
|
4831
|
+
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
4832
|
+
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
4833
|
+
if (singleDoc?.documentId) {
|
|
4834
|
+
docId = singleDoc.documentId;
|
|
4835
|
+
}
|
|
4836
|
+
} catch {
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
const payload = {};
|
|
4840
|
+
for (const [key, rawVal] of Object.entries(row)) {
|
|
4841
|
+
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
4842
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
4843
|
+
if (key.includes(".")) {
|
|
4844
|
+
const [parent, child] = key.split(".");
|
|
4845
|
+
const parentDef = attrs[parent];
|
|
4846
|
+
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
4847
|
+
const comp = strapi.components[parentDef.component];
|
|
4848
|
+
const childDef = comp?.attributes?.[child];
|
|
4849
|
+
if (childDef) {
|
|
4850
|
+
if (!payload[parent]) payload[parent] = {};
|
|
4851
|
+
let val = rawVal;
|
|
4852
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4853
|
+
val = null;
|
|
4854
|
+
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
4855
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
4856
|
+
val = isNaN(parsedInt) ? null : parsedInt;
|
|
4857
|
+
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
4858
|
+
const parsedFloat = parseFloat(rawVal);
|
|
4859
|
+
val = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4860
|
+
} else if (childDef.type === "boolean") {
|
|
4861
|
+
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4862
|
+
} else if (childDef.type === "json") {
|
|
4863
|
+
try {
|
|
4864
|
+
val = JSON.parse(rawVal);
|
|
4865
|
+
} catch {
|
|
4866
|
+
val = rawVal;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
payload[parent][child] = val;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
let attrDef = attrs[key];
|
|
4875
|
+
if (!attrDef) {
|
|
4876
|
+
const matchKey = Object.keys(attrs).find(
|
|
4877
|
+
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
4878
|
+
);
|
|
4879
|
+
if (matchKey) {
|
|
4880
|
+
attrDef = attrs[matchKey];
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
if (!attrDef) continue;
|
|
4884
|
+
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4885
|
+
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
4886
|
+
payload[key] = null;
|
|
4887
|
+
}
|
|
4888
|
+
continue;
|
|
4889
|
+
}
|
|
4592
4890
|
switch (attrDef.type) {
|
|
4593
|
-
case "string":
|
|
4594
|
-
case "text":
|
|
4595
|
-
case "richtext":
|
|
4596
|
-
sampleVal = `Sample ${attrName}`;
|
|
4597
|
-
break;
|
|
4598
4891
|
case "integer":
|
|
4599
|
-
case "biginteger":
|
|
4892
|
+
case "biginteger": {
|
|
4893
|
+
const parsedInt = parseInt(rawVal, 10);
|
|
4894
|
+
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
4895
|
+
break;
|
|
4896
|
+
}
|
|
4600
4897
|
case "float":
|
|
4601
|
-
case "decimal":
|
|
4602
|
-
|
|
4898
|
+
case "decimal": {
|
|
4899
|
+
const parsedFloat = parseFloat(rawVal);
|
|
4900
|
+
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4603
4901
|
break;
|
|
4902
|
+
}
|
|
4604
4903
|
case "boolean":
|
|
4605
|
-
|
|
4904
|
+
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4606
4905
|
break;
|
|
4607
|
-
case "
|
|
4608
|
-
|
|
4906
|
+
case "json":
|
|
4907
|
+
try {
|
|
4908
|
+
payload[key] = JSON.parse(rawVal);
|
|
4909
|
+
} catch {
|
|
4910
|
+
payload[key] = rawVal;
|
|
4911
|
+
}
|
|
4609
4912
|
break;
|
|
4610
|
-
case "
|
|
4611
|
-
|
|
4913
|
+
case "dynamiczone": {
|
|
4914
|
+
try {
|
|
4915
|
+
const parsed = JSON.parse(rawVal);
|
|
4916
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
4917
|
+
payload[key] = parsed;
|
|
4918
|
+
}
|
|
4919
|
+
} catch {
|
|
4920
|
+
}
|
|
4612
4921
|
break;
|
|
4613
|
-
|
|
4614
|
-
|
|
4922
|
+
}
|
|
4923
|
+
case "component": {
|
|
4924
|
+
if (attrDef.repeatable) {
|
|
4925
|
+
try {
|
|
4926
|
+
const parsed = JSON.parse(rawVal);
|
|
4927
|
+
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
4928
|
+
payload[key] = parsed;
|
|
4929
|
+
}
|
|
4930
|
+
} catch {
|
|
4931
|
+
}
|
|
4932
|
+
} else {
|
|
4933
|
+
try {
|
|
4934
|
+
const parsed = JSON.parse(rawVal);
|
|
4935
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
4936
|
+
payload[key] = parsed;
|
|
4937
|
+
}
|
|
4938
|
+
} catch {
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4615
4941
|
break;
|
|
4942
|
+
}
|
|
4943
|
+
case "relation": {
|
|
4944
|
+
try {
|
|
4945
|
+
const parsed = JSON.parse(rawVal);
|
|
4946
|
+
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
4947
|
+
payload[key] = parsed;
|
|
4948
|
+
break;
|
|
4949
|
+
}
|
|
4950
|
+
} catch {
|
|
4951
|
+
}
|
|
4952
|
+
if (isValidDocId(rawVal)) {
|
|
4953
|
+
payload[key] = rawVal;
|
|
4954
|
+
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
4955
|
+
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
4956
|
+
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
4957
|
+
payload[key] = parts;
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
break;
|
|
4961
|
+
}
|
|
4962
|
+
case "media": {
|
|
4963
|
+
if (/^\d+$/.test(rawVal.trim())) {
|
|
4964
|
+
payload[key] = parseInt(rawVal.trim(), 10);
|
|
4965
|
+
} else {
|
|
4966
|
+
try {
|
|
4967
|
+
const parsed = JSON.parse(rawVal);
|
|
4968
|
+
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
4969
|
+
payload[key] = parsed;
|
|
4970
|
+
}
|
|
4971
|
+
} catch {
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
break;
|
|
4975
|
+
}
|
|
4616
4976
|
default:
|
|
4617
|
-
|
|
4977
|
+
payload[key] = rawVal;
|
|
4618
4978
|
}
|
|
4619
|
-
sampleRow.push(sampleVal);
|
|
4620
|
-
}
|
|
4621
|
-
const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
|
|
4622
|
-
return {
|
|
4623
|
-
filename: `${ct.info?.singularName || "template"}_template.csv`,
|
|
4624
|
-
csvContent,
|
|
4625
|
-
headers
|
|
4626
|
-
};
|
|
4627
|
-
},
|
|
4628
|
-
/**
|
|
4629
|
-
* Exports entries of a collection to CSV with deep nested component support,
|
|
4630
|
-
* RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
|
|
4631
|
-
*/
|
|
4632
|
-
async exportCollectionCsv(uid, options = {}) {
|
|
4633
|
-
const ct = strapi.contentTypes[uid];
|
|
4634
|
-
if (!ct) {
|
|
4635
|
-
throw new Error(`Content type ${uid} not found`);
|
|
4636
|
-
}
|
|
4637
|
-
const allComponents = strapi.components || {};
|
|
4638
|
-
const fields = discoverExportFields(ct, allComponents);
|
|
4639
|
-
const filters = {};
|
|
4640
|
-
const { attribute, filterOperator, filterQuery, search, documentIds } = options;
|
|
4641
|
-
if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
|
|
4642
|
-
filters.documentId = { $in: documentIds };
|
|
4643
4979
|
}
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4980
|
+
try {
|
|
4981
|
+
if (mode === "update") {
|
|
4982
|
+
if (!docId) {
|
|
4983
|
+
stats.skipped++;
|
|
4984
|
+
stats.errors.push({
|
|
4985
|
+
row: rowNumber,
|
|
4986
|
+
error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
|
|
4987
|
+
});
|
|
4988
|
+
continue;
|
|
4989
|
+
}
|
|
4990
|
+
const existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
4991
|
+
if (!existing) {
|
|
4992
|
+
stats.skipped++;
|
|
4993
|
+
stats.errors.push({
|
|
4994
|
+
row: rowNumber,
|
|
4995
|
+
documentId: docId,
|
|
4996
|
+
error: `Document with ID "${docId}" not found in database.`
|
|
4997
|
+
});
|
|
4998
|
+
continue;
|
|
4999
|
+
}
|
|
5000
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5001
|
+
const mergedPayload = { ...payload };
|
|
5002
|
+
for (const key of Object.keys(payload)) {
|
|
5003
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5004
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
if (Object.keys(mergedPayload).length === 0) {
|
|
5008
|
+
stats.skipped++;
|
|
5009
|
+
stats.diffs.push({
|
|
5010
|
+
uid,
|
|
5011
|
+
collectionName: ct.info?.displayName || uid,
|
|
5012
|
+
documentId: resolvedDocId,
|
|
5013
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5014
|
+
attribute: "",
|
|
5015
|
+
oldValue: existing,
|
|
5016
|
+
newValue: {},
|
|
5017
|
+
status: "skipped",
|
|
5018
|
+
message: `Skipped (no editable attributes in row ${rowNumber})`
|
|
5019
|
+
});
|
|
5020
|
+
continue;
|
|
5021
|
+
}
|
|
5022
|
+
const updateArgs = {
|
|
5023
|
+
documentId: resolvedDocId,
|
|
5024
|
+
data: mergedPayload
|
|
5025
|
+
};
|
|
5026
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5027
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5028
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5029
|
+
try {
|
|
5030
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5031
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5032
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5033
|
+
} catch (pubErr) {
|
|
5034
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
try {
|
|
5038
|
+
await recordContentHistoryVersion(strapi, {
|
|
5039
|
+
uid,
|
|
5040
|
+
documentId: resolvedDocId,
|
|
5041
|
+
userId: user?.id,
|
|
5042
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5043
|
+
});
|
|
5044
|
+
} catch {
|
|
5045
|
+
}
|
|
5046
|
+
stats.updated++;
|
|
5047
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5048
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5049
|
+
const newVal = payload[attrKey];
|
|
5050
|
+
stats.diffs.push({
|
|
5051
|
+
uid,
|
|
5052
|
+
collectionName: ct.info?.displayName || uid,
|
|
5053
|
+
documentId: resolvedDocId,
|
|
5054
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5055
|
+
attribute: attrKey,
|
|
5056
|
+
oldValue: oldVal,
|
|
5057
|
+
newValue: newVal,
|
|
5058
|
+
status: "updated",
|
|
5059
|
+
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5060
|
+
});
|
|
5061
|
+
}
|
|
5062
|
+
} else if (mode === "create_or_override") {
|
|
5063
|
+
let existing = null;
|
|
5064
|
+
if (docId) {
|
|
5065
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5066
|
+
}
|
|
5067
|
+
if (existing) {
|
|
5068
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5069
|
+
const mergedPayload = { ...payload };
|
|
5070
|
+
for (const key of Object.keys(payload)) {
|
|
5071
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5072
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
const updateArgs = {
|
|
5076
|
+
documentId: resolvedDocId,
|
|
5077
|
+
data: mergedPayload
|
|
5078
|
+
};
|
|
5079
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5080
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5081
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5082
|
+
try {
|
|
5083
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5084
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5085
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5086
|
+
} catch (pubErr) {
|
|
5087
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
try {
|
|
5091
|
+
await recordContentHistoryVersion(strapi, {
|
|
5092
|
+
uid,
|
|
5093
|
+
documentId: resolvedDocId,
|
|
5094
|
+
userId: user?.id,
|
|
5095
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5096
|
+
});
|
|
5097
|
+
} catch {
|
|
5098
|
+
}
|
|
5099
|
+
stats.updated++;
|
|
5100
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5101
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5102
|
+
const newVal = payload[attrKey];
|
|
5103
|
+
stats.diffs.push({
|
|
5104
|
+
uid,
|
|
5105
|
+
collectionName: ct.info?.displayName || uid,
|
|
5106
|
+
documentId: resolvedDocId,
|
|
5107
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5108
|
+
attribute: attrKey,
|
|
5109
|
+
oldValue: oldVal,
|
|
5110
|
+
newValue: newVal,
|
|
5111
|
+
status: "updated",
|
|
5112
|
+
message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
continue;
|
|
5116
|
+
}
|
|
5117
|
+
const createArgs = { data: payload };
|
|
5118
|
+
if (rowLocale) createArgs.locale = rowLocale;
|
|
5119
|
+
const created = await strapi.documents(uid).create(createArgs);
|
|
5120
|
+
const newDocId = created?.documentId || docId;
|
|
5121
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
5122
|
+
try {
|
|
5123
|
+
const pubArgs = { documentId: newDocId };
|
|
5124
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5125
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5126
|
+
} catch (pubErr) {
|
|
5127
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
try {
|
|
5131
|
+
await recordContentHistoryVersion(strapi, {
|
|
5132
|
+
uid,
|
|
5133
|
+
documentId: newDocId,
|
|
5134
|
+
userId: user?.id,
|
|
5135
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5136
|
+
});
|
|
5137
|
+
} catch {
|
|
5138
|
+
}
|
|
5139
|
+
stats.created++;
|
|
5140
|
+
if (!docId) {
|
|
5141
|
+
stats.newWithoutDocId++;
|
|
5142
|
+
}
|
|
5143
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5144
|
+
stats.diffs.push({
|
|
5145
|
+
uid,
|
|
5146
|
+
collectionName: ct.info?.displayName || uid,
|
|
5147
|
+
documentId: newDocId,
|
|
5148
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5149
|
+
attribute: attrKey,
|
|
5150
|
+
oldValue: null,
|
|
5151
|
+
newValue: payload[attrKey],
|
|
5152
|
+
status: "created",
|
|
5153
|
+
message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
|
|
5154
|
+
});
|
|
5155
|
+
}
|
|
5156
|
+
} else {
|
|
5157
|
+
let existing = null;
|
|
5158
|
+
if (docId) {
|
|
5159
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5160
|
+
}
|
|
5161
|
+
if (existing) {
|
|
5162
|
+
const resolvedDocId = existing.documentId || docId;
|
|
5163
|
+
const mergedPayload = { ...payload };
|
|
5164
|
+
for (const key of Object.keys(payload)) {
|
|
5165
|
+
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5166
|
+
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
const updateArgs = {
|
|
5170
|
+
documentId: resolvedDocId,
|
|
5171
|
+
data: mergedPayload
|
|
5172
|
+
};
|
|
5173
|
+
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5174
|
+
await strapi.documents(uid).update(updateArgs);
|
|
5175
|
+
if (publish && ct.options?.draftAndPublish) {
|
|
5176
|
+
try {
|
|
5177
|
+
const pubArgs = { documentId: resolvedDocId };
|
|
5178
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5179
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5180
|
+
} catch (pubErr) {
|
|
5181
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
try {
|
|
5185
|
+
await recordContentHistoryVersion(strapi, {
|
|
5186
|
+
uid,
|
|
5187
|
+
documentId: resolvedDocId,
|
|
5188
|
+
userId: user?.id,
|
|
5189
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5190
|
+
});
|
|
5191
|
+
} catch {
|
|
5192
|
+
}
|
|
5193
|
+
stats.updated++;
|
|
5194
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5195
|
+
const oldVal = existing?.[attrKey] ?? null;
|
|
5196
|
+
const newVal = payload[attrKey];
|
|
5197
|
+
stats.diffs.push({
|
|
5198
|
+
uid,
|
|
5199
|
+
collectionName: ct.info?.displayName || uid,
|
|
5200
|
+
documentId: resolvedDocId,
|
|
5201
|
+
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5202
|
+
attribute: attrKey,
|
|
5203
|
+
oldValue: oldVal,
|
|
5204
|
+
newValue: newVal,
|
|
5205
|
+
status: "updated",
|
|
5206
|
+
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
continue;
|
|
5210
|
+
}
|
|
5211
|
+
const createArgs = { data: payload };
|
|
5212
|
+
if (rowLocale) createArgs.locale = rowLocale;
|
|
5213
|
+
const created = await strapi.documents(uid).create(createArgs);
|
|
5214
|
+
const newDocId = created?.documentId || docId;
|
|
5215
|
+
if (publish && ct.options?.draftAndPublish && newDocId) {
|
|
5216
|
+
try {
|
|
5217
|
+
const pubArgs = { documentId: newDocId };
|
|
5218
|
+
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5219
|
+
await strapi.documents(uid).publish(pubArgs);
|
|
5220
|
+
} catch (pubErr) {
|
|
5221
|
+
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
try {
|
|
5225
|
+
await recordContentHistoryVersion(strapi, {
|
|
5226
|
+
uid,
|
|
5227
|
+
documentId: newDocId,
|
|
5228
|
+
userId: user?.id,
|
|
5229
|
+
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5230
|
+
});
|
|
5231
|
+
} catch {
|
|
5232
|
+
}
|
|
5233
|
+
stats.created++;
|
|
5234
|
+
if (!docId) {
|
|
5235
|
+
stats.newWithoutDocId++;
|
|
5236
|
+
}
|
|
5237
|
+
for (const attrKey of Object.keys(payload)) {
|
|
5238
|
+
stats.diffs.push({
|
|
5239
|
+
uid,
|
|
5240
|
+
collectionName: ct.info?.displayName || uid,
|
|
5241
|
+
documentId: newDocId,
|
|
5242
|
+
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5243
|
+
attribute: attrKey,
|
|
5244
|
+
oldValue: null,
|
|
5245
|
+
newValue: payload[attrKey],
|
|
5246
|
+
status: "created",
|
|
5247
|
+
message: docId ? `Created "${attrKey}" as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created "${attrKey}" as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
stats.errors.push({
|
|
5253
|
+
row: rowNumber,
|
|
5254
|
+
documentId: docId || void 0,
|
|
5255
|
+
error: extractDetailedErrorMessage(err)
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
return stats;
|
|
5260
|
+
}
|
|
5261
|
+
async function recordCsvAuditLog(strapi, ct, uid, batchId, stats, user) {
|
|
5262
|
+
if (stats.updated === 0 && stats.created === 0) return;
|
|
5263
|
+
try {
|
|
5264
|
+
const auditService2 = strapi.plugin("bulk-updater").service("audit");
|
|
5265
|
+
if (!auditService2) return;
|
|
5266
|
+
const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
|
|
5267
|
+
if (!isEnabled) return;
|
|
5268
|
+
const userEmail = user?.email || "admin@strapi.io";
|
|
5269
|
+
const userName = user ? `${user.firstname || ""} ${user.lastname || ""}`.trim() : "CSV Importer";
|
|
5270
|
+
const collectionDisplayName = ct.info?.displayName || uid;
|
|
5271
|
+
const updatedDiffs = stats.diffs.filter(
|
|
5272
|
+
(d) => d.status === "updated" || d.status === "created"
|
|
5273
|
+
);
|
|
5274
|
+
const uniqueAttributes = Array.from(
|
|
5275
|
+
new Set(updatedDiffs.map((d) => d.attribute))
|
|
5276
|
+
).filter(Boolean);
|
|
5277
|
+
const attrListStr = uniqueAttributes.length > 0 ? uniqueAttributes.join(", ") : "fields";
|
|
5278
|
+
const batchAttribute = `CSV [${collectionDisplayName}]: ${attrListStr}`;
|
|
5279
|
+
let batchValue = "";
|
|
5280
|
+
if (updatedDiffs.length === 1) {
|
|
5281
|
+
const d = updatedDiffs[0];
|
|
5282
|
+
const newStr = d.newValue !== null && d.newValue !== void 0 ? String(d.newValue) : "(empty)";
|
|
5283
|
+
batchValue = `"${d.label}": ${d.attribute} → "${newStr}"`;
|
|
5284
|
+
} else if (updatedDiffs.length > 1 && stats.updated === 1 && stats.created === 0) {
|
|
5285
|
+
const d = updatedDiffs[0];
|
|
5286
|
+
const changes = updatedDiffs.map((ud) => `${ud.attribute}: "${ud.newValue ?? ""}"`).join(", ");
|
|
5287
|
+
batchValue = `"${d.label}": ${changes}`;
|
|
5288
|
+
} else {
|
|
5289
|
+
const sampleVal = updatedDiffs[0] ? ` (e.g. ${updatedDiffs[0].attribute}: "${updatedDiffs[0].newValue ?? ""}")` : "";
|
|
5290
|
+
batchValue = `${stats.updated} updated, ${stats.created} created in ${collectionDisplayName}${sampleVal}`;
|
|
5291
|
+
}
|
|
5292
|
+
const batchSummary = {
|
|
5293
|
+
batchId,
|
|
5294
|
+
attribute: batchAttribute,
|
|
5295
|
+
value: batchValue,
|
|
5296
|
+
updatedCount: stats.updated + stats.created,
|
|
5297
|
+
skippedCount: stats.skipped,
|
|
5298
|
+
totalTargets: stats.total
|
|
5299
|
+
};
|
|
5300
|
+
const userObj = {
|
|
5301
|
+
email: userEmail || "admin@strapi.io",
|
|
5302
|
+
firstname: userName || "Admin"
|
|
5303
|
+
};
|
|
5304
|
+
if (typeof auditService2.recordBatch === "function") {
|
|
5305
|
+
await auditService2.recordBatch(batchSummary, stats.diffs, userObj);
|
|
5306
|
+
} else if (typeof auditService2.logOperation === "function") {
|
|
5307
|
+
await auditService2.logOperation({
|
|
5308
|
+
...batchSummary,
|
|
5309
|
+
userEmail,
|
|
5310
|
+
userName,
|
|
5311
|
+
diffs: stats.diffs
|
|
5312
|
+
});
|
|
5313
|
+
}
|
|
5314
|
+
} catch (auditErr) {
|
|
5315
|
+
strapi.log.warn("[bulk-updater] Failed logging CSV import audit:", auditErr);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
const csvService = ({ strapi }) => ({
|
|
5319
|
+
/**
|
|
5320
|
+
* Generates a sample template CSV with schema attribute headers and sample values
|
|
5321
|
+
*/
|
|
5322
|
+
async getTemplateCsv(uid) {
|
|
5323
|
+
const ct = strapi.contentTypes[uid];
|
|
5324
|
+
if (!ct) {
|
|
5325
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5326
|
+
}
|
|
5327
|
+
const attrs = ct.attributes || {};
|
|
5328
|
+
const excluded = ["createdAt", "updatedAt", "publishedAt", "createdBy", "updatedBy", "localizations"];
|
|
5329
|
+
const headers = ["documentId"];
|
|
5330
|
+
const sampleRow = ["(Leave empty to create new, or enter existing docId)"];
|
|
5331
|
+
for (const [attrName, attrDef] of Object.entries(attrs)) {
|
|
5332
|
+
if (excluded.includes(attrName)) continue;
|
|
5333
|
+
if (attrDef.type === "relation") continue;
|
|
5334
|
+
if (attrDef.type === "component" || attrDef.type === "dynamiczone") continue;
|
|
5335
|
+
headers.push(attrName);
|
|
5336
|
+
let sampleVal = "";
|
|
5337
|
+
switch (attrDef.type) {
|
|
5338
|
+
case "string":
|
|
5339
|
+
case "text":
|
|
5340
|
+
case "richtext":
|
|
5341
|
+
sampleVal = `Sample ${attrName}`;
|
|
5342
|
+
break;
|
|
5343
|
+
case "integer":
|
|
5344
|
+
case "biginteger":
|
|
5345
|
+
case "float":
|
|
5346
|
+
case "decimal":
|
|
5347
|
+
sampleVal = "10";
|
|
5348
|
+
break;
|
|
5349
|
+
case "boolean":
|
|
5350
|
+
sampleVal = "true";
|
|
5351
|
+
break;
|
|
5352
|
+
case "email":
|
|
5353
|
+
sampleVal = "sample@example.com";
|
|
5354
|
+
break;
|
|
5355
|
+
case "enumeration":
|
|
5356
|
+
sampleVal = attrDef.enum?.[0] || "option1";
|
|
5357
|
+
break;
|
|
5358
|
+
case "customField":
|
|
5359
|
+
sampleVal = "value";
|
|
5360
|
+
break;
|
|
5361
|
+
default:
|
|
5362
|
+
sampleVal = "sample";
|
|
5363
|
+
}
|
|
5364
|
+
sampleRow.push(sampleVal);
|
|
5365
|
+
}
|
|
5366
|
+
const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
|
|
5367
|
+
return {
|
|
5368
|
+
filename: `${ct.info?.singularName || "template"}_template.csv`,
|
|
5369
|
+
csvContent,
|
|
5370
|
+
headers
|
|
5371
|
+
};
|
|
5372
|
+
},
|
|
5373
|
+
/**
|
|
5374
|
+
* Exports entries of a collection to CSV with deep nested component support,
|
|
5375
|
+
* RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
|
|
5376
|
+
*/
|
|
5377
|
+
async exportCollectionCsv(uid, options = {}) {
|
|
5378
|
+
const ct = strapi.contentTypes[uid];
|
|
5379
|
+
if (!ct) {
|
|
5380
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5381
|
+
}
|
|
5382
|
+
const allComponents = strapi.components || {};
|
|
5383
|
+
const fields = discoverExportFields(ct, allComponents);
|
|
5384
|
+
const filters = {};
|
|
5385
|
+
const { attribute, filterOperator, filterQuery, search, documentIds } = options;
|
|
5386
|
+
if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
|
|
5387
|
+
filters.documentId = { $in: documentIds };
|
|
5388
|
+
}
|
|
5389
|
+
if (attribute && filterOperator && filterOperator !== "none") {
|
|
5390
|
+
const isNested = attribute.includes(".");
|
|
5391
|
+
if (filterOperator === "empty") {
|
|
5392
|
+
if (isNested) {
|
|
5393
|
+
const [parent, child] = attribute.split(".");
|
|
5394
|
+
filters[parent] = { [child]: { $null: true } };
|
|
5395
|
+
} else {
|
|
5396
|
+
filters[attribute] = { $null: true };
|
|
5397
|
+
}
|
|
5398
|
+
} else if (filterOperator === "not_empty") {
|
|
5399
|
+
if (isNested) {
|
|
5400
|
+
const [parent, child] = attribute.split(".");
|
|
5401
|
+
filters[parent] = { [child]: { $notNull: true } };
|
|
5402
|
+
} else {
|
|
5403
|
+
filters[attribute] = { $notNull: true };
|
|
4659
5404
|
}
|
|
4660
5405
|
} else if (filterQuery !== void 0 && filterQuery !== "") {
|
|
4661
5406
|
const opMap = {
|
|
@@ -4693,40 +5438,64 @@ const csvService = ({ strapi }) => ({
|
|
|
4693
5438
|
}
|
|
4694
5439
|
}
|
|
4695
5440
|
const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
|
|
4696
|
-
|
|
5441
|
+
const lines = [];
|
|
5442
|
+
lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
|
|
5443
|
+
let totalExported = 0;
|
|
5444
|
+
const maxLimit = options.limit && options.limit > 0 ? options.limit : Infinity;
|
|
5445
|
+
const CHUNK_SIZE = 1e3;
|
|
4697
5446
|
if (ct.kind === "singleType") {
|
|
4698
5447
|
const findFirstArgs = {};
|
|
4699
5448
|
if (deepPopulate) {
|
|
4700
5449
|
findFirstArgs.populate = deepPopulate;
|
|
4701
5450
|
}
|
|
4702
5451
|
const doc = await strapi.documents(uid).findFirst(findFirstArgs);
|
|
4703
|
-
|
|
5452
|
+
if (doc) {
|
|
5453
|
+
const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(doc, f.key)));
|
|
5454
|
+
lines.push(rowValues.join(","));
|
|
5455
|
+
totalExported = 1;
|
|
5456
|
+
}
|
|
4704
5457
|
} else {
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
5458
|
+
let currentPage = 1;
|
|
5459
|
+
let hasMore = true;
|
|
5460
|
+
while (hasMore && totalExported < maxLimit) {
|
|
5461
|
+
const remaining = maxLimit - totalExported;
|
|
5462
|
+
const currentBatchLimit = Math.min(CHUNK_SIZE, remaining);
|
|
5463
|
+
const findPageArgs = {
|
|
5464
|
+
filters: Object.keys(filters).length > 0 ? filters : void 0,
|
|
5465
|
+
page: currentPage,
|
|
5466
|
+
pageSize: currentBatchLimit,
|
|
5467
|
+
sort: { createdAt: "desc" }
|
|
5468
|
+
};
|
|
5469
|
+
if (deepPopulate) {
|
|
5470
|
+
findPageArgs.populate = deepPopulate;
|
|
5471
|
+
}
|
|
5472
|
+
const chunkEntries = await strapi.documents(uid).findMany(findPageArgs);
|
|
5473
|
+
if (!chunkEntries || chunkEntries.length === 0) {
|
|
5474
|
+
hasMore = false;
|
|
5475
|
+
break;
|
|
5476
|
+
}
|
|
5477
|
+
for (const entry of chunkEntries) {
|
|
5478
|
+
const rowValues = fields.map((f) => escapeCsvValue(getNestedExportValue(entry, f.key)));
|
|
5479
|
+
lines.push(rowValues.join(","));
|
|
5480
|
+
totalExported++;
|
|
5481
|
+
if (totalExported >= maxLimit) {
|
|
5482
|
+
hasMore = false;
|
|
5483
|
+
break;
|
|
5484
|
+
}
|
|
5485
|
+
}
|
|
5486
|
+
if (chunkEntries.length < currentBatchLimit) {
|
|
5487
|
+
hasMore = false;
|
|
5488
|
+
} else {
|
|
5489
|
+
currentPage++;
|
|
5490
|
+
}
|
|
4712
5491
|
}
|
|
4713
|
-
entries = await strapi.documents(uid).findMany(findManyArgs);
|
|
4714
|
-
}
|
|
4715
|
-
const lines = [];
|
|
4716
|
-
lines.push(fields.map((f) => escapeCsvValue(f.key)).join(","));
|
|
4717
|
-
for (const entry of entries || []) {
|
|
4718
|
-
const rowValues = fields.map((f) => {
|
|
4719
|
-
const val = getNestedExportValue(entry, f.key);
|
|
4720
|
-
return escapeCsvValue(val);
|
|
4721
|
-
});
|
|
4722
|
-
lines.push(rowValues.join(","));
|
|
4723
5492
|
}
|
|
4724
5493
|
const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
|
|
4725
5494
|
const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
4726
5495
|
return {
|
|
4727
5496
|
filename,
|
|
4728
5497
|
csvContent,
|
|
4729
|
-
count:
|
|
5498
|
+
count: totalExported,
|
|
4730
5499
|
fields: fields.map((f) => f.key)
|
|
4731
5500
|
};
|
|
4732
5501
|
},
|
|
@@ -4787,7 +5556,7 @@ const csvService = ({ strapi }) => ({
|
|
|
4787
5556
|
const uids = Object.keys(byUid);
|
|
4788
5557
|
if (uids.length === 1) {
|
|
4789
5558
|
const uid = uids[0];
|
|
4790
|
-
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
|
|
5559
|
+
return await this.exportCollectionCsv(uid, { documentIds: byUid[uid], limit: 0 });
|
|
4791
5560
|
}
|
|
4792
5561
|
const rows = [];
|
|
4793
5562
|
const headers = [
|
|
@@ -4838,7 +5607,77 @@ const csvService = ({ strapi }) => ({
|
|
|
4838
5607
|
};
|
|
4839
5608
|
},
|
|
4840
5609
|
/**
|
|
4841
|
-
* Imports
|
|
5610
|
+
* Imports a single chunk of CSV rows (for large dataset streaming/batching).
|
|
5611
|
+
* Aggregates stats across chunks and writes unified audit trail on the final chunk.
|
|
5612
|
+
*/
|
|
5613
|
+
async importChunk(uid, rows, options) {
|
|
5614
|
+
const ct = strapi.contentTypes[uid];
|
|
5615
|
+
if (!ct) {
|
|
5616
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5617
|
+
}
|
|
5618
|
+
cleanupStaleSessions();
|
|
5619
|
+
const batchId = options.batchId || `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5620
|
+
let session = csvImportSessions.get(batchId);
|
|
5621
|
+
if (!session || options.isFirstChunk) {
|
|
5622
|
+
session = {
|
|
5623
|
+
batchId,
|
|
5624
|
+
uid,
|
|
5625
|
+
createdAt: Date.now(),
|
|
5626
|
+
total: 0,
|
|
5627
|
+
created: 0,
|
|
5628
|
+
updated: 0,
|
|
5629
|
+
skipped: 0,
|
|
5630
|
+
newWithoutDocId: 0,
|
|
5631
|
+
errors: [],
|
|
5632
|
+
diffs: []
|
|
5633
|
+
};
|
|
5634
|
+
csvImportSessions.set(batchId, session);
|
|
5635
|
+
}
|
|
5636
|
+
const startRowNumber = options.startRowNumber || session.total + 2;
|
|
5637
|
+
const chunkStats = await processCsvRows(strapi, ct, uid, rows || [], startRowNumber, options);
|
|
5638
|
+
session.total += chunkStats.total;
|
|
5639
|
+
session.created += chunkStats.created;
|
|
5640
|
+
session.updated += chunkStats.updated;
|
|
5641
|
+
session.skipped += chunkStats.skipped;
|
|
5642
|
+
session.newWithoutDocId += chunkStats.newWithoutDocId;
|
|
5643
|
+
session.errors.push(...chunkStats.errors);
|
|
5644
|
+
session.diffs.push(...chunkStats.diffs);
|
|
5645
|
+
if (options.isLastChunk) {
|
|
5646
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, session, options.user);
|
|
5647
|
+
try {
|
|
5648
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5649
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5650
|
+
bulkService.invalidateCollectionCache(uid);
|
|
5651
|
+
}
|
|
5652
|
+
} catch {
|
|
5653
|
+
}
|
|
5654
|
+
const aggregateStats = {
|
|
5655
|
+
total: session.total,
|
|
5656
|
+
created: session.created,
|
|
5657
|
+
updated: session.updated,
|
|
5658
|
+
skipped: session.skipped,
|
|
5659
|
+
newWithoutDocId: session.newWithoutDocId,
|
|
5660
|
+
errors: session.errors,
|
|
5661
|
+
diffs: session.diffs
|
|
5662
|
+
};
|
|
5663
|
+
csvImportSessions.delete(batchId);
|
|
5664
|
+
return {
|
|
5665
|
+
success: true,
|
|
5666
|
+
batchId,
|
|
5667
|
+
chunkStats,
|
|
5668
|
+
stats: aggregateStats,
|
|
5669
|
+
isFinished: true
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
return {
|
|
5673
|
+
success: true,
|
|
5674
|
+
batchId,
|
|
5675
|
+
chunkStats,
|
|
5676
|
+
isFinished: false
|
|
5677
|
+
};
|
|
5678
|
+
},
|
|
5679
|
+
/**
|
|
5680
|
+
* Imports CSV records to update existing records or insert/override records (single payload).
|
|
4842
5681
|
*/
|
|
4843
5682
|
async importCsv(uid, csvText, options) {
|
|
4844
5683
|
const ct = strapi.contentTypes[uid];
|
|
@@ -4849,560 +5688,433 @@ const csvService = ({ strapi }) => ({
|
|
|
4849
5688
|
if (!parsedRows || parsedRows.length === 0) {
|
|
4850
5689
|
throw new Error("CSV is empty or could not be parsed.");
|
|
4851
5690
|
}
|
|
4852
|
-
const attrs = ct.attributes || {};
|
|
4853
|
-
const { mode = "auto", publish = true, user } = options;
|
|
4854
|
-
const stats = {
|
|
4855
|
-
total: parsedRows.length,
|
|
4856
|
-
created: 0,
|
|
4857
|
-
updated: 0,
|
|
4858
|
-
skipped: 0,
|
|
4859
|
-
newWithoutDocId: 0,
|
|
4860
|
-
errors: [],
|
|
4861
|
-
diffs: []
|
|
4862
|
-
};
|
|
4863
5691
|
const batchId = `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
4864
|
-
const
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
if (
|
|
4868
|
-
|
|
4869
|
-
const queryArgs = { documentId: targetDocId };
|
|
4870
|
-
if (docLocale) queryArgs.locale = docLocale;
|
|
4871
|
-
const found = await strapi.documents(uid).findOne(queryArgs);
|
|
4872
|
-
if (found) return found;
|
|
4873
|
-
} catch {
|
|
4874
|
-
}
|
|
4875
|
-
if (/^\d+$/.test(targetDocId)) {
|
|
4876
|
-
try {
|
|
4877
|
-
const queryArgs = {
|
|
4878
|
-
filters: { id: parseInt(targetDocId, 10) },
|
|
4879
|
-
limit: 1
|
|
4880
|
-
};
|
|
4881
|
-
if (docLocale) queryArgs.locale = docLocale;
|
|
4882
|
-
const list = await strapi.documents(uid).findMany(queryArgs);
|
|
4883
|
-
if (list && list.length > 0) return list[0];
|
|
4884
|
-
} catch {
|
|
4885
|
-
}
|
|
5692
|
+
const stats = await processCsvRows(strapi, ct, uid, parsedRows, 2, options);
|
|
5693
|
+
try {
|
|
5694
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5695
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5696
|
+
bulkService.invalidateCollectionCache(uid);
|
|
4886
5697
|
}
|
|
4887
|
-
|
|
5698
|
+
} catch {
|
|
5699
|
+
}
|
|
5700
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, stats, options.user);
|
|
5701
|
+
return {
|
|
5702
|
+
success: true,
|
|
5703
|
+
batchId,
|
|
5704
|
+
stats
|
|
4888
5705
|
};
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
const
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
if (ct.kind === "singleType" && !docId) {
|
|
4909
|
-
try {
|
|
4910
|
-
const findSingleArgs = {};
|
|
4911
|
-
if (rowLocale) findSingleArgs.locale = rowLocale;
|
|
4912
|
-
const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
|
|
4913
|
-
if (singleDoc?.documentId) {
|
|
4914
|
-
docId = singleDoc.documentId;
|
|
4915
|
-
}
|
|
4916
|
-
} catch {
|
|
4917
|
-
}
|
|
4918
|
-
}
|
|
4919
|
-
const payload = {};
|
|
4920
|
-
for (const [key, rawVal] of Object.entries(row)) {
|
|
4921
|
-
const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
|
|
4922
|
-
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
4923
|
-
if (key.includes(".")) {
|
|
4924
|
-
const [parent, child] = key.split(".");
|
|
4925
|
-
const parentDef = attrs[parent];
|
|
4926
|
-
if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
|
|
4927
|
-
const comp = strapi.components[parentDef.component];
|
|
4928
|
-
const childDef = comp?.attributes?.[child];
|
|
4929
|
-
if (childDef) {
|
|
4930
|
-
if (!payload[parent]) payload[parent] = {};
|
|
4931
|
-
let val = rawVal;
|
|
4932
|
-
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4933
|
-
val = null;
|
|
4934
|
-
} else if (childDef.type === "integer" || childDef.type === "biginteger") {
|
|
4935
|
-
const parsedInt = parseInt(rawVal, 10);
|
|
4936
|
-
val = isNaN(parsedInt) ? null : parsedInt;
|
|
4937
|
-
} else if (childDef.type === "float" || childDef.type === "decimal") {
|
|
4938
|
-
const parsedFloat = parseFloat(rawVal);
|
|
4939
|
-
val = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4940
|
-
} else if (childDef.type === "boolean") {
|
|
4941
|
-
val = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4942
|
-
} else if (childDef.type === "json") {
|
|
4943
|
-
try {
|
|
4944
|
-
val = JSON.parse(rawVal);
|
|
4945
|
-
} catch {
|
|
4946
|
-
val = rawVal;
|
|
4947
|
-
}
|
|
4948
|
-
}
|
|
4949
|
-
payload[parent][child] = val;
|
|
4950
|
-
}
|
|
4951
|
-
}
|
|
4952
|
-
continue;
|
|
4953
|
-
}
|
|
4954
|
-
let attrDef = attrs[key];
|
|
4955
|
-
if (!attrDef) {
|
|
4956
|
-
const matchKey = Object.keys(attrs).find(
|
|
4957
|
-
(k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
|
|
4958
|
-
);
|
|
4959
|
-
if (matchKey) {
|
|
4960
|
-
attrDef = attrs[matchKey];
|
|
4961
|
-
}
|
|
4962
|
-
}
|
|
4963
|
-
if (!attrDef) continue;
|
|
4964
|
-
if (rawVal === "" || rawVal === null || rawVal === void 0) {
|
|
4965
|
-
if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
|
|
4966
|
-
payload[key] = null;
|
|
4967
|
-
}
|
|
4968
|
-
continue;
|
|
4969
|
-
}
|
|
4970
|
-
switch (attrDef.type) {
|
|
4971
|
-
case "integer":
|
|
4972
|
-
case "biginteger": {
|
|
4973
|
-
const parsedInt = parseInt(rawVal, 10);
|
|
4974
|
-
payload[key] = isNaN(parsedInt) ? null : parsedInt;
|
|
4975
|
-
break;
|
|
4976
|
-
}
|
|
4977
|
-
case "float":
|
|
4978
|
-
case "decimal": {
|
|
4979
|
-
const parsedFloat = parseFloat(rawVal);
|
|
4980
|
-
payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
|
|
4981
|
-
break;
|
|
4982
|
-
}
|
|
4983
|
-
case "boolean":
|
|
4984
|
-
payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
|
|
4985
|
-
break;
|
|
4986
|
-
case "json":
|
|
4987
|
-
try {
|
|
4988
|
-
payload[key] = JSON.parse(rawVal);
|
|
4989
|
-
} catch {
|
|
4990
|
-
payload[key] = rawVal;
|
|
4991
|
-
}
|
|
4992
|
-
break;
|
|
4993
|
-
case "dynamiczone": {
|
|
4994
|
-
try {
|
|
4995
|
-
const parsed = JSON.parse(rawVal);
|
|
4996
|
-
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
|
|
4997
|
-
payload[key] = parsed;
|
|
4998
|
-
}
|
|
4999
|
-
} catch {
|
|
5000
|
-
}
|
|
5001
|
-
break;
|
|
5002
|
-
}
|
|
5003
|
-
case "component": {
|
|
5004
|
-
if (attrDef.repeatable) {
|
|
5005
|
-
try {
|
|
5006
|
-
const parsed = JSON.parse(rawVal);
|
|
5007
|
-
if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
|
|
5008
|
-
payload[key] = parsed;
|
|
5009
|
-
}
|
|
5010
|
-
} catch {
|
|
5011
|
-
}
|
|
5012
|
-
} else {
|
|
5013
|
-
try {
|
|
5014
|
-
const parsed = JSON.parse(rawVal);
|
|
5015
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
5016
|
-
payload[key] = parsed;
|
|
5017
|
-
}
|
|
5018
|
-
} catch {
|
|
5019
|
-
}
|
|
5020
|
-
}
|
|
5021
|
-
break;
|
|
5022
|
-
}
|
|
5023
|
-
case "relation": {
|
|
5024
|
-
try {
|
|
5025
|
-
const parsed = JSON.parse(rawVal);
|
|
5026
|
-
if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
|
|
5027
|
-
payload[key] = parsed;
|
|
5028
|
-
break;
|
|
5029
|
-
}
|
|
5030
|
-
} catch {
|
|
5031
|
-
}
|
|
5032
|
-
if (isValidDocId(rawVal)) {
|
|
5033
|
-
payload[key] = rawVal;
|
|
5034
|
-
} else if (rawVal.includes(";") || rawVal.includes(",")) {
|
|
5035
|
-
const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
|
|
5036
|
-
if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
|
|
5037
|
-
payload[key] = parts;
|
|
5038
|
-
}
|
|
5039
|
-
}
|
|
5040
|
-
break;
|
|
5041
|
-
}
|
|
5042
|
-
case "media": {
|
|
5043
|
-
if (/^\d+$/.test(rawVal.trim())) {
|
|
5044
|
-
payload[key] = parseInt(rawVal.trim(), 10);
|
|
5045
|
-
} else {
|
|
5046
|
-
try {
|
|
5047
|
-
const parsed = JSON.parse(rawVal);
|
|
5048
|
-
if (typeof parsed === "number" || typeof parsed === "object") {
|
|
5049
|
-
payload[key] = parsed;
|
|
5050
|
-
}
|
|
5051
|
-
} catch {
|
|
5052
|
-
}
|
|
5053
|
-
}
|
|
5054
|
-
break;
|
|
5055
|
-
}
|
|
5056
|
-
default:
|
|
5057
|
-
payload[key] = rawVal;
|
|
5058
|
-
}
|
|
5706
|
+
},
|
|
5707
|
+
/**
|
|
5708
|
+
* Executes a pending CSV batch approved by Checker or queued for off-peak execution
|
|
5709
|
+
*/
|
|
5710
|
+
async executeCsvPendingBatch(uid, rows, batchId, options = {}, checkerUser) {
|
|
5711
|
+
const ct = strapi.contentTypes[uid];
|
|
5712
|
+
if (!ct) {
|
|
5713
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5714
|
+
}
|
|
5715
|
+
const effectiveUser = checkerUser || options.user;
|
|
5716
|
+
const stats = await processCsvRows(strapi, ct, uid, rows || [], 2, {
|
|
5717
|
+
...options,
|
|
5718
|
+
user: effectiveUser
|
|
5719
|
+
});
|
|
5720
|
+
await recordCsvAuditLog(strapi, ct, uid, batchId, stats, effectiveUser);
|
|
5721
|
+
try {
|
|
5722
|
+
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5723
|
+
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5724
|
+
bulkService.invalidateCollectionCache(uid);
|
|
5059
5725
|
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5726
|
+
} catch {
|
|
5727
|
+
}
|
|
5728
|
+
return {
|
|
5729
|
+
success: true,
|
|
5730
|
+
batchId,
|
|
5731
|
+
stats
|
|
5732
|
+
};
|
|
5733
|
+
},
|
|
5734
|
+
/**
|
|
5735
|
+
* Pre-Flight Dry-Run Validation: Non-mutating analysis verifying document existence,
|
|
5736
|
+
* data types, schema integrity, and quota rule violations before execution.
|
|
5737
|
+
*/
|
|
5738
|
+
async validatePreflight(uid, rows, options = {}) {
|
|
5739
|
+
const ct = strapi.contentTypes[uid];
|
|
5740
|
+
if (!ct) {
|
|
5741
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5742
|
+
}
|
|
5743
|
+
const { mode = "auto" } = options;
|
|
5744
|
+
const attrs = ct.attributes || {};
|
|
5745
|
+
let validationRulesService2 = null;
|
|
5746
|
+
let activeRules = [];
|
|
5747
|
+
try {
|
|
5748
|
+
validationRulesService2 = strapi.plugin("bulk-updater").service("validationRules");
|
|
5749
|
+
if (validationRulesService2) {
|
|
5750
|
+
const rulesRes = await validationRulesService2.getRules();
|
|
5751
|
+
activeRules = (rulesRes || []).filter((r) => r.enabled);
|
|
5752
|
+
}
|
|
5753
|
+
} catch {
|
|
5754
|
+
}
|
|
5755
|
+
let validRows = 0;
|
|
5756
|
+
let estimatedUpdates = 0;
|
|
5757
|
+
let estimatedCreates = 0;
|
|
5758
|
+
const errors = [];
|
|
5759
|
+
const warnings = [];
|
|
5760
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5761
|
+
const rowNumber = i + 2;
|
|
5762
|
+
const row = rows[i];
|
|
5763
|
+
const docId = extractDocId(row);
|
|
5764
|
+
const rowLocale = extractLocale(row);
|
|
5765
|
+
let rowHasError = false;
|
|
5766
|
+
let existing = null;
|
|
5767
|
+
if (docId) {
|
|
5768
|
+
existing = await findExistingDocument(strapi, uid, docId, rowLocale);
|
|
5769
|
+
}
|
|
5770
|
+
if (mode === "update") {
|
|
5771
|
+
if (!docId) {
|
|
5772
|
+
errors.push({
|
|
5773
|
+
row: rowNumber,
|
|
5774
|
+
error: "Missing or invalid documentId for Update Only mode."
|
|
5775
|
+
});
|
|
5776
|
+
rowHasError = true;
|
|
5777
|
+
} else if (!existing) {
|
|
5778
|
+
errors.push({
|
|
5779
|
+
row: rowNumber,
|
|
5780
|
+
documentId: docId,
|
|
5781
|
+
error: `Document with ID "${docId}" not found in database.`
|
|
5782
|
+
});
|
|
5783
|
+
rowHasError = true;
|
|
5784
|
+
} else {
|
|
5785
|
+
estimatedUpdates++;
|
|
5786
|
+
}
|
|
5787
|
+
} else if (mode === "create_or_override") {
|
|
5788
|
+
if (existing) {
|
|
5789
|
+
estimatedUpdates++;
|
|
5790
|
+
} else {
|
|
5791
|
+
estimatedCreates++;
|
|
5792
|
+
}
|
|
5793
|
+
} else {
|
|
5794
|
+
if (existing) {
|
|
5795
|
+
estimatedUpdates++;
|
|
5796
|
+
} else {
|
|
5797
|
+
estimatedCreates++;
|
|
5798
|
+
if (docId) {
|
|
5799
|
+
warnings.push({
|
|
5074
5800
|
row: rowNumber,
|
|
5075
5801
|
documentId: docId,
|
|
5076
|
-
|
|
5077
|
-
});
|
|
5078
|
-
continue;
|
|
5079
|
-
}
|
|
5080
|
-
const resolvedDocId = existing.documentId || docId;
|
|
5081
|
-
const mergedPayload = { ...payload };
|
|
5082
|
-
for (const key of Object.keys(payload)) {
|
|
5083
|
-
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5084
|
-
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5085
|
-
}
|
|
5086
|
-
}
|
|
5087
|
-
if (Object.keys(mergedPayload).length === 0) {
|
|
5088
|
-
stats.skipped++;
|
|
5089
|
-
stats.diffs.push({
|
|
5090
|
-
uid,
|
|
5091
|
-
collectionName: ct.info?.displayName || uid,
|
|
5092
|
-
documentId: resolvedDocId,
|
|
5093
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5094
|
-
attribute: "",
|
|
5095
|
-
oldValue: existing,
|
|
5096
|
-
newValue: {},
|
|
5097
|
-
status: "skipped",
|
|
5098
|
-
message: `Skipped (no editable attributes in row ${rowNumber})`
|
|
5099
|
-
});
|
|
5100
|
-
continue;
|
|
5101
|
-
}
|
|
5102
|
-
const updateArgs = {
|
|
5103
|
-
documentId: resolvedDocId,
|
|
5104
|
-
data: mergedPayload
|
|
5105
|
-
};
|
|
5106
|
-
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5107
|
-
await strapi.documents(uid).update(updateArgs);
|
|
5108
|
-
if (publish && ct.options?.draftAndPublish) {
|
|
5109
|
-
try {
|
|
5110
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5111
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5112
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5113
|
-
} catch (pubErr) {
|
|
5114
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5115
|
-
}
|
|
5116
|
-
}
|
|
5117
|
-
try {
|
|
5118
|
-
await recordContentHistoryVersion(strapi, {
|
|
5119
|
-
uid,
|
|
5120
|
-
documentId: resolvedDocId,
|
|
5121
|
-
userId: user?.id,
|
|
5122
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5123
|
-
});
|
|
5124
|
-
} catch {
|
|
5125
|
-
}
|
|
5126
|
-
stats.updated++;
|
|
5127
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5128
|
-
const oldVal = existing?.[attrKey] ?? null;
|
|
5129
|
-
const newVal = payload[attrKey];
|
|
5130
|
-
stats.diffs.push({
|
|
5131
|
-
uid,
|
|
5132
|
-
collectionName: ct.info?.displayName || uid,
|
|
5133
|
-
documentId: resolvedDocId,
|
|
5134
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5135
|
-
attribute: attrKey,
|
|
5136
|
-
oldValue: oldVal,
|
|
5137
|
-
newValue: newVal,
|
|
5138
|
-
status: "updated",
|
|
5139
|
-
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5802
|
+
warning: `Document ID "${docId}" not found in DB; will create as a new entry.`
|
|
5140
5803
|
});
|
|
5141
5804
|
}
|
|
5142
|
-
}
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5164
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5165
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5166
|
-
} catch (pubErr) {
|
|
5167
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5170
|
-
try {
|
|
5171
|
-
await recordContentHistoryVersion(strapi, {
|
|
5172
|
-
uid,
|
|
5173
|
-
documentId: resolvedDocId,
|
|
5174
|
-
userId: user?.id,
|
|
5175
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
for (const [colKey, rawVal] of Object.entries(row)) {
|
|
5808
|
+
if (!rawVal || rawVal.trim() === "") continue;
|
|
5809
|
+
const normalizedKey = colKey.toLowerCase().replace(/[\s_-]/g, "");
|
|
5810
|
+
if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
|
|
5811
|
+
let attrDef = attrs[colKey];
|
|
5812
|
+
if (!attrDef && !colKey.includes(".")) {
|
|
5813
|
+
const matchKey = Object.keys(attrs).find(
|
|
5814
|
+
(k) => k.toLowerCase() === colKey.toLowerCase() || k.toLowerCase() === colKey.toLowerCase().replace(/\s+/g, "")
|
|
5815
|
+
);
|
|
5816
|
+
if (matchKey) attrDef = attrs[matchKey];
|
|
5817
|
+
}
|
|
5818
|
+
if (attrDef) {
|
|
5819
|
+
if (attrDef.type === "integer" || attrDef.type === "biginteger") {
|
|
5820
|
+
const num = parseInt(rawVal, 10);
|
|
5821
|
+
if (isNaN(num)) {
|
|
5822
|
+
errors.push({
|
|
5823
|
+
row: rowNumber,
|
|
5824
|
+
documentId: docId || void 0,
|
|
5825
|
+
error: `Column "${colKey}" expects an integer, received: "${rawVal}"`
|
|
5176
5826
|
});
|
|
5177
|
-
|
|
5827
|
+
rowHasError = true;
|
|
5178
5828
|
}
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
documentId: resolvedDocId,
|
|
5187
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5188
|
-
attribute: attrKey,
|
|
5189
|
-
oldValue: oldVal,
|
|
5190
|
-
newValue: newVal,
|
|
5191
|
-
status: "updated",
|
|
5192
|
-
message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5829
|
+
} else if (attrDef.type === "float" || attrDef.type === "decimal") {
|
|
5830
|
+
const num = parseFloat(rawVal);
|
|
5831
|
+
if (isNaN(num)) {
|
|
5832
|
+
errors.push({
|
|
5833
|
+
row: rowNumber,
|
|
5834
|
+
documentId: docId || void 0,
|
|
5835
|
+
error: `Column "${colKey}" expects a numeric float/decimal, received: "${rawVal}"`
|
|
5193
5836
|
});
|
|
5837
|
+
rowHasError = true;
|
|
5194
5838
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
try {
|
|
5203
|
-
const pubArgs = { documentId: newDocId };
|
|
5204
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5205
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5206
|
-
} catch (pubErr) {
|
|
5207
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
|
|
5208
|
-
}
|
|
5209
|
-
}
|
|
5210
|
-
try {
|
|
5211
|
-
await recordContentHistoryVersion(strapi, {
|
|
5212
|
-
uid,
|
|
5213
|
-
documentId: newDocId,
|
|
5214
|
-
userId: user?.id,
|
|
5215
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5216
|
-
});
|
|
5217
|
-
} catch {
|
|
5218
|
-
}
|
|
5219
|
-
stats.created++;
|
|
5220
|
-
if (!docId) {
|
|
5221
|
-
stats.newWithoutDocId++;
|
|
5222
|
-
}
|
|
5223
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5224
|
-
stats.diffs.push({
|
|
5225
|
-
uid,
|
|
5226
|
-
collectionName: ct.info?.displayName || uid,
|
|
5227
|
-
documentId: newDocId,
|
|
5228
|
-
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5229
|
-
attribute: attrKey,
|
|
5230
|
-
oldValue: null,
|
|
5231
|
-
newValue: payload[attrKey],
|
|
5232
|
-
status: "created",
|
|
5233
|
-
message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
|
|
5234
|
-
});
|
|
5235
|
-
}
|
|
5236
|
-
} else {
|
|
5237
|
-
let existing = null;
|
|
5238
|
-
if (docId) {
|
|
5239
|
-
existing = await findExistingDocument(docId, rowLocale);
|
|
5240
|
-
}
|
|
5241
|
-
if (existing) {
|
|
5242
|
-
const resolvedDocId = existing.documentId || docId;
|
|
5243
|
-
const mergedPayload = { ...payload };
|
|
5244
|
-
for (const key of Object.keys(payload)) {
|
|
5245
|
-
if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
|
|
5246
|
-
mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
|
|
5247
|
-
}
|
|
5248
|
-
}
|
|
5249
|
-
const updateArgs = {
|
|
5250
|
-
documentId: resolvedDocId,
|
|
5251
|
-
data: mergedPayload
|
|
5252
|
-
};
|
|
5253
|
-
if (rowLocale) updateArgs.locale = rowLocale;
|
|
5254
|
-
await strapi.documents(uid).update(updateArgs);
|
|
5255
|
-
if (publish && ct.options?.draftAndPublish) {
|
|
5256
|
-
try {
|
|
5257
|
-
const pubArgs = { documentId: resolvedDocId };
|
|
5258
|
-
if (rowLocale) pubArgs.locale = rowLocale;
|
|
5259
|
-
await strapi.documents(uid).publish(pubArgs);
|
|
5260
|
-
} catch (pubErr) {
|
|
5261
|
-
strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
|
|
5262
|
-
}
|
|
5839
|
+
} else if (attrDef.type === "enumeration" && Array.isArray(attrDef.enum)) {
|
|
5840
|
+
if (!attrDef.enum.includes(rawVal.trim())) {
|
|
5841
|
+
warnings.push({
|
|
5842
|
+
row: rowNumber,
|
|
5843
|
+
documentId: docId || void 0,
|
|
5844
|
+
warning: `Column "${colKey}" value "${rawVal}" is not in defined enum options: [${attrDef.enum.join(", ")}]`
|
|
5845
|
+
});
|
|
5263
5846
|
}
|
|
5847
|
+
} else if (attrDef.type === "json") {
|
|
5264
5848
|
try {
|
|
5265
|
-
|
|
5266
|
-
uid,
|
|
5267
|
-
documentId: resolvedDocId,
|
|
5268
|
-
userId: user?.id,
|
|
5269
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5270
|
-
});
|
|
5849
|
+
JSON.parse(rawVal);
|
|
5271
5850
|
} catch {
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
const newVal = payload[attrKey];
|
|
5277
|
-
stats.diffs.push({
|
|
5278
|
-
uid,
|
|
5279
|
-
collectionName: ct.info?.displayName || uid,
|
|
5280
|
-
documentId: resolvedDocId,
|
|
5281
|
-
label: existing.title || existing.name || `Entry #${resolvedDocId}`,
|
|
5282
|
-
attribute: attrKey,
|
|
5283
|
-
oldValue: oldVal,
|
|
5284
|
-
newValue: newVal,
|
|
5285
|
-
status: "updated",
|
|
5286
|
-
message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
|
|
5851
|
+
warnings.push({
|
|
5852
|
+
row: rowNumber,
|
|
5853
|
+
documentId: docId || void 0,
|
|
5854
|
+
warning: `Column "${colKey}" is type JSON but contains non-JSON text.`
|
|
5287
5855
|
});
|
|
5288
5856
|
}
|
|
5289
|
-
continue;
|
|
5290
5857
|
}
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
const
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
const
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5858
|
+
}
|
|
5859
|
+
if (activeRules.length > 0) {
|
|
5860
|
+
for (const rule of activeRules) {
|
|
5861
|
+
if (rule.collectionUid && rule.collectionUid !== "*" && rule.collectionUid !== uid) continue;
|
|
5862
|
+
if (rule.includeRichTextHtml && (rawVal.includes("<h") || rawVal.includes("# "))) {
|
|
5863
|
+
const headings = extractHeadingsFromRichText(rawVal);
|
|
5864
|
+
for (const [tag, count] of Object.entries(headings)) {
|
|
5865
|
+
const max = rule.quotas?.[tag.toLowerCase()];
|
|
5866
|
+
if (max !== void 0 && count > max) {
|
|
5867
|
+
warnings.push({
|
|
5868
|
+
row: rowNumber,
|
|
5869
|
+
documentId: docId || void 0,
|
|
5870
|
+
warning: `Rule "${rule.name}" quota warning: column "${colKey}" contains ${count} "${tag.toUpperCase()}" headings (maximum allowed: ${max}).`
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5302
5874
|
}
|
|
5303
5875
|
}
|
|
5304
|
-
try {
|
|
5305
|
-
await recordContentHistoryVersion(strapi, {
|
|
5306
|
-
uid,
|
|
5307
|
-
documentId: newDocId,
|
|
5308
|
-
userId: user?.id,
|
|
5309
|
-
statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
|
|
5310
|
-
});
|
|
5311
|
-
} catch {
|
|
5312
|
-
}
|
|
5313
|
-
stats.created++;
|
|
5314
|
-
if (!docId) {
|
|
5315
|
-
stats.newWithoutDocId++;
|
|
5316
|
-
}
|
|
5317
|
-
for (const attrKey of Object.keys(payload)) {
|
|
5318
|
-
stats.diffs.push({
|
|
5319
|
-
uid,
|
|
5320
|
-
collectionName: ct.info?.displayName || uid,
|
|
5321
|
-
documentId: newDocId,
|
|
5322
|
-
label: created?.title || created?.name || `New Entry #${newDocId}`,
|
|
5323
|
-
attribute: attrKey,
|
|
5324
|
-
oldValue: null,
|
|
5325
|
-
newValue: payload[attrKey],
|
|
5326
|
-
status: "created",
|
|
5327
|
-
message: docId ? `Created "${attrKey}" as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created "${attrKey}" as new entry (no documentId provided in CSV - row ${rowNumber})`
|
|
5328
|
-
});
|
|
5329
|
-
}
|
|
5330
5876
|
}
|
|
5331
|
-
} catch (err) {
|
|
5332
|
-
stats.errors.push({
|
|
5333
|
-
row: rowNumber,
|
|
5334
|
-
documentId: docId || void 0,
|
|
5335
|
-
error: extractDetailedErrorMessage(err)
|
|
5336
|
-
});
|
|
5337
5877
|
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
const bulkService = strapi.plugin("bulk-updater").service("bulkUpdater");
|
|
5341
|
-
if (bulkService && bulkService.invalidateCollectionCache) {
|
|
5342
|
-
bulkService.invalidateCollectionCache(uid);
|
|
5878
|
+
if (!rowHasError) {
|
|
5879
|
+
validRows++;
|
|
5343
5880
|
}
|
|
5344
|
-
} catch {
|
|
5345
5881
|
}
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5882
|
+
return {
|
|
5883
|
+
success: true,
|
|
5884
|
+
totalRows: rows.length,
|
|
5885
|
+
validRows,
|
|
5886
|
+
estimatedUpdates,
|
|
5887
|
+
estimatedCreates,
|
|
5888
|
+
hasErrors: errors.length > 0,
|
|
5889
|
+
hasWarnings: warnings.length > 0,
|
|
5890
|
+
errors: errors.slice(0, 100),
|
|
5891
|
+
warnings: warnings.slice(0, 100),
|
|
5892
|
+
totalErrorCount: errors.length,
|
|
5893
|
+
totalWarningCount: warnings.length
|
|
5894
|
+
};
|
|
5895
|
+
},
|
|
5896
|
+
/**
|
|
5897
|
+
* Submits a CSV batch with Guardrails: Maker-Checker (4-Eyes), Execution Windows, Off-Peak Scheduling
|
|
5898
|
+
*/
|
|
5899
|
+
async submitBatch(uid, rows, options = {}) {
|
|
5900
|
+
const ct = strapi.contentTypes[uid];
|
|
5901
|
+
if (!ct) {
|
|
5902
|
+
throw new Error(`Content type ${uid} not found`);
|
|
5903
|
+
}
|
|
5904
|
+
if (!rows || rows.length === 0) {
|
|
5905
|
+
throw new Error("No CSV records provided for batch submission.");
|
|
5906
|
+
}
|
|
5907
|
+
const {
|
|
5908
|
+
executionMode = "immediate",
|
|
5909
|
+
makerNotes = "",
|
|
5910
|
+
forceImmediate = false,
|
|
5911
|
+
mode = "auto",
|
|
5912
|
+
publish = true,
|
|
5913
|
+
user
|
|
5914
|
+
} = options;
|
|
5915
|
+
const executionConfig = getExecutionWindowConfig(strapi);
|
|
5916
|
+
const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
|
|
5917
|
+
const collectionDisplayName = ct.info?.displayName || uid;
|
|
5918
|
+
if (executionMode === "maker_checker") {
|
|
5919
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5920
|
+
const batch = {
|
|
5921
|
+
id: batchId,
|
|
5922
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5923
|
+
createdBy: user ? {
|
|
5924
|
+
id: user.id,
|
|
5925
|
+
email: user.email,
|
|
5926
|
+
firstname: user.firstname,
|
|
5927
|
+
lastname: user.lastname,
|
|
5928
|
+
username: user.username
|
|
5929
|
+
} : { email: "admin@system.local" },
|
|
5930
|
+
payload: {
|
|
5931
|
+
action: "csv_import",
|
|
5932
|
+
uid,
|
|
5933
|
+
rows,
|
|
5934
|
+
options: {
|
|
5935
|
+
mode,
|
|
5936
|
+
publish,
|
|
5937
|
+
user
|
|
5938
|
+
},
|
|
5939
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5940
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5941
|
+
publishOnApproval: publish
|
|
5942
|
+
},
|
|
5943
|
+
summary: {
|
|
5944
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5945
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5946
|
+
totalTargets: rows.length,
|
|
5947
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
5948
|
+
publishOnApproval: publish
|
|
5949
|
+
},
|
|
5950
|
+
status: "pending_approval",
|
|
5951
|
+
executionMode: "maker_checker",
|
|
5952
|
+
makerNotes
|
|
5953
|
+
};
|
|
5954
|
+
registerPendingBatch(batch, {
|
|
5955
|
+
id: batchId,
|
|
5956
|
+
createdAt: batch.createdAt,
|
|
5957
|
+
createdBy: batch.createdBy,
|
|
5958
|
+
status: "pending_approval",
|
|
5959
|
+
executionMode: "maker_checker",
|
|
5960
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
5961
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
5962
|
+
totalTargets: rows.length,
|
|
5963
|
+
processedCount: 0,
|
|
5964
|
+
updatedCount: 0,
|
|
5965
|
+
skippedCount: 0,
|
|
5966
|
+
failedCount: 0,
|
|
5967
|
+
isLive: false,
|
|
5968
|
+
publishOnApproval: publish,
|
|
5969
|
+
makerNotes,
|
|
5970
|
+
results: []
|
|
5971
|
+
});
|
|
5972
|
+
return {
|
|
5973
|
+
success: true,
|
|
5974
|
+
pending: true,
|
|
5975
|
+
batchId,
|
|
5976
|
+
status: "pending_approval",
|
|
5977
|
+
message: "CSV batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
|
|
5978
|
+
batch
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5981
|
+
if (executionMode === "queue_offpeak") {
|
|
5982
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
5983
|
+
const batch = {
|
|
5984
|
+
id: batchId,
|
|
5985
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5986
|
+
createdBy: user ? {
|
|
5987
|
+
id: user.id,
|
|
5988
|
+
email: user.email,
|
|
5989
|
+
firstname: user.firstname,
|
|
5990
|
+
lastname: user.lastname,
|
|
5991
|
+
username: user.username
|
|
5992
|
+
} : { email: "admin@system.local" },
|
|
5993
|
+
payload: {
|
|
5994
|
+
action: "csv_import",
|
|
5995
|
+
uid,
|
|
5996
|
+
rows,
|
|
5997
|
+
options: {
|
|
5998
|
+
mode,
|
|
5999
|
+
publish,
|
|
6000
|
+
user
|
|
6001
|
+
},
|
|
6002
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6003
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6004
|
+
publishOnApproval: publish
|
|
6005
|
+
},
|
|
6006
|
+
summary: {
|
|
6007
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6008
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6009
|
+
totalTargets: rows.length,
|
|
6010
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
6011
|
+
publishOnApproval: publish
|
|
6012
|
+
},
|
|
6013
|
+
status: "queued_offpeak",
|
|
6014
|
+
executionMode: "queue_offpeak",
|
|
6015
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6016
|
+
makerNotes
|
|
6017
|
+
};
|
|
6018
|
+
registerPendingBatch(batch, {
|
|
6019
|
+
id: batchId,
|
|
6020
|
+
createdAt: batch.createdAt,
|
|
6021
|
+
createdBy: batch.createdBy,
|
|
6022
|
+
status: "queued_offpeak",
|
|
6023
|
+
executionMode: "queue_offpeak",
|
|
6024
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6025
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6026
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6027
|
+
totalTargets: rows.length,
|
|
6028
|
+
processedCount: 0,
|
|
6029
|
+
updatedCount: 0,
|
|
6030
|
+
skippedCount: 0,
|
|
6031
|
+
failedCount: 0,
|
|
6032
|
+
isLive: false,
|
|
6033
|
+
publishOnApproval: publish,
|
|
6034
|
+
makerNotes,
|
|
6035
|
+
results: []
|
|
6036
|
+
});
|
|
6037
|
+
return {
|
|
6038
|
+
success: true,
|
|
6039
|
+
pending: true,
|
|
6040
|
+
batchId,
|
|
6041
|
+
status: "queued_offpeak",
|
|
6042
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6043
|
+
message: `CSV batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
|
|
6044
|
+
batch
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
|
|
6048
|
+
const batchId = `csv_batch_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
6049
|
+
const batch = {
|
|
6050
|
+
id: batchId,
|
|
6051
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6052
|
+
createdBy: user ? {
|
|
6053
|
+
id: user.id,
|
|
6054
|
+
email: user.email,
|
|
6055
|
+
firstname: user.firstname,
|
|
6056
|
+
lastname: user.lastname,
|
|
6057
|
+
username: user.username
|
|
6058
|
+
} : { email: "admin@system.local" },
|
|
6059
|
+
payload: {
|
|
6060
|
+
action: "csv_import",
|
|
6061
|
+
uid,
|
|
6062
|
+
rows,
|
|
6063
|
+
options: {
|
|
6064
|
+
mode,
|
|
6065
|
+
publish,
|
|
6066
|
+
user
|
|
6067
|
+
},
|
|
6068
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6069
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6070
|
+
publishOnApproval: publish
|
|
6071
|
+
},
|
|
6072
|
+
summary: {
|
|
6073
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6074
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6075
|
+
totalTargets: rows.length,
|
|
6076
|
+
collectionBreakdown: { [collectionDisplayName]: rows.length },
|
|
6077
|
+
publishOnApproval: publish
|
|
6078
|
+
},
|
|
6079
|
+
status: "queued_offpeak",
|
|
6080
|
+
executionMode: "queue_offpeak",
|
|
6081
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6082
|
+
makerNotes: "Automatically queued due to peak business hours restriction."
|
|
6083
|
+
};
|
|
6084
|
+
registerPendingBatch(batch, {
|
|
6085
|
+
id: batchId,
|
|
6086
|
+
createdAt: batch.createdAt,
|
|
6087
|
+
createdBy: batch.createdBy,
|
|
6088
|
+
status: "queued_offpeak",
|
|
6089
|
+
executionMode: "queue_offpeak",
|
|
6090
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6091
|
+
attribute: `CSV Import: ${collectionDisplayName}`,
|
|
6092
|
+
value: `${rows.length} rows (${mode} mode)`,
|
|
6093
|
+
totalTargets: rows.length,
|
|
6094
|
+
processedCount: 0,
|
|
6095
|
+
updatedCount: 0,
|
|
6096
|
+
skippedCount: 0,
|
|
6097
|
+
failedCount: 0,
|
|
6098
|
+
isLive: false,
|
|
6099
|
+
publishOnApproval: publish,
|
|
6100
|
+
policyMessage,
|
|
6101
|
+
makerNotes: "Automatically queued due to peak business hours restriction.",
|
|
6102
|
+
results: []
|
|
6103
|
+
});
|
|
6104
|
+
return {
|
|
6105
|
+
success: true,
|
|
6106
|
+
pending: true,
|
|
6107
|
+
batchId,
|
|
6108
|
+
status: "queued_offpeak",
|
|
6109
|
+
scheduledFor: executionConfig.offPeakStartTime,
|
|
6110
|
+
message: policyMessage,
|
|
6111
|
+
batch
|
|
6112
|
+
};
|
|
5401
6113
|
}
|
|
5402
6114
|
return {
|
|
5403
6115
|
success: true,
|
|
5404
|
-
|
|
5405
|
-
|
|
6116
|
+
pending: false,
|
|
6117
|
+
executeNow: true
|
|
5406
6118
|
};
|
|
5407
6119
|
}
|
|
5408
6120
|
});
|