@strapi/content-releases 0.0.0-experimental.ec089c69ff953942fb39de032c12daafaf7176e6 → 0.0.0-experimental.edc24aaa3bb5a90fa5fd4fee208167dd4e2e38d4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/{App-HjWtUYmc.js → App-D-lWdVb2.js} +594 -575
  3. package/dist/_chunks/App-D-lWdVb2.js.map +1 -0
  4. package/dist/_chunks/{App-gu1aiP6i.mjs → App-UQxgTJY5.mjs} +575 -554
  5. package/dist/_chunks/App-UQxgTJY5.mjs.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-bpIYXOfu.js → PurchaseContentReleases-Be3acS2L.js} +7 -6
  7. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-3tRbmbY3.mjs → PurchaseContentReleases-_MxP6-Dt.mjs} +8 -7
  9. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-CuUXvABk.mjs +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-CuUXvABk.mjs.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-xfAoY8N3.js +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-xfAoY8N3.js.map +1 -0
  14. package/dist/_chunks/{en-HrREghh3.js → en-BCDLTJn3.js} +8 -2
  15. package/dist/_chunks/en-BCDLTJn3.js.map +1 -0
  16. package/dist/_chunks/{en-ltT1TlKQ.mjs → en-CGXIF4vQ.mjs} +8 -2
  17. package/dist/_chunks/en-CGXIF4vQ.mjs.map +1 -0
  18. package/dist/_chunks/index-b3Ej95H7.mjs +1099 -0
  19. package/dist/_chunks/index-b3Ej95H7.mjs.map +1 -0
  20. package/dist/_chunks/index-jnv9zdcE.js +1118 -0
  21. package/dist/_chunks/index-jnv9zdcE.js.map +1 -0
  22. package/dist/_chunks/schemas-63pFihNF.mjs +44 -0
  23. package/dist/_chunks/schemas-63pFihNF.mjs.map +1 -0
  24. package/dist/_chunks/schemas-z5zp-_Gd.js +62 -0
  25. package/dist/_chunks/schemas-z5zp-_Gd.js.map +1 -0
  26. package/dist/admin/index.js +1 -15
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/index.mjs +2 -16
  29. package/dist/admin/index.mjs.map +1 -1
  30. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  31. package/dist/admin/src/components/ReleaseAction.d.ts +3 -0
  32. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  33. package/dist/admin/src/components/ReleaseActionModal.d.ts +24 -0
  34. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  35. package/dist/admin/src/components/ReleaseListCell.d.ts +0 -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 +110 -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 +845 -675
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +846 -675
  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 +23 -0
  74. package/dist/server/src/controllers/index.d.ts.map +1 -0
  75. package/dist/server/src/controllers/release-action.d.ts +9 -0
  76. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  77. package/dist/server/src/controllers/release.d.ts +17 -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 +2111 -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 +132 -0
  122. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  123. package/dist/shared/contracts/releases.d.ts +183 -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 +30 -36
  130. package/dist/_chunks/App-HjWtUYmc.js.map +0 -1
  131. package/dist/_chunks/App-gu1aiP6i.mjs.map +0 -1
  132. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +0 -1
  133. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +0 -1
  134. package/dist/_chunks/en-HrREghh3.js.map +0 -1
  135. package/dist/_chunks/en-ltT1TlKQ.mjs.map +0 -1
  136. package/dist/_chunks/index-ZNwxYN8H.js +0 -1335
  137. package/dist/_chunks/index-ZNwxYN8H.js.map +0 -1
  138. package/dist/_chunks/index-mvj9PSKd.mjs +0 -1314
  139. package/dist/_chunks/index-mvj9PSKd.mjs.map +0 -1
@@ -1,8 +1,7 @@
1
- import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, async, setCreatorFields, errors, yup as yup$1, validateYupSchema } from "@strapi/utils";
2
2
  import isEqual from "lodash/isEqual";
3
3
  import { difference, keys } from "lodash";
4
4
  import _ from "lodash/fp";
5
- import EE from "@strapi/strapi/dist/utils/ee";
6
5
  import { scheduleJob } from "node-schedule";
7
6
  import * as yup from "yup";
8
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
@@ -49,21 +48,38 @@ const ACTIONS = [
49
48
  displayName: "Add an entry to a release",
50
49
  uid: "create-action",
51
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"
52
68
  }
53
69
  ];
54
70
  const ALLOWED_WEBHOOK_EVENTS = {
55
71
  RELEASES_PUBLISH: "releases.publish"
56
72
  };
57
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
73
+ const getService = (name, { strapi: strapi2 }) => {
58
74
  return strapi2.plugin("content-releases").service(name);
59
75
  };
60
- const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
76
+ const getDraftEntryValidStatus = async ({ contentType, documentId, locale }, { strapi: strapi2 }) => {
61
77
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
62
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
63
- const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
64
- return entry;
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 });
65
81
  };
66
- const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
82
+ const isEntryValid = async (contentTypeUid, entry, { strapi: strapi2 }) => {
67
83
  try {
68
84
  await strapi2.entityValidator.validateEntityCreation(
69
85
  strapi2.getModel(contentTypeUid),
@@ -77,6 +93,38 @@ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } =
77
93
  return false;
78
94
  }
79
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
+ };
80
128
  async function deleteActionsOnDisableDraftAndPublish({
81
129
  oldContentTypes,
82
130
  contentTypes: contentTypes2
@@ -98,7 +146,7 @@ async function deleteActionsOnDisableDraftAndPublish({
98
146
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
99
147
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
100
148
  if (deletedContentTypes.length) {
101
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
149
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
102
150
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
103
151
  });
104
152
  }
@@ -117,25 +165,27 @@ async function migrateIsValidAndStatusReleases() {
117
165
  }
118
166
  }
119
167
  });
120
- mapAsync(releasesWithoutStatus, async (release2) => {
168
+ async.map(releasesWithoutStatus, async (release2) => {
121
169
  const actions = release2.actions;
122
170
  const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
123
171
  for (const action of notValidatedActions) {
124
172
  if (action.entry) {
125
- const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
126
- strapi
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
+ }
127
188
  });
128
- if (populatedEntry) {
129
- const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
130
- await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
131
- where: {
132
- id: action.id
133
- },
134
- data: {
135
- isEntryValid
136
- }
137
- });
138
- }
139
189
  }
