@strapi/content-releases 0.0.0-experimental.f7b9b47085e387e97f990d8695971b51d7f7149a → 0.0.0-experimental.fb442e5e12dd3f611303691bf85a249520ba348b

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 (141) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/App-DMILern_.mjs +1356 -0
  3. package/dist/_chunks/App-DMILern_.mjs.map +1 -0
  4. package/dist/_chunks/App-fAgiijnc.js +1377 -0
  5. package/dist/_chunks/App-fAgiijnc.js.map +1 -0
  6. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js +52 -0
  7. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  8. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs +52 -0
  9. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-YVZJH-oN.js +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-YVZJH-oN.js.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-dwoRuXB-.mjs +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-dwoRuXB-.mjs.map +1 -0
  14. package/dist/_chunks/en-CmYoEnA7.js +93 -0
  15. package/dist/_chunks/en-CmYoEnA7.js.map +1 -0
  16. package/dist/_chunks/en-D0yVZFqf.mjs +93 -0
  17. package/dist/_chunks/en-D0yVZFqf.mjs.map +1 -0
  18. package/dist/_chunks/index--_NWfuDG.js +1358 -0
  19. package/dist/_chunks/index--_NWfuDG.js.map +1 -0
  20. package/dist/_chunks/index-CYsQToWs.mjs +1339 -0
  21. package/dist/_chunks/index-CYsQToWs.mjs.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 -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 +1432 -344
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +1430 -345
  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 -34
  130. package/dist/_chunks/App-a4843fda.mjs +0 -855
  131. package/dist/_chunks/App-a4843fda.mjs.map +0 -1
  132. package/dist/_chunks/App-f2cafd81.js +0 -877
  133. package/dist/_chunks/App-f2cafd81.js.map +0 -1
  134. package/dist/_chunks/en-13576ce2.js +0 -53
  135. package/dist/_chunks/en-13576ce2.js.map +0 -1
  136. package/dist/_chunks/en-e98d8b57.mjs +0 -53
  137. package/dist/_chunks/en-e98d8b57.mjs.map +0 -1
  138. package/dist/_chunks/index-66d129ac.js +0 -838
  139. package/dist/_chunks/index-66d129ac.js.map +0 -1
  140. package/dist/_chunks/index-937f8179.mjs +0 -817
  141. package/dist/_chunks/index-937f8179.mjs.map +0 -1
@@ -1,4 +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";
4
+ import _ from "lodash/fp";
5
+ import { scheduleJob } from "node-schedule";
2
6
  import * as yup from "yup";
3
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
4
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -44,12 +48,447 @@ const ACTIONS = [
44
48
  displayName: "Add an entry to a release",
45
49
  uid: "create-action",
46
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"
47
68
  }
48
69
  ];
