@strapi/content-releases 0.0.0-experimental.f7b9b47085e387e97f990d8695971b51d7f7149a → 0.0.0-experimental.fb8e9fec2e10d6b55e3aee59c4eb76bb7a11432a

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,4 +1,7 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1 } 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";
3
+ import _ from "lodash/fp";
4
+ import EE from "@strapi/strapi/dist/utils/ee";
2
5
  import * as yup from "yup";
3
6
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
4
7
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -46,10 +49,87 @@ const ACTIONS = [
46
49
  pluginName: "content-releases"
47
50
  }
48
51
  ];
49
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
52
+ async function deleteActionsOnDisableDraftAndPublish({
53
+ oldContentTypes,
54
+ contentTypes: contentTypes2
55
+ }) {
56
+ if (!oldContentTypes) {
57
+ return;
58
+ }
59
+ for (const uid in contentTypes2) {
60
+ if (!oldContentTypes[uid]) {
61
+ continue;
62
+ }
63
+ const oldContentType = oldContentTypes[uid];
64
+ const contentType = contentTypes2[uid];
65
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
66
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
67
+ }
68
+ }
69
+ }
70
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
71
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
72
+ if (deletedContentTypes.length) {
73
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
74
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
75
+ });
76
+ }
77
+ }
78
+ const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
50
79
  const register = async ({ strapi: strapi2 }) => {
51
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
80
+ if (features$2.isEnabled("cms-content-releases")) {
52
81
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
82
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
83
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
84
+ }
85
+ };
86
+ const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
87
+ const bootstrap = async ({ strapi: strapi2 }) => {
88
+ if (features$1.isEnabled("cms-content-releases")) {
89
+ strapi2.db.lifecycles.subscribe({
90
+ afterDelete(event) {
91
+ const { model, result } = event;
92
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
93
+ const { id } = result;
94
+ strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
95
+ where: {
96
+ target_type: model.uid,
97
+ target_id: id
98
+ }
99
+ });
100
+ }
101
+ },
102
+ /**
103
+ * deleteMany hook doesn't return the deleted entries ids
104
+ * so we need to fetch them before deleting the entries to save the ids on our state
105
+ */
106
+ async beforeDeleteMany(event) {
107
+ const { model, params } = event;
108
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
109
+ const { where } = params;
110
+ const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
111
+ event.state.entriesToDelete = entriesToDelete;
112
+ }
113
+ },
114
+ /**
115
+ * We delete the release actions related to deleted entries
116
+ * We make this only after deleteMany is succesfully executed to avoid errors
117
+ */
118
+ async afterDeleteMany(event) {
119
+ const { model, state } = event;
120
+ const entriesToDelete = state.entriesToDelete;
121
+ if (entriesToDelete) {
122
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
123
+ where: {
124
+ target_type: model.uid,
125
+ target_id: {
126
+ $in: entriesToDelete.map((entry) => entry.id)
127
+ }
128
+ }
129
+ });
130
+ }
131
+ }
132
+ });
53
133
  }
54
134
  };
55
135
  const schema$1 = {
@@ -122,6 +202,9 @@ const schema = {
122
202
  type: "string",
123
203
  required: true
124
204
  },
205
+ locale: {
206
+ type: "string"
207
+ },
125
208
  release: {
126
209
  type: "relation",
127
210
  relation: "manyToOne",
@@ -140,9 +223,29 @@ const contentTypes = {
140
223
  const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
141
224
  return strapi2.plugin("content-releases").service(name);
142
225
  };
226
+ const getGroupName = (queryValue) => {
227
+ switch (queryValue) {
228
+ case "contentType":
229
+ return "contentType.displayName";
230
+ case "action":
231
+ return "type";
232
+ case "locale":
233
+ return _.getOr("No locale", "locale.name");
234
+ default:
235
+ return "contentType.displayName";
236
+ }
237
+ };
143
238
  const createReleaseService = ({ strapi: strapi2 }) => ({
144
239
  async create(releaseData, { user }) {
145
240
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
241
+ const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
+ "release-validation",
243
+ { strapi: strapi2 }
244
+ );
245
+ await Promise.all([
246
+ validatePendingReleasesLimit(),
247
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
248
+ ]);
146
249
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
147
250
  data: releaseWithCreatorFields
148
251
  });
@@ -164,51 +267,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
164
267
  }
165
268
  });