140
190
  }
141
191
  return getService("release", { strapi }).updateReleaseStatus(release2.id);
@@ -148,7 +198,7 @@ async function migrateIsValidAndStatusReleases() {
148
198
  }
149
199
  }
150
200
  });
151
- mapAsync(publishedReleases, async (release2) => {
201
+ async.map(publishedReleases, async (release2) => {
152
202
  return strapi.db.query(RELEASE_MODEL_UID).update({
153
203
  where: {
154
204
  id: release2.id
@@ -165,7 +215,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
165
215
  (uid) => oldContentTypes[uid]?.options?.draftAndPublish
166
216
  );
167
217
  const releasesAffected = /* @__PURE__ */ new Set();
168
- mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
218
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
169
219
  const oldContentType = oldContentTypes[contentTypeUID];
170
220
  const contentType = contentTypes2[contentTypeUID];
171
221
  if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
@@ -178,30 +228,30 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
178
228
  release: true
179
229
  }
180
230
  });
181
- await mapAsync(actions, async (action) => {
182
- if (action.entry && action.release) {
183
- const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
- strapi
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
+ }
185
249
  });
186
- if (populatedEntry) {
187
- const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
188
- strapi
189
- });
190
- releasesAffected.add(action.release.id);
191
- await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
192
- where: {
193
- id: action.id
194
- },
195
- data: {
196
- isEntryValid
197
- }
198
- });
199
- }
200
250
  }
201
251
  });
202
252
  }
203
253
  }).then(() => {
204
- mapAsync(releasesAffected, async (releaseId) => {
254
+ async.map(releasesAffected, async (releaseId) => {
205
255
  return getService("release", { strapi }).updateReleaseStatus(releaseId);
206
256
  });
207
257
  });
@@ -253,11 +303,39 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
253
303
  }
254
304
  }
255
305
  }
256
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
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
+ };
257
334
  const register = async ({ strapi: strapi2 }) => {
258
- if (features$2.isEnabled("cms-content-releases")) {
259
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
260
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
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);
261
339
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
262
340
  }
263
341
  if (strapi2.plugin("graphql")) {
@@ -266,129 +344,134 @@ const register = async ({ strapi: strapi2 }) => {
266
344
  graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
267
345
  }
268
346
  };
269
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
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
+ };
270
445
  const bootstrap = async ({ strapi: strapi2 }) => {
271
- if (features$1.isEnabled("cms-content-releases")) {
446
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
272
447
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
273
448
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
274
449
  );
275
450
  strapi2.db.lifecycles.subscribe({
276
451
  models: contentTypesWithDraftAndPublish,
277
- async afterDelete(event) {
278
- try {
279
- const { model, result } = event;
280
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
281
- const { id } = result;
282
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
283
- where: {
284
- actions: {
285
- target_type: model.uid,
286
- target_id: id
287
- }
288
- }
289
- });
290
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
291
- where: {
292
- target_type: model.uid,
293
- target_id: id
294
- }
295
- });
296
- for (const release2 of releases) {
297
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
298
- }
299
- }
300
- } catch (error) {
301
- strapi2.log.error("Error while deleting release actions after entry delete", { error });
302
- }
303
- },
304
- /**
305
- * deleteMany hook doesn't return the deleted entries ids
306
- * so we need to fetch them before deleting the entries to save the ids on our state
307
- */
308
- async beforeDeleteMany(event) {
309
- const { model, params } = event;
310
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
311
- const { where } = params;
312
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
313
- event.state.entriesToDelete = entriesToDelete;
314
- }
315
- },
316
452
  /**
317
- * We delete the release actions related to deleted entries
318
- * We make this only after deleteMany is succesfully executed to avoid errors
453
+ * deleteMany is still used outside documents service, for example when deleting a locale
319
454
  */
320
455
  async afterDeleteMany(event) {
321
456
  try {
322
- const { model, state } = event;
323
- const entriesToDelete = state.entriesToDelete;
324
- if (entriesToDelete) {
325
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
326
- where: {
327
- actions: {
328
- target_type: model.uid,
329
- target_id: {
330
- $in: entriesToDelete.map(
331
- (entry) => entry.id
332
- )
333
- }
334
- }
335
- }
336
- });
337
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
338
- where: {
339
- target_type: model.uid,
340
- target_id: {
341
- $in: entriesToDelete.map((entry) => entry.id)
342
- }
343
- }
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 }
344
464
  });
345
- for (const release2 of releases) {
346
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
347
- }
348
465
  }
349
466
  } catch (error) {
350
467
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
351
468
  error
352
469
  });
353
470
  }
354
- },
355
- async afterUpdate(event) {
356
- try {
357
- const { model, result } = event;
358
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
359
- const isEntryValid = await getEntryValidStatus(
360
- model.uid,
361
- result,
362
- {
363
- strapi: strapi2
364
- }
365
- );
366
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
367
- where: {
368
- target_type: model.uid,
369
- target_id: result.id
370
- },
371
- data: {
372
- isEntryValid
373
- }
374
- });
375
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
376
- where: {
377
- actions: {
378
- target_type: model.uid,
379
- target_id: result.id
380
- }
381
- }
382
- });
383
- for (const release2 of releases) {
384
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
385
- }
386
- }
387
- } catch (error) {
388
- strapi2.log.error("Error while updating release actions after entry update", { error });
389
- }
390
471
  }
391
472
  });
473
+ strapi2.documents.use(deleteActionsOnDelete);
474
+ strapi2.documents.use(updateActionsOnUpdate);
392
475
  getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
393
476
  strapi2.log.error(
394
477
  "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
@@ -396,7 +479,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
396
479
  throw err;
397
480
  });
398
481
  Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
399
- strapi2.webhookStore.addAllowedEvent(key, value);
482
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
400
483
  });
401
484
  }
402
485
  };
@@ -480,15 +563,13 @@ const schema = {
480
563
  enum: ["publish", "unpublish"],
481
564
  required: true
482
565
  },
483
- entry: {
484
- type: "relation",
485
- relation: "morphToOne",
486
- configurable: false
487
- },
488
566
  contentType: {
489
567
  type: "string",
490
568
  required: true
491
569
  },
570
+ entryDocumentId: {
571
+ type: "string"
572
+ },
492
573
  locale: {
493
574
  type: "string"
494
575
  },
