@strapi/content-releases 0.0.0-next.56199ab7a5f3320e0debcbe4a24fe0b8cd599e21 → 0.0.0-next.677a639124e715b4bd6bb862d1ef6536358bed8b

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.
@@ -1,6 +1,8 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1, mapAsync } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import { difference, keys } from "lodash";
2
3
  import _ from "lodash/fp";
3
4
  import EE from "@strapi/strapi/dist/utils/ee";
5
+ import { scheduleJob } from "node-schedule";
4
6
  import * as yup from "yup";
5
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
6
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -48,40 +50,50 @@ const ACTIONS = [
48
50
  pluginName: "content-releases"
49
51
  }
50
52
  ];
51
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
52
- return strapi2.plugin("content-releases").service(name);
53
- };
53
+ async function deleteActionsOnDisableDraftAndPublish({
54
+ oldContentTypes,
55
+ contentTypes: contentTypes2
56
+ }) {
57
+ if (!oldContentTypes) {
58
+ return;
59
+ }
60
+ for (const uid in contentTypes2) {
61
+ if (!oldContentTypes[uid]) {
62
+ continue;
63
+ }
64
+ const oldContentType = oldContentTypes[uid];
65
+ const contentType = contentTypes2[uid];
66
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
67
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
68
+ }
69
+ }
70
+ }
71
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
72
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
73
+ if (deletedContentTypes.length) {
74
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
75
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
76
+ });
77
+ }
78
+ }
54
79
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
55
80
  const register = async ({ strapi: strapi2 }) => {
56
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
81
+ if (features$2.isEnabled("cms-content-releases")) {
57
82
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
58
- const releaseActionService = getService("release-action", { strapi: strapi2 });
59
- const eventManager = getService("event-manager", { strapi: strapi2 });
60
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
61
- "content-type.update",
62
- async ({ contentType }) => {
63
- if (contentType.schema.options.draftAndPublish === false) {
64
- await releaseActionService.deleteManyForContentType(contentType.uid);
65
- }
66
- }
67
- );
68
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
69
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
70
- "content-type.delete",
71
- async ({ contentType }) => {
72
- await releaseActionService.deleteManyForContentType(contentType.uid);
73
- }
74
- );
75
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
83
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
84
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
76
85
  }
77
86
  };
87
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
88
+ return strapi2.plugin("content-releases").service(name);
89
+ };
78
90
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
79
91
  const bootstrap = async ({ strapi: strapi2 }) => {
80
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
92
+ if (features$1.isEnabled("cms-content-releases")) {
81
93
  strapi2.db.lifecycles.subscribe({
82
94
  afterDelete(event) {
83
95
  const { model, result } = event;
84
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
96
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
85
97
  const { id } = result;
86
98
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
87
99
  where: {
@@ -97,7 +109,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
97
109
  */
98
110
  async beforeDeleteMany(event) {
99
111
  const { model, params } = event;
100
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
112
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
101
113
  const { where } = params;
102
114
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
103
115
  event.state.entriesToDelete = entriesToDelete;
@@ -122,6 +134,24 @@ const bootstrap = async ({ strapi: strapi2 }) => {
122
134
  }
123
135
  }
124
136
  });
137
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
138
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
139
+ strapi2.log.error(
140
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
141
+ );
142
+ throw err;
143
+ });
144
+ }
145
+ }
146
+ };
147
+ const destroy = async ({ strapi: strapi2 }) => {
148
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
149
+ const scheduledJobs = getService("scheduling", {
150
+ strapi: strapi2
151
+ }).getAll();
152
+ for (const [, job] of scheduledJobs) {
153
+ job.cancel();
154
+ }
125
155
  }
126
156
  };
