@strapi/content-releases 0.0.0-next.836f74517f9a428a4798ed889c3f05057ec6beb1 → 0.0.0-next.8b561475428ed29d8c7ca70ec2e96f051220ebaf

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.
Files changed (31) hide show
  1. package/dist/_chunks/{App-pNsURCL_.js → App-dLXY5ei3.js} +146 -138
  2. package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
  3. package/dist/_chunks/{App-fcvNs2Qb.mjs → App-jrh58sXY.mjs} +150 -142
  4. package/dist/_chunks/App-jrh58sXY.mjs.map +1 -0
  5. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-3tRbmbY3.mjs} +2 -2
  6. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  7. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-bpIYXOfu.js} +2 -2
  8. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  9. package/dist/_chunks/{en-gcJJ5htG.js → en-HrREghh3.js} +11 -3
  10. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  11. package/dist/_chunks/{en-WuuhP6Bn.mjs → en-ltT1TlKQ.mjs} +11 -3
  12. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  13. package/dist/_chunks/{index-gzTuOXiK.js → index-CVO0Rqdm.js} +320 -18
  14. package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
  15. package/dist/_chunks/{index-pxhi8wsT.mjs → index-PiOGBETy.mjs} +325 -23
  16. package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +1 -1
  19. package/dist/server/index.js +290 -158
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +290 -158
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +12 -12
  24. package/dist/_chunks/App-fcvNs2Qb.mjs.map +0 -1
  25. package/dist/_chunks/App-pNsURCL_.js.map +0 -1
  26. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  27. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  28. package/dist/_chunks/en-WuuhP6Bn.mjs.map +0 -1
  29. package/dist/_chunks/en-gcJJ5htG.js.map +0 -1
  30. package/dist/_chunks/index-gzTuOXiK.js.map +0 -1
  31. package/dist/_chunks/index-pxhi8wsT.mjs.map +0 -1
@@ -211,13 +211,16 @@ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: cont
211
211
  if (!oldContentTypes) {
212
212
  return;
213
213
  }
214
+ const i18nPlugin = strapi.plugin("i18n");
215
+ if (!i18nPlugin) {
216
+ return;
217
+ }
214
218
  for (const uid in contentTypes2) {
215
219
  if (!oldContentTypes[uid]) {
216
220
  continue;
217
221
  }
218
222
  const oldContentType = oldContentTypes[uid];
219
223
  const contentType = contentTypes2[uid];
220
- const i18nPlugin = strapi.plugin("i18n");
221
224
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
222
225
  if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
223
226
  await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
@@ -230,13 +233,16 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
230
233
  if (!oldContentTypes) {
231
234
  return;
232
235
  }
236
+ const i18nPlugin = strapi.plugin("i18n");
237
+ if (!i18nPlugin) {
238
+ return;
239
+ }
233
240
  for (const uid in contentTypes2) {
234
241
  if (!oldContentTypes[uid]) {
235
242
  continue;
236
243
  }
237
244
  const oldContentType = oldContentTypes[uid];
238
245
  const contentType = contentTypes2[uid];
239
- const i18nPlugin = strapi.plugin("i18n");
240
246
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
241
247
  const { getDefaultLocale } = i18nPlugin.service("locales");
242
248
  if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
@@ -254,6 +260,11 @@ const register = async ({ strapi: strapi2 }) => {
254
260
  strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
255
261
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
256
262
  }
263
+ if (strapi2.plugin("graphql")) {
264
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
265
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
266
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
267
+ }
257
268
  };
258
269
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
259
270
  const bootstrap = async ({ strapi: strapi2 }) => {
@@ -378,27 +389,23 @@ const bootstrap = async ({ strapi: strapi2 }) => {
378
389
  }
379
390
  }
380
391
  });
381
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
382
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
383
- strapi2.log.error(
384
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
385
- );
386
- throw err;
387
- });
388
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
389
- strapi2.webhookStore.addAllowedEvent(key, value);
390
- });
391
- }
392
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
393
+ strapi2.log.error(
394
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
395
+ );
396
+ throw err;
397
+ });
398
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
399
+ strapi2.webhookStore.addAllowedEvent(key, value);
400
+ });
392
401
  }