49
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
70
+ const ALLOWED_WEBHOOK_EVENTS = {
71
+ RELEASES_PUBLISH: "releases.publish"
72
+ };
73
+ const getService = (name, { strapi: strapi2 }) => {
74
+ return strapi2.plugin("content-releases").service(name);
75
+ };
76
+ const getDraftEntryValidStatus = async ({ contentType, documentId, locale }, { strapi: strapi2 }) => {
77
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
78
+ const populate = await populateBuilderService(contentType).populateDeep(Infinity).build();
79
+ const entry = await getEntry({ contentType, documentId, locale, populate }, { strapi: strapi2 });
80
+ return isEntryValid(contentType, entry, { strapi: strapi2 });
81
+ };
82
+ const isEntryValid = async (contentTypeUid, entry, { strapi: strapi2 }) => {
83
+ try {
84
+ await strapi2.entityValidator.validateEntityCreation(
85
+ strapi2.getModel(contentTypeUid),
86
+ entry,
87
+ void 0,
88
+ // @ts-expect-error - FIXME: entity here is unnecessary
89
+ entry
90
+ );
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ };
96
+ const getEntry = async ({
97
+ contentType,
98
+ documentId,
99
+ locale,
100
+ populate,
101
+ status = "draft"
102
+ }, { strapi: strapi2 }) => {
103
+ if (documentId) {
104
+ return strapi2.documents(contentType).findOne({ documentId, locale, populate, status });
105
+ }
106
+ return strapi2.documents(contentType).findFirst({ locale, populate, status });
107
+ };
108
+ const getEntryStatus = async (contentType, entry) => {
109
+ if (entry.publishedAt) {
110
+ return "published";
111
+ }
112
+ const publishedEntry = await strapi.documents(contentType).findOne({
113
+ documentId: entry.documentId,
114
+ locale: entry.locale,
115
+ status: "published",
116
+ fields: ["updatedAt"]
117
+ });
118
+ if (!publishedEntry) {
119
+ return "draft";
120
+ }
121
+ const entryUpdatedAt = new Date(entry.updatedAt).getTime();
122
+ const publishedEntryUpdatedAt = new Date(publishedEntry.updatedAt).getTime();
123
+ if (entryUpdatedAt > publishedEntryUpdatedAt) {
124
+ return "modified";
125
+ }
126
+ return "published";
127
+ };
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
164
+ }
165
+ }
166
+ }
167
+ });
168
+ async.map(releasesWithoutStatus, async (release2) => {
169
+ const actions = release2.actions;
170
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
171
+ for (const action of notValidatedActions) {
172
+ if (action.entry) {
173
+ const isEntryValid2 = getDraftEntryValidStatus(
174
+ {
175
+ contentType: action.contentType,
176
+ documentId: action.entryDocumentId,
177
+ locale: action.locale
178
+ },
179
+ { strapi }
180
+ );
181
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
182
+ where: {
183
+ id: action.id
184
+ },
185
+ data: {
186
+ isEntryValid: isEntryValid2
187
+ }
188
+ });
189
+ }
190
+ }
191
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
192
+ });
193
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
194
+ where: {
195
+ status: null,
196
+ releasedAt: {
197
+ $notNull: true
198
+ }
199
+ }
200
+ });
201
+ async.map(publishedReleases, async (release2) => {
202
+ return strapi.db.query(RELEASE_MODEL_UID).update({
203
+ where: {
204
+ id: release2.id
205
+ },
206
+ data: {
207
+ status: "done"
208
+ }
209
+ });
210
+ });
211
+ }
212
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
213
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
214
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
215
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
216
+ );
217
+ const releasesAffected = /* @__PURE__ */ new Set();
218
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
219
+ const oldContentType = oldContentTypes[contentTypeUID];
220
+ const contentType = contentTypes2[contentTypeUID];
221
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
222
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
223
+ where: {
224
+ contentType: contentTypeUID
225
+ },
226
+ populate: {
227
+ entry: true,
228
+ release: true
229
+ }
230
+ });
231
+ await async.map(actions, async (action) => {
232
+ if (action.entry && action.release && action.type === "publish") {
233
+ const isEntryValid2 = await getDraftEntryValidStatus(
234
+ {
235
+ contentType: contentTypeUID,
236
+ documentId: action.entryDocumentId,
237
+ locale: action.locale
238
+ },
239
+ { strapi }
240
+ );
241
+ releasesAffected.add(action.release.id);
242
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
243
+ where: {
244
+ id: action.id
245
+ },
246
+ data: {
247
+ isEntryValid: isEntryValid2
248
+ }
249
+ });
250
+ }
251
+ });
252
+ }
253
+ }).then(() => {
254
+ async.map(releasesAffected, async (releaseId) => {
255
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
256
+ });
257
+ });
258
+ }
259
+ }
260
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
261
+ if (!oldContentTypes) {
262
+ return;
263
+ }
264
+ const i18nPlugin = strapi.plugin("i18n");
265
+ if (!i18nPlugin) {
266
+ return;
267
+ }
268
+ for (const uid in contentTypes2) {
269
+ if (!oldContentTypes[uid]) {
270
+ continue;
271
+ }
272
+ const oldContentType = oldContentTypes[uid];
273
+ const contentType = contentTypes2[uid];
274
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
275
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
276
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
277
+ locale: null
278
+ }).where({ contentType: uid }).execute();
279
+ }
280
+ }
281
+ }
282
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
283
+ if (!oldContentTypes) {
284
+ return;
285
+ }
286
+ const i18nPlugin = strapi.plugin("i18n");
287
+ if (!i18nPlugin) {
288
+ return;
289
+ }
290
+ for (const uid in contentTypes2) {
291
+ if (!oldContentTypes[uid]) {
292
+ continue;
293
+ }
294
+ const oldContentType = oldContentTypes[uid];
295
+ const contentType = contentTypes2[uid];
296
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
297
+ const { getDefaultLocale } = i18nPlugin.service("locales");
298
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
299
+ const defaultLocale = await getDefaultLocale();
300
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
301
+ locale: defaultLocale
302
+ }).where({ contentType: uid }).execute();
303
+ }
304
+ }
305
+ }
306
+ const addEntryDocumentToReleaseActions = {
307
+ name: "content-releases::5.0.0-add-entry-document-id-to-release-actions",
308
+ async up(trx, db) {
309
+ const hasPolymorphicColumn = await trx.schema.hasColumn("strapi_release_actions", "target_id");
310
+ if (hasPolymorphicColumn) {
311
+ const hasEntryDocumentIdColumn = await trx.schema.hasColumn(
312
+ "strapi_release_actions",
313
+ "entry_document_id"
314
+ );
315
+ if (!hasEntryDocumentIdColumn) {
316
+ await trx.schema.alterTable("strapi_release_actions", (table) => {
317
+ table.string("entry_document_id");
318
+ });
319
+ }
320
+ const releaseActions = await trx.select("*").from("strapi_release_actions");
321
+ async.map(releaseActions, async (action) => {
322
+ const { target_type, target_id } = action;
323
+ const entry = await db.query(target_type).findOne({ where: { id: target_id } });
324
+ if (entry) {
325
+ await trx("strapi_release_actions").update({ entry_document_id: entry.documentId }).where("id", action.id);
326
+ }
327
+ });
328
+ }
329
+ },
330
+ async down() {
331
+ throw new Error("not implemented");
332
+ }
333
+ };
50
334
  const register = async ({ strapi: strapi2 }) => {
51
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
52
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
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);
443
+ }
444
+ };
445
+ const bootstrap = async ({ strapi: strapi2 }) => {
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
+ );
450
+ strapi2.db.lifecycles.subscribe({
451
+ models: contentTypesWithDraftAndPublish,
452
+ /**
453
+ * deleteMany is still used outside documents service, for example when deleting a locale
454
+ */
455
+ async afterDeleteMany(event) {
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
469
+ });
470
+ }
471
+ }
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();
53
492
  }
