@strapi/content-releases 0.0.0-experimental.cae3a5a17d131a6f59673b62d01cfac869ea9cc2 → 0.0.0-experimental.d362bf200f5f9359a4bbd4a549603de5ee1f04ca

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 (108) hide show
  1. package/dist/_chunks/App-1LckaIGY.js +1352 -0
  2. package/dist/_chunks/App-1LckaIGY.js.map +1 -0
  3. package/dist/_chunks/App-X01LBg5V.mjs +1329 -0
  4. package/dist/_chunks/App-X01LBg5V.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-SOqjCdyh.mjs → en-RdapH-9X.mjs} +18 -7
  10. package/dist/_chunks/en-RdapH-9X.mjs.map +1 -0
  11. package/dist/_chunks/{en-2DuPv5k0.js → en-faJDuv3q.js} +18 -7
  12. package/dist/_chunks/en-faJDuv3q.js.map +1 -0
  13. package/dist/_chunks/{index-bsuc8ZwZ.mjs → index-OD9AlD-6.mjs} +233 -107
  14. package/dist/_chunks/index-OD9AlD-6.mjs.map +1 -0
  15. package/dist/_chunks/{index-_lT-gI3M.js → index-cYWov2wa.js} +226 -100
  16. package/dist/_chunks/index-cYWov2wa.js.map +1 -0
  17. package/dist/admin/index.js +1 -15
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +1 -15
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/admin/src/components/CMReleasesContainer.d.ts +1 -0
  22. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  23. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  24. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  25. package/dist/admin/src/components/ReleaseModal.d.ts +16 -0
  26. package/dist/admin/src/constants.d.ts +58 -0
  27. package/dist/admin/src/index.d.ts +3 -0
  28. package/dist/admin/src/pages/App.d.ts +1 -0
  29. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  30. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  31. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  32. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  33. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  34. package/dist/admin/src/pluginId.d.ts +1 -0
  35. package/dist/admin/src/services/axios.d.ts +29 -0
  36. package/dist/admin/src/services/release.d.ts +369 -0
  37. package/dist/admin/src/store/hooks.d.ts +7 -0
  38. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  39. package/dist/admin/src/utils/time.d.ts +1 -0
  40. package/dist/server/index.js +1014 -417
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/index.mjs +1014 -417
  43. package/dist/server/index.mjs.map +1 -1
  44. package/dist/server/src/bootstrap.d.ts +5 -0
  45. package/dist/server/src/bootstrap.d.ts.map +1 -0
  46. package/dist/server/src/constants.d.ts +12 -0
  47. package/dist/server/src/constants.d.ts.map +1 -0
  48. package/dist/server/src/content-types/index.d.ts +99 -0
  49. package/dist/server/src/content-types/index.d.ts.map +1 -0
  50. package/dist/server/src/content-types/release/index.d.ts +48 -0
  51. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  52. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  53. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  54. package/dist/server/src/content-types/release-action/index.d.ts +50 -0
  55. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  56. package/dist/server/src/content-types/release-action/schema.d.ts +49 -0
  57. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  58. package/dist/server/src/controllers/index.d.ts +19 -0
  59. package/dist/server/src/controllers/index.d.ts.map +1 -0
  60. package/dist/server/src/controllers/release-action.d.ts +10 -0
  61. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  62. package/dist/server/src/controllers/release.d.ts +11 -0
  63. package/dist/server/src/controllers/release.d.ts.map +1 -0
  64. package/dist/server/src/controllers/validation/release-action.d.ts +8 -0
  65. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  66. package/dist/server/src/controllers/validation/release.d.ts +2 -0
  67. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  68. package/dist/server/src/destroy.d.ts +5 -0
  69. package/dist/server/src/destroy.d.ts.map +1 -0
  70. package/dist/server/src/index.d.ts +2095 -0
  71. package/dist/server/src/index.d.ts.map +1 -0
  72. package/dist/server/src/migrations/index.d.ts +13 -0
  73. package/dist/server/src/migrations/index.d.ts.map +1 -0
  74. package/dist/server/src/register.d.ts +5 -0
  75. package/dist/server/src/register.d.ts.map +1 -0
  76. package/dist/server/src/routes/index.d.ts +35 -0
  77. package/dist/server/src/routes/index.d.ts.map +1 -0
  78. package/dist/server/src/routes/release-action.d.ts +18 -0
  79. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  80. package/dist/server/src/routes/release.d.ts +18 -0
  81. package/dist/server/src/routes/release.d.ts.map +1 -0
  82. package/dist/server/src/services/index.d.ts +1826 -0
  83. package/dist/server/src/services/index.d.ts.map +1 -0
  84. package/dist/server/src/services/release.d.ts +66 -0
  85. package/dist/server/src/services/release.d.ts.map +1 -0
  86. package/dist/server/src/services/scheduling.d.ts +18 -0
  87. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  88. package/dist/server/src/services/validation.d.ts +18 -0
  89. package/dist/server/src/services/validation.d.ts.map +1 -0
  90. package/dist/server/src/utils/index.d.ts +14 -0
  91. package/dist/server/src/utils/index.d.ts.map +1 -0
  92. package/dist/shared/contracts/release-actions.d.ts +131 -0
  93. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  94. package/dist/shared/contracts/releases.d.ts +166 -0
  95. package/dist/shared/contracts/releases.d.ts.map +1 -0
  96. package/dist/shared/types.d.ts +24 -0
  97. package/dist/shared/types.d.ts.map +1 -0
  98. package/dist/shared/validation-schemas.d.ts +2 -0
  99. package/dist/shared/validation-schemas.d.ts.map +1 -0
  100. package/package.json +25 -29
  101. package/dist/_chunks/App-_Jj3tWts.mjs +0 -1053
  102. package/dist/_chunks/App-_Jj3tWts.mjs.map +0 -1
  103. package/dist/_chunks/App-iqqoPnBO.js +0 -1075
  104. package/dist/_chunks/App-iqqoPnBO.js.map +0 -1
  105. package/dist/_chunks/en-2DuPv5k0.js.map +0 -1
  106. package/dist/_chunks/en-SOqjCdyh.mjs.map +0 -1
  107. package/dist/_chunks/index-_lT-gI3M.js.map +0 -1
  108. package/dist/_chunks/index-bsuc8ZwZ.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const isEqual = require("lodash/isEqual");
3
4
  const lodash = require("lodash");
4
5
  const _ = require("lodash/fp");
5
- const EE = require("@strapi/strapi/dist/utils/ee");
6
+ const nodeSchedule = require("node-schedule");
6
7
  const yup = require("yup");
