@strapi/content-releases 0.0.0-next.e1ede8c55a0e1e22ce20137bf238fc374bd5dd51 → 0.0.0-next.f8af92b375dc730ba47ed2117f25df893aae696c

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/dist/_chunks/{App-o5_WfqR-.js → App-OK4Xac-O.js} +572 -224
  2. package/dist/_chunks/App-OK4Xac-O.js.map +1 -0
  3. package/dist/_chunks/App-xAkiD42p.mjs +1292 -0
  4. package/dist/_chunks/App-xAkiD42p.mjs.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-haKSQIo8.js → en-r0otWaln.js} +19 -4
  10. package/dist/_chunks/en-r0otWaln.js.map +1 -0
  11. package/dist/_chunks/{en-ngTk74JV.mjs → en-veqvqeEr.mjs} +19 -4
  12. package/dist/_chunks/en-veqvqeEr.mjs.map +1 -0
  13. package/dist/_chunks/{index-EdBmRHRU.js → index-JvA2_26n.js} +220 -54
  14. package/dist/_chunks/index-JvA2_26n.js.map +1 -0
  15. package/dist/_chunks/{index-XAQOX_IB.mjs → index-exoiSU3V.mjs} +231 -65
  16. package/dist/_chunks/index-exoiSU3V.mjs.map +1 -0
  17. package/dist/admin/index.js +2 -1
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +3 -2
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/server/index.js +749 -302
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/index.mjs +749 -303
  24. package/dist/server/index.mjs.map +1 -1
  25. package/package.json +13 -9
  26. package/dist/_chunks/App-g2P5kbSm.mjs +0 -945
  27. package/dist/_chunks/App-g2P5kbSm.mjs.map +0 -1
  28. package/dist/_chunks/App-o5_WfqR-.js.map +0 -1
  29. package/dist/_chunks/en-haKSQIo8.js.map +0 -1
  30. package/dist/_chunks/en-ngTk74JV.mjs.map +0 -1
  31. package/dist/_chunks/index-EdBmRHRU.js.map +0 -1
  32. package/dist/_chunks/index-XAQOX_IB.mjs.map +0 -1
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const lodash = require("lodash");
3
4
  const _ = require("lodash/fp");
5
+ const EE = require("@strapi/strapi/dist/utils/ee");
6
+ const nodeSchedule = require("node-schedule");
4
7
  const yup = require("yup");
5
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
9
  function _interopNamespace(e) {
@@ -22,6 +25,7 @@ function _interopNamespace(e) {
22
25
  return Object.freeze(n);
23
26
  }
24
27
  const ___default = /* @__PURE__ */ _interopDefault(_);
28
+ const EE__default = /* @__PURE__ */ _interopDefault(EE);
25
29
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
26
30
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
27
31
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -69,10 +73,114 @@ const ACTIONS = [
69
73
  pluginName: "content-releases"
70
74
  }
71
75
  ];