166
269
  },
167
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
168
- hasEntryAttached
169
- } = {
170
- hasEntryAttached: false
171
- }) {
172
- const whereActions = hasEntryAttached ? {
173
- // Find all Releases where the content type entry is present
174
- actions: {
175
- target_type: contentTypeUid,
176
- target_id: entryId
177
- }
178
- } : {
179
- // Find all Releases where the content type entry is not present
180
- $or: [
181
- {
182
- $not: {
183
- actions: {
184
- target_type: contentTypeUid,
185
- target_id: entryId
186
- }
187
- }
270
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
271
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
272
+ where: {
273
+ actions: {
274
+ target_type: contentTypeUid,
275
+ target_id: entryId
188
276
  },
189
- {
190
- actions: null
277
+ releasedAt: {
278
+ $null: true
191
279
  }
192
- ]
193
- };
194
- const populateAttachedAction = hasEntryAttached ? {
195
- // Filter the action to get only the content type entry
196
- actions: {
197
- where: {
280
+ },
281
+ populate: {
282
+ // Filter the action to get only the content type entry
283
+ actions: {
284
+ where: {
285
+ target_type: contentTypeUid,
286
+ target_id: entryId
287
+ }
288
+ }
289
+ }
290
+ });
291
+ return releases.map((release2) => {
292
+ if (release2.actions?.length) {
293
+ const [actionForEntry] = release2.actions;
294
+ delete release2.actions;
295
+ return {
296
+ ...release2,
297
+ action: actionForEntry
298
+ };
299
+ }
300
+ return release2;
301
+ });
302
+ },
303
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
304
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
+ where: {
306
+ releasedAt: {
307
+ $null: true
308
+ },
309
+ actions: {
198
310
  target_type: contentTypeUid,
199
311
  target_id: entryId
200
312
  }
201
313
  }
202
- } : {};
314
+ });
203
315
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
204
316
  where: {
205
- ...whereActions,
317
+ $or: [
318
+ {
319
+ id: {
320
+ $notIn: releasesRelated.map((release2) => release2.id)
321
+ }
322
+ },
323
+ {
324
+ actions: null
325
+ }
326
+ ],
206
327
  releasedAt: {
207
328
  $null: true
208
329
  }
209
- },
210
- populate: {
211
- ...populateAttachedAction
212
330
  }
213
331
  });
214
332
  return releases.map((release2) => {
@@ -224,19 +342,23 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
224
342
  });
225
343
  },
226
344
  async update(id, releaseData, { user }) {
227
- const updatedRelease = await setCreatorFields({ user, isEdition: true })(releaseData);
228
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
345
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
346
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
347
+ if (!release2) {
348
+ throw new errors.NotFoundError(`No release found for id ${id}`);
349
+ }
350
+ if (release2.releasedAt) {
351
+ throw new errors.ValidationError("Release already published");
352
+ }
353
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
229
354
  /*
230
355
  * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
231
356
  * is not compatible with the type we are passing here: UpdateRelease.Request['body']
232
357
  */
233
358
  // @ts-expect-error see above
234
- data: updatedRelease
359
+ data: releaseWithCreatorFields
235
360
  });
236
- if (!release2) {
237
- throw new errors.NotFoundError(`No release found for id ${id}`);
238
- }
239
- return release2;
361
+ return updatedRelease;
240
362
  },