7
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
8
9
  function _interopNamespace(e) {
@@ -23,8 +24,8 @@ function _interopNamespace(e) {
23
24
  n.default = e;
24
25
  return Object.freeze(n);
25
26
  }
27
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
26
28
  const ___default = /* @__PURE__ */ _interopDefault(_);
27
- const EE__default = /* @__PURE__ */ _interopDefault(EE);
28
29
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
29
30
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
30
31
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -72,6 +73,35 @@ const ACTIONS = [
72
73
  pluginName: "content-releases"
73
74
  }
74
75
  ];
76
+ const ALLOWED_WEBHOOK_EVENTS = {
77
+ RELEASES_PUBLISH: "releases.publish"
78
+ };
79
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
80
+ return strapi2.plugin("content-releases").service(name);
81
+ };
82
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
83
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
84
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
85
+ const entry = await strapi2.db.query(contentTypeUid).findOne({
86
+ where: { id: entryId },
87
+ populate
88
+ });
89
+ return entry;
90
+ };
91
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
92
+ try {
93
+ await strapi2.entityValidator.validateEntityCreation(
94
+ strapi2.getModel(contentTypeUid),
95
+ entry,
96
+ void 0,
97
+ // @ts-expect-error - FIXME: entity here is unnecessary
98
+ entry
99
+ );
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ };
75
105
  async function deleteActionsOnDisableDraftAndPublish({
76
106
  oldContentTypes,
77
107
  contentTypes: contentTypes2
@@ -93,33 +123,199 @@ async function deleteActionsOnDisableDraftAndPublish({
93
123
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
94
124
  const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
95
125
  if (deletedContentTypes.length) {
96
- await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
126
+ await utils.async.map(deletedContentTypes, async (deletedContentTypeUID) => {
97
127
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
98
128
  });
99
129
  }
100
130
  }
101
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
131
+ async function migrateIsValidAndStatusReleases() {
132
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
133
+ where: {
134
+ status: null,
135
+ releasedAt: null
136
+ },
137
+ populate: {
138
+ actions: {
139
+ populate: {
140
+ entry: true
141
+ }
142
+ }
143
+ }
144
+ });
145
+ utils.async.map(releasesWithoutStatus, async (release2) => {
146
+ const actions = release2.actions;
147
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
148
+ for (const action of notValidatedActions) {
149
+ if (action.entry) {
150
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
151
+ strapi
152
+ });
153
+ if (populatedEntry) {
154
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
155
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
156
+ where: {
157
+ id: action.id
158
+ },
159
+ data: {
160
+ isEntryValid
161
+ }
162
+ });
163
+ }
164
+ }
165
+ }
166
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
167
+ });
168
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
169
+ where: {
170
+ status: null,
171
+ releasedAt: {
172
+ $notNull: true
173
+ }
174
+ }
175
+ });
176
+ utils.async.map(publishedReleases, async (release2) => {
177
+ return strapi.db.query(RELEASE_MODEL_UID).update({
178
+ where: {
179
+ id: release2.id
180
+ },
181
+ data: {
182
+ status: "done"
183
+ }
184
+ });
185
+ });
186
+ }
187
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
188
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
189
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
190
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
191
+ );
192
+ const releasesAffected = /* @__PURE__ */ new Set();
193
+ utils.async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
194
+ const oldContentType = oldContentTypes[contentTypeUID];
195
+ const contentType = contentTypes2[contentTypeUID];
196
+ if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
197
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
198
+ where: {
199
+ contentType: contentTypeUID
200
+ },
201
+ populate: {
202
+ entry: true,
203
+ release: true
204
+ }
205
+ });
206
+ await utils.async.map(actions, async (action) => {
207
+ if (action.entry && action.release) {
208
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
209
+ strapi
210
+ });
211
+ if (populatedEntry) {
212
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
213
+ strapi
214
+ });
215
+ releasesAffected.add(action.release.id);
216
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
217
+ where: {
218
+ id: action.id
219
+ },
220
+ data: {
221
+ isEntryValid
222
+ }
223
+ });
224
+ }
225
+ }
226
+ });
227
+ }
228
+ }).then(() => {
229
+ utils.async.map(releasesAffected, async (releaseId) => {
230
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
231
+ });
232
+ });
233
+ }
234
+ }
235
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
236
+ if (!oldContentTypes) {
237
+ return;
238
+ }
239
+ for (const uid in contentTypes2) {
240
+ if (!oldContentTypes[uid]) {
241
+ continue;
242
+ }
243
+ const oldContentType = oldContentTypes[uid];
244
+ const contentType = contentTypes2[uid];
245
+ const i18nPlugin = strapi.plugin("i18n");
246
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
247
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
248
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
249
+ locale: null
250
+ }).where({ contentType: uid }).execute();
251
+ }
252
+ }
253
+ }
254
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
255
+ if (!oldContentTypes) {
256
+ return;
257
+ }
258
+ for (const uid in contentTypes2) {
259
+ if (!oldContentTypes[uid]) {
260
+ continue;
261
+ }
262
+ const oldContentType = oldContentTypes[uid];
263
+ const contentType = contentTypes2[uid];
264
+ const i18nPlugin = strapi.plugin("i18n");
265
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
266
+ const { getDefaultLocale } = i18nPlugin.service("locales");
267
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
268
+ const defaultLocale = await getDefaultLocale();
269
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
270
+ locale: defaultLocale
271
+ }).where({ contentType: uid }).execute();
272
+ }
273
+ }
274
+ }
102
275
  const register = async ({ strapi: strapi2 }) => {
103
- if (features$2.isEnabled("cms-content-releases")) {
276
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
104
277
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
105
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
106
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
278
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
279
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
280
+ }
281
+ if (strapi2.plugin("graphql")) {
282
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
283
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
284
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
107
285
  }
108
286
  };
109
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
110
287
  const bootstrap = async ({ strapi: strapi2 }) => {
111
- if (features$1.isEnabled("cms-content-releases")) {
288
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
289
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
290
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
291
+ );
112
292
  strapi2.db.lifecycles.subscribe({
113
- afterDelete(event) {
114
- const { model, result } = event;
115
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
116
- const { id } = result;
117
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
118
- where: {
119
- target_type: model.uid,
120
- target_id: id
293
+ models: contentTypesWithDraftAndPublish,
294
+ async afterDelete(event) {
295
+ try {
296
+ const { model, result } = event;
297
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
298
+ const { id } = result;
299
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
300
+ where: {
301
+ actions: {
302
+ target_type: model.uid,
303
+ target_id: id
304
+ }
305
+ }
306
+ });
307
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
308
+ where: {
309
+ target_type: model.uid,
310
+ target_id: id
311
+ }
312
+ });
313
+ for (const release2 of releases) {
314
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
121
315
  }
122
- });
316
+ }
317
+ } catch (error) {
318
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
123
319
  }