72
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
76
+ const ALLOWED_WEBHOOK_EVENTS = {
77
+ RELEASES_PUBLISH: "releases.publish"
78
+ };
79
+ async function deleteActionsOnDisableDraftAndPublish({
80
+ oldContentTypes,
81
+ contentTypes: contentTypes2
82
+ }) {
83
+ if (!oldContentTypes) {
84
+ return;
85
+ }
86
+ for (const uid in contentTypes2) {
87
+ if (!oldContentTypes[uid]) {
88
+ continue;
89
+ }
90
+ const oldContentType = oldContentTypes[uid];
91
+ const contentType = contentTypes2[uid];
92
+ if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
93
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
94
+ }
95
+ }
96
+ }
97
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
98
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
99
+ if (deletedContentTypes.length) {
100
+ await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
101
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
102
+ });
103
+ }
104
+ }
105
+ const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
73
106
  const register = async ({ strapi: strapi2 }) => {
74
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
107
+ if (features$2.isEnabled("cms-content-releases")) {
75
108
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
109
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
110
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
111
+ }
112
+ };
113
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
114
+ return strapi2.plugin("content-releases").service(name);
115
+ };
116
+ const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
117
+ const bootstrap = async ({ strapi: strapi2 }) => {
118
+ if (features$1.isEnabled("cms-content-releases")) {
119
+ strapi2.db.lifecycles.subscribe({
120
+ afterDelete(event) {
121
+ const { model, result } = event;
122
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
123
+ const { id } = result;
124
+ strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
125
+ where: {
126
+ target_type: model.uid,
127
+ target_id: id
128
+ }
129
+ });
130
+ }
131
+ },
132
+ /**
133
+ * deleteMany hook doesn't return the deleted entries ids
134
+ * so we need to fetch them before deleting the entries to save the ids on our state
135
+ */
136
+ async beforeDeleteMany(event) {
137
+ const { model, params } = event;
138
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
139
+ const { where } = params;
140
+ const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
141
+ event.state.entriesToDelete = entriesToDelete;
142
+ }
143
+ },
144
+ /**
145
+ * We delete the release actions related to deleted entries
146
+ * We make this only after deleteMany is succesfully executed to avoid errors
147
+ */
148
+ async afterDeleteMany(event) {
149
+ const { model, state } = event;
150
+ const entriesToDelete = state.entriesToDelete;
151
+ if (entriesToDelete) {
152
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
153
+ where: {
154
+ target_type: model.uid,
155
+ target_id: {
156
+ $in: entriesToDelete.map((entry) => entry.id)
157
+ }
158
+ }
159
+ });
160
+ }
161
+ }
162
+ });
163
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
164
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
165
+ strapi2.log.error(
166
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
167
+ );
168
+ throw err;
169
+ });
170
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
171
+ strapi2.webhookStore.addAllowedEvent(key, value);
172
+ });
173
+ }
174
+ }
175
+ };
176
+ const destroy = async ({ strapi: strapi2 }) => {
177
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
178
+ const scheduledJobs = getService("scheduling", {
179
+ strapi: strapi2
180
+ }).getAll();
181
+ for (const [, job] of scheduledJobs) {
182
+ job.cancel();
183
+ }
76
184
  }
77
185
  };
78
186
  const schema$1 = {
@@ -101,6 +209,12 @@ const schema$1 = {
101
209
  releasedAt: {
102
210
  type: "datetime"
103
211
  },
212
+ scheduledAt: {
213
+ type: "datetime"
214
+ },
215
+ timezone: {
216
+ type: "string"
217
+ },
104
218
  actions: {
105
219
  type: "relation",
106
220
  relation: "oneToMany",
@@ -163,327 +277,516 @@ const contentTypes = {
163
277
  release: release$1,
164
278
  "release-action": releaseAction$1
165
279
  };
166
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
167
- return strapi2.plugin("content-releases").service(name);
168
- };
169
280
  const getGroupName = (queryValue) => {
170
281
  switch (queryValue) {
171
282
  case "contentType":
172
- return "entry.contentType.displayName";
283
+ return "contentType.displayName";
173
284
  case "action":
174
285
  return "type";
175
286
  case "locale":
176
- return ___default.default.getOr("No locale", "entry.locale.name");
287
+ return ___default.default.getOr("No locale", "locale.name");
177
288
  default:
178
- return "entry.contentType.displayName";
289
+ return "contentType.displayName";
179
290
  }
180
291
  };
181
- const createReleaseService = ({ strapi: strapi2 }) => ({
182
- async create(releaseData, { user }) {
183
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
184
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
185
- data: releaseWithCreatorFields
292
+ const createReleaseService = ({ strapi: strapi2 }) => {
293
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
294
+ strapi2.eventHub.emit(event, {
295
+ isPublished,
296
+ error,
297
+ release: release2
186
298
  });
187
- },
188
- async findOne(id, query = {}) {
189
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
190
- ...query
191
- });
192
- return release2;
193
- },
194
- findPage(query) {
195
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
196
- ...query,
197
- populate: {
198
- actions: {
199
- // @ts-expect-error Ignore missing properties
200
- count: true
201
- }
299
+ };
300
+ return {
301
+ async create(releaseData, { user }) {
302
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
303
+ const {
304
+ validatePendingReleasesLimit,
305
+ validateUniqueNameForPendingRelease,
306
+ validateScheduledAtIsLaterThanNow
307
+ } = getService("release-validation", { strapi: strapi2 });
308
+ await Promise.all([
309
+ validatePendingReleasesLimit(),
310
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
311
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
+ ]);
313
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
+ data: releaseWithCreatorFields
315
+ });
316
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
317
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
318
+ await schedulingService.set(release2.id, release2.scheduledAt);
202
319
  }
203
- });
204
- },
205
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
206
- hasEntryAttached
207
- } = {
208
- hasEntryAttached: false
209
- }) {
210
- const whereActions = hasEntryAttached ? {
211
- // Find all Releases where the content type entry is present
212
- actions: {
213
- target_type: contentTypeUid,
214
- target_id: entryId
215
- }
216
- } : {
217
- // Find all Releases where the content type entry is not present
218
- $or: [
219
- {
220
- $not: {
221
- actions: {
320
+ strapi2.telemetry.send("didCreateContentRelease");
321
+ return release2;
322
+ },
323
+ async findOne(id, query = {}) {
324
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
325
+ ...query
326
+ });
327
+ return release2;
328
+ },
329
+ findPage(query) {
330
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
331
+ ...query,
332
+ populate: {
333
+ actions: {
334
+ // @ts-expect-error Ignore missing properties
335
+ count: true
336
+ }
337
+ }
338
+ });
339
+ },
340
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
341
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
342
+ where: {
343
+ actions: {
344
+ target_type: contentTypeUid,
345
+ target_id: entryId
346
+ },
347
+ releasedAt: {
348
+ $null: true
349
+ }
350
+ },
351
+ populate: {
352
+ // Filter the action to get only the content type entry
353
+ actions: {
354
+ where: {
222
355
  target_type: contentTypeUid,
223
356
  target_id: entryId
224
357
  }
225
358
  }
226
- },
227
- {
228
- actions: null
229
359
  }
230
- ]
231
- };
232
- const populateAttachedAction = hasEntryAttached ? {
233
- // Filter the action to get only the content type entry
234
- actions: {
360
+ });
361
+ return releases.map((release2) => {
362
+ if (release2.actions?.length) {
363
+ const [actionForEntry] = release2.actions;
364
+ delete release2.actions;
365
+ return {
366
+ ...release2,
367
+ action: actionForEntry
368
+ };
369
+ }
370
+ return release2;
371
+ });
372
+ },
373
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
374
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
235
375
  where: {
236
- target_type: contentTypeUid,
237
- target_id: entryId
376
+ releasedAt: {
377
+ $null: true
378
+ },
379
+ actions: {
380
+ target_type: contentTypeUid,
381
+ target_id: entryId
382
+ }
238
383
  }
