@strapi/content-releases 0.0.0-next.b6d552f6e63dec5627cb8611ab2adcb8244359be → 0.0.0-next.bb6ff32f5168f3e380d3d9acba90a9d53bfcfb89

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 (143) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/App-BX6_LcmS.mjs +1559 -0
  3. package/dist/_chunks/App-BX6_LcmS.mjs.map +1 -0
  4. package/dist/_chunks/App-DITZWWqI.js +1580 -0
  5. package/dist/_chunks/App-DITZWWqI.js.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-Be3acS2L.js} +8 -7
  7. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-_MxP6-Dt.mjs} +9 -8
  9. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-BMgLwqci.mjs +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-BMgLwqci.mjs.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-DZcRvN_O.js +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-DZcRvN_O.js.map +1 -0
  14. package/dist/_chunks/{en-gcJJ5htG.js → en-BWPPsSH-.js} +28 -4
  15. package/dist/_chunks/en-BWPPsSH-.js.map +1 -0
  16. package/dist/_chunks/{en-WuuhP6Bn.mjs → en-D9Q4YW03.mjs} +28 -4
  17. package/dist/_chunks/en-D9Q4YW03.mjs.map +1 -0
  18. package/dist/_chunks/index-CBsSVKTv.mjs +1380 -0
  19. package/dist/_chunks/index-CBsSVKTv.mjs.map +1 -0
  20. package/dist/_chunks/index-TfMp19WL.js +1399 -0
  21. package/dist/_chunks/index-TfMp19WL.js.map +1 -0
  22. package/dist/_chunks/schemas-BE1LxE9J.js +62 -0
  23. package/dist/_chunks/schemas-BE1LxE9J.js.map +1 -0
  24. package/dist/_chunks/schemas-DdA2ic2U.mjs +44 -0
  25. package/dist/_chunks/schemas-DdA2ic2U.mjs.map +1 -0
  26. package/dist/admin/index.js +1 -15
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/index.mjs +2 -16
  29. package/dist/admin/index.mjs.map +1 -1
  30. package/dist/admin/src/components/EntryValidationPopover.d.ts +13 -0
  31. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  32. package/dist/admin/src/components/ReleaseAction.d.ts +3 -0
  33. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  34. package/dist/admin/src/components/ReleaseActionModal.d.ts +24 -0
  35. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  36. package/dist/admin/src/components/ReleaseListCell.d.ts +28 -0
  37. package/dist/admin/src/components/ReleaseModal.d.ts +17 -0
  38. package/dist/admin/src/components/ReleasesPanel.d.ts +3 -0
  39. package/dist/admin/src/constants.d.ts +76 -0
  40. package/dist/admin/src/index.d.ts +3 -0
  41. package/dist/admin/src/modules/hooks.d.ts +7 -0
  42. package/dist/admin/src/pages/App.d.ts +1 -0
  43. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  44. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  45. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  46. package/dist/admin/src/pages/ReleasesSettingsPage.d.ts +1 -0
  47. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  48. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  49. package/dist/admin/src/pluginId.d.ts +1 -0
  50. package/dist/admin/src/services/release.d.ts +112 -0
  51. package/dist/admin/src/store/hooks.d.ts +7 -0
  52. package/dist/admin/src/utils/api.d.ts +6 -0
  53. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  54. package/dist/admin/src/utils/time.d.ts +10 -0
  55. package/dist/admin/src/validation/schemas.d.ts +6 -0
  56. package/dist/server/index.js +1107 -662
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/server/index.mjs +1108 -662
  59. package/dist/server/index.mjs.map +1 -1
  60. package/dist/server/src/bootstrap.d.ts +5 -0
  61. package/dist/server/src/bootstrap.d.ts.map +1 -0
  62. package/dist/server/src/constants.d.ts +21 -0
  63. package/dist/server/src/constants.d.ts.map +1 -0
  64. package/dist/server/src/content-types/index.d.ts +97 -0
  65. package/dist/server/src/content-types/index.d.ts.map +1 -0
  66. package/dist/server/src/content-types/release/index.d.ts +48 -0
  67. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  68. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  69. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  70. package/dist/server/src/content-types/release-action/index.d.ts +48 -0
  71. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  72. package/dist/server/src/content-types/release-action/schema.d.ts +47 -0
  73. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  74. package/dist/server/src/controllers/index.d.ts +25 -0
  75. package/dist/server/src/controllers/index.d.ts.map +1 -0
  76. package/dist/server/src/controllers/release-action.d.ts +10 -0
  77. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  78. package/dist/server/src/controllers/release.d.ts +18 -0
  79. package/dist/server/src/controllers/release.d.ts.map +1 -0
  80. package/dist/server/src/controllers/settings.d.ts +11 -0
  81. package/dist/server/src/controllers/settings.d.ts.map +1 -0
  82. package/dist/server/src/controllers/validation/release-action.d.ts +14 -0
  83. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  84. package/dist/server/src/controllers/validation/release.d.ts +4 -0
  85. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  86. package/dist/server/src/controllers/validation/settings.d.ts +3 -0
  87. package/dist/server/src/controllers/validation/settings.d.ts.map +1 -0
  88. package/dist/server/src/destroy.d.ts +5 -0
  89. package/dist/server/src/destroy.d.ts.map +1 -0
  90. package/dist/server/src/index.d.ts +2111 -0
  91. package/dist/server/src/index.d.ts.map +1 -0
  92. package/dist/server/src/middlewares/documents.d.ts +6 -0
  93. package/dist/server/src/middlewares/documents.d.ts.map +1 -0
  94. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts +9 -0
  95. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts.map +1 -0
  96. package/dist/server/src/migrations/index.d.ts +13 -0
  97. package/dist/server/src/migrations/index.d.ts.map +1 -0
  98. package/dist/server/src/register.d.ts +5 -0
  99. package/dist/server/src/register.d.ts.map +1 -0
  100. package/dist/server/src/routes/index.d.ts +51 -0
  101. package/dist/server/src/routes/index.d.ts.map +1 -0
  102. package/dist/server/src/routes/release-action.d.ts +18 -0
  103. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  104. package/dist/server/src/routes/release.d.ts +18 -0
  105. package/dist/server/src/routes/release.d.ts.map +1 -0
  106. package/dist/server/src/routes/settings.d.ts +18 -0
  107. package/dist/server/src/routes/settings.d.ts.map +1 -0
  108. package/dist/server/src/services/index.d.ts +1824 -0
  109. package/dist/server/src/services/index.d.ts.map +1 -0
  110. package/dist/server/src/services/release-action.d.ts +34 -0
  111. package/dist/server/src/services/release-action.d.ts.map +1 -0
  112. package/dist/server/src/services/release.d.ts +31 -0
  113. package/dist/server/src/services/release.d.ts.map +1 -0
  114. package/dist/server/src/services/scheduling.d.ts +18 -0
  115. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  116. package/dist/server/src/services/settings.d.ts +13 -0
  117. package/dist/server/src/services/settings.d.ts.map +1 -0
  118. package/dist/server/src/services/validation.d.ts +18 -0
  119. package/dist/server/src/services/validation.d.ts.map +1 -0
  120. package/dist/server/src/utils/index.d.ts +35 -0
  121. package/dist/server/src/utils/index.d.ts.map +1 -0
  122. package/dist/shared/contracts/release-actions.d.ts +137 -0
  123. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  124. package/dist/shared/contracts/releases.d.ts +184 -0
  125. package/dist/shared/contracts/releases.d.ts.map +1 -0
  126. package/dist/shared/contracts/settings.d.ts +39 -0
  127. package/dist/shared/contracts/settings.d.ts.map +1 -0
  128. package/dist/shared/types.d.ts +24 -0
  129. package/dist/shared/types.d.ts.map +1 -0
  130. package/package.json +35 -39
  131. package/dist/_chunks/App-5G7GEzBM.js +0 -1317
  132. package/dist/_chunks/App-5G7GEzBM.js.map +0 -1
  133. package/dist/_chunks/App-WMxox0mk.mjs +0 -1294
  134. package/dist/_chunks/App-WMxox0mk.mjs.map +0 -1
  135. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  136. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  137. package/dist/_chunks/en-WuuhP6Bn.mjs.map +0 -1
  138. package/dist/_chunks/en-gcJJ5htG.js.map +0 -1
  139. package/dist/_chunks/index-BZ8RPGiV.mjs +0 -1013
  140. package/dist/_chunks/index-BZ8RPGiV.mjs.map +0 -1
  141. package/dist/_chunks/index-pQ3hnZJy.js +0 -1034
  142. package/dist/_chunks/index-pQ3hnZJy.js.map +0 -1
  143. package/strapi-server.js +0 -3
@@ -3,7 +3,6 @@ const utils = require("@strapi/utils");
3
3
  const isEqual = require("lodash/isEqual");
4
4
  const lodash = require("lodash");
5
5
  const _ = require("lodash/fp");
6
- const EE = require("@strapi/strapi/dist/utils/ee");
7
6
  const nodeSchedule = require("node-schedule");
8
7
  const yup = require("yup");
9
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -27,7 +26,6 @@ function _interopNamespace(e) {
27
26
  }
28
27
  const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
29
28
  const ___default = /* @__PURE__ */ _interopDefault(_);
30
- const EE__default = /* @__PURE__ */ _interopDefault(EE);
31
29
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
32
30
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
33
31
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -73,21 +71,38 @@ const ACTIONS = [
73
71
  displayName: "Add an entry to a release",
74
72
  uid: "create-action",
75
73
  pluginName: "content-releases"
74
+ },
75
+ // Settings
76
+ {
77
+ uid: "settings.read",
78
+ section: "settings",
79
+ displayName: "Read",
80
+ category: "content releases",
81
+ subCategory: "options",
82
+ pluginName: "content-releases"
83
+ },
84
+ {
85
+ uid: "settings.update",
86
+ section: "settings",
87
+ displayName: "Edit",
88
+ category: "content releases",
89
+ subCategory: "options",
90
+ pluginName: "content-releases"
76
91
  }