241
363
  async createAction(releaseId, action) {
242
364
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
@@ -246,11 +368,19 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
246
368
  validateEntryContentType(action.entry.contentType),
247
369
  validateUniqueEntry(releaseId, action)
248
370
  ]);
371
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
372
+ if (!release2) {
373
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
374
+ }
375
+ if (release2.releasedAt) {
376
+ throw new errors.ValidationError("Release already published");
377
+ }
249
378
  const { entry, type } = action;
250
379
  return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
251
380
  data: {
252
381
  type,
253
382
  contentType: entry.contentType,
383
+ locale: entry.locale,
254
384
  entry: {
255
385
  id: entry.id,
256
386
  __type: entry.contentType,
@@ -262,14 +392,18 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
262
392
  });
263
393
  },
264
394
  async findActions(releaseId, query) {
265
- const result = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
266
- if (!result) {
395
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
396
+ fields: ["id"]
397
+ });
398
+ if (!release2) {
267
399
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
268
400
  }
269
401
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
270
402
  ...query,
271
403
  populate: {
272
- entry: true
404
+ entry: {
405
+ populate: "*"
406
+ }
273
407
  },
274
408
  filters: {
275
409
  release: releaseId
@@ -279,18 +413,43 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
279
413
  async countActions(query) {
280
414
  return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
281
415
  },
282
- async getAllContentTypeUids(releaseId) {
283
- const contentTypesFromReleaseActions = await strapi2.db.queryBuilder(RELEASE_ACTION_MODEL_UID).select("content_type").where({
284
- $and: [
285
- {
286
- release: releaseId
416
+ async groupActions(actions, groupBy) {
417
+ const contentTypeUids = actions.reduce((acc, action) => {
418
+ if (!acc.includes(action.contentType)) {
419
+ acc.push(action.contentType);
420
+ }
421
+ return acc;
422
+ }, []);
423
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
424
+ contentTypeUids
425
+ );
426
+ const allLocalesDictionary = await this.getLocalesDataForActions();
427
+ const formattedData = actions.map((action) => {
428
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
429
+ return {
430
+ ...action,
431
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
432
+ contentType: {
433
+ displayName,
434
+ mainFieldValue: action.entry[mainField],
435
+ uid: action.contentType
287
436
  }
288
- ]
289
- }).groupBy("content_type").execute();
290
- return contentTypesFromReleaseActions.map(({ contentType: contentTypeUid }) => contentTypeUid);
437
+ };
438
+ });
439
+ const groupName = getGroupName(groupBy);
440
+ return _.groupBy(groupName)(formattedData);
441
+ },
442
+ async getLocalesDataForActions() {
443
+ if (!strapi2.plugin("i18n")) {
444
+ return {};
445
+ }
446
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
447
+ return allLocales.reduce((acc, locale) => {
448
+ acc[locale.code] = { name: locale.name, code: locale.code };
449
+ return acc;
450
+ }, {});
291
451
  },
292
- async getContentTypesDataForActions(releaseId) {
293
- const contentTypesUids = await this.getAllContentTypeUids(releaseId);
452
+ async getContentTypesDataForActions(contentTypesUids) {
294
453
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
295
454
  const contentTypesData = {};
296
455
  for (const contentTypeUid of contentTypesUids) {
@@ -304,6 +463,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
304
463
  }
305
464
  return contentTypesData;
306
465
  },
466
+ getContentTypeModelsFromActions(actions) {
467
+ const contentTypeUids = actions.reduce((acc, action) => {
468
+ if (!acc.includes(action.contentType)) {
469
+ acc.push(action.contentType);
470
+ }
471
+ return acc;
472
+ }, []);
473
+ const contentTypeModelsMap = contentTypeUids.reduce(
474
+ (acc, contentTypeUid) => {
475
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
476
+ return acc;
477
+ },
478
+ {}
479
+ );
480
+ return contentTypeModelsMap;
481
+ },
482
+ async getAllComponents() {
483
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
484
+ const components = await contentManagerComponentsService.findAllComponents();
485
+ const componentsMap = components.reduce(
486
+ (acc, component) => {
487
+ acc[component.uid] = component;
488
+ return acc;
489
+ },
490
+ {}
491
+ );
492
+ return componentsMap;
493
+ },
307
494
  async delete(releaseId) {
308
495
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
309
496
  populate: {
@@ -338,7 +525,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
338
525
  populate: {
339
526
  actions: {
340
527
  populate: {
341
- entry: true
528
+ entry: {
529
+ fields: ["id"]
530
+ }
342
531
  }
343
532
  }
344
533
  }
@@ -358,25 +547,49 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
358
547
  const contentTypeUid = action.contentType;
359
548
  if (!actions[contentTypeUid]) {
360
549
  actions[contentTypeUid] = {
361
- publish: [],
362
- unpublish: []
550
+ entriestoPublishIds: [],
551
+ entriesToUnpublishIds: []
363
552
  };
364
553
  }
365
554
  if (action.type === "publish") {
366
- actions[contentTypeUid].publish.push(action.entry);
555
+ actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
367
556
  } else {
368
- actions[contentTypeUid].unpublish.push(action.entry);
557
+ actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
369
558
  }
370
559
  }
371
560
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
372
562
  await strapi2.db.transaction(async () => {
373
563
  for (const contentTypeUid of Object.keys(actions)) {
374
- const { publish, unpublish } = actions[contentTypeUid];
375
- if (publish.length > 0) {
376
- await entityManagerService.publishMany(publish, contentTypeUid);
564
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
+ const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
566
+ const entriesToPublish = await strapi2.entityService.findMany(
567
+ contentTypeUid,
568
+ {
569
+ filters: {
570
+ id: {
571
+ $in: entriestoPublishIds
572
+ }
573
+ },
574
+ populate
575
+ }
576
+ );
577
+ const entriesToUnpublish = await strapi2.entityService.findMany(
578
+ contentTypeUid,
579
+ {
580
+ filters: {
581
+ id: {
582
+ $in: entriesToUnpublishIds
583
+ }
584
+ },
585
+ populate
586
+ }
587
+ );
588
+ if (entriesToPublish.length > 0) {
589
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
377
590
  }
378
- if (unpublish.length > 0) {
379
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
591
+ if (entriesToUnpublish.length > 0) {
592
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
380
593
  }
381
594
  }
382
595
  });
@@ -395,13 +608,18 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
395
608
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
396
609
  where: {
397
610
  id: actionId,
398
- release: releaseId
611
+ release: {
612
+ id: releaseId,
613
+ releasedAt: {
614
+ $null: true
615
+ }
616
+ }
399
617
  },
400
618
  data: update
401
619
  });
402
620
  if (!updatedAction) {
403
621
  throw new errors.NotFoundError(
404
- `Action with id ${actionId} not found in release with id ${releaseId}`
622
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
405
623
  );
406
624
  }
407
625
  return updatedAction;
@@ -410,12 +628,17 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
410
628
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
411
629
  where: {
412
630
  id: actionId,
413
- release: releaseId
631
+ release: {
632
+ id: releaseId,
633
+ releasedAt: {
634
+ $null: true
635
+ }
636
+ }
414
637
  }
415
638
  });
416
639
  if (!deletedAction) {
417
640
  throw new errors.NotFoundError(
418
- `Action with id ${actionId} not found in release with id ${releaseId}`
641
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
419
642
  );
420
643
  }
421
644
  return deletedAction;
@@ -448,9 +671,42 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
448
671
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
449
672
  );
450
673
  }