127
157
  const schema$1 = {
@@ -150,6 +180,9 @@ const schema$1 = {
150
180
  releasedAt: {
151
181
  type: "datetime"
152
182
  },
183
+ scheduledAt: {
184
+ type: "datetime"
185
+ },
153
186
  actions: {
154
187
  type: "relation",
155
188
  relation: "oneToMany",
@@ -212,15 +245,6 @@ const contentTypes = {
212
245
  release: release$1,
213
246
  "release-action": releaseAction$1
214
247
  };
215
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
216
- async deleteManyForContentType(contentTypeUid) {
217
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
218
- where: {
219
- target_type: contentTypeUid
220
- }
221
- });
222
- }
223
- });
224
248
  const getGroupName = (queryValue) => {
225
249
  switch (queryValue) {
226
250
  case "contentType":
@@ -236,10 +260,24 @@ const getGroupName = (queryValue) => {
236
260
  const createReleaseService = ({ strapi: strapi2 }) => ({
237
261
  async create(releaseData, { user }) {
238
262
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
240
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
263
+ const {
264
+ validatePendingReleasesLimit,
265
+ validateUniqueNameForPendingRelease,
266
+ validateScheduledAtIsLaterThanNow
267
+ } = getService("release-validation", { strapi: strapi2 });
268
+ await Promise.all([
269
+ validatePendingReleasesLimit(),
270
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
271
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
272
+ ]);
273
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
241
274
  data: releaseWithCreatorFields
242
275
  });
276
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
277
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
278
+ await schedulingService.set(release2.id, release2.scheduledAt);
279
+ }
280
+ return release2;
243
281
  },
244
282
  async findOne(id, query = {}) {
245
283
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -258,51 +296,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
258
296
  }
259
297
  });
260
298
  },
261
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
262
- hasEntryAttached
263
- } = {
264
- hasEntryAttached: false
265
- }) {
266
- const whereActions = hasEntryAttached ? {
267
- // Find all Releases where the content type entry is present
268
- actions: {
269
- target_type: contentTypeUid,
270
- target_id: entryId
271
- }
272
- } : {
273
- // Find all Releases where the content type entry is not present
274
- $or: [
275
- {
276
- $not: {
277
- actions: {
278
- target_type: contentTypeUid,
279
- target_id: entryId
280
- }
281
- }
299
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
300
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
301
+ where: {
302
+ actions: {
303
+ target_type: contentTypeUid,
304
+ target_id: entryId
282
305
  },
283
- {
284
- actions: null
306
+ releasedAt: {
307
+ $null: true
285
308
  }
286
- ]
287
- };
288
- const populateAttachedAction = hasEntryAttached ? {
289
- // Filter the action to get only the content type entry
290
- actions: {
291
- where: {
309
+ },
310
+ populate: {
311
+ // Filter the action to get only the content type entry
312
+ actions: {
313
+ where: {
314
+ target_type: contentTypeUid,
315
+ target_id: entryId
316
+ }
317
+ }
318
+ }
319
+ });
320
+ return releases.map((release2) => {
321
+ if (release2.actions?.length) {
322
+ const [actionForEntry] = release2.actions;
323
+ delete release2.actions;
324
+ return {
325
+ ...release2,
326
+ action: actionForEntry
327
+ };
328
+ }
329
+ return release2;
330
+ });
331
+ },
332
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
333
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
334
+ where: {
335
+ releasedAt: {
336
+ $null: true
337
+ },
338
+ actions: {
292
339
  target_type: contentTypeUid,
293
340
  target_id: entryId
294
341
  }
295
342
  }
296
- } : {};
343
+ });
297
344
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
298
345
  where: {
299
- ...whereActions,
346
+ $or: [
347
+ {
348
+ id: {
349
+ $notIn: releasesRelated.map((release2) => release2.id)
350
+ }
351
+ },
352
+ {
353
+ actions: null
354
+ }
355
+ ],
300
356
  releasedAt: {
301
357
  $null: true
302
358
  }
303
- },
304
- populate: {
305
- ...populateAttachedAction
306
359
  }
307
360
  });
308
361
  return releases.map((release2) => {
@@ -319,6 +372,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
319
372
  },
320
373
  async update(id, releaseData, { user }) {
321
374
  const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
375
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
376
+ "release-validation",
377
+ { strapi: strapi2 }
378
+ );
379
+ await Promise.all([
380
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
381
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
382
+ ]);
322
383
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
323
384
  if (!release2) {
324
385
  throw new errors.NotFoundError(`No release found for id ${id}`);
@@ -334,6 +395,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
334
395
  // @ts-expect-error see above
335
396
  data: releaseWithCreatorFields
336
397
  });
398
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
399
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
400
+ if (releaseData.scheduledAt) {
401
+ await schedulingService.set(id, releaseData.scheduledAt);
402
+ } else if (release2.scheduledAt) {
403
+ schedulingService.cancel(id);
404
+ }
405
+ }
337
406
  return updatedRelease;
338
407
  },
339
408
  async createAction(releaseId, action) {
@@ -501,7 +570,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
501
570
  populate: {
502
571
  actions: {
503
572
  populate: {
504
- entry: true
573
+ entry: {
574
+ fields: ["id"]
575
+ }
505
576
  }
506
577
  }
507
578
  }
@@ -516,30 +587,80 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
516
587
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
517
588
  throw new errors.ValidationError("No entries to publish");
518
589
  }