77
92
  ];
78
93
  const ALLOWED_WEBHOOK_EVENTS = {
79
94
  RELEASES_PUBLISH: "releases.publish"
80
95
  };
81
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
96
+ const getService = (name, { strapi: strapi2 }) => {
82
97
  return strapi2.plugin("content-releases").service(name);
83
98
  };
84
- const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
99
+ const getDraftEntryValidStatus = async ({ contentType, documentId, locale }, { strapi: strapi2 }) => {
85
100
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
86
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
87
- const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
88
- return entry;
101
+ const populate = await populateBuilderService(contentType).populateDeep(Infinity).build();
102
+ const entry = await getEntry({ contentType, documentId, locale, populate }, { strapi: strapi2 });
103
+ return isEntryValid(contentType, entry, { strapi: strapi2 });
89
104
  };
90
- const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
105
+ const isEntryValid = async (contentTypeUid, entry, { strapi: strapi2 }) => {
91
106
  try {
92
107
  await strapi2.entityValidator.validateEntityCreation(
93
108
  strapi2.getModel(contentTypeUid),
@@ -96,11 +111,54 @@ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } =
96
111
  // @ts-expect-error - FIXME: entity here is unnecessary
97
112
  entry
98
113
  );
114
+ const workflowsService = strapi2.plugin("review-workflows").service("workflows");
115
+ const workflow = await workflowsService.getAssignedWorkflow(contentTypeUid, {
116
+ populate: "stageRequiredToPublish"
117
+ });
118
+ if (workflow?.stageRequiredToPublish) {
119
+ return entry.strapi_stage.id === workflow.stageRequiredToPublish.id;
120
+ }
99
121
  return true;
100
122
  } catch {
101
123
  return false;
102
124
  }
103
125
  };
126
+ const getEntry = async ({
127
+ contentType,
128
+ documentId,
129
+ locale,
130
+ populate,
131
+ status = "draft"
132
+ }, { strapi: strapi2 }) => {
133
+ if (documentId) {
134
+ const entry = await strapi2.documents(contentType).findOne({ documentId, locale, populate, status });
135
+ if (status === "published" && !entry) {
136
+ return strapi2.documents(contentType).findOne({ documentId, locale, populate, status: "draft" });
137
+ }
138
+ return entry;
139
+ }
140
+ return strapi2.documents(contentType).findFirst({ locale, populate, status });
141
+ };
142
+ const getEntryStatus = async (contentType, entry) => {
143
+ if (entry.publishedAt) {
144
+ return "published";
145
+ }
146
+ const publishedEntry = await strapi.documents(contentType).findOne({
147
+ documentId: entry.documentId,
148
+ locale: entry.locale,
149
+ status: "published",
150
+ fields: ["updatedAt"]
151
+ });
152
+ if (!publishedEntry) {
153
+ return "draft";
154
+ }
155
+ const entryUpdatedAt = new Date(entry.updatedAt).getTime();
156
+ const publishedEntryUpdatedAt = new Date(publishedEntry.updatedAt).getTime();
157
+ if (entryUpdatedAt > publishedEntryUpdatedAt) {
158
+ return "modified";
159
+ }
160
+ return "published";
161
+ };
104
162
  async function deleteActionsOnDisableDraftAndPublish({
105
163
  oldContentTypes,
106
164
  contentTypes: contentTypes2
@@ -122,7 +180,7 @@ async function deleteActionsOnDisableDraftAndPublish({
122
180
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
123
181
  const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
124
182
  if (deletedContentTypes.length) {
125
- await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
183
+ await utils.async.map(deletedContentTypes, async (deletedContentTypeUID) => {
126
184
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
127
185
  });
128
186
  }
@@ -141,25 +199,27 @@ async function migrateIsValidAndStatusReleases() {
141
199
  }
142
200
  }
143
201
  });
144
- utils.mapAsync(releasesWithoutStatus, async (release2) => {
202
+ utils.async.map(releasesWithoutStatus, async (release2) => {
145
203
  const actions = release2.actions;
146
204
  const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
147
205
  for (const action of notValidatedActions) {
148
206
  if (action.entry) {
149
- const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
150
- strapi
207
+ const isEntryValid2 = getDraftEntryValidStatus(
208
+ {
209
+ contentType: action.contentType,
210
+ documentId: action.entryDocumentId,
211
+ locale: action.locale
212
+ },
213
+ { strapi }
214
+ );
215
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
216
+ where: {
217
+ id: action.id
218
+ },
219
+ data: {
220
+ isEntryValid: isEntryValid2
221
+ }
151
222
  });
152
- if (populatedEntry) {
153
- const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
154
- await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
155
- where: {
156
- id: action.id
157
- },
158
- data: {
159
- isEntryValid
160
- }
161
- });
162
- }
163
223
  }
164
224
  }
165
225
  return getService("release", { strapi }).updateReleaseStatus(release2.id);
@@ -172,7 +232,7 @@ async function migrateIsValidAndStatusReleases() {
172
232
  }
173
233
  }
174
234
  });
175
- utils.mapAsync(publishedReleases, async (release2) => {
235
+ utils.async.map(publishedReleases, async (release2) => {
176
236
  return strapi.db.query(RELEASE_MODEL_UID).update({
177
237
  where: {
178
238
  id: release2.id
@@ -189,7 +249,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
189
249
  (uid) => oldContentTypes[uid]?.options?.draftAndPublish
190
250
  );
191
251
  const releasesAffected = /* @__PURE__ */ new Set();
192
- utils.mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
252
+ utils.async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
193
253
  const oldContentType = oldContentTypes[contentTypeUID];
194
254
  const contentType = contentTypes2[contentTypeUID];
195
255
  if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
@@ -202,187 +262,272 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
202
262
  release: true
203
263
  }
204
264
  });
205
- await utils.mapAsync(actions, async (action) => {
206
- if (action.entry) {
207
- const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
208
- strapi
265
+ await utils.async.map(actions, async (action) => {
266
+ if (action.entry && action.release && action.type === "publish") {
267
+ const isEntryValid2 = await getDraftEntryValidStatus(
268
+ {
269
+ contentType: contentTypeUID,
270
+ documentId: action.entryDocumentId,
271
+ locale: action.locale
272
+ },
273
+ { strapi }
274
+ );
275
+ releasesAffected.add(action.release.id);
276
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
277
+ where: {
278
+ id: action.id
279
+ },
280
+ data: {
281
+ isEntryValid: isEntryValid2
282
+ }
209
283
  });
210
- if (populatedEntry) {
211
- const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
212
- strapi
213
- });
214
- releasesAffected.add(action.release.id);
215
- await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
216
- where: {
217
- id: action.id
218
- },
219
- data: {
220
- isEntryValid
221
- }
222
- });
223
- }
224
284
  }
225
285
  });
226
286
  }
227
287
  }).then(() => {
228
- utils.mapAsync(releasesAffected, async (releaseId) => {
288
+ utils.async.map(releasesAffected, async (releaseId) => {
229
289
  return getService("release", { strapi }).updateReleaseStatus(releaseId);
230
290
  });
231
291
  });
232
292
  }
233
293
  }