674
+ },
675
+ async validatePendingReleasesLimit() {
676
+ const maximumPendingReleases = (
677
+ // @ts-expect-error - options is not typed into features
678
+ EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
679
+ );
680
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
681
+ filters: {
682
+ releasedAt: {
683
+ $null: true
684
+ }
685
+ }
686
+ });
687
+ if (pendingReleasesCount >= maximumPendingReleases) {
688
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
+ }
690
+ },
691
+ async validateUniqueNameForPendingRelease(name) {
692
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
+ filters: {
694
+ releasedAt: {
695
+ $null: true
696
+ },
697
+ name
698
+ }
699
+ });
700
+ const isNameUnique = pendingReleases.length === 0;
701
+ if (!isNameUnique) {
702
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
703
+ }
451
704
  }
452
705
  });
453
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
706
+ const services = {
707
+ release: createReleaseService,
708
+ "release-validation": createReleaseValidationService
709
+ };
454
710
  const RELEASE_SCHEMA = yup.object().shape({
455
711
  name: yup.string().trim().required()
456
712
  }).required().noUnknown();
@@ -469,9 +725,7 @@ const releaseController = {
469
725
  const contentTypeUid = query.contentTypeUid;
470
726
  const entryId = query.entryId;
471
727
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
472
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
473
- hasEntryAttached
474
- });
728
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
475
729
  ctx.body = { data };
