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