239
- }
240
- } : {};
241
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
242
- where: {
243
- ...whereActions,
244
- releasedAt: {
245
- $null: true
384
+ });
385
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
386
+ where: {
387
+ $or: [
388
+ {
389
+ id: {
390
+ $notIn: releasesRelated.map((release2) => release2.id)
391
+ }
392
+ },
393
+ {
394
+ actions: null
395
+ }
396
+ ],
397
+ releasedAt: {
398
+ $null: true
399
+ }
246
400
  }
247
- },
248
- populate: {
249
- ...populateAttachedAction
401
+ });
402
+ return releases.map((release2) => {
403
+ if (release2.actions?.length) {
404
+ const [actionForEntry] = release2.actions;
405
+ delete release2.actions;
406
+ return {
407
+ ...release2,
408
+ action: actionForEntry
409
+ };
410
+ }
411
+ return release2;
412
+ });
413
+ },
414
+ async update(id, releaseData, { user }) {
415
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
416
+ releaseData
417
+ );
418
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
419
+ "release-validation",
420
+ { strapi: strapi2 }
421
+ );
422
+ await Promise.all([
423
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
424
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
425
+ ]);
426
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
427
+ if (!release2) {
428
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
250
429
  }
251
- });
252
- return releases.map((release2) => {
253
- if (release2.actions?.length) {
254
- const [actionForEntry] = release2.actions;
255
- delete release2.actions;
256
- return {
257
- ...release2,
258
- action: actionForEntry
259
- };
430
+ if (release2.releasedAt) {
431
+ throw new utils.errors.ValidationError("Release already published");
260
432
  }
261
- return release2;
262
- });
263
- },
264
- async update(id, releaseData, { user }) {
265
- const updatedRelease = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
266
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
267
- /*
268
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
269
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
270
- */
271
- // @ts-expect-error see above
272
- data: updatedRelease
273
- });
274
- if (!release2) {
275
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
276
- }
277
- return release2;
278
- },
279
- async createAction(releaseId, action) {
280
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
281
- strapi: strapi2
282
- });
283
- await Promise.all([
284
- validateEntryContentType(action.entry.contentType),
285
- validateUniqueEntry(releaseId, action)
286
- ]);
287
- const { entry, type } = action;
288
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
289
- data: {
290
- type,
291
- contentType: entry.contentType,
292
- locale: entry.locale,
293
- entry: {
294
- id: entry.id,
295
- __type: entry.contentType,
296
- __pivot: { field: "entry" }
297
- },
298
- release: releaseId
299
- },
300
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
301
- });
302
- },
303
- async findActions(releaseId, query) {
304
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
305
- fields: ["id"]
306
- });
307
- if (!release2) {
308
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
309
- }
310
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
311
- ...query,
312
- populate: {
313
- entry: true
314
- },
315
- filters: {
316
- release: releaseId
433
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
434
+ /*
435
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
436
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
437
+ */
438
+ // @ts-expect-error see above
439
+ data: releaseWithCreatorFields
440
+ });
441
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
442
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
443
+ if (releaseData.scheduledAt) {
444
+ await schedulingService.set(id, releaseData.scheduledAt);
445
+ } else if (release2.scheduledAt) {
446
+ schedulingService.cancel(id);
447
+ }
317
448
  }