124
320
  },
125
321
  /**
@@ -139,20 +335,88 @@ const bootstrap = async ({ strapi: strapi2 }) => {
139
335
  * We make this only after deleteMany is succesfully executed to avoid errors
140
336
  */
141
337
  async afterDeleteMany(event) {
142
- const { model, state } = event;
143
- const entriesToDelete = state.entriesToDelete;
144
- if (entriesToDelete) {
145
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
146
- where: {
147
- target_type: model.uid,
148
- target_id: {
149
- $in: entriesToDelete.map((entry) => entry.id)
338
+ try {
339
+ const { model, state } = event;
340
+ const entriesToDelete = state.entriesToDelete;
341
+ if (entriesToDelete) {
342
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
343
+ where: {
344
+ actions: {
345
+ target_type: model.uid,
346
+ target_id: {
347
+ $in: entriesToDelete.map((entry) => entry.id)
348
+ }
349
+ }
350
+ }
351
+ });
352
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
353
+ where: {
354
+ target_type: model.uid,
355
+ target_id: {
356
+ $in: entriesToDelete.map((entry) => entry.id)
357
+ }
150
358
  }
359
+ });
360
+ for (const release2 of releases) {
361
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
151
362
  }
363
+ }
364
+ } catch (error) {
365
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
366
+ error
152
367
  });
153
368
  }
369
+ },
370
+ async afterUpdate(event) {
371
+ try {
372
+ const { model, result } = event;
373
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
374
+ const isEntryValid = await getEntryValidStatus(model.uid, result, {
375
+ strapi: strapi2
376
+ });
377
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
378
+ where: {
379
+ target_type: model.uid,
380
+ target_id: result.id
381
+ },
382
+ data: {
383
+ isEntryValid
384
+ }
385
+ });
386
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
387
+ where: {
388
+ actions: {
389
+ target_type: model.uid,
390
+ target_id: result.id
391
+ }
392
+ }
393
+ });
394
+ for (const release2 of releases) {
395
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
396
+ }
397
+ }
398
+ } catch (error) {
399
+ strapi2.log.error("Error while updating release actions after entry update", { error });
400
+ }
154
401
  }
155
402
  });
403
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
404
+ strapi2.log.error(
405
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
406
+ );
407
+ throw err;
408
+ });
409
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
410
+ strapi2.webhookStore.addAllowedEvent(key, value);
411
+ });
412
+ }
413
+ };
414
+ const destroy = async ({ strapi: strapi2 }) => {
415
+ const scheduledJobs = getService("scheduling", {
416
+ strapi: strapi2
417
+ }).getAll();
418
+ for (const [, job] of scheduledJobs) {
419
+ job.cancel();
156
420
  }
157
421
  };
158
422
  const schema$1 = {
@@ -181,6 +445,17 @@ const schema$1 = {
181
445
  releasedAt: {
182
446
  type: "datetime"
183
447
  },
448
+ scheduledAt: {
449
+ type: "datetime"
450
+ },
451
+ timezone: {
452
+ type: "string"
453
+ },
454
+ status: {
455
+ type: "enumeration",
456
+ enum: ["ready", "blocked", "failed", "done", "empty"],
457
+ required: true
458
+ },
184
459
  actions: {
185
460
  type: "relation",
186
461
  relation: "oneToMany",
@@ -233,6 +508,9 @@ const schema = {
233
508
  relation: "manyToOne",
234
509
  target: RELEASE_MODEL_UID,
235
510
  inversedBy: "actions"
511
+ },
512
+ isEntryValid: {
513
+ type: "boolean"
236
514
  }
237
515
  }
238
516
  };
@@ -243,9 +521,6 @@ const contentTypes = {
243
521
  release: release$1,
244
522
  "release-action": releaseAction$1
245
523
  };
246
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
- return strapi2.plugin("content-releases").service(name);
248
- };
249
524
  const getGroupName = (queryValue) => {
250
525
  switch (queryValue) {
251
526
  case "contentType":
@@ -258,419 +533,594 @@ const getGroupName = (queryValue) => {
258
533
  return "contentType.displayName";
259
534
  }
260
535
  };
261
- const createReleaseService = ({ strapi: strapi2 }) => ({
262
- async create(releaseData, { user }) {
263
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
264
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
- "release-validation",
266
- { strapi: strapi2 }
267
- );
268
- await Promise.all([
269
- validatePendingReleasesLimit(),
270
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
271
- ]);
272
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
273
- data: releaseWithCreatorFields
536
+ const createReleaseService = ({ strapi: strapi2 }) => {
537
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
538
+ strapi2.eventHub.emit(event, {
539
+ isPublished,
540
+ error,
541
+ release: release2
274
542
  });
275
- },
276
- async findOne(id, query = {}) {
277
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
278
- ...query
543
+ };
544
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
545
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
546
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
547
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
548
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
549
+ try {
550
+ if (actionType === "publish") {
551
+ await entityManagerService.publish(entry, uid);
552
+ } else {
553
+ await entityManagerService.unpublish(entry, uid);
554
+ }
555
+ } catch (error) {
556
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
557
+ ;
558
+ else {
559
+ throw error;
560
+ }
561
+ }
562
+ };
563
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
564
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
565
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
566
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
567
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
568
+ filters: {
569
+ id: {
570
+ $in: entriesToPublishIds
571
+ }
572
+ },
573
+ populate
279
574
  });
280
- return release2;
281
- },
282
- findPage(query) {
283
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
284
- ...query,
285
- populate: {
286
- actions: {
287
- // @ts-expect-error Ignore missing properties
288
- count: true
575
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
576
+ filters: {
577
+ id: {
578
+ $in: entriestoUnpublishIds
289
579
  }
290
- }
580
+ },
581
+ populate
291
582
  });
