@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,7 +1,7 @@
1
- import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, async, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import isEqual from "lodash/isEqual";
2
3
  import { difference, keys } from "lodash";
3
4
  import _ from "lodash/fp";
4
- import EE from "@strapi/strapi/dist/utils/ee";
5
5
  import { scheduleJob } from "node-schedule";
6
6
  import * as yup from "yup";
7
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
@@ -53,6 +53,32 @@ const ACTIONS = [
53
53
  const ALLOWED_WEBHOOK_EVENTS = {
54
54
  RELEASES_PUBLISH: "releases.publish"
55
55
  };
56
+ const getService = (name, { strapi: strapi2 }) => {
57
+ return strapi2.plugin("content-releases").service(name);
58
+ };
59
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 }) => {
60
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
61
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
62
+ const entry = await strapi2.db.query(contentTypeUid).findOne({
63
+ where: { id: entryId },
64
+ populate
65
+ });
66
+ return entry;
67
+ };
68
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 }) => {
69
+ try {
70
+ await strapi2.entityValidator.validateEntityCreation(
71
+ strapi2.getModel(contentTypeUid),
72
+ entry,
73
+ void 0,
74
+ // @ts-expect-error - FIXME: entity here is unnecessary
75
+ entry
76
+ );
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
56
82
  async function deleteActionsOnDisableDraftAndPublish({
57
83
  oldContentTypes,
58
84
  contentTypes: contentTypes2
@@ -74,36 +100,205 @@ async function deleteActionsOnDisableDraftAndPublish({
74
100
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
75
101
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
76
102
  if (deletedContentTypes.length) {
77
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
103
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
78
104
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
79
105
  });
80
106
  }
81
107
  }
82
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
108
+ async function migrateIsValidAndStatusReleases() {
109
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
110
+ where: {
111
+ status: null,
112
+ releasedAt: null
113
+ },
114
+ populate: {
115
+ actions: {
116
+ populate: {
117
+ entry: true
118
+ }
119
+ }
120
+ }
121
+ });
122
+ async.map(releasesWithoutStatus, async (release2) => {
123
+ const actions = release2.actions;
124
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
125
+ for (const action of notValidatedActions) {
126
+ if (action.entry) {
127
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
128
+ strapi
129
+ });
130
+ if (populatedEntry) {
131
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
132
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
133
+ where: {
134
+ id: action.id
135
+ },
136
+ data: {
137
+ isEntryValid
138
+ }
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
144
+ });
145
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
146
+ where: {
147
+ status: null,
148
+ releasedAt: {
149
+ $notNull: true
150
+ }
151
+ }
152
+ });
153
+ async.map(publishedReleases, async (release2) => {
154
+ return strapi.db.query(RELEASE_MODEL_UID).update({
155
+ where: {
156
+ id: release2.id
157
+ },
158
+ data: {
159
+ status: "done"
160
+ }
161
+ });
162
+ });
163
+ }
164
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
165
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
166
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
167
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
168
+ );
169
+ const releasesAffected = /* @__PURE__ */ new Set();
170
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
171
+ const oldContentType = oldContentTypes[contentTypeUID];
172
+ const contentType = contentTypes2[contentTypeUID];
173
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
174
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
175
+ where: {
176
+ contentType: contentTypeUID
177
+ },
178
+ populate: {
179
+ entry: true,
180
+ release: true
181
+ }
182
+ });
183
+ await async.map(actions, async (action) => {
184
+ if (action.entry && action.release) {
185
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
186
+ strapi
187
+ });
188
+ if (populatedEntry) {
189
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
190
+ strapi
191
+ });
192
+ releasesAffected.add(action.release.id);
193
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
194
+ where: {
195
+ id: action.id
196
+ },
197
+ data: {
198
+ isEntryValid
199
+ }
200
+ });
201
+ }
202
+ }
203
+ });
204
+ }
205
+ }).then(() => {
206
+ async.map(releasesAffected, async (releaseId) => {
207
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
208
+ });
209
+ });
210
+ }
211
+ }
212
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
213
+ if (!oldContentTypes) {
214
+ return;
215
+ }
216
+ const i18nPlugin = strapi.plugin("i18n");
217
+ if (!i18nPlugin) {
218
+ return;
219
+ }
220
+ for (const uid in contentTypes2) {
221
+ if (!oldContentTypes[uid]) {
222
+ continue;
223
+ }
224
+ const oldContentType = oldContentTypes[uid];
225
+ const contentType = contentTypes2[uid];
226
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
227
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
228
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
229
+ locale: null
230
+ }).where({ contentType: uid }).execute();
231
+ }
232
+ }
233
+ }
234
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
235
+ if (!oldContentTypes) {
236
+ return;
237
+ }
238
+ const i18nPlugin = strapi.plugin("i18n");
239
+ if (!i18nPlugin) {
240
+ return;
241
+ }
242
+ for (const uid in contentTypes2) {
243
+ if (!oldContentTypes[uid]) {
244
+ continue;
245
+ }
246
+ const oldContentType = oldContentTypes[uid];
247
+ const contentType = contentTypes2[uid];
248
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
249
+ const { getDefaultLocale } = i18nPlugin.service("locales");
250
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
251
+ const defaultLocale = await getDefaultLocale();
252
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
253
+ locale: defaultLocale
254
+ }).where({ contentType: uid }).execute();
255
+ }
256
+ }
257
+ }
83
258
  const register = async ({ strapi: strapi2 }) => {
84
- if (features$2.isEnabled("cms-content-releases")) {
85
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
86
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
87
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
259
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
260
+ await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
261
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
262
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
263
+ }
264
+ if (strapi2.plugin("graphql")) {
265
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
266
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
267
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
88
268
  }
89
269
  };
