strapi-plugin-bulk-updater 1.5.0 → 1.5.3

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.
@@ -68,7 +68,7 @@ const index = {
68
68
  id: `${PLUGIN_ID}.plugin.name`,
69
69
  defaultMessage: "Bulk Attribute Updater"
70
70
  },
71
- Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-CSmtXjbM.js")),
71
+ Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-DMkx6dLf.js")),
72
72
  permissions: [],
73
73
  position: 3
74
74
  });
@@ -65,7 +65,7 @@ const index = {
65
65
  id: `${PLUGIN_ID}.plugin.name`,
66
66
  defaultMessage: "Bulk Attribute Updater"
67
67
  },
68
- Component: () => import("../_chunks/BulkUpdater-By9Dd_YF.mjs"),
68
+ Component: () => import("../_chunks/BulkUpdater-DUfv52ng.mjs"),
69
69
  permissions: [],
70
70
  position: 3
71
71
  });
@@ -365,10 +365,10 @@ const bulkUpdaterController = ({ strapi }) => ({
365
365
  ctx.set("ETag", data.etag);
366
366
  ctx.set("Cache-Control", "private, no-cache");
367
367
  }
368
- ctx.body = { success: true, ...data };
368
+ ctx.body = { success: true, status: "success", ...data };
369
369
  } catch (err) {
370
370
  ctx.status = 500;
371
- ctx.body = { success: false, error: err.message };
371
+ ctx.body = { success: false, status: "error", error: err.message };
372
372
  }
373
373
  },