54
493
  };
55
494
  const schema$1 = {
@@ -78,6 +517,17 @@ const schema$1 = {
78
517
  releasedAt: {
79
518
  type: "datetime"
80
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
+ },
81
531
  actions: {
82
532
  type: "relation",
83
533
  relation: "oneToMany",
@@ -113,20 +563,24 @@ const schema = {
113
563
  enum: ["publish", "unpublish"],
114
564
  required: true
115
565
  },
116
- entry: {
117
- type: "relation",
118
- relation: "morphToOne",
119
- configurable: false
120
- },
121
566
  contentType: {
122
567
  type: "string",
123
568
  required: true
124
569
  },
570
+ entryDocumentId: {
571
+ type: "string"
572
+ },
573
+ locale: {
574
+ type: "string"
575
+ },
125
576
  release: {
126
577
  type: "relation",
127
578
  relation: "manyToOne",
128
579
  target: RELEASE_MODEL_UID,
129
580
  inversedBy: "actions"
581
+ },
582
+ isEntryValid: {
583
+ type: "boolean"
130
584
  }
131
585
  }
132
586
  };
@@ -137,160 +591,307 @@ const contentTypes = {
137
591
  release: release$1,
138
592
  "release-action": releaseAction$1
139
593
  };
140
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
141
- return strapi2.plugin("content-releases").service(name);
142
- };
143
- const createReleaseService = ({ strapi: strapi2 }) => ({
144
- async create(releaseData, { user }) {
145
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
146
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
147
- data: releaseWithCreatorFields
148
- });
149
- },
150
- async findOne(id, query = {}) {
151
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
152
- ...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
153
600
  });