90
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
- return strapi2.plugin("content-releases").service(name);
92
- };
93
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
94
270
  const bootstrap = async ({ strapi: strapi2 }) => {
95
- if (features$1.isEnabled("cms-content-releases")) {
271
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
272
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
273
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
274
+ );
96
275
  strapi2.db.lifecycles.subscribe({
97
- afterDelete(event) {
98
- const { model, result } = event;
99
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
100
- const { id } = result;
101
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
102
- where: {
103
- target_type: model.uid,
104
- target_id: id
276
+ models: contentTypesWithDraftAndPublish,
277
+ async afterDelete(event) {
278
+ try {
279
+ const { model, result } = event;
280
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
281
+ const { id } = result;
282
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
283
+ where: {
284
+ actions: {
285
+ target_type: model.uid,
286
+ target_id: id
287
+ }
288
+ }
289
+ });
290
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
291
+ where: {
292
+ target_type: model.uid,
293
+ target_id: id
294
+ }
295
+ });
296
+ for (const release2 of releases) {
297
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
105
298
  }
106
- });
299
+ }
300
+ } catch (error) {
301
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
107
302
  }
108
303
  },
109
304
  /**
@@ -123,41 +318,88 @@ const bootstrap = async ({ strapi: strapi2 }) => {
123
318
  * We make this only after deleteMany is succesfully executed to avoid errors
124
319
  */
125
320
  async afterDeleteMany(event) {
126
- const { model, state } = event;
127
- const entriesToDelete = state.entriesToDelete;
128
- if (entriesToDelete) {
129
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
130
- where: {
131
- target_type: model.uid,
132
- target_id: {
133
- $in: entriesToDelete.map((entry) => entry.id)
321
+ try {
322
+ const { model, state } = event;
323
+ const entriesToDelete = state.entriesToDelete;
324
+ if (entriesToDelete) {
325
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
326
+ where: {
327
+ actions: {
328
+ target_type: model.uid,
329
+ target_id: {
330
+ $in: entriesToDelete.map((entry) => entry.id)
331
+ }
332
+ }
134
333
  }
334
+ });
335
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
336
+ where: {
337
+ target_type: model.uid,
338
+ target_id: {
339
+ $in: entriesToDelete.map((entry) => entry.id)
340
+ }
341
+ }
342
+ });
343
+ for (const release2 of releases) {
344
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
135
345
  }
346
+ }
347
+ } catch (error) {
348
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
349
+ error
136
350
  });
137
351
  }
352
+ },
353
+ async afterUpdate(event) {
354
+ try {
355
+ const { model, result } = event;
356
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
357
+ const isEntryValid = await getEntryValidStatus(model.uid, result, {
358
+ strapi: strapi2
359
+ });
360
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
361
+ where: {
362
+ target_type: model.uid,
363
+ target_id: result.id
364
+ },
365
+ data: {
366
+ isEntryValid
367
+ }
368
+ });
369
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
370
+ where: {
371
+ actions: {
372
+ target_type: model.uid,
373
+ target_id: result.id
374
+ }
375
+ }
376
+ });
377
+ for (const release2 of releases) {
378
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
379
+ }
380
+ }
381
+ } catch (error) {
382
+ strapi2.log.error("Error while updating release actions after entry update", { error });
383
+ }
138
384
  }