292
- },
293
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
294
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
583
+ if (entriesToPublish.length > 0) {
584
+ await entityManagerService.publishMany(entriesToPublish, uid);
585
+ }
586
+ if (entriesToUnpublish.length > 0) {
587
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
588
+ }
589
+ };
590
+ const getFormattedActions = async (releaseId) => {
591
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
295
592
  where: {
296
- actions: {
297
- target_type: contentTypeUid,
298
- target_id: entryId
299
- },
300
- releasedAt: {
301
- $null: true
593
+ release: {
594
+ id: releaseId
302
595
  }
303
596
  },
304
597
  populate: {
305
- // Filter the action to get only the content type entry
306
- actions: {
307
- where: {
308
- target_type: contentTypeUid,
309
- target_id: entryId
310
- }
598
+ entry: {
599
+ fields: ["id"]
311
600
  }
312
601
  }
313
602
  });
314
- return releases.map((release2) => {
315
- if (release2.actions?.length) {
316
- const [actionForEntry] = release2.actions;
317
- delete release2.actions;
318
- return {
319
- ...release2,
320
- action: actionForEntry
321
- };
603
+ if (actions.length === 0) {
604
+ throw new utils.errors.ValidationError("No entries to publish");
605
+ }
606
+ const collectionTypeActions = {};
607
+ const singleTypeActions = [];
608
+ for (const action of actions) {
609
+ const contentTypeUid = action.contentType;
610
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
611
+ if (!collectionTypeActions[contentTypeUid]) {
612
+ collectionTypeActions[contentTypeUid] = {
613
+ entriesToPublishIds: [],
614
+ entriesToUnpublishIds: []
615
+ };
616
+ }
617
+ if (action.type === "publish") {
618
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
619
+ } else {
620
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
621
+ }
622
+ } else {
623
+ singleTypeActions.push({
624
+ uid: contentTypeUid,
625
+ action: action.type,
626
+ id: action.entry.id
627
+ });
322
628
  }
323
- return release2;
324
- });
325
- },
326
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
327
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
328
- where: {
329
- releasedAt: {
330
- $null: true
331
- },
332
- actions: {
333
- target_type: contentTypeUid,
334
- target_id: entryId
629
+ }
630
+ return { collectionTypeActions, singleTypeActions };
631
+ };
632
+ return {
633
+ async create(releaseData, { user }) {
634
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
635
+ const {
636
+ validatePendingReleasesLimit,
637
+ validateUniqueNameForPendingRelease,
638
+ validateScheduledAtIsLaterThanNow
639
+ } = getService("release-validation", { strapi: strapi2 });
640
+ await Promise.all([
641
+ validatePendingReleasesLimit(),
642
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
643
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
644
+ ]);
645
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
646
+ data: {
647
+ ...releaseWithCreatorFields,
648
+ status: "empty"
335
649
  }
650
+ });
651
+ if (releaseWithCreatorFields.scheduledAt) {
652
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
653
+ await schedulingService.set(release2.id, release2.scheduledAt);
336
654
  }
337
- });
338
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
339
- where: {
340
- $or: [
341
- {
342
- id: {
343
- $notIn: releasesRelated.map((release2) => release2.id)
655
+ strapi2.telemetry.send("didCreateContentRelease");
656
+ return release2;
657
+ },
658
+ async findOne(id, query = {}) {
659
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
660
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
661
+ ...dbQuery,
662
+ where: { id }
663
+ });
664
+ return release2;
665
+ },
666
+ findPage(query) {
667
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
668
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
669
+ ...dbQuery,
670
+ populate: {
671
+ actions: {
672
+ count: true
673
+ }
674
+ }
675
+ });
676
+ },
677
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
678
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
679
+ where: {
680
+ actions: {
681
+ target_type: contentTypeUid,
682
+ target_id: entryId
683
+ },
684
+ releasedAt: {
685
+ $null: true
686
+ }
687
+ },
688
+ populate: {
689
+ // Filter the action to get only the content type entry
690
+ actions: {
691
+ where: {
692
+ target_type: contentTypeUid,
693
+ target_id: entryId
344
694
  }
695
+ }
696
+ }
697
+ });
698
+ return releases.map((release2) => {
699
+ if (release2.actions?.length) {
700
+ const [actionForEntry] = release2.actions;
701
+ delete release2.actions;
702
+ return {
703
+ ...release2,
704
+ action: actionForEntry
705
+ };
706
+ }
707
+ return release2;
708
+ });
709
+ },
710
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
711
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
712
+ where: {
713
+ releasedAt: {
714
+ $null: true
345
715
  },
346
- {
347
- actions: null
716
+ actions: {
717
+ target_type: contentTypeUid,
718
+ target_id: entryId
348
719
  }
349
- ],
350
- releasedAt: {
351
- $null: true
352
720
  }
721
+ });
722
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
723
+ where: {
724
+ $or: [
725
+ {
726
+ id: {
727
+ $notIn: releasesRelated.map((release2) => release2.id)
728
+ }
729
+ },
730
+ {
731
+ actions: null
732
+ }
733
+ ],
734
+ releasedAt: {
735
+ $null: true
736
+ }
737
+ }
738
+ });
739
+ return releases.map((release2) => {
740
+ if (release2.actions?.length) {
741
+ const [actionForEntry] = release2.actions;
742
+ delete release2.actions;
743
+ return {
744
+ ...release2,
745
+ action: actionForEntry
746
+ };
747
+ }
748
+ return release2;
749
+ });
750
+ },
751
+ async update(id, releaseData, { user }) {
752
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
753
+ releaseData
754
+ );
755
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
756
+ "release-validation",
757
+ { strapi: strapi2 }
758
+ );
759
+ await Promise.all([
760
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
761
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
762
+ ]);
763
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
764
+ if (!release2) {
765
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
353
766
  }
354
- });
355
- return releases.map((release2) => {
356
- if (release2.actions?.length) {
357
- const [actionForEntry] = release2.actions;
358
- delete release2.actions;
359
- return {
360
- ...release2,
361
- action: actionForEntry
362
- };
767
+ if (release2.releasedAt) {
768
+ throw new utils.errors.ValidationError("Release already published");
363
769
  }
364
- return release2;
365
- });
366
- },
367
- async update(id, releaseData, { user }) {
368
- const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
369
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
370
- if (!release2) {
371
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
372
- }
373
- if (release2.releasedAt) {
374
- throw new utils.errors.ValidationError("Release already published");
375
- }
376
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
377
- /*
378
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
379
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
380
- */
381
- // @ts-expect-error see above
382
- data: releaseWithCreatorFields
383
- });
384
- return updatedRelease;
385
- },
386
- async createAction(releaseId, action) {
387
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
388
- strapi: strapi2
389
- });
390
- await Promise.all([
391
- validateEntryContentType(action.entry.contentType),
392
- validateUniqueEntry(releaseId, action)
393
- ]);
394
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
395
- if (!release2) {
396
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
397
- }
398
- if (release2.releasedAt) {
399
- throw new utils.errors.ValidationError("Release already published");
400
- }
401
- const { entry, type } = action;
402
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
403
- data: {
404
- type,
405
- contentType: entry.contentType,
406
- locale: entry.locale,
407
- entry: {
408
- id: entry.id,
409
- __type: entry.contentType,
410
- __pivot: { field: "entry" }
411
- },
412
- release: releaseId
413
- },
414
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
415
- });
416
- },
417
- async findActions(releaseId, query) {
418
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
419
- fields: ["id"]
420
- });
421
- if (!release2) {
422
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
423
- }
424
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
425
- ...query,
426
- populate: {
427
- entry: {
428
- populate: "*"
429
- }
430
- },
431
- filters: {
432
- release: releaseId
770
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
771
+ where: { id },
772
+ data: releaseWithCreatorFields
773
+ });
774
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
775
+ if (releaseData.scheduledAt) {
776
+ await schedulingService.set(id, releaseData.scheduledAt);
777
+ } else if (release2.scheduledAt) {
778
+ schedulingService.cancel(id);
433
779
  }