318
- });
319
- },
320
- async countActions(query) {
321
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
322
- },
323
- async groupActions(actions, groupBy) {
324
- const contentTypeUids = actions.reduce((acc, action) => {
325
- if (!acc.includes(action.contentType)) {
326
- acc.push(action.contentType);
449
+ strapi2.telemetry.send("didUpdateContentRelease");
450
+ return updatedRelease;
451
+ },
452
+ async createAction(releaseId, action) {
453
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
454
+ strapi: strapi2
455
+ });
456
+ await Promise.all([
457
+ validateEntryContentType(action.entry.contentType),
458
+ validateUniqueEntry(releaseId, action)
459
+ ]);
460
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
461
+ if (!release2) {
462
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
327
463
  }
328
- return acc;
329
- }, []);
330
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
331
- contentTypeUids
332
- );
333
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
334
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
335
- acc[locale.code] = { name: locale.name, code: locale.code };
336
- return acc;
337
- }, {});
338
- const formattedData = actions.map((action) => {
339
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
340
- return {
341
- ...action,
342
- entry: {
343
- id: action.entry.id,
344
- contentType: {
345
- displayName,
346
- mainFieldValue: action.entry[mainField]
464
+ if (release2.releasedAt) {
465
+ throw new utils.errors.ValidationError("Release already published");
466
+ }
467
+ const { entry, type } = action;
468
+ return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
469
+ data: {
470
+ type,
471
+ contentType: entry.contentType,
472
+ locale: entry.locale,
473
+ entry: {
474
+ id: entry.id,
475
+ __type: entry.contentType,
476
+ __pivot: { field: "entry" }
347
477
  },
348
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
349
- status: action.entry.publishedAt ? "published" : "draft"
350
- }
351
- };
352
- });
353
- const groupName = getGroupName(groupBy);
354
- return ___default.default.groupBy(groupName)(formattedData);
355
- },
356
- async getContentTypesDataForActions(contentTypesUids) {
357
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
358
- const contentTypesData = {};
359
- for (const contentTypeUid of contentTypesUids) {
360
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
361
- uid: contentTypeUid
478
+ release: releaseId
479
+ },
480
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
362
481
  });
363
- contentTypesData[contentTypeUid] = {
364
- mainField: contentTypeConfig.settings.mainField,
365
- displayName: strapi2.getModel(contentTypeUid).info.displayName
366
- };
367
- }
368
- return contentTypesData;
369
- },
370
- async delete(releaseId) {
371
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
372
- populate: {
373
- actions: {
374
- fields: ["id"]
375
- }
482
+ },
483
+ async findActions(releaseId, query) {
484
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
485
+ fields: ["id"]
486
+ });
487
+ if (!release2) {
488
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
376
489
  }