393
402
  };
394
403
  const destroy = async ({ strapi: strapi2 }) => {
395
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
396
- const scheduledJobs = getService("scheduling", {
397
- strapi: strapi2
398
- }).getAll();
399
- for (const [, job] of scheduledJobs) {
400
- job.cancel();
401
- }
404
+ const scheduledJobs = getService("scheduling", {
405
+ strapi: strapi2
406
+ }).getAll();
407
+ for (const [, job] of scheduledJobs) {
408
+ job.cancel();
402
409
  }
403
410
  };
404
411
  const schema$1 = {
@@ -523,6 +530,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
523
530
  release: release2
524
531
  });
525
532
  };
533
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
534
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
535
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
536
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
537
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
538
+ try {
539
+ if (actionType === "publish") {
540
+ await entityManagerService.publish(entry, uid);
541
+ } else {
542
+ await entityManagerService.unpublish(entry, uid);
543
+ }
544
+ } catch (error) {
545
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
546
+ ;
547
+ else {
548
+ throw error;
549
+ }
550
+ }
551
+ };
552
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
553
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
554
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
555
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
556
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
557
+ filters: {
558
+ id: {
559
+ $in: entriesToPublishIds
560
+ }
561
+ },
562
+ populate
563
+ });
564
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
565
+ filters: {
566
+ id: {
567
+ $in: entriestoUnpublishIds
568
+ }
569
+ },
570
+ populate
571
+ });
572
+ if (entriesToPublish.length > 0) {
573
+ await entityManagerService.publishMany(entriesToPublish, uid);
574
+ }
575
+ if (entriesToUnpublish.length > 0) {
576
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
577
+ }
578
+ };
579
+ const getFormattedActions = async (releaseId) => {
580
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
581
+ where: {
582
+ release: {
583
+ id: releaseId
584
+ }
585
+ },
586
+ populate: {
587
+ entry: {
588
+ fields: ["id"]
589
+ }
590
+ }
591
+ });
592
+ if (actions.length === 0) {
593
+ throw new errors.ValidationError("No entries to publish");
594
+ }
595
+ const collectionTypeActions = {};
596
+ const singleTypeActions = [];
597
+ for (const action of actions) {
598
+ const contentTypeUid = action.contentType;
599
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
600
+ if (!collectionTypeActions[contentTypeUid]) {
601
+ collectionTypeActions[contentTypeUid] = {
602
+ entriesToPublishIds: [],
603
+ entriesToUnpublishIds: []
604
+ };
605
+ }
606
+ if (action.type === "publish") {
607
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
608
+ } else {
609
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
610
+ }
611
+ } else {
612
+ singleTypeActions.push({
613
+ uid: contentTypeUid,
614
+ action: action.type,
615
+ id: action.entry.id
616
+ });
617
+ }
618
+ }
619
+ return { collectionTypeActions, singleTypeActions };
620
+ };
526
621
  return {
527
622
  async create(releaseData, { user }) {
528
623
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@@ -542,7 +637,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
542
637
  status: "empty"
543
638
  }
544
639
  });
545
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
640
+ if (releaseWithCreatorFields.scheduledAt) {
546
641
  const schedulingService = getService("scheduling", { strapi: strapi2 });
547
642
  await schedulingService.set(release2.id, release2.scheduledAt);
548
643
  }
@@ -566,12 +661,18 @@ const createReleaseService = ({ strapi: strapi2 }) => {
566
661
  }
567
662
  });
568
663
  },
569
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
664
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
665
+ let entries = entriesIds;
666
+ if (!Array.isArray(entriesIds)) {
667
+ entries = [entriesIds];
668
+ }
570
669
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
571
670
  where: {
572
671
  actions: {
573
672
  target_type: contentTypeUid,
574
- target_id: entryId
673
+ target_id: {
674
+ $in: entries
675
+ }
575
676
  },
576
677
  releasedAt: {
577
678
  $null: true
@@ -582,18 +683,25 @@ const createReleaseService = ({ strapi: strapi2 }) => {
582
683
  actions: {
583
684
  where: {
584
685
  target_type: contentTypeUid,
585
- target_id: entryId
686
+ target_id: {
687
+ $in: entries
688
+ }
689
+ },
690
+ populate: {
691
+ entry: {
692
+ select: ["id"]
693
+ }
586
694
  }
587
695
  }
588
696
  }
589
697
  });