434
- });
435
- },
436
- async countActions(query) {
437
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
438
- },
439
- async groupActions(actions, groupBy) {
440
- const contentTypeUids = actions.reduce((acc, action) => {
441
- if (!acc.includes(action.contentType)) {
442
- acc.push(action.contentType);
780
+ this.updateReleaseStatus(id);
781
+ strapi2.telemetry.send("didUpdateContentRelease");
782
+ return updatedRelease;
783
+ },
784
+ async createAction(releaseId, action) {
785
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
786
+ strapi: strapi2
787
+ });
788
+ await Promise.all([
789
+ validateEntryContentType(action.entry.contentType),
790
+ validateUniqueEntry(releaseId, action)
791
+ ]);
792
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
793
+ if (!release2) {
794
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
443
795
  }
444
- return acc;
445
- }, []);
446
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
447
- contentTypeUids
448
- );
449
- const allLocalesDictionary = await this.getLocalesDataForActions();
450
- const formattedData = actions.map((action) => {
451
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
452
- return {
453
- ...action,
454
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
455
- contentType: {
456
- displayName,
457
- mainFieldValue: action.entry[mainField],
458
- uid: action.contentType
459
- }
460
- };
461
- });
462
- const groupName = getGroupName(groupBy);
463
- return ___default.default.groupBy(groupName)(formattedData);
464
- },
465
- async getLocalesDataForActions() {
466
- if (!strapi2.plugin("i18n")) {
467
- return {};
468
- }
469
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
470
- return allLocales.reduce((acc, locale) => {
471
- acc[locale.code] = { name: locale.name, code: locale.code };
472
- return acc;
473
- }, {});
474
- },
475
- async getContentTypesDataForActions(contentTypesUids) {
476
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
477
- const contentTypesData = {};
478
- for (const contentTypeUid of contentTypesUids) {
479
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
480
- uid: contentTypeUid
796
+ if (release2.releasedAt) {
797
+ throw new utils.errors.ValidationError("Release already published");
798
+ }
799
+ const { entry, type } = action;
800
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
801
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
802
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
803
+ data: {
804
+ type,
805
+ contentType: entry.contentType,
806
+ locale: entry.locale,
807
+ isEntryValid,
808
+ entry: {
809
+ id: entry.id,
810
+ __type: entry.contentType,
811
+ __pivot: { field: "entry" }
812
+ },
813
+ release: releaseId
814
+ },
815
+ populate: { release: { select: ["id"] }, entry: { select: ["id"] } }
481
816
  });
482
- contentTypesData[contentTypeUid] = {
483
- mainField: contentTypeConfig.settings.mainField,
484
- displayName: strapi2.getModel(contentTypeUid).info.displayName
485
- };
486
- }
487
- return contentTypesData;
488
- },
489
- getContentTypeModelsFromActions(actions) {
490
- const contentTypeUids = actions.reduce((acc, action) => {
491
- if (!acc.includes(action.contentType)) {
492
- acc.push(action.contentType);
817
+ this.updateReleaseStatus(releaseId);
818
+ return releaseAction2;
819
+ },
820
+ async findActions(releaseId, query) {
821
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
822
+ where: { id: releaseId },
823
+ select: ["id"]
824
+ });
825
+ if (!release2) {
826
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
493
827
  }
494
- return acc;
495
- }, []);
496
- const contentTypeModelsMap = contentTypeUids.reduce(
497
- (acc, contentTypeUid) => {
498
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
828
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
829
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
830
+ ...dbQuery,
831
+ populate: {
832
+ entry: {
833
+ populate: "*"
834
+ }
835
+ },
836
+ where: {
837
+ release: releaseId
838
+ }
839
+ });
840
+ },
841
+ async countActions(query) {
842
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
843
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
844
+ },
845
+ async groupActions(actions, groupBy) {
846
+ const contentTypeUids = actions.reduce((acc, action) => {
847
+ if (!acc.includes(action.contentType)) {
848
+ acc.push(action.contentType);
849
+ }
499
850
  return acc;
500
- },
501
- {}
502
- );
503
- return contentTypeModelsMap;
504
- },
505
- async getAllComponents() {
506
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
507
- const components = await contentManagerComponentsService.findAllComponents();
508
- const componentsMap = components.reduce(
509
- (acc, component) => {
510
- acc[component.uid] = component;
851
+ }, []);
852
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
853
+ contentTypeUids
854
+ );
855
+ const allLocalesDictionary = await this.getLocalesDataForActions();
856
+ const formattedData = actions.map((action) => {
857
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
858
+ return {
859
+ ...action,
860
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
861
+ contentType: {
862
+ displayName,
863
+ mainFieldValue: action.entry[mainField],
864
+ uid: action.contentType
865
+ }
866
+ };
867
+ });
868
+ const groupName = getGroupName(groupBy);
869
+ return ___default.default.groupBy(groupName)(formattedData);
870
+ },
871
+ async getLocalesDataForActions() {
872
+ if (!strapi2.plugin("i18n")) {
873
+ return {};
874
+ }
875
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
876
+ return allLocales.reduce((acc, locale) => {
877
+ acc[locale.code] = { name: locale.name, code: locale.code };
511
878
  return acc;
512
- },
513
- {}
514
- );
515
- return componentsMap;
516
- },
517
- async delete(releaseId) {
518
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
519
- populate: {
520
- actions: {
521
- fields: ["id"]
522
- }
879
+ }, {});
880
+ },
881
+ async getContentTypesDataForActions(contentTypesUids) {
882
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
883
+ const contentTypesData = {};
884
+ for (const contentTypeUid of contentTypesUids) {
885
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
886
+ uid: contentTypeUid
887
+ });
888
+ contentTypesData[contentTypeUid] = {
889
+ mainField: contentTypeConfig.settings.mainField,
890
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
891
+ };
523
892
  }
