@strapi/content-releases 0.0.0-experimental.d4cb32ce579e12a4436d68036f2327132fba1309 → 0.0.0-experimental.d53e940834bf72ddc725f1d2fd36dac9abec30cb

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-xAkiD42p.mjs → App-B2R2exNT.mjs} +656 -625
  2. package/dist/_chunks/App-B2R2exNT.mjs.map +1 -0
  3. package/dist/_chunks/{App-OK4Xac-O.js → App-CEwOQkKT.js} +671 -641
  4. package/dist/_chunks/App-CEwOQkKT.js.map +1 -0
  5. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-Be3acS2L.js} +8 -7
  6. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  7. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-_MxP6-Dt.mjs} +9 -8
  8. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  9. package/dist/_chunks/{en-veqvqeEr.mjs → en-B9Ur3VsE.mjs} +14 -5
  10. package/dist/_chunks/en-B9Ur3VsE.mjs.map +1 -0
  11. package/dist/_chunks/{en-r0otWaln.js → en-DtFJ5ViE.js} +14 -5
  12. package/dist/_chunks/en-DtFJ5ViE.js.map +1 -0
  13. package/dist/_chunks/{index-JvA2_26n.js → index-BrWv-zV4.js} +258 -244
  14. package/dist/_chunks/index-BrWv-zV4.js.map +1 -0
  15. package/dist/_chunks/{index-exoiSU3V.mjs → index-DbmynICx.mjs} +264 -248
  16. package/dist/_chunks/index-DbmynICx.mjs.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 +17 -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 +675 -232
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +675 -232
  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 +29 -36
  103. package/dist/_chunks/App-OK4Xac-O.js.map +0 -1
  104. package/dist/_chunks/App-xAkiD42p.mjs.map +0 -1
  105. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  106. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  107. package/dist/_chunks/en-r0otWaln.js.map +0 -1
  108. package/dist/_chunks/en-veqvqeEr.mjs.map +0 -1
  109. package/dist/_chunks/index-JvA2_26n.js.map +0 -1
  110. package/dist/_chunks/index-exoiSU3V.mjs.map +0 -1
@@ -1,8 +1,8 @@
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
6
  const nodeSchedule = require("node-schedule");
7
7
  const yup = require("yup");
8
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -24,8 +24,8 @@ function _interopNamespace(e) {
24
24
  n.default = e;
25
25
  return Object.freeze(n);
26
26
  }
27
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
27
28
  const ___default = /* @__PURE__ */ _interopDefault(_);
28
- const EE__default = /* @__PURE__ */ _interopDefault(EE);
29
29
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
30
30
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
31
31
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -76,6 +76,32 @@ const ACTIONS = [
76
76
  const ALLOWED_WEBHOOK_EVENTS = {
77
77
  RELEASES_PUBLISH: "releases.publish"
78
78
  };
79
+ const getService = (name, { strapi: strapi2 }) => {
80
+ return strapi2.plugin("content-releases").service(name);
81
+ };
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
+ };
79
105
  async function deleteActionsOnDisableDraftAndPublish({
80
106
  oldContentTypes,
81
107
  contentTypes: contentTypes2
@@ -97,36 +123,205 @@ async function deleteActionsOnDisableDraftAndPublish({
97
123
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
98
124
  const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
99
125
  if (deletedContentTypes.length) {
100
- await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
126
+ await utils.async.map(deletedContentTypes, async (deletedContentTypeUID) => {
101
127
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
102
128
  });
103
129
  }
104
130
  }
105
- 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
+ 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
+ }
106
281
  const register = async ({ strapi: strapi2 }) => {
107
- if (features$2.isEnabled("cms-content-releases")) {
108
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
109
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
110
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
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();
111
291
  }
112
292
  };
113
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
114
- return strapi2.plugin("content-releases").service(name);
115
- };
116
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
117
293
  const bootstrap = async ({ strapi: strapi2 }) => {
118
- 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
+ );
119
298
  strapi2.db.lifecycles.subscribe({
120
- afterDelete(event) {
121
- const { model, result } = event;
122
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
123
- const { id } = result;
124
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
125
- where: {
126
- target_type: model.uid,
127
- target_id: id
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);
128
321
  }
129
- });
322
+ }
323
+ } catch (error) {
324
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
130
325
  }