590
698
  return releases.map((release2) => {
591
699
  if (release2.actions?.length) {
592
- const [actionForEntry] = release2.actions;
700
+ const actionsForEntry = release2.actions;
593
701
  delete release2.actions;
594
702
  return {
595
703
  ...release2,
596
- action: actionForEntry
704
+ actions: actionsForEntry
597
705
  };
598
706
  }
599
707
  return release2;
@@ -667,13 +775,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
667
775
  // @ts-expect-error see above
668
776
  data: releaseWithCreatorFields
669
777
  });
670
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
671
- const schedulingService = getService("scheduling", { strapi: strapi2 });
672
- if (releaseData.scheduledAt) {
673
- await schedulingService.set(id, releaseData.scheduledAt);
674
- } else if (release2.scheduledAt) {
675
- schedulingService.cancel(id);
676
- }
778
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
779
+ if (releaseData.scheduledAt) {
780
+ await schedulingService.set(id, releaseData.scheduledAt);
781
+ } else if (release2.scheduledAt) {
782
+ schedulingService.cancel(id);
677
783
  }
678
784
  this.updateReleaseStatus(id);
679
785
  strapi2.telemetry.send("didUpdateContentRelease");
@@ -839,7 +945,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
839
945
  });
840
946
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
841
947
  });
842
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
948
+ if (release2.scheduledAt) {
843
949
  const schedulingService = getService("scheduling", { strapi: strapi2 });
844
950
  await schedulingService.cancel(release2.id);
845
951
  }
@@ -847,145 +953,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
847
953
  return release2;
848
954
  },
849
955
  async publish(releaseId) {
850
- try {
851
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
852
- RELEASE_MODEL_UID,
853
- releaseId,
854
- {
855
- populate: {
856
- actions: {
857
- populate: {
858
- entry: {
859
- fields: ["id"]
860
- }
861
- }
862
- }
863
- }
864
- }
865
- );
866
- if (!releaseWithPopulatedActionEntries) {
956
+ const {
957
+ release: release2,
958
+ error
959
+ } = await strapi2.db.transaction(async ({ trx }) => {
960
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
961
+ if (!lockedRelease) {
867
962
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
868
963
  }
869
- if (releaseWithPopulatedActionEntries.releasedAt) {
964
+ if (lockedRelease.releasedAt) {
870
965
  throw new errors.ValidationError("Release already published");
871
966
  }
872
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
873
- throw new errors.ValidationError("No entries to publish");
874
- }
875
- const collectionTypeActions = {};
876
- const singleTypeActions = [];
877
- for (const action of releaseWithPopulatedActionEntries.actions) {
878
- const contentTypeUid = action.contentType;
879
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
880
- if (!collectionTypeActions[contentTypeUid]) {
881
- collectionTypeActions[contentTypeUid] = {
882
- entriestoPublishIds: [],
883
- entriesToUnpublishIds: []
884
- };
885
- }
886
- if (action.type === "publish") {
887
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
888
- } else {
889
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
890
- }
891
- } else {
892
- singleTypeActions.push({
893
- uid: contentTypeUid,
894
- action: action.type,
895
- id: action.entry.id
896
- });
897
- }
967
+ if (lockedRelease.status === "failed") {
968
+ throw new errors.ValidationError("Release failed to publish");
898
969
  }
899
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
900
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
901
- await strapi2.db.transaction(async () => {
902
- for (const { uid, action, id } of singleTypeActions) {
903
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
904
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
905
- try {
906
- if (action === "publish") {
907
- await entityManagerService.publish(entry, uid);
908
- } else {
909
- await entityManagerService.unpublish(entry, uid);
910
- }
911
- } catch (error) {
912
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
913
- } else {
914
- throw error;
915
- }
916
- }
917
- }
918
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
919
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
920
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
921
- const entriesToPublish = await strapi2.entityService.findMany(
922
- contentTypeUid,
923
- {
924
- filters: {
925
- id: {
926
- $in: entriestoPublishIds
927
- }
928
- },
929
- populate
930
- }
931
- );
932
- const entriesToUnpublish = await strapi2.entityService.findMany(
933
- contentTypeUid,
934
- {
935
- filters: {
936
- id: {
937
- $in: entriesToUnpublishIds
938
- }
939
- },
940
- populate
941
- }
942
- );
943
- if (entriesToPublish.length > 0) {
944
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
970
+ try {
971
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
972
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
973
+ releaseId
974
+ );
975
+ await strapi2.db.transaction(async () => {
976
+ for (const { uid, action, id } of singleTypeActions) {
977
+ await publishSingleTypeAction(uid, action, id);
945
978
  }
946
- if (entriesToUnpublish.length > 0) {
947
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
979
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
980
+ const uid = contentTypeUid;
981
+ await publishCollectionTypeAction(
982
+ uid,
983
+ collectionTypeActions[uid].entriesToPublishIds,
984
+ collectionTypeActions[uid].entriesToUnpublishIds
985
+ );
948
986
  }
949
- }
950
- });
951
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
952
- data: {
953
- /*
954
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
955
- */
956
- // @ts-expect-error see above
957
- releasedAt: /* @__PURE__ */ new Date()
958
- },
959
- populate: {
960
- actions: {
961
- // @ts-expect-error is not expecting count but it is working
962
- count: true
987
+ });
988
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
989
+ where: {
990
+ id: releaseId
991
+ },
992
+ data: {
993
+ status: "done",
994
+ releasedAt: /* @__PURE__ */ new Date()
963
995
  }
964
- }
965
- });
966
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
996
+ });
967
997
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
968
998
  isPublished: true,
