@strapi/content-releases 0.0.0-next.d10040847b91742ccb8083938399b63ffa289c7a → 0.0.0-next.d54a672641d83234230d3d80d92aaf87f60f8c75

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 (32) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/{App-lnXbSPgp.js → App-dLXY5ei3.js} +673 -630
  3. package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
  4. package/dist/_chunks/{App-g3vtS2Wa.mjs → App-jrh58sXY.mjs} +686 -643
  5. package/dist/_chunks/App-jrh58sXY.mjs.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-3tRbmbY3.mjs} +2 -2
  7. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-bpIYXOfu.js} +2 -2
  9. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  10. package/dist/_chunks/{en-gcJJ5htG.js → en-HrREghh3.js} +11 -3
  11. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  12. package/dist/_chunks/{en-WuuhP6Bn.mjs → en-ltT1TlKQ.mjs} +11 -3
  13. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  14. package/dist/_chunks/{index-ItlgrLcr.js → index-CVO0Rqdm.js} +320 -18
  15. package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
  16. package/dist/_chunks/{index-uGex_IIQ.mjs → index-PiOGBETy.mjs} +335 -33
  17. package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.mjs +1 -1
  20. package/dist/server/index.js +331 -159
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +331 -159
  23. package/dist/server/index.mjs.map +1 -1
  24. package/package.json +13 -13
  25. package/dist/_chunks/App-g3vtS2Wa.mjs.map +0 -1
  26. package/dist/_chunks/App-lnXbSPgp.js.map +0 -1
  27. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  28. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  29. package/dist/_chunks/en-WuuhP6Bn.mjs.map +0 -1
  30. package/dist/_chunks/en-gcJJ5htG.js.map +0 -1
  31. package/dist/_chunks/index-ItlgrLcr.js.map +0 -1
  32. package/dist/_chunks/index-uGex_IIQ.mjs.map +0 -1
@@ -179,7 +179,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
179
179
  }
180
180
  });
181
181
  await mapAsync(actions, async (action) => {
182
- if (action.entry) {
182
+ if (action.entry && action.release) {
183
183
  const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
184
  strapi
185
185
  });
@@ -207,12 +207,63 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
207
207
  });
208
208
  }
209
209
  }
210
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
211
+ if (!oldContentTypes) {
212
+ return;
213
+ }
214
+ const i18nPlugin = strapi.plugin("i18n");
215
+ if (!i18nPlugin) {
216
+ return;
217
+ }
218
+ for (const uid in contentTypes2) {
219
+ if (!oldContentTypes[uid]) {
220
+ continue;
221
+ }
222
+ const oldContentType = oldContentTypes[uid];
223
+ const contentType = contentTypes2[uid];
224
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
225
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
226
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
227
+ locale: null
228
+ }).where({ contentType: uid }).execute();
229
+ }
230
+ }
231
+ }
232
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
233
+ if (!oldContentTypes) {
234
+ return;
235
+ }
236
+ const i18nPlugin = strapi.plugin("i18n");
237
+ if (!i18nPlugin) {
238
+ return;
239
+ }
240
+ for (const uid in contentTypes2) {
241
+ if (!oldContentTypes[uid]) {
242
+ continue;
243
+ }
244
+ const oldContentType = oldContentTypes[uid];
245
+ const contentType = contentTypes2[uid];
246
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
247
+ const { getDefaultLocale } = i18nPlugin.service("locales");
248
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
249
+ const defaultLocale = await getDefaultLocale();
250
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
251
+ locale: defaultLocale
252
+ }).where({ contentType: uid }).execute();
253
+ }
254
+ }
255
+ }
210
256
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
211
257
  const register = async ({ strapi: strapi2 }) => {
212
258
  if (features$2.isEnabled("cms-content-releases")) {
213
259
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
214
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
215
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
260
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
261
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
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();
216
267
  }
217
268
  };
218
269
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
@@ -338,27 +389,23 @@ const bootstrap = async ({ strapi: strapi2 }) => {
338
389
  }
339
390
  }
340
391
  });
341
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
342
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
343
- strapi2.log.error(
344
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
345
- );
346
- throw err;
347
- });
348
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
349
- strapi2.webhookStore.addAllowedEvent(key, value);
350
- });
351
- }
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
+ });
352
401
  }
353
402
  };
354
403
  const destroy = async ({ strapi: strapi2 }) => {
355
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
356
- const scheduledJobs = getService("scheduling", {
357
- strapi: strapi2
358
- }).getAll();
359
- for (const [, job] of scheduledJobs) {
360
- job.cancel();
361
- }
404
+ const scheduledJobs = getService("scheduling", {
405
+ strapi: strapi2
406
+ }).getAll();
407
+ for (const [, job] of scheduledJobs) {
408
+ job.cancel();
362
409
  }
363
410
  };
