strapi-plugin-bulk-updater 1.2.10 → 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",
@@ -281,10 +289,6 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
281
289
  if (data?.etag) {
282
290
  ctx.set("ETag", data.etag);
283
291
  ctx.set("Cache-Control", "private, no-cache");
284
- if (ctx.request.headers["if-none-match"] === data.etag) {
285
- ctx.status = 304;
286
- return;
287
- }
288
292
  }
289
293
  ctx.body = data;
290
294
  } catch (err) {
@@ -310,10 +314,6 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
310
314
  if (data?.etag) {
311
315
  ctx.set("ETag", data.etag);
312
316
  ctx.set("Cache-Control", "private, no-cache");
313
- if (ctx.request.headers["if-none-match"] === data.etag) {
314
- ctx.status = 304;
315
- return;
316
- }
317
317
  }
318
318
  ctx.body = { success: true, ...data };
319
319
  } catch (err) {
@@ -348,6 +348,17 @@ const bulkUpdaterController = ({ strapi: strapi2 }) => ({
348
348
  ctx.body = { success: false, error: err.message };
349
349
  }
350
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
+ },
351
362
  async previewBatch(ctx) {
352
363
  try {
353
364
  const service = strapi2.plugin("bulk-updater").service("bulkUpdater");
@@ -1088,6 +1099,164 @@ async function executeBatchInternal(strapi2, {
1088
1099
  results
1089
1100
  };
1090
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
+ }
1091
1260
  const bulkUpdaterService = ({ strapi: strapi2 }) => {
1092
1261
  if (!runnerInterval) {
1093
1262
  runnerInterval = setInterval(async () => {
@@ -1099,7 +1268,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1099
1268
  for (const [id, batch] of pendingBatches.entries()) {
1100
1269
  if (batch.status === "queued_offpeak" && !isBusinessHours) {
1101
1270
  strapi2.log.info(`[bulk-updater] Executing off-peak queued batch: ${id}`);
1102
- 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);
1103
1272
  batch.status = "executed";
1104
1273
  batch.executionResult = result;
1105
1274
  }
@@ -1196,6 +1365,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1196
1365
  uid,
1197
1366
  kind: contentType.kind,
1198
1367
  isSingleType,
1368
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1199
1369
  singularName: contentType.info?.singularName || uid,
1200
1370
  pluralName: contentType.info?.pluralName || uid,
1201
1371
  displayName: contentType.info?.displayName || uid,
@@ -1377,6 +1547,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1377
1547
  uid,
1378
1548
  displayName: contentType.info?.displayName || uid,
1379
1549
  isSingleType,
1550
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1380
1551
  entries: [],
1381
1552
  pagination: {
1382
1553
  page: 1,
@@ -1390,10 +1561,12 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1390
1561
  }
1391
1562
  let condition = void 0;
1392
1563
  const q = (filterQuery || "").trim();
1564
+ const targetAttrSchema = isNestedAttr ? strapi2.components[attrs[parentKey]?.component]?.attributes?.[childKey] : attrs[attribute];
1565
+ const isStringLike = ["string", "text", "email", "richtext", "uid"].includes(targetAttrSchema?.type);
1393
1566
  if (normOp === "empty") {
1394
- condition = { $or: [{ $null: true }, { $eq: "" }] };
1567
+ condition = isStringLike ? { $or: [{ $null: true }, { $eq: "" }] } : { $null: true };
1395
1568
  } else if (normOp === "notempty") {
1396
- condition = { $and: [{ $notNull: true }, { $ne: "" }] };
1569
+ condition = isStringLike ? { $and: [{ $notNull: true }, { $ne: "" }] } : { $notNull: true };
1397
1570
  } else if (q !== "") {
1398
1571
  switch (normOp) {
1399
1572
  case "equals":
@@ -1422,12 +1595,12 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1422
1595
  [childKey]: condition
1423
1596
  };
1424
1597
  } else {
1425
- if (normOp === "empty") {
1598
+ if (normOp === "empty" && isStringLike) {
1426
1599
  filters2.$or = [
1427
1600
  { [attribute]: { $null: true } },
1428
1601
  { [attribute]: { $eq: "" } }
1429
1602
  ];
1430
- } else if (normOp === "notempty") {
1603
+ } else if (normOp === "notempty" && isStringLike) {
1431
1604
  filters2[attribute] = { $notNull: true, $ne: "" };
1432
1605
  } else {
1433
1606
  filters2[attribute] = condition;
@@ -1466,6 +1639,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1466
1639
  uid,
1467
1640
  displayName: contentType.info?.displayName || uid,
1468
1641
  isSingleType: true,
1642
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1469
1643
  entries: singleDoc ? [singleDoc] : [],
1470
1644
  pagination: {
1471
1645
  page: 1,
@@ -1509,6 +1683,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
1509
1683
  uid,
1510
1684
  displayName: contentType.info?.displayName || uid,
1511
1685
  isSingleType: false,
1686
+ draftAndPublish: Boolean(contentType.options?.draftAndPublish),
1512
1687
  entries: (entries || []).map((entry) => ({
1513
1688
  id: entry.id,
1514
1689
  documentId: entry.documentId,
@@ -2009,6 +2184,199 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2009
2184
  currentUser
2010
2185
  );
2011
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
+ },
2012
2380
  /**
2013
2381
  * Get Pending & Queued Batches
2014
2382
  */
@@ -2052,7 +2420,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2052
2420
  const { isBusinessHours } = checkBusinessHours(config);
2053
2421
  if (options.executeImmediately || !isBusinessHours) {
2054
2422
  batch.status = "approved";
2055
- 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);
2056
2424
  batch.status = "executed";
2057
2425
  batch.executionResult = result;
2058
2426
  return {
@@ -2113,7 +2481,7 @@ const bulkUpdaterService = ({ strapi: strapi2 }) => {
2113
2481
  if (batch.status === "executed") {
2114
2482
  throw new Error(`Batch ${batchId} has already been executed.`);
2115
2483
  }
2116
- 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);
2117
2485
  batch.status = "executed";
2118
2486
  batch.executionResult = result;
2119
2487
  return {