131
326
  },
132
327
  /**
@@ -146,41 +341,88 @@ const bootstrap = async ({ strapi: strapi2 }) => {
146
341
  * We make this only after deleteMany is succesfully executed to avoid errors
147
342
  */
148
343
  async afterDeleteMany(event) {
149
- const { model, state } = event;
150
- const entriesToDelete = state.entriesToDelete;
151
- if (entriesToDelete) {
152
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
153
- where: {
154
- target_type: model.uid,
155
- target_id: {
156
- $in: entriesToDelete.map((entry) => entry.id)
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
+ }
157
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
+ }
364
+ }
365
+ });
366
+ for (const release2 of releases) {
367
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
158
368
  }
369
+ }
370
+ } catch (error) {
371
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
372
+ error
159
373
  });
160
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
+ }
161
407
  }
162
408
  });
163
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
164
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
165
- strapi2.log.error(
166
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
167
- );
168
- throw err;
169
- });
170
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
171
- strapi2.webhookStore.addAllowedEvent(key, value);
172
- });
173
- }
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
+ });
174
418
  }
175
419
  };
176
420
  const destroy = async ({ strapi: strapi2 }) => {
177
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
178
- const scheduledJobs = getService("scheduling", {
179
- strapi: strapi2
180
- }).getAll();
181
- for (const [, job] of scheduledJobs) {
182
- job.cancel();
183
- }
421
+ const scheduledJobs = getService("scheduling", {
422
+ strapi: strapi2
423
+ }).getAll();
424
+ for (const [, job] of scheduledJobs) {
425
+ job.cancel();
184
426
  }
185
427
  };
186
428
  const schema$1 = {
@@ -215,6 +457,11 @@ const schema$1 = {
215
457
  timezone: {
216
458
  type: "string"
217
459
  },
460
+ status: {
461
+ type: "enumeration",
462
+ enum: ["ready", "blocked", "failed", "done", "empty"],
463
+ required: true
464
+ },
218
465
  actions: {
219
466
  type: "relation",
220
467
  relation: "oneToMany",
@@ -267,6 +514,9 @@ const schema = {
267
514
  relation: "manyToOne",
268
515
  target: RELEASE_MODEL_UID,
269
516
  inversedBy: "actions"
517
+ },
518
+ isEntryValid: {
519
+ type: "boolean"
270
520
  }
271
521
  }
272
522
  };
@@ -297,6 +547,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
297
547
  release: release2
298
548
  });
299
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);
560
+ }
561
+ } catch (error) {
562
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
563
+ ;
564
+ else {
565
+ throw error;
566
+ }
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
577
+ }
578
+ },
579
+ populate
580
+ });
581
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
582
+ filters: {
583
+ id: {
584
+ $in: entriestoUnpublishIds
585
+ }
586
+ },
587
+ populate
588
+ });
589
+ if (entriesToPublish.length > 0) {
590
+ await entityManagerService.publishMany(entriesToPublish, uid);
591
+ }
592
+ if (entriesToUnpublish.length > 0) {
593
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
594
+ }
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
+ }
602
+ },
603
+ populate: {
604
+ entry: {
605
+ fields: ["id"]
606
+ }
607
+ }
608
+ });
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
+ };
622
+ }
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
+ }
635
+ }
636
+ return { collectionTypeActions, singleTypeActions };
637
+ };
300
638
  return {
301
639
  async create(releaseData, { user }) {
302
640
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -310,10 +648,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
310
648
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
311
649
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
650
  ]);
313
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
- data: releaseWithCreatorFields
651
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
652
+ data: {
653
+ ...releaseWithCreatorFields,
654
+ status: "empty"
655
+ }
315
656
  });
316
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
657
+ if (releaseWithCreatorFields.scheduledAt) {
317
658
  const schedulingService = getService("scheduling", { strapi: strapi2 });
318
659
  await schedulingService.set(release2.id, release2.scheduledAt);
319
660
  }