476
730
  } else {
477
731
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -595,33 +849,34 @@ const releaseActionController = {
595
849
  });
596
850
  const query = await permissionsManager.sanitizeQuery(ctx.query);
597
851
  const releaseService = getService("release", { strapi });
598
- const { results, pagination } = await releaseService.findActions(releaseId, query);
599
- const allReleaseContentTypesDictionary = await releaseService.getContentTypesDataForActions(
600
- releaseId
601
- );
602
- const allLocales = await strapi.plugin("i18n").service("locales").find();
603
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
604
- acc[locale.code] = { name: locale.name, code: locale.code };
852
+ const { results, pagination } = await releaseService.findActions(releaseId, {
853
+ sort: query.groupBy === "action" ? "type" : query.groupBy,
854
+ ...query
855
+ });
856
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
857
+ if (acc[action.contentType]) {
858
+ return acc;
859
+ }
860
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
861
+ ability: ctx.state.userAbility,
862
+ model: action.contentType
863
+ });
864
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
605
865
  return acc;
606
866
  }, {});
607
- const data = results.map((action) => {
608
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
609
- return {
610
- ...action,
611
- entry: {
612
- id: action.entry.id,
613
- contentType: {
614
- displayName,
615
- mainFieldValue: action.entry[mainField]
616
- },
617
- locale: allLocalesDictionary[action.entry.locale]
618
- }
619
- };
620
- });
867
+ const sanitizedResults = await mapAsync(results, async (action) => ({
868
+ ...action,
869
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
870
+ }));
871
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
872
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
873
+ const components = await releaseService.getAllComponents();
621
874
  ctx.body = {
622
- data,
875
+ data: groupedData,
623
876
  meta: {
624
- pagination
877
+ pagination,
878
+ contentTypes: contentTypes2,
879
+ components
625
880
  }
626
881
  };
627
882
  },
@@ -643,10 +898,8 @@ const releaseActionController = {
643
898
  async delete(ctx) {
644
899
  const actionId = ctx.params.actionId;
645
900
  const releaseId = ctx.params.releaseId;
646
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
647
- actionId,
648
- releaseId
649
- );
901
+ const releaseService = getService("release", { strapi });
902
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
650
903
  ctx.body = {
651
904
  data: deletedReleaseAction
652
905
  };
@@ -829,9 +1082,10 @@ const routes = {
829
1082
  };
830
1083
  const { features } = require("@strapi/strapi/dist/utils/ee");
831
1084
  const getPlugin = () => {
832
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1085
+ if (features.isEnabled("cms-content-releases")) {
833
1086
  return {
834
1087
  register,
1088
+ bootstrap,
835
1089
  contentTypes,
836
1090
  services,
837
1091
  controllers,