524
- });
525
- if (!release2) {
526
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
527
- }
528
- if (release2.releasedAt) {
529
- throw new utils.errors.ValidationError("Release already published");
530
- }
531
- await strapi2.db.transaction(async () => {
532
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
533
- where: {
534
- id: {
535
- $in: release2.actions.map((action) => action.id)
536
- }
893
+ return contentTypesData;
894
+ },
895
+ getContentTypeModelsFromActions(actions) {
896
+ const contentTypeUids = actions.reduce((acc, action) => {
897
+ if (!acc.includes(action.contentType)) {
898
+ acc.push(action.contentType);
537
899
  }
538
- });
539
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
540
- });
541
- return release2;
542
- },
543
- async publish(releaseId) {
544
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
545
- RELEASE_MODEL_UID,
546
- releaseId,
547
- {
900
+ return acc;
901
+ }, []);
902
+ const contentTypeModelsMap = contentTypeUids.reduce(
903
+ (acc, contentTypeUid) => {
904
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
905
+ return acc;
906
+ },
907
+ {}
908
+ );
909
+ return contentTypeModelsMap;
910
+ },
911
+ async getAllComponents() {
912
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
913
+ const components = await contentManagerComponentsService.findAllComponents();
914
+ const componentsMap = components.reduce(
915
+ (acc, component) => {
916
+ acc[component.uid] = component;
917
+ return acc;
918
+ },
919
+ {}
920
+ );
921
+ return componentsMap;
922
+ },
923
+ async delete(releaseId) {
924
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
925
+ where: { id: releaseId },
548
926
  populate: {
549
927
  actions: {
550
- populate: {
551
- entry: {
552
- fields: ["id"]
553
- }
554
- }
928
+ select: ["id"]
555
929
  }
556
930
  }
931
+ });
932
+ if (!release2) {
933
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
557
934
  }
558
- );
559
- if (!releaseWithPopulatedActionEntries) {
560
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
561
- }
562
- if (releaseWithPopulatedActionEntries.releasedAt) {
563
- throw new utils.errors.ValidationError("Release already published");
564
- }
565
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
566
- throw new utils.errors.ValidationError("No entries to publish");
567
- }
568
- const actions = {};
569
- for (const action of releaseWithPopulatedActionEntries.actions) {
570
- const contentTypeUid = action.contentType;
571
- if (!actions[contentTypeUid]) {
572
- actions[contentTypeUid] = {
573
- entriestoPublishIds: [],
574
- entriesToUnpublishIds: []
575
- };
576
- }
577
- if (action.type === "publish") {
578
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
579
- } else {
580
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
935
+ if (release2.releasedAt) {
936
+ throw new utils.errors.ValidationError("Release already published");
581
937
  }
582
- }
583
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
585
- await strapi2.db.transaction(async () => {
586
- for (const contentTypeUid of Object.keys(actions)) {
587
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
589
- const entriesToPublish = await strapi2.entityService.findMany(
590
- contentTypeUid,
591
- {
592
- filters: {
593
- id: {
594
- $in: entriestoPublishIds
595
- }
596
- },
597
- populate
938
+ await strapi2.db.transaction(async () => {
939
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
940
+ where: {
941
+ id: {
942
+ $in: release2.actions.map((action) => action.id)
943
+ }
598
944
  }
599
- );
600
- const entriesToUnpublish = await strapi2.entityService.findMany(
601
- contentTypeUid,
602
- {
603
- filters: {
604
- id: {
605
- $in: entriesToUnpublishIds
606
- }
607
- },
608
- populate
945
+ });
946
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
947
+ where: {
948
+ id: releaseId
609
949
  }
610
- );
611
- if (entriesToPublish.length > 0) {
612
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
950
+ });
951
+ });
952
+ if (release2.scheduledAt) {
953
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
954
+ await schedulingService.cancel(release2.id);
955
+ }
956
+ strapi2.telemetry.send("didDeleteContentRelease");
957
+ return release2;
958
+ },
959
+ async publish(releaseId) {
960
+ const {
961
+ release: release2,
962
+ error
963
+ } = await strapi2.db.transaction(async ({ trx }) => {
964
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
965
+ if (!lockedRelease) {
966
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
613
967
  }
614
- if (entriesToUnpublish.length > 0) {
615
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
968
+ if (lockedRelease.releasedAt) {
969
+ throw new utils.errors.ValidationError("Release already published");
616
970
  }
971
+ if (lockedRelease.status === "failed") {
972
+ throw new utils.errors.ValidationError("Release failed to publish");
973
+ }
974
+ try {
975
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
976
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
977
+ releaseId
978
+ );
979
+ await strapi2.db.transaction(async () => {
980
+ for (const { uid, action, id } of singleTypeActions) {
981
+ await publishSingleTypeAction(uid, action, id);
982
+ }
983
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
984
+ const uid = contentTypeUid;
985
+ await publishCollectionTypeAction(
986
+ uid,
987
+ collectionTypeActions[uid].entriesToPublishIds,
988
+ collectionTypeActions[uid].entriesToUnpublishIds
989
+ );
990
+ }
991
+ });
992
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
993
+ where: {
994
+ id: releaseId
995
+ },
996
+ data: {
997
+ status: "done",
998
+ releasedAt: /* @__PURE__ */ new Date()
999
+ }
1000
+ });
1001
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1002
+ isPublished: true,
1003
+ release: release22
1004
+ });
1005
+ strapi2.telemetry.send("didPublishContentRelease");
1006
+ return { release: release22, error: null };
1007
+ } catch (error2) {
1008
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1009
+ isPublished: false,
1010
+ error: error2
1011
+ });
1012
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1013
+ status: "failed"
1014
+ }).transacting(trx).execute();
1015
+ return {
1016
+ release: null,
1017
+ error: error2
1018
+ };
1019
+ }
1020
+ });
1021
+ if (error instanceof Error) {
1022
+ throw error;
617
1023
  }