139
385
  });
140
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
141
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
142
- strapi2.log.error(
143
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
144
- );
145
- throw err;
146
- });
147
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
148
- strapi2.webhookStore.addAllowedEvent(key, value);
149
- });
150
- }
386
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
387
+ strapi2.log.error(
388
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
389
+ );
390
+ throw err;
391
+ });
392
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
393
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
394
+ });
151
395
  }
152
396
  };
153
397
  const destroy = async ({ strapi: strapi2 }) => {
154
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
155
- const scheduledJobs = getService("scheduling", {
156
- strapi: strapi2
157
- }).getAll();
158
- for (const [, job] of scheduledJobs) {
159
- job.cancel();
160
- }
398
+ const scheduledJobs = getService("scheduling", {
399
+ strapi: strapi2
400
+ }).getAll();
401
+ for (const [, job] of scheduledJobs) {
402
+ job.cancel();
161
403
  }
162
404
  };
163
405
  const schema$1 = {
@@ -192,6 +434,11 @@ const schema$1 = {
192
434
  timezone: {
193
435
  type: "string"
194
436
  },
437
+ status: {
438
+ type: "enumeration",
439
+ enum: ["ready", "blocked", "failed", "done", "empty"],
440
+ required: true
441
+ },
195
442
  actions: {
196
443
  type: "relation",
197
444
  relation: "oneToMany",
@@ -244,6 +491,9 @@ const schema = {
244
491
  relation: "manyToOne",
245
492
  target: RELEASE_MODEL_UID,
246
493
  inversedBy: "actions"
494
+ },
495
+ isEntryValid: {
496
+ type: "boolean"
247
497
  }
248
498
  }
249
499
  };
@@ -274,6 +524,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
274
524
  release: release2
275
525
  });
276
526
  };
527
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
528
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
529
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
530
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
531
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
532
+ try {
533
+ if (actionType === "publish") {
534
+ await entityManagerService.publish(entry, uid);
535
+ } else {
536
+ await entityManagerService.unpublish(entry, uid);
537
+ }
538
+ } catch (error) {
539
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
540
+ ;
541
+ else {
542
+ throw error;
543
+ }
544
+ }
545
+ };
546
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
547
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
548
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
549
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
550
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
551
+ filters: {
552
+ id: {
553
+ $in: entriesToPublishIds
554
+ }
555
+ },
556
+ populate
557
+ });
558
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
559
+ filters: {
560
+ id: {
561
+ $in: entriestoUnpublishIds
562
+ }
563
+ },
564
+ populate
565
+ });
566
+ if (entriesToPublish.length > 0) {
567
+ await entityManagerService.publishMany(entriesToPublish, uid);
568
+ }
569
+ if (entriesToUnpublish.length > 0) {
570
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
571
+ }
572
+ };
573
+ const getFormattedActions = async (releaseId) => {
574
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
575
+ where: {
576
+ release: {
577
+ id: releaseId
578
+ }
579
+ },
580
+ populate: {
581
+ entry: {
582
+ fields: ["id"]
583
+ }
584
+ }
585
+ });
586
+ if (actions.length === 0) {
587
+ throw new errors.ValidationError("No entries to publish");
588
+ }
589
+ const collectionTypeActions = {};
590
+ const singleTypeActions = [];
591
+ for (const action of actions) {
592
+ const contentTypeUid = action.contentType;
593
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
594
+ if (!collectionTypeActions[contentTypeUid]) {
595
+ collectionTypeActions[contentTypeUid] = {
596
+ entriesToPublishIds: [],
597
+ entriesToUnpublishIds: []
598
+ };
599
+ }
600
+ if (action.type === "publish") {
601
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
602
+ } else {
603
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
604
+ }
605
+ } else {
606
+ singleTypeActions.push({
607
+ uid: contentTypeUid,
608
+ action: action.type,
609
+ id: action.entry.id
610
+ });
611
+ }
612
+ }
613
+ return { collectionTypeActions, singleTypeActions };
614
+ };
277
615
  return {
278
616
  async create(releaseData, { user }) {
279
617
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@@ -287,10 +625,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
287
625
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
288
626
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
627
  ]);
290
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
- data: releaseWithCreatorFields
628
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
629
+ data: {
630
+ ...releaseWithCreatorFields,
631
+ status: "empty"
632
+ }
292
633
  });
293
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
634
+ if (releaseWithCreatorFields.scheduledAt) {
294
635
  const schedulingService = getService("scheduling", { strapi: strapi2 });
295
636
  await schedulingService.set(release2.id, release2.scheduledAt);
296
637
  }