@@ -321,28 +662,36 @@ const createReleaseService = ({ strapi: strapi2 }) => {
321
662
  return release2;
322
663
  },
323
664
  async findOne(id, query = {}) {
324
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
325
- ...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 }
326
669
  });
327
670
  return release2;
328
671
  },
329
672
  findPage(query) {
330
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
331
- ...query,
673
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
674
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
675
+ ...dbQuery,
332
676
  populate: {
333
677
  actions: {
334
- // @ts-expect-error Ignore missing properties
335
678
  count: true
336
679
  }
337
680
  }
338
681
  });
339
682
  },
340
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
683
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
684
+ let entries = entriesIds;
685
+ if (!Array.isArray(entriesIds)) {
686
+ entries = [entriesIds];
687
+ }
341
688
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
342
689
  where: {
343
690
  actions: {
344
691
  target_type: contentTypeUid,
345
- target_id: entryId
692
+ target_id: {
693
+ $in: entries
694
+ }
346
695
  },
347
696
  releasedAt: {
348
697
  $null: true
@@ -353,18 +702,25 @@ const createReleaseService = ({ strapi: strapi2 }) => {
353
702
  actions: {
354
703
  where: {
355
704
  target_type: contentTypeUid,
356
- target_id: entryId
705
+ target_id: {
706
+ $in: entries
707
+ }
708
+ },
709
+ populate: {
710
+ entry: {
711
+ select: ["id"]
712
+ }
357
713
  }
358
714
  }
359
715
  }
360
716
  });
361
717
  return releases.map((release2) => {
362
718
  if (release2.actions?.length) {
363
- const [actionForEntry] = release2.actions;
719
+ const actionsForEntry = release2.actions;
364
720
  delete release2.actions;
365
721
  return {
366
722
  ...release2,
367
- action: actionForEntry
723
+ actions: actionsForEntry
368
724
  };
369
725
  }
370
726
  return release2;
@@ -423,29 +779,24 @@ const createReleaseService = ({ strapi: strapi2 }) => {
423
779
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
424
780
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
425
781
  ]);
426
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
782
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
427
783
  if (!release2) {
428
784
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
429
785
  }
430
786
  if (release2.releasedAt) {
431
787
  throw new utils.errors.ValidationError("Release already published");
432
788
  }
433
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
434
- /*
435
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
436
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
437
- */
438
- // @ts-expect-error see above
789
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
790
+ where: { id },
439
791
  data: releaseWithCreatorFields
440
792
  });
441
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
442
- const schedulingService = getService("scheduling", { strapi: strapi2 });
443
- if (releaseData.scheduledAt) {
444
- await schedulingService.set(id, releaseData.scheduledAt);
445
- } else if (release2.scheduledAt) {
446
- schedulingService.cancel(id);
447
- }
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);
448
798
  }
799
+ this.updateReleaseStatus(id);
449
800
  strapi2.telemetry.send("didUpdateContentRelease");
450
801
  return updatedRelease;
451
802
  },
@@ -457,7 +808,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
457
808
  validateEntryContentType(action.entry.contentType),
458
809
  validateUniqueEntry(releaseId, action)
459
810
  ]);
460
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
811
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
461
812
  if (!release2) {
462
813
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
463
814
  }
@@ -465,11 +816,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
465
816
  throw new utils.errors.ValidationError("Release already published");
466
817
  }
467
818
  const { entry, type } = action;
468
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
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({
469
822
  data: {
470
823
  type,
471
824
  contentType: entry.contentType,
472
825
  locale: entry.locale,
826
+ isEntryValid,
473
827
  entry: {
474
828
  id: entry.id,
475
829
  __type: entry.contentType,
@@ -477,30 +831,35 @@ const createReleaseService = ({ strapi: strapi2 }) => {
477
831
  },
478
832
  release: releaseId
479
833
  },
480
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
834
+ populate: { release: { select: ["id"] }, entry: { select: ["id"] } }
481
835
  });
836
+ this.updateReleaseStatus(releaseId);
837
+ return releaseAction2;
482
838
  },
483
839
  async findActions(releaseId, query) {
484
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
485
- fields: ["id"]
840
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
841
+ where: { id: releaseId },
842
+ select: ["id"]
486
843
  });