154
- return release2;
155
- },
156
- findPage(query) {
157
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
158
- ...query,
159
- populate: {
160
- actions: {
161
- // @ts-expect-error Ignore missing properties
162
- 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
163
607
  }
164
608
  }
165
609
  });
166
- },
167
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
168
- hasEntryAttached
169
- } = {
170
- hasEntryAttached: false
171
- }) {
172
- const whereActions = hasEntryAttached ? {
173
- // Find all Releases where the content type entry is present
174
- actions: {
175
- target_type: contentTypeUid,
176
- 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
+ };
177
621
  }
178
- } : {
179
- // Find all Releases where the content type entry is not present
180
- $or: [
181
- {
182
- $not: {
183
- actions: {
184
- target_type: contentTypeUid,
185
- target_id: entryId
186
- }
187
- }
188
- },
189
- {
190
- 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"
191
646
  }
192
- ]
193
- };
194
- const populateAttachedAction = hasEntryAttached ? {
195
- // Filter the action to get only the content type entry
196
- actions: {
197
- where: {
198
- target_type: contentTypeUid,
199
- 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
+ }
200
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}`);
201
695
  }
202
- } : {};
203
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
204
- where: {
205
- ...whereActions,
206
- releasedAt: {
207
- $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
+ }
208
732
  }
209
- },
210
- populate: {
211
- ...populateAttachedAction
733
+ });
734
+ if (!release2) {
735
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
212
736
  }
213
- });
214
- return releases.map((release2) => {
215
- if (release2.actions?.length) {
216
- const [actionForEntry] = release2.actions;
217
- delete release2.actions;
218
- return {
219
- ...release2,
220
- action: actionForEntry
221
- };
737
+ if (release2.releasedAt) {
738
+ throw new errors.ValidationError("Release already published");
739
+ }
740
+ await strapi2.db.transaction(async () => {
741
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
742
+ where: {
743
+ id: {
744
+ $in: release2.actions.map((action) => action.id)
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);
222
757
  }
758
+ strapi2.telemetry.send("didDeleteContentRelease");
223
759
  return release2;
224
- });
225
- },
226
- async update(id, releaseData, { user }) {
227
- const updatedRelease = await setCreatorFields({ user, isEdition: true })(releaseData);
228
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
229
- /*
230
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
231
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
232
- */
233
- // @ts-expect-error see above
234
- data: updatedRelease
235
- });
236
- if (!release2) {
237
- throw new errors.NotFoundError(`No release found for id ${id}`);
238
- }
239
- return release2;
240
- },
241
- async createAction(releaseId, action) {
242
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
243
- strapi: strapi2
244
- });
245
- await Promise.all([
246
- validateEntryContentType(action.entry.contentType),
247
- validateUniqueEntry(releaseId, action)
248
- ]);
249
- const { entry, type } = action;
250
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
251
- data: {
252
- type,
253
- contentType: entry.contentType,
254
- entry: {
255
- id: entry.id,
256
- __type: entry.contentType,
257
- __pivot: { field: "entry" }
258
- },
259
- release: releaseId
260
- },
261
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
262
- });
263
- },
264
- async findActions(releaseId, query) {
265
- const result = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
266
- if (!result) {
267
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
268
- }
269
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
270
- ...query,
271
- populate: {
272
- entry: true
273
- },
274
- filters: {
275
- release: releaseId
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}`);
769
+ }
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;
276
822
  }
277
- });
278
- },
279
- async countActions(query) {
280
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
281
- },
282
- async getAllContentTypeUids(releaseId) {
283
- const contentTypesFromReleaseActions = await strapi2.db.queryBuilder(RELEASE_ACTION_MODEL_UID).select("content_type").where({
284
- $and: [
285
- {
286
- release: releaseId
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
+ });
287
850
  }
288
- ]
289
- }).groupBy("content_type").execute();
290
- return contentTypesFromReleaseActions.map(({ contentType: contentTypeUid }) => contentTypeUid);
291
- },
292
- async getContentTypesDataForActions(releaseId) {
293
- const contentTypesUids = await this.getAllContentTypeUids(releaseId);
851
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
852
+ where: {
853
+ id: releaseId
854
+ },
855
+ data: {
856
+ status: "ready"
857
+ }
858
+ });
859
+ }
860
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
861
+ where: {
862
+ id: releaseId
863
+ },
864
+ data: {
865
+ status: "empty"
866
+ }
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 () => {
885
+ if (!strapi2.plugin("i18n")) {
886
+ return {};
887
+ }
888
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
889
+ return allLocales.reduce((acc, locale) => {
890
+ acc[locale.code] = { name: locale.name, code: locale.code };
891
+ return acc;
892
+ }, {});
893
+ };
894
+ const getContentTypesDataForActions = async (contentTypesUids) => {
294
895
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
295
896
  const contentTypesData = {};
296
897
  for (const contentTypeUid of contentTypesUids) {
@@ -303,210 +904,477 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
303
904
  };
304
905
  }
