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.
@@ -190,6 +190,22 @@ const routes = {
190
190
  policies: []
191
191
  }
192
192
  },
193
+ {
194
+ method: "POST",
195
+ path: "/csv/export/:uid",
196
+ handler: "csv.exportCollection",
197
+ config: {
198
+ policies: []
199
+ }
200
+ },
201
+ {
202
+ method: "POST",
203
+ path: "/csv/export-bulk",
204
+ handler: "csv.exportBulk",
205
+ config: {
206
+ policies: []
207
+ }
208
+ },
193
209
  {
194
210
  method: "GET",
195
211
  path: "/csv/export-violations/:ruleId",
@@ -299,12 +315,13 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
299
315
  async getCollectionEntries(ctx) {
300
316
  try {
301
317
  const { uid } = ctx.params;
302
- const { page, pageSize, attribute, filterOperator, filterQuery, search, sortField, sortOrder } = ctx.query;
318
+ const { page, pageSize, attribute, searchAttribute, filterOperator, filterQuery, search, sortField, sortOrder } = ctx.query;
303
319
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
304
320
  const data = await service.getCollectionEntries(uid, {
305
321
  page: page ? Number(page) : 1,
306
322
  pageSize: pageSize ? Number(pageSize) : 10,
307
323
  attribute: attribute ? String(attribute) : void 0,
324
+ searchAttribute: searchAttribute ? String(searchAttribute) : void 0,
308
325
  filterOperator: filterOperator ? String(filterOperator) : void 0,
309
326
  filterQuery: filterQuery !== void 0 ? String(filterQuery) : void 0,
310
327
  search: search ? String(search) : void 0,
@@ -569,20 +586,46 @@ const csvController = ({ strapi: strapi2 }) => ({
569
586
  async exportCollection(ctx) {
570
587
  try {
571
588
  const { uid } = ctx.params;
572
- const { search, attribute, filterOperator, filterQuery, limit } = ctx.query;
589
+ const { search, attribute, filterOperator, filterQuery, limit, documentIds } = {
590
+ ...ctx.query,
591
+ ...ctx.request?.body || {}
592
+ };
573
593
  const csvService2 = strapi2.plugin("bulk-updater").service("csv");
594
+ let parsedDocIds;
595
+ if (documentIds) {
596
+ parsedDocIds = Array.isArray(documentIds) ? documentIds : String(documentIds).split(",").map((s) => s.trim()).filter(Boolean);
597
+ }
574
598
  const result = await csvService2.exportCollectionCsv(uid, {
575
599
  search: search ? String(search) : void 0,
576
600
  attribute: attribute ? String(attribute) : void 0,
577
601
  filterOperator: filterOperator ? String(filterOperator) : void 0,
578
602
  filterQuery: filterQuery !== void 0 ? String(filterQuery) : void 0,
579
- limit: limit ? Number(limit) : void 0
603
+ limit: limit ? Number(limit) : void 0,
604
+ documentIds: parsedDocIds
580
605
  });
581
606
  ctx.body = {
582
607
  success: true,
583
608
  filename: result.filename,
584
609
  csvContent: result.csvContent,
585
- count: result.count
610
+ count: result.count,
611
+ fields: result.fields
612
+ };
613
+ } catch (err) {
614
+ ctx.status = 500;
615
+ ctx.body = { success: false, error: err.message };
616
+ }
617
+ },
618
+ async exportBulk(ctx) {
619
+ try {
620
+ const { selectedEntries } = ctx.request.body || {};
621
+ const csvService2 = strapi2.plugin("bulk-updater").service("csv");
622
+ const result = await csvService2.exportBulkCsv(selectedEntries);
623
+ ctx.body = {
624
+ success: true,
625
+ filename: result.filename,
626
+ csvContent: result.csvContent,
627
+ count: result.count,
628
+ isMultiCollection: result.isMultiCollection
586
629
  };
587
630
  } catch (err) {
588
631
  ctx.status = 500;
@@ -637,6 +680,127 @@ const modelsCache = /* @__PURE__ */ new Map();
637
680
  const MAX_QUERY_CACHE_SIZE = 500;
638
681
  const CACHE_TTL_MS = 60 * 1e3;
639
682
  let staticSchemaCache = null;
683
+ function buildDeepPopulate$1(attributes, allComponents, depth2 = 0) {
684
+ if (depth2 > 5) return true;
685
+ const populate2 = {};
686
+ for (const [key, def] of Object.entries(attributes || {})) {
687
+ if (def.type === "component" && def.component) {
688
+ const comp = allComponents[def.component];
689
+ if (comp?.attributes) {
690
+ populate2[key] = {
691
+ populate: buildDeepPopulate$1(comp.attributes, allComponents, depth2 + 1)
692
+ };
693
+ } else {
694
+ populate2[key] = true;
695
+ }
696
+ } else if (def.type === "dynamiczone") {
697
+ populate2[key] = true;
698
+ } else if (def.type === "relation" || def.type === "media") {
699
+ populate2[key] = true;
700
+ }
701
+ }
702
+ return Object.keys(populate2).length > 0 ? populate2 : true;
703
+ }
704
+ function discoverComponentAttributes(strapi2, compUid, prefix, currentDepth, maxDepth, systemFields, allContentTypes, relationsByAttr, allRelationsMeta, attributesMeta) {
705
+ if (currentDepth > maxDepth || !compUid) return [];
706
+ const compSchema = strapi2.components[compUid];
707
+ if (!compSchema?.attributes) return [];
708
+ const discoveredAttrs = [];
709
+ for (const [subKey, subAttr] of Object.entries(compSchema.attributes)) {
710
+ if (systemFields.includes(subKey)) continue;
711
+ if (subAttr.type === "dynamiczone") continue;
712
+ const fullPath = prefix ? `${prefix}.${subKey}` : subKey;
713
+ if (subAttr.type === "component") {
714
+ if (!subAttr.repeatable && subAttr.component) {
715
+ const deeper = discoverComponentAttributes(
716
+ strapi2,
717
+ subAttr.component,
718
+ fullPath,
719
+ currentDepth + 1,
720
+ maxDepth,
721
+ systemFields,
722
+ allContentTypes,
723
+ relationsByAttr,
724
+ allRelationsMeta,
725
+ attributesMeta
726
+ );
727
+ discoveredAttrs.push(...deeper);
728
+ }
729
+ } else if (subAttr.type === "relation") {
730
+ const targetUid = subAttr.target;
731
+ if (targetUid && allContentTypes[targetUid]) {
732
+ const targetInfo = allContentTypes[targetUid]?.info;
733
+ const isMulti = subAttr.relation === "oneToMany" || subAttr.relation === "manyToMany";
734
+ const relMeta = {
735
+ isRelation: true,
736
+ relationType: subAttr.relation,
737
+ targetUid,
738
+ targetDisplayName: targetInfo?.displayName || targetUid,
739
+ isMulti
740
+ };
741
+ relationsByAttr[fullPath] = relMeta;
742
+ allRelationsMeta[fullPath] = relMeta;
743
+ attributesMeta[fullPath] = { depth: currentDepth, type: "relation", component: compUid, isRelation: true };
744
+ discoveredAttrs.push(fullPath);
745
+ }
746
+ } else {
747
+ attributesMeta[fullPath] = { depth: currentDepth, type: subAttr.type, component: compUid };
748
+ discoveredAttrs.push(fullPath);
749
+ }
750
+ }
751
+ return discoveredAttrs;
752
+ }
753
+ function isDeepComponentField(contentType, allComponents, attribute) {
754
+ if (!attribute || !attribute.includes(".")) return false;
755
+ const parts = attribute.split(".");
756
+ let currentSchema = contentType;
757
+ for (let i = 0; i < parts.length - 1; i++) {
758
+ const part = parts[i];
759
+ const attrDef = currentSchema?.attributes?.[part];
760
+ if (!attrDef || attrDef.type !== "component" || !attrDef.component) return false;
761
+ currentSchema = allComponents[attrDef.component];
762
+ if (!currentSchema) return false;
763
+ }
764
+ const leafField = parts[parts.length - 1];
765
+ return Boolean(currentSchema?.attributes?.[leafField]);
766
+ }
767
+ function getLeafAttributeSchema(contentType, allComponents, attribute) {
768
+ if (!attribute) return null;
769
+ if (!attribute.includes(".")) return contentType?.attributes?.[attribute] || null;
770
+ const parts = attribute.split(".");
771
+ let currentSchema = contentType;
772
+ for (let i = 0; i < parts.length - 1; i++) {
773
+ const part = parts[i];
774
+ const attrDef = currentSchema?.attributes?.[part];
775
+ if (!attrDef || attrDef.type !== "component" || !attrDef.component) return null;
776
+ currentSchema = allComponents[attrDef.component];
777
+ if (!currentSchema) return null;
778
+ }
779
+ return currentSchema?.attributes?.[parts[parts.length - 1]] || null;
780
+ }
781
+ function setDeepProperty(obj, pathParts, value) {
782
+ if (pathParts.length === 0) return value;
783
+ const current = { ...obj || {} };
784
+ const [head, ...tail] = pathParts;
785
+ if (tail.length === 0) {
786
+ current[head] = value;
787
+ } else {
788
+ current[head] = setDeepProperty(current[head] || {}, tail, value);
789
+ }
790
+ return current;
791
+ }
792
+ function getDeepProperty(obj, path2) {
793
+ if (!obj || !path2) return void 0;
794
+ if (!path2.includes(".")) return obj[path2];
795
+ return path2.split(".").reduce((acc, part) => acc && typeof acc === "object" ? acc[part] : void 0, obj);
796
+ }
797
+ function buildDeepFilter(pathParts, condition) {
798
+ if (pathParts.length === 0) return condition;
799
+ const [head, ...tail] = pathParts;
800
+ return {
801
+ [head]: buildDeepFilter(tail, condition)
802
+ };
803
+ }
640
804
  function getSafeTimezone(timeZone) {
641
805
  if (!timeZone || typeof timeZone !== "string") {
642
806
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -845,43 +1009,28 @@ async function executeBatchInternal(strapi2, {
845
1009
  const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
846
1010
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
847
1011
  const isNested = attribute.includes(".");
848
- let isComponentNested = false;
849
- let parentKey = "";
850
- let childKey = "";
851
- if (isNested) {
852
- const parts = attribute.split(".");
853
- parentKey = parts[0];
854
- childKey = parts[1];
855
- const parentAttrDef = contentType?.attributes?.[parentKey];
856
- if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
857
- const compSchema = strapi2.components[parentAttrDef.component];
858
- if (compSchema?.attributes?.[childKey]) {
859
- isComponentNested = true;
860
- }
861
- }
862
- }
1012
+ const isComponentNested = isNested && isDeepComponentField(contentType, strapi2.components, attribute);
1013
+ const parts = isNested ? attribute.split(".") : [];
1014
+ const rootKey = isNested ? parts[0] : "";
863
1015
  const attrDef = contentType?.attributes?.[attribute];
864
1016
  const hasAttribute = Boolean(attrDef) || isComponentNested;
865
1017
  if (hasAttribute) {
866
1018
  if (isComponentNested) {
867
1019
  const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
868
1020
  documentId: target.documentId,
869
- populate: [parentKey]
1021
+ populate: buildDeepPopulate$1(contentType.attributes, strapi2.components)
870
1022
  });
871
- const existingParent = currentEntryWithPopulate?.[parentKey] || {};
872
- const currentVal2 = existingParent?.[childKey];
1023
+ const existingRoot = currentEntryWithPopulate?.[rootKey] || {};
1024
+ const currentVal2 = getDeepProperty(existingRoot, parts.slice(1).join("."));
873
1025
  let newValue2 = value;
874
1026
  if (regexObj && typeof currentVal2 === "string") {
875
1027
  newValue2 = currentVal2.replace(regexObj, value);
876
1028
  }
877
- const updatedParent = {
878
- ...existingParent,
879
- [childKey]: newValue2
880
- };
1029
+ const updatedRoot = setDeepProperty(existingRoot, parts.slice(1), newValue2);
881
1030
  await strapi2.documents(target.uid).update({
882
1031
  documentId: target.documentId,
883
1032
  data: {
884
- [parentKey]: updatedParent
1033
+ [rootKey]: updatedRoot
885
1034
  }
886
1035
  });
887
1036
  const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
@@ -1316,12 +1465,14 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1316
1465
  }
1317
1466
  const metaByUid = {};
1318
1467
  const allRelationsMeta = {};
1468
+ const allAttributesMeta = {};
1319
1469
  for (const uid of targetUids) {
1320
1470
  const contentType = allContentTypes[uid];
1321
1471
  if (!contentType || contentType.kind !== "collectionType" && contentType.kind !== "singleType") continue;
1322
1472
  const isSingleType = contentType.kind === "singleType";
1323
1473
  const rawAttrs = contentType.attributes || {};
1324
1474
  const relationsByAttr = {};
1475
+ const colAttributesMeta = {};
1325
1476
  const nestedComponentAttrs = [];
1326
1477
  const validAttrs = Object.keys(rawAttrs).filter((attrKey) => {
1327
1478
  if (systemFields.includes(attrKey)) return false;
@@ -1329,14 +1480,19 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1329
1480
  if (rawAttrs[attrKey].type === "component") {
1330
1481
  const compDef = rawAttrs[attrKey];
1331
1482
  if (!compDef.repeatable && compDef.component) {
1332
- const componentSchema = strapi2.components[compDef.component];
1333
- if (componentSchema && componentSchema.attributes) {
1334
- for (const [subKey, subAttr] of Object.entries(componentSchema.attributes)) {
1335
- if (systemFields.includes(subKey)) continue;
1336
- if (["component", "dynamiczone", "relation"].includes(subAttr.type)) continue;
1337
- nestedComponentAttrs.push(`${attrKey}.${subKey}`);
1338
- }
1339
- }
1483
+ const compAttrs = discoverComponentAttributes(
1484
+ strapi2,
1485
+ compDef.component,
1486
+ attrKey,
1487
+ 1,
1488
+ 5,
1489
+ systemFields,
1490
+ allContentTypes,
1491
+ relationsByAttr,
1492
+ allRelationsMeta,
1493
+ colAttributesMeta
1494
+ );
1495
+ nestedComponentAttrs.push(...compAttrs);
1340
1496
  }
1341
1497
  return false;
1342
1498
  }
@@ -1355,12 +1511,19 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1355
1511
  };
1356
1512
  relationsByAttr[attrKey] = relMeta;
1357
1513
  allRelationsMeta[attrKey] = relMeta;
1514
+ colAttributesMeta[attrKey] = { depth: 0, type: "relation", isRelation: true };
1358
1515
  return true;
1359
1516
  }
1517
+ colAttributesMeta[attrKey] = { depth: 0, type: rawAttrs[attrKey].type };
1360
1518
  return true;
1361
1519
  });
1362
1520
  const allColAttrs = [...validAttrs, ...nestedComponentAttrs];
1363
- allColAttrs.forEach((attr) => allAttributesSet.add(attr));
1521
+ allColAttrs.forEach((attr) => {
1522
+ allAttributesSet.add(attr);
1523
+ if (colAttributesMeta[attr]) {
1524
+ allAttributesMeta[attr] = colAttributesMeta[attr];
1525
+ }
1526
+ });
1364
1527
  metaByUid[uid] = {
1365
1528
  uid,
1366
1529
  kind: contentType.kind,
@@ -1370,11 +1533,13 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1370
1533
  pluralName: contentType.info?.pluralName || uid,
1371
1534
  displayName: contentType.info?.displayName || uid,
1372
1535
  attributes: allColAttrs,
1536
+ attributesMeta: colAttributesMeta,
1373
1537
  relations: relationsByAttr
1374
1538
  };
1375
1539
  }
1376
1540
  staticSchemaCache = {
1377
1541
  attributes: Array.from(allAttributesSet),
1542
+ attributesMeta: allAttributesMeta,
1378
1543
  targetUids: Object.keys(metaByUid),
1379
1544
  metaByUid,
1380
1545
  relationsMeta: allRelationsMeta
@@ -1405,17 +1570,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1405
1570
  let entries = [];
1406
1571
  let totalCount = 0;
1407
1572
  try {
1408
- const ctAttrs = allContentTypes[uid]?.attributes || {};
1409
- const relKeys = Object.keys(meta.relations || {});
1410
- const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
1411
- const populateOptionObj = {};
1412
- relKeys.forEach((k) => {
1413
- populateOptionObj[k] = true;
1414
- });
1415
- compKeys.forEach((k) => {
1416
- populateOptionObj[k] = true;
1417
- });
1418
- const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
1573
+ const populateOption = buildDeepPopulate$1(allContentTypes[uid]?.attributes || {}, strapi2.components);
1419
1574
  if (meta.isSingleType) {
1420
1575
  const singleDoc = await strapi2.documents(uid).findFirst({
1421
1576
  populate: populateOption
@@ -1474,8 +1629,10 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1474
1629
  return {
1475
1630
  success: true,
1476
1631
  attributes: staticSchemaCache.attributes,
1632
+ attributesMeta: staticSchemaCache.attributesMeta,
1477
1633
  collections,
1478
1634
  relationsMeta: staticSchemaCache.relationsMeta,
1635
+ maxDiscoveredDepth: 5,
1479
1636
  executionWindow: {
1480
1637
  ...executionConfig,
1481
1638
  ...windowStatus
@@ -1494,6 +1651,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1494
1651
  page = 1,
1495
1652
  pageSize = 10,
1496
1653
  attribute,
1654
+ searchAttribute,
1497
1655
  filterOperator,
1498
1656
  filterQuery,
1499
1657
  search,
@@ -1508,8 +1666,9 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1508
1666
  const currentPage = Math.max(Number(page) || 1, 1);
1509
1667
  const start = (currentPage - 1) * limit;
1510
1668
  const isSingleType = contentType.kind === "singleType";
1669
+ const targetFilterAttr = searchAttribute || attribute;
1511
1670
  const signature = await getCollectionSignature(strapi2, uid);
1512
- const cacheKey = `${uid}:${limit}:${start}:${attribute || ""}:${filterOperator || ""}:${filterQuery || ""}:${search || ""}:${sortField || ""}:${sortOrder}`;
1671
+ const cacheKey = `${uid}:${limit}:${start}:${attribute || ""}:${searchAttribute || ""}:${filterOperator || ""}:${filterQuery || ""}:${search || ""}:${sortField || ""}:${sortOrder}`;
1513
1672
  const cached = queryCache.get(cacheKey);
1514
1673
  const etag = `W/"entry-${crypto.createHash("md5").update(`${cacheKey}:${signature}`).digest("hex")}"`;
1515
1674
  if (cached && cached.signature === signature && Date.now() - cached.timestamp < CACHE_TTL_MS) {
@@ -1524,19 +1683,15 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1524
1683
  const attrs = contentType.attributes || {};
1525
1684
  let colHasAttribute = false;
1526
1685
  let isNestedAttr = false;
1527
- let parentKey = "";
1528
- let childKey = "";
1529
- if (attribute) {
1530
- if (attribute.includes(".")) {
1531
- [parentKey, childKey] = attribute.split(".");
1532
- if (attrs[parentKey]?.type === "component" && !attrs[parentKey].repeatable && attrs[parentKey].component) {
1533
- const compSchema = strapi2.components[attrs[parentKey].component];
1534
- if (compSchema?.attributes?.[childKey]) {
1535
- colHasAttribute = true;
1536
- isNestedAttr = true;
1537
- }
1686
+ let parts = [];
1687
+ if (targetFilterAttr) {
1688
+ if (targetFilterAttr.includes(".")) {
1689
+ isNestedAttr = isDeepComponentField(contentType, strapi2.components, targetFilterAttr);
1690
+ if (isNestedAttr) {
1691
+ colHasAttribute = true;
1692
+ parts = targetFilterAttr.split(".");
1538
1693
  }
1539
- } else if (attrs[attribute]) {
1694
+ } else if (attrs[targetFilterAttr]) {
1540
1695
  colHasAttribute = true;
1541
1696
  }
1542
1697
  }
@@ -1561,7 +1716,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1561
1716
  }
1562
1717
  let condition = void 0;
1563
1718
  const q = (filterQuery || "").trim();
1564
- const targetAttrSchema = isNestedAttr ? strapi2.components[attrs[parentKey]?.component]?.attributes?.[childKey] : attrs[attribute];
1719
+ const targetAttrSchema = isNestedAttr ? getLeafAttributeSchema(contentType, strapi2.components, targetFilterAttr) : attrs[targetFilterAttr];
1565
1720
  const isStringLike = ["string", "text", "email", "richtext", "uid"].includes(targetAttrSchema?.type);
1566
1721
  if (normOp === "empty") {
1567
1722
  condition = isStringLike ? { $or: [{ $null: true }, { $eq: "" }] } : { $null: true };
@@ -1591,19 +1746,18 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1591
1746
  }
1592
1747
  if (condition !== void 0) {
1593
1748
  if (isNestedAttr) {
1594
- filters2[parentKey] = {
1595
- [childKey]: condition
1596
- };
1749
+ const deepFilter = buildDeepFilter(parts, condition);
1750
+ Object.assign(filters2, deepFilter);
1597
1751
  } else {
1598
1752
  if (normOp === "empty" && isStringLike) {
1599
1753
  filters2.$or = [
1600
- { [attribute]: { $null: true } },
1601
- { [attribute]: { $eq: "" } }
1754
+ { [targetFilterAttr]: { $null: true } },
1755
+ { [targetFilterAttr]: { $eq: "" } }
1602
1756
  ];
1603
1757
  } else if (normOp === "notempty" && isStringLike) {
1604
- filters2[attribute] = { $notNull: true, $ne: "" };
1758
+ filters2[targetFilterAttr] = { $notNull: true, $ne: "" };
1605
1759
  } else {
1606
- filters2[attribute] = condition;
1760
+ filters2[targetFilterAttr] = condition;
1607
1761
  }
1608
1762
  }
1609
1763
  }
@@ -1626,11 +1780,13 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1626
1780
  }
1627
1781
  }
1628
1782
  }
1783
+ const populateOption = buildDeepPopulate$1(contentType.attributes, strapi2.components);
1629
1784
  if (isSingleType) {
1630
1785
  let singleDoc = null;
1631
1786
  try {
1632
1787
  singleDoc = await strapi2.documents(uid).findFirst({
1633
- filters: Object.keys(filters2).length > 0 ? filters2 : void 0
1788
+ filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
1789
+ populate: populateOption
1634
1790
  });
1635
1791
  } catch {
1636
1792
  singleDoc = null;
@@ -1649,11 +1805,11 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1649
1805
  }
1650
1806
  };
1651
1807
  } else {
1652
- const sortClause = {};
1808
+ let sortClause = {};
1653
1809
  if (sortField && sortField.includes(".")) {
1654
- const [parent, child] = sortField.split(".");
1655
- if (contentType.attributes?.[parent]?.type === "component") {
1656
- sortClause[parent] = { [child]: sortOrder };
1810
+ const sortParts = sortField.split(".");
1811
+ if (isDeepComponentField(contentType, strapi2.components, sortField)) {
1812
+ sortClause = buildDeepFilter(sortParts, sortOrder);
1657
1813
  } else {
1658
1814
  sortClause["createdAt"] = "desc";
1659
1815
  }
@@ -1662,10 +1818,6 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1662
1818
  } else {
1663
1819
  sortClause["createdAt"] = "desc";
1664
1820
  }
1665
- const populateAttrs = Object.keys(contentType.attributes || {}).filter(
1666
- (k) => contentType.attributes[k].type === "relation" || contentType.attributes[k].type === "component"
1667
- );
1668
- const populateOption = populateAttrs.length > 0 ? populateAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1669
1821
  const [entries, totalEntries] = await Promise.all([
1670
1822
  strapi2.documents(uid).findMany({
1671
1823
  start,
@@ -1787,21 +1939,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1787
1939
  const displayName = contentType?.info?.displayName || target.uid;
1788
1940
  try {
1789
1941
  const isNested = attribute.includes(".");
1790
- let isComponentNested = false;
1791
- let parentKey = "";
1792
- let childKey = "";
1793
- if (isNested) {
1794
- const parts = attribute.split(".");
1795
- parentKey = parts[0];
1796
- childKey = parts[1];
1797
- const parentAttrDef = contentType?.attributes?.[parentKey];
1798
- if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
1799
- const compSchema = strapi2.components[parentAttrDef.component];
1800
- if (compSchema?.attributes?.[childKey]) {
1801
- isComponentNested = true;
1802
- }
1803
- }
1804
- }
1942
+ const isComponentNested = isNested && isDeepComponentField(contentType, strapi2.components, attribute);
1805
1943
  const attrDef = contentType?.attributes?.[attribute];
1806
1944
  const hasAttribute = Boolean(attrDef) || isComponentNested;
1807
1945
  if (!hasAttribute) {
@@ -1826,7 +1964,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1826
1964
  if (isComponentNested) {
1827
1965
  currentEntry = await strapi2.documents(target.uid).findOne({
1828
1966
  documentId: target.documentId,
1829
- populate: [parentKey]
1967
+ populate: buildDeepPopulate$1(contentType.attributes, strapi2.components)
1830
1968
  });
1831
1969
  } else if (attrDef?.type === "relation") {
1832
1970
  currentEntry = await strapi2.documents(target.uid).findOne({
@@ -1840,8 +1978,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1840
1978
  }
1841
1979
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
1842
1980
  if (isComponentNested) {
1843
- const existingParent = currentEntry?.[parentKey] || {};
1844
- const currentVal2 = existingParent?.[childKey];
1981
+ const currentVal2 = getDeepProperty(currentEntry, attribute);
1845
1982
  let newValue2 = value;
1846
1983
  if (regexObj && typeof currentVal2 === "string") {
1847
1984
  newValue2 = currentVal2.replace(regexObj, value);
@@ -2581,7 +2718,11 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2581
2718
  function escapeCsvVal(val) {
2582
2719
  if (val === null || val === void 0) return "";
2583
2720
  let str2 = typeof val === "object" ? JSON.stringify(val) : String(val);
2584
- if (str2.includes(",") || str2.includes('"') || str2.includes("\n") || str2.includes("\r")) {
2721
+ str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2722
+ if (/^[=+@-]$/.test(str2.charAt(0))) {
2723
+ str2 = `'${str2}`;
2724
+ }
2725
+ if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
2585
2726
  return `"${str2.replace(/"/g, '""')}"`;
2586
2727
  }
2587
2728
  return str2;
@@ -2625,7 +2766,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2625
2766
  return {
2626
2767
  success: true,
2627
2768
  filename,
2628
- csvContent: rows.join("\n"),
2769
+ csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
2629
2770
  summary: {
2630
2771
  batchId: batch.id,
2631
2772
  status: batch.status,
@@ -3449,23 +3590,34 @@ const validationRulesService = ({ strapi: strapi2 }) => ({
3449
3590
  function escapeCsvValue(val) {
3450
3591
  if (val === null || val === void 0) return "";
3451
3592
  let str2 = "";
3452
- if (typeof val === "object") {
3593
+ if (val instanceof Date) {
3594
+ str2 = val.toISOString();
3595
+ } else if (typeof val === "object") {
3453
3596
  str2 = JSON.stringify(val);
3454
3597
  } else {
3455
3598
  str2 = String(val);
3456
3599
  }
3457
- if (str2.includes(",") || str2.includes('"') || str2.includes("\n") || str2.includes("\r")) {
3600
+ str2 = str2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
3601
+ if (/^[=+@-]$/.test(str2.charAt(0))) {
3602
+ str2 = `'${str2}`;
3603
+ }
3604
+ if (str2.includes(",") || str2.includes('"') || str2.includes("\n")) {
3458
3605
  return `"${str2.replace(/"/g, '""')}"`;
3459
3606
  }
3460
3607
  return str2;
3461
3608
  }
3462
3609
  function parseCsv(csvText) {
3610
+ if (!csvText) return [];
3611
+ let cleanText = csvText;
3612
+ if (cleanText.charCodeAt(0) === 65279) {
3613
+ cleanText = cleanText.slice(1);
3614
+ }
3463
3615
  const lines = [];
3464
3616
  let current = "";
3465
3617
  let inQuotes = false;
3466
- for (let i = 0; i < csvText.length; i++) {
3467
- const char = csvText[i];
3468
- const nextChar = csvText[i + 1];
3618
+ for (let i = 0; i < cleanText.length; i++) {
3619
+ const char = cleanText[i];
3620
+ const nextChar = cleanText[i + 1];
3469
3621
  if (char === '"') {
3470
3622
  if (inQuotes && nextChar === '"') {
3471
3623
  current += '"';
@@ -3525,6 +3677,103 @@ function parseCsv(csvText) {
3525
3677
  }
3526
3678
  return rows;
3527
3679
  }
3680
+ function discoverExportFields(ct, allComponents, maxDepth = 4) {
3681
+ const fields2 = [{ key: "documentId", label: "documentId", type: "string" }];
3682
+ const excluded = ["createdBy", "updatedBy", "localizations"];
3683
+ function walkComponent(compUid, pathPrefix, labelPrefix, depth2) {
3684
+ if (depth2 >= maxDepth) return;
3685
+ const comp = allComponents[compUid];
3686
+ if (!comp?.attributes) return;
3687
+ for (const [subKey, subDef] of Object.entries(comp.attributes)) {
3688
+ if (excluded.includes(subKey)) continue;
3689
+ const keyPath = `${pathPrefix}.${subKey}`;
3690
+ const displayLabel = `${labelPrefix} > ${subKey}`;
3691
+ if (subDef.type === "component" && subDef.component) {
3692
+ if (!subDef.repeatable) {
3693
+ walkComponent(subDef.component, keyPath, displayLabel, depth2 + 1);
3694
+ } else {
3695
+ fields2.push({ key: keyPath, label: displayLabel, type: "component" });
3696
+ }
3697
+ } else {
3698
+ fields2.push({ key: keyPath, label: displayLabel, type: subDef.type });
3699
+ }
3700
+ }
3701
+ }
3702
+ for (const [attrName, attrDef] of Object.entries(ct.attributes || {})) {
3703
+ if (excluded.includes(attrName)) continue;
3704
+ if (attrDef.type === "component" && attrDef.component) {
3705
+ if (!attrDef.repeatable) {
3706
+ walkComponent(attrDef.component, attrName, attrName, 0);
3707
+ } else {
3708
+ fields2.push({ key: attrName, label: attrName, type: "component" });
3709
+ }
3710
+ } else {
3711
+ fields2.push({ key: attrName, label: attrName, type: attrDef.type });
3712
+ }
3713
+ }
3714
+ return fields2;
3715
+ }
3716
+ function buildDeepPopulate(attributes, allComponents, depth2 = 0) {
3717
+ if (depth2 > 4) return true;
3718
+ const populate2 = {};
3719
+ for (const [key, def] of Object.entries(attributes || {})) {
3720
+ if (def.type === "component" && def.component) {
3721
+ const comp = allComponents[def.component];
3722
+ if (comp?.attributes) {
3723
+ populate2[key] = {
3724
+ populate: buildDeepPopulate(comp.attributes, allComponents, depth2 + 1)
3725
+ };
3726
+ } else {
3727
+ populate2[key] = true;
3728
+ }
3729
+ } else if (def.type === "dynamiczone") {
3730
+ populate2[key] = true;
3731
+ } else if (def.type === "relation" || def.type === "media") {
3732
+ populate2[key] = true;
3733
+ }
3734
+ }
3735
+ return Object.keys(populate2).length > 0 ? populate2 : true;
3736
+ }
3737
+ function getNestedExportValue(entry, path2) {
3738
+ if (!entry || !path2) return "";
3739
+ const parts = path2.split(".");
3740
+ let current = entry;
3741
+ for (let i = 0; i < parts.length; i++) {
3742
+ const part = parts[i];
3743
+ if (current === null || current === void 0) return "";
3744
+ if (Array.isArray(current)) {
3745
+ const subPath = parts.slice(i).join(".");
3746
+ return current.map((item) => {
3747
+ if (!item) return "";
3748
+ if (typeof item === "object") {
3749
+ const val = getNestedExportValue(item, subPath);
3750
+ return typeof val === "object" ? JSON.stringify(val) : String(val ?? "");
3751
+ }
3752
+ return String(item);
3753
+ }).filter(Boolean).join("; ");
3754
+ }
3755
+ current = current[part];
3756
+ }
3757
+ if (current === null || current === void 0) return "";
3758
+ if (typeof current === "object" && current.url) {
3759
+ return current.url;
3760
+ }
3761
+ if (typeof current === "object" && !Array.isArray(current)) {
3762
+ if (current.documentId || current.name || current.title) {
3763
+ return current.name || current.title || current.documentId;
3764
+ }
3765
+ }
3766
+ if (Array.isArray(current)) {
3767
+ return current.map((item) => {
3768
+ if (!item) return "";
3769
+ if (typeof item === "object") {
3770
+ return item.name || item.title || item.documentId || JSON.stringify(item);
3771
+ }
3772
+ return String(item);
3773
+ }).filter(Boolean).join("; ");
3774
+ }
3775
+ return current;
3776
+ }
3528
3777
  const csvService = ({ strapi: strapi2 }) => ({
3529
3778
  /**
3530
3779
  * Generates a sample template CSV with schema attribute headers and sample values
@@ -3573,9 +3822,7 @@ const csvService = ({ strapi: strapi2 }) => ({
3573
3822
  }
3574
3823
  sampleRow.push(sampleVal);
3575
3824
  }
3576
- const csvContent = `${headers.map(escapeCsvValue).join(",")}
3577
- ${sampleRow.map(escapeCsvValue).join(",")}
3578
- `;
3825
+ const csvContent = "\uFEFF" + headers.map(escapeCsvValue).join(",") + "\r\n" + sampleRow.map(escapeCsvValue).join(",") + "\r\n";
3579
3826
  return {
3580
3827
  filename: `${ct.info?.singularName || "template"}_template.csv`,
3581
3828
  csvContent,
@@ -3583,65 +3830,60 @@ ${sampleRow.map(escapeCsvValue).join(",")}
3583
3830
  };
3584
3831
  },
3585
3832
  /**
3586
- * Exports entries of a collection to CSV, with optional filters
3833
+ * Exports entries of a collection to CSV with deep nested component support,
3834
+ * RFC-4180 CRLF line endings, and UTF-8 BOM for 100% Microsoft Excel compatibility.
3587
3835
  */
3588
3836
  async exportCollectionCsv(uid, options = {}) {
3589
3837
  const ct = strapi2.contentTypes[uid];
3590
3838
  if (!ct) {
3591
3839
  throw new Error(`Content type ${uid} not found`);
3592
3840
  }
3593
- const attrs = ct.attributes || {};
3594
- const excluded = ["createdBy", "updatedBy", "localizations"];
3595
- const exportFields = ["documentId"];
3596
- const compFieldsToPopulate = [];
3597
- for (const [attrName, attrDef] of Object.entries(attrs)) {
3598
- if (excluded.includes(attrName)) continue;
3599
- if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
3600
- if (attrDef.type === "component") {
3601
- if (!attrDef.repeatable && attrDef.component) {
3602
- const comp = strapi2.components[attrDef.component];
3603
- if (comp?.attributes) {
3604
- compFieldsToPopulate.push(attrName);
3605
- for (const [subKey, subDef] of Object.entries(comp.attributes)) {
3606
- if (excluded.includes(subKey)) continue;
3607
- if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
3608
- exportFields.push(`${attrName}.${subKey}`);
3609
- }
3610
- }
3611
- }
3612
- continue;
3613
- }
3614
- exportFields.push(attrName);
3615
- }
3841
+ const allComponents = strapi2.components || {};
3842
+ const fields2 = discoverExportFields(ct, allComponents);
3616
3843
  const filters2 = {};
3617
- const { attribute, filterOperator, filterQuery, search } = options;
3618
- if (attribute && filterOperator && filterQuery !== void 0 && filterQuery !== "") {
3619
- switch (filterOperator) {
3620
- case "equals":
3621
- filters2[attribute] = { $eq: filterQuery };
3622
- break;
3623
- case "notEquals":
3624
- filters2[attribute] = { $ne: filterQuery };
3625
- break;
3626
- case "contains":
3627
- filters2[attribute] = { $containsi: filterQuery };
3628
- break;
3629
- case "notContains":
3630
- filters2[attribute] = { $notContainsi: filterQuery };
3631
- break;
3632
- case "startsWith":
3633
- filters2[attribute] = { $startsWith: filterQuery };
3634
- break;
3635
- case "endsWith":
3636
- filters2[attribute] = { $endsWith: filterQuery };
3637
- break;
3844
+ const { attribute, filterOperator, filterQuery, search, documentIds } = options;
3845
+ if (documentIds && Array.isArray(documentIds) && documentIds.length > 0) {
3846
+ filters2.documentId = { $in: documentIds };
3847
+ }
3848
+ if (attribute && filterOperator && filterOperator !== "none") {
3849
+ const isNested = attribute.includes(".");
3850
+ if (filterOperator === "empty") {
3851
+ if (isNested) {
3852
+ const [parent, child] = attribute.split(".");
3853
+ filters2[parent] = { [child]: { $null: true } };
3854
+ } else {
3855
+ filters2[attribute] = { $null: true };
3856
+ }
3857
+ } else if (filterOperator === "not_empty") {
3858
+ if (isNested) {
3859
+ const [parent, child] = attribute.split(".");
3860
+ filters2[parent] = { [child]: { $notNull: true } };
3861
+ } else {
3862
+ filters2[attribute] = { $notNull: true };
3863
+ }
3864
+ } else if (filterQuery !== void 0 && filterQuery !== "") {
3865
+ const opMap = {
3866
+ equals: "$eq",
3867
+ notEquals: "$ne",
3868
+ contains: "$containsi",
3869
+ notContains: "$notContainsi",
3870
+ startsWith: "$startsWith",
3871
+ endsWith: "$endsWith"
3872
+ };
3873
+ const strapiOp = opMap[filterOperator] || "$eq";
3874
+ if (isNested) {
3875
+ const [parent, child] = attribute.split(".");
3876
+ filters2[parent] = { [child]: { [strapiOp]: filterQuery } };
3877
+ } else {
3878
+ filters2[attribute] = { [strapiOp]: filterQuery };
3879
+ }
3638
3880
  }
3639
3881
  }
3640
3882
  if (search && search.trim() !== "") {
3641
3883
  const q = search.trim();
3642
3884
  const searchConditions = [];
3643
3885
  const searchFields = ["name", "title", "email", "slug", "description"].filter(
3644
- (f) => attrs[f] && ["string", "text", "email"].includes(attrs[f].type)
3886
+ (f) => ct.attributes?.[f] && ["string", "text", "email"].includes(ct.attributes[f].type)
3645
3887
  );
3646
3888
  for (const field of searchFields) {
3647
3889
  searchConditions.push({ [field]: { $containsi: q } });
@@ -3654,34 +3896,37 @@ ${sampleRow.map(escapeCsvValue).join(",")}
3654
3896
  }
3655
3897
  }
3656
3898
  }
3657
- const populateOption = compFieldsToPopulate.length > 0 ? compFieldsToPopulate.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
3658
- const entries = await strapi2.documents(uid).findMany({
3659
- filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
3660
- limit: options.limit || 5e3,
3661
- sort: { createdAt: "desc" },
3662
- populate: populateOption
3663
- });
3899
+ const deepPopulate = buildDeepPopulate(ct.attributes || {}, allComponents);
3900
+ let entries = [];
3901
+ if (ct.kind === "singleType") {
3902
+ const doc = await strapi2.documents(uid).findFirst({
3903
+ populate: deepPopulate
3904
+ });
3905
+ entries = doc ? [doc] : [];
3906
+ } else {
3907
+ entries = await strapi2.documents(uid).findMany({
3908
+ filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
3909
+ limit: options.limit || 5e3,
3910
+ sort: { createdAt: "desc" },
3911
+ populate: deepPopulate
3912
+ });
3913
+ }
3664
3914
  const lines = [];
3665
- lines.push(exportFields.map(escapeCsvValue).join(","));
3915
+ lines.push(fields2.map((f) => escapeCsvValue(f.key)).join(","));
3666
3916
  for (const entry of entries || []) {
3667
- const rowValues = exportFields.map((field) => {
3668
- let val;
3669
- if (field.includes(".")) {
3670
- const [parent, child] = field.split(".");
3671
- val = entry[parent]?.[child];
3672
- } else {
3673
- val = entry[field];
3674
- }
3917
+ const rowValues = fields2.map((f) => {
3918
+ const val = getNestedExportValue(entry, f.key);
3675
3919
  return escapeCsvValue(val);
3676
3920
  });
3677
3921
  lines.push(rowValues.join(","));
3678
3922
  }
3679
- const csvContent = lines.join("\n");
3923
+ const csvContent = "\uFEFF" + lines.join("\r\n") + "\r\n";
3680
3924
  const filename = `${ct.info?.singularName || uid}_export_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
3681
3925
  return {
3682
3926
  filename,
3683
3927
  csvContent,
3684
- count: entries ? entries.length : 0
3928
+ count: entries ? entries.length : 0,
3929
+ fields: fields2.map((f) => f.key)
3685
3930
  };
3686
3931
  },
3687
3932
  /**
@@ -3721,11 +3966,76 @@ ${sampleRow.map(escapeCsvValue).join(",")}
3721
3966
  const filename = `violations_${safeRuleName}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
3722
3967
  return {
3723
3968
  filename,
3724
- csvContent: lines.join("\n"),
3969
+ csvContent: "\uFEFF" + lines.join("\r\n") + "\r\n",
3725
3970
  count: scanReport.violations.length,
3726
3971
  ruleName: scanReport.rule.name
3727
3972
  };
3728
3973
  },
3974
+ /**
3975
+ * Bulk Export: Exports selected entries across multiple collections or a multi-collection batch.
3976
+ */
3977
+ async exportBulkCsv(selectedEntries) {
3978
+ if (!selectedEntries || selectedEntries.length === 0) {
3979
+ throw new Error("No entries selected for bulk export");
3980
+ }
3981
+ const byUid = {};
3982
+ for (const item of selectedEntries) {
3983
+ if (!byUid[item.uid]) byUid[item.uid] = [];
3984
+ byUid[item.uid].push(item.documentId);
3985
+ }
3986
+ const uids = Object.keys(byUid);
3987
+ if (uids.length === 1) {
3988
+ const uid = uids[0];
3989
+ return await this.exportCollectionCsv(uid, { documentIds: byUid[uid] });
3990
+ }
3991
+ const rows = [];
3992
+ const headers = [
3993
+ "Collection UID",
3994
+ "Collection Name",
3995
+ "Document ID",
3996
+ "Record Title / Name",
3997
+ "Primary Identifier",
3998
+ "Created At",
3999
+ "Updated At",
4000
+ "Published Status"
4001
+ ];
4002
+ rows.push(headers.map(escapeCsvValue).join(","));
4003
+ let totalExported = 0;
4004
+ for (const uid of uids) {
4005
+ const ct = strapi2.contentTypes[uid];
4006
+ if (!ct) continue;
4007
+ const displayName = ct.info?.displayName || uid;
4008
+ const docIds = byUid[uid];
4009
+ const entries = await strapi2.documents(uid).findMany({
4010
+ filters: { documentId: { $in: docIds } },
4011
+ limit: docIds.length
4012
+ });
4013
+ for (const entry of entries || []) {
4014
+ const titleOrName = entry.title || entry.name || entry.subject || entry.email || `Entry #${entry.documentId}`;
4015
+ const identifier = entry.slug || entry.email || entry.username || entry.documentId;
4016
+ const isPublished = Boolean(entry.publishedAt);
4017
+ const row = [
4018
+ uid,
4019
+ displayName,
4020
+ entry.documentId,
4021
+ titleOrName,
4022
+ identifier,
4023
+ entry.createdAt || "",
4024
+ entry.updatedAt || "",
4025
+ isPublished ? "Published" : "Draft"
4026
+ ];
4027
+ rows.push(row.map(escapeCsvValue).join(","));
4028
+ totalExported++;
4029
+ }
4030
+ }
4031
+ const filename = `bulk_export_selected_${totalExported}_entries_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
4032
+ return {
4033
+ filename,
4034
+ csvContent: "\uFEFF" + rows.join("\r\n") + "\r\n",
4035
+ count: totalExported,
4036
+ isMultiCollection: true
4037
+ };
4038
+ },
3729
4039
  /**
3730
4040
  * Imports CSV records to update existing records or insert/override records
3731
4041
  */