@strapi/content-releases 0.0.0-next.37dd1e3ff22e1635b69683abadd444912ae0dbff → 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-haKSQIo8.js → en-CmYoEnA7.js} +38 -7
  15. package/dist/_chunks/en-CmYoEnA7.js.map +1 -0
  16. package/dist/_chunks/{en-ngTk74JV.mjs → en-D0yVZFqf.mjs} +38 -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 -14
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/index.mjs +2 -15
  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 +1419 -451
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +1420 -453
  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 -34
  130. package/dist/_chunks/App-5PRKHpa2.js +0 -972
  131. package/dist/_chunks/App-5PRKHpa2.js.map +0 -1
  132. package/dist/_chunks/App-J4jrthEu.mjs +0 -950
  133. package/dist/_chunks/App-J4jrthEu.mjs.map +0 -1
  134. package/dist/_chunks/en-haKSQIo8.js.map +0 -1
  135. package/dist/_chunks/en-ngTk74JV.mjs.map +0 -1
  136. package/dist/_chunks/index-PEkKIRyJ.js +0 -849
  137. package/dist/_chunks/index-PEkKIRyJ.js.map +0 -1
  138. package/dist/_chunks/index-_Zsj8MUA.mjs +0 -828
  139. package/dist/_chunks/index-_Zsj8MUA.mjs.map +0 -1
  140. package/strapi-server.js +0 -3
@@ -1,5 +1,8 @@
1
- import { 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";
3
+ import { difference, keys } from "lodash";
2
4
  import _ from "lodash/fp";
5
+ import { scheduleJob } from "node-schedule";
3
6
  import * as yup from "yup";
4
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
5
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -45,82 +48,447 @@ const ACTIONS = [
45
48
  displayName: "Add an entry to a release",
46
49
  uid: "create-action",
47
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"
48
68
  }
49
69
  ];
50
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
70
+ const ALLOWED_WEBHOOK_EVENTS = {
71
+ RELEASES_PUBLISH: "releases.publish"
72
+ };
73
+ const getService = (name, { strapi: strapi2 }) => {
51
74
  return strapi2.plugin("content-releases").service(name);
52
75
  };
53
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
54
- const register = async ({ strapi: strapi2 }) => {
55
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
56
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
57
- const releaseActionService = getService("release-action", { strapi: strapi2 });
58
- const eventManager = getService("event-manager", { strapi: strapi2 });
59
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
60
- "content-type.update",
61
- async ({ contentType }) => {
62
- if (contentType.schema.options.draftAndPublish === false) {
63
- await releaseActionService.deleteManyForContentType(contentType.uid);
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
+ };
128
+ async function deleteActionsOnDisableDraftAndPublish({
129
+ oldContentTypes,
130
+ contentTypes: contentTypes2
131
+ }) {
132
+ if (!oldContentTypes) {
133
+ return;
134
+ }
135
+ for (const uid in contentTypes2) {
136
+ if (!oldContentTypes[uid]) {
137
+ continue;
138
+ }
139
+ const oldContentType = oldContentTypes[uid];
140
+ const contentType = contentTypes2[uid];
141
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
142
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
143
+ }
144
+ }
145
+ }
146
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
147
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
148
+ if (deletedContentTypes.length) {
149
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
150
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
151
+ });
152
+ }
153
+ }
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
64
164
  }
65
165
  }
66
- );
67
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
68
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
69
- "content-type.delete",
70
- async ({ contentType }) => {
71
- await releaseActionService.deleteManyForContentType(contentType.uid);
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
+ });
72
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
73
216
  );
74
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
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
+ };
334
+ const register = async ({ strapi: strapi2 }) => {
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);
75
443
  }
76
444
  };
77
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
78
445
  const bootstrap = async ({ strapi: strapi2 }) => {
79
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
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
+ );
80
450
  strapi2.db.lifecycles.subscribe({
81
- afterDelete(event) {
82
- const { model, result } = event;
83
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
84
- const { id } = result;
85
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
86
- where: {
87
- target_type: model.uid,
88
- target_id: id
89
- }
90
- });
91
- }
92
- },
451
+ models: contentTypesWithDraftAndPublish,
93
452
  /**
94
- * deleteMany hook doesn't return the deleted entries ids
95
- * so we need to fetch them before deleting the entries to save the ids on our state
96
- */
97
- async beforeDeleteMany(event) {
98
- const { model, params } = event;
99
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
100
- const { where } = params;
101
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
102
- event.state.entriesToDelete = entriesToDelete;
103
- }
104
- },
105
- /**
106
- * We delete the release actions related to deleted entries
107
- * 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
108
454
  */