487
844
  if (!release2) {
488
845
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
489
846
  }
490
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
491
- ...query,
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,
492
850
  populate: {
493
851
  entry: {
494
852
  populate: "*"
495
853
  }
496
854
  },
497
- filters: {
855
+ where: {
498
856
  release: releaseId
499
857
  }
500
858
  });
501
859
  },
502
860
  async countActions(query) {
503
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, 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);
504
863
  },
505
864
  async groupActions(actions, groupBy) {
506
865
  const contentTypeUids = actions.reduce((acc, action) => {
@@ -509,9 +868,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
509
868
  }
510
869
  return acc;
511
870
  }, []);
512
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
513
- contentTypeUids
514
- );
871
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(contentTypeUids);
515
872
  const allLocalesDictionary = await this.getLocalesDataForActions();
516
873
  const formattedData = actions.map((action) => {
517
874
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
@@ -581,10 +938,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
581
938
  return componentsMap;
582
939
  },
583
940
  async delete(releaseId) {
584
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
941
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
942
+ where: { id: releaseId },
585
943
  populate: {
586
944
  actions: {
587
- fields: ["id"]
945
+ select: ["id"]
588
946
  }
589
947
  }
590
948
  });
@@ -602,9 +960,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
602
960
  }
603
961
  }
604
962
  });
605
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
963
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
964
+ where: {
965
+ id: releaseId
966
+ }
967
+ });
606
968
  });
607
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
969
+ if (release2.scheduledAt) {
608
970
  const schedulingService = getService("scheduling", { strapi: strapi2 });
609
971
  await schedulingService.cancel(release2.id);
610
972
  }
@@ -612,139 +974,69 @@ const createReleaseService = ({ strapi: strapi2 }) => {
612
974
  return release2;
613
975
  },
614
976
  async publish(releaseId) {
615
- try {
616
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
617
- RELEASE_MODEL_UID,
618
- releaseId,
619
- {
620
- populate: {
621
- actions: {
622
- populate: {
623
- entry: {
624
- fields: ["id"]
625
- }
626
- }
627
- }
628
- }
629
- }
630
- );
631
- if (!releaseWithPopulatedActionEntries) {
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) {
632
983
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
633
984
  }
634
- if (releaseWithPopulatedActionEntries.releasedAt) {
985
+ if (lockedRelease.releasedAt) {
635
986
  throw new utils.errors.ValidationError("Release already published");
636
987
  }
637
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
- throw new utils.errors.ValidationError("No entries to publish");
988
+ if (lockedRelease.status === "failed") {
989
+ throw new utils.errors.ValidationError("Release failed to publish");
639
990
  }
640
- const collectionTypeActions = {};
641
- const singleTypeActions = [];
642
- for (const action of releaseWithPopulatedActionEntries.actions) {
643
- const contentTypeUid = action.contentType;
644
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
645
- if (!collectionTypeActions[contentTypeUid]) {
646
- collectionTypeActions[contentTypeUid] = {
647
- entriestoPublishIds: [],
648
- entriesToUnpublishIds: []
649
- };
650
- }
651
- if (action.type === "publish") {
652
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
653
- } else {
654
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
655
- }
656
- } else {
657
- singleTypeActions.push({
658
- uid: contentTypeUid,
659
- action: action.type,
660
- id: action.entry.id
661
- });
662
- }
663
- }
664
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
665
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
666
- await strapi2.db.transaction(async () => {
667
- for (const { uid, action, id } of singleTypeActions) {
668
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
669
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
670
- try {
671
- if (action === "publish") {
672
- await entityManagerService.publish(entry, uid);
673
- } else {
674
- await entityManagerService.unpublish(entry, uid);
675
- }
676
- } catch (error) {
677
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
678
- } else {
679
- throw error;
680
- }
681
- }
682
- }
683
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
684
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
685
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
686
- const entriesToPublish = await strapi2.entityService.findMany(
687
- contentTypeUid,
688
- {
689
- filters: {
690
- id: {
691
- $in: entriestoPublishIds
692
- }
693
- },
694
- populate
695
- }
696
- );
697
- const entriesToUnpublish = await strapi2.entityService.findMany(
698
- contentTypeUid,
699
- {
700
- filters: {
701
- id: {
702
- $in: entriesToUnpublishIds
703
- }
704
- },
705
- populate
706
- }
707
- );
708
- if (entriesToPublish.length > 0) {
709
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
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);
710
997
  }
711
- if (entriesToUnpublish.length > 0) {
712
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
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
+ );
713
1005
  }