305
906
  return contentTypesData;
306
- },
307
- async delete(releaseId) {
308
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
309
- populate: {
310
- actions: {
311
- fields: ["id"]
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}`);
920
+ }
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
312
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}`);
313
952
  }
314
- });
315
- if (!release2) {
316
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
317
- }
318
- if (release2.releasedAt) {
319
- throw new errors.ValidationError("Release already published");
320
- }
321
- await strapi2.db.transaction(async () => {
322
- 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,
323
956
  where: {
324
- id: {
325
- $in: release2.actions.map((action) => action.id)
326
- }
957
+ release: releaseId
327
958
  }
328
959
  });
329
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
330
- });
331
- return release2;
332
- },
333
- async publish(releaseId) {
334
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
335
- RELEASE_MODEL_UID,
336
- releaseId,
337
- {
338
- populate: {
339
- actions: {
340
- populate: {
341
- entry: true
342
- }
343
- }
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
977
+ };
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);
344
988
  }
345
- }
346
- );
347
- if (!releaseWithPopulatedActionEntries) {
348
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
349
- }
350
- if (releaseWithPopulatedActionEntries.releasedAt) {
351
- throw new errors.ValidationError("Release already published");
352
- }
353
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
354
- throw new errors.ValidationError("No entries to publish");
355
- }
356
- const actions = {};
357
- for (const action of releaseWithPopulatedActionEntries.actions) {
358
- const contentTypeUid = action.contentType;
359
- if (!actions[contentTypeUid]) {
360
- actions[contentTypeUid] = {
361
- publish: [],
362
- unpublish: []
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
+ }
363
1003
  };
364
- }
365
- if (action.type === "publish") {
366
- actions[contentTypeUid].publish.push(action.entry);
367
- } else {
368
- actions[contentTypeUid].unpublish.push(action.entry);
369
- }
370
- }
371
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
372
- await strapi2.db.transaction(async () => {
373
- for (const contentTypeUid of Object.keys(actions)) {
374
- const { publish, unpublish } = actions[contentTypeUid];
375
- if (publish.length > 0) {
376
- await entityManagerService.publishMany(publish, contentTypeUid);
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);
377
1012
  }
378
- if (unpublish.length > 0) {
379
- 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
+ }
380
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
+ );
381
1044
  }
382
- });
383
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
384
- data: {
385
- /*
386
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
387
- */
388
- // @ts-expect-error see above
389
- releasedAt: /* @__PURE__ */ new Date()
390
- }
391
- });
392
- return release2;
393
- },
394
- async updateAction(actionId, releaseId, update) {
395
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
396
- where: {
397
- id: actionId,
398
- release: releaseId
399
- },
400
- data: update
401
- });
402
- if (!updatedAction) {
403
- throw new errors.NotFoundError(
404
- `Action with id ${actionId} not found in release with id ${releaseId}`
405
- );
406
- }
407
- return updatedAction;
408
- },
409
- async deleteAction(actionId, releaseId) {
410
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
411
- where: {
412
- id: actionId,
413
- release: releaseId
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
+ }
1063
+ }
1064
+ },
1065
+ data: {
1066
+ ...update,
1067
+ isEntryValid: actionStatus
1068
+ }
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
+ }
1082
+ }
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
+ );
414
1089
  }
415
- });
416
- if (!deletedAction) {
417
- throw new errors.NotFoundError(
418
- `Action with id ${actionId} not found in release with id ${releaseId}`
419
- );
1090
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1091
+ return deletedAction;
420
1092
  }