377
- });
378
- if (!release2) {
379
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
380
- }
381
- if (release2.releasedAt) {
382
- throw new utils.errors.ValidationError("Release already published");
383
- }
384
- await strapi2.db.transaction(async () => {
385
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
386
- where: {
387
- id: {
388
- $in: release2.actions.map((action) => action.id)
490
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
491
+ ...query,
492
+ populate: {
493
+ entry: {
494
+ populate: "*"
389
495
  }
496
+ },
497
+ filters: {
498
+ release: releaseId
390
499
  }
391
500
  });
392
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
393
- });
394
- return release2;
395
- },
396
- async publish(releaseId) {
397
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
398
- RELEASE_MODEL_UID,
399
- releaseId,
400
- {
501
+ },
502
+ async countActions(query) {
503
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
504
+ },
505
+ async groupActions(actions, groupBy) {
506
+ const contentTypeUids = actions.reduce((acc, action) => {
507
+ if (!acc.includes(action.contentType)) {
508
+ acc.push(action.contentType);
509
+ }
510
+ return acc;
511
+ }, []);
512
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
513
+ contentTypeUids
514
+ );
515
+ const allLocalesDictionary = await this.getLocalesDataForActions();
516
+ const formattedData = actions.map((action) => {
517
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
518
+ return {
519
+ ...action,
520
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
521
+ contentType: {
522
+ displayName,
523
+ mainFieldValue: action.entry[mainField],
524
+ uid: action.contentType
525
+ }
526
+ };
527
+ });
528
+ const groupName = getGroupName(groupBy);
529
+ return ___default.default.groupBy(groupName)(formattedData);
530
+ },
531
+ async getLocalesDataForActions() {
532
+ if (!strapi2.plugin("i18n")) {
533
+ return {};
534
+ }
535
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
536
+ return allLocales.reduce((acc, locale) => {
537
+ acc[locale.code] = { name: locale.name, code: locale.code };
538
+ return acc;
539
+ }, {});
540
+ },
541
+ async getContentTypesDataForActions(contentTypesUids) {
542
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
543
+ const contentTypesData = {};
544
+ for (const contentTypeUid of contentTypesUids) {
545
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
546
+ uid: contentTypeUid
547
+ });
548
+ contentTypesData[contentTypeUid] = {
549
+ mainField: contentTypeConfig.settings.mainField,
550
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
551
+ };
552
+ }
553
+ return contentTypesData;
554
+ },
555
+ getContentTypeModelsFromActions(actions) {
556
+ const contentTypeUids = actions.reduce((acc, action) => {
557
+ if (!acc.includes(action.contentType)) {
558
+ acc.push(action.contentType);
559
+ }
560
+ return acc;
561
+ }, []);
562
+ const contentTypeModelsMap = contentTypeUids.reduce(
563
+ (acc, contentTypeUid) => {
564
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
565
+ return acc;
566
+ },
567
+ {}
568
+ );
569
+ return contentTypeModelsMap;
570
+ },
571
+ async getAllComponents() {
572
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
573
+ const components = await contentManagerComponentsService.findAllComponents();
574
+ const componentsMap = components.reduce(
575
+ (acc, component) => {
576
+ acc[component.uid] = component;
577
+ return acc;
578
+ },
579
+ {}
580
+ );
581
+ return componentsMap;
582
+ },
583
+ async delete(releaseId) {
584
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
401
585
  populate: {
402
586
  actions: {
403
- populate: {
404
- entry: true
405
- }
587
+ fields: ["id"]
406
588
  }
407
589
  }
590
+ });
591
+ if (!release2) {
592
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
408
593
  }
409
- );
410
- if (!releaseWithPopulatedActionEntries) {
411
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
412
- }
413
- if (releaseWithPopulatedActionEntries.releasedAt) {
414
- throw new utils.errors.ValidationError("Release already published");
415
- }
416
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
417
- throw new utils.errors.ValidationError("No entries to publish");
418
- }
419
- const actions = {};
420
- for (const action of releaseWithPopulatedActionEntries.actions) {
421
- const contentTypeUid = action.contentType;
422
- if (!actions[contentTypeUid]) {
423
- actions[contentTypeUid] = {
424
- publish: [],
425
- unpublish: []
426
- };
594
+ if (release2.releasedAt) {
595
+ throw new utils.errors.ValidationError("Release already published");
427
596
  }
428
- if (action.type === "publish") {
429
- actions[contentTypeUid].publish.push(action.entry);
430
- } else {
431
- actions[contentTypeUid].unpublish.push(action.entry);
597
+ await strapi2.db.transaction(async () => {
598
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
599
+ where: {
600
+ id: {
601
+ $in: release2.actions.map((action) => action.id)
602
+ }
603
+ }
604
+ });
605
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
606
+ });
607
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
608
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
609
+ await schedulingService.cancel(release2.id);
432
610
  }