@@ -510,18 +591,6 @@ const contentTypes = {
510
591
  release: release$1,
511
592
  "release-action": releaseAction$1
512
593
  };
513
- const getGroupName = (queryValue) => {
514
- switch (queryValue) {
515
- case "contentType":
516
- return "contentType.displayName";
517
- case "action":
518
- return "type";
519
- case "locale":
520
- return _.getOr("No locale", "locale.name");
521
- default:
522
- return "contentType.displayName";
523
- }
524
- };
525
594
  const createReleaseService = ({ strapi: strapi2 }) => {
526
595
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
527
596
  strapi2.eventHub.emit(event, {
@@ -530,93 +599,32 @@ const createReleaseService = ({ strapi: strapi2 }) => {
530
599
  release: release2
531
600
  });
532
601
  };
533
- const publishSingleTypeAction = async (uid, actionType, entryId) => {
534
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
535
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
536
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
537
- const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
538
- try {
539
- if (actionType === "publish") {
540
- await entityManagerService.publish(entry, uid);
541
- } else {
542
- await entityManagerService.unpublish(entry, uid);
543
- }
544
- } catch (error) {
545
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
546
- ;
547
- else {
548
- throw error;
549
- }
550
- }
551
- };
552
- const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
553
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
554
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
555
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
556
- const entriesToPublish = await strapi2.entityService.findMany(uid, {
557
- filters: {
558
- id: {
559
- $in: entriesToPublishIds
560
- }
561
- },
562
- populate
563
- });
564
- const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
565
- filters: {
566
- id: {
567
- $in: entriestoUnpublishIds
568
- }
569
- },
570
- populate
571
- });
572
- if (entriesToPublish.length > 0) {
573
- await entityManagerService.publishMany(entriesToPublish, uid);
574
- }
575
- if (entriesToUnpublish.length > 0) {
576
- await entityManagerService.unpublishMany(entriesToUnpublish, uid);
577
- }
578
- };
579
602
  const getFormattedActions = async (releaseId) => {
580
603
  const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
581
604
  where: {
582
605
  release: {
583
606
  id: releaseId
584
607
  }
585
- },
586
- populate: {
587
- entry: {
588
- fields: ["id"]
589
- }
590
608
  }
591
609
  });
592
610
  if (actions.length === 0) {
593
611
  throw new errors.ValidationError("No entries to publish");
594
612
  }
595
- const collectionTypeActions = {};
596
- const singleTypeActions = [];
613
+ const formattedActions = {};
597
614
  for (const action of actions) {
598
615
  const contentTypeUid = action.contentType;
599
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
600
- if (!collectionTypeActions[contentTypeUid]) {
601
- collectionTypeActions[contentTypeUid] = {
602
- entriesToPublishIds: [],
603
- entriesToUnpublishIds: []
604
- };
605
- }
606
- if (action.type === "publish") {
607
- collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
608
- } else {
609
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
610
- }
611
- } else {
612
- singleTypeActions.push({
613
- uid: contentTypeUid,
614
- action: action.type,
615
- id: action.entry.id
616
- });
616
+ if (!formattedActions[contentTypeUid]) {
617
+ formattedActions[contentTypeUid] = {
618
+ publish: [],
619
+ unpublish: []
620
+ };
617
621
  }
622
+ formattedActions[contentTypeUid][action.type].push({
623
+ documentId: action.entryDocumentId,
624
+ locale: action.locale
625
+ });
618
626
  }
619
- return { collectionTypeActions, singleTypeActions };
627
+ return formattedActions;
620
628
  };
621
629
  return {
622
630
  async create(releaseData, { user }) {
@@ -631,7 +639,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
631
639
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
632
640
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
633
641
  ]);
634
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
642
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
635
643
  data: {
636
644
  ...releaseWithCreatorFields,
637
645
  status: "empty"
@@ -645,107 +653,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
645
653
  return release2;
646
654
  },
647
655
  async findOne(id, query = {}) {
648
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
649
- ...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 }
650
660
  });
651
661
  return release2;
652
662
  },
653
663
  findPage(query) {
654
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
655
- ...query,
664
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
665
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
666
+ ...dbQuery,
656
667
  populate: {
657
668
  actions: {
658
- // @ts-expect-error Ignore missing properties
659
669
  count: true
660
670
  }
661
671
  }
662
672
  });
663
673
  },
664
- async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
665
- let entries = entriesIds;
666
- if (!Array.isArray(entriesIds)) {
667
- entries = [entriesIds];
668
- }
669
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
670
- where: {
671
- actions: {
672
- target_type: contentTypeUid,
673
- target_id: {
674
- $in: entries
675
- }
676
- },
677
- releasedAt: {
678
- $null: true
679
- }
680
- },
681
- populate: {
682
- // Filter the action to get only the content type entry
683
- actions: {
684
- where: {
685
- target_type: contentTypeUid,
686
- target_id: {
687
- $in: entries
688
- }
689
- },
690
- populate: {
691
- entry: {
692
- select: ["id"]
693
- }
694
- }
695
- }
696
- }
697
- });
698
- return releases.map((release2) => {
699
- if (release2.actions?.length) {
700
- const actionsForEntry = release2.actions;
701
- delete release2.actions;
702
- return {
703
- ...release2,
704
- actions: actionsForEntry
705
- };
706
- }
707
- return release2;
708
- });
709
- },
710
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
711
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
712
- where: {
713
- releasedAt: {
714
- $null: true
715
- },
716
- actions: {
717
- target_type: contentTypeUid,
718
- target_id: entryId
719
- }
720
- }
721
- });
722
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
723
- where: {
724
- $or: [
725
- {
726
- id: {
727
- $notIn: releasesRelated.map((release2) => release2.id)
728
- }
729
- },
730
- {
731
- actions: null
732
- }
733
- ],
734
- releasedAt: {
735
- $null: true
736
- }
737
- }
738
- });
739
- return releases.map((release2) => {
740
- if (release2.actions?.length) {
741
- const [actionForEntry] = release2.actions;
742
- delete release2.actions;
743
- return {
744
- ...release2,
745
- action: actionForEntry
746
- };
747
- }
748
- return release2;
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
749
678
  });
750
679
  },
751
680
  async update(id, releaseData, { user }) {
@@ -760,19 +689,15 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
689
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
761
690
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
762
691
  ]);
