strapi-plugin-bulk-updater 1.2.11 → 1.2.12

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.
@@ -10,7 +10,7 @@ const index = {
10
10
  id: `${PLUGIN_ID}.plugin.name`,
11
11
  defaultMessage: "Bulk Attribute Updater"
12
12
  },
13
- Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-DS6gm_qd.js")),
13
+ Component: () => Promise.resolve().then(() => require("../_chunks/BulkUpdater-CQ9zGdat.js")),
14
14
  permissions: [],
15
15
  position: 3
16
16
  });
@@ -9,7 +9,7 @@ const index = {
9
9
  id: `${PLUGIN_ID}.plugin.name`,
10
10
  defaultMessage: "Bulk Attribute Updater"
11
11
  },
12
- Component: () => import("../_chunks/BulkUpdater-B_COVQmo.mjs"),
12
+ Component: () => import("../_chunks/BulkUpdater-DvCSr1vU.mjs"),
13
13
  permissions: [],
14
14
  position: 3
15
15
  });
@@ -59,6 +59,14 @@ const routes = {
59
59
  policies: []
60
60
  }
61
61
  },
62
+ {
63
+ method: "POST",
64
+ path: "/batch-publish",
65
+ handler: "bulkUpdater.batchPublish",
66
+ config: {
67
+ policies: []
68
+ }
69
+ },
62
70
  {
63
71
  method: "POST",
64
72
  path: "/preview",
@@ -353,6 +361,17 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
353
361
  ctx.body = { success: false, error: err.message };
354
362
  }
355
363
  },
