@strapi/content-releases 0.0.0-experimental.ee4d311a5e6a131fad03cf07e4696f49fdd9c2e6 → 0.0.0-experimental.f75e3c6d67cc47c64ab37479efdbb7b43be50b78

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