618
- });
619
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
620
- data: {
621
- /*
622
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
623
- */
624
- // @ts-expect-error see above
625
- releasedAt: /* @__PURE__ */ new Date()
1024
+ return release2;
1025
+ },
1026
+ async updateAction(actionId, releaseId, update) {
1027
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1028
+ where: {
1029
+ id: actionId,
1030
+ release: {
1031
+ id: releaseId,
1032
+ releasedAt: {
1033
+ $null: true
1034
+ }
1035
+ }
1036
+ },
1037
+ data: update
1038
+ });
1039
+ if (!updatedAction) {
1040
+ throw new utils.errors.NotFoundError(
1041
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1042
+ );
626
1043
  }
627
- });
628
- return release2;
629
- },
630
- async updateAction(actionId, releaseId, update) {
631
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
632
- where: {
633
- id: actionId,
634
- release: {
635
- id: releaseId,
636
- releasedAt: {
637
- $null: true
1044
+ return updatedAction;
1045
+ },
1046
+ async deleteAction(actionId, releaseId) {
1047
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1048
+ where: {
1049
+ id: actionId,
1050
+ release: {
1051
+ id: releaseId,
1052
+ releasedAt: {
1053
+ $null: true
1054
+ }
638
1055
  }
639
1056
  }
640
- },
641
- data: update
642
- });
643
- if (!updatedAction) {
644
- throw new utils.errors.NotFoundError(
645
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
646
- );
647
- }
648
- return updatedAction;
649
- },
650
- async deleteAction(actionId, releaseId) {
651
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
652
- where: {
653
- id: actionId,
654
- release: {
655
- id: releaseId,
656
- releasedAt: {
657
- $null: true
1057
+ });
1058
+ if (!deletedAction) {
1059
+ throw new utils.errors.NotFoundError(
1060
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1061
+ );
1062
+ }
1063
+ this.updateReleaseStatus(releaseId);
1064
+ return deletedAction;
1065
+ },
1066
+ async updateReleaseStatus(releaseId) {
1067
+ const [totalActions, invalidActions] = await Promise.all([
1068
+ this.countActions({
1069
+ filters: {
1070
+ release: releaseId
658
1071
  }
1072
+ }),
1073
+ this.countActions({
1074
+ filters: {
1075
+ release: releaseId,
1076
+ isEntryValid: false
1077
+ }
1078
+ })
1079
+ ]);
1080
+ if (totalActions > 0) {
1081
+ if (invalidActions > 0) {
1082
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1083
+ where: {
1084
+ id: releaseId
1085
+ },
1086
+ data: {
1087
+ status: "blocked"
1088
+ }
1089
+ });
659
1090
  }
1091
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1092
+ where: {
1093
+ id: releaseId
1094
+ },
1095
+ data: {
1096
+ status: "ready"
1097
+ }
1098
+ });
660
1099
  }
661
- });
662
- if (!deletedAction) {
663
- throw new utils.errors.NotFoundError(
664
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
665
- );
1100
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1101
+ where: {
1102
+ id: releaseId
1103
+ },
1104
+ data: {
1105
+ status: "empty"
1106
+ }
1107
+ });
666
1108
  }
667
- return deletedAction;
1109
+ };
1110
+ };
1111
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1112
+ constructor(message) {
1113
+ super(message);
1114
+ this.name = "AlreadyOnReleaseError";
668
1115
  }
669
- });
1116
+ }
670
1117
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
671
1118
  async validateUniqueEntry(releaseId, releaseActionArgs) {
672
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
673
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1119
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1120
+ where: {
1121
+ id: releaseId
1122
+ },
1123
+ populate: { actions: { populate: { entry: { select: ["id"] } } } }
674
1124
  });
675
1125
  if (!release2) {
676
1126
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
@@ -679,7 +1129,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
679
1129
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
680
1130
  );
681
1131
  if (isEntryInRelease) {
682
- throw new utils.errors.ValidationError(
1132
+ throw new AlreadyOnReleaseError(
683
1133
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
684
1134
  );
685
1135
  }
@@ -696,10 +1146,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
696
1146
  }
697
1147
  },
698
1148
  async validatePendingReleasesLimit() {
699
- const maximumPendingReleases = (
700
- // @ts-expect-error - options is not typed into features
701
- EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
702
- );
1149
+ const maximumPendingReleases = strapi2.ee.features.get("cms-content-releases")?.options?.maximumReleases || 3;
703
1150
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
704
1151
  filters: {
705
1152
  releasedAt: {
@@ -711,27 +1158,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
711
1158
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
712
1159
  }
713
1160
  },
714
- async validateUniqueNameForPendingRelease(name) {
715
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
- filters: {
1161
+ async validateUniqueNameForPendingRelease(name, id) {
1162
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1163
+ where: {
717
1164
  releasedAt: {
718
1165
  $null: true
719
1166
  },
720
- name
1167
+ name,
1168
+ ...id && { id: { $ne: id } }
721
1169
  }
722
1170
  });
723
1171
  const isNameUnique = pendingReleases.length === 0;
724
1172
  if (!isNameUnique) {
725
1173
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
1174
  }
1175
+ },
1176
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1177
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1178
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
1179
+ }
727
1180
  }
728
1181
  });
1182
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1183
+ const scheduledJobs = /* @__PURE__ */ new Map();
1184
+ return {
1185
+ async set(releaseId, scheduleDate) {
1186
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1187
+ if (!release2) {
1188
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1189
+ }
1190
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1191
+ try {
1192
+ await getService("release").publish(releaseId);
1193
+ } catch (error) {
1194
+ }
1195
+ this.cancel(releaseId);
1196
+ });
1197
+ if (scheduledJobs.has(releaseId)) {
1198
+ this.cancel(releaseId);
1199
+ }
1200
+ scheduledJobs.set(releaseId, job);
1201
+ return scheduledJobs;
1202
+ },
1203
+ cancel(releaseId) {
1204
+ if (scheduledJobs.has(releaseId)) {
1205
+ scheduledJobs.get(releaseId).cancel();
1206
+ scheduledJobs.delete(releaseId);
1207
+ }
1208
+ return scheduledJobs;
1209
+ },
1210
+ getAll() {
1211
+ return scheduledJobs;
1212
+ },
1213
+ /**
1214
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1215
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1216
+ * This also could be used to sync different Strapi instances in case of a cluster
1217
+ */
1218
+ async syncFromDatabase() {
1219
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1220
+ where: {
1221
+ scheduledAt: {
1222
+ $gte: /* @__PURE__ */ new Date()
1223
+ },
1224
+ releasedAt: null
1225
+ }
1226
+ });
1227
+ for (const release2 of releases) {
1228
+ this.set(release2.id, release2.scheduledAt);
1229
+ }
1230
+ return scheduledJobs;
1231
+ }
1232
+ };
1233
+ };
729
1234
  const services = {
730
1235
  release: createReleaseService,
731
- "release-validation": createReleaseValidationService
1236
+ "release-validation": createReleaseValidationService,
1237
+ scheduling: createSchedulingService
732
1238
  };