374
374
  async getRelationOptions(ctx) {
@@ -970,6 +970,7 @@ function formatEntryRecord(entry) {
970
970
  const formatted = {
971
971
  id: entry.id,
972
972
  documentId: entry.documentId,
973
+ status: entry.status !== void 0 ? entry.status : void 0,
973
974
  ...entry
974
975
  };
975
976
  if (entry.updatedAt !== void 0 && formatted.updated_at === void 0) {
@@ -992,6 +993,119 @@ function formatEntryRecord(entry) {
992
993
  }
993
994
  return formatted;
994
995
  }
996
+ async function populateDocumentLifecycleStatus(strapi, uid, entries) {
997
+ if (!entries || !Array.isArray(entries) || entries.length === 0) {
998
+ return entries;
999
+ }
1000
+ const contentType = strapi?.contentTypes?.[uid];
1001
+ if (!contentType) {
1002
+ return entries;
1003
+ }
1004
+ const hasDraftAndPublish = Boolean(contentType.options?.draftAndPublish);
1005
+ if (!hasDraftAndPublish) {
1006
+ for (const entry of entries) {
1007
+ if (entry && typeof entry === "object") {
1008
+ entry.status = "published";
1009
+ }
1010
+ }
1011
+ return entries;
1012
+ }
1013
+ const hasSlugInSchema = Boolean(contentType.attributes?.slug);
1014
+ const docIds = [];
1015
+ const slugs = [];
1016
+ for (const entry of entries) {
1017
+ if (!entry || typeof entry !== "object") continue;
1018
+ if (entry.documentId) {
1019
+ docIds.push(String(entry.documentId));
1020
+ } else if (hasSlugInSchema && entry.slug) {
1021
+ slugs.push(String(entry.slug));
1022
+ } else {
1023
+ entry.status = "unknown";
1024
+ }
1025
+ }
1026
+ if (docIds.length === 0 && slugs.length === 0) {
1027
+ return entries;
1028
+ }
1029
+ let filterClause;
1030
+ if (docIds.length > 0 && slugs.length > 0) {
1031
+ filterClause = {
1032
+ $or: [
1033
+ { documentId: { $in: docIds } },
1034
+ { slug: { $in: slugs } }
1035
+ ]
1036
+ };
1037
+ } else if (docIds.length > 0) {
1038
+ filterClause = { documentId: { $in: docIds } };
1039
+ } else {
1040
+ filterClause = { slug: { $in: slugs } };
1041
+ }
1042
+ const queryFields = ["documentId", "updatedAt", "publishedAt"];
1043
+ if (hasSlugInSchema) {
1044
+ queryFields.push("slug");
1045
+ }
1046
+ let publishedRecords = [];
1047
+ try {
1048
+ const docService = strapi?.documents?.(uid);
1049
+ if (docService?.findMany) {
1050
+ publishedRecords = await docService.findMany({
1051
+ status: "published",
1052
+ filters: filterClause,
1053
+ fields: queryFields,
1054
+ limit: Math.max(entries.length, 100)
1055
+ });
1056
+ }
1057
+ } catch (err) {
1058
+ strapi?.log?.warn?.(`[bulk-updater] Failed to fetch published status for ${uid}: ${err?.message || err}`);
1059
+ for (const entry of entries) {
1060
+ if (entry && typeof entry === "object" && entry.status !== "unknown") {
1061
+ entry.status = "unknown";
1062
+ }
1063
+ }
1064
+ return entries;
1065
+ }
1066
+ const publishedByDocId = /* @__PURE__ */ new Map();
1067
+ const publishedBySlug = /* @__PURE__ */ new Map();
1068
+ for (const pub of publishedRecords || []) {
1069
+ if (pub?.documentId) {
1070
+ publishedByDocId.set(String(pub.documentId), pub);
1071
+ }
1072
+ if (pub?.slug && hasSlugInSchema) {
1073
+ publishedBySlug.set(String(pub.slug), pub);
1074
+ }
1075
+ }
1076
+ for (const entry of entries) {
1077
+ if (!entry || typeof entry !== "object") continue;
1078
+ if (entry.status === "unknown") continue;
1079
+ const pub = entry.documentId && publishedByDocId.get(String(entry.documentId)) || hasSlugInSchema && entry.slug && publishedBySlug.get(String(entry.slug));
1080
+ if (!pub) {
1081
+ entry.status = "draft";
1082
+ entry.publishedAt = null;
1083
+ entry.published_at = null;
1084
+ } else {
1085
+ entry.publishedAt = pub.publishedAt || pub.published_at || null;
1086
+ entry.published_at = entry.publishedAt;
1087
+ const draftUpdatedTime = new Date(entry.updatedAt || entry.updated_at || 0).getTime();
1088
+ const pubUpdatedTime = new Date(pub.updatedAt || pub.updated_at || 0).getTime();
1089
+ const pubPublishedTime = new Date(pub.publishedAt || pub.published_at || 0).getTime();
1090
+ const lastPublishedTime = Math.max(
1091
+ isNaN(pubUpdatedTime) ? 0 : pubUpdatedTime,
1092
+ isNaN(pubPublishedTime) ? 0 : pubPublishedTime
1093
+ );
1094
+ if (isNaN(draftUpdatedTime) || lastPublishedTime === 0) {
1095
+ if (pub.publishedAt) {
1096
+ entry.status = "published";
1097
+ } else {
1098
+ entry.status = "unknown";
1099
+ }
1100
+ } else if (draftUpdatedTime > lastPublishedTime) {
1101
+ entry.status = "modified";
1102
+ } else {
1103
+ entry.status = "published";
1104
+ }
1105
+ }
1106
+ }
1107
+ return entries;
1108
+ }
995
1109
  function resolveContentTypeField(contentType, field) {
996
1110
  if (!field) return null;
997
1111
  const attrs = contentType?.attributes || {};
@@ -1116,14 +1230,15 @@ function discoverComponentAttributes(strapi, compUid, prefix, currentDepth, maxD
1116
1230
  }
1117
1231
  function parseDynamicZonePath(path) {
1118
1232
  if (!path) return { cleanPath: "", rootKey: "", subPath: "" };
1119
- const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\]\.(.+)$/);
1233
+ const bracketMatch = path.match(/^([^.[]+)\[([^\]]+)\](?:\.(.+))?$/);
1120
1234
  if (bracketMatch) {
1121
1235
  const rootKey = bracketMatch[1];
1122
- const selector = bracketMatch[2];
1123
- const subPath = bracketMatch[3];
1124
- const cleanPath = `${rootKey}.${subPath}`;
1125
- if (/^\d+$/.test(selector)) {
1126
- return { cleanPath, rootKey, subPath, targetIndex: parseInt(selector, 10) };
1236
+ const selector = bracketMatch[2].trim();
1237
+ const subPath = bracketMatch[3] || "";
1238
+ const cleanPath = subPath ? `${rootKey}.${subPath}` : rootKey;
1239
+ const numMatch = selector.match(/^#?(\d+)/);
1240
+ if (numMatch) {
1241
+ return { cleanPath, rootKey, subPath, targetIndex: parseInt(numMatch[1], 10) };
1127
1242
  }
1128
1243
  return { cleanPath, rootKey, subPath, targetComponentUid: selector };
1129
1244
  }
@@ -2055,6 +2170,9 @@ const bulkUpdaterService = ({ strapi }) => {
2055
2170
  isRunnerRunning = false;
2056
2171
  }
2057
2172
  }, 3e4);
2173
+ if (typeof runnerInterval.unref === "function") {
2174
+ runnerInterval.unref();
2175
+ }
2058
2176
  }
2059
2177
  return {
2060
2178
  /**
@@ -2257,6 +2375,7 @@ const bulkUpdaterService = ({ strapi }) => {
2257
2375
  totalCount = 0;
2258
2376
  }
2259
2377
  const formattedEntries = (entries || []).map(formatEntryRecord);
2378
+ await populateDocumentLifecycleStatus(strapi, uid, formattedEntries);
2260
2379
  modelsCache.set(uid, {
2261
2380
  signature,
2262
2381
  totalCount,
@@ -2372,6 +2491,7 @@ const bulkUpdaterService = ({ strapi }) => {
2372
2491
  if (!colHasAttribute) {
2373
2492
  result = {
2374
2493
  uid,
2494
+ status: "success",
2375
2495
  displayName: contentType.info?.displayName || uid,
2376
2496
  isSingleType,
2377
2497
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
@@ -2513,12 +2633,15 @@ const bulkUpdaterService = ({ strapi }) => {
2513
2633
  } catch {
2514
2634
  singleDoc = null;
2515
2635
  }
2636
+ const formattedEntries = singleDoc ? [formatEntryRecord(singleDoc)] : [];
2637
+ await populateDocumentLifecycleStatus(strapi, uid, formattedEntries);
2516
2638
  result = {
2517
2639
  uid,
2640
+ status: "success",
2518
2641
  displayName: contentType.info?.displayName || uid,
2519
2642
  isSingleType: true,
2520
2643
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2521
- entries: singleDoc ? [formatEntryRecord(singleDoc)] : [],
2644
+ entries: formattedEntries,
2522
2645
  pagination: {
2523
2646
  page: 1,
2524
2647
  pageSize: 1,
@@ -2605,12 +2728,15 @@ const bulkUpdaterService = ({ strapi }) => {
2605
2728
  const matchedEntries = (rawEntries || []).filter(passesDzFilter);
2606
2729
  const totalEntries = matchedEntries.length;
2607
2730
  const paginatedEntries = matchedEntries.slice(start, start + limit);
2731
+ const formattedEntries = paginatedEntries.map(formatEntryRecord);
2732
+ await populateDocumentLifecycleStatus(strapi, uid, formattedEntries);
2608
2733
  result = {
2609
2734
  uid,
2735
+ status: "success",
2610
2736
  displayName: contentType.info?.displayName || uid,
2611
2737
  isSingleType: false,
2612
2738
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2613
- entries: paginatedEntries.map(formatEntryRecord),
2739
+ entries: formattedEntries,
2614
2740
  pagination: {
2615
2741
  page: currentPage,
2616
2742
  pageSize: limit,
@@ -2634,12 +2760,15 @@ const bulkUpdaterService = ({ strapi }) => {
2634
2760
  filters: Object.keys(filters).length > 0 ? filters : void 0
2635
2761
  })
2636
2762
  ]);
2763
+ const formattedEntries = (entries || []).map(formatEntryRecord);
2764
+ await populateDocumentLifecycleStatus(strapi, uid, formattedEntries);
2637
2765
  result = {
2638
2766
  uid,
2767
+ status: "success",
2639
2768
  displayName: contentType.info?.displayName || uid,
2640
2769
  isSingleType: false,
2641
2770
  draftAndPublish: Boolean(contentType.options?.draftAndPublish),
2642
- entries: (entries || []).map(formatEntryRecord),
2771
+ entries: formattedEntries,
2643
2772
  pagination: {
2644
2773
  page: currentPage,
2645
2774
  pageSize: limit,
@@ -4276,42 +4405,74 @@ const auditService = ({ strapi }) => ({
4276
4405
  for (const item of diffs) {
4277
4406
  if (item.status === "updated" && item.uid && item.documentId && item.attribute) {
4278
4407
  try {
4408
+ const ct = strapi.contentTypes[item.uid];
4409
+ if (!ct) {
4410
+ throw new Error(`Content type "${item.uid}" does not exist in schema.`);
4411
+ }
4279
4412
  const isRel = item.oldValue && typeof item.oldValue === "object" && item.oldValue.isRelation;
4280
4413
  let rollbackPayload;
4281
4414
  if (isRel) {
4282
4415
  const oldDocIds = Array.isArray(item.oldValue.documentIds) ? item.oldValue.documentIds : [];
4283
4416
  rollbackPayload = { set: oldDocIds };
4284
4417
  } else {
4285
- rollbackPayload = item.oldValue;
4418
+ rollbackPayload = item.oldValue ?? null;
4286
4419
  }
4287
- if (item.attribute.includes(".")) {
4288
- const [parentKey, childKey] = item.attribute.split(".");
4289
- const ct2 = strapi.contentTypes[item.uid];
4290
- if (ct2?.attributes?.[parentKey]?.type === "component") {
4291
- const currentDoc = await strapi.documents(item.uid).findOne({
4292
- documentId: item.documentId,
4293
- populate: [parentKey]
4294
- });
4295
- const currentParent = currentDoc?.[parentKey] || {};
4296
- const restoredParent = {
4297
- ...currentParent,
4298
- [childKey]: rollbackPayload
4299
- };
4300
- await strapi.documents(item.uid).update({
4301
- documentId: item.documentId,
4302
- data: {
4303
- [parentKey]: restoredParent
4304
- }
4305
- });
4420
+ const parsed = parseDynamicZonePath(item.attribute);
4421
+ const rootKey = parsed.rootKey;
4422
+ const rootAttrDef = ct.attributes?.[rootKey];
4423
+ const isComponent = rootAttrDef?.type === "component";
4424
+ const isDynamicZone = rootAttrDef?.type === "dynamiczone";
4425
+ const isNested = item.attribute.includes(".") || item.attribute.includes("[") || isComponent || isDynamicZone || isDeepComponentField(ct, strapi.components, item.attribute);
4426
+ let wasPublished = false;
4427
+ if (isNested && (isComponent || isDynamicZone || rootAttrDef)) {
4428
+ const populateOption = buildDeepPopulate$1(ct.attributes, strapi.components);
4429
+ const findOneArgs = { documentId: item.documentId };
4430
+ if (populateOption) {
4431
+ findOneArgs.populate = populateOption;
4432
+ }
4433
+ const currentDoc = await strapi.documents(item.uid).findOne(findOneArgs);
4434
+ if (!currentDoc) {
4435
+ throw new Error(`Document "${item.documentId}" not found in collection "${item.uid}".`);
4436
+ }
4437
+ wasPublished = Boolean(currentDoc?.publishedAt);
4438
+ const isRootDz = isDynamicZone;
4439
+ const existingRoot = currentDoc?.[rootKey] || (isRootDz ? [] : {});
4440
+ let restoredRoot;
4441
+ if (Array.isArray(item.dynamicZoneDiffs) && item.dynamicZoneDiffs.length > 0) {
4442
+ let workingRoot = existingRoot;
4443
+ for (const dzDiff of item.dynamicZoneDiffs) {
4444
+ const parsedDz = parseDynamicZonePath(dzDiff.attribute || item.attribute);
4445
+ const dzParts = parsedDz.cleanPath.split(".").slice(1);
4446
+ const targetIndexes = dzDiff.index !== void 0 ? [dzDiff.index] : parsedDz.targetIndex !== void 0 ? [parsedDz.targetIndex] : void 0;
4447
+ workingRoot = setDeepProperty(workingRoot, dzParts, dzDiff.oldValue ?? null, {
4448
+ targetIndexes,
4449
+ targetComponentUid: dzDiff.componentUid || parsedDz.targetComponentUid,
4450
+ strapi
4451
+ });
4452
+ }
4453
+ restoredRoot = workingRoot;
4306
4454
  } else {
4307
- await strapi.documents(item.uid).update({
4308
- documentId: item.documentId,
4309
- data: {
4310
- [item.attribute]: rollbackPayload
4311
- }
4455
+ const parts = parsed.cleanPath.split(".");
4456
+ const pathParts = parts.slice(1);
4457
+ const targetIndexes = parsed.targetIndex !== void 0 ? [parsed.targetIndex] : void 0;
4458
+ const targetComponentUid = parsed.targetComponentUid;
4459
+ restoredRoot = setDeepProperty(existingRoot, pathParts, rollbackPayload, {
4460
+ targetIndexes,
4461
+ targetComponentUid,
4462
+ strapi
4312
4463
  });
4313
4464
  }
4465
+ await strapi.documents(item.uid).update({
4466
+ documentId: item.documentId,
4467
+ data: {
4468
+ [rootKey]: restoredRoot
4469
+ }
4470
+ });
4314
4471
  } else {
4472
+ const currentDoc = await strapi.documents(item.uid).findOne({
4473
+ documentId: item.documentId
4474
+ });
4475
+ wasPublished = Boolean(currentDoc?.publishedAt);
4315
4476
  await strapi.documents(item.uid).update({
4316
4477
  documentId: item.documentId,
4317
4478
  data: {
@@ -4319,13 +4480,13 @@ const auditService = ({ strapi }) => ({
4319
4480
  }
4320
4481
  });
4321
4482
  }
4322
- const ct = strapi.contentTypes[item.uid];
4323
- if (ct?.options?.draftAndPublish) {
4483
+ if (ct?.options?.draftAndPublish && wasPublished) {
4324
4484
  try {
4325
4485
  await strapi.documents(item.uid).publish({
4326
4486
  documentId: item.documentId
4327
4487
  });
4328
- } catch {
4488
+ } catch (publishErr) {
4489
+ strapi.log.warn(`[bulk-updater] Failed to auto-publish rollback for ${item.uid}/${item.documentId}:`, publishErr?.message);
4329
4490
  }
4330
4491
  }
4331
4492
  try {
@@ -4333,7 +4494,7 @@ const auditService = ({ strapi }) => ({
4333
4494
  uid: item.uid,
4334
4495
  documentId: item.documentId,
4335
4496
  userId,
4336
- statusOverride: ct?.options?.draftAndPublish ? "published" : void 0
4497
+ statusOverride: ct?.options?.draftAndPublish ? wasPublished ? "published" : "draft" : void 0
4337
4498
  });
4338
4499
  } catch {
4339
4500
  }