234
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
294
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
295
+ if (!oldContentTypes) {
296
+ return;
297
+ }
298
+ const i18nPlugin = strapi.plugin("i18n");
299
+ if (!i18nPlugin) {
300
+ return;
301
+ }
302
+ for (const uid in contentTypes2) {
303
+ if (!oldContentTypes[uid]) {
304
+ continue;
305
+ }
306
+ const oldContentType = oldContentTypes[uid];
307
+ const contentType = contentTypes2[uid];
308
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
309
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
310
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
311
+ locale: null
312
+ }).where({ contentType: uid }).execute();
313
+ }
314
+ }
315
+ }
316
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
317
+ if (!oldContentTypes) {
318
+ return;
319
+ }
320
+ const i18nPlugin = strapi.plugin("i18n");
321
+ if (!i18nPlugin) {
322
+ return;
323
+ }
324
+ for (const uid in contentTypes2) {
325
+ if (!oldContentTypes[uid]) {
326
+ continue;
327
+ }
328
+ const oldContentType = oldContentTypes[uid];
329
+ const contentType = contentTypes2[uid];
330
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
331
+ const { getDefaultLocale } = i18nPlugin.service("locales");
332
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
333
+ const defaultLocale = await getDefaultLocale();
334
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
335
+ locale: defaultLocale
336
+ }).where({ contentType: uid }).execute();
337
+ }
338
+ }
339
+ }
340
+ const addEntryDocumentToReleaseActions = {
341
+ name: "content-releases::5.0.0-add-entry-document-id-to-release-actions",
342
+ async up(trx, db) {
343
+ const hasTable = await trx.schema.hasTable("strapi_release_actions");
344
+ if (!hasTable) {
345
+ return;
346
+ }
347
+ const hasPolymorphicColumn = await trx.schema.hasColumn("strapi_release_actions", "target_id");
348
+ if (hasPolymorphicColumn) {
349
+ const hasEntryDocumentIdColumn = await trx.schema.hasColumn(
350
+ "strapi_release_actions",
351
+ "entry_document_id"
352
+ );
353
+ if (!hasEntryDocumentIdColumn) {
354
+ await trx.schema.alterTable("strapi_release_actions", (table) => {
355
+ table.string("entry_document_id");
356
+ });
357
+ }
358
+ const releaseActions = await trx.select("*").from("strapi_release_actions");
359
+ utils.async.map(releaseActions, async (action) => {
360
+ const { target_type, target_id } = action;
361
+ const entry = await db.query(target_type).findOne({ where: { id: target_id } });
362
+ if (entry) {
363
+ await trx("strapi_release_actions").update({ entry_document_id: entry.documentId }).where("id", action.id);
364
+ }
365
+ });
366
+ }
367
+ },
368
+ async down() {
369
+ throw new Error("not implemented");
370
+ }
371
+ };
235
372
  const register = async ({ strapi: strapi2 }) => {
236
- if (features$2.isEnabled("cms-content-releases")) {
237
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
238
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
239
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
373
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
374
+ await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
375
+ strapi2.db.migrations.providers.internal.register(addEntryDocumentToReleaseActions);
376
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
377
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
378
+ }
379
+ if (strapi2.plugin("graphql")) {
380
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
381
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
382
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
383
+ }
384
+ };
385
+ const updateActionsStatusAndUpdateReleaseStatus = async (contentType, entry) => {
386
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
387
+ where: {
388
+ releasedAt: null,
389
+ actions: {
390
+ contentType,
391
+ entryDocumentId: entry.documentId,
392
+ locale: entry.locale
393
+ }
394
+ }
395
+ });
396
+ const entryStatus = await isEntryValid(contentType, entry, { strapi });
397
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).updateMany({
398
+ where: {
399
+ contentType,
400
+ entryDocumentId: entry.documentId,
401
+ locale: entry.locale
402
+ },
403
+ data: {
404
+ isEntryValid: entryStatus
405
+ }
406
+ });
407
+ for (const release2 of releases) {
408
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
409
+ }
410
+ };
411
+ const deleteActionsAndUpdateReleaseStatus = async (params) => {
412
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
413
+ where: {
414
+ actions: params
415
+ }
416
+ });
417
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
418
+ where: params
419
+ });
420
+ for (const release2 of releases) {
421
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
422
+ }
423
+ };
424
+ const deleteActionsOnDelete = async (ctx, next) => {
425
+ if (ctx.action !== "delete") {
426
+ return next();
427
+ }
428
+ if (!utils.contentTypes.hasDraftAndPublish(ctx.contentType)) {
429
+ return next();
430
+ }
431
+ const contentType = ctx.contentType.uid;
432
+ const { documentId, locale } = ctx.params;
433
+ const result = await next();
434
+ if (!result) {
435
+ return result;
436
+ }
437
+ try {
438
+ deleteActionsAndUpdateReleaseStatus({
439
+ contentType,
440
+ entryDocumentId: documentId,
441
+ ...locale !== "*" && { locale }
442
+ });
443
+ } catch (error) {
444
+ strapi.log.error("Error while deleting release actions after delete", {
445
+ error
446
+ });
447
+ }
448
+ return result;
449
+ };
450
+ const updateActionsOnUpdate = async (ctx, next) => {
451
+ if (ctx.action !== "update") {
452
+ return next();
453
+ }
454
+ if (!utils.contentTypes.hasDraftAndPublish(ctx.contentType)) {
455
+ return next();
456
+ }
457
+ const contentType = ctx.contentType.uid;
458
+ const result = await next();
459
+ if (!result) {
460
+ return result;
461
+ }
462
+ try {
463
+ updateActionsStatusAndUpdateReleaseStatus(contentType, result);
464
+ } catch (error) {
465
+ strapi.log.error("Error while updating release actions after update", {
466
+ error
467
+ });
468
+ }
469
+ return result;
470
+ };
471
+ const deleteReleasesActionsAndUpdateReleaseStatus = async (params) => {
472
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
473
+ where: {
474
+ actions: params
475
+ }
476
+ });
477
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
478
+ where: params
479
+ });
480
+ for (const release2 of releases) {
481
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
240
482
  }
241
483
  };
242
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
243
484
  const bootstrap = async ({ strapi: strapi2 }) => {
244
- if (features$1.isEnabled("cms-content-releases")) {
485
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
245
486
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
246
487
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
247
488
  );
248
489
  strapi2.db.lifecycles.subscribe({
249
490
  models: contentTypesWithDraftAndPublish,
250
- async afterDelete(event) {
251
- try {
252
- const { model, result } = event;
253
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
254
- const { id } = result;
255
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
256
- where: {
257
- actions: {
258
- target_type: model.uid,
259
- target_id: id
260
- }
261
- }
262
- });
263
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
264
- where: {
265
- target_type: model.uid,
266
- target_id: id
267
- }
268
- });
269
- for (const release2 of releases) {
270
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
271
- }
272
- }
273
- } catch (error) {
274
- strapi2.log.error("Error while deleting release actions after entry delete", { error });
275
- }
276
- },
277
- /**
278
- * deleteMany hook doesn't return the deleted entries ids
279
- * so we need to fetch them before deleting the entries to save the ids on our state
280
- */
281
- async beforeDeleteMany(event) {
282
- const { model, params } = event;
283
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
284
- const { where } = params;
285
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
286
- event.state.entriesToDelete = entriesToDelete;
287
- }
288
- },
289
491
  /**
290
- * We delete the release actions related to deleted entries
291
- * We make this only after deleteMany is succesfully executed to avoid errors
492
+ * deleteMany is still used outside documents service, for example when deleting a locale
292
493
  */
293
494
  async afterDeleteMany(event) {
294
495
  try {
295
- const { model, state } = event;
296
- const entriesToDelete = state.entriesToDelete;
297
- if (entriesToDelete) {
298
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
299
- where: {
300
- actions: {
301
- target_type: model.uid,
302
- target_id: {
303
- $in: entriesToDelete.map(
304
- (entry) => entry.id
305
- )
306
- }
307
- }
308
- }
309
- });
310
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
311
- where: {
312
- target_type: model.uid,
313
- target_id: {
314
- $in: entriesToDelete.map((entry) => entry.id)
315
- }
316
- }
496
+ const model = strapi2.getModel(event.model.uid);
497
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
498
+ const { where } = event.params;
499
+ deleteReleasesActionsAndUpdateReleaseStatus({
500
+ contentType: model.uid,
501
+ locale: where?.locale ?? null,
502
+ ...where?.documentId && { entryDocumentId: where.documentId }
317
503
  });
318
- for (const release2 of releases) {
319
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
320
- }
321
504
  }
322
505
  } catch (error) {
323
506
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
324
507
  error
325
508
  });
326
509
  }
327
- },
328
- async afterUpdate(event) {
329
- try {
330
- const { model, result } = event;
331
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
332
- const isEntryValid = await getEntryValidStatus(
333
- model.uid,
334
- result,
335
- {
336
- strapi: strapi2
337
- }
338
- );
339
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
340
- where: {
341
- target_type: model.uid,
342
- target_id: result.id
343
- },
344
- data: {
345
- isEntryValid
346
- }
347
- });
348
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
349
- where: {
350
- actions: {
351
- target_type: model.uid,
352
- target_id: result.id
353
- }
354
- }
355
- });
356
- for (const release2 of releases) {
357
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
358
- }
359
- }
360
- } catch (error) {
361
- strapi2.log.error("Error while updating release actions after entry update", { error });
362
- }
363
510
  }
364
511
  });
365
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
366
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
367
- strapi2.log.error(
368
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
369
- );
370
- throw err;
371
- });
372
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
373
- strapi2.webhookStore.addAllowedEvent(key, value);
374
- });
375
- }
512
+ strapi2.documents.use(deleteActionsOnDelete);
513
+ strapi2.documents.use(updateActionsOnUpdate);
514
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
515
+ strapi2.log.error(
516
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
517
+ );
518
+ throw err;
519
+ });
520
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
521
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
522
+ });
376
523
  }
377
524
  };
378
525
  const destroy = async ({ strapi: strapi2 }) => {
379
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
380
- const scheduledJobs = getService("scheduling", {
381
- strapi: strapi2
382
- }).getAll();
383
- for (const [, job] of scheduledJobs) {
384
- job.cancel();
385
- }
526
+ const scheduledJobs = getService("scheduling", {
527
+ strapi: strapi2
528
+ }).getAll();
529
+ for (const [, job] of scheduledJobs) {
530
+ job.cancel();
386
531
  }
387
532
  };
388
533
  const schema$1 = {
@@ -457,15 +602,13 @@ const schema = {
457
602
  enum: ["publish", "unpublish"],
458
603
  required: true
459
604
  },
460
- entry: {
461
- type: "relation",
462
- relation: "morphToOne",
463
- configurable: false
464
- },
465
605
  contentType: {
466
606
  type: "string",
467
607
  required: true
468
608
  },
609
+ entryDocumentId: {
610
+ type: "string"
611
+ },
469
612
  locale: {
470
613
  type: "string"
471
614
  },
@@ -487,18 +630,6 @@ const contentTypes = {
487
630
  release: release$1,
488
631
  "release-action": releaseAction$1
489
632
  };
490
- const getGroupName = (queryValue) => {
491
- switch (queryValue) {
492
- case "contentType":
493
- return "contentType.displayName";
494
- case "action":
495
- return "type";
496
- case "locale":
497
- return ___default.default.getOr("No locale", "locale.name");
498
- default:
499
- return "contentType.displayName";
500
- }
501
- };
502
633
  const createReleaseService = ({ strapi: strapi2 }) => {
503
634
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
504
635
  strapi2.eventHub.emit(event, {
@@ -507,6 +638,33 @@ const createReleaseService = ({ strapi: strapi2 }) => {
507
638
  release: release2
508
639
  });
509
640
  };
641
+ const getFormattedActions = async (releaseId) => {
642
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
643
+ where: {
644
+ release: {
645
+ id: releaseId
646
+ }
647
+ }
648
+ });
649
+ if (actions.length === 0) {
650
+ throw new utils.errors.ValidationError("No entries to publish");
651
+ }
652
+ const formattedActions = {};
653
+ for (const action of actions) {
654
+ const contentTypeUid = action.contentType;
655
+ if (!formattedActions[contentTypeUid]) {
656
+ formattedActions[contentTypeUid] = {
657
+ publish: [],
658
+ unpublish: []
659
+ };
660
+ }
661
+ formattedActions[contentTypeUid][action.type].push({
662
+ documentId: action.entryDocumentId,
663
+ locale: action.locale
664
+ });
665
+ }
666
+ return formattedActions;
667
+ };
510
668
  return {
511
669
  async create(releaseData, { user }) {
512
670
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -520,13 +678,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
520
678
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
521
679
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
522
680
  ]);