519
- const actions = {};
590
+ const collectionTypeActions = {};
591
+ const singleTypeActions = [];
520
592
  for (const action of releaseWithPopulatedActionEntries.actions) {
521
593
  const contentTypeUid = action.contentType;
522
- if (!actions[contentTypeUid]) {
523
- actions[contentTypeUid] = {
524
- publish: [],
525
- unpublish: []
526
- };
527
- }
528
- if (action.type === "publish") {
529
- actions[contentTypeUid].publish.push(action.entry);
594
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
595
+ if (!collectionTypeActions[contentTypeUid]) {
596
+ collectionTypeActions[contentTypeUid] = {
597
+ entriestoPublishIds: [],
598
+ entriesToUnpublishIds: []
599
+ };
600
+ }
601
+ if (action.type === "publish") {
602
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
603
+ } else {
604
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
605
+ }
530
606
  } else {
531
- actions[contentTypeUid].unpublish.push(action.entry);
607
+ singleTypeActions.push({
608
+ uid: contentTypeUid,
609
+ action: action.type,
610
+ id: action.entry.id
611
+ });
532
612
  }
533
613
  }
534
614
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
615
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
535
616
  await strapi2.db.transaction(async () => {
536
- for (const contentTypeUid of Object.keys(actions)) {
537
- const { publish, unpublish } = actions[contentTypeUid];
538
- if (publish.length > 0) {
539
- await entityManagerService.publishMany(publish, contentTypeUid);
617
+ for (const { uid, action, id } of singleTypeActions) {
618
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
619
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
620
+ try {
621
+ if (action === "publish") {
622
+ await entityManagerService.publish(entry, uid);
623
+ } else {
624
+ await entityManagerService.unpublish(entry, uid);
625
+ }
626
+ } catch (error) {
627
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
628
+ ;
629
+ else {
630
+ throw error;
631
+ }
540
632
  }
541
- if (unpublish.length > 0) {
542
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
633
+ }
634
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
635
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
636
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
637
+ const entriesToPublish = await strapi2.entityService.findMany(
638
+ contentTypeUid,
639
+ {
640
+ filters: {
641
+ id: {
642
+ $in: entriestoPublishIds
643
+ }
644
+ },
645
+ populate
646
+ }
647
+ );
648
+ const entriesToUnpublish = await strapi2.entityService.findMany(
649
+ contentTypeUid,
650
+ {
651
+ filters: {
652
+ id: {
653
+ $in: entriesToUnpublishIds
654
+ }
655
+ },
656
+ populate
657
+ }
658
+ );
659
+ if (entriesToPublish.length > 0) {
660
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
661
+ }
662
+ if (entriesToUnpublish.length > 0) {
663
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
543
664
  }
544
665
  }
545
666
  });
@@ -637,34 +758,89 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
637
758
  if (pendingReleasesCount >= maximumPendingReleases) {
638
759
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
639
760
  }
761
+ },
762
+ async validateUniqueNameForPendingRelease(name, id) {
763
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
764
+ filters: {
765
+ releasedAt: {
766
+ $null: true
767
+ },
768
+ name,
769
+ ...id && { id: { $ne: id } }
770
+ }
771
+ });
772
+ const isNameUnique = pendingReleases.length === 0;
773
+ if (!isNameUnique) {
774
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
775
+ }
776
+ },
777
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
778
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
779
+ throw new errors.ValidationError("Scheduled at must be later than now");
780
+ }
640
781
  }
641
782
  });