421
- return deletedAction;
1093
+ };
1094
+ };
1095
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1096
+ constructor(message) {
1097
+ super(message);
1098
+ this.name = "AlreadyOnReleaseError";
422
1099
  }
423
- });
1100
+ }
424
1101
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
425
1102
  async validateUniqueEntry(releaseId, releaseActionArgs) {
426
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
427
- 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
+ }
428
1110
  });
429
1111
  if (!release2) {
430
1112
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
431
1113
  }
432
1114
  const isEntryInRelease = release2.actions.some(
433
- (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)
434
1116
  );
435
1117
  if (isEntryInRelease) {
436
- throw new errors.ValidationError(
437
- `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}`
438
1120
  );
439
1121
  }
440
1122
  },
441
- validateEntryContentType(contentTypeUid) {
1123
+ validateEntryData(contentTypeUid, entryDocumentId) {
442
1124
  const contentType = strapi2.contentType(contentTypeUid);
443
1125
  if (!contentType) {
444
1126
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
445
1127
  }
446
- if (!contentType.options?.draftAndPublish) {
1128
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
447
1129
  throw new errors.ValidationError(
448
1130
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
449
1131
  );
450
1132
  }
1133
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1134
+ throw new errors.ValidationError("Document id is required for collection type");
1135
+ }
1136
+ },
1137
+ async validatePendingReleasesLimit() {
1138
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1139
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1140
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1141
+ filters: {
1142
+ releasedAt: {
1143
+ $null: true
1144
+ }
1145
+ }
1146
+ });
1147
+ if (pendingReleasesCount >= maximumPendingReleases) {
1148
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
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
+ }
451
1170
  }
452
1171
  });
453
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
454
- const RELEASE_SCHEMA = yup.object().shape({
455
- name: yup.string().trim().required()
1172
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1173
+ const scheduledJobs = /* @__PURE__ */ new Map();
1174
+ return {
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;
1192
+ },
1193
+ cancel(releaseId) {
1194
+ if (scheduledJobs.has(releaseId)) {
1195
+ scheduledJobs.get(releaseId).cancel();
1196
+ scheduledJobs.delete(releaseId);
1197
+ }
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
+ }
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
+ };
1242
+ }
1243
+ };
1244
+ };
1245
+ const services = {
1246
+ release: createReleaseService,
1247
+ "release-action": createReleaseActionService,
1248
+ "release-validation": createReleaseValidationService,
1249
+ scheduling: createSchedulingService,
1250
+ settings: createSettingsService
1251
+ };
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()
456
1266
  }).required().noUnknown();
457
1267
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1268
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1269
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1270
+ );
458
1271
  const releaseController = {
459
- async findMany(ctx) {
460
- 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({
461
1279
  ability: ctx.state.userAbility,
462
1280
  model: RELEASE_MODEL_UID
463
1281
  });
464
1282
  await permissionsManager.validateQuery(ctx.query);
465
1283
  const releaseService = getService("release", { strapi });
466
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
467
- if (isFindManyForContentTypeEntry) {
468
- const query = await permissionsManager.sanitizeQuery(ctx.query);
469
- const contentTypeUid = query.contentTypeUid;
470
- const entryId = query.entryId;
471
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
472
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
473
- 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
+ }
474
1303
  });