714
- }
715
- });
716
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
717
- data: {
718
- /*
719
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
720
- */
721
- // @ts-expect-error see above
722
- releasedAt: /* @__PURE__ */ new Date()
723
- },
724
- populate: {
725
- actions: {
726
- // @ts-expect-error is not expecting count but it is working
727
- count: true
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()
728
1014
  }
729
- }
730
- });
731
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1015
+ });
732
1016
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
733
1017
  isPublished: true,
734
- release: release2
1018
+ release: release22
735
1019
  });
736
- }
737
- strapi2.telemetry.send("didPublishContentRelease");
738
- return release2;
739
- } catch (error) {
740
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1020
+ strapi2.telemetry.send("didPublishContentRelease");
1021
+ return { release: release22, error: null };
1022
+ } catch (error2) {
741
1023
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
742
1024
  isPublished: false,
743
- error
1025
+ error: error2
744
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
+ };
745
1034
  }
1035
+ });
1036
+ if (error instanceof Error) {
746
1037
  throw error;
747
1038
  }
1039
+ return release2;
748
1040
  },
749
1041
  async updateAction(actionId, releaseId, update) {
750
1042
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -783,14 +1075,67 @@ const createReleaseService = ({ strapi: strapi2 }) => {
783
1075
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
1076
  );
785
1077
  }
1078
+ this.updateReleaseStatus(releaseId);
786
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
+ });
787
1123
  }
788
1124
  };
789
1125
  };
1126
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1127
+ constructor(message) {
1128
+ super(message);
1129
+ this.name = "AlreadyOnReleaseError";
1130
+ }
1131
+ }
790
1132
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
791
1133
  async validateUniqueEntry(releaseId, releaseActionArgs) {
792
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
793
- 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"] } } } }
794
1139
  });
795
1140
  if (!release2) {
796
1141
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
@@ -799,7 +1144,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
799
1144
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
800
1145
  );
801
1146
  if (isEntryInRelease) {
802
- throw new utils.errors.ValidationError(
1147
+ throw new AlreadyOnReleaseError(
803
1148
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
804
1149
  );
805
1150
  }
@@ -816,10 +1161,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
816
1161
  }
817
1162
  },
818
1163
  async validatePendingReleasesLimit() {
819
- const maximumPendingReleases = (
820
- // @ts-expect-error - options is not typed into features
821
- EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
822
- );
1164
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1165
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
823
1166
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
824
1167
  filters: {
825
1168
  releasedAt: {
@@ -832,8 +1175,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
832
1175
  }
833
1176
  },
834
1177
  async validateUniqueNameForPendingRelease(name, id) {
835
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
836
- filters: {
1178
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1179
+ where: {
837
1180
  releasedAt: {
838
1181
  $null: true
839
1182
  },
@@ -862,7 +1205,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
862
1205
  }
863
1206
  const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
864
1207
  try {
865
- await getService("release").publish(releaseId);
1208
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
866
1209
  } catch (error) {
867
1210
  }
868
1211
  this.cancel(releaseId);
@@ -907,7 +1250,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
907
1250
  const services = {
908
1251
  release: createReleaseService,
909
1252
  "release-validation": createReleaseValidationService,
910
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1253
+ scheduling: createSchedulingService
911
1254
  };
912
1255
  const RELEASE_SCHEMA = yup__namespace.object().shape({
913
1256
  name: yup__namespace.string().trim().required(),
@@ -932,7 +1275,7 @@ const RELEASE_SCHEMA = yup__namespace.object().shape({
932
1275
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
933
1276
  const releaseController = {
934
1277
  async findMany(ctx) {
935
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1278
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
936
1279
  ability: ctx.state.userAbility,
937
1280
  model: RELEASE_MODEL_UID
938
1281
  });
@@ -960,7 +1303,12 @@ const releaseController = {
960
1303
  }
961
1304
  };
962
1305
  });
963
- 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 } };
964
1312
  }
965
1313
  },