969
- release: release2
999
+ release: release22
970
1000
  });
971
- }
972
- strapi2.telemetry.send("didPublishContentRelease");
973
- return release2;
974
- } catch (error) {
975
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1001
+ strapi2.telemetry.send("didPublishContentRelease");
1002
+ return { release: release22, error: null };
1003
+ } catch (error2) {
976
1004
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
977
1005
  isPublished: false,
978
- error
1006
+ error: error2
979
1007
  });
980
- }
981
- strapi2.db.query(RELEASE_MODEL_UID).update({
982
- where: { id: releaseId },
983
- data: {
1008
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
984
1009
  status: "failed"
985
- }
986
- });
1010
+ }).transacting(trx).execute();
1011
+ return {
1012
+ release: null,
1013
+ error: error2
1014
+ };
1015
+ }
1016
+ });
1017
+ if (error) {
987
1018
  throw error;
988
1019
  }
1020
+ return release2;
989
1021
  },
990
1022
  async updateAction(actionId, releaseId, update) {
991
1023
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -1072,6 +1104,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1072
1104
  }
1073
1105
  };
1074
1106
  };
1107
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1108
+ constructor(message) {
1109
+ super(message);
1110
+ this.name = "AlreadyOnReleaseError";
1111
+ }
1112
+ }
1075
1113
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1076
1114
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1077
1115
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -1084,7 +1122,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1084
1122
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1085
1123
  );