475
- ctx.body = { data };
1304
+ ctx.body = { data: releases };
476
1305
  } else {
477
- const query = await permissionsManager.sanitizeQuery(ctx.query);
478
- const { results, pagination } = await releaseService.findPage(query);
479
- const data = results.map((release2) => {
480
- const { actions, ...releaseData } = release2;
481
- return {
482
- ...releaseData,
1306
+ const relatedReleases = await releaseService.findMany({
1307
+ where: {
1308
+ releasedAt: null,
483
1309
  actions: {
484
- meta: {
485
- count: actions.count
486
- }
1310
+ contentType,
1311
+ entryDocumentId: entryDocumentId ?? null,
1312
+ locale: locale ?? null
487
1313
  }
488
- };
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
+ }
489
1330
  });
490
- ctx.body = { data, meta: { pagination } };
1331
+ ctx.body = { data: releases };
491
1332
  }
492
1333
  },
493
- async findOne(ctx) {
494
- const id = ctx.params.id;
495
- const releaseService = getService("release", { strapi });
496
- const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
497
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1334
+ async findPage(ctx) {
1335
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
498
1336
  ability: ctx.state.userAbility,
499
1337
  model: RELEASE_MODEL_UID
500
1338
  });
501
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
502
- const count = await releaseService.countActions({
503
- filters: {
504
- 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
505
1357
  }
506
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"] });
507
1366
  if (!release2) {
508
1367
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
509
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
+ };
510
1378
  const data = {
511
1379
  ...sanitizedRelease,
512
1380
  actions: {
@@ -517,19 +1385,63 @@ const releaseController = {
517
1385
  };
518
1386
  ctx.body = { data };
519
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
+ },
520
1432
  async create(ctx) {
521
1433
  const user = ctx.state.user;
522
1434
  const releaseArgs = ctx.request.body;
523
1435
  await validateRelease(releaseArgs);
524
1436
  const releaseService = getService("release", { strapi });
525
1437
  const release2 = await releaseService.create(releaseArgs, { user });
526
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1438
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
527
1439
  ability: ctx.state.userAbility,
528
1440
  model: RELEASE_MODEL_UID
529
1441
  });
530
- ctx.body = {
1442
+ ctx.created({
531
1443
  data: await permissionsManager.sanitizeOutput(release2)
532
- };
1444
+ });
533
1445
  },
534
1446
  async update(ctx) {
535
1447
  const user = ctx.state.user;
@@ -538,7 +1450,7 @@ const releaseController = {
538
1450
  await validateRelease(releaseArgs);
539
1451
  const releaseService = getService("release", { strapi });
540
1452
  const release2 = await releaseService.update(id, releaseArgs, { user });
541
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1453
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
542
1454
  ability: ctx.state.userAbility,
543
1455
  model: RELEASE_MODEL_UID
544
1456
  });
@@ -555,73 +1467,136 @@ const releaseController = {
555
1467
  };
556
1468
  },
557
1469
  async publish(ctx) {
558
- const user = ctx.state.user;
559
1470
  const id = ctx.params.id;
560
1471
  const releaseService = getService("release", { strapi });
561
- 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
+ ]);
562
1488
  ctx.body = {
563
- data: release2
1489
+ data: release2,
1490
+ meta: {
1491
+ totalEntries: countPublishActions + countUnpublishActions,
1492
+ totalPublishedEntries: countPublishActions,
1493
+ totalUnpublishedEntries: countUnpublishActions
1494
+ }
564
1495
  };
565
1496
  }
566
1497
  };
567
1498
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
568
- entry: yup$1.object().shape({
569
- id: yup$1.strapiID().required(),
570
- contentType: yup$1.string().required()
571
- }).required(),
1499
+ contentType: yup$1.string().required(),
1500
+ entryDocumentId: yup$1.strapiID(),
1501
+ locale: yup$1.string(),
572
1502
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
573
1503
  });
574
1504
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
575
1505
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
576
1506
  });
1507
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1508
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1509
+ });
577
1510
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
578
1511
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1512
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
579
1513
  const releaseActionController = {
580
1514
  async create(ctx) {
581
1515
  const releaseId = ctx.params.releaseId;
582
1516
  const releaseActionArgs = ctx.request.body;
583
1517
  await validateReleaseAction(releaseActionArgs);
584
- const releaseService = getService("release", { strapi });
585
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
586
- ctx.body = {
1518
+ const releaseActionService = getService("release-action", { strapi });
1519
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1520
+ ctx.created({
587
1521
  data: releaseAction2
588
- };
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
+ });
589
1555
  },
590
1556
  async findMany(ctx) {
591
1557
  const releaseId = ctx.params.releaseId;
592
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1558
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
593
1559
  ability: ctx.state.userAbility,
594
1560
  model: RELEASE_ACTION_MODEL_UID
595
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;
596
1570
  const query = await permissionsManager.sanitizeQuery(ctx.query);
597
- const releaseService = getService("release", { strapi });
598
- const { results, pagination } = await releaseService.findActions(releaseId, query);
599
- const allReleaseContentTypesDictionary = await releaseService.getContentTypesDataForActions(
600
- releaseId
601
- );
602
- const allLocales = await strapi.plugin("i18n").service("locales").find();
603
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
604
- acc[locale.code] = { name: locale.name, code: locale.code };
1571
+ const releaseActionService = getService("release-action", { strapi });
1572
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1573
+ ...query
1574
+ });
1575
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1576
+ if (acc[action.contentType]) {
1577
+ return acc;
1578
+ }
1579
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1580
+ ability: ctx.state.userAbility,
1581
+ model: action.contentType
1582
+ });
1583
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
605
1584
  return acc;
