@strapi/content-releases 0.0.0-experimental.ee4d311a5e6a131fad03cf07e4696f49fdd9c2e6 → 0.0.0-experimental.f2351bcfa3965c60f063a492da51faa2c636eee8

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