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.
@@ -224,7 +224,7 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
224
224
  return;
225
225
  }
226
226
  }
227
- ctx.body = data;
227
+ ctx.body = { success: true, ...data };
228
228
  } catch (err) {
229
229
  ctx.status = 500;
230
230
  ctx.body = { success: false, error: err.message };
@@ -673,9 +673,69 @@ async function executeBatchInternal(strapi2, {
673
673
  try {
674
674
  const currentEntry = await getDocumentDeduplicated(target.uid, target.documentId);
675
675
  const entryLabel = currentEntry?.name || currentEntry?.title || currentEntry?.email || `Entry #${target.documentId}`;
676
+ const isNested = attribute.includes(".");
677
+ let isComponentNested = false;
678
+ let parentKey = "";
679
+ let childKey = "";
680
+ if (isNested) {
681
+ const parts = attribute.split(".");
682
+ parentKey = parts[0];
683
+ childKey = parts[1];
684
+ const parentAttrDef = contentType?.attributes?.[parentKey];
685
+ if (parentAttrDef?.type === "component" && !parentAttrDef.repeatable && parentAttrDef.component) {
686
+ const compSchema = strapi2.components[parentAttrDef.component];
687
+ if (compSchema?.attributes?.[childKey]) {
688
+ isComponentNested = true;
689
+ }
690
+ }
691
+ }
676
692
  const attrDef = contentType?.attributes?.[attribute];
677
- const hasAttribute = Boolean(attrDef);
693
+ const hasAttribute = Boolean(attrDef) || isComponentNested;
678
694
  if (hasAttribute) {
695
+ if (isComponentNested) {
696
+ const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
697
+ documentId: target.documentId,
698
+ populate: [parentKey]
699
+ });
700
+ const existingParent = currentEntryWithPopulate?.[parentKey] || {};
701
+ const currentVal2 = existingParent?.[childKey];
702
+ let newValue2 = value;
703
+ if (regexObj && typeof currentVal2 === "string") {
704
+ newValue2 = currentVal2.replace(regexObj, value);
705
+ }
706
+ const updatedParent = {
707
+ ...existingParent,
708
+ [childKey]: newValue2
709
+ };
710
+ await strapi2.documents(target.uid).update({
711
+ documentId: target.documentId,
712
+ data: {
713
+ [parentKey]: updatedParent
714
+ }
715
+ });
716
+ const isDraftAndPublishComp = Boolean(contentType?.options?.draftAndPublish);
717
+ if (publishOnApproval && isDraftAndPublishComp) {
718
+ try {
719
+ await strapi2.documents(target.uid).publish({
720
+ documentId: target.documentId
721
+ });
722
+ } catch (publishErr) {
723
+ strapi2.log.warn(`[bulk-updater] Failed to auto-publish ${target.uid}/${target.documentId}:`, publishErr);
724
+ }
725
+ }
726
+ invalidateCollectionCache(target.uid);
727
+ return {
728
+ uid: target.uid,
729
+ collectionName: displayName,
730
+ documentId: target.documentId,
731
+ label: entryLabel,
732
+ attribute,
733
+ oldValue: currentVal2 ?? null,
734
+ newValue: newValue2,
735
+ status: "updated",
736
+ message: `Successfully updated nested field ${attribute}`
737
+ };
738
+ }
679
739
  if (attrDef.type === "relation") {
680
740
  const isMulti = attrDef.relation === "oneToMany" || attrDef.relation === "manyToMany";
681
741
  const currentEntryWithPopulate = await strapi2.documents(target.uid).findOne({
@@ -917,9 +977,24 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
917
977
  const isSingleType = contentType.kind === "singleType";
918
978
  const rawAttrs = contentType.attributes || {};
919
979
  const relationsByAttr = {};
980
+ const nestedComponentAttrs = [];
920
981
  const validAttrs = Object.keys(rawAttrs).filter((attrKey) => {
921
982
  if (systemFields.includes(attrKey)) return false;
922
- if (rawAttrs[attrKey].type === "component" || rawAttrs[attrKey].type === "dynamiczone") return false;
983
+ if (rawAttrs[attrKey].type === "dynamiczone") return false;
984
+ if (rawAttrs[attrKey].type === "component") {
985
+ const compDef = rawAttrs[attrKey];
986
+ if (!compDef.repeatable && compDef.component) {
987
+ const componentSchema = strapi2.components[compDef.component];
988
+ if (componentSchema && componentSchema.attributes) {
989
+ for (const [subKey, subAttr] of Object.entries(componentSchema.attributes)) {
990
+ if (systemFields.includes(subKey)) continue;
991
+ if (["component", "dynamiczone", "relation"].includes(subAttr.type)) continue;
992
+ nestedComponentAttrs.push(`${attrKey}.${subKey}`);
993
+ }
994
+ }
995
+ }
996
+ return false;
997
+ }
923
998
  if (rawAttrs[attrKey].type === "relation") {
924
999
  const rel = rawAttrs[attrKey];
925
1000
  const targetUid = rel.target;
@@ -939,7 +1014,8 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
939
1014
  }
940
1015
  return true;
941
1016
  });
942
- validAttrs.forEach((attr) => allAttributesSet.add(attr));
1017
+ const allColAttrs = [...validAttrs, ...nestedComponentAttrs];
1018
+ allColAttrs.forEach((attr) => allAttributesSet.add(attr));
943
1019
  metaByUid[uid] = {
944
1020
  uid,
945
1021
  kind: contentType.kind,
@@ -947,7 +1023,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
947
1023
  singularName: contentType.info?.singularName || uid,
948
1024
  pluralName: contentType.info?.pluralName || uid,
949
1025
  displayName: contentType.info?.displayName || uid,
950
- attributes: validAttrs,
1026
+ attributes: allColAttrs,
951
1027
  relations: relationsByAttr
952
1028
  };
953
1029
  }
@@ -976,8 +1052,17 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
976
1052
  let entries = [];
977
1053
  let totalCount = 0;
978
1054
  try {
1055
+ const ctAttrs = allContentTypes[uid]?.attributes || {};
979
1056
  const relKeys = Object.keys(meta.relations || {});
980
- const populateOption = relKeys.length > 0 ? relKeys.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1057
+ const compKeys = Object.keys(ctAttrs).filter((k) => ctAttrs[k].type === "component");
1058
+ const populateOptionObj = {};
1059
+ relKeys.forEach((k) => {
1060
+ populateOptionObj[k] = true;
1061
+ });
1062
+ compKeys.forEach((k) => {
1063
+ populateOptionObj[k] = true;
1064
+ });
1065
+ const populateOption = Object.keys(populateOptionObj).length > 0 ? populateOptionObj : void 0;
981
1066
  if (meta.isSingleType) {
982
1067
  const singleDoc = await strapi2.documents(uid).findFirst({
983
1068
  populate: populateOption
@@ -1055,7 +1140,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1055
1140
  if (!contentType) {
1056
1141
  throw new Error(`Collection with UID "${uid}" does not exist.`);
1057
1142
  }
1058
- const limit = Math.min(Math.max(Number(pageSize) || 10, 1), 100);
1143
+ const limit = Math.min(Math.max(Number(pageSize) || 10, 1), 500);
1059
1144
  const currentPage = Math.max(Number(page) || 1, 1);
1060
1145
  const start = (currentPage - 1) * limit;
1061
1146
  const isSingleType = contentType.kind === "singleType";
@@ -1071,8 +1156,118 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1071
1156
  };
1072
1157
  }
1073
1158
  let result;
1159
+ const filters2 = {};
1160
+ const attrs = contentType.attributes || {};
1161
+ let colHasAttribute = false;
1162
+ let isNestedAttr = false;
1163
+ let parentKey = "";
1164
+ let childKey = "";
1165
+ if (attribute) {
1166
+ if (attribute.includes(".")) {
1167
+ [parentKey, childKey] = attribute.split(".");
1168
+ if (attrs[parentKey]?.type === "component" && !attrs[parentKey].repeatable && attrs[parentKey].component) {
1169
+ const compSchema = strapi2.components[attrs[parentKey].component];
1170
+ if (compSchema?.attributes?.[childKey]) {
1171
+ colHasAttribute = true;
1172
+ isNestedAttr = true;
1173
+ }
1174
+ }
1175
+ } else if (attrs[attribute]) {
1176
+ colHasAttribute = true;
1177
+ }
1178
+ }
1179
+ const normOp = (filterOperator || "").toLowerCase().replace(/_/g, "");
1180
+ if (normOp && normOp !== "none") {
1181
+ if (!colHasAttribute) {
1182
+ result = {
1183
+ uid,
1184
+ displayName: contentType.info?.displayName || uid,
1185
+ isSingleType,
1186
+ entries: [],
1187
+ pagination: {
1188
+ page: 1,
1189
+ pageSize: limit,
1190
+ total: 0,
1191
+ pageCount: 0
1192
+ }
1193
+ };
1194
+ queryCache.set(cacheKey, { signature, data: result, timestamp: Date.now() });
1195
+ return { ...result, cached: false, etag };
1196
+ }
1197
+ let condition = void 0;
1198
+ const q = (filterQuery || "").trim();
1199
+ if (normOp === "empty") {
1200
+ condition = { $or: [{ $null: true }, { $eq: "" }] };
1201
+ } else if (normOp === "notempty") {
1202
+ condition = { $and: [{ $notNull: true }, { $ne: "" }] };
1203
+ } else if (q !== "") {
1204
+ switch (normOp) {
1205
+ case "equals":
1206
+ condition = { $containsi: q };
1207
+ break;
1208
+ case "notequals":
1209
+ condition = { $notContainsi: q };
1210
+ break;
1211
+ case "contains":
1212
+ condition = { $containsi: q };
1213
+ break;
1214
+ case "notcontains":
1215
+ condition = { $notContainsi: q };
1216
+ break;
1217
+ case "startswith":
1218
+ condition = { $startsWith: q };
1219
+ break;
1220
+ case "endswith":
1221
+ condition = { $endsWith: q };
1222
+ break;
1223
+ }
1224
+ }
1225
+ if (condition !== void 0) {
1226
+ if (isNestedAttr) {
1227
+ filters2[parentKey] = {
1228
+ [childKey]: condition
1229
+ };
1230
+ } else {
1231
+ if (normOp === "empty") {
1232
+ filters2.$or = [
1233
+ { [attribute]: { $null: true } },
1234
+ { [attribute]: { $eq: "" } }
1235
+ ];
1236
+ } else if (normOp === "notempty") {
1237
+ filters2[attribute] = { $notNull: true, $ne: "" };
1238
+ } else {
1239
+ filters2[attribute] = condition;
1240
+ }
1241
+ }
1242
+ }
1243
+ }
1244
+ if (search && search.trim() !== "") {
1245
+ const q = search.trim();
1246
+ const searchableAttrs = Object.keys(attrs).filter(
1247
+ (k) => ["string", "text", "email", "uid"].includes(attrs[k]?.type)
1248
+ );
1249
+ if (searchableAttrs.length > 0) {
1250
+ const orList = searchableAttrs.slice(0, 6).map((k) => ({
1251
+ [k]: { $containsi: q }
1252
+ }));
1253
+ if (Object.keys(filters2).length > 0) {
1254
+ const existingFilters = { ...filters2 };
1255
+ Object.keys(filters2).forEach((k) => delete filters2[k]);
1256
+ filters2.$and = [existingFilters, { $or: orList }];
1257
+ } else {
1258
+ filters2.$or = orList;
1259
+ }
1260
+ }
1261
+ }
1074
1262
  if (isSingleType) {
1075
- const singleDoc = await strapi2.documents(uid).findFirst();
1263
+ let singleDoc = null;
1264
+ try {
1265
+ singleDoc = await strapi2.documents(uid).findFirst({
1266
+ filters: Object.keys(filters2).length > 0 ? filters2 : void 0
1267
+ });
1268
+ } catch {
1269
+ singleDoc = null;
1270
+ }
1076
1271
  result = {
1077
1272
  uid,
1078
1273
  displayName: contentType.info?.displayName || uid,
@@ -1086,55 +1281,23 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1086
1281
  }
1087
1282
  };
1088
1283
  } else {
1089
- const filters2 = {};
1090
- if (attribute && filterOperator && filterQuery !== void 0 && filterQuery !== "") {
1091
- switch (filterOperator) {
1092
- case "equals":
1093
- filters2[attribute] = { $eq: filterQuery };
1094
- break;
1095
- case "notEquals":
1096
- filters2[attribute] = { $ne: filterQuery };
1097
- break;
1098
- case "contains":
1099
- filters2[attribute] = { $containsi: filterQuery };
1100
- break;
1101
- case "notContains":
1102
- filters2[attribute] = { $notContainsi: filterQuery };
1103
- break;
1104
- case "startsWith":
1105
- filters2[attribute] = { $startsWith: filterQuery };
1106
- break;
1107
- case "endsWith":
1108
- filters2[attribute] = { $endsWith: filterQuery };
1109
- break;
1110
- case "empty":
1111
- filters2[attribute] = { $null: true };
1112
- break;
1113
- case "notEmpty":
1114
- filters2[attribute] = { $notNull: true };
1115
- break;
1116
- }
1117
- }
1118
- if (search && search.trim() !== "") {
1119
- const stringAttrs = Object.keys(contentType.attributes || {}).filter(
1120
- (k) => contentType.attributes[k].type === "string" || contentType.attributes[k].type === "text" || contentType.attributes[k].type === "email"
1121
- );
1122
- if (stringAttrs.length > 0) {
1123
- filters2.$or = stringAttrs.slice(0, 5).map((attr) => ({
1124
- [attr]: { $containsi: search.trim() }
1125
- }));
1126
- }
1127
- }
1128
1284
  const sortClause = {};
1129
- if (sortField && contentType.attributes?.[sortField]) {
1285
+ if (sortField && sortField.includes(".")) {
1286
+ const [parent, child] = sortField.split(".");
1287
+ if (contentType.attributes?.[parent]?.type === "component") {
1288
+ sortClause[parent] = { [child]: sortOrder };
1289
+ } else {
1290
+ sortClause["createdAt"] = "desc";
1291
+ }
1292
+ } else if (sortField && contentType.attributes?.[sortField]) {
1130
1293
  sortClause[sortField] = sortOrder;
1131
1294
  } else {
1132
1295
  sortClause["createdAt"] = "desc";
1133
1296
  }
1134
- const relationAttrs = Object.keys(contentType.attributes || {}).filter(
1135
- (k) => contentType.attributes[k].type === "relation"
1297
+ const populateAttrs = Object.keys(contentType.attributes || {}).filter(
1298
+ (k) => contentType.attributes[k].type === "relation" || contentType.attributes[k].type === "component"
1136
1299
  );
1137
- const populateOption = relationAttrs.length > 0 ? relationAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1300
+ const populateOption = populateAttrs.length > 0 ? populateAttrs.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
1138
1301
  const [entries, totalEntries] = await Promise.all([
1139
1302
  strapi2.documents(uid).findMany({
1140
1303
  start,
@@ -1473,6 +1636,9 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1473
1636
  modelsCache.clear();
1474
1637
  staticSchemaCache = null;
1475
1638
  }
1639
+ },
1640
+ invalidateCollectionCache(uid) {
1641
+ invalidateCollectionCache(uid);
1476
1642
  }
1477
1643
  };
1478
1644
  };
@@ -1734,12 +1900,41 @@ const auditService = ({ strapi: strapi2 }) => ({
1734
1900
  } else {
1735
1901
  rollbackPayload = item.oldValue;
1736
1902
  }
1737
- await strapi2.documents(item.uid).update({
1738
- documentId: item.documentId,
1739
- data: {
1740
- [item.attribute]: rollbackPayload
1903
+ if (item.attribute.includes(".")) {
1904
+ const [parentKey, childKey] = item.attribute.split(".");
1905
+ const ct2 = strapi2.contentTypes[item.uid];
1906
+ if (ct2?.attributes?.[parentKey]?.type === "component") {
1907
+ const currentDoc = await strapi2.documents(item.uid).findOne({
1908
+ documentId: item.documentId,
1909
+ populate: [parentKey]
1910
+ });
1911
+ const currentParent = currentDoc?.[parentKey] || {};
1912
+ const restoredParent = {
1913
+ ...currentParent,
1914
+ [childKey]: rollbackPayload
1915
+ };
1916
+ await strapi2.documents(item.uid).update({
1917
+ documentId: item.documentId,
1918
+ data: {
1919
+ [parentKey]: restoredParent
1920
+ }
1921
+ });
1922
+ } else {
1923
+ await strapi2.documents(item.uid).update({
1924
+ documentId: item.documentId,
1925
+ data: {
1926
+ [item.attribute]: rollbackPayload
1927
+ }
1928
+ });
1741
1929
  }
1742
- });
1930
+ } else {
1931
+ await strapi2.documents(item.uid).update({
1932
+ documentId: item.documentId,
1933
+ data: {
1934
+ [item.attribute]: rollbackPayload
1935
+ }
1936
+ });
1937
+ }
1743
1938
  const ct = strapi2.contentTypes[item.uid];
1744
1939
  if (ct?.options?.draftAndPublish) {
1745
1940
  try {
@@ -1750,7 +1945,10 @@ const auditService = ({ strapi: strapi2 }) => ({
1750
1945
  }
1751
1946
  }
1752
1947
  try {
1753
- strapi2.plugin("bulk-updater").service("bulkUpdater").invalidateCollectionCache(item.uid);
1948
+ const bulkService = strapi2.plugin("bulk-updater").service("bulkUpdater");
1949
+ if (bulkService?.invalidateCache) {
1950
+ bulkService.invalidateCache(item.uid);
1951
+ }
1754
1952
  } catch {
1755
1953
  }
1756
1954
  revertedCount++;
@@ -2130,10 +2328,24 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2130
2328
  const attrs = ct.attributes || {};
2131
2329
  const excluded = ["createdBy", "updatedBy", "localizations"];
2132
2330
  const exportFields = ["documentId"];
2331
+ const compFieldsToPopulate = [];
2133
2332
  for (const [attrName, attrDef] of Object.entries(attrs)) {
2134
2333
  if (excluded.includes(attrName)) continue;
2135
- if (["component", "dynamiczone", "media"].includes(attrDef.type)) continue;
2136
- if (attrDef.type === "relation") continue;
2334
+ if (["dynamiczone", "media", "relation"].includes(attrDef.type)) continue;
2335
+ if (attrDef.type === "component") {
2336
+ if (!attrDef.repeatable && attrDef.component) {
2337
+ const comp = strapi2.components[attrDef.component];
2338
+ if (comp?.attributes) {
2339
+ compFieldsToPopulate.push(attrName);
2340
+ for (const [subKey, subDef] of Object.entries(comp.attributes)) {
2341
+ if (excluded.includes(subKey)) continue;
2342
+ if (["component", "dynamiczone", "media", "relation"].includes(subDef.type)) continue;
2343
+ exportFields.push(`${attrName}.${subKey}`);
2344
+ }
2345
+ }
2346
+ }
2347
+ continue;
2348
+ }
2137
2349
  exportFields.push(attrName);
2138
2350
  }
2139
2351
  const filters2 = {};
@@ -2177,16 +2389,25 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2177
2389
  }
2178
2390
  }
2179
2391
  }
2392
+ const populateOption = compFieldsToPopulate.length > 0 ? compFieldsToPopulate.reduce((acc, k) => ({ ...acc, [k]: true }), {}) : void 0;
2180
2393
  const entries = await strapi2.documents(uid).findMany({
2181
2394
  filters: Object.keys(filters2).length > 0 ? filters2 : void 0,
2182
2395
  limit: options.limit || 5e3,
2183
- sort: { createdAt: "desc" }
2396
+ sort: { createdAt: "desc" },
2397
+ populate: populateOption
2184
2398
  });
2185
2399
  const lines = [];
2186
2400
  lines.push(exportFields.map(escapeCsvValue).join(","));
2187
2401
  for (const entry of entries || []) {
2188
2402
  const rowValues = exportFields.map((field) => {
2189
- return escapeCsvValue(entry[field]);
2403
+ let val;
2404
+ if (field.includes(".")) {
2405
+ const [parent, child] = field.split(".");
2406
+ val = entry[parent]?.[child];
2407
+ } else {
2408
+ val = entry[field];
2409
+ }
2410
+ return escapeCsvValue(val);
2190
2411
  });
2191
2412
  lines.push(rowValues.join(","));
2192
2413
  }
@@ -2272,6 +2493,29 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2272
2493
  const payload = {};
2273
2494
  for (const [key, rawVal] of Object.entries(row)) {
2274
2495
  if (key === "documentId" || key === "id") continue;
2496
+ if (key.includes(".")) {
2497
+ const [parent, child] = key.split(".");
2498
+ const parentDef = attrs[parent];
2499
+ if (parentDef?.type === "component" && !parentDef.repeatable && parentDef.component) {
2500
+ const comp = strapi2.components[parentDef.component];
2501
+ const childDef = comp?.attributes?.[child];
2502
+ if (childDef) {
2503
+ if (!payload[parent]) payload[parent] = {};
2504
+ let val = rawVal;
2505
+ if (rawVal === "" || rawVal === null || rawVal === void 0) {
2506
+ val = null;
2507
+ } else if (childDef.type === "integer" || childDef.type === "biginteger") {
2508
+ val = parseInt(rawVal, 10);
2509
+ } else if (childDef.type === "float" || childDef.type === "decimal") {
2510
+ val = parseFloat(rawVal);
2511
+ } else if (childDef.type === "boolean") {
2512
+ val = rawVal.toLowerCase() === "true" || rawVal === "1";
2513
+ }
2514
+ payload[parent][child] = val;
2515
+ }
2516
+ }
2517
+ continue;
2518
+ }
2275
2519
  const attrDef = attrs[key];
2276
2520
  if (!attrDef) continue;
2277
2521
  if (rawVal === "" || rawVal === null || rawVal === void 0) {
@@ -2323,9 +2567,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2323
2567
  });
2324
2568
  continue;
2325
2569
  }
2570
+ const mergedPayload = { ...payload };
2571
+ for (const key of Object.keys(payload)) {
2572
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
2573
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
2574
+ }
2575
+ }
2326
2576
  await strapi2.documents(uid).update({
2327
2577
  documentId: docId,
2328
- data: payload
2578
+ data: mergedPayload
2329
2579
  });
2330
2580
  if (publish && ct.options?.draftAndPublish) {
2331
2581
  try {
@@ -2352,9 +2602,15 @@ ${sampleRow.map(escapeCsvValue).join(",")}
2352
2602
  documentId: docId
2353
2603
  });
2354
2604
  if (existing) {
2605
+ const mergedPayload = { ...payload };
2606
+ for (const key of Object.keys(payload)) {
2607
+ if (attrs[key]?.type === "component" && typeof payload[key] === "object" && payload[key] !== null) {
2608
+ mergedPayload[key] = { ...existing[key] || {}, ...payload[key] };
2609
+ }
2610
+ }
2355
2611
  await strapi2.documents(uid).update({
2356
2612
  documentId: docId,
2357
- data: payload
2613
+ data: mergedPayload
2358
2614
  });
2359
2615
  if (publish && ct.options?.draftAndPublish) {
2360
2616
  try {