433
- }
434
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
435
- await strapi2.db.transaction(async () => {
436
- for (const contentTypeUid of Object.keys(actions)) {
437
- const { publish, unpublish } = actions[contentTypeUid];
438
- if (publish.length > 0) {
439
- await entityManagerService.publishMany(publish, contentTypeUid);
611
+ strapi2.telemetry.send("didDeleteContentRelease");
612
+ return release2;
613
+ },
614
+ async publish(releaseId) {
615
+ try {
616
+ const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
617
+ RELEASE_MODEL_UID,
618
+ releaseId,
619
+ {
620
+ populate: {
621
+ actions: {
622
+ populate: {
623
+ entry: {
624
+ fields: ["id"]
625
+ }
626
+ }
627
+ }
628
+ }
629
+ }
630
+ );
631
+ if (!releaseWithPopulatedActionEntries) {
632
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
633
+ }
634
+ if (releaseWithPopulatedActionEntries.releasedAt) {
635
+ throw new utils.errors.ValidationError("Release already published");
636
+ }
637
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
+ throw new utils.errors.ValidationError("No entries to publish");
440
639
  }
441
- if (unpublish.length > 0) {
442
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
640
+ const collectionTypeActions = {};
641
+ const singleTypeActions = [];
642
+ for (const action of releaseWithPopulatedActionEntries.actions) {
643
+ const contentTypeUid = action.contentType;
644
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
645
+ if (!collectionTypeActions[contentTypeUid]) {
646
+ collectionTypeActions[contentTypeUid] = {
647
+ entriestoPublishIds: [],
648
+ entriesToUnpublishIds: []
649
+ };
650
+ }
651
+ if (action.type === "publish") {
652
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
653
+ } else {
654
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
655
+ }
656
+ } else {
657
+ singleTypeActions.push({
658
+ uid: contentTypeUid,
659
+ action: action.type,
660
+ id: action.entry.id
661
+ });
662
+ }
443
663
  }
664
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
665
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
666
+ await strapi2.db.transaction(async () => {
667
+ for (const { uid, action, id } of singleTypeActions) {
668
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
669
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
670
+ try {
671
+ if (action === "publish") {
672
+ await entityManagerService.publish(entry, uid);
673
+ } else {
674
+ await entityManagerService.unpublish(entry, uid);
675
+ }
676
+ } catch (error) {
677
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
678
+ } else {
679
+ throw error;
680
+ }
681
+ }
682
+ }
683
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
684
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
685
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
686
+ const entriesToPublish = await strapi2.entityService.findMany(
687
+ contentTypeUid,
688
+ {
689
+ filters: {
690
+ id: {
691
+ $in: entriestoPublishIds
692
+ }
693
+ },
694
+ populate
695
+ }
696
+ );
697
+ const entriesToUnpublish = await strapi2.entityService.findMany(
698
+ contentTypeUid,
699
+ {
700
+ filters: {
701
+ id: {
702
+ $in: entriesToUnpublishIds
703
+ }
704
+ },
705
+ populate
706
+ }
707
+ );
708
+ if (entriesToPublish.length > 0) {
709
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
710
+ }
711
+ if (entriesToUnpublish.length > 0) {
712
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
713
+ }
714
+ }
715
+ });
716
+ const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
717
+ data: {
718
+ /*
719
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
720
+ */
721
+ // @ts-expect-error see above
722
+ releasedAt: /* @__PURE__ */ new Date()
723
+ },
724
+ populate: {
725
+ actions: {
726
+ // @ts-expect-error is not expecting count but it is working
727
+ count: true
728
+ }
729
+ }
730
+ });
731
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
732
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
733
+ isPublished: true,
734
+ release: release2
735
+ });
736
+ }
737
+ strapi2.telemetry.send("didPublishContentRelease");
738
+ return release2;
739
+ } catch (error) {
740
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
741
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
742
+ isPublished: false,
743
+ error
744
+ });
745
+ }
746
+ throw error;
444
747
  }
445
- });
446
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
447
- data: {
448
- /*
449
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
450
- */
451
- // @ts-expect-error see above
452
- releasedAt: /* @__PURE__ */ new Date()
748
+ },
749
+ async updateAction(actionId, releaseId, update) {
750
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
751
+ where: {
752
+ id: actionId,
753
+ release: {
754
+ id: releaseId,
755
+ releasedAt: {
756
+ $null: true
757
+ }
758
+ }
759
+ },
760
+ data: update
761
+ });
762
+ if (!updatedAction) {
763
+ throw new utils.errors.NotFoundError(
764
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
765
+ );
453
766
  }