763
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
692
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
764
693
  if (!release2) {
765
694
  throw new errors.NotFoundError(`No release found for id ${id}`);
766
695
  }
767
696
  if (release2.releasedAt) {
768
697
  throw new errors.ValidationError("Release already published");
769
698
  }
770
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
771
- /*
772
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
773
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
774
- */
775
- // @ts-expect-error see above
699
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
700
+ where: { id },
776
701
  data: releaseWithCreatorFields
777
702
  });
778
703
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -785,130 +710,6 @@ const createReleaseService = ({ strapi: strapi2 }) => {
785
710
  strapi2.telemetry.send("didUpdateContentRelease");
786
711
  return updatedRelease;
787
712
  },
788
- async createAction(releaseId, action) {
789
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
790
- strapi: strapi2
791
- });
792
- await Promise.all([
793
- validateEntryContentType(action.entry.contentType),
794
- validateUniqueEntry(releaseId, action)
795
- ]);
796
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
797
- if (!release2) {
798
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
799
- }
800
- if (release2.releasedAt) {
801
- throw new errors.ValidationError("Release already published");
802
- }
803
- const { entry, type } = action;
804
- const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
805
- const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
806
- const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
807
- data: {
808
- type,
809
- contentType: entry.contentType,
810
- locale: entry.locale,
811
- isEntryValid,
812
- entry: {
813
- id: entry.id,
814
- __type: entry.contentType,
815
- __pivot: { field: "entry" }
816
- },
817
- release: releaseId
818
- },
819
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
820
- });
821
- this.updateReleaseStatus(releaseId);
822
- return releaseAction2;
823
- },
824
- async findActions(releaseId, query) {
825
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
826
- fields: ["id"]
827
- });
828
- if (!release2) {
829
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
830
- }
831
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
832
- ...query,
833
- populate: {
834
- entry: {
835
- populate: "*"
836
- }
837
- },
838
- filters: {
839
- release: releaseId
840
- }
841
- });
842
- },
843
- async countActions(query) {
844
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
845
- },
846
- async groupActions(actions, groupBy) {
847
- const contentTypeUids = actions.reduce((acc, action) => {
848
- if (!acc.includes(action.contentType)) {
849
- acc.push(action.contentType);
850
- }
851
- return acc;
852
- }, []);
853
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
854
- contentTypeUids
855
- );
856
- const allLocalesDictionary = await this.getLocalesDataForActions();
857
- const formattedData = actions.map((action) => {
858
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
859
- return {
860
- ...action,
861
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
862
- contentType: {
863
- displayName,
864
- mainFieldValue: action.entry[mainField],
865
- uid: action.contentType
866
- }
867
- };
868
- });
869
- const groupName = getGroupName(groupBy);
870
- return _.groupBy(groupName)(formattedData);
871
- },
872
- async getLocalesDataForActions() {
873
- if (!strapi2.plugin("i18n")) {
874
- return {};
875
- }
876
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
877
- return allLocales.reduce((acc, locale) => {
878
- acc[locale.code] = { name: locale.name, code: locale.code };
879
- return acc;
880
- }, {});
881
- },
882
- async getContentTypesDataForActions(contentTypesUids) {
883
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
884
- const contentTypesData = {};
885
- for (const contentTypeUid of contentTypesUids) {
886
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
887
- uid: contentTypeUid
888
- });
889
- contentTypesData[contentTypeUid] = {
890
- mainField: contentTypeConfig.settings.mainField,
891
- displayName: strapi2.getModel(contentTypeUid).info.displayName
892
- };
893
- }
894
- return contentTypesData;
895
- },
896
- getContentTypeModelsFromActions(actions) {
897
- const contentTypeUids = actions.reduce((acc, action) => {
898
- if (!acc.includes(action.contentType)) {
899
- acc.push(action.contentType);
900
- }
901
- return acc;
902
- }, []);
903
- const contentTypeModelsMap = contentTypeUids.reduce(
904
- (acc, contentTypeUid) => {
905
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
906
- return acc;
907
- },
908
- {}
909
- );
910
- return contentTypeModelsMap;
911
- },
912
713
  async getAllComponents() {
913
714
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
914
715
  const components = await contentManagerComponentsService.findAllComponents();
@@ -922,10 +723,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
922
723
  return componentsMap;
923
724
  },
924
725
  async delete(releaseId) {
925
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
726
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
727
+ where: { id: releaseId },
926
728
  populate: {
927
729
  actions: {
928
- fields: ["id"]
730
+ select: ["id"]
929
731
  }
930
732
  }
931
733
  });
@@ -943,7 +745,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
943
745
  }
944
746
  }
945
747
  });
946
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
748
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
749
+ where: {
750
+ id: releaseId
751
+ }
752
+ });
947
753
  });
948
754
  if (release2.scheduledAt) {
949
755
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -969,22 +775,19 @@ const createReleaseService = ({ strapi: strapi2 }) => {
969
775
  }
970
776
  try {
971
777
  strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
972
- const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
973
- releaseId
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
+ )
974
790
  );
975
- await strapi2.db.transaction(async () => {
976
- for (const { uid, action, id } of singleTypeActions) {
977
- await publishSingleTypeAction(uid, action, id);
978
- }
979
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
980
- const uid = contentTypeUid;
981
- await publishCollectionTypeAction(
982
- uid,
983
- collectionTypeActions[uid].entriesToPublishIds,
984
- collectionTypeActions[uid].entriesToUnpublishIds
985
- );
986
- }
987
- });
988
791
  const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
989
792
  where: {
990
793
  id: releaseId
@@ -1014,13 +817,216 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1014
817
  };
1015
818
  }
1016
819
  });