523
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
681
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
524
682
  data: {
525
683
  ...releaseWithCreatorFields,
526
684
  status: "empty"
527
685
  }
528
686
  });
529
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
687
+ if (releaseWithCreatorFields.scheduledAt) {
530
688
  const schedulingService = getService("scheduling", { strapi: strapi2 });
531
689
  await schedulingService.set(release2.id, release2.scheduledAt);
532
690
  }
@@ -534,94 +692,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
534
692
  return release2;
535
693
  },
536
694
  async findOne(id, query = {}) {
537
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
538
- ...query
695
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
696
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
697
+ ...dbQuery,
698
+ where: { id }
539
699
  });
540
700
  return release2;
541
701
  },
542
702
  findPage(query) {
543
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
544
- ...query,
703
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
704
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
705
+ ...dbQuery,
545
706
  populate: {
546
707
  actions: {
547
- // @ts-expect-error Ignore missing properties
548
708
  count: true
549
709
  }
550
710
  }
551
711
  });
552
712
  },
553
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
554
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
555
- where: {
556
- actions: {
557
- target_type: contentTypeUid,
558
- target_id: entryId
559
- },
560
- releasedAt: {
561
- $null: true
562
- }
563
- },
564
- populate: {
565
- // Filter the action to get only the content type entry
566
- actions: {
567
- where: {
568
- target_type: contentTypeUid,
569
- target_id: entryId
570
- }
571
- }
572
- }
573
- });
574
- return releases.map((release2) => {
575
- if (release2.actions?.length) {
576
- const [actionForEntry] = release2.actions;
577
- delete release2.actions;
578
- return {
579
- ...release2,
580
- action: actionForEntry
581
- };
582
- }
583
- return release2;
584
- });
585
- },
586
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
587
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
588
- where: {
589
- releasedAt: {
590
- $null: true
591
- },
592
- actions: {
593
- target_type: contentTypeUid,
594
- target_id: entryId
595
- }
596
- }
597
- });
598
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
599
- where: {
600
- $or: [
601
- {
602
- id: {
603
- $notIn: releasesRelated.map((release2) => release2.id)
604
- }
605
- },
606
- {
607
- actions: null
608
- }
609
- ],
610
- releasedAt: {
611
- $null: true
612
- }
613
- }
614
- });
615
- return releases.map((release2) => {
616
- if (release2.actions?.length) {
617
- const [actionForEntry] = release2.actions;
618
- delete release2.actions;
619
- return {
620
- ...release2,
621
- action: actionForEntry
622
- };
623
- }
624
- return release2;
713
+ findMany(query) {
714
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
715
+ return strapi2.db.query(RELEASE_MODEL_UID).findMany({
716
+ ...dbQuery
625
717
  });
626
718
  },
627
719
  async update(id, releaseData, { user }) {
@@ -636,157 +728,27 @@ const createReleaseService = ({ strapi: strapi2 }) => {
636
728
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
637
729
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
638
730
  ]);
639
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
731
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
640
732
  if (!release2) {
641
733
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
642
734
  }
643
735
  if (release2.releasedAt) {
644
736
  throw new utils.errors.ValidationError("Release already published");
645
737
  }
646
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
647
- /*
648
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
649
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
650
- */
651
- // @ts-expect-error see above
738
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
739
+ where: { id },
652
740
  data: releaseWithCreatorFields
653
741
  });
654
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
655
- const schedulingService = getService("scheduling", { strapi: strapi2 });
656
- if (releaseData.scheduledAt) {
657
- await schedulingService.set(id, releaseData.scheduledAt);
658
- } else if (release2.scheduledAt) {
659
- schedulingService.cancel(id);
660
- }
742
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
743
+ if (releaseData.scheduledAt) {
744
+ await schedulingService.set(id, releaseData.scheduledAt);
745
+ } else if (release2.scheduledAt) {
746
+ schedulingService.cancel(id);
661
747
  }
662
748
  this.updateReleaseStatus(id);
663
749
  strapi2.telemetry.send("didUpdateContentRelease");
664
750
  return updatedRelease;
665
751
  },
666
- async createAction(releaseId, action) {
667
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
668
- strapi: strapi2
669
- });
670
- await Promise.all([
671
- validateEntryContentType(action.entry.contentType),
672
- validateUniqueEntry(releaseId, action)
673
- ]);
674
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
675
- if (!release2) {
676
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
677
- }
678
- if (release2.releasedAt) {
679
- throw new utils.errors.ValidationError("Release already published");
680
- }
681
- const { entry, type } = action;
682
- const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
683
- const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
684
- const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
685
- data: {
686
- type,
687
- contentType: entry.contentType,
688
- locale: entry.locale,
689
- isEntryValid,
690
- entry: {
691
- id: entry.id,
692
- __type: entry.contentType,
693
- __pivot: { field: "entry" }
694
- },
695
- release: releaseId
696
- },
697
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
698
- });
699
- this.updateReleaseStatus(releaseId);
700
- return releaseAction2;
701
- },
702
- async findActions(releaseId, query) {
703
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
704
- fields: ["id"]
705
- });
706
- if (!release2) {
707
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
708
- }
709
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
710
- ...query,
711
- populate: {
712
- entry: {
713
- populate: "*"
714
- }
715
- },
716
- filters: {
717
- release: releaseId
718
- }
719
- });
720
- },
721
- async countActions(query) {
722
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
723
- },
724
- async groupActions(actions, groupBy) {
725
- const contentTypeUids = actions.reduce((acc, action) => {
726
- if (!acc.includes(action.contentType)) {
727
- acc.push(action.contentType);
728
- }
729
- return acc;
730
- }, []);
731
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
732
- contentTypeUids
733
- );
734
- const allLocalesDictionary = await this.getLocalesDataForActions();
735
- const formattedData = actions.map((action) => {
736
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
737
- return {
738
- ...action,
739
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
740
- contentType: {
741
- displayName,
742
- mainFieldValue: action.entry[mainField],
743
- uid: action.contentType
744
- }
745
- };
746
- });
747
- const groupName = getGroupName(groupBy);
748
- return ___default.default.groupBy(groupName)(formattedData);
749
- },
750
- async getLocalesDataForActions() {
751
- if (!strapi2.plugin("i18n")) {
752
- return {};
753
- }
754
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
755
- return allLocales.reduce((acc, locale) => {
756
- acc[locale.code] = { name: locale.name, code: locale.code };
757
- return acc;
758
- }, {});
759
- },
760
- async getContentTypesDataForActions(contentTypesUids) {
761
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
762
- const contentTypesData = {};
763
- for (const contentTypeUid of contentTypesUids) {
764
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
765
- uid: contentTypeUid
766
- });
767
- contentTypesData[contentTypeUid] = {
768
- mainField: contentTypeConfig.settings.mainField,
769
- displayName: strapi2.getModel(contentTypeUid).info.displayName
770
- };
771
- }
772
- return contentTypesData;
773
- },
774
- getContentTypeModelsFromActions(actions) {
775
- const contentTypeUids = actions.reduce((acc, action) => {
776
- if (!acc.includes(action.contentType)) {
777
- acc.push(action.contentType);
778
- }
779
- return acc;
780
- }, []);
781
- const contentTypeModelsMap = contentTypeUids.reduce(
782
- (acc, contentTypeUid) => {
783
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
784
- return acc;
785
- },
786
- {}
787
- );
788
- return contentTypeModelsMap;
789
- },
790
752
  async getAllComponents() {
791
753
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
792
754
  const components = await contentManagerComponentsService.findAllComponents();
@@ -800,10 +762,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
800
762
  return componentsMap;
801
763
  },
802
764
  async delete(releaseId) {
803
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
765
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
766
+ where: { id: releaseId },
804
767
  populate: {
805
768
  actions: {
806
- fields: ["id"]
769
+ select: ["id"]
807
770
  }
808
771
  }
809
772
  });
@@ -821,9 +784,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
821
784
  }
822
785
  }
823
786
  });
824
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
787
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
788
+ where: {
789
+ id: releaseId
790
+ }
791
+ });
825
792
  });
826
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
793
+ if (release2.scheduledAt) {
827
794
  const schedulingService = getService("scheduling", { strapi: strapi2 });
828
795
  await schedulingService.cancel(release2.id);
829
796
  }
@@ -831,148 +798,294 @@ const createReleaseService = ({ strapi: strapi2 }) => {
831
798
  return release2;
832
799
  },