733
1239
  const RELEASE_SCHEMA = yup__namespace.object().shape({
734
- name: yup__namespace.string().trim().required()
1240
+ name: yup__namespace.string().trim().required(),
1241
+ scheduledAt: yup__namespace.string().nullable(),
1242
+ isScheduled: yup__namespace.boolean().optional(),
1243
+ time: yup__namespace.string().when("isScheduled", {
1244
+ is: true,
1245
+ then: yup__namespace.string().trim().required(),
1246
+ otherwise: yup__namespace.string().nullable()
1247
+ }),
1248
+ timezone: yup__namespace.string().when("isScheduled", {
1249
+ is: true,
1250
+ then: yup__namespace.string().required().nullable(),
1251
+ otherwise: yup__namespace.string().nullable()
1252
+ }),
1253
+ date: yup__namespace.string().when("isScheduled", {
1254
+ is: true,
1255
+ then: yup__namespace.string().required().nullable(),
1256
+ otherwise: yup__namespace.string().nullable()
1257
+ })
735
1258
  }).required().noUnknown();
736
1259
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
737
1260
  const releaseController = {
@@ -764,26 +1287,30 @@ const releaseController = {
764
1287
  }
765
1288
  };
766
1289
  });
767
- ctx.body = { data, meta: { pagination } };
1290
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1291
+ where: {
1292
+ releasedAt: null
1293
+ }
1294
+ });
1295
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
768
1296
  }
769
1297
  },
770
1298
  async findOne(ctx) {
771
1299
  const id = ctx.params.id;
772
1300
  const releaseService = getService("release", { strapi });
773
1301
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
774
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
775
- ability: ctx.state.userAbility,
776
- model: RELEASE_MODEL_UID
777
- });
778
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1302
+ if (!release2) {
1303
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1304
+ }
779
1305
  const count = await releaseService.countActions({
780
1306
  filters: {
781
1307
  release: id
782
1308
  }
783
1309
  });
784
- if (!release2) {
785
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
786
- }
1310
+ const sanitizedRelease = {
1311
+ ...release2,
1312
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1313
+ };
787
1314
  const data = {
788
1315
  ...sanitizedRelease,
789
1316
  actions: {
@@ -836,8 +1363,27 @@ const releaseController = {
836
1363
  const id = ctx.params.id;
837
1364
  const releaseService = getService("release", { strapi });
838
1365
  const release2 = await releaseService.publish(id, { user });
1366
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1367
+ releaseService.countActions({
1368
+ filters: {
1369
+ release: id,
1370
+ type: "publish"
1371
+ }
1372
+ }),
1373
+ releaseService.countActions({
1374
+ filters: {
1375
+ release: id,
1376
+ type: "unpublish"
1377
+ }
1378
+ })
1379
+ ]);
839
1380
  ctx.body = {
840
- data: release2
1381
+ data: release2,
1382
+ meta: {
1383
+ totalEntries: countPublishActions + countUnpublishActions,
1384
+ totalPublishedEntries: countPublishActions,
1385
+ totalUnpublishedEntries: countUnpublishActions
1386
+ }
841
1387
  };
842
1388
  }
843
1389
  };
@@ -864,6 +1410,38 @@ const releaseActionController = {
864
1410
  data: releaseAction2
865
1411
  };
866
1412
  },
1413
+ async createMany(ctx) {
1414
+ const releaseId = ctx.params.releaseId;
1415
+ const releaseActionsArgs = ctx.request.body;
1416
+ await Promise.all(
1417
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1418
+ );
1419
+ const releaseService = getService("release", { strapi });
1420
+ const releaseActions = await strapi.db.transaction(async () => {
1421
+ const releaseActions2 = await Promise.all(
1422
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1423
+ try {
1424
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1425
+ return action;
1426
+ } catch (error) {
1427
+ if (error instanceof AlreadyOnReleaseError) {
1428
+ return null;
1429
+ }
1430
+ throw error;
1431
+ }
1432
+ })
1433
+ );
1434
+ return releaseActions2;
1435
+ });
1436
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1437
+ ctx.body = {
1438
+ data: newReleaseActions,
1439
+ meta: {
1440
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1441
+ totalEntries: releaseActions.length
1442
+ }
1443
+ };
1444
+ },
867
1445
  async findMany(ctx) {
868
1446
  const releaseId = ctx.params.releaseId;
869
1447
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -887,7 +1465,7 @@ const releaseActionController = {
887
1465
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
888
1466
  return acc;
889
1467
  }, {});
890
- const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1468
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
891
1469
  ...action,
892
1470
  entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
893
1471
  }));
@@ -1049,6 +1627,22 @@ const releaseAction = {
1049
1627
  ]
1050
1628
  }
1051
1629
  },
1630
+ {
1631
+ method: "POST",
1632
+ path: "/:releaseId/actions/bulk",
1633
+ handler: "release-action.createMany",
1634
+ config: {
1635
+ policies: [
1636
+ "admin::isAuthenticatedAdmin",
1637
+ {
1638
+ name: "admin::hasPermissions",
1639
+ config: {
1640
+ actions: ["plugin::content-releases.create-action"]
1641
+ }
1642
+ }
1643
+ ]
1644
+ }
1645
+ },
1052
1646
  {
1053
1647
  method: "GET",
1054
1648
  path: "/:releaseId/actions",
@@ -1103,12 +1697,12 @@ const routes = {
1103
1697
  release,
1104
1698
  "release-action": releaseAction
1105
1699
  };
1106
- const { features } = require("@strapi/strapi/dist/utils/ee");
1107
1700
  const getPlugin = () => {
1108
- if (features.isEnabled("cms-content-releases")) {
1701
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1109
1702
  return {
1110
1703
  register,
1111
1704
  bootstrap,
1705
+ destroy,
1112
1706
  contentTypes,
1113
1707
  services,
1114
1708
  controllers,
@@ -1116,6 +1710,9 @@ const getPlugin = () => {
1116
1710
  };
1117
1711
  }
1118
1712
  return {
1713
+ // Always return register, it handles its own feature check
1714
+ register,
1715
+ // Always return contentTypes to avoid losing data when the feature is disabled
1119
1716
  contentTypes
1120
1717
  };
1121
1718
  };