1017
- if (error) {
1018
- throw error;
1019
- }
1020
- return release2;
820
+ if (error instanceof Error) {
821
+ throw error;
822
+ }
823
+ return release2;
824
+ },
825
+ async updateReleaseStatus(releaseId) {
826
+ const releaseActionService = getService("release-action", { strapi: strapi2 });
827
+ const [totalActions, invalidActions] = await Promise.all([
828
+ releaseActionService.countActions({
829
+ filters: {
830
+ release: releaseId
831
+ }
832
+ }),
833
+ releaseActionService.countActions({
834
+ filters: {
835
+ release: releaseId,
836
+ isEntryValid: false
837
+ }
838
+ })
839
+ ]);
840
+ if (totalActions > 0) {
841
+ if (invalidActions > 0) {
842
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
843
+ where: {
844
+ id: releaseId
845
+ },
846
+ data: {
847
+ status: "blocked"
848
+ }
849
+ });
850
+ }
851
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
852
+ where: {
853
+ id: releaseId
854
+ },
855
+ data: {
856
+ status: "ready"
857
+ }
858
+ });
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) => {
895
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
896
+ const contentTypesData = {};
897
+ for (const contentTypeUid of contentTypesUids) {
898
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
899
+ uid: contentTypeUid
900
+ });
901
+ contentTypesData[contentTypeUid] = {
902
+ mainField: contentTypeConfig.settings.mainField,
903
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
904
+ };
905
+ }
906
+ return contentTypesData;
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
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}`);
952
+ }
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,
956
+ where: {
957
+ release: releaseId
958
+ }
959
+ });
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);
988
+ }
989
+ return acc;
990
+ }, []);
991
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
992
+ const allLocalesDictionary = await getLocalesDataForActions();
993
+ const formattedData = actions.map((action) => {
994
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
995
+ return {
996
+ ...action,
997
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
998
+ contentType: {
999
+ displayName,
1000
+ mainFieldValue: action.entry[mainField],
1001
+ uid: action.contentType
1002
+ }
1003
+ };
1004
+ });
1005
+ const groupName = getGroupName(groupBy);
1006
+ return _.groupBy(groupName)(formattedData);
1021
1007
  },
1022
- async updateAction(actionId, releaseId, update) {
1023
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1008
+ getContentTypeModelsFromActions(actions) {
1009
+ const contentTypeUids = actions.reduce((acc, action) => {
1010
+ if (!acc.includes(action.contentType)) {
1011
+ acc.push(action.contentType);
1012
+ }
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({
1024
1030
  where: {
1025
1031
  id: actionId,
1026
1032
  release: {
@@ -1029,17 +1035,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1029
1035
  $null: true
1030
1036
  }
1031
1037
  }
1032
- },
1033
- data: update
1038
+ }
1034
1039
  });
1035
- if (!updatedAction) {
1040
+ if (!action) {
1036
1041
  throw new errors.NotFoundError(
1037
1042
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1038
1043
  );
1039
1044
  }
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);
1040
1071
  return updatedAction;
1041
1072
  },
1042
- async deleteAction(actionId, releaseId) {
1073
+ async delete(actionId, releaseId) {
1043
1074
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1044
1075
  where: {
1045
1076
  id: actionId,
@@ -1056,51 +1087,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1056
1087
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1057
1088
  );
1058
1089
  }
1059
- this.updateReleaseStatus(releaseId);
1090
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1060
1091
  return deletedAction;
1061
- },
1062
- async updateReleaseStatus(releaseId) {
1063
- const [totalActions, invalidActions] = await Promise.all([
1064
- this.countActions({
1065
- filters: {
1066
- release: releaseId
1067
- }
1068
- }),
1069
- this.countActions({
1070
- filters: {
1071
- release: releaseId,
1072
- isEntryValid: false
1073
- }
1074
- })
1075
- ]);
1076
- if (totalActions > 0) {
1077
- if (invalidActions > 0) {
1078
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1079
- where: {
1080
- id: releaseId
1081
- },
1082
- data: {
1083
- status: "blocked"
1084
- }
1085
- });
1086
- }
1087
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1088
- where: {
1089
- id: releaseId
1090
- },
1091
- data: {
1092
- status: "ready"
1093
- }
1094
- });
1095
- }
1096
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1097
- where: {
1098
- id: releaseId
1099
- },
1100
- data: {
1101
- status: "empty"
1102
- }
1103
- });
1104
1092
  }
1105
1093
  };
1106
1094
  };
@@ -1112,37 +1100,43 @@ class AlreadyOnReleaseError extends errors.ApplicationError {
1112
1100
  }
1113
1101
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1114
1102
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1115
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1116
- 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
+ }
1117
1110
  });
1118
1111
  if (!release2) {
1119
1112
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1120
1113
  }
1121
1114
  const isEntryInRelease = release2.actions.some(
1122
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1115
+ (action) => Number(action.entryDocumentId) === Number(releaseActionArgs.entryDocumentId) && action.contentType === releaseActionArgs.contentType && action.locale === releaseActionArgs.locale
1123
1116
  );
1124
1117
  if (isEntryInRelease) {
1125
1118
  throw new AlreadyOnReleaseError(
1126
- `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1119
+ `Entry with documentId ${releaseActionArgs.entryDocumentId} ${releaseActionArgs.locale ? `(${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1127
1120
  );
1128
1121
  }
1129
1122
  },
1130
- validateEntryContentType(contentTypeUid) {
1123
+ validateEntryData(contentTypeUid, entryDocumentId) {
1131
1124
  const contentType = strapi2.contentType(contentTypeUid);
1132
1125
  if (!contentType) {
1133
1126
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1134
1127
  }
1135
- if (!contentType.options?.draftAndPublish) {
1128
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
1136
1129
  throw new errors.ValidationError(
1137
1130
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1138
1131
  );
1139
1132
  }
1133
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1134
+ throw new errors.ValidationError("Document id is required for collection type");
1135
+ }
1140
1136
  },
