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.
@@ -46,6 +46,14 @@ const routes = {
46
46
  policies: []
47
47
  }
48
48
  },
49
+ {
50
+ method: "POST",
51
+ path: "/batch-publish",
52
+ handler: "bulkUpdater.batchPublish",
53
+ config: {
54
+ policies: []
55
+ }
56
+ },
49
57
  {
50
58
  method: "POST",
51
59
  path: "/preview",
@@ -340,6 +348,17 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
340
348
  ctx.body = { success: false, error: err.message };
341
349
  }
342
350
  },
351
+ async batchPublish(ctx) {
352
+ try {
353
+ const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
354
+ const currentUser = ctx.state.user;
355
+ const result = await service.batchPublish(ctx.request.body || {}, currentUser);
356
+ ctx.body = result;
357
+ } catch (err) {
358
+ ctx.status = 400;
359
+ ctx.body = { success: false, error: err.message };
360
+ }
361
+ },
343
362
  async previewBatch(ctx) {
344
363
  try {
345
364
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
@@ -1080,6 +1099,164 @@ async function executeBatchInternal(strapi2, {
1080
1099
  results
1081
1100
  };
1082
1101
  }
1102
+ async function executePublishBatchInternal(strapi2, {
1103
+ selectedEntries = [],
1104
+ selectedCollections = []
1105
+ }, currentUser, existingBatchId) {
1106
+ const targetsMap = /* @__PURE__ */ new Map();
1107
+ for (const colUid of selectedCollections) {
1108
+ if (!strapi2.contentTypes[colUid]) continue;
1109
+ const entries = await strapi2.documents(colUid).findMany();
1110
+ for (const entry of entries || []) {
1111
+ const key = `${colUid}@@@${entry.documentId}`;
1112
+ targetsMap.set(key, { uid: colUid, documentId: entry.documentId });
1113
+ }
1114
+ }
1115
+ for (const item of selectedEntries) {
1116
+ if (!item.uid || !item.documentId) continue;
1117
+ const key = `${item.uid}@@@${item.documentId}`;
1118
+ targetsMap.set(key, { uid: item.uid, documentId: item.documentId });
1119
+ }
1120
+ if (targetsMap.size === 0) {
1121
+ throw new Error("No collections or entries were selected to publish.");
1122
+ }
1123
+ const batchId = existingBatchId || `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
1124
+ const results = [];
1125
+ let publishedCount = 0;
1126
+ let skippedCount = 0;
1127
+ const jobState = {
1128
+ id: batchId,
1129
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1130
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1131
+ createdBy: currentUser ? {
1132
+ id: currentUser.id,
1133
+ email: currentUser.email,
1134
+ firstname: currentUser.firstname,
1135
+ lastname: currentUser.lastname,
1136
+ username: currentUser.username
1137
+ } : { email: "admin@system.local" },
1138
+ status: "processing",
1139
+ executionMode: "immediate",
1140
+ attribute: "(publish)",
1141
+ value: "published",
1142
+ totalTargets: targetsMap.size,
1143
+ processedCount: 0,
1144
+ updatedCount: 0,
1145
+ skippedCount: 0,
1146
+ failedCount: 0,
1147
+ isLive: false,
1148
+ publishOnApproval: true,
1149
+ results: []
1150
+ };
1151
+ batchJobsMap.set(batchId, jobState);
1152
+ const targets = Array.from(targetsMap.values());
1153
+ const CHUNK_SIZE = 25;
1154
+ for (let i = 0; i < targets.length; i += CHUNK_SIZE) {
1155
+ const chunk = targets.slice(i, i + CHUNK_SIZE);
1156
+ const chunkPromises = chunk.map(async (target) => {
1157
+ try {
1158
+ const contentType = strapi2.contentTypes[target.uid];
1159
+ const displayName = contentType?.info?.displayName || target.uid;
1160
+ const isDraftAndPublish = Boolean(contentType?.options?.draftAndPublish);
1161
+ let existingDoc = null;
1162
+ try {
1163
+ existingDoc = await strapi2.documents(target.uid).findOne({
1164
+ documentId: target.documentId
1165
+ });
1166
+ } catch {
1167
+ existingDoc = null;
1168
+ }
1169
+ const entryLabel = existingDoc?.name || existingDoc?.title || existingDoc?.label || existingDoc?.documentId || `Entry #${target.documentId}`;
1170
+ if (!isDraftAndPublish) {
1171
+ return {
1172
+ uid: target.uid,
1173
+ collectionName: displayName,
1174
+ documentId: target.documentId,
1175
+ label: entryLabel,
1176
+ attribute: "(publish)",
1177
+ status: "skipped",
1178
+ message: `Skipped: Draft & Publish is disabled for collection ${displayName}`
1179
+ };
1180
+ }
1181
+ const isAlreadyPublished = Boolean(existingDoc?.publishedAt);
1182
+ await strapi2.documents(target.uid).publish({
1183
+ documentId: target.documentId
1184
+ });
1185
+ invalidateCollectionCache(target.uid);
1186
+ return {
1187
+ uid: target.uid,
1188
+ collectionName: displayName,
1189
+ documentId: target.documentId,
1190
+ label: entryLabel,
1191
+ attribute: "(publish)",
1192
+ oldValue: isAlreadyPublished ? "published" : "draft",
1193
+ newValue: "published",
1194
+ status: "updated",
1195
+ message: isAlreadyPublished ? "Re-published latest version live" : "Successfully published to live"
1196
+ };
1197
+ } catch (pubErr) {
1198
+ return {
1199
+ uid: target.uid,
1200
+ collectionName: strapi2.contentTypes[target.uid]?.info?.displayName || target.uid,
1201
+ documentId: target.documentId,
1202
+ label: `Entry #${target.documentId}`,
1203
+ attribute: "(publish)",
1204
+ status: "error",
1205
+ message: pubErr.message || "Failed to publish entry"
1206
+ };
1207
+ }
1208
+ });
1209
+ const chunkResults = await Promise.all(chunkPromises);
1210
+ for (const res of chunkResults) {
1211
+ if (res.status === "updated") publishedCount++;
1212
+ else if (res.status === "skipped") skippedCount++;
1213
+ results.push(res);
1214
+ }
1215
+ jobState.processedCount = results.length;
1216
+ jobState.updatedCount = publishedCount;
1217
+ jobState.skippedCount = skippedCount;
1218
+ jobState.failedCount = results.filter((r) => r.status === "error").length;
1219
+ }
1220
+ const affectedUids = new Set(targets.map((t) => t.uid));
1221
+ affectedUids.forEach((uid) => {
1222
+ invalidateCollectionCache(uid);
1223
+ });
1224
+ const failedCount = results.filter((r) => r.status === "error").length;
1225
+ jobState.failedCount = failedCount;
1226
+ jobState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1227
+ jobState.isLive = publishedCount > 0;
1228
+ jobState.status = failedCount === 0 && publishedCount > 0 ? "successful" : failedCount > 0 && publishedCount > 0 ? "partial_failure" : failedCount > 0 ? "failed" : "successful";
1229
+ jobState.results = results.map((r) => ({
1230
+ ...r,
1231
+ isLive: r.status === "updated"
1232
+ }));
1233
+ const batchSummary = {
1234
+ batchId,
1235
+ attribute: "(publish)",
1236
+ value: "published",
1237
+ updatedCount: publishedCount,
1238
+ skippedCount,
1239
+ totalTargets: targetsMap.size,
1240
+ publishOnApproval: true
1241
+ };
1242
+ await strapi2.plugin("bulk-updater").service("audit").recordBatch(
1243
+ batchSummary,
1244
+ results,
1245
+ currentUser
1246
+ );
1247
+ return {
1248
+ success: true,
1249
+ batchId,
1250
+ publishedCount,
1251
+ updatedCount: publishedCount,
1252
+ skippedCount,
1253
+ failedCount,
1254
+ totalTargets: targetsMap.size,
1255
+ isLive: publishedCount > 0,
1256
+ status: jobState.status,
1257
+ results
1258
+ };
1259
+ }
1083
1260
  const bulkUpdaterService = ({ strapi: strapi2 }) => {
1084
1261
  if (!runnerInterval) {
1085
1262
  runnerInterval = setInterval(async () => {
@@ -1091,7 +1268,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1091
1268
  for (const [id, batch] of pendingBatches.entries()) {
1092
1269
  if (batch.status === "queued_offpeak" && !isBusinessHours) {
1093
1270
  strapi2.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
1094
- const result = await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
1271
+ const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, batch.createdBy, id) : await executeBatchInternal(strapi2, batch.payload, batch.createdBy, id);
1095
1272
  batch.status = "executed";
1096
1273
  batch.executionResult = result;
1097
1274
  }
@@ -1188,6 +1365,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1188
1365
  uid,
1189
1366
  kind: contentType.kind,
1190
1367
  isSingleType,
1368
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1191
1369
  singularName: contentType.info?.singularName || uid,
1192
1370
  pluralName: contentType.info?.pluralName || uid,
1193
1371
  displayName: contentType.info?.displayName || uid,
@@ -1369,6 +1547,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1369
1547
  uid,
1370
1548
  displayName: contentType.info?.displayName || uid,
1371
1549
  isSingleType,
1550
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1372
1551
  entries: [],
1373
1552
  pagination: {
1374
1553
  page: 1,
@@ -1460,6 +1639,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1460
1639
  uid,
1461
1640
  displayName: contentType.info?.displayName || uid,
1462
1641
  isSingleType: true,
1642
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1463
1643
  entries: singleDoc ? [singleDoc] : [],
1464
1644
  pagination: {
1465
1645
  page: 1,
@@ -1503,6 +1683,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1503
1683
  uid,
1504
1684
  displayName: contentType.info?.displayName || uid,
1505
1685
  isSingleType: false,
1686
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1506
1687
  entries: (entries || []).map((entry) => ({
1507
1688
  id: entry.id,
1508
1689
  documentId: entry.documentId,
@@ -2003,6 +2184,199 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2003
2184
  currentUser
2004
2185
  );
2005
2186
  },
2187
+ /**
2188
+ * Bulk Publish selected entries or collections
2189
+ */
2190
+ async batchPublish({
2191
+ selectedEntries = [],
2192
+ selectedCollections = [],
2193
+ executionMode = "immediate",
2194
+ makerNotes = "",
2195
+ forceImmediate = false
2196
+ }, currentUser) {
2197
+ const executionConfig = getExecutionWindowConfig(strapi2);
2198
+ const { isBusinessHours, policyMessage } = checkBusinessHours(executionConfig);
2199
+ if (executionMode === "maker_checker") {
2200
+ const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
2201
+ if (summary.totalTargets === 0) {
2202
+ throw new Error("No collections or entries were selected to publish.");
2203
+ }
2204
+ const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
2205
+ const batch = {
2206
+ id: batchId,
2207
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2208
+ createdBy: currentUser ? {
2209
+ id: currentUser.id,
2210
+ email: currentUser.email,
2211
+ firstname: currentUser.firstname,
2212
+ lastname: currentUser.lastname,
2213
+ username: currentUser.username
2214
+ } : { email: "admin@system.local" },
2215
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
2216
+ summary: {
2217
+ attribute: "(publish)",
2218
+ value: "published",
2219
+ totalTargets: summary.totalTargets,
2220
+ collectionBreakdown: summary.collectionBreakdown,
2221
+ publishOnApproval: true
2222
+ },
2223
+ status: "pending_approval",
2224
+ executionMode: "maker_checker",
2225
+ makerNotes
2226
+ };
2227
+ pendingBatches.set(batchId, batch);
2228
+ batchJobsMap.set(batchId, {
2229
+ id: batchId,
2230
+ createdAt: batch.createdAt,
2231
+ createdBy: batch.createdBy,
2232
+ status: "pending_approval",
2233
+ executionMode: "maker_checker",
2234
+ attribute: "(publish)",
2235
+ value: "published",
2236
+ totalTargets: summary.totalTargets,
2237
+ processedCount: 0,
2238
+ updatedCount: 0,
2239
+ skippedCount: 0,
2240
+ failedCount: 0,
2241
+ isLive: false,
2242
+ publishOnApproval: true,
2243
+ makerNotes,
2244
+ results: []
2245
+ });
2246
+ return {
2247
+ success: true,
2248
+ pending: true,
2249
+ batchId,
2250
+ status: "pending_approval",
2251
+ message: "Bulk publish batch submitted for Checker authorization (4-Eyes Maker-Checker Workflow).",
2252
+ batch
2253
+ };
2254
+ }
2255
+ if (executionMode === "queue_offpeak") {
2256
+ const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
2257
+ if (summary.totalTargets === 0) {
2258
+ throw new Error("No collections or entries were selected to publish.");
2259
+ }
2260
+ const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
2261
+ const batch = {
2262
+ id: batchId,
2263
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2264
+ createdBy: currentUser ? {
2265
+ id: currentUser.id,
2266
+ email: currentUser.email,
2267
+ firstname: currentUser.firstname,
2268
+ lastname: currentUser.lastname,
2269
+ username: currentUser.username
2270
+ } : { email: "admin@system.local" },
2271
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
2272
+ summary: {
2273
+ attribute: "(publish)",
2274
+ value: "published",
2275
+ totalTargets: summary.totalTargets,
2276
+ collectionBreakdown: summary.collectionBreakdown,
2277
+ publishOnApproval: true
2278
+ },
2279
+ status: "queued_offpeak",
2280
+ executionMode: "queue_offpeak",
2281
+ scheduledFor: executionConfig.offPeakStartTime,
2282
+ makerNotes
2283
+ };
2284
+ pendingBatches.set(batchId, batch);
2285
+ batchJobsMap.set(batchId, {
2286
+ id: batchId,
2287
+ createdAt: batch.createdAt,
2288
+ createdBy: batch.createdBy,
2289
+ status: "queued_offpeak",
2290
+ executionMode: "queue_offpeak",
2291
+ scheduledFor: executionConfig.offPeakStartTime,
2292
+ attribute: "(publish)",
2293
+ value: "published",
2294
+ totalTargets: summary.totalTargets,
2295
+ processedCount: 0,
2296
+ updatedCount: 0,
2297
+ skippedCount: 0,
2298
+ failedCount: 0,
2299
+ isLive: false,
2300
+ publishOnApproval: true,
2301
+ makerNotes,
2302
+ results: []
2303
+ });
2304
+ return {
2305
+ success: true,
2306
+ pending: true,
2307
+ batchId,
2308
+ status: "queued_offpeak",
2309
+ scheduledFor: executionConfig.offPeakStartTime,
2310
+ message: `Bulk publish batch queued for server off-peak maintenance window (${executionConfig.offPeakStartTime}).`,
2311
+ batch
2312
+ };
2313
+ }
2314
+ if (executionConfig.enabled && executionConfig.autoQueueDuringBusinessHours && isBusinessHours && !forceImmediate) {
2315
+ const summary = await computeTargetsSummary(strapi2, { selectedCollections, selectedEntries });
2316
+ if (summary.totalTargets === 0) {
2317
+ throw new Error("No collections or entries were selected to publish.");
2318
+ }
2319
+ const batchId = `batch_pub_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
2320
+ const batch = {
2321
+ id: batchId,
2322
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2323
+ createdBy: currentUser ? {
2324
+ id: currentUser.id,
2325
+ email: currentUser.email,
2326
+ firstname: currentUser.firstname,
2327
+ lastname: currentUser.lastname,
2328
+ username: currentUser.username
2329
+ } : { email: "admin@system.local" },
2330
+ payload: { action: "publish", attribute: "(publish)", value: "published", selectedEntries, selectedCollections, publishOnApproval: true },
2331
+ summary: {
2332
+ attribute: "(publish)",
2333
+ value: "published",
2334
+ totalTargets: summary.totalTargets,
2335
+ collectionBreakdown: summary.collectionBreakdown,
2336
+ publishOnApproval: true
2337
+ },
2338
+ status: "queued_offpeak",
2339
+ executionMode: "queue_offpeak",
2340
+ scheduledFor: executionConfig.offPeakStartTime,
2341
+ makerNotes: "Automatically queued due to peak business hours restriction."
2342
+ };
2343
+ pendingBatches.set(batchId, batch);
2344
+ batchJobsMap.set(batchId, {
2345
+ id: batchId,
2346
+ createdAt: batch.createdAt,
2347
+ createdBy: batch.createdBy,
2348
+ status: "queued_offpeak",
2349
+ executionMode: "queue_offpeak",
2350
+ scheduledFor: executionConfig.offPeakStartTime,
2351
+ attribute: "(publish)",
2352
+ value: "published",
2353
+ totalTargets: summary.totalTargets,
2354
+ processedCount: 0,
2355
+ updatedCount: 0,
2356
+ skippedCount: 0,
2357
+ failedCount: 0,
2358
+ isLive: false,
2359
+ publishOnApproval: true,
2360
+ policyMessage,
2361
+ makerNotes: "Automatically queued due to peak business hours restriction.",
2362
+ results: []
2363
+ });
2364
+ return {
2365
+ success: true,
2366
+ pending: true,
2367
+ batchId,
2368
+ status: "queued_offpeak",
2369
+ scheduledFor: executionConfig.offPeakStartTime,
2370
+ message: policyMessage,
2371
+ batch
2372
+ };
2373
+ }
2374
+ return await executePublishBatchInternal(
2375
+ strapi2,
2376
+ { selectedEntries, selectedCollections },
2377
+ currentUser
2378
+ );
2379
+ },
2006
2380
  /**
2007
2381
  * Get Pending & Queued Batches
2008
2382
  */
@@ -2046,7 +2420,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2046
2420
  const { isBusinessHours } = checkBusinessHours(config);
2047
2421
  if (options.executeImmediately || !isBusinessHours) {
2048
2422
  batch.status = "approved";
2049
- const result = await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
2423
+ const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, checkerUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, checkerUser, batch.id);
2050
2424
  batch.status = "executed";
2051
2425
  batch.executionResult = result;
2052
2426
  return {
@@ -2107,7 +2481,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2107
2481
  if (batch.status === "executed") {
2108
2482
  throw new Error(`Batch ${batchId} has already been executed.`);
2109
2483
  }
2110
- const result = await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
2484
+ const result = batch.payload?.action === "publish" ? await executePublishBatchInternal(strapi2, batch.payload, currentUser, batch.id) : await executeBatchInternal(strapi2, batch.payload, currentUser, batch.id);
2111
2485
  batch.status = "executed";
2112
2486
  batch.executionResult = result;
2113
2487
  return {