606
1585
  }, {});
607
- const data = results.map((action) => {
608
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
609
- return {
610
- ...action,
611
- entry: {
612
- id: action.entry.id,
613
- contentType: {
614
- displayName,
615
- mainFieldValue: action.entry[mainField]
616
- },
617
- locale: allLocalesDictionary[action.entry.locale]
618
- }
619
- };
620
- });
1586
+ const sanitizedResults = await async.map(results, async (action) => ({
1587
+ ...action,
1588
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1589
+ }));
1590
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1591
+ const contentTypes2 = releaseActionService.getContentTypeModelsFromActions(results);
1592
+ const releaseService = getService("release", { strapi });
1593
+ const components = await releaseService.getAllComponents();
621
1594
  ctx.body = {
622
- data,
1595
+ data: groupedData,
623
1596
  meta: {
624
- pagination
1597
+ pagination,
1598
+ contentTypes: contentTypes2,
1599
+ components
625
1600
  }
626
1601
  };
627
1602
  },
@@ -630,8 +1605,8 @@ const releaseActionController = {
630
1605
  const releaseId = ctx.params.releaseId;
631
1606
  const releaseActionUpdateArgs = ctx.request.body;
632
1607
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
633
- const releaseService = getService("release", { strapi });
634
- const updatedAction = await releaseService.updateAction(
1608
+ const releaseActionService = getService("release-action", { strapi });
1609
+ const updatedAction = await releaseActionService.update(
635
1610
  actionId,
636
1611
  releaseId,
637
1612
  releaseActionUpdateArgs
@@ -643,19 +1618,71 @@ const releaseActionController = {
643
1618
  async delete(ctx) {
644
1619
  const actionId = ctx.params.actionId;
645
1620
  const releaseId = ctx.params.releaseId;
646
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
647
- actionId,
648
- releaseId
649
- );
1621
+ const releaseActionService = getService("release-action", { strapi });
1622
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
650
1623
  ctx.body = {
651
1624
  data: deletedReleaseAction
652
1625
  };
653
1626
  }
654
1627
  };
655
- 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
+ };
656
1651
  const release = {
657
1652
  type: "admin",
658
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
+ },
659
1686
  {
660
1687
  method: "POST",
661
1688
  path: "/",
@@ -675,7 +1702,7 @@ const release = {
675
1702
  {
676
1703
  method: "GET",
677
1704
  path: "/",
678
- handler: "release.findMany",
1705
+ handler: "release.findPage",
679
1706
  config: {
680
1707
  policies: [
681
1708
  "admin::isAuthenticatedAdmin",
@@ -773,6 +1800,22 @@ const releaseAction = {
773
1800
  ]
774
1801
  }
775
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
+ },
776
1819
  {
777
1820
  method: "GET",
778
1821
  path: "/:releaseId/actions",
@@ -823,15 +1866,54 @@ const releaseAction = {
823
1866
  }
824
1867
  ]
825
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
+ };
826
1906
  const routes = {
1907
+ settings,
827
1908
  release,
828
1909
  "release-action": releaseAction
829
1910
  };
830
- const { features } = require("@strapi/strapi/dist/utils/ee");
831
1911
  const getPlugin = () => {
832
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1912
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
833
1913
  return {
834
1914
  register,
1915
+ bootstrap,
1916
+ destroy,
835
1917
  contentTypes,
836
1918
  services,
837
1919
  controllers,
@@ -839,6 +1921,9 @@ const getPlugin = () => {
839
1921
  };
840
1922
  }
841
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
842
1927
  contentTypes
843
1928
  };
844
1929
  };