1141
1137
  async validatePendingReleasesLimit() {
1142
- const maximumPendingReleases = (
1143
- // @ts-expect-error - options is not typed into features
1144
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
1145
- );
1138
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1139
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1146
1140
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1147
1141
  filters: {
1148
1142
  releasedAt: {
@@ -1155,8 +1149,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1155
1149
  }
1156
1150
  },
1157
1151
  async validateUniqueNameForPendingRelease(name, id) {
1158
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1159
- filters: {
1152
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1153
+ where: {
1160
1154
  releasedAt: {
1161
1155
  $null: true
1162
1156
  },
@@ -1185,7 +1179,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1185
1179
  }
1186
1180
  const job = scheduleJob(scheduleDate, async () => {
1187
1181
  try {
1188
- await getService("release").publish(releaseId);
1182
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1189
1183
  } catch (error) {
1190
1184
  }
1191
1185
  this.cancel(releaseId);
@@ -1227,85 +1221,159 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1227
1221
  }
1228
1222
  };
1229
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
+ };
1230
1245
  const services = {
1231
1246
  release: createReleaseService,
1247
+ "release-action": createReleaseActionService,
1232
1248
  "release-validation": createReleaseValidationService,
1233
- scheduling: createSchedulingService
1249
+ scheduling: createSchedulingService,
1250
+ settings: createSettingsService
1234
1251
  };
1235
- const RELEASE_SCHEMA = yup.object().shape({
1236
- name: yup.string().trim().required(),
1237
- scheduledAt: yup.string().nullable(),
1238
- isScheduled: yup.boolean().optional(),
1239
- time: yup.string().when("isScheduled", {
1240
- is: true,
1241
- then: yup.string().trim().required(),
1242
- otherwise: yup.string().nullable()
1243
- }),
1244
- timezone: yup.string().when("isScheduled", {
1245
- is: true,
1246
- then: yup.string().required().nullable(),
1247
- otherwise: yup.string().nullable()
1248
- }),
1249
- date: yup.string().when("isScheduled", {
1250
- is: true,
1251
- then: yup.string().required().nullable(),
1252
- otherwise: yup.string().nullable()
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()
1253
1259
  })
1254
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()
1266
+ }).required().noUnknown();
1255
1267
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1268
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1269
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1270
+ );
1256
1271
  const releaseController = {
1257
- async findMany(ctx) {
1258
- 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({
1259
1279
  ability: ctx.state.userAbility,
1260
1280
  model: RELEASE_MODEL_UID
1261
1281
  });
1262
1282
  await permissionsManager.validateQuery(ctx.query);
1263
1283
  const releaseService = getService("release", { strapi });
1264
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1265
- if (isFindManyForContentTypeEntry) {
1266
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1267
- const contentTypeUid = query.contentTypeUid;
1268
- const entryId = query.entryId;
1269
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1270
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1271
- ctx.body = { data };
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
+ }
1303
+ });
1304
+ ctx.body = { data: releases };
1272
1305
  } else {
1273
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1274
- const { results, pagination } = await releaseService.findPage(query);
1275
- const data = results.map((release2) => {
1276
- const { actions, ...releaseData } = release2;
1277
- return {
1278
- ...releaseData,
1306
+ const relatedReleases = await releaseService.findMany({
1307
+ where: {
1308
+ releasedAt: null,
1279
1309
  actions: {
1280
- meta: {
1281
- count: actions.count
1282
- }
1310
+ contentType,
1311
+ entryDocumentId: entryDocumentId ?? null,
1312
+ locale: locale ?? null
1283
1313
  }
1284
- };
1314
+ }
1285
1315
  });
1286
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1316
+ const releases = await releaseService.findMany({
1287
1317
  where: {
1318
+ $or: [
1319
+ {
1320
+ id: {
1321
+ $notIn: relatedReleases.map((release2) => release2.id)
1322
+ }
1323
+ },
1324
+ {
1325
+ actions: null
1326
+ }
1327
+ ],
1288
1328
  releasedAt: null
1289
1329
  }
1290
1330
  });
1291
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1331
+ ctx.body = { data: releases };
1292
1332
  }
1293
1333
  },
1334
+ async findPage(ctx) {
1335
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1336
+ ability: ctx.state.userAbility,
1337
+ model: RELEASE_MODEL_UID
1338
+ });
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
1357
+ }
1358
+ });
1359
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1360
+ },
1294
1361
  async findOne(ctx) {
1295
1362
  const id = ctx.params.id;
1296
1363
  const releaseService = getService("release", { strapi });
1364
+ const releaseActionService = getService("release-action", { strapi });
1297
1365
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1298
1366
  if (!release2) {
1299
1367
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
1300
1368
  }
1301
- const count = await releaseService.countActions({
1369
+ const count = await releaseActionService.countActions({
1302
1370
  filters: {
1303
1371
  release: id
1304
1372
  }
1305
1373
  });
1306
1374
  const sanitizedRelease = {
1307
1375
  ...release2,
1308
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1376
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1309
1377
  };
1310
1378
  const data = {
1311
1379
  ...sanitizedRelease,
@@ -1317,46 +1385,56 @@ const releaseController = {
1317
1385
  };
1318
1386
  ctx.body = { data };
1319
1387
  },
1320
- async mapEntriesToReleases(ctx) {
1321
- const { contentTypeUid, entriesIds } = ctx.query;
1322
- if (!contentTypeUid || !entriesIds) {
1323
- throw new errors.ValidationError("Missing required query parameters");
1324
- }
1325
- const releaseService = getService("release", { strapi });
1326
- const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1327
- contentTypeUid,
1328
- entriesIds
1329
- );
1330
- const mappedEntriesInReleases = releasesWithActions.reduce(
1331
- (acc, release2) => {
1332
- release2.actions.forEach((action) => {
1333
- if (!acc[action.entry.id]) {
1334
- acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1335
- } else {
1336
- acc[action.entry.id].push({ id: release2.id, name: release2.name });
1337
- }
1338
- });
1339
- return acc;
1340
- },
1341
- {}
1342
- );
1343
- ctx.body = {
1344
- data: mappedEntriesInReleases
1345
- };
1346
- },
1388
+ /* @TODO: Migrate to new api
1389
+ async mapEntriesToReleases(ctx: Koa.Context) {
1390
+ const { contentTypeUid, entriesIds } = ctx.query;
1391
+
1392
+ if (!contentTypeUid || !entriesIds) {
1393
+ throw new errors.ValidationError('Missing required query parameters');
1394
+ }
1395
+
1396
+ const releaseService = getService('release', { strapi });
1397
+
1398
+ const releasesWithActions = await releaseService.findMany(
1399
+ contentTypeUid,
1400
+ entriesIds
1401
+ );
1402
+
1403
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1404
+ // TODO: Fix for v5 removed mappedEntriedToRelease
1405
+ (acc: MapEntriesToReleases.Response['data'], release: Release) => {
1406
+ release.actions.forEach((action) => {
1407
+ if (!acc[action.entry.id]) {
1408
+ acc[action.entry.id] = [{ id: release.id, name: release.name }];
1409
+ } else {
1410
+ acc[action.entry.id].push({ id: release.id, name: release.name });
1411
+ }
1412
+ });
1413
+
1414
+ return acc;
1415
+ },
1416
+ // TODO: Fix for v5 removed mappedEntriedToRelease
1417
+ {} as MapEntriesToReleases.Response['data']
1418
+ );
1419
+
1420
+ ctx.body = {
1421
+ data: mappedEntriesInReleases,
1422
+ };
1423
+ },
1424
+ */
1347
1425
  async create(ctx) {
1348
1426
  const user = ctx.state.user;
1349
1427
  const releaseArgs = ctx.request.body;
1350
1428
  await validateRelease(releaseArgs);
1351
1429
  const releaseService = getService("release", { strapi });
1352
1430
  const release2 = await releaseService.create(releaseArgs, { user });
1353
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1431
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1354
1432
  ability: ctx.state.userAbility,
1355
1433
  model: RELEASE_MODEL_UID
1356
1434
  });
1357
- ctx.body = {
1435
+ ctx.created({
1358
1436
  data: await permissionsManager.sanitizeOutput(release2)
1359
- };
1437
+ });
1360
1438
  },