833
800
  async publish(releaseId) {
834
- try {
835
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
836
- RELEASE_MODEL_UID,
837
- releaseId,
838
- {
839
- populate: {
840
- actions: {
841
- populate: {
842
- entry: {
843
- fields: ["id"]
844
- }
845
- }
846
- }
847
- }
848
- }
849
- );
850
- if (!releaseWithPopulatedActionEntries) {
801
+ const {
802
+ release: release2,
803
+ error
804
+ } = await strapi2.db.transaction(async ({ trx }) => {
805
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
806
+ if (!lockedRelease) {
851
807
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
852
808
  }
853
- if (releaseWithPopulatedActionEntries.releasedAt) {
809
+ if (lockedRelease.releasedAt) {
854
810
  throw new utils.errors.ValidationError("Release already published");
855
811
  }
856
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
857
- throw new utils.errors.ValidationError("No entries to publish");
812
+ if (lockedRelease.status === "failed") {
813
+ throw new utils.errors.ValidationError("Release failed to publish");
858
814
  }
859
- const collectionTypeActions = {};
860
- const singleTypeActions = [];
861
- for (const action of releaseWithPopulatedActionEntries.actions) {
862
- const contentTypeUid = action.contentType;
863
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
864
- if (!collectionTypeActions[contentTypeUid]) {
865
- collectionTypeActions[contentTypeUid] = {
866
- entriestoPublishIds: [],
867
- entriesToUnpublishIds: []
868
- };
869
- }
870
- if (action.type === "publish") {
871
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
872
- } else {
873
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
874
- }
875
- } else {
876
- singleTypeActions.push({
877
- uid: contentTypeUid,
878
- action: action.type,
879
- id: action.entry.id
880
- });
881
- }
882
- }
883
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
884
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
885
- await strapi2.db.transaction(async () => {
886
- for (const { uid, action, id } of singleTypeActions) {
887
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
888
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
889
- try {
890
- if (action === "publish") {
891
- await entityManagerService.publish(entry, uid);
892
- } else {
893
- await entityManagerService.unpublish(entry, uid);
894
- }
895
- } catch (error) {
896
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
897
- } else {
898
- throw error;
899
- }
900
- }
901
- }
902
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
903
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
904
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
905
- const entriesToPublish = await strapi2.entityService.findMany(
906
- contentTypeUid,
907
- {
908
- filters: {
909
- id: {
910
- $in: entriestoPublishIds
911
- }
912
- },
913
- populate
914
- }
915
- );
916
- const entriesToUnpublish = await strapi2.entityService.findMany(
917
- contentTypeUid,
918
- {
919
- filters: {
920
- id: {
921
- $in: entriesToUnpublishIds
922
- }
923
- },
924
- populate
925
- }
926
- );
927
- if (entriesToPublish.length > 0) {
928
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
929
- }
930
- if (entriesToUnpublish.length > 0) {
931
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
932
- }
933
- }
934
- });
935
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
936
- data: {
937
- /*
938
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
939
- */
940
- // @ts-expect-error see above
941
- releasedAt: /* @__PURE__ */ new Date()
942
- },
943
- populate: {
944
- actions: {
945
- // @ts-expect-error is not expecting count but it is working
946
- count: true
815
+ try {
816
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
817
+ const formattedActions = await getFormattedActions(releaseId);
818
+ await strapi2.db.transaction(
819
+ async () => Promise.all(
820
+ Object.keys(formattedActions).map(async (contentTypeUid) => {
821
+ const contentType = contentTypeUid;
822
+ const { publish, unpublish } = formattedActions[contentType];
823
+ return Promise.all([
824
+ ...publish.map((params) => strapi2.documents(contentType).publish(params)),
825
+ ...unpublish.map((params) => strapi2.documents(contentType).unpublish(params))
826
+ ]);
827
+ })
828
+ )
829
+ );
830
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
831
+ where: {
832
+ id: releaseId
833
+ },
834
+ data: {
835
+ status: "done",
836
+ releasedAt: /* @__PURE__ */ new Date()
947
837
  }
948
- }
949
- });
950
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
838
+ });
951
839
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
952
840
  isPublished: true,
953
- release: release2
841
+ release: release22
954
842
  });
955
- }
956
- strapi2.telemetry.send("didPublishContentRelease");
957
- return release2;
958
- } catch (error) {
959
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
843
+ strapi2.telemetry.send("didPublishContentRelease");
844
+ return { release: release22, error: null };
845
+ } catch (error2) {
960
846
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
961
847
  isPublished: false,
962
- error
848
+ error: error2
849
+ });
850
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
851
+ status: "failed"
852
+ }).transacting(trx).execute();
853
+ return {
854
+ release: null,
855
+ error: error2
856
+ };
857
+ }
858
+ });
859
+ if (error instanceof Error) {
860
+ throw error;
861
+ }
862
+ return release2;
863
+ },
864
+ async updateReleaseStatus(releaseId) {
865
+ const releaseActionService = getService("release-action", { strapi: strapi2 });
866
+ const [totalActions, invalidActions] = await Promise.all([
867
+ releaseActionService.countActions({
868
+ filters: {
869
+ release: releaseId
870
+ }
871
+ }),
872
+ releaseActionService.countActions({
873
+ filters: {
874
+ release: releaseId,
875
+ isEntryValid: false
876
+ }
877
+ })
878
+ ]);
879
+ if (totalActions > 0) {
880
+ if (invalidActions > 0) {
881
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
882
+ where: {
883
+ id: releaseId
884
+ },
885
+ data: {
886
+ status: "blocked"
887
+ }
963
888
  });
964
889
  }
965
- strapi2.db.query(RELEASE_MODEL_UID).update({
966
- where: { id: releaseId },
890
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
891
+ where: {
892
+ id: releaseId
893
+ },
967
894
  data: {
968
- status: "failed"
895
+ status: "ready"
969
896
  }
970
897
  });
971
- throw error;
972
898
  }
899
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
900
+ where: {
901
+ id: releaseId
902
+ },
903
+ data: {
904
+ status: "empty"
905
+ }
906
+ });
907
+ }
908
+ };
909
+ };
910
+ const getGroupName = (queryValue) => {
911
+ switch (queryValue) {
912
+ case "contentType":
913
+ return "contentType.displayName";
914
+ case "type":
915
+ return "type";
916
+ case "locale":
917
+ return ___default.default.getOr("No locale", "locale.name");
918
+ default:
919
+ return "contentType.displayName";
920
+ }
921
+ };
922
+ const createReleaseActionService = ({ strapi: strapi2 }) => {
923
+ const getLocalesDataForActions = async () => {
924
+ if (!strapi2.plugin("i18n")) {
925
+ return {};
926
+ }
927
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
928
+ return allLocales.reduce((acc, locale) => {
929
+ acc[locale.code] = { name: locale.name, code: locale.code };
930
+ return acc;
931
+ }, {});
932
+ };
933
+ const getContentTypesDataForActions = async (contentTypesUids) => {
934
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
935
+ const contentTypesData = {};
936
+ for (const contentTypeUid of contentTypesUids) {
937
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
938
+ uid: contentTypeUid
939
+ });
940
+ contentTypesData[contentTypeUid] = {
941
+ mainField: contentTypeConfig.settings.mainField,
942
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
943
+ };
944
+ }
945
+ return contentTypesData;
946
+ };
947
+ return {
948
+ async create(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
949
+ const { validateEntryData, validateUniqueEntry } = getService("release-validation", {
950
+ strapi: strapi2
951
+ });
952
+ await Promise.all([
953
+ validateEntryData(action.contentType, action.entryDocumentId),
954
+ validateUniqueEntry(releaseId, action)
955
+ ]);
956
+ const model = strapi2.contentType(action.contentType);
957
+ if (model.kind === "singleType") {
958
+ const document = await strapi2.db.query(model.uid).findOne({ select: ["documentId"] });
959
+ if (!document) {
960
+ throw new utils.errors.NotFoundError(`No entry found for contentType ${action.contentType}`);
961
+ }
962
+ action.entryDocumentId = document.documentId;
963
+ }
964
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
965
+ if (!release2) {
966
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
967
+ }
968
+ if (release2.releasedAt) {
969
+ throw new utils.errors.ValidationError("Release already published");
970
+ }
971
+ const actionStatus = action.type === "publish" ? await getDraftEntryValidStatus(
972
+ {
973
+ contentType: action.contentType,
974
+ documentId: action.entryDocumentId,
975
+ locale: action.locale
976
+ },
977
+ {
978
+ strapi: strapi2
979
+ }
980
+ ) : true;
981
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
982
+ data: {
983
+ ...action,
984
+ release: release2.id,
985
+ isEntryValid: actionStatus
986
+ },
987
+ populate: { release: { select: ["id"] } }
988
+ });
989
+ if (!disableUpdateReleaseStatus) {
990
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
991
+ }
992
+ return releaseAction2;
993
+ },
994
+ async findPage(releaseId, query) {
995
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
996
+ where: { id: releaseId },
997
+ select: ["id"]
998
+ });
999
+ if (!release2) {
1000
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1001
+ }
1002
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1003
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
1004
+ ...dbQuery,
1005
+ where: {
1006
+ release: releaseId
1007
+ }
1008
+ });
1009
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
1010
+ const actionsWithEntry = await utils.async.map(actions, async (action) => {
1011
+ const populate = await populateBuilderService(action.contentType).populateDeep(Infinity).build();
1012
+ const entry = await getEntry(
1013
+ {
1014
+ contentType: action.contentType,
1015
+ documentId: action.entryDocumentId,
1016
+ locale: action.locale,
1017
+ populate,
1018
+ status: action.type === "publish" ? "draft" : "published"
1019
+ },
1020
+ { strapi: strapi2 }
1021
+ );
1022
+ return {
1023
+ ...action,
1024
+ entry,
1025
+ status: entry ? await getEntryStatus(action.contentType, entry) : null
1026
+ };
1027
+ });
1028
+ return {
1029
+ results: actionsWithEntry,
1030
+ pagination
1031
+ };
1032
+ },
1033
+ async groupActions(actions, groupBy) {
1034
+ const contentTypeUids = actions.reduce((acc, action) => {
1035
+ if (!acc.includes(action.contentType)) {
1036
+ acc.push(action.contentType);
1037
+ }
1038
+ return acc;
1039
+ }, []);
1040
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1041
+ const allLocalesDictionary = await getLocalesDataForActions();
1042
+ const formattedData = actions.map((action) => {
1043
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
1044
+ return {
1045
+ ...action,
1046
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
1047
+ contentType: {
1048
+ displayName,
1049
+ mainFieldValue: action.entry[mainField],
1050
+ uid: action.contentType
1051
+ }
1052
+ };
1053
+ });
1054
+ const groupName = getGroupName(groupBy);
1055
+ return ___default.default.groupBy(groupName)(formattedData);
1056
+ },
1057
+ async getContentTypeModelsFromActions(actions) {
1058
+ const contentTypeUids = actions.reduce((acc, action) => {
1059
+ if (!acc.includes(action.contentType)) {
1060
+ acc.push(action.contentType);
1061
+ }
1062
+ return acc;
1063
+ }, []);
1064
+ const workflowsService = strapi2.plugin("review-workflows").service("workflows");
1065
+ const contentTypeModelsMap = await utils.async.reduce(contentTypeUids)(
1066
+ async (accPromise, contentTypeUid) => {
1067
+ const acc = await accPromise;
1068
+ const contentTypeModel = strapi2.getModel(contentTypeUid);
1069
+ const workflow = await workflowsService.getAssignedWorkflow(contentTypeUid, {
1070
+ populate: "stageRequiredToPublish"
1071
+ });
1072
+ acc[contentTypeUid] = {
1073
+ ...contentTypeModel,
1074
+ hasReviewWorkflow: !!workflow,
1075
+ stageRequiredToPublish: workflow?.stageRequiredToPublish
1076
+ };
1077
+ return acc;
1078
+ },
1079
+ {}
1080
+ );
1081
+ return contentTypeModelsMap;
973
1082
  },