364
+ async batchPublish(ctx) {
365
+ try {
366
+ const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
367
+ const currentUser = ctx.state.user;
368
+ const result = await service.batchPublish(ctx.request.body || {}, currentUser);
369
+ ctx.body = result;
370
+ } catch (err) {
371
+ ctx.status = 400;
372
+ ctx.body = { success: false, error: err.message };
373
+ }
374
+ },
356
375
  async previewBatch(ctx) {
357
376
  try {
358
377
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
@@ -1093,6 +1112,164 @@ async function executeBatchInternal(strapi2, {
1093
1112
  results
1094
1113
  };
1095
1114
  }
1115
+ async function executePublishBatchInternal(strapi2, {
1116
+ selectedEntries = [],
1117
+ selectedCollections = []
1118
+ }, currentUser, existingBatchId) {
1119
+ const targetsMap = /* @__PURE__ */ new Map();
1120
+ for (const colUid of selectedCollections) {
1121
+ if (!strapi2.contentTypes[colUid]) continue;
1122
+ const entries = await strapi2.documents(colUid).findMany();
1123
+ for (const entry of entries || []) {
1124
+ const key = `${colUid}@@@${entry.documentId}`;
1125
+ targetsMap.set(key, { uid: colUid, documentId: entry.documentId });
1126
+ }
1127
+ }
1128
+ for (const item of selectedEntries) {
1129
+ if (!item.uid || !item.documentId) continue;
1130
+ const key = `${item.uid}@@@${item.documentId}`;
1131
+ targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
1132
+ }
1133
+ if (targetsMap.size === 0) {
1134
+ throw new Error("No collections or entries were selected to publish.");
1135
+ }
1136
+ const batchId = existingBatchId || `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
1137
+ const results = [];
1138
+ let publishedCount = 0;
1139
+ let skippedCount = 0;
1140
+ const jobState = {
1141
+ id: batchId,
1142
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1143
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1144
+ createdBy: currentUser ? {
1145
+ id: currentUser.id,
1146
+ email: currentUser.email,
1147
+ firstname: currentUser.firstname,
1148
+ lastname: currentUser.lastname,
1149
+ username: currentUser.username
1150
+ } : { email: "admin@system.local" },
1151
+ status: "processing",
1152
+ executionMode: "immediate",
1153
+ attribute: "(publish)",
1154
+ value: "published",
1155
+ totalTargets: targetsMap.size,
1156
+ processedCount: 0,
1157
+ updatedCount: 0,
1158
+ skippedCount: 0,
1159
+ failedCount: 0,
1160
+ isLive: false,
1161
+ publishOnApproval: true,
1162
+ results: []
1163
+ };
1164
+ batchJobsMap.set(batchId, jobState);
1165
+ const targets = Array.from(targetsMap.values());
1166
+ const CHUNK_SIZE = 25;
1167
+ for (let i = 0; i < targets.length; i += CHUNK_SIZE) {
1168
+ const chunk = targets.slice(i, i + CHUNK_SIZE);
1169
+ const chunkPromises = chunk.map(async (target) => {
1170
+ try {
1171
+ const contentType = strapi2.contentTypes[target.uid];
1172
+ const displayName = contentType?.info?.displayName || target.uid;
1173
+ const isDraftAndPublish = Boolean(contentType?.options?.draftAndPublish);
1174
+ let existingDoc = null;
1175
+ try {
1176
+ existingDoc = await strapi2.documents(target.uid).findOne({
1177
+ documentId: target.documentId
1178
+ });
1179
+ } catch {
1180
+ existingDoc = null;
1181
+ }
1182
+ const entryLabel = existingDoc?.name || existingDoc?.title || existingDoc?.label || existingDoc?.documentId || `Entry #${target.documentId}`;
1183
+ if (!isDraftAndPublish) {
1184
+ return {
1185
+ uid: target.uid,
1186
+ collectionName: displayName,
1187
+ documentId: target.documentId,
1188
+ label: entryLabel,
1189
+ attribute: "(publish)",
1190
+ status: "skipped",
1191
+ message: `Skipped: Draft & Publish is disabled for collection ${displayName}`
1192
+ };
1193
+ }
1194
+ const isAlreadyPublished = Boolean(existingDoc?.publishedAt);
1195
+ await strapi2.documents(target.uid).publish({
1196
+ documentId: target.documentId
1197
+ });
1198
+ invalidateCollectionCache(target.uid);
1199
+ return {
1200
+ uid: target.uid,
1201
+ collectionName: displayName,
1202
+ documentId: target.documentId,
1203
+ label: entryLabel,
1204
+ attribute: "(publish)",
1205
+ oldValue: isAlreadyPublished ? "published" : "draft",
1206
+ newValue: "published",
1207
+ status: "updated",
1208
+ message: isAlreadyPublished ? "Re-published latest version live" : "Successfully published to live"
1209
+ };
1210
+ } catch (pubErr) {
1211
+ return {
1212
+ uid: target.uid,
1213
+ collectionName: strapi2.contentTypes[target.uid]?.info?.displayName || target.uid,
1214
+ documentId: target.documentId,
1215
+ label: `Entry #${target.documentId}`,
1216
+ attribute: "(publish)",
1217
+ status: "error",
1218
+ message: pubErr.message || "Failed to publish entry"
1219
+ };
1220
+ }
1221
+ });
1222
+ const chunkResults = await Promise.all(chunkPromises);
1223
+ for (const res of chunkResults) {
1224
+ if (res.status === "updated") publishedCount++;
1225
+ else if (res.status === "skipped") skippedCount++;
1226
+ results.push(res);
1227
+ }
1228
+ jobState.processedCount = results.length;
1229
+ jobState.updatedCount = publishedCount;
1230
+ jobState.skippedCount = skippedCount;
1231
+ jobState.failedCount = results.filter((r) => r.status === "error").length;
1232
+ }
1233
+ const affectedUids = new Set(targets.map((t) => t.uid));
1234
+ affectedUids.forEach((uid) => {
1235
+ invalidateCollectionCache(uid);
1236
+ });
1237
+ const failedCount = results.filter((r) => r.status === "error").length;
1238
+ jobState.failedCount = failedCount;
1239
+ jobState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1240
+ jobState.isLive = publishedCount > 0;
1241
+ jobState.status = failedCount === 0 && publishedCount > 0 ? "successful" : failedCount > 0 && publishedCount > 0 ? "partial_failure" : failedCount > 0 ? "failed" : "successful";
1242
+ jobState.results = results.map((r) => ({
1243
+ ...r,
1244
+ isLive: r.status === "updated"
1245
+ }));
1246
+ const batchSummary = {
1247
+ batchId,
1248
+ attribute: "(publish)",
1249
+ value: "published",
1250
+ updatedCount: publishedCount,
1251
+ skippedCount,
1252
+ totalTargets: targetsMap.size,
1253
+ publishOnApproval: true
1254
+ };
1255
+ await strapi2.plugin("bulk-updater").service("audit").recordBatch(
1256
+ batchSummary,
1257
+ results,
1258
+ currentUser
1259
+ );
1260
+ return {
1261
+ success: true,
1262
+ batchId,
1263
+ publishedCount,
1264
+ updatedCount: publishedCount,
1265
+ skippedCount,
1266
+ failedCount,
1267
+ totalTargets: targetsMap.size,
1268
+ isLive: publishedCount > 0,
1269
+ status: jobState.status,
1270
+ results
1271
+ };
1272
+ }
1096
1273
  const bulkUpdaterService = ({ strapi: strapi2 }) => {
1097
1274
  if (!runnerInterval) {
1098
1275
  runnerInterval = setInterval(async () => {
@@ -1104,7 +1281,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1104
1281
  for (const [id, batch] of pendingBatches.entries()) {
1105
1282
  if (batch.status === "queued_offpeak" && !isBusinessHours) {
1106
1283
  strapi2.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
1107
- const result = await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
1284
+ const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, batch.createdBy, id) : await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
1108
1285
  batch.status = "executed";
1109
1286
  batch.executionResult = result;
1110
1287
  }
@@ -1201,6 +1378,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1201
1378
  uid,
1202
1379
  kind: contentType.kind,
1203
1380
  isSingleType,
1381
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1204
1382
  singularName: contentType.info?.singularName || uid,
1205
1383
  pluralName: contentType.info?.pluralName || uid,
1206
1384
  displayName: contentType.info?.displayName || uid,
@@ -1382,6 +1560,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1382
1560
  uid,
1383
1561
  displayName: contentType.info?.displayName || uid,
1384
1562
  isSingleType,
1563
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1385
1564
  entries: [],
1386
1565
  pagination: {
1387
1566
  page: 1,
@@ -1473,6 +1652,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1473
1652
  uid,
1474
1653
  displayName: contentType.info?.displayName || uid,
1475
1654
  isSingleType: true,
1655
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1476
1656
  entries: singleDoc ? [singleDoc] : [],
1477
1657
  pagination: {
1478
1658
  page: 1,
@@ -1516,6 +1696,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1516
1696
  uid,
1517
1697
  displayName: contentType.info?.displayName || uid,
1518
1698
  isSingleType: false,
1699
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1519
1700
  entries: (entries || []).map((entry) => ({
1520
1701
  id: entry.id,
1521
1702
  documentId: entry.documentId,
@@ -2016,6 +2197,199 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2016
2197
  currentUser
2017
2198
  );
2018
2199
  },
2200
+ /**
2201
+ * Bulk Publish selected entries or collections
2202
+ */
2203
+ async batchPublish({
2204
+ selectedEntries = [],
2205
+ selectedCollections = [],
2206
+ executionMode = "immediate",
2207
+ makerNotes = "",
2208
+ forceImmediate = false
2209
+ }, currentUser) {
2210
+ const executionConfig = getExecutionWindowConfig(strapi2);
2211
+ const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
2212
+ if (executionMode === "maker_checker") {
2213
+ const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
2214
+ if (summary.totalTargets === 0) {
2215
+ throw new Error("No collections or entries were selected to publish.");
2216
+ }
2217
+ const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
2218
+ const batch = {
2219
+ id: batchId,
2220
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2221
+ createdBy: currentUser ? {
2222
+ id: currentUser.id,
2223
+ email: currentUser.email,
2224
+ firstname: currentUser.firstname,
2225
+ lastname: currentUser.lastname,
2226
+ username: currentUser.username
2227
+ } : { email: "admin@system.local" },
2228
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
2229
+ summary: {
2230
+ attribute: "(publish)",
2231
+ value: "published",
2232
+ totalTargets: summary.totalTargets,
2233
+ collectionBreakdown: summary.collectionBreakdown,
2234
+ publishOnApproval: true
2235
+ },
2236
+ status: "pending_approval",
2237
+ executionMode: "maker_checker",
2238
+ makerNotes
2239
+ };
2240
+ pendingBatches.set(batchId, batch);
2241
+ batchJobsMap.set(batchId, {
2242
+ id: batchId,
2243
+ createdAt: batch.createdAt,
2244
+ createdBy: batch.createdBy,
2245
+ status: "pending_approval",
2246
+ executionMode: "maker_checker",
2247
+ attribute: "(publish)",
2248
+ value: "published",
2249
+ totalTargets: summary.totalTargets,
2250
+ processedCount: 0,
2251
+ updatedCount: 0,
2252
+ skippedCount: 0,
2253
+ failedCount: 0,
2254
+ isLive: false,
2255
+ publishOnApproval: true,
2256
+ makerNotes,
2257
+ results: []
2258
+ });
2259
+ return {
2260
+ success: true,
2261
+ pending: true,
2262
+ batchId,
2263
+ status: "pending_approval",
2264
+ message: "Bulk publish batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
2265
+ batch
2266
+ };
2267
+ }
2268
+ if (executionMode === "queue_offpeak") {
2269
+ const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
2270
+ if (summary.totalTargets === 0) {
2271
+ throw new Error("No collections or entries were selected to publish.");
2272
+ }
2273
+ const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
2274
+ const batch = {
2275
+ id: batchId,
2276
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2277
+ createdBy: currentUser ? {
2278
+ id: currentUser.id,
2279
+ email: currentUser.email,
2280
+ firstname: currentUser.firstname,
2281
+ lastname: currentUser.lastname,
2282
+ username: currentUser.username
2283
+ } : { email: "admin@system.local" },
2284
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
2285
+ summary: {
2286
+ attribute: "(publish)",
2287
+ value: "published",
2288
+ totalTargets: summary.totalTargets,
2289
+ collectionBreakdown: summary.collectionBreakdown,
2290
+ publishOnApproval: true
2291
+ },
2292
+ status: "queued_offpeak",
2293
+ executionMode: "queue_offpeak",
2294
+ scheduledFor: executionConfig.offPeakStartTime,
2295
+ makerNotes
2296
+ };
2297
+ pendingBatches.set(batchId, batch);
2298
+ batchJobsMap.set(batchId, {
2299
+ id: batchId,
2300
+ createdAt: batch.createdAt,
2301
+ createdBy: batch.createdBy,
2302
+ status: "queued_offpeak",
2303
+ executionMode: "queue_offpeak",
2304
+ scheduledFor: executionConfig.offPeakStartTime,
2305
+ attribute: "(publish)",
2306
+ value: "published",
2307
+ totalTargets: summary.totalTargets,
2308
+ processedCount: 0,
2309
+ updatedCount: 0,
2310
+ skippedCount: 0,
2311
+ failedCount: 0,
2312
+ isLive: false,
2313
+ publishOnApproval: true,
2314
+ makerNotes,
2315
+ results: []
2316
+ });
2317
+ return {
2318
+ success: true,
2319
+ pending: true,
2320
+ batchId,
2321
+ status: "queued_offpeak",
2322
+ scheduledFor: executionConfig.offPeakStartTime,
2323
+ message: `Bulk publish batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
2324
+ batch
2325
+ };
2326
+ }
2327
+ if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
2328
+ const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
2329
+ if (summary.totalTargets === 0) {
2330
+ throw new Error("No collections or entries were selected to publish.");
2331
+ }
2332
+ const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
2333
+ const batch = {
2334
+ id: batchId,
2335
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2336
+ createdBy: currentUser ? {
2337
+ id: currentUser.id,
2338
+ email: currentUser.email,
2339
+ firstname: currentUser.firstname,
2340
+ lastname: currentUser.lastname,
2341
+ username: currentUser.username
2342
+ } : { email: "admin@system.local" },
2343
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
2344
+ summary: {
2345
+ attribute: "(publish)",
2346
+ value: "published",
2347
+ totalTargets: summary.totalTargets,
2348
+ collectionBreakdown: summary.collectionBreakdown,
2349
+ publishOnApproval: true
2350
+ },
2351
+ status: "queued_offpeak",
2352
+ executionMode: "queue_offpeak",
2353
+ scheduledFor: executionConfig.offPeakStartTime,
2354
+ makerNotes: "Automatically queued due to peak business hours restriction."
2355
+ };
2356
+ pendingBatches.set(batchId, batch);
2357
+ batchJobsMap.set(batchId, {
2358
+ id: batchId,
2359
+ createdAt: batch.createdAt,
2360
+ createdBy: batch.createdBy,
2361
+ status: "queued_offpeak",
2362
+ executionMode: "queue_offpeak",
2363
+ scheduledFor: executionConfig.offPeakStartTime,
2364
+ attribute: "(publish)",
2365
+ value: "published",
2366
+ totalTargets: summary.totalTargets,
2367
+ processedCount: 0,
2368
+ updatedCount: 0,
2369
+ skippedCount: 0,
2370
+ failedCount: 0,
2371
+ isLive: false,
2372
+ publishOnApproval: true,
2373
+ policyMessage,
2374
+ makerNotes: "Automatically queued due to peak business hours restriction.",
2375
+ results: []
2376
+ });
2377
+ return {
2378
+ success: true,
2379
+ pending: true,
2380
+ batchId,
2381
+ status: "queued_offpeak",
2382
+ scheduledFor: executionConfig.offPeakStartTime,
2383
+ message: policyMessage,
2384
+ batch
2385
+ };
2386
+ }
2387
+ return await executePublishBatchInternal(
2388
+ strapi2,
2389
+ { selectedEntries, selectedCollections },
2390
+ currentUser
2391
+ );
2392
+ },
2019
2393
  /**
2020
2394
  * Get Pending & Queued Batches
2021
2395
  */
@@ -2059,7 +2433,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2059
2433
  const { isBusinessHours } = checkBusinessHours(config);
2060
2434
  if (options.executeImmediately || !isBusinessHours) {
2061
2435
  batch.status = "approved";
2062
- const result = await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
2436
+ const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, checkerUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
2063
2437
  batch.status = "executed";
2064
2438
  batch.executionResult = result;
2065
2439
  return {
@@ -2120,7 +2494,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2120
2494
  if (batch.status === "executed") {
2121
2495
  throw new Error(`Batch ${batchId} has already been executed.`);
2122
2496
  }
2123
- const result = await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
2497
+ const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, currentUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
2124
2498
  batch.status = "executed";
2125
2499
  batch.executionResult = result;
2126
2500
  return {