1361
1439
  async update(ctx) {
1362
1440
  const user = ctx.state.user;
@@ -1365,7 +1443,7 @@ const releaseController = {
1365
1443
  await validateRelease(releaseArgs);
1366
1444
  const releaseService = getService("release", { strapi });
1367
1445
  const release2 = await releaseService.update(id, releaseArgs, { user });
1368
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1446
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1369
1447
  ability: ctx.state.userAbility,
1370
1448
  model: RELEASE_MODEL_UID
1371
1449
  });
@@ -1382,18 +1460,18 @@ const releaseController = {
1382
1460
  };
1383
1461
  },
1384
1462
  async publish(ctx) {
1385
- const user = ctx.state.user;
1386
1463
  const id = ctx.params.id;
1387
1464
  const releaseService = getService("release", { strapi });
1388
- const release2 = await releaseService.publish(id, { user });
1465
+ const releaseActionService = getService("release-action", { strapi });
1466
+ const release2 = await releaseService.publish(id);
1389
1467
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1390
- releaseService.countActions({
1468
+ releaseActionService.countActions({
1391
1469
  filters: {
1392
1470
  release: id,
1393
1471
  type: "publish"
1394
1472
  }
1395
1473
  }),
1396
- releaseService.countActions({
1474
+ releaseActionService.countActions({
1397
1475
  filters: {
1398
1476
  release: id,
1399
1477
  type: "unpublish"
@@ -1411,42 +1489,47 @@ const releaseController = {
1411
1489
  }
1412
1490
  };
1413
1491
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
1414
- entry: yup$1.object().shape({
1415
- id: yup$1.strapiID().required(),
1416
- contentType: yup$1.string().required()
1417
- }).required(),
1492
+ contentType: yup$1.string().required(),
1493
+ entryDocumentId: yup$1.strapiID(),
1494
+ locale: yup$1.string(),
1418
1495
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1419
1496
  });
1420
1497
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
1421
1498
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1422
1499
  });
1500
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1501
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1502
+ });
1423
1503
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
1424
1504
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1505
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1425
1506
  const releaseActionController = {
1426
1507
  async create(ctx) {
1427
1508
  const releaseId = ctx.params.releaseId;
1428
1509
  const releaseActionArgs = ctx.request.body;
1429
1510
  await validateReleaseAction(releaseActionArgs);
1430
- const releaseService = getService("release", { strapi });
1431
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1432
- ctx.body = {
1511
+ const releaseActionService = getService("release-action", { strapi });
1512
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1513
+ ctx.created({
1433
1514
  data: releaseAction2
1434
- };
1515
+ });
1435
1516
  },
1436
- async createMany(ctx) {
1437
- const releaseId = ctx.params.releaseId;
1438
- const releaseActionsArgs = ctx.request.body;
1517
+ /*
1518
+ async createMany(ctx: Koa.Context) {
1519
+ const releaseId: CreateManyReleaseActions.Request['params']['releaseId'] = ctx.params.releaseId;
1520
+ const releaseActionsArgs = ctx.request.body as CreateManyReleaseActions.Request['body'];
1439
1521
  await Promise.all(
1440
1522
  releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1441
1523
  );
1442
- const releaseService = getService("release", { strapi });
1524
+ const releaseActionService = getService('release-action', { strapi });
1443
1525
  const releaseActions = await strapi.db.transaction(async () => {
1444
- const releaseActions2 = await Promise.all(
1526
+ const releaseActions = await Promise.all(
1445
1527
  releaseActionsArgs.map(async (releaseActionArgs) => {
1446
1528
  try {
1447
- const action = await releaseService.createAction(releaseId, releaseActionArgs);
1529
+ const action = await releaseActionService.create(releaseId, releaseActionArgs);
1448
1530
  return action;
1449
1531
  } catch (error) {
1532
+ // If the entry is already in the release, we don't want to throw an error, so we catch and ignore it
1450
1533
  if (error instanceof AlreadyOnReleaseError) {
1451
1534
  return null;
1452
1535
  }
@@ -1454,46 +1537,55 @@ const releaseActionController = {
1454
1537
  }
1455
1538
  })
1456
1539
  );
1457
- return releaseActions2;
1540
+ return releaseActions;
1458
1541
  });
1459
1542
  const newReleaseActions = releaseActions.filter((action) => action !== null);
1460
- ctx.body = {
1543
+ ctx.created({
1461
1544
  data: newReleaseActions,
1462
1545
  meta: {
1463
1546
  entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1464
- totalEntries: releaseActions.length
1465
- }
1466
- };
1547
+ totalEntries: releaseActions.length,
1548
+ },
1549
+ });
1467
1550
  },
1551
+ */
1468
1552
  async findMany(ctx) {
1469
1553
  const releaseId = ctx.params.releaseId;
1470
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1554
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1471
1555
  ability: ctx.state.userAbility,
1472
1556
  model: RELEASE_ACTION_MODEL_UID
1473
1557
  });
1558
+ await validateFindManyActionsParams(ctx.query);
1559
+ if (ctx.query.groupBy) {
1560
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1561
+ ctx.badRequest("Invalid groupBy parameter");
1562
+ }
1563
+ }
1564
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1565
+ delete ctx.query.groupBy;
1474
1566
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1475
- const releaseService = getService("release", { strapi });
1476
- const { results, pagination } = await releaseService.findActions(releaseId, {
1477
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1567
+ const releaseActionService = getService("release-action", { strapi });
1568
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1478
1569
  ...query
1479
1570
  });
1480
1571
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1481
1572
  if (acc[action.contentType]) {
1482
1573
  return acc;
1483
1574
  }
1484
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1575
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1485
1576
  ability: ctx.state.userAbility,
1486
1577
  model: action.contentType
1487
1578
  });