1086
1124
  if (isEntryInRelease) {
1087
- throw new errors.ValidationError(
1125
+ throw new AlreadyOnReleaseError(
1088
1126
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1089
1127
  );
1090
1128
  }
@@ -1192,7 +1230,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1192
1230
  const services = {
1193
1231
  release: createReleaseService,
1194
1232
  "release-validation": createReleaseValidationService,
1195
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1233
+ scheduling: createSchedulingService
1196
1234
  };
1197
1235
  const RELEASE_SCHEMA = yup.object().shape({
1198
1236
  name: yup.string().trim().required(),
@@ -1279,6 +1317,33 @@ const releaseController = {
1279
1317
  };
1280
1318
  ctx.body = { data };
1281
1319
  },
1320
+ async mapEntriesToReleases(ctx) {
1321
+ const { contentTypeUid, entriesIds } = ctx.query;
1322
+ if (!contentTypeUid || !entriesIds) {
1323
+ throw new errors.ValidationError("Missing required query parameters");
1324
+ }
1325
+ const releaseService = getService("release", { strapi });
1326
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1327
+ contentTypeUid,
1328
+ entriesIds
1329
+ );
1330
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1331
+ (acc, release2) => {
1332
+ release2.actions.forEach((action) => {
1333
+ if (!acc[action.entry.id]) {
1334
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1335
+ } else {
1336
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1337
+ }
1338
+ });
1339
+ return acc;
1340
+ },
1341
+ {}
1342
+ );
1343
+ ctx.body = {
1344
+ data: mappedEntriesInReleases
1345
+ };
1346
+ },
1282
1347
  async create(ctx) {
1283
1348
  const user = ctx.state.user;
1284
1349
  const releaseArgs = ctx.request.body;
@@ -1368,6 +1433,38 @@ const releaseActionController = {
1368
1433
  data: releaseAction2
1369
1434
  };
1370
1435
  },
1436
+ async createMany(ctx) {
1437
+ const releaseId = ctx.params.releaseId;
1438
+ const releaseActionsArgs = ctx.request.body;
1439
+ await Promise.all(
1440
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1441
+ );
1442
+ const releaseService = getService("release", { strapi });
1443
+ const releaseActions = await strapi.db.transaction(async () => {
1444
+ const releaseActions2 = await Promise.all(
1445
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1446
+ try {
1447
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1448
+ return action;
1449
+ } catch (error) {
1450
+ if (error instanceof AlreadyOnReleaseError) {
1451
+ return null;
1452
+ }
1453
+ throw error;
1454
+ }
1455
+ })
1456
+ );
1457
+ return releaseActions2;
1458
+ });
1459
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1460
+ ctx.body = {
1461
+ data: newReleaseActions,
1462
+ meta: {
1463
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1464
+ totalEntries: releaseActions.length
1465
+ }
1466
+ };
1467
+ },
1371
1468
  async findMany(ctx) {
1372
1469
  const releaseId = ctx.params.releaseId;
1373
1470
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1436,6 +1533,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1436
1533
  const release = {
1437
1534
  type: "admin",
1438
1535
  routes: [
1536
+ {
1537
+ method: "GET",
1538
+ path: "/mapEntriesToReleases",
1539
+ handler: "release.mapEntriesToReleases",
1540
+ config: {
1541
+ policies: [
1542
+ "admin::isAuthenticatedAdmin",
1543
+ {
1544
+ name: "admin::hasPermissions",
1545
+ config: {
1546
+ actions: ["plugin::content-releases.read"]
1547
+ }
1548
+ }
1549
+ ]
1550
+ }
1551
+ },
1439
1552
  {
1440
1553
  method: "POST",
1441
1554
  path: "/",
@@ -1553,6 +1666,22 @@ const releaseAction = {
1553
1666
  ]
1554
1667
  }
1555
1668
  },
1669
+ {
1670
+ method: "POST",
1671
+ path: "/:releaseId/actions/bulk",
1672
+ handler: "release-action.createMany",
1673
+ config: {
1674
+ policies: [
1675
+ "admin::isAuthenticatedAdmin",
1676
+ {
1677
+ name: "admin::hasPermissions",
1678
+ config: {
1679
+ actions: ["plugin::content-releases.create-action"]
1680
+ }
1681
+ }
1682
+ ]
1683
+ }
1684
+ },
1556
1685
  {
1557
1686
  method: "GET",
1558
1687
  path: "/:releaseId/actions",
@@ -1621,6 +1750,9 @@ const getPlugin = () => {
1621
1750
  };
1622
1751
  }
1623
1752
  return {
1753
+ // Always return register, it handles its own feature check
1754
+ register,
1755
+ // Always return contentTypes to avoid losing data when the feature is disabled
1624
1756
  contentTypes
1625
1757
  };
1626
1758
  };