454
- });
455
- return release2;
456
- },
457
- async updateAction(actionId, releaseId, update) {
458
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
459
- where: {
460
- id: actionId,
461
- release: releaseId
462
- },
463
- data: update
464
- });
465
- if (!updatedAction) {
466
- throw new utils.errors.NotFoundError(
467
- `Action with id ${actionId} not found in release with id ${releaseId}`
468
- );
469
- }
470
- return updatedAction;
471
- },
472
- async deleteAction(actionId, releaseId) {
473
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
474
- where: {
475
- id: actionId,
476
- release: releaseId
767
+ return updatedAction;
768
+ },
769
+ async deleteAction(actionId, releaseId) {
770
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
771
+ where: {
772
+ id: actionId,
773
+ release: {
774
+ id: releaseId,
775
+ releasedAt: {
776
+ $null: true
777
+ }
778
+ }
779
+ }
780
+ });
781
+ if (!deletedAction) {
782
+ throw new utils.errors.NotFoundError(
783
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
+ );
477
785
  }
478
- });
479
- if (!deletedAction) {
480
- throw new utils.errors.NotFoundError(
481
- `Action with id ${actionId} not found in release with id ${releaseId}`
482
- );
786
+ return deletedAction;
483
787
  }
484
- return deletedAction;
485
- }
486
- });
788
+ };
789
+ };
487
790
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
488
791
  async validateUniqueEntry(releaseId, releaseActionArgs) {
489
792
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -511,11 +814,120 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
511
814
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
512
815
  );
513
816
  }
817
+ },
818
+ async validatePendingReleasesLimit() {
819
+ const maximumPendingReleases = (
820
+ // @ts-expect-error - options is not typed into features
821
+ EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
822
+ );
823
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
824
+ filters: {
825
+ releasedAt: {
826
+ $null: true
827
+ }
828
+ }
829
+ });
830
+ if (pendingReleasesCount >= maximumPendingReleases) {
831
+ throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
832
+ }
833
+ },
834
+ async validateUniqueNameForPendingRelease(name, id) {
835
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
836
+ filters: {
837
+ releasedAt: {
838
+ $null: true
839
+ },
840
+ name,
841
+ ...id && { id: { $ne: id } }
842
+ }
843
+ });
844
+ const isNameUnique = pendingReleases.length === 0;
845
+ if (!isNameUnique) {
846
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
847
+ }
848
+ },
849
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
850
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
851
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
852
+ }
514
853
  }
515
854
  });
516
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
855
+ const createSchedulingService = ({ strapi: strapi2 }) => {
856
+ const scheduledJobs = /* @__PURE__ */ new Map();
857
+ return {
858
+ async set(releaseId, scheduleDate) {
859
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
860
+ if (!release2) {
861
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
862
+ }
863
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
864
+ try {
865
+ await getService("release").publish(releaseId);
866
+ } catch (error) {
867
+ }
868
+ this.cancel(releaseId);
869
+ });
870
+ if (scheduledJobs.has(releaseId)) {
871
+ this.cancel(releaseId);
872
+ }
873
+ scheduledJobs.set(releaseId, job);
874
+ return scheduledJobs;
875
+ },
876
+ cancel(releaseId) {
877
+ if (scheduledJobs.has(releaseId)) {
878
+ scheduledJobs.get(releaseId).cancel();
879
+ scheduledJobs.delete(releaseId);
880
+ }
881
+ return scheduledJobs;
882
+ },
883
+ getAll() {
884
+ return scheduledJobs;
885
+ },
886
+ /**
887
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
888
+ * This is useful in case the server was restarted and the scheduled jobs were lost
889
+ * This also could be used to sync different Strapi instances in case of a cluster
890
+ */
891
+ async syncFromDatabase() {
892
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
893
+ where: {
894
+ scheduledAt: {
895
+ $gte: /* @__PURE__ */ new Date()
896
+ },
897
+ releasedAt: null
898
+ }
899
+ });
900
+ for (const release2 of releases) {
901
+ this.set(release2.id, release2.scheduledAt);
902
+ }
903
+ return scheduledJobs;
904
+ }
905
+ };
906
+ };
907
+ const services = {
908
+ release: createReleaseService,
909
+ "release-validation": createReleaseValidationService,
910
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
911
+ };
517
912
  const RELEASE_SCHEMA = yup__namespace.object().shape({
518
- name: yup__namespace.string().trim().required()
913
+ name: yup__namespace.string().trim().required(),
914
+ scheduledAt: yup__namespace.string().nullable(),
915
+ isScheduled: yup__namespace.boolean().optional(),
916
+ time: yup__namespace.string().when("isScheduled", {
917
+ is: true,
918
+ then: yup__namespace.string().trim().required(),
919
+ otherwise: yup__namespace.string().nullable()
920
+ }),
921
+ timezone: yup__namespace.string().when("isScheduled", {
922
+ is: true,
923
+ then: yup__namespace.string().required().nullable(),
924
+ otherwise: yup__namespace.string().nullable()
925
+ }),
926
+ date: yup__namespace.string().when("isScheduled", {
927
+ is: true,
928
+ then: yup__namespace.string().required().nullable(),
929
+ otherwise: yup__namespace.string().nullable()
930
+ })
519
931
  }).required().noUnknown();