109
455
  async afterDeleteMany(event) {
110
- const { model, state } = event;
111
- const entriesToDelete = state.entriesToDelete;
112
- if (entriesToDelete) {
113
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
114
- where: {
115
- target_type: model.uid,
116
- target_id: {
117
- $in: entriesToDelete.map((entry) => entry.id)
118
- }
119
- }
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
120
469
  });
121
470
  }
122
471
  }
123
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();
124
492
  }
125
493
  };
126
494
  const schema$1 = {
@@ -149,6 +517,17 @@ const schema$1 = {
149
517
  releasedAt: {
150
518
  type: "datetime"
151
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
+ },
152
531
  actions: {
153
532
  type: "relation",
154
533
  relation: "oneToMany",
@@ -184,15 +563,13 @@ const schema = {
184
563
  enum: ["publish", "unpublish"],
185
564
  required: true
186
565
  },
187
- entry: {
188
- type: "relation",
189
- relation: "morphToOne",
190
- configurable: false
191
- },
192
566
  contentType: {
193
567
  type: "string",
194
568
  required: true
195
569
  },
570
+ entryDocumentId: {
571
+ type: "string"
572
+ },
196
573
  locale: {
197
574
  type: "string"
198
575
  },
@@ -201,6 +578,9 @@ const schema = {
201
578
  relation: "manyToOne",
202
579
  target: RELEASE_MODEL_UID,
203
580
  inversedBy: "actions"
581
+ },
582
+ isEntryValid: {
583
+ type: "boolean"
204
584
  }
205
585
  }
206
586
  };
@@ -211,214 +591,307 @@ const contentTypes = {
211
591
  release: release$1,
212
592
  "release-action": releaseAction$1
213
593
  };
214
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
215
- async deleteManyForContentType(contentTypeUid) {
216
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
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
600
+ });
601
+ };
602
+ const getFormattedActions = async (releaseId) => {
603
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
217
604
  where: {
218
- target_type: contentTypeUid
605
+ release: {
606
+ id: releaseId
607
+ }
219
608
  }
220
609
  });
221
- }
222
- });
223
- const getGroupName = (queryValue) => {
224
- switch (queryValue) {
225
- case "contentType":
226
- return "entry.contentType.displayName";
227
- case "action":
228
- return "type";
229
- case "locale":
230
- return _.getOr("No locale", "entry.locale.name");
231
- default:
232
- return "entry.contentType.displayName";
233
- }
234
- };
235
- const createReleaseService = ({ strapi: strapi2 }) => ({
236
- async create(releaseData, { user }) {
237
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
238
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
239
- data: releaseWithCreatorFields
240
- });
241
- },
242
- async findOne(id, query = {}) {
243
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
244
- ...query
245
- });
246
- return release2;
247
- },
248
- findPage(query) {
249
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
250
- ...query,
251
- populate: {
252
- actions: {
253
- // @ts-expect-error Ignore missing properties
254
- count: 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"
255
646
  }
647
+ });
648
+ if (releaseWithCreatorFields.scheduledAt) {
649
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
650
+ await schedulingService.set(release2.id, release2.scheduledAt);
256
651
  }
257
- });
258
- },
259
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
260
- hasEntryAttached
261
- } = {
262
- hasEntryAttached: false
263
- }) {
264
- const whereActions = hasEntryAttached ? {
265
- // Find all Releases where the content type entry is present
266
- actions: {
267
- target_type: contentTypeUid,
268
- target_id: entryId
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
670
+ }
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}`);
269
695
  }
270
- } : {
271
- // Find all Releases where the content type entry is not present
272
- $or: [
273
- {
274
- $not: {
275
- actions: {
276
- target_type: contentTypeUid,
277
- target_id: entryId
696
+ if (release2.releasedAt) {
697
+ throw new errors.ValidationError("Release already published");
698
+ }
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;
720
+ },
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
+ }
732
+ }
733
+ });
734
+ if (!release2) {
735
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
736
+ }
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: {
743
+ id: {
744
+ $in: release2.actions.map((action) => action.id)
278
745
  }
279
746
  }
280
- },
281
- {
282
- actions: null
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);
757
+ }
758
+ strapi2.telemetry.send("didDeleteContentRelease");
759
+ return release2;
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}`);
283
769
  }
