strapi-plugin-bulk-updater 1.3.11 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4157,6 +4157,99 @@ function parseCsv(csvText) {
4157
4157
  }
4158
4158
  return records;
4159
4159
  }
4160
+ const LIFECYCLE_STATUS_WORDS = /* @__PURE__ */ new Set([
4161
+ "pending review",
4162
+ "pending_review",
4163
+ "pending approval",
4164
+ "pending_approval",
4165
+ "approved",
4166
+ "rejected",
4167
+ "draft",
4168
+ "published",
4169
+ "modified",
4170
+ "supported",
4171
+ "ignored"
4172
+ ]);
4173
+ function isValidDocId(val) {
4174
+ if (!val || typeof val !== "string") return false;
4175
+ const trimmed = val.trim();
4176
+ if (!trimmed) return false;
4177
+ if (LIFECYCLE_STATUS_WORDS.has(trimmed.toLowerCase())) return false;
4178
+ if (/\s/.test(trimmed)) return false;
4179
+ return /^[a-zA-Z0-9_-]+$/.test(trimmed);
4180
+ }
4181
+ function extractDocId(row) {
4182
+ if (!row || typeof row !== "object") return "";
4183
+ const candidates = [
4184
+ row.documentId,
4185
+ row.document_id,
4186
+ row["Document ID"],
4187
+ row["Document Id"],
4188
+ row["document_id"],
4189
+ row.docId,
4190
+ row["docId"],
4191
+ row.id,
4192
+ row.ID,
4193
+ row.Id
4194
+ ];
4195
+ for (const c of candidates) {
4196
+ if (c !== void 0 && c !== null) {
4197
+ const str = String(c).trim();
4198
+ if (isValidDocId(str)) {
4199
+ return str;
4200
+ }
4201
+ }
4202
+ }
4203
+ return "";
4204
+ }
4205
+ function extractLocale(row) {
4206
+ if (!row || typeof row !== "object") return void 0;
4207
+ const candidates = [row.locale, row.Locale, row["locale"], row["Locale"]];
4208
+ for (const c of candidates) {
4209
+ if (c !== void 0 && c !== null) {
4210
+ const str = String(c).trim();
4211
+ if (str && str.toLowerCase() !== "null" && str.toLowerCase() !== "undefined") {
4212
+ return str;
4213
+ }
4214
+ }
4215
+ }
4216
+ return void 0;
4217
+ }
4218
+ function extractDetailedErrorMessage(err) {
4219
+ if (!err) return "Error processing row";
4220
+ const detailsErrors = err.details?.errors || err.error?.details?.errors;
4221
+ if (Array.isArray(detailsErrors) && detailsErrors.length > 0) {
4222
+ const messages = detailsErrors.map((e) => {
4223
+ if (!e) return "";
4224
+ const path = Array.isArray(e.path) ? e.path.join(".") : e.path;
4225
+ const msg = e.message || (typeof e === "string" ? e : JSON.stringify(e));
4226
+ return path ? `${path}: ${msg}` : msg;
4227
+ }).filter(Boolean);
4228
+ if (messages.length > 0) return messages.join("; ");
4229
+ }
4230
+ if (Array.isArray(err.inner) && err.inner.length > 0) {
4231
+ const messages = err.inner.map((e) => {
4232
+ if (!e) return "";
4233
+ const path = e.path;
4234
+ const msg = e.message || (typeof e === "string" ? e : JSON.stringify(e));
4235
+ return path ? `${path}: ${msg}` : msg;
4236
+ }).filter(Boolean);
4237
+ if (messages.length > 0) return messages.join("; ");
4238
+ }
4239
+ if (Array.isArray(err.errors) && err.errors.length > 0) {
4240
+ return err.errors.join("; ");
4241
+ }
4242
+ if (typeof err.details === "string" && err.details.trim()) {
4243
+ return err.details;
4244
+ }
4245
+ if (err.details?.message) {
4246
+ return err.details.message;
4247
+ }
4248
+ if (err.message && err.message !== "[object Object]") {
4249
+ return err.message;
4250
+ }
4251
+ return "Error processing row";
4252
+ }
4160
4253
  function discoverExportFields(ct, allComponents, maxDepth = 4) {
4161
4254
  const fields = [{ key: "documentId", label: "documentId", type: "string" }];
4162
4255
  const excluded = ["createdBy", "updatedBy", "localizations"];
@@ -4552,13 +4645,63 @@ const csvService = ({ strapi }) => ({
4552
4645
  const batchId = `csv_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
4553
4646
  const userEmail = user?.email || "admin@strapi.io";
4554
4647
  const userName = user ? `${user.firstname || ""} ${user.lastname || ""}`.trim() : "CSV Importer";
4648
+ const findExistingDocument = async (targetDocId, docLocale) => {
4649
+ if (!targetDocId) return null;
4650
+ try {
4651
+ const queryArgs = { documentId: targetDocId };
4652
+ if (docLocale) queryArgs.locale = docLocale;
4653
+ const found = await strapi.documents(uid).findOne(queryArgs);
4654
+ if (found) return found;
4655
+ } catch {
4656
+ }
4657
+ if (/^\d+$/.test(targetDocId)) {
4658
+ try {
4659
+ const queryArgs = {
4660
+ filters: { id: parseInt(targetDocId, 10) },
4661
+ limit: 1
4662
+ };
4663
+ if (docLocale) queryArgs.locale = docLocale;
4664
+ const list = await strapi.documents(uid).findMany(queryArgs);
4665
+ if (list && list.length > 0) return list[0];
4666
+ } catch {
4667
+ }
4668
+ }
4669
+ return null;
4670
+ };
4671
+ const SYSTEM_FIELD_KEYS = /* @__PURE__ */ new Set([
4672
+ "documentid",
4673
+ "document_id",
4674
+ "docid",
4675
+ "id",
4676
+ "createdat",
4677
+ "updatedat",
4678
+ "publishedat",
4679
+ "createdby",
4680
+ "updatedby",
4681
+ "localizations",
4682
+ "locale",
4683
+ "status"
4684
+ ]);
4555
4685
  for (let i = 0; i < parsedRows.length; i++) {
4556
4686
  const rowNumber = i + 2;
4557
4687
  const row = parsedRows[i];
4558
- const docId = (row.documentId || row.id || "").trim();
4688
+ let docId = extractDocId(row);
4689
+ const rowLocale = extractLocale(row);
4690
+ if (ct.kind === "singleType" && !docId) {
4691
+ try {
4692
+ const findSingleArgs = {};
4693
+ if (rowLocale) findSingleArgs.locale = rowLocale;
4694
+ const singleDoc = await strapi.documents(uid).findFirst(findSingleArgs);
4695
+ if (singleDoc?.documentId) {
4696
+ docId = singleDoc.documentId;
4697
+ }
4698
+ } catch {
4699
+ }
4700
+ }
4559
4701
  const payload = {};
4560
4702
  for (const [key, rawVal] of Object.entries(row)) {
4561
- if (key === "documentId" || key === "id") continue;
4703
+ const normalizedKey = key.toLowerCase().replace(/[\s_-]/g, "");
4704
+ if (SYSTEM_FIELD_KEYS.has(normalizedKey)) continue;
4562
4705
  if (key.includes(".")) {
4563
4706
  const [parent, child] = key.split(".");
4564
4707
  const parentDef = attrs[parent];
@@ -4571,32 +4714,54 @@ const csvService = ({ strapi }) => ({
4571
4714
  if (rawVal === "" || rawVal === null || rawVal === void 0) {
4572
4715
  val = null;
4573
4716
  } else if (childDef.type === "integer" || childDef.type === "biginteger") {
4574
- val = parseInt(rawVal, 10);
4717
+ const parsedInt = parseInt(rawVal, 10);
4718
+ val = isNaN(parsedInt) ? null : parsedInt;
4575
4719
  } else if (childDef.type === "float" || childDef.type === "decimal") {
4576
- val = parseFloat(rawVal);
4720
+ const parsedFloat = parseFloat(rawVal);
4721
+ val = isNaN(parsedFloat) ? null : parsedFloat;
4577
4722
  } else if (childDef.type === "boolean") {
4578
4723
  val = rawVal.toLowerCase() === "true" || rawVal === "1";
4724
+ } else if (childDef.type === "json") {
4725
+ try {
4726
+ val = JSON.parse(rawVal);
4727
+ } catch {
4728
+ val = rawVal;
4729
+ }
4579
4730
  }
4580
4731
  payload[parent][child] = val;
4581
4732
  }
4582
4733
  }
4583
4734
  continue;
4584
4735
  }
4585
- const attrDef = attrs[key];
4736
+ let attrDef = attrs[key];
4737
+ if (!attrDef) {
4738
+ const matchKey = Object.keys(attrs).find(
4739
+ (k) => k.toLowerCase() === key.toLowerCase() || k.toLowerCase() === key.toLowerCase().replace(/\s+/g, "")
4740
+ );
4741
+ if (matchKey) {
4742
+ attrDef = attrs[matchKey];
4743
+ }
4744
+ }
4586
4745
  if (!attrDef) continue;
4587
4746
  if (rawVal === "" || rawVal === null || rawVal === void 0) {
4588
- payload[key] = null;
4747
+ if (attrDef.type !== "dynamiczone" && !(attrDef.type === "component" && attrDef.repeatable)) {
4748
+ payload[key] = null;
4749
+ }
4589
4750
  continue;
4590
4751
  }
4591
4752
  switch (attrDef.type) {
4592
4753
  case "integer":
4593
- case "biginteger":
4594
- payload[key] = parseInt(rawVal, 10);
4754
+ case "biginteger": {
4755
+ const parsedInt = parseInt(rawVal, 10);
4756
+ payload[key] = isNaN(parsedInt) ? null : parsedInt;
4595
4757
  break;
4758
+ }
4596
4759
  case "float":
4597
- case "decimal":
4598
- payload[key] = parseFloat(rawVal);
4760
+ case "decimal": {
4761
+ const parsedFloat = parseFloat(rawVal);
4762
+ payload[key] = isNaN(parsedFloat) ? null : parsedFloat;
4599
4763
  break;
4764
+ }
4600
4765
  case "boolean":
4601
4766
  payload[key] = rawVal.toLowerCase() === "true" || rawVal === "1";
4602
4767
  break;
@@ -4607,6 +4772,69 @@ const csvService = ({ strapi }) => ({
4607
4772
  payload[key] = rawVal;
4608
4773
  }
4609
4774
  break;
4775
+ case "dynamiczone": {
4776
+ try {
4777
+ const parsed = JSON.parse(rawVal);
4778
+ if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object" && typeof item.__component === "string")) {
4779
+ payload[key] = parsed;
4780
+ }
4781
+ } catch {
4782
+ }
4783
+ break;
4784
+ }
4785
+ case "component": {
4786
+ if (attrDef.repeatable) {
4787
+ try {
4788
+ const parsed = JSON.parse(rawVal);
4789
+ if (Array.isArray(parsed) && parsed.every((item) => item && typeof item === "object")) {
4790
+ payload[key] = parsed;
4791
+ }
4792
+ } catch {
4793
+ }
4794
+ } else {
4795
+ try {
4796
+ const parsed = JSON.parse(rawVal);
4797
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
4798
+ payload[key] = parsed;
4799
+ }
4800
+ } catch {
4801
+ }
4802
+ }
4803
+ break;
4804
+ }
4805
+ case "relation": {
4806
+ try {
4807
+ const parsed = JSON.parse(rawVal);
4808
+ if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
4809
+ payload[key] = parsed;
4810
+ break;
4811
+ }
4812
+ } catch {
4813
+ }
4814
+ if (isValidDocId(rawVal)) {
4815
+ payload[key] = rawVal;
4816
+ } else if (rawVal.includes(";") || rawVal.includes(",")) {
4817
+ const parts = rawVal.split(/[;,]/).map((s) => s.trim()).filter(Boolean);
4818
+ if (parts.length > 0 && parts.every((p) => isValidDocId(p))) {
4819
+ payload[key] = parts;
4820
+ }
4821
+ }
4822
+ break;
4823
+ }
4824
+ case "media": {
4825
+ if (/^\d+$/.test(rawVal.trim())) {
4826
+ payload[key] = parseInt(rawVal.trim(), 10);
4827
+ } else {
4828
+ try {
4829
+ const parsed = JSON.parse(rawVal);
4830
+ if (typeof parsed === "number" || typeof parsed === "object") {
4831
+ payload[key] = parsed;
4832
+ }
4833
+ } catch {
4834
+ }
4835
+ }
4836
+ break;
4837
+ }
4610
4838
  default:
4611
4839
  payload[key] = rawVal;
4612
4840
  }
@@ -4617,114 +4845,146 @@ const csvService = ({ strapi }) => ({
4617
4845
  stats.skipped++;
4618
4846
  stats.errors.push({
4619
4847
  row: rowNumber,
4620
- error: "Missing documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
4848
+ error: "Missing or invalid documentId for Update mode. (Tip: Use Smart Auto-Detect mode to auto-add entries without document IDs)."
4621
4849
  });
4622
4850
  continue;
4623
4851
  }
4624
- const existing = await strapi.documents(uid).findOne({
4625
- documentId: docId
4626
- });
4852
+ const existing = await findExistingDocument(docId, rowLocale);
4627
4853
  if (!existing) {
4628
4854
  stats.skipped++;
4629
4855
  stats.errors.push({
4630
4856
  row: rowNumber,
4631
4857
  documentId: docId,
4632
- error: `Document with documentId "${docId}" not found.`
4858
+ error: `Document with ID "${docId}" not found in database.`
4633
4859
  });
4634
4860
  continue;
4635
4861
  }
4862
+ const resolvedDocId = existing.documentId || docId;
4636
4863
  const mergedPayload = { ...payload };
4637
4864
  for (const key of Object.keys(payload)) {
4638
4865
  if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4639
4866
  mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4640
4867
  }
4641
4868
  }
4642
- await strapi.documents(uid).update({
4643
- documentId: docId,
4869
+ if (Object.keys(mergedPayload).length === 0) {
4870
+ stats.skipped++;
4871
+ stats.diffs.push({
4872
+ uid,
4873
+ collectionName: ct.info?.displayName || uid,
4874
+ documentId: resolvedDocId,
4875
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
4876
+ attribute: "",
4877
+ oldValue: existing,
4878
+ newValue: {},
4879
+ status: "skipped",
4880
+ message: `Skipped (no editable attributes in row ${rowNumber})`
4881
+ });
4882
+ continue;
4883
+ }
4884
+ const updateArgs = {
4885
+ documentId: resolvedDocId,
4644
4886
  data: mergedPayload
4645
- });
4887
+ };
4888
+ if (rowLocale) updateArgs.locale = rowLocale;
4889
+ await strapi.documents(uid).update(updateArgs);
4646
4890
  if (publish && ct.options?.draftAndPublish) {
4647
4891
  try {
4648
- await strapi.documents(uid).publish({ documentId: docId });
4892
+ const pubArgs = { documentId: resolvedDocId };
4893
+ if (rowLocale) pubArgs.locale = rowLocale;
4894
+ await strapi.documents(uid).publish(pubArgs);
4649
4895
  } catch (pubErr) {
4650
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
4896
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
4651
4897
  }
4652
4898
  }
4653
4899
  try {
4654
4900
  await recordContentHistoryVersion(strapi, {
4655
4901
  uid,
4656
- documentId: docId,
4902
+ documentId: resolvedDocId,
4657
4903
  userId: user?.id,
4658
4904
  statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4659
4905
  });
4660
4906
  } catch {
4661
4907
  }
4662
4908
  stats.updated++;
4663
- stats.diffs.push({
4664
- uid,
4665
- collectionName: ct.info?.displayName || uid,
4666
- documentId: docId,
4667
- label: existing.title || existing.name || `Entry #${docId}`,
4668
- attribute: Object.keys(payload).join(", "),
4669
- oldValue: existing,
4670
- newValue: payload,
4671
- status: "updated",
4672
- message: `Updated via CSV import (row ${rowNumber})`
4673
- });
4909
+ for (const attrKey of Object.keys(payload)) {
4910
+ const oldVal = existing?.[attrKey] ?? null;
4911
+ const newVal = payload[attrKey];
4912
+ stats.diffs.push({
4913
+ uid,
4914
+ collectionName: ct.info?.displayName || uid,
4915
+ documentId: resolvedDocId,
4916
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
4917
+ attribute: attrKey,
4918
+ oldValue: oldVal,
4919
+ newValue: newVal,
4920
+ status: "updated",
4921
+ message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
4922
+ });
4923
+ }
4674
4924
  } else if (mode === "create_or_override") {
4925
+ let existing = null;
4675
4926
  if (docId) {
4676
- const existing = await strapi.documents(uid).findOne({
4677
- documentId: docId
4678
- });
4679
- if (existing) {
4680
- const mergedPayload = { ...payload };
4681
- for (const key of Object.keys(payload)) {
4682
- if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4683
- mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4684
- }
4685
- }
4686
- await strapi.documents(uid).update({
4687
- documentId: docId,
4688
- data: mergedPayload
4689
- });
4690
- if (publish && ct.options?.draftAndPublish) {
4691
- try {
4692
- await strapi.documents(uid).publish({ documentId: docId });
4693
- } catch (pubErr) {
4694
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
4695
- }
4927
+ existing = await findExistingDocument(docId, rowLocale);
4928
+ }
4929
+ if (existing) {
4930
+ const resolvedDocId = existing.documentId || docId;
4931
+ const mergedPayload = { ...payload };
4932
+ for (const key of Object.keys(payload)) {
4933
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4934
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4696
4935
  }
4936
+ }
4937
+ const updateArgs = {
4938
+ documentId: resolvedDocId,
4939
+ data: mergedPayload
4940
+ };
4941
+ if (rowLocale) updateArgs.locale = rowLocale;
4942
+ await strapi.documents(uid).update(updateArgs);
4943
+ if (publish && ct.options?.draftAndPublish) {
4697
4944
  try {
4698
- await recordContentHistoryVersion(strapi, {
4699
- uid,
4700
- documentId: docId,
4701
- userId: user?.id,
4702
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4703
- });
4704
- } catch {
4945
+ const pubArgs = { documentId: resolvedDocId };
4946
+ if (rowLocale) pubArgs.locale = rowLocale;
4947
+ await strapi.documents(uid).publish(pubArgs);
4948
+ } catch (pubErr) {
4949
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
4705
4950
  }
4706
- stats.updated++;
4951
+ }
4952
+ try {
4953
+ await recordContentHistoryVersion(strapi, {
4954
+ uid,
4955
+ documentId: resolvedDocId,
4956
+ userId: user?.id,
4957
+ statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4958
+ });
4959
+ } catch {
4960
+ }
4961
+ stats.updated++;
4962
+ for (const attrKey of Object.keys(payload)) {
4963
+ const oldVal = existing?.[attrKey] ?? null;
4964
+ const newVal = payload[attrKey];
4707
4965
  stats.diffs.push({
4708
4966
  uid,
4709
4967
  collectionName: ct.info?.displayName || uid,
4710
- documentId: docId,
4711
- label: existing.title || existing.name || `Entry #${docId}`,
4712
- attribute: Object.keys(payload).join(", "),
4713
- oldValue: existing,
4714
- newValue: payload,
4968
+ documentId: resolvedDocId,
4969
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
4970
+ attribute: attrKey,
4971
+ oldValue: oldVal,
4972
+ newValue: newVal,
4715
4973
  status: "updated",
4716
- message: `Overridden via CSV import (row ${rowNumber})`
4974
+ message: `Overridden "${attrKey}" via CSV import (row ${rowNumber})`
4717
4975
  });
4718
- continue;
4719
4976
  }
4977
+ continue;
4720
4978
  }
4721
- const created = await strapi.documents(uid).create({
4722
- data: payload
4723
- });
4979
+ const createArgs = { data: payload };
4980
+ if (rowLocale) createArgs.locale = rowLocale;
4981
+ const created = await strapi.documents(uid).create(createArgs);
4724
4982
  const newDocId = created?.documentId || docId;
4725
4983
  if (publish && ct.options?.draftAndPublish && newDocId) {
4726
4984
  try {
4727
- await strapi.documents(uid).publish({ documentId: newDocId });
4985
+ const pubArgs = { documentId: newDocId };
4986
+ if (rowLocale) pubArgs.locale = rowLocale;
4987
+ await strapi.documents(uid).publish(pubArgs);
4728
4988
  } catch (pubErr) {
4729
4989
  strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
4730
4990
  }
@@ -4742,71 +5002,83 @@ const csvService = ({ strapi }) => ({
4742
5002
  if (!docId) {
4743
5003
  stats.newWithoutDocId++;
4744
5004
  }
4745
- stats.diffs.push({
4746
- uid,
4747
- collectionName: ct.info?.displayName || uid,
4748
- documentId: newDocId,
4749
- label: created?.title || created?.name || `New Entry #${newDocId}`,
4750
- attribute: Object.keys(payload).join(", "),
4751
- oldValue: null,
4752
- newValue: payload,
4753
- status: "updated",
4754
- message: docId ? `Created via CSV import (row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
4755
- });
5005
+ for (const attrKey of Object.keys(payload)) {
5006
+ stats.diffs.push({
5007
+ uid,
5008
+ collectionName: ct.info?.displayName || uid,
5009
+ documentId: newDocId,
5010
+ label: created?.title || created?.name || `New Entry #${newDocId}`,
5011
+ attribute: attrKey,
5012
+ oldValue: null,
5013
+ newValue: payload[attrKey],
5014
+ status: "created",
5015
+ message: docId ? `Created "${attrKey}" via CSV import (row ${rowNumber})` : `Created "${attrKey}" as new entry (row ${rowNumber})`
5016
+ });
5017
+ }
4756
5018
  } else {
5019
+ let existing = null;
4757
5020
  if (docId) {
4758
- const existing = await strapi.documents(uid).findOne({
4759
- documentId: docId
4760
- });
4761
- if (existing) {
4762
- const mergedPayload = { ...payload };
4763
- for (const key of Object.keys(payload)) {
4764
- if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
4765
- mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4766
- }
4767
- }
4768
- await strapi.documents(uid).update({
4769
- documentId: docId,
4770
- data: mergedPayload
4771
- });
4772
- if (publish && ct.options?.draftAndPublish) {
4773
- try {
4774
- await strapi.documents(uid).publish({ documentId: docId });
4775
- } catch (pubErr) {
4776
- strapi.log.warn(`[bulk-updater] Auto-publish failed for ${docId}:`, pubErr);
4777
- }
5021
+ existing = await findExistingDocument(docId, rowLocale);
5022
+ }
5023
+ if (existing) {
5024
+ const resolvedDocId = existing.documentId || docId;
5025
+ const mergedPayload = { ...payload };
5026
+ for (const key of Object.keys(payload)) {
5027
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
5028
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
4778
5029
  }
5030
+ }
5031
+ const updateArgs = {
5032
+ documentId: resolvedDocId,
5033
+ data: mergedPayload
5034
+ };
5035
+ if (rowLocale) updateArgs.locale = rowLocale;
5036
+ await strapi.documents(uid).update(updateArgs);
5037
+ if (publish && ct.options?.draftAndPublish) {
4779
5038
  try {
4780
- await recordContentHistoryVersion(strapi, {
4781
- uid,
4782
- documentId: docId,
4783
- userId: user?.id,
4784
- statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
4785
- });
4786
- } catch {
5039
+ const pubArgs = { documentId: resolvedDocId };
5040
+ if (rowLocale) pubArgs.locale = rowLocale;
5041
+ await strapi.documents(uid).publish(pubArgs);
5042
+ } catch (pubErr) {
5043
+ strapi.log.warn(`[bulk-updater] Auto-publish failed for ${resolvedDocId}:`, pubErr);
4787
5044
  }
4788
- stats.updated++;
5045
+ }
5046
+ try {
5047
+ await recordContentHistoryVersion(strapi, {
5048
+ uid,
5049
+ documentId: resolvedDocId,
5050
+ userId: user?.id,
5051
+ statusOverride: publish && ct.options?.draftAndPublish ? "published" : void 0
5052
+ });
5053
+ } catch {
5054
+ }
5055
+ stats.updated++;
5056
+ for (const attrKey of Object.keys(payload)) {
5057
+ const oldVal = existing?.[attrKey] ?? null;
5058
+ const newVal = payload[attrKey];
4789
5059
  stats.diffs.push({
4790
5060
  uid,
4791
5061
  collectionName: ct.info?.displayName || uid,
4792
- documentId: docId,
4793
- label: existing.title || existing.name || `Entry #${docId}`,
4794
- attribute: Object.keys(payload).join(", "),
4795
- oldValue: existing,
4796
- newValue: payload,
5062
+ documentId: resolvedDocId,
5063
+ label: existing.title || existing.name || `Entry #${resolvedDocId}`,
5064
+ attribute: attrKey,
5065
+ oldValue: oldVal,
5066
+ newValue: newVal,
4797
5067
  status: "updated",
4798
- message: `Updated existing entry via smart match (row ${rowNumber})`
5068
+ message: `Updated "${attrKey}" via CSV import (row ${rowNumber})`
4799
5069
  });
4800
- continue;
4801
5070
  }
5071
+ continue;
4802
5072
  }
4803
- const created = await strapi.documents(uid).create({
4804
- data: payload
4805
- });
5073
+ const createArgs = { data: payload };
5074
+ if (rowLocale) createArgs.locale = rowLocale;
5075
+ const created = await strapi.documents(uid).create(createArgs);
4806
5076
  const newDocId = created?.documentId || docId;
4807
5077
  if (publish && ct.options?.draftAndPublish && newDocId) {
4808
5078
  try {
4809
- await strapi.documents(uid).publish({ documentId: newDocId });
5079
+ const pubArgs = { documentId: newDocId };
5080
+ if (rowLocale) pubArgs.locale = rowLocale;
5081
+ await strapi.documents(uid).publish(pubArgs);
4810
5082
  } catch (pubErr) {
4811
5083
  strapi.log.warn(`[bulk-updater] Auto-publish failed for ${newDocId}:`, pubErr);
4812
5084
  }
@@ -4824,23 +5096,25 @@ const csvService = ({ strapi }) => ({
4824
5096
  if (!docId) {
4825
5097
  stats.newWithoutDocId++;
4826
5098
  }
4827
- stats.diffs.push({
4828
- uid,
4829
- collectionName: ct.info?.displayName || uid,
4830
- documentId: newDocId,
4831
- label: created?.title || created?.name || `New Entry #${newDocId}`,
4832
- attribute: Object.keys(payload).join(", "),
4833
- oldValue: null,
4834
- newValue: payload,
4835
- status: "updated",
4836
- message: docId ? `Created as new entry (documentId "${docId}" not found in database - row ${rowNumber})` : `Created as new entry (no documentId provided in CSV - row ${rowNumber})`
4837
- });
5099
+ for (const attrKey of Object.keys(payload)) {
5100
+ stats.diffs.push({
5101
+ uid,
5102
+ collectionName: ct.info?.displayName || uid,
5103
+ documentId: newDocId,
5104
+ label: created?.title || created?.name || `New Entry #${newDocId}`,
5105
+ attribute: attrKey,
5106
+ oldValue: null,
5107
+ newValue: payload[attrKey],
5108
+ status: "created",
5109
+ 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})`
5110
+ });
5111
+ }
4838
5112
  }
4839
5113
  } catch (err) {
4840
5114
  stats.errors.push({
4841
5115
  row: rowNumber,
4842
5116
  documentId: docId || void 0,
4843
- error: err.message || "Error processing row"
5117
+ error: extractDetailedErrorMessage(err)
4844
5118
  });
4845
5119
  }
4846
5120
  }
@@ -4857,10 +5131,32 @@ const csvService = ({ strapi }) => ({
4857
5131
  if (auditService2) {
4858
5132
  const isEnabled = typeof auditService2.isAuditEnabled === "function" ? auditService2.isAuditEnabled() : Boolean(auditService2.getAuditConfig?.().enabled ?? true);
4859
5133
  if (isEnabled) {
5134
+ const collectionDisplayName = ct.info?.displayName || uid;
5135
+ const updatedDiffs = stats.diffs.filter(
5136
+ (d) => d.status === "updated" || d.status === "created"
5137
+ );
5138
+ const uniqueAttributes = Array.from(
5139
+ new Set(updatedDiffs.map((d) => d.attribute))
5140
+ ).filter(Boolean);
5141
+ const attrListStr = uniqueAttributes.length > 0 ? uniqueAttributes.join(", ") : "fields";
5142
+ const batchAttribute = `CSV [${collectionDisplayName}]: ${attrListStr}`;
5143
+ let batchValue = "";
5144
+ if (updatedDiffs.length === 1) {
5145
+ const d = updatedDiffs[0];
5146
+ const newStr = d.newValue !== null && d.newValue !== void 0 ? String(d.newValue) : "(empty)";
5147
+ batchValue = `"${d.label}": ${d.attribute} → "${newStr}"`;
5148
+ } else if (updatedDiffs.length > 1 && stats.updated === 1 && stats.created === 0) {
5149
+ const d = updatedDiffs[0];
5150
+ const changes = updatedDiffs.map((ud) => `${ud.attribute}: "${ud.newValue ?? ""}"`).join(", ");
5151
+ batchValue = `"${d.label}": ${changes}`;
5152
+ } else {
5153
+ const sampleVal = updatedDiffs[0] ? ` (e.g. ${updatedDiffs[0].attribute}: "${updatedDiffs[0].newValue ?? ""}")` : "";
5154
+ batchValue = `${stats.updated} updated, ${stats.created} created in ${collectionDisplayName}${sampleVal}`;
5155
+ }
4860
5156
  const batchSummary = {
4861
5157
  batchId,
4862
- attribute: "CSV_IMPORT",
4863
- value: `Mode: ${mode} (${stats.created} created, ${stats.updated} updated)`,
5158
+ attribute: batchAttribute,
5159
+ value: batchValue,
4864
5160
  updatedCount: stats.updated + stats.created,
4865
5161
  skippedCount: stats.skipped,
4866
5162
  totalTargets: stats.total