520
932
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
521
933
  const releaseController = {
@@ -532,9 +944,7 @@ const releaseController = {
532
944
  const contentTypeUid = query.contentTypeUid;
533
945
  const entryId = query.entryId;
534
946
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
535
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
536
- hasEntryAttached
537
- });
947
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
538
948
  ctx.body = { data };
539
949
  } else {
540
950
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -557,19 +967,18 @@ const releaseController = {
557
967
  const id = ctx.params.id;
558
968
  const releaseService = getService("release", { strapi });
559
969
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
560
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
561
- ability: ctx.state.userAbility,
562
- model: RELEASE_MODEL_UID
563
- });
564
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
970
+ if (!release2) {
971
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
972
+ }
565
973
  const count = await releaseService.countActions({
566
974
  filters: {
567
975
  release: id
568
976
  }
569
977
  });
570
- if (!release2) {
571
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
572
- }
978
+ const sanitizedRelease = {
979
+ ...release2,
980
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
981
+ };
573
982
  const data = {
574
983
  ...sanitizedRelease,
575
984
  actions: {
@@ -622,8 +1031,27 @@ const releaseController = {
622
1031
  const id = ctx.params.id;
623
1032
  const releaseService = getService("release", { strapi });
624
1033
  const release2 = await releaseService.publish(id, { user });
1034
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1035
+ releaseService.countActions({
1036
+ filters: {
1037
+ release: id,
1038
+ type: "publish"
1039
+ }
1040
+ }),
1041
+ releaseService.countActions({
1042
+ filters: {
1043
+ release: id,
1044
+ type: "unpublish"
1045
+ }
1046
+ })
1047
+ ]);
625
1048
  ctx.body = {
626
- data: release2
1049
+ data: release2,
1050
+ meta: {
1051
+ totalEntries: countPublishActions + countUnpublishActions,
1052
+ totalPublishedEntries: countPublishActions,
1053
+ totalUnpublishedEntries: countUnpublishActions
1054
+ }
627
1055
  };
628
1056
  }
629
1057
  };
@@ -662,11 +1090,30 @@ const releaseActionController = {
662
1090
  sort: query.groupBy === "action" ? "type" : query.groupBy,
663
1091
  ...query
664
1092
  });
665
- const groupedData = await releaseService.groupActions(results, query.groupBy);
1093
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1094
+ if (acc[action.contentType]) {
1095
+ return acc;
1096
+ }
1097
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1098
+ ability: ctx.state.userAbility,
1099
+ model: action.contentType
1100
+ });
1101
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1102
+ return acc;
1103
+ }, {});
1104
+ const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1105
+ ...action,
1106
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1107
+ }));
1108
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1109
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1110
+ const components = await releaseService.getAllComponents();
666
1111
  ctx.body = {
667
1112
  data: groupedData,
668
1113
  meta: {
669
- pagination
1114
+ pagination,
1115
+ contentTypes: contentTypes2,
1116
+ components
670
1117
  }
671
1118
  };
672
1119
  },
@@ -688,10 +1135,8 @@ const releaseActionController = {
688
1135
  async delete(ctx) {
689
1136
  const actionId = ctx.params.actionId;
690
1137
  const releaseId = ctx.params.releaseId;
691
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
692
- actionId,
693
- releaseId
694
- );
1138
+ const releaseService = getService("release", { strapi });
1139
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
695
1140
  ctx.body = {
696
1141
  data: deletedReleaseAction
697
1142
  };
@@ -874,9 +1319,11 @@ const routes = {
874
1319
  };
875
1320
  const { features } = require("@strapi/strapi/dist/utils/ee");
876
1321
  const getPlugin = () => {
877
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1322
+ if (features.isEnabled("cms-content-releases")) {
878
1323
  return {
879
1324
  register,
1325
+ bootstrap,
1326
+ destroy,
880
1327
  contentTypes,
881
1328
  services,
882
1329
  controllers,