284
- ]
285
- };
286
- const populateAttachedAction = hasEntryAttached ? {
287
- // Filter the action to get only the content type entry
288
- actions: {
289
- where: {
290
- target_type: contentTypeUid,
291
- target_id: entryId
770
+ if (lockedRelease.releasedAt) {
771
+ throw new errors.ValidationError("Release already published");
292
772
  }
293
- }
294
- } : {};
295
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
296
- where: {
297
- ...whereActions,
298
- releasedAt: {
299
- $null: true
773
+ if (lockedRelease.status === "failed") {
774
+ throw new errors.ValidationError("Release failed to publish");
300
775
  }
301
- },
302
- populate: {
303
- ...populateAttachedAction
304
- }
305
- });
306
- return releases.map((release2) => {
307
- if (release2.actions?.length) {
308
- const [actionForEntry] = release2.actions;
309
- delete release2.actions;
310
- return {
311
- ...release2,
312
- action: actionForEntry
313
- };
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;
314
822
  }
315
823
  return release2;
316
- });
317
- },
318
- async update(id, releaseData, { user }) {
319
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
320
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
321
- if (!release2) {
322
- throw new errors.NotFoundError(`No release found for id ${id}`);
323
- }
324
- if (release2.releasedAt) {
325
- throw new errors.ValidationError("Release already published");
326
- }
327
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
328
- /*
329
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
330
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
331
- */
332
- // @ts-expect-error see above
333
- data: releaseWithCreatorFields
334
- });
335
- return updatedRelease;
336
- },
337
- async createAction(releaseId, action) {
338
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
339
- strapi: strapi2
340
- });
341
- await Promise.all([
342
- validateEntryContentType(action.entry.contentType),
343
- validateUniqueEntry(releaseId, action)
344
- ]);
345
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
346
- if (!release2) {
347
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
348
- }
349
- if (release2.releasedAt) {
350
- throw new errors.ValidationError("Release already published");
351
- }
352
- const { entry, type } = action;
353
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
354
- data: {
355
- type,
356
- contentType: entry.contentType,
357
- locale: entry.locale,
358
- entry: {
359
- id: entry.id,
360
- __type: entry.contentType,
361
- __pivot: { field: "entry" }
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
+ });
859
+ }
860
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
861
+ where: {
862
+ id: releaseId
362
863
  },
363
- release: releaseId
364
- },
365
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
366
- });
367
- },
368
- async findActions(releaseId, query) {
369
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
370
- fields: ["id"]
371
- });
372
- if (!release2) {
373
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
864
+ data: {
865
+ status: "empty"
866
+ }
867
+ });
374
868
  }
375
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
376
- ...query,
377
- populate: {
378
- entry: true
379
- },
380
- filters: {
381
- release: releaseId
382
- }
383
- });
384
- },
385
- async countActions(query) {
386
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
387
- },
388
- async groupActions(actions, groupBy) {
389
- const contentTypeUids = actions.reduce((acc, action) => {
390
- if (!acc.includes(action.contentType)) {
391
- acc.push(action.contentType);
392
- }
393
- return acc;
394
- }, []);
395
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
396
- contentTypeUids
397
- );
398
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
399
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
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 () => {
885
+ if (!strapi2.plugin("i18n")) {
886
+ return {};
887
+ }
888
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
889
+ return allLocales.reduce((acc, locale) => {
400
890
  acc[locale.code] = { name: locale.name, code: locale.code };
401
891
  return acc;
402
892
  }, {});
403
- const formattedData = actions.map((action) => {
404
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
405
- return {
406
- ...action,
407
- entry: {
408
- id: action.entry.id,
409
- contentType: {
410
- displayName,
411
- mainFieldValue: action.entry[mainField]
412
- },
413
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
414
- status: action.entry.publishedAt ? "published" : "draft"
415
- }
416
- };
417
- });
418
- const groupName = getGroupName(groupBy);
419
- return _.groupBy(groupName)(formattedData);
420
- },
421
- async getContentTypesDataForActions(contentTypesUids) {
893
+ };
894
+ const getContentTypesDataForActions = async (contentTypesUids) => {
422
895
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
423
896
  const contentTypesData = {};
424
897
  for (const contentTypeUid of contentTypesUids) {
@@ -431,178 +904,351 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
431
904
  };
432
905
  }
