@strapi/content-releases 0.0.0-next.37dd1e3ff22e1635b69683abadd444912ae0dbff → 0.0.0-next.3c5400321681b66eb35ab84c11113a78c1d9386e

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