364
411
  const schema$1 = {
@@ -483,6 +530,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
483
530
  release: release2
484
531
  });
485
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
+ };
486
621
  return {
487
622
  async create(releaseData, { user }) {
488
623
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@@ -502,7 +637,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
502
637
  status: "empty"
503
638
  }
504
639
  });
505
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
640
+ if (releaseWithCreatorFields.scheduledAt) {
506
641
  const schedulingService = getService("scheduling", { strapi: strapi2 });
507
642
  await schedulingService.set(release2.id, release2.scheduledAt);
508
643
  }
@@ -526,12 +661,18 @@ const createReleaseService = ({ strapi: strapi2 }) => {
526
661
  }
527
662
  });
528
663
  },
529
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
664
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
665
+ let entries = entriesIds;
666
+ if (!Array.isArray(entriesIds)) {
667
+ entries = [entriesIds];
668
+ }
530
669
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
531
670
  where: {
532
671
  actions: {
533
672
  target_type: contentTypeUid,
534
- target_id: entryId
673
+ target_id: {
674
+ $in: entries
675
+ }
535
676
  },
536
677
  releasedAt: {
537
678
  $null: true
@@ -542,18 +683,25 @@ const createReleaseService = ({ strapi: strapi2 }) => {
542
683
  actions: {
543
684
  where: {
544
685
  target_type: contentTypeUid,
545
- target_id: entryId
686
+ target_id: {
687
+ $in: entries
688
+ }
689
+ },
690
+ populate: {
691
+ entry: {
692
+ select: ["id"]
693
+ }
546
694
  }
547
695
  }
548
696
  }
549
697
  });
550
698
  return releases.map((release2) => {
551
699
  if (release2.actions?.length) {
552
- const [actionForEntry] = release2.actions;
700
+ const actionsForEntry = release2.actions;
553
701
  delete release2.actions;
554
702
  return {
555
703
  ...release2,
556
- action: actionForEntry
704
+ actions: actionsForEntry
557
705
  };
558
706
  }
559
707
  return release2;
@@ -627,13 +775,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
627
775
  // @ts-expect-error see above
628
776
  data: releaseWithCreatorFields
629
777
  });
630
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
631
- const schedulingService = getService("scheduling", { strapi: strapi2 });
632
- if (releaseData.scheduledAt) {
633
- await schedulingService.set(id, releaseData.scheduledAt);
634
- } else if (release2.scheduledAt) {
635
- schedulingService.cancel(id);
636
- }
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);
637
783
  }
638
784
  this.updateReleaseStatus(id);
639
785
  strapi2.telemetry.send("didUpdateContentRelease");
@@ -799,7 +945,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
799
945
  });
800
946
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
801
947
  });
802
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
948
+ if (release2.scheduledAt) {
803
949
  const schedulingService = getService("scheduling", { strapi: strapi2 });
804
950
  await schedulingService.cancel(release2.id);
805
951
  }
@@ -807,145 +953,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
807
953
  return release2;
808
954
  },
809
955
  async publish(releaseId) {
810
- try {
811
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
812
- RELEASE_MODEL_UID,
813
- releaseId,
814
- {
815
- populate: {
816
- actions: {
817
- populate: {
818
- entry: {
819
- fields: ["id"]
820
- }
821
- }
822
- }
823
- }
824
- }
825
- );
826
- 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) {
827
962
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
828
963
  }
829
- if (releaseWithPopulatedActionEntries.releasedAt) {
964
+ if (lockedRelease.releasedAt) {
830
965
  throw new errors.ValidationError("Release already published");
831
966
  }
832
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
833
- throw new errors.ValidationError("No entries to publish");
967
+ if (lockedRelease.status === "failed") {
968
+ throw new errors.ValidationError("Release failed to publish");
834
969
  }
835
- const collectionTypeActions = {};
836
- const singleTypeActions = [];
837
- for (const action of releaseWithPopulatedActionEntries.actions) {
838
- const contentTypeUid = action.contentType;
839
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
840
- if (!collectionTypeActions[contentTypeUid]) {
841
- collectionTypeActions[contentTypeUid] = {
842
- entriestoPublishIds: [],
843
- entriesToUnpublishIds: []
844
- };
845
- }
846
- if (action.type === "publish") {
847
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
848
- } else {
849
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
850
- }
851
- } else {
852
- singleTypeActions.push({
853
- uid: contentTypeUid,
854
- action: action.type,
855
- id: action.entry.id
856
- });
857
- }
858
- }
859
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
860
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
861
- await strapi2.db.transaction(async () => {
862
- for (const { uid, action, id } of singleTypeActions) {
863
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
864
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
865
- try {
866
- if (action === "publish") {
867
- await entityManagerService.publish(entry, uid);
868
- } else {
869
- await entityManagerService.unpublish(entry, uid);
870
- }
871
- } catch (error) {
872
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
873
- } else {
874
- throw error;
875
- }
876
- }
877
- }
878
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
879
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
880
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
881
- const entriesToPublish = await strapi2.entityService.findMany(
882
- contentTypeUid,
883
- {
884
- filters: {
885
- id: {
886
- $in: entriestoPublishIds
887
- }
888
- },
889
- populate
890
- }
891
- );
892
- const entriesToUnpublish = await strapi2.entityService.findMany(
893
- contentTypeUid,
894
- {
895
- filters: {
896
- id: {
897
- $in: entriesToUnpublishIds
898
- }
899
- },
900
- populate
901
- }
902
- );
903
- if (entriesToPublish.length > 0) {
904
- 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);
905
978
  }