966
1314
  async findOne(ctx) {
@@ -977,7 +1325,7 @@ const releaseController = {
977
1325
  });
978
1326
  const sanitizedRelease = {
979
1327
  ...release2,
980
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1328
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
981
1329
  };
982
1330
  const data = {
983
1331
  ...sanitizedRelease,
@@ -989,19 +1337,48 @@ const releaseController = {
989
1337
  };
990
1338
  ctx.body = { data };
991
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
+ },
992
1369
  async create(ctx) {
993
1370
  const user = ctx.state.user;
994
1371
  const releaseArgs = ctx.request.body;
995
1372
  await validateRelease(releaseArgs);
996
1373
  const releaseService = getService("release", { strapi });
997
1374
  const release2 = await releaseService.create(releaseArgs, { user });
998
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1375
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
999
1376
  ability: ctx.state.userAbility,
1000
1377
  model: RELEASE_MODEL_UID
1001
1378
  });
1002
- ctx.body = {
1379
+ ctx.created({
1003
1380
  data: await permissionsManager.sanitizeOutput(release2)
1004
- };
1381
+ });
1005
1382
  },
1006
1383
  async update(ctx) {
1007
1384
  const user = ctx.state.user;
@@ -1010,7 +1387,7 @@ const releaseController = {
1010
1387
  await validateRelease(releaseArgs);
1011
1388
  const releaseService = getService("release", { strapi });
1012
1389
  const release2 = await releaseService.update(id, releaseArgs, { user });
1013
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1390
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1014
1391
  ability: ctx.state.userAbility,
1015
1392
  model: RELEASE_MODEL_UID
1016
1393
  });
@@ -1074,13 +1451,45 @@ const releaseActionController = {
1074
1451
  await validateReleaseAction(releaseActionArgs);
1075
1452
  const releaseService = getService("release", { strapi });
1076
1453
  const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1077
- ctx.body = {
1454
+ ctx.created({
1078
1455
  data: releaseAction2
1079
- };
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
+ });
1080
1489
  },
1081
1490
  async findMany(ctx) {
1082
1491
  const releaseId = ctx.params.releaseId;
1083
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1492
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1084
1493
  ability: ctx.state.userAbility,
1085
1494
  model: RELEASE_ACTION_MODEL_UID
1086
1495
  });
@@ -1094,14 +1503,14 @@ const releaseActionController = {
1094
1503
  if (acc[action.contentType]) {
1095
1504
  return acc;
1096
1505
  }
1097
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1506
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1098
1507
  ability: ctx.state.userAbility,
1099
1508
  model: action.contentType
1100
1509
  });
1101
1510
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1102
1511
  return acc;
1103
1512
  }, {});
1104
- const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1513
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
1105
1514
  ...action,
1106
1515
  entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1107
1516
  }));
@@ -1146,6 +1555,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1146
1555
  const release = {
1147
1556
  type: "admin",
1148
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
+ },
1149
1574
  {
1150
1575
  method: "POST",
1151
1576
  path: "/",
@@ -1263,6 +1688,22 @@ const releaseAction = {
1263
1688
  ]
1264
1689
  }
1265
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
+ },
1266
1707
  {
1267
1708
  method: "GET",
1268
1709
  path: "/:releaseId/actions",
@@ -1317,9 +1758,8 @@ const routes = {
1317
1758
  release,
1318
1759
  "release-action": releaseAction
1319
1760
  };
1320
- const { features } = require("@strapi/strapi/dist/utils/ee");
1321
1761
  const getPlugin = () => {
1322
- if (features.isEnabled("cms-content-releases")) {
1762
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1323
1763
  return {
1324
1764
  register,
1325
1765
  bootstrap,
@@ -1331,6 +1771,9 @@ const getPlugin = () => {
1331
1771
  };
1332
1772
  }
1333
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
1334
1777
  contentTypes
1335
1778
  };
1336
1779
  };