642
- const createEventManagerService = () => {
643
- const state = {
644
- destroyListenerCallbacks: []
645
- };
783
+ const createSchedulingService = ({ strapi: strapi2 }) => {
784
+ const scheduledJobs = /* @__PURE__ */ new Map();
646
785
  return {
647
- addDestroyListenerCallback(destroyListenerCallback) {
648
- state.destroyListenerCallbacks.push(destroyListenerCallback);
786
+ async set(releaseId, scheduleDate) {
787
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
788
+ if (!release2) {
789
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
790
+ }
791
+ const job = scheduleJob(scheduleDate, async () => {
792
+ try {
793
+ await getService("release").publish(releaseId);
794
+ } catch (error) {
795
+ }
796
+ this.cancel(releaseId);
797
+ });
798
+ if (scheduledJobs.has(releaseId)) {
799
+ this.cancel(releaseId);
800
+ }
801
+ scheduledJobs.set(releaseId, job);
802
+ return scheduledJobs;
649
803
  },
650
- destroyAllListeners() {
651
- if (!state.destroyListenerCallbacks.length) {
652
- return;
804
+ cancel(releaseId) {
805
+ if (scheduledJobs.has(releaseId)) {
806
+ scheduledJobs.get(releaseId).cancel();
807
+ scheduledJobs.delete(releaseId);
653
808
  }
654
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
655
- destroyListenerCallback();
809
+ return scheduledJobs;
810
+ },
811
+ getAll() {
812
+ return scheduledJobs;
813
+ },
814
+ /**
815
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
816
+ * This is useful in case the server was restarted and the scheduled jobs were lost
817
+ * This also could be used to sync different Strapi instances in case of a cluster
818
+ */
819
+ async syncFromDatabase() {
820
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
821
+ where: {
822
+ scheduledAt: {
823
+ $gte: /* @__PURE__ */ new Date()
824
+ },
825
+ releasedAt: null
826
+ }
656
827
  });
828
+ for (const release2 of releases) {
829
+ this.set(release2.id, release2.scheduledAt);
830
+ }
831
+ return scheduledJobs;
657
832
  }
658
833
  };
659
834
  };
660
835
  const services = {
661
836
  release: createReleaseService,
662
- "release-action": createReleaseActionService,
663
837
  "release-validation": createReleaseValidationService,
664
- "event-manager": createEventManagerService
838
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
665
839
  };
666
840
  const RELEASE_SCHEMA = yup.object().shape({
667
- name: yup.string().trim().required()
841
+ name: yup.string().trim().required(),
842
+ // scheduledAt is a date, but we always receive strings from the client
843
+ scheduledAt: yup.string().nullable()
668
844
  }).required().noUnknown();
669
845
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
670
846
  const releaseController = {
@@ -681,9 +857,7 @@ const releaseController = {
681
857
  const contentTypeUid = query.contentTypeUid;
682
858
  const entryId = query.entryId;
683
859
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
684
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
685
- hasEntryAttached
686
- });
860
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
687
861
  ctx.body = { data };
688
862
  } else {
689
863
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -706,19 +880,18 @@ const releaseController = {
706
880
  const id = ctx.params.id;
707
881
  const releaseService = getService("release", { strapi });
708
882
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
709
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
710
- ability: ctx.state.userAbility,
711
- model: RELEASE_MODEL_UID
712
- });
713
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
883
+ if (!release2) {
884
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
885
+ }
714
886
  const count = await releaseService.countActions({
715
887
  filters: {
716
888
  release: id
717
889
  }
718
890
  });
719
- if (!release2) {
720
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
721
- }
891
+ const sanitizedRelease = {
892
+ ...release2,
893
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
894
+ };
722
895
  const data = {
723
896
  ...sanitizedRelease,
724
897
  actions: {
@@ -771,8 +944,27 @@ const releaseController = {
771
944
  const id = ctx.params.id;
772
945
  const releaseService = getService("release", { strapi });
773
946
  const release2 = await releaseService.publish(id, { user });
947
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
948
+ releaseService.countActions({
949
+ filters: {
950
+ release: id,
951
+ type: "publish"
952
+ }
953
+ }),
954
+ releaseService.countActions({
955
+ filters: {
956
+ release: id,
957
+ type: "unpublish"
958
+ }
959
+ })
960
+ ]);
774
961
  ctx.body = {
775
- data: release2
962
+ data: release2,
963
+ meta: {
964
+ totalEntries: countPublishActions + countUnpublishActions,
965
+ totalPublishedEntries: countPublishActions,
966
+ totalUnpublishedEntries: countUnpublishActions
967
+ }
776
968
  };
777
969
  }
778
970
  };
@@ -1040,19 +1232,15 @@ const routes = {
1040
1232
  };
1041
1233
  const { features } = require("@strapi/strapi/dist/utils/ee");
1042
1234
  const getPlugin = () => {
1043
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1235
+ if (features.isEnabled("cms-content-releases")) {
1044
1236
  return {
1045
1237
  register,
1046
1238
  bootstrap,
1239
+ destroy,
1047
1240
  contentTypes,
1048
1241
  services,
1049
1242
  controllers,
1050
- routes,
1051
- destroy() {
1052
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1053
- getService("event-manager").destroyAllListeners();
1054
- }
1055
- }
1243
+ routes
1056
1244
  };
1057
1245
  }
1058
1246
  return {