433
906
  return contentTypesData;
434
- },
435
- async delete(releaseId) {
436
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
437
- populate: {
438
- actions: {
439
- fields: ["id"]
440
- }
441
- }
442
- });
443
- if (!release2) {
444
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
445
- }
446
- if (release2.releasedAt) {
447
- throw new errors.ValidationError("Release already published");
448
- }
449
- await strapi2.db.transaction(async () => {
450
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
451
- where: {
452
- id: {
453
- $in: release2.actions.map((action) => action.id)
454
- }
455
- }
907
+ };
908
+ return {
909
+ async create(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
910
+ const { validateEntryData, validateUniqueEntry } = getService("release-validation", {
911
+ strapi: strapi2
456
912
  });
457
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
458
- });
459
- return release2;
460
- },
461
- async publish(releaseId) {
462
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
463
- RELEASE_MODEL_UID,
464
- releaseId,
465
- {
466
- populate: {
467
- actions: {
468
- populate: {
469
- entry: true
470
- }
471
- }
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}`);
472
922
  }
923
+ action.entryDocumentId = document.documentId;
473
924
  }
474
- );
475
- if (!releaseWithPopulatedActionEntries) {
476
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
477
- }
478
- if (releaseWithPopulatedActionEntries.releasedAt) {
479
- throw new errors.ValidationError("Release already published");
480
- }
481
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
482
- throw new errors.ValidationError("No entries to publish");
483
- }
484
- const actions = {};
485
- for (const action of releaseWithPopulatedActionEntries.actions) {
486
- const contentTypeUid = action.contentType;
487
- if (!actions[contentTypeUid]) {
488
- actions[contentTypeUid] = {
489
- publish: [],
490
- unpublish: []
491
- };
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}`);
492
928
  }