974
- async updateAction(actionId, releaseId, update) {
975
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1083
+ async countActions(query) {
1084
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1085
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
1086
+ },
1087
+ async update(actionId, releaseId, update) {
1088
+ const action = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findOne({
976
1089
  where: {
977
1090
  id: actionId,
978
1091
  release: {
@@ -981,17 +1094,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
981
1094
  $null: true
982
1095
  }
983
1096
  }
984
- },
985
- data: update
1097
+ }
986
1098
  });
987
- if (!updatedAction) {
1099
+ if (!action) {
988
1100
  throw new utils.errors.NotFoundError(
989
1101
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
990
1102
  );
991
1103
  }
1104
+ const actionStatus = update.type === "publish" ? await getDraftEntryValidStatus(
1105
+ {
1106
+ contentType: action.contentType,
1107
+ documentId: action.entryDocumentId,
1108
+ locale: action.locale
1109
+ },
1110
+ {
1111
+ strapi: strapi2
1112
+ }
1113
+ ) : true;
1114
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1115
+ where: {
1116
+ id: actionId,
1117
+ release: {
1118
+ id: releaseId,
1119
+ releasedAt: {
1120
+ $null: true
1121
+ }
1122
+ }
1123
+ },
1124
+ data: {
1125
+ ...update,
1126
+ isEntryValid: actionStatus
1127
+ }
1128
+ });
1129
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
992
1130
  return updatedAction;
993
1131
  },
994
- async deleteAction(actionId, releaseId) {
1132
+ async delete(actionId, releaseId) {
995
1133
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
996
1134
  where: {
997
1135
  id: actionId,
@@ -1008,87 +1146,104 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1008
1146
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1009
1147
  );
1010
1148
  }
1011
- this.updateReleaseStatus(releaseId);
1149
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1012
1150
  return deletedAction;
1013
1151
  },
1014
- async updateReleaseStatus(releaseId) {
1015
- const [totalActions, invalidActions] = await Promise.all([
1016
- this.countActions({
1017
- filters: {
1018
- release: releaseId
1019
- }
1020
- }),
1021
- this.countActions({
1022
- filters: {
1023
- release: releaseId,
1024
- isEntryValid: false
1025
- }
1026
- })
1027
- ]);
1028
- if (totalActions > 0) {
1029
- if (invalidActions > 0) {
1030
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1031
- where: {
1032
- id: releaseId
1033
- },
1034
- data: {
1035
- status: "blocked"
1152
+ async validateActionsByContentTypes(contentTypeUids) {
1153
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
1154
+ where: {
1155
+ contentType: {
1156
+ $in: contentTypeUids
1157
+ },
1158
+ // We only want to validate actions that are going to be published
1159
+ type: "publish",
1160
+ release: {
1161
+ releasedAt: {
1162
+ $null: true
1036
1163
  }
1037
- });
1038
- }
1039
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1164
+ }
1165
+ },
1166
+ populate: { release: true }
1167
+ });
1168
+ const releasesUpdated = [];
1169
+ await utils.async.map(actions, async (action) => {
1170
+ const isValid = await getDraftEntryValidStatus(
1171
+ {
1172
+ contentType: action.contentType,
1173
+ documentId: action.entryDocumentId,
1174
+ locale: action.locale
1175
+ },
1176
+ { strapi: strapi2 }
1177
+ );
1178
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1040
1179
  where: {
1041
- id: releaseId
1180
+ id: action.id
1042
1181
  },
1043
1182
  data: {
1044
- status: "ready"
1183
+ isEntryValid: isValid
1045
1184
  }
1046
1185
  });
1047
- }
1048
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1049
- where: {
1050
- id: releaseId
1051
- },
1052
- data: {
1053
- status: "empty"
1186
+ if (!releasesUpdated.includes(action.release.id)) {
1187
+ releasesUpdated.push(action.release.id);
1054
1188
  }
1189
+ return {
1190
+ id: action.id,
1191
+ isEntryValid: isValid
1192
+ };
1055
1193
  });
1194
+ if (releasesUpdated.length > 0) {
1195
+ await utils.async.map(releasesUpdated, async (releaseId) => {
1196
+ await getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1197
+ });
1198
+ }
1056
1199
  }
1057
1200
  };
1058
1201
  };
1202
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1203
+ constructor(message) {
1204
+ super(message);
1205
+ this.name = "AlreadyOnReleaseError";
1206
+ }
1207
+ }
1059
1208
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1060
1209
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1061
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1062
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1210
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1211
+ where: {
1212
+ id: releaseId
1213
+ },
1214
+ populate: {
1215
+ actions: true
1216
+ }
1063
1217
  });
1064
1218
  if (!release2) {
1065
1219
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1066
1220
  }
1067
1221
  const isEntryInRelease = release2.actions.some(
1068
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1222
+ (action) => action.entryDocumentId === releaseActionArgs.entryDocumentId && action.contentType === releaseActionArgs.contentType && (releaseActionArgs.locale ? action.locale === releaseActionArgs.locale : true)
1069
1223
  );