906
- if (entriesToUnpublish.length > 0) {
907
- 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
+ );
908
986
  }
909
- }
910
- });
911
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
912
- data: {
913
- /*
914
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
915
- */
916
- // @ts-expect-error see above
917
- releasedAt: /* @__PURE__ */ new Date()
918
- },
919
- populate: {
920
- actions: {
921
- // @ts-expect-error is not expecting count but it is working
922
- 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()
923
995
  }
924
- }
925
- });
926
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
996
+ });
927
997
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
928
998
  isPublished: true,
929
- release: release2
999
+ release: release22
930
1000
  });
931
- }
932
- strapi2.telemetry.send("didPublishContentRelease");
933
- return release2;
934
- } catch (error) {
935
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1001
+ strapi2.telemetry.send("didPublishContentRelease");
1002
+ return { release: release22, error: null };
1003
+ } catch (error2) {
936
1004
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
937
1005
  isPublished: false,
938
- error
1006
+ error: error2
939
1007
  });
940
- }
941
- strapi2.db.query(RELEASE_MODEL_UID).update({
942
- where: { id: releaseId },
943
- data: {
1008
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
944
1009
  status: "failed"
945
- }
946
- });
1010
+ }).transacting(trx).execute();
1011
+ return {
1012
+ release: null,
1013
+ error: error2
1014
+ };
1015
+ }
1016
+ });
1017
+ if (error) {
947
1018
  throw error;
948
1019
  }
1020
+ return release2;
949
1021
  },
950
1022
  async updateAction(actionId, releaseId, update) {
951
1023
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -1032,6 +1104,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1032
1104
  }
1033
1105
  };
1034
1106
  };
1107
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1108
+ constructor(message) {
1109
+ super(message);
1110
+ this.name = "AlreadyOnReleaseError";
1111
+ }
1112
+ }
1035
1113
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1036
1114
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1037
1115
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -1044,7 +1122,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1044
1122
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1045
1123
  );
1046
1124
  if (isEntryInRelease) {
1047
- throw new errors.ValidationError(
1125
+ throw new AlreadyOnReleaseError(
1048
1126
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1049
1127
  );
1050
1128
  }
@@ -1152,7 +1230,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1152
1230
  const services = {
1153
1231
  release: createReleaseService,
1154
1232
  "release-validation": createReleaseValidationService,
1155
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1233
+ scheduling: createSchedulingService
1156
1234
  };
1157
1235
  const RELEASE_SCHEMA = yup.object().shape({
1158
1236
  name: yup.string().trim().required(),
@@ -1239,6 +1317,33 @@ const releaseController = {
1239
1317
  };
1240
1318
  ctx.body = { data };
1241
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
+ },
1242
1347
  async create(ctx) {
1243
1348
  const user = ctx.state.user;
1244
1349
  const releaseArgs = ctx.request.body;
@@ -1328,6 +1433,38 @@ const releaseActionController = {
1328
1433
  data: releaseAction2
1329
1434
  };
1330
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
+ },
1331
1468
  async findMany(ctx) {
1332
1469
  const releaseId = ctx.params.releaseId;
1333
1470
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1396,6 +1533,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1396
1533
  const release = {
1397
1534
  type: "admin",
1398
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
+ },
1399
1552
  {
1400
1553
  method: "POST",
1401
1554
  path: "/",
@@ -1513,6 +1666,22 @@ const releaseAction = {
1513
1666
  ]
1514
1667
  }
1515
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
+ },
1516
1685
  {
1517
1686
  method: "GET",
1518
1687
  path: "/:releaseId/actions",
@@ -1581,6 +1750,9 @@ const getPlugin = () => {
1581
1750
  };
1582
1751
  }
1583
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
1584
1756
  contentTypes
1585
1757
  };
1586
1758
  };