@@ -298,28 +639,36 @@ const createReleaseService = ({ strapi: strapi2 }) => {
298
639
  return release2;
299
640
  },
300
641
  async findOne(id, query = {}) {
301
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
302
- ...query
642
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
643
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
644
+ ...dbQuery,
645
+ where: { id }
303
646
  });
304
647
  return release2;
305
648
  },
306
649
  findPage(query) {
307
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
308
- ...query,
650
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
651
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
652
+ ...dbQuery,
309
653
  populate: {
310
654
  actions: {
311
- // @ts-expect-error Ignore missing properties
312
655
  count: true
313
656
  }
314
657
  }
315
658
  });
316
659
  },
317
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
660
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
661
+ let entries = entriesIds;
662
+ if (!Array.isArray(entriesIds)) {
663
+ entries = [entriesIds];
664
+ }
318
665
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
319
666
  where: {
320
667
  actions: {
321
668
  target_type: contentTypeUid,
322
- target_id: entryId
669
+ target_id: {
670
+ $in: entries
671
+ }
323
672
  },
324
673
  releasedAt: {
325
674
  $null: true
@@ -330,18 +679,25 @@ const createReleaseService = ({ strapi: strapi2 }) => {
330
679
  actions: {
331
680
  where: {
332
681
  target_type: contentTypeUid,
333
- target_id: entryId
682
+ target_id: {
683
+ $in: entries
684
+ }
685
+ },
686
+ populate: {
687
+ entry: {
688
+ select: ["id"]
689
+ }
334
690
  }
335
691
  }
336
692
  }
337
693
  });
338
694
  return releases.map((release2) => {
339
695
  if (release2.actions?.length) {
340
- const [actionForEntry] = release2.actions;
696
+ const actionsForEntry = release2.actions;
341
697
  delete release2.actions;
342
698
  return {
343
699
  ...release2,
344
- action: actionForEntry
700
+ actions: actionsForEntry
345
701
  };
346
702
  }
347
703
  return release2;
@@ -400,29 +756,24 @@ const createReleaseService = ({ strapi: strapi2 }) => {
400
756
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
401
757
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
402
758
  ]);
403
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
759
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
404
760
  if (!release2) {
405
761
  throw new errors.NotFoundError(`No release found for id ${id}`);
406
762
  }
407
763
  if (release2.releasedAt) {
408
764
  throw new errors.ValidationError("Release already published");
409
765
  }
410
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
411
- /*
412
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
413
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
414
- */
415
- // @ts-expect-error see above
766
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
767
+ where: { id },
416
768
  data: releaseWithCreatorFields
417
769
  });
418
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
419
- const schedulingService = getService("scheduling", { strapi: strapi2 });
420
- if (releaseData.scheduledAt) {
421
- await schedulingService.set(id, releaseData.scheduledAt);
422
- } else if (release2.scheduledAt) {
423
- schedulingService.cancel(id);
424
- }
770
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
771
+ if (releaseData.scheduledAt) {
772
+ await schedulingService.set(id, releaseData.scheduledAt);
773
+ } else if (release2.scheduledAt) {
774
+ schedulingService.cancel(id);
425
775
  }
776
+ this.updateReleaseStatus(id);
426
777
  strapi2.telemetry.send("didUpdateContentRelease");
427
778
  return updatedRelease;
428
779
  },
@@ -434,7 +785,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
434
785
  validateEntryContentType(action.entry.contentType),
435
786
  validateUniqueEntry(releaseId, action)
436
787
  ]);
437
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
788
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
438
789
  if (!release2) {
439
790
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
440
791
  }
@@ -442,11 +793,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
442
793
  throw new errors.ValidationError("Release already published");
443
794
  }
444
795
  const { entry, type } = action;
445
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
796
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
797
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
798
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
446
799
  data: {
447
800
  type,
448
801
  contentType: entry.contentType,
449
802
  locale: entry.locale,
803
+ isEntryValid,
450
804
  entry: {
451
805
  id: entry.id,
452
806
  __type: entry.contentType,
@@ -454,30 +808,35 @@ const createReleaseService = ({ strapi: strapi2 }) => {
454
808
  },
455
809
  release: releaseId
456
810
  },
457
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
811
+ populate: { release: { select: ["id"] }, entry: { select: ["id"] } }
458
812
  });
813
+ this.updateReleaseStatus(releaseId);
814
+ return releaseAction2;
459
815
  },