1070
1224
  if (isEntryInRelease) {
1071
- throw new utils.errors.ValidationError(
1072
- `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1225
+ throw new AlreadyOnReleaseError(
1226
+ `Entry with documentId ${releaseActionArgs.entryDocumentId}${releaseActionArgs.locale ? `( ${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1073
1227
  );
1074
1228
  }
1075
1229
  },
1076
- validateEntryContentType(contentTypeUid) {
1230
+ validateEntryData(contentTypeUid, entryDocumentId) {
1077
1231
  const contentType = strapi2.contentType(contentTypeUid);
1078
1232
  if (!contentType) {
1079
1233
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1080
1234
  }
1081
- if (!contentType.options?.draftAndPublish) {
1235
+ if (!utils.contentTypes.hasDraftAndPublish(contentType)) {
1082
1236
  throw new utils.errors.ValidationError(
1083
1237
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1084
1238
  );
1085
1239
  }
1240
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1241
+ throw new utils.errors.ValidationError("Document id is required for collection type");
1242
+ }
1086
1243
  },
1087
1244
  async validatePendingReleasesLimit() {
1088
- const maximumPendingReleases = (
1089
- // @ts-expect-error - options is not typed into features
1090
- EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
1091
- );
1245
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1246
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1092
1247
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1093
1248
  filters: {
1094
1249
  releasedAt: {
@@ -1101,8 +1256,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1101
1256
  }
1102
1257
  },
1103
1258
  async validateUniqueNameForPendingRelease(name, id) {
1104
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1105
- filters: {
1259
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1260
+ where: {
1106
1261
  releasedAt: {
1107
1262
  $null: true
1108
1263
  },
@@ -1131,7 +1286,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1131
1286
  }
1132
1287
  const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1133
1288
  try {
1134
- await getService("release").publish(releaseId);
1289
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1135
1290
  } catch (error) {
1136
1291
  }
1137
1292
  this.cancel(releaseId);
@@ -1173,85 +1328,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1173
1328
  }
1174
1329
  };
1175
1330
  };
1331
+ const DEFAULT_SETTINGS = {
1332
+ defaultTimezone: null
1333
+ };
1334
+ const createSettingsService = ({ strapi: strapi2 }) => {
1335
+ const getStore = async () => strapi2.store({ type: "core", name: "content-releases" });
1336
+ return {
1337
+ async update({ settings: settings2 }) {
1338
+ const store = await getStore();
1339
+ store.set({ key: "settings", value: settings2 });
1340
+ return settings2;
1341
+ },
1342
+ async find() {
1343
+ const store = await getStore();
1344
+ const settings2 = await store.get({ key: "settings" });
1345
+ return {
1346
+ ...DEFAULT_SETTINGS,
1347
+ ...settings2 || {}
1348
+ };
1349
+ }
1350
+ };
1351
+ };
1176
1352
  const services = {
1177
1353
  release: createReleaseService,
1354
+ "release-action": createReleaseActionService,
1178
1355
  "release-validation": createReleaseValidationService,
1179
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1356
+ scheduling: createSchedulingService,
1357
+ settings: createSettingsService
1180
1358
  };
1181
- const RELEASE_SCHEMA = yup__namespace.object().shape({
1182
- name: yup__namespace.string().trim().required(),
1183
- scheduledAt: yup__namespace.string().nullable(),
1184
- isScheduled: yup__namespace.boolean().optional(),
1185
- time: yup__namespace.string().when("isScheduled", {
1186
- is: true,
1187
- then: yup__namespace.string().trim().required(),
1188
- otherwise: yup__namespace.string().nullable()
1189
- }),
1190
- timezone: yup__namespace.string().when("isScheduled", {
1191
- is: true,
1192
- then: yup__namespace.string().required().nullable(),
1193
- otherwise: yup__namespace.string().nullable()
1194
- }),
1195
- date: yup__namespace.string().when("isScheduled", {
1196
- is: true,
1197
- then: yup__namespace.string().required().nullable(),
1198
- otherwise: yup__namespace.string().nullable()
1359
+ const RELEASE_SCHEMA = utils.yup.object().shape({
1360
+ name: utils.yup.string().trim().required(),
1361
+ scheduledAt: utils.yup.string().nullable(),
1362
+ timezone: utils.yup.string().when("scheduledAt", {
1363
+ is: (value) => value !== null && value !== void 0,
1364
+ then: utils.yup.string().required(),
1365
+ otherwise: utils.yup.string().nullable()
1199
1366
  })
1200
1367
  }).required().noUnknown();
1368
+ const FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA = utils.yup.object().shape({
1369
+ contentType: utils.yup.string().required(),
1370
+ entryDocumentId: utils.yup.string().nullable(),
1371
+ hasEntryAttached: utils.yup.string().nullable(),
1372
+ locale: utils.yup.string().nullable()
1373
+ }).required().noUnknown();
1201
1374
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
1375
+ const validatefindByDocumentAttachedParams = utils.validateYupSchema(
1376
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1377
+ );
1202
1378
  const releaseController = {
1203
- async findMany(ctx) {
1204
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1379
+ /**
1380
+ * Find releases based on documents attached or not to the release.
1381
+ * If `hasEntryAttached` is true, it will return all releases that have the entry attached.
1382
+ * If `hasEntryAttached` is false, it will return all releases that don't have the entry attached.
1383
+ */
1384
+ async findByDocumentAttached(ctx) {
1385
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1205
1386
  ability: ctx.state.userAbility,
1206
1387
  model: RELEASE_MODEL_UID
1207
1388
  });
1208
1389
  await permissionsManager.validateQuery(ctx.query);
1209
1390
  const releaseService = getService("release", { strapi });
1210
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1211
- if (isFindManyForContentTypeEntry) {
1212
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1213
- const contentTypeUid = query.contentTypeUid;
1214
- const entryId = query.entryId;
1215
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1216
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1217
- ctx.body = { data };
1218
- } else {
1219
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1220
- const { results, pagination } = await releaseService.findPage(query);
1221
- const data = results.map((release2) => {
1222
- const { actions, ...releaseData } = release2;
1223
- return {
1224
- ...releaseData,
1391
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1392
+ await validatefindByDocumentAttachedParams(query);
1393
+ const model = strapi.getModel(query.contentType);
1394
+ if (model.kind && model.kind === "singleType") {
1395
+ const document = await strapi.db.query(model.uid).findOne({ select: ["documentId"] });
1396
+ if (!document) {
1397
+ throw new utils.errors.NotFoundError(`No entry found for contentType ${query.contentType}`);
1398
+ }
1399
+ query.entryDocumentId = document.documentId;
1400
+ }
1401
+ const { contentType, hasEntryAttached, entryDocumentId, locale } = query;
1402
+ const isEntryAttached = typeof hasEntryAttached === "string" ? Boolean(JSON.parse(hasEntryAttached)) : false;
1403
+ if (isEntryAttached) {
1404
+ const releases = await releaseService.findMany({
1405
+ where: {
1406
+ releasedAt: null,
1225
1407
  actions: {
1226
- meta: {
1227
- count: actions.count
1408
+ contentType,
1409
+ entryDocumentId: entryDocumentId ?? null,
1410
+ locale: locale ?? null
1411
+ }
1412
+ },
1413
+ populate: {
1414
+ actions: {
1415
+ fields: ["type"],
1416
+ filters: {
1417
+ contentType,
1418
+ entryDocumentId: entryDocumentId ?? null,
1419
+ locale: locale ?? null
1228
1420
  }
1229
1421
  }
1230
- };
1422
+ }
1423
+ });
1424
+ ctx.body = { data: releases };
1425
+ } else {
1426
+ const relatedReleases = await releaseService.findMany({
1427
+ where: {
1428
+ releasedAt: null,
1429
+ actions: {
1430
+ contentType,
1431
+ entryDocumentId: entryDocumentId ?? null,
1432
+ locale: locale ?? null
1433
+ }
1434
+ }
1231
1435
  });
1232
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1436
+ const releases = await releaseService.findMany({
1233
1437
  where: {
1438
+ $or: [
1439
+ {
1440
+ id: {
1441
+ $notIn: relatedReleases.map((release2) => release2.id)
1442
+ }
1443
+ },
1444
+ {
1445
+ actions: null
1446
+ }
1447
+ ],
1234
1448
  releasedAt: null
1235
1449
  }
1236
1450
  });
1237
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1451
+ ctx.body = { data: releases };
1238
1452
  }
1239
1453
  },
1454
+ async findPage(ctx) {
1455
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1456
+ ability: ctx.state.userAbility,
1457
+ model: RELEASE_MODEL_UID
1458
+ });
1459
+ await permissionsManager.validateQuery(ctx.query);
1460
+ const releaseService = getService("release", { strapi });
1461
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1462
+ const { results, pagination } = await releaseService.findPage(query);
1463
+ const data = results.map((release2) => {
1464
+ const { actions, ...releaseData } = release2;
1465
+ return {
1466
+ ...releaseData,
1467
+ actions: {
1468
+ meta: {
1469
+ count: actions.count
1470
+ }
1471
+ }
1472
+ };
1473
+ });
1474
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1475
+ where: {
1476
+ releasedAt: null
1477
+ }
1478
+ });
1479
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1480
+ },
1240
1481
  async findOne(ctx) {
1241
1482
  const id = ctx.params.id;
1242
1483
  const releaseService = getService("release", { strapi });
1484
+ const releaseActionService = getService("release-action", { strapi });
1243
1485
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1244
1486
  if (!release2) {
1245
1487
  throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1246
1488
  }
1247
- const count = await releaseService.countActions({
1489
+ const count = await releaseActionService.countActions({
1248
1490
  filters: {
1249
1491
  release: id
1250
1492
  }
1251
1493
  });
1252
1494
  const sanitizedRelease = {
1253
1495
  ...release2,
1254
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1496
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1255
1497
  };
1256
1498
  const data = {
1257
1499
  ...sanitizedRelease,
@@ -1263,19 +1505,63 @@ const releaseController = {
1263
1505
  };
1264
1506
  ctx.body = { data };
1265
1507
  },
1508
+ async mapEntriesToReleases(ctx) {
1509
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1510
+ if (!contentTypeUid || !documentIds) {
1511
+ throw new utils.errors.ValidationError("Missing required query parameters");
1512
+ }
1513
+ const releaseService = getService("release", { strapi });
1514
+ const releasesWithActions = await releaseService.findMany({
1515
+ where: {
1516
+ releasedAt: null,
1517
+ actions: {
1518
+ contentType: contentTypeUid,
1519
+ entryDocumentId: {
1520
+ $in: documentIds
1521
+ },
1522
+ locale
1523
+ }
1524
+ },
1525
+ populate: {
1526
+ actions: true
1527
+ }
1528
+ });
1529
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1530
+ (acc, release2) => {
1531
+ release2.actions.forEach((action) => {
1532
+ if (action.contentType !== contentTypeUid) {
1533
+ return;
1534
+ }
1535
+ if (locale && action.locale !== locale) {
1536
+ return;
1537
+ }
1538
+ if (!acc[action.entryDocumentId]) {
1539
+ acc[action.entryDocumentId] = [{ id: release2.id, name: release2.name }];
1540
+ } else {
1541
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1542
+ }
1543
+ });
1544
+ return acc;
1545
+ },
1546
+ {}
1547
+ );
1548
+ ctx.body = {
1549
+ data: mappedEntriesInReleases
1550
+ };
1551
+ },
1266
1552
  async create(ctx) {
1267
1553
  const user = ctx.state.user;
1268
1554
  const releaseArgs = ctx.request.body;
1269
1555
  await validateRelease(releaseArgs);
1270
1556
  const releaseService = getService("release", { strapi });
1271
1557
  const release2 = await releaseService.create(releaseArgs, { user });
1272
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1558
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1273
1559
  ability: ctx.state.userAbility,
1274
1560
  model: RELEASE_MODEL_UID
1275
1561
  });
1276
- ctx.body = {
1562
+ ctx.created({
1277
1563
  data: await permissionsManager.sanitizeOutput(release2)
1278
- };
1564
+ });
1279
1565
  },
1280
1566
  async update(ctx) {
1281
1567
  const user = ctx.state.user;
@@ -1284,7 +1570,7 @@ const releaseController = {
1284
1570
  await validateRelease(releaseArgs);
1285
1571
  const releaseService = getService("release", { strapi });
1286
1572
  const release2 = await releaseService.update(id, releaseArgs, { user });
1287
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1573
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1288
1574
  ability: ctx.state.userAbility,
1289
1575
  model: RELEASE_MODEL_UID
1290
1576
  });
@@ -1301,18 +1587,18 @@ const releaseController = {
1301
1587
  };
1302
1588
  },
1303
1589
  async publish(ctx) {
1304
- const user = ctx.state.user;
1305
1590
  const id = ctx.params.id;
1306
1591
  const releaseService = getService("release", { strapi });
1307
- const release2 = await releaseService.publish(id, { user });
1592
+ const releaseActionService = getService("release-action", { strapi });
1593
+ const release2 = await releaseService.publish(id);
1308
1594
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1309
- releaseService.countActions({
1595
+ releaseActionService.countActions({
1310
1596
  filters: {
1311
1597
  release: id,
1312
1598
  type: "publish"
1313
1599
  }
1314
1600
  }),
1315
- releaseService.countActions({
1601
+ releaseActionService.countActions({
1316
1602
  filters: {
1317
1603
  release: id,
1318
1604
  type: "unpublish"
@@ -1330,57 +1616,106 @@ const releaseController = {
1330
1616
  }
1331
1617
  };
1332
1618
  const RELEASE_ACTION_SCHEMA = utils.yup.object().shape({
1333
- entry: utils.yup.object().shape({
1334
- id: utils.yup.strapiID().required(),
1335
- contentType: utils.yup.string().required()
1336
- }).required(),
1619
+ contentType: utils.yup.string().required(),
1620
+ entryDocumentId: utils.yup.strapiID(),
1621
+ locale: utils.yup.string(),
1337
1622
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1338
1623
  });
1339
1624
  const RELEASE_ACTION_UPDATE_SCHEMA = utils.yup.object().shape({
1340
1625
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1341
1626
  });
1627
+ const FIND_MANY_ACTIONS_PARAMS = utils.yup.object().shape({
1628
+ groupBy: utils.yup.string().oneOf(["action", "contentType", "locale"])
1629
+ });
1342
1630
  const validateReleaseAction = utils.validateYupSchema(RELEASE_ACTION_SCHEMA);
1343
1631
  const validateReleaseActionUpdateSchema = utils.validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1632
+ const validateFindManyActionsParams = utils.validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1344
1633
  const releaseActionController = {
1345
1634
  async create(ctx) {
1346
1635
  const releaseId = ctx.params.releaseId;
1347
1636
  const releaseActionArgs = ctx.request.body;
1348
1637
  await validateReleaseAction(releaseActionArgs);
1349
- const releaseService = getService("release", { strapi });
1350
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1351
- ctx.body = {
1638
+ const releaseActionService = getService("release-action", { strapi });
1639
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1640
+ ctx.created({
1352
1641
  data: releaseAction2
1353
- };
1642
+ });
1643
+ },
1644
+ async createMany(ctx) {
1645
+ const releaseId = ctx.params.releaseId;
1646
+ const releaseActionsArgs = ctx.request.body;
1647
+ await Promise.all(
1648
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1649
+ );
1650
+ const releaseActionService = getService("release-action", { strapi });
1651
+ const releaseService = getService("release", { strapi });
1652
+ const releaseActions = await strapi.db.transaction(async () => {
1653
+ const releaseActions2 = await Promise.all(
1654
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1655
+ try {
1656
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1657
+ disableUpdateReleaseStatus: true
1658
+ });
1659
+ return action;
1660
+ } catch (error) {
1661
+ if (error instanceof AlreadyOnReleaseError) {
1662
+ return null;
1663
+ }
1664
+ throw error;
1665
+ }
1666
+ })
1667
+ );
1668
+ return releaseActions2;
1669
+ });
1670
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1671
+ if (newReleaseActions.length > 0) {
1672
+ releaseService.updateReleaseStatus(releaseId);
1673
+ }
1674
+ ctx.created({
1675
+ data: newReleaseActions,
1676
+ meta: {
1677
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1678
+ totalEntries: releaseActions.length
1679
+ }
1680
+ });
1354
1681
  },
1355
1682
  async findMany(ctx) {
1356
1683
  const releaseId = ctx.params.releaseId;
1357
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1684
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1358
1685
  ability: ctx.state.userAbility,
1359
1686
  model: RELEASE_ACTION_MODEL_UID
1360
1687
  });
1688
+ await validateFindManyActionsParams(ctx.query);
1689
+ if (ctx.query.groupBy) {
1690
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1691
+ ctx.badRequest("Invalid groupBy parameter");
1692
+ }
1693
+ }
1694
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1695
+ delete ctx.query.groupBy;
1361
1696
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1362
- const releaseService = getService("release", { strapi });
1363
- const { results, pagination } = await releaseService.findActions(releaseId, {
1364
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1697
+ const releaseActionService = getService("release-action", { strapi });
1698
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1365
1699
  ...query
1366
1700
  });
1367
1701
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1368
1702
  if (acc[action.contentType]) {
1369
1703
  return acc;
1370
1704
  }
1371
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1705
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1372
1706
  ability: ctx.state.userAbility,
1373
1707
  model: action.contentType
1374
1708
  });
1375
1709
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1376
1710
  return acc;
1377
1711
  }, {});
1378
- const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1712
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
1379
1713
  ...action,
1380
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1714
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1381
1715
  }));
1382
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1383
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1716
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1717
+ const contentTypes2 = await releaseActionService.getContentTypeModelsFromActions(results);
1718
+ const releaseService = getService("release", { strapi });
1384
1719
  const components = await releaseService.getAllComponents();
1385
1720
  ctx.body = {
1386
1721
  data: groupedData,
@@ -1396,8 +1731,8 @@ const releaseActionController = {
1396
1731
  const releaseId = ctx.params.releaseId;
1397
1732
  const releaseActionUpdateArgs = ctx.request.body;
1398
1733
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1399
- const releaseService = getService("release", { strapi });
1400
- const updatedAction = await releaseService.updateAction(
1734
+ const releaseActionService = getService("release-action", { strapi });
1735
+ const updatedAction = await releaseActionService.update(
1401
1736
  actionId,
1402
1737
  releaseId,
1403
1738
  releaseActionUpdateArgs
@@ -1409,17 +1744,71 @@ const releaseActionController = {
1409
1744
  async delete(ctx) {
1410
1745
  const actionId = ctx.params.actionId;
1411
1746
  const releaseId = ctx.params.releaseId;
1412
- const releaseService = getService("release", { strapi });
1413
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1747
+ const releaseActionService = getService("release-action", { strapi });
1748
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1414
1749
  ctx.body = {
1415
1750
  data: deletedReleaseAction
1416
1751
  };
1417
1752
  }
1418
1753
  };
1419
- const controllers = { release: releaseController, "release-action": releaseActionController };
1754
+ const SETTINGS_SCHEMA = yup__namespace.object().shape({
1755
+ defaultTimezone: yup__namespace.string().nullable().default(null)
1756
+ }).required().noUnknown();
1757
+ const validateSettings = utils.validateYupSchema(SETTINGS_SCHEMA);
1758
+ const settingsController = {
1759
+ async find(ctx) {
1760
+ const settingsService = getService("settings", { strapi });
1761
+ const settings2 = await settingsService.find();
1762
+ ctx.body = { data: settings2 };
1763
+ },
1764
+ async update(ctx) {
1765
+ const settingsBody = ctx.request.body;
1766
+ const settings2 = await validateSettings(settingsBody);
1767
+ const settingsService = getService("settings", { strapi });
1768
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1769
+ ctx.body = { data: updatedSettings };
1770
+ }
1771
+ };
1772
+ const controllers = {
1773
+ release: releaseController,
1774
+ "release-action": releaseActionController,
1775
+ settings: settingsController
1776
+ };
1420
1777
  const release = {
1421
1778
  type: "admin",
1422
1779
  routes: [
1780
+ {
1781
+ method: "GET",
1782
+ path: "/mapEntriesToReleases",
1783
+ handler: "release.mapEntriesToReleases",
1784
+ config: {
1785
+ policies: [
1786
+ "admin::isAuthenticatedAdmin",
1787
+ {
1788
+ name: "admin::hasPermissions",
1789
+ config: {
1790
+ actions: ["plugin::content-releases.read"]
1791
+ }
1792
+ }
1793
+ ]
1794
+ }
1795
+ },
1796
+ {
1797
+ method: "GET",
1798
+ path: "/getByDocumentAttached",
1799
+ handler: "release.findByDocumentAttached",
1800
+ config: {
1801
+ policies: [
1802
+ "admin::isAuthenticatedAdmin",
1803
+ {
1804
+ name: "admin::hasPermissions",
1805
+ config: {
1806
+ actions: ["plugin::content-releases.read"]
1807
+ }
1808
+ }
1809
+ ]
1810
+ }
1811
+ },
1423
1812
  {
1424
1813
  method: "POST",
1425
1814
  path: "/",
@@ -1439,7 +1828,7 @@ const release = {
1439
1828
  {
1440
1829
  method: "GET",
1441
1830
  path: "/",
1442
- handler: "release.findMany",
1831
+ handler: "release.findPage",
1443
1832
  config: {
1444
1833
  policies: [
1445
1834
  "admin::isAuthenticatedAdmin",
@@ -1537,6 +1926,22 @@ const releaseAction = {
1537
1926
  ]
1538
1927
  }
1539
1928
  },
1929
+ {
1930
+ method: "POST",
1931
+ path: "/:releaseId/actions/bulk",
1932
+ handler: "release-action.createMany",
1933
+ config: {
1934
+ policies: [
1935
+ "admin::isAuthenticatedAdmin",
1936
+ {
1937
+ name: "admin::hasPermissions",
1938
+ config: {
1939
+ actions: ["plugin::content-releases.create-action"]
1940
+ }
1941
+ }
1942
+ ]
1943
+ }
1944
+ },
1540
1945
  {
1541
1946
  method: "GET",
1542
1947
  path: "/:releaseId/actions",
@@ -1587,13 +1992,50 @@ const releaseAction = {
1587
1992
  }
1588
1993
  ]
1589
1994
  };
1995
+ const settings = {
1996
+ type: "admin",
1997
+ routes: [
1998
+ {
1999
+ method: "GET",
2000
+ path: "/settings",
2001
+ handler: "settings.find",
2002
+ config: {
2003
+ policies: [
2004
+ "admin::isAuthenticatedAdmin",
2005
+ {
2006
+ name: "admin::hasPermissions",
2007
+ config: {
2008
+ actions: ["plugin::content-releases.settings.read"]
2009
+ }
2010
+ }
2011
+ ]
2012
+ }
2013
+ },
2014
+ {
2015
+ method: "PUT",
2016
+ path: "/settings",
2017
+ handler: "settings.update",
2018
+ config: {
2019
+ policies: [
2020
+ "admin::isAuthenticatedAdmin",
2021
+ {
2022
+ name: "admin::hasPermissions",
2023
+ config: {
2024
+ actions: ["plugin::content-releases.settings.update"]
2025
+ }
2026
+ }
2027
+ ]
2028
+ }
2029
+ }
2030
+ ]
2031
+ };
1590
2032
  const routes = {
2033
+ settings,
1591
2034
  release,
1592
2035
  "release-action": releaseAction
1593
2036
  };
1594
- const { features } = require("@strapi/strapi/dist/utils/ee");
1595
2037
  const getPlugin = () => {
1596
- if (features.isEnabled("cms-content-releases")) {
2038
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1597
2039
  return {
1598
2040
  register,
1599
2041
  bootstrap,
@@ -1605,6 +2047,9 @@ const getPlugin = () => {
1605
2047
  };
1606
2048
  }
1607
2049
  return {
2050
+ // Always return register, it handles its own feature check
2051
+ register,
2052
+ // Always return contentTypes to avoid losing data when the feature is disabled
1608
2053
  contentTypes
1609
2054
  };
1610
2055
  };