strapi-plugin-bulk-updater 1.1.0 → 1.2.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.
@@ -211,7 +211,7 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
211
211
  return;
212
212
  }
213
213
  }
214
- ctx.body = data;
214
+ ctx.body = { success: true, ...data };
215
215
  } catch (err) {
216
216
  ctx.status = 500;
217
217
  ctx.body = { success: false, error: err.message };
@@ -660,9 +660,69 @@ async function executeBatchInternal(strapi2, {
660
660
  try {
661
661
  const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
662
662
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
663
+ const isNested = attribute.includes(".");
664
+ let isComponentNested = false;
665
+ let parentKey = "";
666
+ let childKey = "";
667
+ if (isNested) {
668
+ const parts = attribute.split(".");
669
+ parentKey = parts[0];
670
+ childKey = parts[1];
671
+ const parentAttrDef = contentType?.attributes?.[parentKey];
672
+ if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
673
+ const compSchema = strapi2.components[parentAttrDef.component];
674
+ if (compSchema?.attributes?.[childKey]) {
675
+ isComponentNested = true;
676
+ }
677
+ }
678
+ }
663
679
  const attrDef = contentType?.attributes?.[attribute];
664
- const hasAttribute = Boolean(attrDef);
680
+ const hasAttribute = Boolean(attrDef) || isComponentNested;
665
681
  if (hasAttribute) {
682
+ if (isComponentNested) {
683
+ const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
684
+ documentId: target.documentId,
685
+ populate: [parentKey]
686
+ });
687
+ const existingParent = currentEntryWithPopulate?.[parentKey] || {};
688
+ const currentVal2 = existingParent?.[childKey];
689
+ let newValue2 = value;
690
+ if (regexObj && typeof currentVal2 === "string") {
691
+ newValue2 = currentVal2.replace(regexObj, value);
692
+ }
693
+ const updatedParent = {
694
+ ...existingParent,
695
+ [childKey]: newValue2
696
+ };
697
+ await strapi2.documents(target.uid).update({
698
+ documentId: target.documentId,
699
+ data: {
700
+ [parentKey]: updatedParent
701
+ }
702
+ });
703
+ const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
704
+ if (publishOnApproval && isDraftAndPublishComp) {
705
+ try {
706
+ await strapi2.documents(target.uid).publish({
707
+ documentId: target.documentId
708
+ });
709
+ } catch (publishErr) {
710
+ strapi2.log.warn(`[bulk-updater] Failed to auto-publish ${target.uid}/${target.documentId}:`, publishErr);
711
+ }
712
+ }
713
+ invalidateCollectionCache(target.uid);
714
+ return {
715
+ uid: target.uid,
716
+ collectionName: displayName,
717
+ documentId: target.documentId,
718
+ label: entryLabel,
719
+ attribute,
720
+ oldValue: currentVal2 ?? null,
721
+ newValue: newValue2,
722
+ status: "updated",
723
+ message: `Successfully updated nested field ${attribute}`
724
+ };
725
+ }
666
726
  if (attrDef.type === "relation") {
667
727
  const isMulti = attrDef.relation === "oneToMany" || attrDef.relation === "manyToMany";
668
728
  const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
@@ -904,9 +964,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
904
964
  const isSingleType = contentType.kind === "singleType";
905
965
  const rawAttrs = contentType.attributes || {};
906
966
  const relationsByAttr = {};
967
+ const nestedComponentAttrs = [];
907
968
  const validAttrs = Object.keys(rawAttrs).filter((attrKey) => {
908
969
  if (systemFields.includes(attrKey)) return false;
909
- if (rawAttrs[attrKey].type === "component" || rawAttrs[attrKey].type === "dynamiczone") return false;
970
+ if (rawAttrs[attrKey].type === "dynamiczone") return false;
971
+ if (rawAttrs[attrKey].type === "component") {
972
+ const compDef = rawAttrs[attrKey];
973
+ if (!compDef.repeatable && compDef.component) {
974
+ const componentSchema = strapi2.components[compDef.component];
975
+ if (componentSchema && componentSchema.attributes) {
976
+ for (const [subKey, subAttr] of Object.entries(componentSchema.attributes)) {
977
+ if (systemFields.includes(subKey)) continue;
978
+ if (["component", "dynamiczone", "relation"].includes(subAttr.type)) continue;
979
+ nestedComponentAttrs.push(`${attrKey}.${subKey}`);
980
+ }
981
+ }
982
+ }
983
+ return false;
984
+ }
910
985
  if (rawAttrs[attrKey].type === "relation") {
911
986
  const rel = rawAttrs[attrKey];
912
987
  const targetUid = rel.target;
@@ -926,7 +1001,8 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
926
1001
  }
927
1002
  return true;
928
1003
  });
929
- validAttrs.forEach((attr) => allAttributesSet.add(attr));
1004
+ const allColAttrs = [...validAttrs, ...nestedComponentAttrs];
1005
+ allColAttrs.forEach((attr) => allAttributesSet.add(attr));
930
1006
  metaByUid[uid] = {
931
1007
  uid,
932
1008
  kind: contentType.kind,
@@ -934,7 +1010,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
934
1010
  singularName: contentType.info?.singularName || uid,
935
1011
  pluralName: contentType.info?.pluralName || uid,
936
1012
  displayName: contentType.info?.displayName || uid,
937
- attributes: validAttrs,
1013
+ attributes: allColAttrs,
938
1014
  relations: relationsByAttr
939
1015
  };
940
1016
  }
@@ -963,8 +1039,17 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
963
1039
  let entries = [];
964
1040
  let totalCount = 0;
965
1041
  try {
1042
+ const ctAttrs = allContentTypes[uid]?.attributes || {};
966
1043
  const relKeys = Object.keys(meta.relations || {});
967
- const populateOption = relKeys.length > 0 ? relKeys.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1044
+ const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
1045
+ const populateOptionObj = {};
1046
+ relKeys.forEach((k) => {
1047
+ populateOptionObj[k] = true;
1048
+ });
1049
+ compKeys.forEach((k) => {
1050
+ populateOptionObj[k] = true;
1051
+ });
1052
+ const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
968
1053
  if (meta.isSingleType) {
969
1054
  const singleDoc = await strapi2.documents(uid).findFirst({
970
1055
  populate: populateOption
@@ -1042,7 +1127,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1042
1127
  if (!contentType) {
1043
1128
  throw new Error(`Collection with UID "${uid}" does not exist.`);
1044
1129
  }
1045
- const limit = Math.min(Math.max(Number(pageSize) || 10, 1), 100);
1130
+ const limit = Math.min(Math.max(Number(pageSize) || 10, 1), 500);
1046
1131
  const currentPage = Math.max(Number(page) || 1, 1);
1047
1132
  const start = (currentPage - 1) * limit;
1048
1133
  const isSingleType = contentType.kind === "singleType";
@@ -1058,8 +1143,118 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1058
1143
  };
1059
1144
  }
1060
1145
  let result;
1146
+ const filters2 = {};
1147
+ const attrs = contentType.attributes || {};
1148
+ let colHasAttribute = false;
1149
+ let isNestedAttr = false;
1150
+ let parentKey = "";
1151
+ let childKey = "";
1152
+ if (attribute) {
1153
+ if (attribute.includes(".")) {
1154
+ [parentKey, childKey] = attribute.split(".");
1155
+ if (attrs[parentKey]?.type === "component" && !attrs[parentKey].repeatable && attrs[parentKey].component) {
1156
+ const compSchema = strapi2.components[attrs[parentKey].component];
1157
+ if (compSchema?.attributes?.[childKey]) {
1158
+ colHasAttribute = true;
1159
+ isNestedAttr = true;
1160
+ }
1161
+ }
1162
+ } else if (attrs[attribute]) {
1163
+ colHasAttribute = true;
1164
+ }
1165
+ }
1166
+ const normOp = (filterOperator || "").toLowerCase().replace(/_/g, "");
1167
+ if (normOp && normOp !== "none") {
1168
+ if (!colHasAttribute) {
1169
+ result = {
1170
+ uid,
1171
+ displayName: contentType.info?.displayName || uid,
1172
+ isSingleType,
1173
+ entries: [],
1174
+ pagination: {
1175
+ page: 1,
1176
+ pageSize: limit,
1177
+ total: 0,
1178
+ pageCount: 0
1179
+ }
1180
+ };
1181
+ queryCache.set(cacheKey, { signature, data: result, timestamp: Date.now() });
1182
+ return { ...result, cached: false, etag };
1183
+ }
1184
+ let condition = void 0;
1185
+ const q = (filterQuery || "").trim();
1186
+ if (normOp === "empty") {
1187
+ condition = { $or: [{ $null: true }, { $eq: "" }] };
1188
+ } else if (normOp === "notempty") {
1189
+ condition = { $and: [{ $notNull: true }, { $ne: "" }] };
1190
+ } else if (q !== "") {
1191
+ switch (normOp) {
1192
+ case "equals":
1193
+ condition = { $containsi: q };
1194
+ break;
1195
+ case "notequals":
1196
+ condition = { $notContainsi: q };
1197
+ break;
1198
+ case "contains":
1199
+ condition = { $containsi: q };
1200
+ break;
1201
+ case "notcontains":
1202
+ condition = { $notContainsi: q };
1203
+ break;
1204
+ case "startswith":
1205
+ condition = { $startsWith: q };
1206
+ break;
1207
+ case "endswith":
1208
+ condition = { $endsWith: q };
1209
+ break;
1210
+ }
1211
+ }
1212
+ if (condition !== void 0) {
1213
+ if (isNestedAttr) {
1214
+ filters2[parentKey] = {
1215
+ [childKey]: condition
1216
+ };
1217
+ } else {
1218
+ if (normOp === "empty") {
1219
+ filters2.$or = [
1220
+ { [attribute]: { $null: true } },
1221
+ { [attribute]: { $eq: "" } }
1222
+ ];
1223
+ } else if (normOp === "notempty") {
1224
+ filters2[attribute] = { $notNull: true, $ne: "" };
1225
+ } else {
1226
+ filters2[attribute] = condition;
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+ if (search && search.trim() !== "") {
1232
+ const q = search.trim();
1233
+ const searchableAttrs = Object.keys(attrs).filter(
1234
+ (k) => ["string", "text", "email", "uid"].includes(attrs[k]?.type)
1235
+ );
1236
+ if (searchableAttrs.length > 0) {
1237
+ const orList = searchableAttrs.slice(0, 6).map((k) => ({
1238
+ [k]: { $containsi: q }
1239
+ }));
1240
+ if (Object.keys(filters2).length > 0) {
1241
+ const existingFilters = { ...filters2 };
1242
+ Object.keys(filters2).forEach((k) => delete filters2[k]);
1243
+ filters2.$and = [existingFilters, { $or: orList }];
1244
+ } else {
1245
+ filters2.$or = orList;
1246
+ }
1247
+ }
1248
+ }
1061
1249
  if (isSingleType) {
1062
- const singleDoc = await strapi2.documents(uid).findFirst();
1250
+ let singleDoc = null;
1251
+ try {
1252
+ singleDoc = await strapi2.documents(uid).findFirst({
1253
+ filters: Object.keys(filters2).length > 0 ? filters2 : void 0
1254
+ });
1255
+ } catch {
1256
+ singleDoc = null;
1257
+ }
1063
1258
  result = {
1064
1259
  uid,
1065
1260
  displayName: contentType.info?.displayName || uid,
@@ -1073,55 +1268,23 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1073
1268
  }
1074
1269
  };
1075
1270
  } else {
1076
- const filters2 = {};
1077
- if (attribute && filterOperator && filterQuery !== void 0 && filterQuery !== "") {
1078
- switch (filterOperator) {
1079
- case "equals":
1080
- filters2[attribute] = { $eq: filterQuery };
1081
- break;
1082
- case "notEquals":
1083
- filters2[attribute] = { $ne: filterQuery };
1084
- break;
1085
- case "contains":
1086
- filters2[attribute] = { $containsi: filterQuery };
1087
- break;
1088
- case "notContains":
1089
- filters2[attribute] = { $notContainsi: filterQuery };
1090
- break;
1091
- case "startsWith":
1092
- filters2[attribute] = { $startsWith: filterQuery };
1093
- break;
1094
- case "endsWith":
1095
- filters2[attribute] = { $endsWith: filterQuery };
1096
- break;
1097
- case "empty":
1098
- filters2[attribute] = { $null: true };
1099
- break;
1100
- case "notEmpty":
1101
- filters2[attribute] = { $notNull: true };
1102
- break;
1103
- }
1104
- }
1105
- if (search && search.trim() !== "") {
1106
- const stringAttrs = Object.keys(contentType.attributes || {}).filter(
1107
- (k) => contentType.attributes[k].type === "string" || contentType.attributes[k].type === "text" || contentType.attributes[k].type === "email"
1108
- );
1109
- if (stringAttrs.length > 0) {
1110
- filters2.$or = stringAttrs.slice(0, 5).map((attr) => ({
1111
- [attr]: { $containsi: search.trim() }
1112
- }));
1113
- }
1114
- }
1115
1271
  const sortClause = {};
1116
- if (sortField && contentType.attributes?.[sortField]) {
1272
+ if (sortField && sortField.includes(".")) {
1273
+ const [parent, child] = sortField.split(".");
1274
+ if (contentType.attributes?.[parent]?.type === "component") {
1275
+ sortClause[parent] = { [child]: sortOrder };
1276
+ } else {
1277
+ sortClause["createdAt"] = "desc";
1278
+ }
1279
+ } else if (sortField && contentType.attributes?.[sortField]) {
1117
1280
  sortClause[sortField] = sortOrder;
1118
1281
  } else {
1119
1282
  sortClause["createdAt"] = "desc";
1120
1283
  }
1121
- const relationAttrs = Object.keys(contentType.attributes || {}).filter(
1122
- (k) => contentType.attributes[k].type === "relation"
1284
+ const populateAttrs = Object.keys(contentType.attributes || {}).filter(
1285
+ (k) => contentType.attributes[k].type === "relation" || contentType.attributes[k].type === "component"
1123
1286
  );
1124
- const populateOption = relationAttrs.length > 0 ? relationAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1287
+ const populateOption = populateAttrs.length > 0 ? populateAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1125
1288
  const [entries, totalEntries] = await Promise.all([
1126
1289
  strapi2.documents(uid).findMany({
1127
1290
  start,
@@ -1460,6 +1623,9 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1460
1623
  modelsCache.clear();
1461
1624
  staticSchemaCache = null;
1462
1625
  }
1626
+ },
1627
+ invalidateCollectionCache(uid) {
1628
+ invalidateCollectionCache(uid);
1463
1629
  }
1464
1630
  };
1465
1631
  };
@@ -1721,12 +1887,41 @@ const auditService = ({ strapi: strapi2 }) => ({
1721
1887
  } else {
1722
1888
  rollbackPayload = item.oldValue;
1723
1889
  }
1724
- await strapi2.documents(item.uid).update({
1725
- documentId: item.documentId,
1726
- data: {
1727
- [item.attribute]: rollbackPayload
1890
+ if (item.attribute.includes(".")) {
1891
+ const [parentKey, childKey] = item.attribute.split(".");
1892
+ const ct2 = strapi2.contentTypes[item.uid];
1893
+ if (ct2?.attributes?.[parentKey]?.type === "component") {
1894
+ const currentDoc = await strapi2.documents(item.uid).findOne({
1895
+ documentId: item.documentId,
1896
+ populate: [parentKey]
1897
+ });
1898
+ const currentParent = currentDoc?.[parentKey] || {};
1899
+ const restoredParent = {
1900
+ ...currentParent,
1901
+ [childKey]: rollbackPayload
1902
+ };
1903
+ await strapi2.documents(item.uid).update({
1904
+ documentId: item.documentId,
1905
+ data: {
1906
+ [parentKey]: restoredParent
1907
+ }
1908
+ });
1909
+ } else {
1910
+ await strapi2.documents(item.uid).update({
1911
+ documentId: item.documentId,
1912
+ data: {
1913
+ [item.attribute]: rollbackPayload
1914
+ }
1915
+ });
1728
1916
  }
1729
- });
1917
+ } else {
1918
+ await strapi2.documents(item.uid).update({
1919
+ documentId: item.documentId,
1920
+ data: {
1921
+ [item.attribute]: rollbackPayload
1922
+ }
1923
+ });
1924
+ }
1730
1925
  const ct = strapi2.contentTypes[item.uid];
1731
1926
  if (ct?.options?.draftAndPublish) {
1732
1927
  try {
@@ -1737,7 +1932,10 @@ const auditService = ({ strapi: strapi2 }) => ({
1737
1932
  }
1738
1933
  }
1739
1934
  try {
1740
- strapi2.plugin("bulk-updater").service("bulkUpdater").invalidateCollectionCache(item.uid);
1935
+ const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
1936
+ if (bulkService?.invalidateCache) {
1937
+ bulkService.invalidateCache(item.uid);
1938
+ }
1741
1939
  } catch {
1742
1940
  }
1743
1941
  revertedCount++;
@@ -2117,10 +2315,24 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2117
2315
  const attrs = ct.attributes || {};
2118
2316
  const excluded = ["createdBy", "updatedBy", "localizations"];
2119
2317
  const exportFields = ["documentId"];
2318
+ const compFieldsToPopulate = [];
2120
2319
  for (const [attrName, attrDef] of Object.entries(attrs)) {
2121
2320
  if (excluded.includes(attrName)) continue;
2122
- if (["component", "dynamiczone", "media"].includes(attrDef.type)) continue;
2123
- if (attrDef.type === "relation") continue;
2321
+ if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
2322
+ if (attrDef.type === "component") {
2323
+ if (!attrDef.repeatable && attrDef.component) {
2324
+ const comp = strapi2.components[attrDef.component];
2325
+ if (comp?.attributes) {
2326
+ compFieldsToPopulate.push(attrName);
2327
+ for (const [subKey, subDef] of Object.entries(comp.attributes)) {
2328
+ if (excluded.includes(subKey)) continue;
2329
+ if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
2330
+ exportFields.push(`${attrName}.${subKey}`);
2331
+ }
2332
+ }
2333
+ }
2334
+ continue;
2335
+ }
2124
2336
  exportFields.push(attrName);
2125
2337
  }
2126
2338
  const filters2 = {};
@@ -2164,16 +2376,25 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2164
2376
  }
2165
2377
  }
2166
2378
  }
2379
+ const populateOption = compFieldsToPopulate.length > 0 ? compFieldsToPopulate.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
2167
2380
  const entries = await strapi2.documents(uid).findMany({
2168
2381
  filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
2169
2382
  limit: options.limit || 5e3,
2170
- sort: { createdAt: "desc" }
2383
+ sort: { createdAt: "desc" },
2384
+ populate: populateOption
2171
2385
  });
2172
2386
  const lines = [];
2173
2387
  lines.push(exportFields.map(escapeCsvValue).join(","));
2174
2388
  for (const entry of entries || []) {
2175
2389
  const rowValues = exportFields.map((field) => {
2176
- return escapeCsvValue(entry[field]);
2390
+ let val;
2391
+ if (field.includes(".")) {
2392
+ const [parent, child] = field.split(".");
2393
+ val = entry[parent]?.[child];
2394
+ } else {
2395
+ val = entry[field];
2396
+ }
2397
+ return escapeCsvValue(val);
2177
2398
  });
2178
2399
  lines.push(rowValues.join(","));
2179
2400
  }
@@ -2259,6 +2480,29 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2259
2480
  const payload = {};
2260
2481
  for (const [key, rawVal] of Object.entries(row)) {
2261
2482
  if (key === "documentId" || key === "id") continue;
2483
+ if (key.includes(".")) {
2484
+ const [parent, child] = key.split(".");
2485
+ const parentDef = attrs[parent];
2486
+ if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
2487
+ const comp = strapi2.components[parentDef.component];
2488
+ const childDef = comp?.attributes?.[child];
2489
+ if (childDef) {
2490
+ if (!payload[parent]) payload[parent] = {};
2491
+ let val = rawVal;
2492
+ if (rawVal === "" || rawVal === null || rawVal === void 0) {
2493
+ val = null;
2494
+ } else if (childDef.type === "integer" || childDef.type === "biginteger") {
2495
+ val = parseInt(rawVal, 10);
2496
+ } else if (childDef.type === "float" || childDef.type === "decimal") {
2497
+ val = parseFloat(rawVal);
2498
+ } else if (childDef.type === "boolean") {
2499
+ val = rawVal.toLowerCase() === "true" || rawVal === "1";
2500
+ }
2501
+ payload[parent][child] = val;
2502
+ }
2503
+ }
2504
+ continue;
2505
+ }
2262
2506
  const attrDef = attrs[key];
2263
2507
  if (!attrDef) continue;
2264
2508
  if (rawVal === "" || rawVal === null || rawVal === void 0) {
@@ -2310,9 +2554,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2310
2554
  });
2311
2555
  continue;
2312
2556
  }
2557
+ const mergedPayload = { ...payload };
2558
+ for (const key of Object.keys(payload)) {
2559
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
2560
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
2561
+ }
2562
+ }
2313
2563
  await strapi2.documents(uid).update({
2314
2564
  documentId: docId,
2315
- data: payload
2565
+ data: mergedPayload
2316
2566
  });
2317
2567
  if (publish && ct.options?.draftAndPublish) {
2318
2568
  try {
@@ -2339,9 +2589,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2339
2589
  documentId: docId
2340
2590
  });
2341
2591
  if (existing) {
2592
+ const mergedPayload = { ...payload };
2593
+ for (const key of Object.keys(payload)) {
2594
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
2595
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
2596
+ }
2597
+ }
2342
2598
  await strapi2.documents(uid).update({
2343
2599
  documentId: docId,
2344
- data: payload
2600
+ data: mergedPayload
2345
2601
  });
2346
2602
  if (publish && ct.options?.draftAndPublish) {
2347
2603
  try {