strapi-plugin-bulk-updater 1.2.12 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -203,6 +203,22 @@ const routes = {
203
203
  policies: []
204
204
  }
205
205
  },
206
+ {
207
+ method: "POST",
208
+ path: "/csv/export/:uid",
209
+ handler: "csv.exportCollection",
210
+ config: {
211
+ policies: []
212
+ }
213
+ },
214
+ {
215
+ method: "POST",
216
+ path: "/csv/export-bulk",
217
+ handler: "csv.exportBulk",
218
+ config: {
219
+ policies: []
220
+ }
221
+ },
206
222
  {
207
223
  method: "GET",
208
224
  path: "/csv/export-violations/:ruleId",
@@ -312,12 +328,13 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
312
328
  async getCollectionEntries(ctx) {
313
329
  try {
314
330
  const { uid } = ctx.params;
315
- const { page, pageSize, attribute, filterOperator, filterQuery, search, sortField, sortOrder } = ctx.query;
331
+ const { page, pageSize, attribute, searchAttribute, filterOperator, filterQuery, search, sortField, sortOrder } = ctx.query;
316
332
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
317
333
  const data = await service.getCollectionEntries(uid, {
318
334
  page: page ? Number(page) : 1,
319
335
  pageSize: pageSize ? Number(pageSize) : 10,
320
336
  attribute: attribute ? String(attribute) : void 0,
337
+ searchAttribute: searchAttribute ? String(searchAttribute) : void 0,
321
338
  filterOperator: filterOperator ? String(filterOperator) : void 0,
322
339
  filterQuery: filterQuery !== void 0 ? String(filterQuery) : void 0,
323
340
  search: search ? String(search) : void 0,
@@ -582,20 +599,46 @@ const csvController = ({ strapi: strapi2 }) => ({
582
599
  async exportCollection(ctx) {
583
600
  try {
584
601
  const { uid } = ctx.params;
585
- const { search, attribute, filterOperator, filterQuery, limit } = ctx.query;
602
+ const { search, attribute, filterOperator, filterQuery, limit, documentIds } = {
603
+ ...ctx.query,
604
+ ...ctx.request?.body || {}
605
+ };
586
606
  const csvService2 = strapi2.plugin("bulk-updater").service("csv");
607
+ let parsedDocIds;
608
+ if (documentIds) {
609
+ parsedDocIds = Array.isArray(documentIds) ? documentIds : String(documentIds).split(",").map((s) => s.trim()).filter(Boolean);
610
+ }
587
611
  const result = await csvService2.exportCollectionCsv(uid, {
588
612
  search: search ? String(search) : void 0,
589
613
  attribute: attribute ? String(attribute) : void 0,
590
614
  filterOperator: filterOperator ? String(filterOperator) : void 0,
591
615
  filterQuery: filterQuery !== void 0 ? String(filterQuery) : void 0,
592
- limit: limit ? Number(limit) : void 0
616
+ limit: limit ? Number(limit) : void 0,
617
+ documentIds: parsedDocIds
593
618
  });
594
619
  ctx.body = {
595
620
  success: true,
596
621
  filename: result.filename,
597
622
  csvContent: result.csvContent,
598
- count: result.count
623
+ count: result.count,
624
+ fields: result.fields
625
+ };
626
+ } catch (err) {
627
+ ctx.status = 500;
628
+ ctx.body = { success: false, error: err.message };
629
+ }
630
+ },
631
+ async exportBulk(ctx) {
632
+ try {
633
+ const { selectedEntries } = ctx.request.body || {};
634
+ const csvService2 = strapi2.plugin("bulk-updater").service("csv");
635
+ const result = await csvService2.exportBulkCsv(selectedEntries);
636
+ ctx.body = {
637
+ success: true,
638
+ filename: result.filename,
639
+ csvContent: result.csvContent,
640
+ count: result.count,
641
+ isMultiCollection: result.isMultiCollection
599
642
  };
600
643
  } catch (err) {
601
644
  ctx.status = 500;
@@ -650,6 +693,127 @@ const modelsCache = /* @__PURE__ */ new Map();
650
693
  const MAX_QUERY_CACHE_SIZE = 500;
651
694
  const CACHE_TTL_MS = 60 * 1e3;
652
695
  let staticSchemaCache = null;
696
+ function buildDeepPopulate$1(attributes, allComponents, depth2 = 0) {
697
+ if (depth2 > 5) return true;
698
+ const populate2 = {};
699
+ for (const [key, def] of Object.entries(attributes || {})) {
700
+ if (def.type === "component" && def.component) {
701
+ const comp = allComponents[def.component];
702
+ if (comp?.attributes) {
703
+ populate2[key] = {
704
+ populate: buildDeepPopulate$1(comp.attributes, allComponents, depth2 + 1)
705
+ };
706
+ } else {
707
+ populate2[key] = true;
708
+ }
709
+ } else if (def.type === "dynamiczone") {
710
+ populate2[key] = true;
711
+ } else if (def.type === "relation" || def.type === "media") {
712
+ populate2[key] = true;
713
+ }
714
+ }
715
+ return Object.keys(populate2).length > 0 ? populate2 : true;
716
+ }
717
+ function discoverComponentAttributes(strapi2, compUid, prefix, currentDepth, maxDepth, systemFields, allContentTypes, relationsByAttr, allRelationsMeta, attributesMeta) {
718
+ if (currentDepth > maxDepth || !compUid) return [];
719
+ const compSchema = strapi2.components[compUid];
720
+ if (!compSchema?.attributes) return [];
721
+ const discoveredAttrs = [];
722
+ for (const [subKey, subAttr] of Object.entries(compSchema.attributes)) {
723
+ if (systemFields.includes(subKey)) continue;
724
+ if (subAttr.type === "dynamiczone") continue;
725
+ const fullPath = prefix ? `${prefix}.${subKey}` : subKey;
726
+ if (subAttr.type === "component") {
727
+ if (!subAttr.repeatable && subAttr.component) {
728
+ const deeper = discoverComponentAttributes(
729
+ strapi2,
730
+ subAttr.component,
731
+ fullPath,
732
+ currentDepth + 1,
733
+ maxDepth,
734
+ systemFields,
735
+ allContentTypes,
736
+ relationsByAttr,
737
+ allRelationsMeta,
738
+ attributesMeta
739
+ );
740
+ discoveredAttrs.push(...deeper);
741
+ }
742
+ } else if (subAttr.type === "relation") {
743
+ const targetUid = subAttr.target;
744
+ if (targetUid && allContentTypes[targetUid]) {
745
+ const targetInfo = allContentTypes[targetUid]?.info;
746
+ const isMulti = subAttr.relation === "oneToMany" || subAttr.relation === "manyToMany";
747
+ const relMeta = {
748
+ isRelation: true,
749
+ relationType: subAttr.relation,
750
+ targetUid,
751
+ targetDisplayName: targetInfo?.displayName || targetUid,
752
+ isMulti
753
+ };
754
+ relationsByAttr[fullPath] = relMeta;
755
+ allRelationsMeta[fullPath] = relMeta;
756
+ attributesMeta[fullPath] = { depth: currentDepth, type: "relation", component: compUid, isRelation: true };
757
+ discoveredAttrs.push(fullPath);
758
+ }
759
+ } else {
760
+ attributesMeta[fullPath] = { depth: currentDepth, type: subAttr.type, component: compUid };
761
+ discoveredAttrs.push(fullPath);
762
+ }
763
+ }
764
+ return discoveredAttrs;
765
+ }
766
+ function isDeepComponentField(contentType, allComponents, attribute) {
767
+ if (!attribute || !attribute.includes(".")) return false;
768
+ const parts = attribute.split(".");
769
+ let currentSchema = contentType;
770
+ for (let i = 0; i < parts.length - 1; i++) {
771
+ const part = parts[i];
772
+ const attrDef = currentSchema?.attributes?.[part];
773
+ if (!attrDef || attrDef.type !== "component" || !attrDef.component) return false;
774
+ currentSchema = allComponents[attrDef.component];
775
+ if (!currentSchema) return false;
776
+ }
777
+ const leafField = parts[parts.length - 1];
778
+ return Boolean(currentSchema?.attributes?.[leafField]);
779
+ }
780
+ function getLeafAttributeSchema(contentType, allComponents, attribute) {
781
+ if (!attribute) return null;
782
+ if (!attribute.includes(".")) return contentType?.attributes?.[attribute] || null;
783
+ const parts = attribute.split(".");
784
+ let currentSchema = contentType;
785
+ for (let i = 0; i < parts.length - 1; i++) {
786
+ const part = parts[i];
787
+ const attrDef = currentSchema?.attributes?.[part];
788
+ if (!attrDef || attrDef.type !== "component" || !attrDef.component) return null;
789
+ currentSchema = allComponents[attrDef.component];
790
+ if (!currentSchema) return null;
791
+ }
792
+ return currentSchema?.attributes?.[parts[parts.length - 1]] || null;
793
+ }
794
+ function setDeepProperty(obj, pathParts, value) {
795
+ if (pathParts.length === 0) return value;
796
+ const current = { ...obj || {} };
797
+ const [head, ...tail] = pathParts;
798
+ if (tail.length === 0) {
799
+ current[head] = value;
800
+ } else {
801
+ current[head] = setDeepProperty(current[head] || {}, tail, value);
802
+ }
803
+ return current;
804
+ }
805
+ function getDeepProperty(obj, path2) {
806
+ if (!obj || !path2) return void 0;
807
+ if (!path2.includes(".")) return obj[path2];
808
+ return path2.split(".").reduce((acc, part) => acc && typeof acc === "object" ? acc[part] : void 0, obj);
809
+ }
810
+ function buildDeepFilter(pathParts, condition) {
811
+ if (pathParts.length === 0) return condition;
812
+ const [head, ...tail] = pathParts;
813
+ return {
814
+ [head]: buildDeepFilter(tail, condition)
815
+ };
816
+ }
653
817
  function getSafeTimezone(timeZone) {
654
818
  if (!timeZone || typeof timeZone !== "string") {
655
819
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -858,43 +1022,28 @@ async function executeBatchInternal(strapi2, {
858
1022
  const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
859
1023
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
860
1024
  const isNested = attribute.includes(".");
861
- let isComponentNested = false;
862
- let parentKey = "";
863
- let childKey = "";
864
- if (isNested) {
865
- const parts = attribute.split(".");
866
- parentKey = parts[0];
867
- childKey = parts[1];
868
- const parentAttrDef = contentType?.attributes?.[parentKey];
869
- if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
870
- const compSchema = strapi2.components[parentAttrDef.component];
871
- if (compSchema?.attributes?.[childKey]) {
872
- isComponentNested = true;
873
- }
874
- }
875
- }
1025
+ const isComponentNested = isNested && isDeepComponentField(contentType, strapi2.components, attribute);
1026
+ const parts = isNested ? attribute.split(".") : [];
1027
+ const rootKey = isNested ? parts[0] : "";
876
1028
  const attrDef = contentType?.attributes?.[attribute];
877
1029
  const hasAttribute = Boolean(attrDef) || isComponentNested;
878
1030
  if (hasAttribute) {
879
1031
  if (isComponentNested) {
880
1032
  const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
881
1033
  documentId: target.documentId,
882
- populate: [parentKey]
1034
+ populate: buildDeepPopulate$1(contentType.attributes, strapi2.components)
883
1035
  });
884
- const existingParent = currentEntryWithPopulate?.[parentKey] || {};
885
- const currentVal2 = existingParent?.[childKey];
1036
+ const existingRoot = currentEntryWithPopulate?.[rootKey] || {};
1037
+ const currentVal2 = getDeepProperty(existingRoot, parts.slice(1).join("."));
886
1038
  let newValue2 = value;
887
1039
  if (regexObj && typeof currentVal2 === "string") {
888
1040
  newValue2 = currentVal2.replace(regexObj, value);
889
1041
  }
890
- const updatedParent = {
891
- ...existingParent,
892
- [childKey]: newValue2
893
- };
1042
+ const updatedRoot = setDeepProperty(existingRoot, parts.slice(1), newValue2);
894
1043
  await strapi2.documents(target.uid).update({
895
1044
  documentId: target.documentId,
896
1045
  data: {
897
- [parentKey]: updatedParent
1046
+ [rootKey]: updatedRoot
898
1047
  }
899
1048
  });
900
1049
  const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
@@ -1329,12 +1478,14 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1329
1478
  }
1330
1479
  const metaByUid = {};
1331
1480
  const allRelationsMeta = {};
1481
+ const allAttributesMeta = {};
1332
1482
  for (const uid of targetUids) {
1333
1483
  const contentType = allContentTypes[uid];
1334
1484
  if (!contentType || contentType.kind !== "collectionType" && contentType.kind !== "singleType") continue;
1335
1485
  const isSingleType = contentType.kind === "singleType";
1336
1486
  const rawAttrs = contentType.attributes || {};
1337
1487
  const relationsByAttr = {};
1488
+ const colAttributesMeta = {};
1338
1489
  const nestedComponentAttrs = [];
1339
1490
  const validAttrs = Object.keys(rawAttrs).filter((attrKey) => {
1340
1491
  if (systemFields.includes(attrKey)) return false;
@@ -1342,14 +1493,19 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1342
1493
  if (rawAttrs[attrKey].type === "component") {
1343
1494
  const compDef = rawAttrs[attrKey];
1344
1495
  if (!compDef.repeatable && compDef.component) {
1345
- const componentSchema = strapi2.components[compDef.component];
1346
- if (componentSchema && componentSchema.attributes) {
1347
- for (const [subKey, subAttr] of Object.entries(componentSchema.attributes)) {
1348
- if (systemFields.includes(subKey)) continue;
1349
- if (["component", "dynamiczone", "relation"].includes(subAttr.type)) continue;
1350
- nestedComponentAttrs.push(`${attrKey}.${subKey}`);
1351
- }
1352
- }
1496
+ const compAttrs = discoverComponentAttributes(
1497
+ strapi2,
1498
+ compDef.component,
1499
+ attrKey,
1500
+ 1,
1501
+ 5,
1502
+ systemFields,
1503
+ allContentTypes,
1504
+ relationsByAttr,
1505
+ allRelationsMeta,
1506
+ colAttributesMeta
1507
+ );
1508
+ nestedComponentAttrs.push(...compAttrs);
1353
1509
  }
1354
1510
  return false;
1355
1511
  }
@@ -1368,12 +1524,19 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1368
1524
  };
1369
1525
  relationsByAttr[attrKey] = relMeta;
1370
1526
  allRelationsMeta[attrKey] = relMeta;
1527
+ colAttributesMeta[attrKey] = { depth: 0, type: "relation", isRelation: true };
1371
1528
  return true;
1372
1529
  }
1530
+ colAttributesMeta[attrKey] = { depth: 0, type: rawAttrs[attrKey].type };
1373
1531
  return true;
1374
1532
  });
1375
1533
  const allColAttrs = [...validAttrs, ...nestedComponentAttrs];
1376
- allColAttrs.forEach((attr) => allAttributesSet.add(attr));
1534
+ allColAttrs.forEach((attr) => {
1535
+ allAttributesSet.add(attr);
1536
+ if (colAttributesMeta[attr]) {
1537
+ allAttributesMeta[attr] = colAttributesMeta[attr];
1538
+ }
1539
+ });
1377
1540
  metaByUid[uid] = {
1378
1541
  uid,
1379
1542
  kind: contentType.kind,
@@ -1383,11 +1546,13 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1383
1546
  pluralName: contentType.info?.pluralName || uid,
1384
1547
  displayName: contentType.info?.displayName || uid,
1385
1548
  attributes: allColAttrs,
1549
+ attributesMeta: colAttributesMeta,
1386
1550
  relations: relationsByAttr
1387
1551
  };
1388
1552
  }
1389
1553
  staticSchemaCache = {
1390
1554
  attributes: Array.from(allAttributesSet),
1555
+ attributesMeta: allAttributesMeta,
1391
1556
  targetUids: Object.keys(metaByUid),
1392
1557
  metaByUid,
1393
1558
  relationsMeta: allRelationsMeta
@@ -1418,17 +1583,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1418
1583
  let entries = [];
1419
1584
  let totalCount = 0;
1420
1585
  try {
1421
- const ctAttrs = allContentTypes[uid]?.attributes || {};
1422
- const relKeys = Object.keys(meta.relations || {});
1423
- const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
1424
- const populateOptionObj = {};
1425
- relKeys.forEach((k) => {
1426
- populateOptionObj[k] = true;
1427
- });
1428
- compKeys.forEach((k) => {
1429
- populateOptionObj[k] = true;
1430
- });
1431
- const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
1586
+ const populateOption = buildDeepPopulate$1(allContentTypes[uid]?.attributes || {}, strapi2.components);
1432
1587
  if (meta.isSingleType) {
1433
1588
  const singleDoc = await strapi2.documents(uid).findFirst({
1434
1589
  populate: populateOption
@@ -1487,8 +1642,10 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1487
1642
  return {
1488
1643
  success: true,
1489
1644
  attributes: staticSchemaCache.attributes,
1645
+ attributesMeta: staticSchemaCache.attributesMeta,
1490
1646
  collections,
1491
1647
  relationsMeta: staticSchemaCache.relationsMeta,
1648
+ maxDiscoveredDepth: 5,
1492
1649
  executionWindow: {
1493
1650
  ...executionConfig,
1494
1651
  ...windowStatus
@@ -1507,6 +1664,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1507
1664
  page = 1,
1508
1665
  pageSize = 10,
1509
1666
  attribute,
1667
+ searchAttribute,
1510
1668
  filterOperator,
1511
1669
  filterQuery,
1512
1670
  search,
@@ -1521,8 +1679,9 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1521
1679
  const currentPage = Math.max(Number(page) || 1, 1);
1522
1680
  const start = (currentPage - 1) * limit;
1523
1681
  const isSingleType = contentType.kind === "singleType";
1682
+ const targetFilterAttr = searchAttribute || attribute;
1524
1683
  const signature = await getCollectionSignature(strapi2, uid);
1525
- const cacheKey = `${uid}:${limit}:${start}:${attribute || ""}:${filterOperator || ""}:${filterQuery || ""}:${search || ""}:${sortField || ""}:${sortOrder}`;
1684
+ const cacheKey = `${uid}:${limit}:${start}:${attribute || ""}:${searchAttribute || ""}:${filterOperator || ""}:${filterQuery || ""}:${search || ""}:${sortField || ""}:${sortOrder}`;
1526
1685
  const cached = queryCache.get(cacheKey);
1527
1686
  const etag = `W/"entry-${crypto__default.default.createHash("md5").update(`${cacheKey}:${signature}`).digest("hex")}"`;
1528
1687
  if (cached && cached.signature === signature && Date.now() - cached.timestamp < CACHE_TTL_MS) {
@@ -1537,19 +1696,15 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1537
1696
  const attrs = contentType.attributes || {};
1538
1697
  let colHasAttribute = false;
1539
1698
  let isNestedAttr = false;
1540
- let parentKey = "";
1541
- let childKey = "";
1542
- if (attribute) {
1543
- if (attribute.includes(".")) {
1544
- [parentKey, childKey] = attribute.split(".");
1545
- if (attrs[parentKey]?.type === "component" && !attrs[parentKey].repeatable && attrs[parentKey].component) {
1546
- const compSchema = strapi2.components[attrs[parentKey].component];
1547
- if (compSchema?.attributes?.[childKey]) {
1548
- colHasAttribute = true;
1549
- isNestedAttr = true;
1550
- }
1699
+ let parts = [];
1700
+ if (targetFilterAttr) {
1701
+ if (targetFilterAttr.includes(".")) {
1702
+ isNestedAttr = isDeepComponentField(contentType, strapi2.components, targetFilterAttr);
1703
+ if (isNestedAttr) {
1704
+ colHasAttribute = true;
1705
+ parts = targetFilterAttr.split(".");
1551
1706
  }
1552
- } else if (attrs[attribute]) {
1707
+ } else if (attrs[targetFilterAttr]) {
1553
1708
  colHasAttribute = true;
1554
1709
  }
1555
1710
  }
@@ -1574,7 +1729,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1574
1729
  }
1575
1730
  let condition = void 0;
1576
1731
  const q = (filterQuery || "").trim();
1577
- const targetAttrSchema = isNestedAttr ? strapi2.components[attrs[parentKey]?.component]?.attributes?.[childKey] : attrs[attribute];
1732
+ const targetAttrSchema = isNestedAttr ? getLeafAttributeSchema(contentType, strapi2.components, targetFilterAttr) : attrs[targetFilterAttr];
1578
1733
  const isStringLike = ["string", "text", "email", "richtext", "uid"].includes(targetAttrSchema?.type);
1579
1734
  if (normOp === "empty") {
1580
1735
  condition = isStringLike ? { $or: [{ $null: true }, { $eq: "" }] } : { $null: true };
@@ -1604,19 +1759,18 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1604
1759
  }
1605
1760
  if (condition !== void 0) {
1606
1761
  if (isNestedAttr) {
1607
- filters2[parentKey] = {
1608
- [childKey]: condition
1609
- };
1762
+ const deepFilter = buildDeepFilter(parts, condition);
1763
+ Object.assign(filters2, deepFilter);
1610
1764
  } else {
1611
1765
  if (normOp === "empty" && isStringLike) {
1612
1766
  filters2.$or = [
1613
- { [attribute]: { $null: true } },
1614
- { [attribute]: { $eq: "" } }
1767
+ { [targetFilterAttr]: { $null: true } },
1768
+ { [targetFilterAttr]: { $eq: "" } }
1615
1769
  ];
1616
1770
  } else if (normOp === "notempty" && isStringLike) {
1617
- filters2[attribute] = { $notNull: true, $ne: "" };
1771
+ filters2[targetFilterAttr] = { $notNull: true, $ne: "" };
1618
1772
  } else {
1619
- filters2[attribute] = condition;
1773
+ filters2[targetFilterAttr] = condition;
1620
1774
  }
1621
1775
  }
1622
1776
  }
@@ -1639,11 +1793,13 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1639
1793
  }
1640
1794
  }
1641
1795
  }
1796
+ const populateOption = buildDeepPopulate$1(contentType.attributes, strapi2.components);
1642
1797
  if (isSingleType) {
1643
1798
  let singleDoc = null;
1644
1799
  try {
1645
1800
  singleDoc = await strapi2.documents(uid).findFirst({
1646
- filters: Object.keys(filters2).length > 0 ? filters2 : void 0
1801
+ filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
1802
+ populate: populateOption
1647
1803
  });
1648
1804
  } catch {
1649
1805
  singleDoc = null;
@@ -1662,11 +1818,11 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1662
1818
  }
1663
1819
  };
1664
1820
  } else {
1665
- const sortClause = {};
1821
+ let sortClause = {};
1666
1822
  if (sortField && sortField.includes(".")) {
1667
- const [parent, child] = sortField.split(".");
1668
- if (contentType.attributes?.[parent]?.type === "component") {
1669
- sortClause[parent] = { [child]: sortOrder };
1823
+ const sortParts = sortField.split(".");
1824
+ if (isDeepComponentField(contentType, strapi2.components, sortField)) {
1825
+ sortClause = buildDeepFilter(sortParts, sortOrder);
1670
1826
  } else {
1671
1827
  sortClause["createdAt"] = "desc";
1672
1828
  }
@@ -1675,10 +1831,6 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1675
1831
  } else {
1676
1832
  sortClause["createdAt"] = "desc";
1677
1833
  }
1678
- const populateAttrs = Object.keys(contentType.attributes || {}).filter(
1679
- (k) => contentType.attributes[k].type === "relation" || contentType.attributes[k].type === "component"
1680
- );
1681
- const populateOption = populateAttrs.length > 0 ? populateAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1682
1834
  const [entries, totalEntries] = await Promise.all([
1683
1835
  strapi2.documents(uid).findMany({
1684
1836
  start,
@@ -1800,21 +1952,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1800
1952
  const displayName = contentType?.info?.displayName || target.uid;
1801
1953
  try {
1802
1954
  const isNested = attribute.includes(".");
1803
- let isComponentNested = false;
1804
- let parentKey = "";
1805
- let childKey = "";
1806
- if (isNested) {
1807
- const parts = attribute.split(".");
1808
- parentKey = parts[0];
1809
- childKey = parts[1];
1810
- const parentAttrDef = contentType?.attributes?.[parentKey];
1811
- if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
1812
- const compSchema = strapi2.components[parentAttrDef.component];
1813
- if (compSchema?.attributes?.[childKey]) {
1814
- isComponentNested = true;
1815
- }
1816
- }
1817
- }
1955
+ const isComponentNested = isNested && isDeepComponentField(contentType, strapi2.components, attribute);
1818
1956
  const attrDef = contentType?.attributes?.[attribute];
1819
1957
  const hasAttribute = Boolean(attrDef) || isComponentNested;
1820
1958
  if (!hasAttribute) {
@@ -1839,7 +1977,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1839
1977
  if (isComponentNested) {
1840
1978
  currentEntry = await strapi2.documents(target.uid).findOne({
1841
1979
  documentId: target.documentId,
1842
- populate: [parentKey]
1980
+ populate: buildDeepPopulate$1(contentType.attributes, strapi2.components)
1843
1981
  });
1844
1982
  } else if (attrDef?.type === "relation") {
1845
1983
  currentEntry = await strapi2.documents(target.uid).findOne({
@@ -1853,8 +1991,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1853
1991
  }
1854
1992
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
1855
1993
  if (isComponentNested) {
1856
- const existingParent = currentEntry?.[parentKey] || {};
1857
- const currentVal2 = existingParent?.[childKey];
1994
+ const currentVal2 = getDeepProperty(currentEntry, attribute);
1858
1995
  let newValue2 = value;
1859
1996
  if (regexObj && typeof currentVal2 === "string") {
1860
1997
  newValue2 = currentVal2.replace(regexObj, value);
@@ -2594,7 +2731,11 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2594
2731
  function escapeCsvVal(val) {
2595
2732
  if (val === null || val === void 0) return "";
2596
2733
  let str2 = typeof val === "object" ? JSON.stringify(val) : String(val);
2597
- if (str2.includes(",") || str2.includes('"') || str2.includes("\n") || str2.includes("\r")) {
2734
+ str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2735
+ if (/^[=+@-]$/.test(str2.charAt(0))) {
2736
+ str2 = `'${str2}`;
2737
+ }
2738
+ if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
2598
2739
  return `"${str2.replace(/"/g, '""')}"`;
2599
2740
  }
2600
2741
  return str2;
@@ -2638,7 +2779,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2638
2779
  return {
2639
2780
  success: true,
2640
2781
  filename,
2641
- csvContent: rows.join("\n"),
2782
+ csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
2642
2783
  summary: {
2643
2784
  batchId: batch.id,
2644
2785
  status: batch.status,
@@ -3462,23 +3603,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3462
3603
  function escapeCsvValue(val) {
3463
3604
  if (val === null || val === void 0) return "";
3464
3605
  let str2 = "";
3465
- if (typeof val === "object") {
3606
+ if (val instanceof Date) {
3607
+ str2 = val.toISOString();
3608
+ } else if (typeof val === "object") {
3466
3609
  str2 = JSON.stringify(val);
3467
3610
  } else {
3468
3611
  str2 = String(val);
3469
3612
  }
3470
- if (str2.includes(",") || str2.includes('"') || str2.includes("\n") || str2.includes("\r")) {
3613
+ str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
3614
+ if (/^[=+@-]$/.test(str2.charAt(0))) {
3615
+ str2 = `'${str2}`;
3616
+ }
3617
+ if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
3471
3618
  return `"${str2.replace(/"/g, '""')}"`;
3472
3619
  }
3473
3620
  return str2;
3474
3621
  }
3475
3622
  function parseCsv(csvText) {
3623
+ if (!csvText) return [];
3624
+ let cleanText = csvText;
3625
+ if (cleanText.charCodeAt(0) === 65279) {
3626
+ cleanText = cleanText.slice(1);
3627
+ }
3476
3628
  const lines = [];
3477
3629
  let current = "";
3478
3630
  let inQuotes = false;
3479
- for (let i = 0; i < csvText.length; i++) {
3480
- const char = csvText[i];
3481
- const nextChar = csvText[i + 1];
3631
+ for (let i = 0; i < cleanText.length; i++) {
3632
+ const char = cleanText[i];
3633
+ const nextChar = cleanText[i + 1];
3482
3634
  if (char === '"') {
3483
3635
  if (inQuotes && nextChar === '"') {
3484
3636
  current += '"';
@@ -3538,6 +3690,103 @@ function parseCsv(csvText) {
3538
3690
  }
3539
3691
  return rows;
3540
3692
  }
3693
+ function discoverExportFields(ct, allComponents, maxDepth = 4) {
3694
+ const fields2 = [{ key: "documentId", label: "documentId", type: "string" }];
3695
+ const excluded = ["createdBy", "updatedBy", "localizations"];
3696
+ function walkComponent(compUid, pathPrefix, labelPrefix, depth2) {
3697
+ if (depth2 >= maxDepth) return;
3698
+ const comp = allComponents[compUid];
3699
+ if (!comp?.attributes) return;
3700
+ for (const [subKey, subDef] of Object.entries(comp.attributes)) {
3701
+ if (excluded.includes(subKey)) continue;
3702
+ const keyPath = `${pathPrefix}.${subKey}`;
3703
+ const displayLabel = `${labelPrefix} > ${subKey}`;
3704
+ if (subDef.type === "component" && subDef.component) {
3705
+ if (!subDef.repeatable) {
3706
+ walkComponent(subDef.component, keyPath, displayLabel, depth2 + 1);
3707
+ } else {
3708
+ fields2.push({ key: keyPath, label: displayLabel, type: "component" });
3709
+ }
3710
+ } else {
3711
+ fields2.push({ key: keyPath, label: displayLabel, type: subDef.type });
3712
+ }
3713
+ }
3714
+ }
3715
+ for (const [attrName, attrDef] of Object.entries(ct.attributes || {})) {
3716
+ if (excluded.includes(attrName)) continue;
3717
+ if (attrDef.type === "component" && attrDef.component) {
3718
+ if (!attrDef.repeatable) {
3719
+ walkComponent(attrDef.component, attrName, attrName, 0);
3720
+ } else {
3721
+ fields2.push({ key: attrName, label: attrName, type: "component" });
3722
+ }
3723
+ } else {
3724
+ fields2.push({ key: attrName, label: attrName, type: attrDef.type });
3725
+ }
3726
+ }
3727
+ return fields2;
3728
+ }
3729
+ function buildDeepPopulate(attributes, allComponents, depth2 = 0) {
3730
+ if (depth2 > 4) return true;
3731
+ const populate2 = {};
3732
+ for (const [key, def] of Object.entries(attributes || {})) {
3733
+ if (def.type === "component" && def.component) {
3734
+ const comp = allComponents[def.component];
3735
+ if (comp?.attributes) {
3736
+ populate2[key] = {
3737
+ populate: buildDeepPopulate(comp.attributes, allComponents, depth2 + 1)
3738
+ };
3739
+ } else {
3740
+ populate2[key] = true;
3741
+ }
3742
+ } else if (def.type === "dynamiczone") {
3743
+ populate2[key] = true;
3744
+ } else if (def.type === "relation" || def.type === "media") {
3745
+ populate2[key] = true;
3746
+ }
3747
+ }
3748
+ return Object.keys(populate2).length > 0 ? populate2 : true;
3749
+ }
3750
+ function getNestedExportValue(entry, path2) {
3751
+ if (!entry || !path2) return "";
3752
+ const parts = path2.split(".");
3753
+ let current = entry;
3754
+ for (let i = 0; i < parts.length; i++) {
3755
+ const part = parts[i];
3756
+ if (current === null || current === void 0) return "";
3757
+ if (Array.isArray(current)) {
3758
+ const subPath = parts.slice(i).join(".");
3759
+ return current.map((item) => {
3760
+ if (!item) return "";
3761
+ if (typeof item === "object") {
3762
+ const val = getNestedExportValue(item, subPath);
3763
+ return typeof val === "object" ? JSON.stringify(val) : String(val ?? "");
3764
+ }
3765
+ return String(item);
3766
+ }).filter(Boolean).join("; ");
3767
+ }
3768
+ current = current[part];
3769
+ }
3770
+ if (current === null || current === void 0) return "";
3771
+ if (typeof current === "object" && current.url) {
3772
+ return current.url;
3773
+ }
3774
+ if (typeof current === "object" && !Array.isArray(current)) {
3775
+ if (current.documentId || current.name || current.title) {
3776
+ return current.name || current.title || current.documentId;
3777
+ }
3778
+ }
3779
+ if (Array.isArray(current)) {
3780
+ return current.map((item) => {
3781
+ if (!item) return "";
3782
+ if (typeof item === "object") {
3783
+ return item.name || item.title || item.documentId || JSON.stringify(item);
3784
+ }
3785
+ return String(item);
3786
+ }).filter(Boolean).join("; ");
3787
+ }
3788
+ return current;
3789
+ }
3541
3790
  const csvService = ({ strapi: strapi2 }) => ({
3542
3791
  /**
3543
3792
  * Generates a sample template CSV with schema attribute headers and sample values
@@ -3586,9 +3835,7 @@ const csvService = ({ strapi: strapi2 }) => ({
3586
3835
  }
3587
3836
  sampleRow.push(sampleVal);
3588
3837
  }
3589
- const csvContent = `${headers.map(escapeCsvValue).join(",")}
3590
- ${sampleRow.map(escapeCsvValue).join(",")}
3591
- `;
3838
+ const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
3592
3839
  return {
3593
3840
  filename: `${ct.info?.singularName || "template"}_template.csv`,
3594
3841
  csvContent,
@@ -3596,65 +3843,60 @@ ${sampleRow.map(escapeCsvValue).join(",")}
3596
3843
  };
3597
3844
  },
3598
3845
  /**
3599
- * Exports entries of a collection to CSV, with optional filters
3846
+ * Exports entries of a collection to CSV with deep nested component support,
3847
+ * RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
3600
3848
  */
3601
3849
  async exportCollectionCsv(uid, options = {}) {
3602
3850
  const ct = strapi2.contentTypes[uid];
3603
3851
  if (!ct) {
3604
3852
  throw new Error(`Content type ${uid} not found`);
3605
3853
  }
3606
- const attrs = ct.attributes || {};
3607
- const excluded = ["createdBy", "updatedBy", "localizations"];
3608
- const exportFields = ["documentId"];
3609
- const compFieldsToPopulate = [];
3610
- for (const [attrName, attrDef] of Object.entries(attrs)) {
3611
- if (excluded.includes(attrName)) continue;
3612
- if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
3613
- if (attrDef.type === "component") {
3614
- if (!attrDef.repeatable && attrDef.component) {
3615
- const comp = strapi2.components[attrDef.component];
3616
- if (comp?.attributes) {
3617
- compFieldsToPopulate.push(attrName);
3618
- for (const [subKey, subDef] of Object.entries(comp.attributes)) {
3619
- if (excluded.includes(subKey)) continue;
3620
- if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
3621
- exportFields.push(`${attrName}.${subKey}`);
3622
- }
3623
- }
3624
- }
3625
- continue;
3626
- }
3627
- exportFields.push(attrName);
3628
- }
3854
+ const allComponents = strapi2.components || {};
3855
+ const fields2 = discoverExportFields(ct, allComponents);
3629
3856
  const filters2 = {};
3630
- const { attribute, filterOperator, filterQuery, search } = options;
3631
- if (attribute && filterOperator && filterQuery !== void 0 && filterQuery !== "") {
3632
- switch (filterOperator) {
3633
- case "equals":
3634
- filters2[attribute] = { $eq: filterQuery };
3635
- break;
3636
- case "notEquals":
3637
- filters2[attribute] = { $ne: filterQuery };
3638
- break;
3639
- case "contains":
3640
- filters2[attribute] = { $containsi: filterQuery };
3641
- break;
3642
- case "notContains":
3643
- filters2[attribute] = { $notContainsi: filterQuery };
3644
- break;
3645
- case "startsWith":
3646
- filters2[attribute] = { $startsWith: filterQuery };
3647
- break;
3648
- case "endsWith":
3649
- filters2[attribute] = { $endsWith: filterQuery };
3650
- break;
3857
+ const { attribute, filterOperator, filterQuery, search, documentIds } = options;
3858
+ if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
3859
+ filters2.documentId = { $in: documentIds };
3860
+ }
3861
+ if (attribute && filterOperator && filterOperator !== "none") {
3862
+ const isNested = attribute.includes(".");
3863
+ if (filterOperator === "empty") {
3864
+ if (isNested) {
3865
+ const [parent, child] = attribute.split(".");
3866
+ filters2[parent] = { [child]: { $null: true } };
3867
+ } else {
3868
+ filters2[attribute] = { $null: true };
3869
+ }
3870
+ } else if (filterOperator === "not_empty") {
3871
+ if (isNested) {
3872
+ const [parent, child] = attribute.split(".");
3873
+ filters2[parent] = { [child]: { $notNull: true } };
3874
+ } else {
3875
+ filters2[attribute] = { $notNull: true };
3876
+ }
3877
+ } else if (filterQuery !== void 0 && filterQuery !== "") {
3878
+ const opMap = {
3879
+ equals: "$eq",
3880
+ notEquals: "$ne",
3881
+ contains: "$containsi",
3882
+ notContains: "$notContainsi",
3883
+ startsWith: "$startsWith",
3884
+ endsWith: "$endsWith"
3885
+ };
3886
+ const strapiOp = opMap[filterOperator] || "$eq";
3887
+ if (isNested) {
3888
+ const [parent, child] = attribute.split(".");
3889
+ filters2[parent] = { [child]: { [strapiOp]: filterQuery } };
3890
+ } else {
3891
+ filters2[attribute] = { [strapiOp]: filterQuery };
3892
+ }
3651
3893
  }
3652
3894
  }
3653
3895
  if (search && search.trim() !== "") {
3654
3896
  const q = search.trim();
3655
3897
  const searchConditions = [];
3656
3898
  const searchFields = ["name", "title", "email", "slug", "description"].filter(
3657
- (f) => attrs[f] && ["string", "text", "email"].includes(attrs[f].type)
3899
+ (f) => ct.attributes?.[f] && ["string", "text", "email"].includes(ct.attributes[f].type)
3658
3900
  );
3659
3901
  for (const field of searchFields) {
3660
3902
  searchConditions.push({ [field]: { $containsi: q } });
@@ -3667,34 +3909,37 @@ ${sampleRow.map(escapeCsvValue).join(",")}
3667
3909
  }
3668
3910
  }
3669
3911
  }
3670
- const populateOption = compFieldsToPopulate.length > 0 ? compFieldsToPopulate.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
3671
- const entries = await strapi2.documents(uid).findMany({
3672
- filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
3673
- limit: options.limit || 5e3,
3674
- sort: { createdAt: "desc" },
3675
- populate: populateOption
3676
- });
3912
+ const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
3913
+ let entries = [];
3914
+ if (ct.kind === "singleType") {
3915
+ const doc = await strapi2.documents(uid).findFirst({
3916
+ populate: deepPopulate
3917
+ });
3918
+ entries = doc ? [doc] : [];
3919
+ } else {
3920
+ entries = await strapi2.documents(uid).findMany({
3921
+ filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
3922
+ limit: options.limit || 5e3,
3923
+ sort: { createdAt: "desc" },
3924
+ populate: deepPopulate
3925
+ });
3926
+ }
3677
3927
  const lines = [];
3678
- lines.push(exportFields.map(escapeCsvValue).join(","));
3928
+ lines.push(fields2.map((f) => escapeCsvValue(f.key)).join(","));
3679
3929
  for (const entry of entries || []) {
3680
- const rowValues = exportFields.map((field) => {
3681
- let val;
3682
- if (field.includes(".")) {
3683
- const [parent, child] = field.split(".");
3684
- val = entry[parent]?.[child];
3685
- } else {
3686
- val = entry[field];
3687
- }
3930
+ const rowValues = fields2.map((f) => {
3931
+ const val = getNestedExportValue(entry, f.key);
3688
3932
  return escapeCsvValue(val);
3689
3933
  });
3690
3934
  lines.push(rowValues.join(","));
3691
3935
  }
3692
- const csvContent = lines.join("\n");
3936
+ const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
3693
3937
  const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
3694
3938
  return {
3695
3939
  filename,
3696
3940
  csvContent,
3697
- count: entries ? entries.length : 0
3941
+ count: entries ? entries.length : 0,
3942
+ fields: fields2.map((f) => f.key)
3698
3943
  };
3699
3944
  },
3700
3945
  /**
@@ -3734,11 +3979,76 @@ ${sampleRow.map(escapeCsvValue).join(",")}
3734
3979
  const filename = `violations_${safeRuleName}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
3735
3980
  return {
3736
3981
  filename,
3737
- csvContent: lines.join("\n"),
3982
+ csvContent: "\uFEFF" + lines.join("\r\n") + "\r\n",
3738
3983
  count: scanReport.violations.length,
3739
3984
  ruleName: scanReport.rule.name
3740
3985
  };
3741
3986
  },
3987
+ /**
3988
+ * Bulk Export: Exports selected entries across multiple collections or a multi-collection batch.
3989
+ */
3990
+ async exportBulkCsv(selectedEntries) {
3991
+ if (!selectedEntries || selectedEntries.length === 0) {
3992
+ throw new Error("No entries selected for bulk export");
3993
+ }
3994
+ const byUid = {};
3995
+ for (const item of selectedEntries) {
3996
+ if (!byUid[item.uid]) byUid[item.uid] = [];
3997
+ byUid[item.uid].push(item.documentId);
3998
+ }
3999
+ const uids = Object.keys(byUid);
4000
+ if (uids.length === 1) {
4001
+ const uid = uids[0];
4002
+ return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
4003
+ }
4004
+ const rows = [];
4005
+ const headers = [
4006
+ "Collection UID",
4007
+ "Collection Name",
4008
+ "Document ID",
4009
+ "Record Title / Name",
4010
+ "Primary Identifier",
4011
+ "Created At",
4012
+ "Updated At",
4013
+ "Published Status"
4014
+ ];
4015
+ rows.push(headers.map(escapeCsvValue).join(","));
4016
+ let totalExported = 0;
4017
+ for (const uid of uids) {
4018
+ const ct = strapi2.contentTypes[uid];
4019
+ if (!ct) continue;
4020
+ const displayName = ct.info?.displayName || uid;
4021
+ const docIds = byUid[uid];
4022
+ const entries = await strapi2.documents(uid).findMany({
4023
+ filters: { documentId: { $in: docIds } },
4024
+ limit: docIds.length
4025
+ });
4026
+ for (const entry of entries || []) {
4027
+ const titleOrName = entry.title || entry.name || entry.subject || entry.email || `Entry #${entry.documentId}`;
4028
+ const identifier = entry.slug || entry.email || entry.username || entry.documentId;
4029
+ const isPublished = Boolean(entry.publishedAt);
4030
+ const row = [
4031
+ uid,
4032
+ displayName,
4033
+ entry.documentId,
4034
+ titleOrName,
4035
+ identifier,
4036
+ entry.createdAt || "",
4037
+ entry.updatedAt || "",
4038
+ isPublished ? "Published" : "Draft"
4039
+ ];
4040
+ rows.push(row.map(escapeCsvValue).join(","));
4041
+ totalExported++;
4042
+ }
4043
+ }
4044
+ const filename = `bulk_export_selected_${totalExported}_entries_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
4045
+ return {
4046
+ filename,
4047
+ csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
4048
+ count: totalExported,
4049
+ isMultiCollection: true
4050
+ };
4051
+ },
3742
4052
  /**
3743
4053
  * Imports CSV records to update existing records or insert/override records
3744
4054
  */