@strapi/content-releases 0.0.0-next.836f74517f9a428a4798ed889c3f05057ec6beb1 → 0.0.0-next.837384c065457f44cba22eb6fb56079cc4b04a2b

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