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