1488
1579
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1489
1580
  return acc;
1490
1581
  }, {});
1491
- const sanitizedResults = await mapAsync(results, async (action) => ({
1582
+ const sanitizedResults = await async.map(results, async (action) => ({
1492
1583
  ...action,
1493
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1584
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1494
1585
  }));
1495
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1496
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1586
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1587
+ const contentTypes2 = releaseActionService.getContentTypeModelsFromActions(results);
1588
+ const releaseService = getService("release", { strapi });
1497
1589
  const components = await releaseService.getAllComponents();
1498
1590
  ctx.body = {
1499
1591
  data: groupedData,
@@ -1509,8 +1601,8 @@ const releaseActionController = {
1509
1601
  const releaseId = ctx.params.releaseId;
1510
1602
  const releaseActionUpdateArgs = ctx.request.body;
1511
1603
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1512
- const releaseService = getService("release", { strapi });
1513
- const updatedAction = await releaseService.updateAction(
1604
+ const releaseActionService = getService("release-action", { strapi });
1605
+ const updatedAction = await releaseActionService.update(
1514
1606
  actionId,
1515
1607
  releaseId,
1516
1608
  releaseActionUpdateArgs
@@ -1522,21 +1614,61 @@ const releaseActionController = {
1522
1614
  async delete(ctx) {
1523
1615
  const actionId = ctx.params.actionId;
1524
1616
  const releaseId = ctx.params.releaseId;
1525
- const releaseService = getService("release", { strapi });
1526
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1617
+ const releaseActionService = getService("release-action", { strapi });
1618
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1527
1619
  ctx.body = {
1528
1620
  data: deletedReleaseAction
1529
1621
  };
1530
1622
  }
1531
1623
  };
1532
- const controllers = { release: releaseController, "release-action": releaseActionController };
1624
+ const SETTINGS_SCHEMA = yup.object().shape({
1625
+ defaultTimezone: yup.string().nullable().default(null)
1626
+ }).required().noUnknown();
1627
+ const validateSettings = validateYupSchema(SETTINGS_SCHEMA);
1628
+ const settingsController = {
1629
+ async find(ctx) {
1630
+ const settingsService = getService("settings", { strapi });
1631
+ const settings2 = await settingsService.find();
1632
+ ctx.body = { data: settings2 };
1633
+ },
1634
+ async update(ctx) {
1635
+ const settingsBody = ctx.request.body;
1636
+ const settings2 = await validateSettings(settingsBody);
1637
+ const settingsService = getService("settings", { strapi });
1638
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1639
+ ctx.body = { data: updatedSettings };
1640
+ }
1641
+ };
1642
+ const controllers = {
1643
+ release: releaseController,
1644
+ "release-action": releaseActionController,
1645
+ settings: settingsController
1646
+ };
1533
1647
  const release = {
1534
1648
  type: "admin",
1535
1649
  routes: [
1650
+ /*
1651
+ {
1652
+ method: 'GET',
1653
+ path: '/mapEntriesToReleases',
1654
+ handler: 'release.mapEntriesToReleases',
1655
+ config: {
1656
+ policies: [
1657
+ 'admin::isAuthenticatedAdmin',
1658
+ {
1659
+ name: 'admin::hasPermissions',
1660
+ config: {
1661
+ actions: ['plugin::content-releases.read'],
1662
+ },
1663
+ },
1664
+ ],
1665
+ },
1666
+ },
1667
+ */
1536
1668
  {
1537
1669
  method: "GET",
1538
- path: "/mapEntriesToReleases",
1539
- handler: "release.mapEntriesToReleases",
1670
+ path: "/getByDocumentAttached",
1671
+ handler: "release.findByDocumentAttached",
1540
1672
  config: {
1541
1673
  policies: [
1542
1674
  "admin::isAuthenticatedAdmin",
@@ -1568,7 +1700,7 @@ const release = {
1568
1700
  {
1569
1701
  method: "GET",
1570
1702
  path: "/",
1571
- handler: "release.findMany",
1703
+ handler: "release.findPage",
1572
1704
  config: {
1573
1705
  policies: [
1574
1706
  "admin::isAuthenticatedAdmin",
@@ -1666,22 +1798,24 @@ const releaseAction = {
1666
1798
  ]
1667
1799
  }
1668
1800
  },
1801
+ /*
1669
1802
  {
1670
- method: "POST",
1671
- path: "/:releaseId/actions/bulk",
1672
- handler: "release-action.createMany",
1803
+ method: 'POST',
1804
+ path: '/:releaseId/actions/bulk',
1805
+ handler: 'release-action.createMany',
1673
1806
  config: {
1674
1807
  policies: [
1675
- "admin::isAuthenticatedAdmin",
1808
+ 'admin::isAuthenticatedAdmin',
1676
1809
  {
1677
- name: "admin::hasPermissions",
1810
+ name: 'admin::hasPermissions',
1678
1811
  config: {
1679
- actions: ["plugin::content-releases.create-action"]
1680
- }
1681
- }
1682
- ]
1683
- }
1812
+ actions: ['plugin::content-releases.create-action'],
1813
+ },
1814
+ },
1815
+ ],
1816
+ },
1684
1817
  },
1818
+ */
1685
1819
  {
1686
1820
  method: "GET",
1687
1821
  path: "/:releaseId/actions",
@@ -1732,13 +1866,50 @@ const releaseAction = {
1732
1866
  }
1733
1867
  ]
1734
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
+ };
1735
1906
  const routes = {
1907
+ settings,
1736
1908
  release,
1737
1909
  "release-action": releaseAction
1738
1910
  };
1739
- const { features } = require("@strapi/strapi/dist/utils/ee");
1740
1911
  const getPlugin = () => {
1741
- if (features.isEnabled("cms-content-releases")) {
1912
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1742
1913
  return {
1743
1914
  register,
1744
1915
  bootstrap,