460
816
  async findActions(releaseId, query) {
461
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
462
- fields: ["id"]
817
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
818
+ where: { id: releaseId },
819
+ select: ["id"]
463
820
  });
464
821
  if (!release2) {
465
822
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
466
823
  }
467
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
468
- ...query,
824
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
825
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
826
+ ...dbQuery,
469
827
  populate: {
470
828
  entry: {
471
829
  populate: "*"
472
830
  }
473
831
  },
474
- filters: {
832
+ where: {
475
833
  release: releaseId
476
834
  }
477
835
  });
478
836
  },
479
837
  async countActions(query) {
480
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
838
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
839
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
481
840
  },
482
841
  async groupActions(actions, groupBy) {
483
842
  const contentTypeUids = actions.reduce((acc, action) => {
@@ -486,9 +845,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
486
845
  }
487
846
  return acc;
488
847
  }, []);
489
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
490
- contentTypeUids
491
- );
848
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(contentTypeUids);
492
849
  const allLocalesDictionary = await this.getLocalesDataForActions();
493
850
  const formattedData = actions.map((action) => {
494
851
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
@@ -558,10 +915,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
558
915
  return componentsMap;
559
916
  },
560
917
  async delete(releaseId) {
561
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
918
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
919
+ where: { id: releaseId },
562
920
  populate: {
563
921
  actions: {
564
- fields: ["id"]
922
+ select: ["id"]
565
923
  }
566
924
  }
567
925
  });
@@ -579,9 +937,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
579
937
  }
580
938
  }
581
939
  });
582
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
940
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
941
+ where: {
942
+ id: releaseId
943
+ }
944
+ });
583
945
  });
584
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
946
+ if (release2.scheduledAt) {
585
947
  const schedulingService = getService("scheduling", { strapi: strapi2 });
586
948
  await schedulingService.cancel(release2.id);
587
949
  }
@@ -589,139 +951,69 @@ const createReleaseService = ({ strapi: strapi2 }) => {
589
951
  return release2;
590
952
  },
591
953
  async publish(releaseId) {
592
- try {
593
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
594
- RELEASE_MODEL_UID,
595
- releaseId,
596
- {
597
- populate: {
598
- actions: {
599
- populate: {
600
- entry: {
601
- fields: ["id"]
602
- }
603
- }
604
- }
605
- }
606
- }
607
- );
608
- if (!releaseWithPopulatedActionEntries) {
954
+ const {
955
+ release: release2,
956
+ error
957
+ } = await strapi2.db.transaction(async ({ trx }) => {
958
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
959
+ if (!lockedRelease) {
609
960
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
610
961
  }
611
- if (releaseWithPopulatedActionEntries.releasedAt) {
962
+ if (lockedRelease.releasedAt) {
612
963
  throw new errors.ValidationError("Release already published");
613
964
  }
614
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
- throw new errors.ValidationError("No entries to publish");
965
+ if (lockedRelease.status === "failed") {
966
+ throw new errors.ValidationError("Release failed to publish");
616
967
  }
617
- const collectionTypeActions = {};
618
- const singleTypeActions = [];
619
- for (const action of releaseWithPopulatedActionEntries.actions) {
620
- const contentTypeUid = action.contentType;
621
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
622
- if (!collectionTypeActions[contentTypeUid]) {
623
- collectionTypeActions[contentTypeUid] = {
624
- entriestoPublishIds: [],
625
- entriesToUnpublishIds: []
626
- };
627
- }
628
- if (action.type === "publish") {
629
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
630
- } else {
631
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
632
- }
633
- } else {
634
- singleTypeActions.push({
635
- uid: contentTypeUid,
636
- action: action.type,
637
- id: action.entry.id
638
- });
639
- }
640
- }
641
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
642
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
643
- await strapi2.db.transaction(async () => {
644
- for (const { uid, action, id } of singleTypeActions) {
645
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
646
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
647
- try {
648
- if (action === "publish") {
649
- await entityManagerService.publish(entry, uid);
650
- } else {
651
- await entityManagerService.unpublish(entry, uid);
652
- }
653
- } catch (error) {
654
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
655
- } else {
656
- throw error;
657
- }
658
- }
659
- }
660
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
661
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
662
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
663
- const entriesToPublish = await strapi2.entityService.findMany(
664
- contentTypeUid,
665
- {
666
- filters: {
667
- id: {
668
- $in: entriestoPublishIds
669
- }
670
- },
671
- populate
672
- }
673
- );
674
- const entriesToUnpublish = await strapi2.entityService.findMany(
675
- contentTypeUid,
676
- {
677
- filters: {
678
- id: {
679
- $in: entriesToUnpublishIds
680
- }
681
- },
682
- populate
683
- }
684
- );
685
- if (entriesToPublish.length > 0) {
686
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
968
+ try {
969
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
970
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(releaseId);
971
+ await strapi2.db.transaction(async () => {
972
+ for (const { uid, action, id } of singleTypeActions) {
973
+ await publishSingleTypeAction(uid, action, id);
687
974
  }
688
- if (entriesToUnpublish.length > 0) {
689
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
975
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
976
+ const uid = contentTypeUid;
977
+ await publishCollectionTypeAction(
978
+ uid,
979
+ collectionTypeActions[uid].entriesToPublishIds,
980
+ collectionTypeActions[uid].entriesToUnpublishIds
981
+ );
690
982
  }
691
- }
692
- });
693
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
694
- data: {
695
- /*
696
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
697
- */
698
- // @ts-expect-error see above
699
- releasedAt: /* @__PURE__ */ new Date()
700
- },
701
- populate: {
702
- actions: {
703
- // @ts-expect-error is not expecting count but it is working
704
- count: true
983
+ });
984
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
985
+ where: {
986
+ id: releaseId
987
+ },
988
+ data: {
989
+ status: "done",
990
+ releasedAt: /* @__PURE__ */ new Date()
705
991
  }
706
- }
707
- });
708
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
992
+ });
709
993
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
710
994
  isPublished: true,
711
- release: release2
995
+ release: release22
712
996
  });