493
- if (action.type === "publish") {
494
- actions[contentTypeUid].publish.push(action.entry);
495
- } else {
496
- actions[contentTypeUid].unpublish.push(action.entry);
929
+ if (release2.releasedAt) {
930
+ throw new errors.ValidationError("Release already published");
497
931
  }
498
- }
499
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
500
- await strapi2.db.transaction(async () => {
501
- for (const contentTypeUid of Object.keys(actions)) {
502
- const { publish, unpublish } = actions[contentTypeUid];
503
- if (publish.length > 0) {
504
- await entityManagerService.publishMany(publish, contentTypeUid);
505
- }
506
- if (unpublish.length > 0) {
507
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
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
508
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"] } }
949
+ });
950
+ if (!disableUpdateReleaseStatus) {
951
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
509
952
  }
510
- });
511
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
512
- data: {
513
- /*
514
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
515
- */
516
- // @ts-expect-error see above
517
- releasedAt: /* @__PURE__ */ new Date()
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}`);
518
962
  }
519
- });
520
- return release2;
521
- },
522
- async updateAction(actionId, releaseId, update) {
523
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
524
- where: {
525
- id: actionId,
526
- release: {
527
- id: releaseId,
528
- releasedAt: {
529
- $null: true
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(
974
+ {
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 }
982
+ );
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
+ }
1047
+ }
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
+ );
1054
+ }
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
+ }
530
1073
  }
1074
+ },
1075
+ data: {
1076
+ ...update,
1077
+ isEntryValid: actionStatus
531
1078
  }
532
- },
533
- data: update
534
- });
535
- if (!updatedAction) {
536
- throw new errors.NotFoundError(
537
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
538
- );
539
- }
540
- return updatedAction;
541
- },
542
- async deleteAction(actionId, releaseId) {
543
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
544
- where: {
545
- id: actionId,
546
- release: {
547
- id: releaseId,
548
- releasedAt: {
549
- $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
+ }
550
1092
  }
551
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
+ );
552
1099
  }
553
- });
554
- if (!deletedAction) {
555
- throw new errors.NotFoundError(
556
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
557
- );
1100
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1101
+ return deletedAction;
558
1102
  }
559
- return deletedAction;
1103
+ };
1104
+ };
1105
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1106
+ constructor(message) {
1107
+ super(message);
1108
+ this.name = "AlreadyOnReleaseError";
560
1109
  }
561
- });
1110
+ }
562
1111
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
563
1112
  async validateUniqueEntry(releaseId, releaseActionArgs) {
564
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
565
- 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
+ }
566
1120
  });
567
1121
  if (!release2) {
568
1122
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
569
1123
  }
570
1124
  const isEntryInRelease = release2.actions.some(
571
- (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)
572
1126
  );
573
1127
  if (isEntryInRelease) {
574
- throw new errors.ValidationError(
575
- `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}`
576
1130
  );
577
1131
  }
578
1132
  },
579
- validateEntryContentType(contentTypeUid) {
1133
+ validateEntryData(contentTypeUid, entryDocumentId) {
580
1134
  const contentType = strapi2.contentType(contentTypeUid);
581
1135
  if (!contentType) {
582
1136
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
583
1137
  }
584
- if (!contentType.options?.draftAndPublish) {
1138
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
585
1139
  throw new errors.ValidationError(
586
1140
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
587
1141
  );
588
1142
  }
1143
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1144
+ throw new errors.ValidationError("Document id is required for collection type");
1145
+ }
1146
+ },
1147
+ async validatePendingReleasesLimit() {
1148
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1149
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1150
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1151
+ filters: {
1152
+ releasedAt: {
1153
+ $null: true
1154
+ }
1155
+ }
1156
+ });
1157
+ if (pendingReleasesCount >= maximumPendingReleases) {
1158
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
1159
+ }
1160
+ },
1161
+ async validateUniqueNameForPendingRelease(name, id) {
1162
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1163
+ where: {
1164
+ releasedAt: {
1165
+ $null: true
1166
+ },
1167
+ name,
1168
+ ...id && { id: { $ne: id } }
1169
+ }
1170
+ });
1171
+ const isNameUnique = pendingReleases.length === 0;
1172
+ if (!isNameUnique) {
1173
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
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
+ }
589
1180
  }
590
1181
  });
591
- const createEventManagerService = () => {
592
- const state = {
593
- destroyListenerCallbacks: []
594
- };
1182
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1183
+ const scheduledJobs = /* @__PURE__ */ new Map();
595
1184
  return {
596
- addDestroyListenerCallback(destroyListenerCallback) {
597
- state.destroyListenerCallbacks.push(destroyListenerCallback);
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;
598
1202
  },
599
- destroyAllListeners() {
600
- if (!state.destroyListenerCallbacks.length) {
601
- return;
1203
+ cancel(releaseId) {
1204
+ if (scheduledJobs.has(releaseId)) {
1205
+ scheduledJobs.get(releaseId).cancel();
1206
+ scheduledJobs.delete(releaseId);
602
1207
  }
603
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
604
- destroyListenerCallback();
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
+ }
605
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
+ };
606
1252
  }
607
1253
  };
608
1254
  };
@@ -610,64 +1256,148 @@ const services = {
610
1256
  release: createReleaseService,
611
1257
  "release-action": createReleaseActionService,
612
1258
  "release-validation": createReleaseValidationService,
613
- "event-manager": createEventManagerService
1259
+ scheduling: createSchedulingService,
1260
+ settings: createSettingsService
614
1261
  };
615
- const RELEASE_SCHEMA = yup.object().shape({
616
- 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()
617
1276
  }).required().noUnknown();
618
1277
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1278
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1279
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1280
+ );
619
1281
  const releaseController = {
620
- async findMany(ctx) {
621
- 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({
622
1289
  ability: ctx.state.userAbility,
623
1290
  model: RELEASE_MODEL_UID
624
1291
  });
625
1292
  await permissionsManager.validateQuery(ctx.query);
626
1293
  const releaseService = getService("release", { strapi });
627
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
628
- if (isFindManyForContentTypeEntry) {
629
- const query = await permissionsManager.sanitizeQuery(ctx.query);
630
- const contentTypeUid = query.contentTypeUid;
631
- const entryId = query.entryId;
632
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
633
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
634
- hasEntryAttached
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: {
1317
+ actions: {
1318
+ fields: ["type"],
1319
+ filters: {
1320
+ contentType,
1321
+ entryDocumentId: entryDocumentId ?? null,
1322
+ locale: locale ?? null
1323
+ }
1324
+ }
1325
+ }
635
1326
  });
636
- ctx.body = { data };
1327
+ ctx.body = { data: releases };
637
1328
  } else {
638
- const query = await permissionsManager.sanitizeQuery(ctx.query);
639
- const { results, pagination } = await releaseService.findPage(query);
640
- const data = results.map((release2) => {
641
- const { actions, ...releaseData } = release2;
642
- return {
643
- ...releaseData,
1329
+ const relatedReleases = await releaseService.findMany({
1330
+ where: {
1331
+ releasedAt: null,
644
1332
  actions: {
645
- meta: {
646
- count: actions.count
647
- }
1333
+ contentType,
1334
+ entryDocumentId: entryDocumentId ?? null,
1335
+ locale: locale ?? null
648
1336
  }
649
- };
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
+ }
650
1353
  });
651
- ctx.body = { data, meta: { pagination } };
1354
+ ctx.body = { data: releases };
652
1355
  }
653
1356
  },
654
- async findOne(ctx) {
655
- const id = ctx.params.id;
656
- const releaseService = getService("release", { strapi });
657
- const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
658
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1357
+ async findPage(ctx) {
1358
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
659
1359
  ability: ctx.state.userAbility,
660
1360
  model: RELEASE_MODEL_UID
661
1361
  });
662
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
663
- const count = await releaseService.countActions({
664
- filters: {
665
- 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
666
1380
  }
667
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"] });
668
1389
  if (!release2) {
669
1390
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
670
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
+ };
671
1401
  const data = {
672
1402
  ...sanitizedRelease,
673
1403
  actions: {
@@ -678,19 +1408,63 @@ const releaseController = {
678
1408
  };
679
1409
  ctx.body = { data };
680
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
+ },
681
1455
  async create(ctx) {
682
1456
  const user = ctx.state.user;
683
1457
  const releaseArgs = ctx.request.body;
684
1458
  await validateRelease(releaseArgs);
685
1459
  const releaseService = getService("release", { strapi });
686
1460
  const release2 = await releaseService.create(releaseArgs, { user });
687
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1461
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
688
1462
  ability: ctx.state.userAbility,
689
1463
  model: RELEASE_MODEL_UID
690
1464
  });
691
- ctx.body = {
1465
+ ctx.created({
692
1466
  data: await permissionsManager.sanitizeOutput(release2)
693
- };
1467
+ });
694
1468
  },
695
1469
  async update(ctx) {
696
1470
  const user = ctx.state.user;
@@ -699,7 +1473,7 @@ const releaseController = {
699
1473
  await validateRelease(releaseArgs);
700
1474
  const releaseService = getService("release", { strapi });
701
1475
  const release2 = await releaseService.update(id, releaseArgs, { user });
702
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1476
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
703
1477
  ability: ctx.state.userAbility,
704
1478
  model: RELEASE_MODEL_UID
705
1479
  });
@@ -716,55 +1490,142 @@ const releaseController = {
716
1490
  };
717
1491
  },
718
1492
  async publish(ctx) {
719
- const user = ctx.state.user;
720
1493
  const id = ctx.params.id;
721
1494
  const releaseService = getService("release", { strapi });
722
- 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
+ ]);
723
1511
  ctx.body = {
724
- data: release2
1512
+ data: release2,
1513
+ meta: {
1514
+ totalEntries: countPublishActions + countUnpublishActions,
1515
+ totalPublishedEntries: countPublishActions,
1516
+ totalUnpublishedEntries: countUnpublishActions
1517
+ }
725
1518
  };
726
1519
  }
727
1520
  };
728
1521
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
729
- entry: yup$1.object().shape({
730
- id: yup$1.strapiID().required(),
731
- contentType: yup$1.string().required()
732
- }).required(),
1522
+ contentType: yup$1.string().required(),
1523
+ entryDocumentId: yup$1.strapiID(),
1524
+ locale: yup$1.string(),
733
1525
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
734
1526
  });
735
1527
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
736
1528
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
737
1529
  });
1530
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1531
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1532
+ });
738
1533
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
739
1534
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1535
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
740
1536
  const releaseActionController = {
741
1537
  async create(ctx) {
742
1538
  const releaseId = ctx.params.releaseId;
743
1539
  const releaseActionArgs = ctx.request.body;
744
1540
  await validateReleaseAction(releaseActionArgs);
745
- const releaseService = getService("release", { strapi });
746
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
747
- ctx.body = {
1541
+ const releaseActionService = getService("release-action", { strapi });
1542
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1543
+ ctx.created({
748
1544
  data: releaseAction2
749
- };
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
+ });
750
1584
  },
751
1585
  async findMany(ctx) {
752
1586
  const releaseId = ctx.params.releaseId;
753
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1587
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
754
1588
  ability: ctx.state.userAbility,
755
1589
  model: RELEASE_ACTION_MODEL_UID
756
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;
757
1599
  const query = await permissionsManager.sanitizeQuery(ctx.query);
758
- const releaseService = getService("release", { strapi });
759
- const { results, pagination } = await releaseService.findActions(releaseId, {
760
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1600
+ const releaseActionService = getService("release-action", { strapi });
1601
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
761
1602
  ...query
762
1603
  });
763
- const groupedData = await releaseService.groupActions(results, query.groupBy);
1604
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1605
+ if (acc[action.contentType]) {
1606
+ return acc;
1607
+ }
1608
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1609
+ ability: ctx.state.userAbility,
1610
+ model: action.contentType
1611
+ });
1612
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1613
+ return acc;
1614
+ }, {});
1615
+ const sanitizedResults = await async.map(results, async (action) => ({
1616
+ ...action,
1617
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1618
+ }));
1619
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1620
+ const contentTypes2 = releaseActionService.getContentTypeModelsFromActions(results);
1621
+ const releaseService = getService("release", { strapi });
1622
+ const components = await releaseService.getAllComponents();
764
1623
  ctx.body = {
765
1624
  data: groupedData,
766
1625
  meta: {
767
- pagination
1626
+ pagination,
1627
+ contentTypes: contentTypes2,
1628
+ components
768
1629
  }
769
1630
  };
770
1631
  },
@@ -773,8 +1634,8 @@ const releaseActionController = {
773
1634
  const releaseId = ctx.params.releaseId;
774
1635
  const releaseActionUpdateArgs = ctx.request.body;
775
1636
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
776
- const releaseService = getService("release", { strapi });
777
- const updatedAction = await releaseService.updateAction(
1637
+ const releaseActionService = getService("release-action", { strapi });
1638
+ const updatedAction = await releaseActionService.update(
778
1639
  actionId,
779
1640
  releaseId,
780
1641
  releaseActionUpdateArgs
@@ -786,17 +1647,71 @@ const releaseActionController = {
786
1647
  async delete(ctx) {
787
1648
  const actionId = ctx.params.actionId;
788
1649
  const releaseId = ctx.params.releaseId;
789
- const releaseService = getService("release", { strapi });
790
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1650
+ const releaseActionService = getService("release-action", { strapi });
1651
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
791
1652
  ctx.body = {
792
1653
  data: deletedReleaseAction
793
1654
  };
794
1655
  }
795
1656
  };
796
- 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
+ };
797
1680
  const release = {
798
1681
  type: "admin",
799
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
+ },
800
1715
  {
801
1716
  method: "POST",
802
1717
  path: "/",
@@ -816,7 +1731,7 @@ const release = {
816
1731
  {
817
1732
  method: "GET",
818
1733
  path: "/",
819
- handler: "release.findMany",
1734
+ handler: "release.findPage",
820
1735
  config: {
821
1736
  policies: [
822
1737
  "admin::isAuthenticatedAdmin",
@@ -914,6 +1829,22 @@ const releaseAction = {
914
1829
  ]
915
1830
  }
916
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
+ },
917
1848
  {
918
1849
  method: "GET",
919
1850
  path: "/:releaseId/actions",
@@ -964,28 +1895,64 @@ const releaseAction = {
964
1895
  }
965
1896
  ]
966
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
+ };
967
1935
  const routes = {
1936
+ settings,
968
1937
  release,
969
1938
  "release-action": releaseAction
970
1939
  };
971
- const { features } = require("@strapi/strapi/dist/utils/ee");
972
1940
  const getPlugin = () => {
973
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1941
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
974
1942
  return {
975
1943
  register,
976
1944
  bootstrap,
1945
+ destroy,
977
1946
  contentTypes,
978
1947
  services,
979
1948
  controllers,
980
- routes,
981
- destroy() {
982
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
983
- getService("event-manager").destroyAllListeners();
984
- }
985
- }
1949
+ routes
986
1950
  };
987
1951
  }
988
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
989
1956
  contentTypes
990
1957
  };
991
1958
  };