@strapi/content-releases 0.0.0-next.4af8963f6880c5fb9fae32ecd580f5cd33eaddda → 0.0.0-next.4cf36024d8e6011c265fdc01924d063589432f8f

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