713
- }
714
- strapi2.telemetry.send("didPublishContentRelease");
715
- return release2;
716
- } catch (error) {
717
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
997
+ strapi2.telemetry.send("didPublishContentRelease");
998
+ return { release: release22, error: null };
999
+ } catch (error2) {
718
1000
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
719
1001
  isPublished: false,
720
- error
1002
+ error: error2
721
1003
  });
1004
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1005
+ status: "failed"
1006
+ }).transacting(trx).execute();
1007
+ return {
1008
+ release: null,
1009
+ error: error2
1010
+ };
722
1011
  }
1012
+ });
1013
+ if (error instanceof Error) {
723
1014
  throw error;
724
1015
  }
1016
+ return release2;
725
1017
  },
726
1018
  async updateAction(actionId, releaseId, update) {
727
1019
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -760,14 +1052,67 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1052
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
1053
  );
762
1054
  }
1055
+ this.updateReleaseStatus(releaseId);
763
1056
  return deletedAction;
1057
+ },
1058
+ async updateReleaseStatus(releaseId) {
1059
+ const [totalActions, invalidActions] = await Promise.all([
1060
+ this.countActions({
1061
+ filters: {
1062
+ release: releaseId
1063
+ }
1064
+ }),
1065
+ this.countActions({
1066
+ filters: {
1067
+ release: releaseId,
1068
+ isEntryValid: false
1069
+ }
1070
+ })
1071
+ ]);
1072
+ if (totalActions > 0) {
1073
+ if (invalidActions > 0) {
1074
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1075
+ where: {
1076
+ id: releaseId
1077
+ },
1078
+ data: {
1079
+ status: "blocked"
1080
+ }
1081
+ });
1082
+ }
1083
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1084
+ where: {
1085
+ id: releaseId
1086
+ },
1087
+ data: {
1088
+ status: "ready"
1089
+ }
1090
+ });
1091
+ }
1092
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1093
+ where: {
1094
+ id: releaseId
1095
+ },
1096
+ data: {
1097
+ status: "empty"
1098
+ }
1099
+ });
764
1100
  }
765
1101
  };
766
1102
  };
1103
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1104
+ constructor(message) {
1105
+ super(message);
1106
+ this.name = "AlreadyOnReleaseError";
1107
+ }
1108
+ }
767
1109
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
768
1110
  async validateUniqueEntry(releaseId, releaseActionArgs) {
769
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
770
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1111
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1112
+ where: {
1113
+ id: releaseId
1114
+ },
1115
+ populate: { actions: { populate: { entry: { select: ["id"] } } } }
771
1116
  });
772
1117
  if (!release2) {
773
1118
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
@@ -776,7 +1121,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
776
1121
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
777
1122
  );
778
1123
  if (isEntryInRelease) {
779
- throw new errors.ValidationError(
1124
+ throw new AlreadyOnReleaseError(
780
1125
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
781
1126
  );
782
1127
  }
@@ -793,10 +1138,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
793
1138
  }
