@strapi/content-releases 0.0.0-next.3844395bef7efa05c25c6d4337306935905bc653 → 0.0.0-next.3c5bc3f35387771b185349533729e99e6e59c525

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