794
1139
  },
795
1140
  async validatePendingReleasesLimit() {
796
- const maximumPendingReleases = (
797
- // @ts-expect-error - options is not typed into features
798
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
799
- );
1141
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1142
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
800
1143
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
801
1144
  filters: {
802
1145
  releasedAt: {
@@ -809,8 +1152,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
809
1152
  }
810
1153
  },
811
1154
  async validateUniqueNameForPendingRelease(name, id) {
812
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
813
- filters: {
1155
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1156
+ where: {
814
1157
  releasedAt: {
815
1158
  $null: true
816
1159
  },
@@ -839,7 +1182,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
839
1182
  }
840
1183
  const job = scheduleJob(scheduleDate, async () => {
841
1184
  try {
842
- await getService("release").publish(releaseId);
1185
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
843
1186
  } catch (error) {
844
1187
  }
845
1188
  this.cancel(releaseId);
@@ -884,7 +1227,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
884
1227
  const services = {
885
1228
  release: createReleaseService,
886
1229
  "release-validation": createReleaseValidationService,
887
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1230
+ scheduling: createSchedulingService
888
1231
  };
889
1232
  const RELEASE_SCHEMA = yup.object().shape({
890
1233
  name: yup.string().trim().required(),
@@ -909,7 +1252,7 @@ const RELEASE_SCHEMA = yup.object().shape({
909
1252
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
910
1253
  const releaseController = {
911
1254
  async findMany(ctx) {
912
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1255
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
913
1256
  ability: ctx.state.userAbility,
914
1257
  model: RELEASE_MODEL_UID
915
1258
  });
@@ -937,7 +1280,12 @@ const releaseController = {
937
1280
  }
938
1281
  };
939
1282
  });
940
- ctx.body = { data, meta: { pagination } };
1283
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1284
+ where: {
1285
+ releasedAt: null
1286
+ }
1287
+ });
1288
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
941
1289
  }
942
1290
  },
943
1291
  async findOne(ctx) {
@@ -954,7 +1302,7 @@ const releaseController = {
954
1302
  });
955
1303
  const sanitizedRelease = {
956
1304
  ...release2,
957
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1305
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
958
1306
  };
959
1307
  const data = {
960
1308
  ...sanitizedRelease,
@@ -966,19 +1314,48 @@ const releaseController = {
966
1314
  };
967
1315
  ctx.body = { data };
968
1316
  },
1317
+ async mapEntriesToReleases(ctx) {
1318
+ const { contentTypeUid, entriesIds } = ctx.query;
1319
+ if (!contentTypeUid || !entriesIds) {
1320
+ throw new errors.ValidationError("Missing required query parameters");
1321
+ }
1322
+ const releaseService = getService("release", { strapi });
1323
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1324
+ contentTypeUid,
1325
+ entriesIds
1326
+ );
1327
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1328
+ // TODO: Fix for v5 removed mappedEntriedToRelease
1329
+ (acc, release2) => {
1330
+ release2.actions.forEach((action) => {
1331
+ if (!acc[action.entry.id]) {
1332
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1333
+ } else {
1334
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1335
+ }
1336
+ });
1337
+ return acc;
1338
+ },
1339
+ // TODO: Fix for v5 removed mappedEntriedToRelease
1340
+ {}
1341
+ );
1342
+ ctx.body = {
1343
+ data: mappedEntriesInReleases
1344
+ };
1345
+ },
969
1346
  async create(ctx) {
970
1347
  const user = ctx.state.user;
971
1348
  const releaseArgs = ctx.request.body;
972
1349
  await validateRelease(releaseArgs);
973
1350
  const releaseService = getService("release", { strapi });
974
1351
  const release2 = await releaseService.create(releaseArgs, { user });
975
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1352
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
976
1353
  ability: ctx.state.userAbility,
977
1354
  model: RELEASE_MODEL_UID
978
1355
  });
979
- ctx.body = {
1356
+ ctx.created({
980
1357
  data: await permissionsManager.sanitizeOutput(release2)
981
- };
1358
+ });
982
1359
  },
983
1360
  async update(ctx) {
984
1361
  const user = ctx.state.user;
@@ -987,7 +1364,7 @@ const releaseController = {
987
1364
  await validateRelease(releaseArgs);
988
1365
  const releaseService = getService("release", { strapi });
989
1366
  const release2 = await releaseService.update(id, releaseArgs, { user });
990
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1367
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
991
1368
  ability: ctx.state.userAbility,
992
1369
  model: RELEASE_MODEL_UID
993
1370
  });
@@ -1051,13 +1428,45 @@ const releaseActionController = {
1051
1428
  await validateReleaseAction(releaseActionArgs);
1052
1429
  const releaseService = getService("release", { strapi });
1053
1430
  const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1054
- ctx.body = {
1431
+ ctx.created({
1055
1432
  data: releaseAction2
1056
- };
1433
+ });
1434
+ },
1435
+ async createMany(ctx) {
1436
+ const releaseId = ctx.params.releaseId;
1437
+ const releaseActionsArgs = ctx.request.body;
1438
+ await Promise.all(
1439
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1440
+ );
1441
+ const releaseService = getService("release", { strapi });
1442
+ const releaseActions = await strapi.db.transaction(async () => {
1443
+ const releaseActions2 = await Promise.all(
1444
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1445
+ try {
1446
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1447
+ return action;
1448
+ } catch (error) {
1449
+ if (error instanceof AlreadyOnReleaseError) {
1450
+ return null;
1451
+ }
1452
+ throw error;
1453
+ }
1454
+ })
1455
+ );
1456
+ return releaseActions2;
1457
+ });
1458
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1459
+ ctx.created({
1460
+ data: newReleaseActions,
1461
+ meta: {
1462
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1463
+ totalEntries: releaseActions.length
1464
+ }
1465
+ });
1057
1466
  },
1058
1467
  async findMany(ctx) {
1059
1468
  const releaseId = ctx.params.releaseId;
1060
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1469
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1061
1470
  ability: ctx.state.userAbility,
1062
1471
  model: RELEASE_ACTION_MODEL_UID
1063
1472
  });
@@ -1071,14 +1480,14 @@ const releaseActionController = {
1071
1480
  if (acc[action.contentType]) {
1072
1481
  return acc;
1073
1482
  }
1074
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1483
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1075
1484
  ability: ctx.state.userAbility,
1076
1485
  model: action.contentType
1077
1486
  });
1078
1487
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1079
1488
  return acc;
1080
1489
  }, {});
1081
- const sanitizedResults = await mapAsync(results, async (action) => ({
1490
+ const sanitizedResults = await async.map(results, async (action) => ({
1082
1491
  ...action,
1083
1492
  entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1084
1493
  }));
@@ -1123,6 +1532,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1123
1532
  const release = {
1124
1533
  type: "admin",
1125
1534
  routes: [
1535
+ {
1536
+ method: "GET",
1537
+ path: "/mapEntriesToReleases",
1538
+ handler: "release.mapEntriesToReleases",
1539
+ config: {
1540
+ policies: [
1541
+ "admin::isAuthenticatedAdmin",
1542
+ {
1543
+ name: "admin::hasPermissions",
1544
+ config: {
1545
+ actions: ["plugin::content-releases.read"]
1546
+ }
1547
+ }
1548
+ ]
1549
+ }
1550
+ },
1126
1551
  {
1127
1552
  method: "POST",
1128
1553
  path: "/",
@@ -1240,6 +1665,22 @@ const releaseAction = {
1240
1665
  ]
1241
1666
  }
1242
1667
  },
1668
+ {
1669
+ method: "POST",
1670
+ path: "/:releaseId/actions/bulk",
1671
+ handler: "release-action.createMany",
1672
+ config: {
1673
+ policies: [
1674
+ "admin::isAuthenticatedAdmin",
1675
+ {
1676
+ name: "admin::hasPermissions",
1677
+ config: {
1678
+ actions: ["plugin::content-releases.create-action"]
1679
+ }
1680
+ }
1681
+ ]
1682
+ }
1683
+ },
1243
1684
  {
1244
1685
  method: "GET",
1245
1686
  path: "/:releaseId/actions",
@@ -1294,9 +1735,8 @@ const routes = {
1294
1735
  release,
1295
1736
  "release-action": releaseAction
1296
1737
  };
1297
- const { features } = require("@strapi/strapi/dist/utils/ee");
1298
1738
  const getPlugin = () => {
1299
- if (features.isEnabled("cms-content-releases")) {
1739
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1300
1740
  return {
1301
1741
  register,
1302
1742
  bootstrap,
@@ -1308,6 +1748,9 @@ const getPlugin = () => {
1308
1748
  };
1309
1749
  }
1310
1750
  return {
1751
+ // Always return register, it handles its own feature check
1752
+ register,
1753
+ // Always return contentTypes to avoid losing data when the feature is disabled
1311
1754
  contentTypes
1312
1755
  };
1313
1756
  };