@strapi/content-releases 0.0.0-next.3cc05002fb92029975799c3113971bb5b5198d7c → 0.0.0-next.3fdd03038d558a8190aa7e17574020c5f65395e6

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 (142) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/App-BKB1esYS.js +1395 -0
  3. package/dist/_chunks/App-BKB1esYS.js.map +1 -0
  4. package/dist/_chunks/App-Cne--1Z8.mjs +1374 -0
  5. package/dist/_chunks/App-Cne--1Z8.mjs.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-Be3acS2L.js} +8 -7
  7. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-_MxP6-Dt.mjs} +9 -8
  9. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-C1WwGWIH.mjs +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-C1WwGWIH.mjs.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-kuXIwpWp.js +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-kuXIwpWp.js.map +1 -0
  14. package/dist/_chunks/{en-faJDuv3q.js → en-CmYoEnA7.js} +19 -3
  15. package/dist/_chunks/en-CmYoEnA7.js.map +1 -0
  16. package/dist/_chunks/{en-RdapH-9X.mjs → en-D0yVZFqf.mjs} +19 -3
  17. package/dist/_chunks/en-D0yVZFqf.mjs.map +1 -0
  18. package/dist/_chunks/index-5Odi61vw.js +1381 -0
  19. package/dist/_chunks/index-5Odi61vw.js.map +1 -0
  20. package/dist/_chunks/index-Cy7qwpaU.mjs +1362 -0
  21. package/dist/_chunks/index-Cy7qwpaU.mjs.map +1 -0
  22. package/dist/_chunks/schemas-BE1LxE9J.js +62 -0
  23. package/dist/_chunks/schemas-BE1LxE9J.js.map +1 -0
  24. package/dist/_chunks/schemas-DdA2ic2U.mjs +44 -0
  25. package/dist/_chunks/schemas-DdA2ic2U.mjs.map +1 -0
  26. package/dist/admin/index.js +1 -15
  27. package/dist/admin/index.js.map +1 -1
  28. package/dist/admin/index.mjs +2 -16
  29. package/dist/admin/index.mjs.map +1 -1
  30. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  31. package/dist/admin/src/components/ReleaseAction.d.ts +3 -0
  32. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  33. package/dist/admin/src/components/ReleaseActionModal.d.ts +24 -0
  34. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  35. package/dist/admin/src/components/ReleaseListCell.d.ts +28 -0
  36. package/dist/admin/src/components/ReleaseModal.d.ts +17 -0
  37. package/dist/admin/src/components/ReleasesPanel.d.ts +3 -0
  38. package/dist/admin/src/constants.d.ts +76 -0
  39. package/dist/admin/src/index.d.ts +3 -0
  40. package/dist/admin/src/modules/hooks.d.ts +7 -0
  41. package/dist/admin/src/pages/App.d.ts +1 -0
  42. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  43. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  44. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  45. package/dist/admin/src/pages/ReleasesSettingsPage.d.ts +1 -0
  46. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  47. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  48. package/dist/admin/src/pluginId.d.ts +1 -0
  49. package/dist/admin/src/services/release.d.ts +112 -0
  50. package/dist/admin/src/store/hooks.d.ts +7 -0
  51. package/dist/admin/src/utils/api.d.ts +6 -0
  52. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  53. package/dist/admin/src/utils/time.d.ts +10 -0
  54. package/dist/admin/src/validation/schemas.d.ts +6 -0
  55. package/dist/server/index.js +886 -617
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +887 -617
  58. package/dist/server/index.mjs.map +1 -1
  59. package/dist/server/src/bootstrap.d.ts +5 -0
  60. package/dist/server/src/bootstrap.d.ts.map +1 -0
  61. package/dist/server/src/constants.d.ts +21 -0
  62. package/dist/server/src/constants.d.ts.map +1 -0
  63. package/dist/server/src/content-types/index.d.ts +97 -0
  64. package/dist/server/src/content-types/index.d.ts.map +1 -0
  65. package/dist/server/src/content-types/release/index.d.ts +48 -0
  66. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  67. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  68. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  69. package/dist/server/src/content-types/release-action/index.d.ts +48 -0
  70. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  71. package/dist/server/src/content-types/release-action/schema.d.ts +47 -0
  72. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  73. package/dist/server/src/controllers/index.d.ts +25 -0
  74. package/dist/server/src/controllers/index.d.ts.map +1 -0
  75. package/dist/server/src/controllers/release-action.d.ts +10 -0
  76. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  77. package/dist/server/src/controllers/release.d.ts +18 -0
  78. package/dist/server/src/controllers/release.d.ts.map +1 -0
  79. package/dist/server/src/controllers/settings.d.ts +11 -0
  80. package/dist/server/src/controllers/settings.d.ts.map +1 -0
  81. package/dist/server/src/controllers/validation/release-action.d.ts +14 -0
  82. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  83. package/dist/server/src/controllers/validation/release.d.ts +4 -0
  84. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  85. package/dist/server/src/controllers/validation/settings.d.ts +3 -0
  86. package/dist/server/src/controllers/validation/settings.d.ts.map +1 -0
  87. package/dist/server/src/destroy.d.ts +5 -0
  88. package/dist/server/src/destroy.d.ts.map +1 -0
  89. package/dist/server/src/index.d.ts +2115 -0
  90. package/dist/server/src/index.d.ts.map +1 -0
  91. package/dist/server/src/middlewares/documents.d.ts +6 -0
  92. package/dist/server/src/middlewares/documents.d.ts.map +1 -0
  93. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts +9 -0
  94. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts.map +1 -0
  95. package/dist/server/src/migrations/index.d.ts +13 -0
  96. package/dist/server/src/migrations/index.d.ts.map +1 -0
  97. package/dist/server/src/register.d.ts +5 -0
  98. package/dist/server/src/register.d.ts.map +1 -0
  99. package/dist/server/src/routes/index.d.ts +51 -0
  100. package/dist/server/src/routes/index.d.ts.map +1 -0
  101. package/dist/server/src/routes/release-action.d.ts +18 -0
  102. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  103. package/dist/server/src/routes/release.d.ts +18 -0
  104. package/dist/server/src/routes/release.d.ts.map +1 -0
  105. package/dist/server/src/routes/settings.d.ts +18 -0
  106. package/dist/server/src/routes/settings.d.ts.map +1 -0
  107. package/dist/server/src/services/index.d.ts +1828 -0
  108. package/dist/server/src/services/index.d.ts.map +1 -0
  109. package/dist/server/src/services/release-action.d.ts +38 -0
  110. package/dist/server/src/services/release-action.d.ts.map +1 -0
  111. package/dist/server/src/services/release.d.ts +31 -0
  112. package/dist/server/src/services/release.d.ts.map +1 -0
  113. package/dist/server/src/services/scheduling.d.ts +18 -0
  114. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  115. package/dist/server/src/services/settings.d.ts +13 -0
  116. package/dist/server/src/services/settings.d.ts.map +1 -0
  117. package/dist/server/src/services/validation.d.ts +18 -0
  118. package/dist/server/src/services/validation.d.ts.map +1 -0
  119. package/dist/server/src/utils/index.d.ts +35 -0
  120. package/dist/server/src/utils/index.d.ts.map +1 -0
  121. package/dist/shared/contracts/release-actions.d.ts +130 -0
  122. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  123. package/dist/shared/contracts/releases.d.ts +184 -0
  124. package/dist/shared/contracts/releases.d.ts.map +1 -0
  125. package/dist/shared/contracts/settings.d.ts +39 -0
  126. package/dist/shared/contracts/settings.d.ts.map +1 -0
  127. package/dist/shared/types.d.ts +24 -0
  128. package/dist/shared/types.d.ts.map +1 -0
  129. package/package.json +34 -39
  130. package/dist/_chunks/App-R-kksTW7.mjs +0 -1308
  131. package/dist/_chunks/App-R-kksTW7.mjs.map +0 -1
  132. package/dist/_chunks/App-WZHc_d3m.js +0 -1331
  133. package/dist/_chunks/App-WZHc_d3m.js.map +0 -1
  134. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  135. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  136. package/dist/_chunks/en-RdapH-9X.mjs.map +0 -1
  137. package/dist/_chunks/en-faJDuv3q.js.map +0 -1
  138. package/dist/_chunks/index-k6fw6RYi.js +0 -1033
  139. package/dist/_chunks/index-k6fw6RYi.js.map +0 -1
  140. package/dist/_chunks/index-vpSczx8v.mjs +0 -1012
  141. package/dist/_chunks/index-vpSczx8v.mjs.map +0 -1
  142. package/strapi-server.js +0 -3
@@ -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,42 @@ 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
+ const entry = await strapi2.documents(contentType).findOne({ documentId, locale, populate, status });
105
+ if (status === "published" && !entry) {
106
+ return strapi2.documents(contentType).findOne({ documentId, locale, populate, status: "draft" });
107
+ }
108
+ return entry;
109
+ }
110
+ return strapi2.documents(contentType).findFirst({ locale, populate, status });
111
+ };
112
+ const getEntryStatus = async (contentType, entry) => {
113
+ if (entry.publishedAt) {
114
+ return "published";
115
+ }
116
+ const publishedEntry = await strapi.documents(contentType).findOne({
117
+ documentId: entry.documentId,
118
+ locale: entry.locale,
119
+ status: "published",
120
+ fields: ["updatedAt"]
121
+ });
122
+ if (!publishedEntry) {
123
+ return "draft";
124
+ }
125
+ const entryUpdatedAt = new Date(entry.updatedAt).getTime();
126
+ const publishedEntryUpdatedAt = new Date(publishedEntry.updatedAt).getTime();
127
+ if (entryUpdatedAt > publishedEntryUpdatedAt) {
128
+ return "modified";
129
+ }
130
+ return "published";
131
+ };
80
132
  async function deleteActionsOnDisableDraftAndPublish({
81
133
  oldContentTypes,
82
134
  contentTypes: contentTypes2
@@ -98,7 +150,7 @@ async function deleteActionsOnDisableDraftAndPublish({
98
150
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
99
151
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
100
152
  if (deletedContentTypes.length) {
101
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
153
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
102
154
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
103
155
  });
104
156
  }
@@ -117,25 +169,27 @@ async function migrateIsValidAndStatusReleases() {
117
169
  }
118
170
  }
119
171
  });
120
- mapAsync(releasesWithoutStatus, async (release2) => {
172
+ async.map(releasesWithoutStatus, async (release2) => {
121
173
  const actions = release2.actions;
122
174
  const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
123
175
  for (const action of notValidatedActions) {
124
176
  if (action.entry) {
125
- const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
126
- strapi
177
+ const isEntryValid2 = getDraftEntryValidStatus(
178
+ {
179
+ contentType: action.contentType,
180
+ documentId: action.entryDocumentId,
181
+ locale: action.locale
182
+ },
183
+ { strapi }
184
+ );
185
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
186
+ where: {
187
+ id: action.id
188
+ },
189
+ data: {
190
+ isEntryValid: isEntryValid2
191
+ }
127
192
  });
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
193
  }
140
194
  }
141
195
  return getService("release", { strapi }).updateReleaseStatus(release2.id);
@@ -148,7 +202,7 @@ async function migrateIsValidAndStatusReleases() {
148
202
  }
149
203
  }
150
204
  });
151
- mapAsync(publishedReleases, async (release2) => {
205
+ async.map(publishedReleases, async (release2) => {
152
206
  return strapi.db.query(RELEASE_MODEL_UID).update({
153
207
  where: {
154
208
  id: release2.id
@@ -165,7 +219,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
165
219
  (uid) => oldContentTypes[uid]?.options?.draftAndPublish
166
220
  );
167
221
  const releasesAffected = /* @__PURE__ */ new Set();
168
- mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
222
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
169
223
  const oldContentType = oldContentTypes[contentTypeUID];
170
224
  const contentType = contentTypes2[contentTypeUID];
171
225
  if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
@@ -178,30 +232,30 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
178
232
  release: true
179
233
  }
180
234
  });
181
- await mapAsync(actions, async (action) => {
182
- if (action.entry && action.release) {
183
- const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
- strapi
235
+ await async.map(actions, async (action) => {
236
+ if (action.entry && action.release && action.type === "publish") {
237
+ const isEntryValid2 = await getDraftEntryValidStatus(
238
+ {
239
+ contentType: contentTypeUID,
240
+ documentId: action.entryDocumentId,
241
+ locale: action.locale
242
+ },
243
+ { strapi }
244
+ );
245
+ releasesAffected.add(action.release.id);
246
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
247
+ where: {
248
+ id: action.id
249
+ },
250
+ data: {
251
+ isEntryValid: isEntryValid2
252
+ }
185
253
  });
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
254
  }
201
255
  });
202
256
  }
203
257
  }).then(() => {
204
- mapAsync(releasesAffected, async (releaseId) => {
258
+ async.map(releasesAffected, async (releaseId) => {
205
259
  return getService("release", { strapi }).updateReleaseStatus(releaseId);
206
260
  });
207
261
  });
@@ -211,13 +265,16 @@ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: cont
211
265
  if (!oldContentTypes) {
212
266
  return;
213
267
  }
268
+ const i18nPlugin = strapi.plugin("i18n");
269
+ if (!i18nPlugin) {
270
+ return;
271
+ }
214
272
  for (const uid in contentTypes2) {
215
273
  if (!oldContentTypes[uid]) {
216
274
  continue;
217
275
  }
218
276
  const oldContentType = oldContentTypes[uid];
219
277
  const contentType = contentTypes2[uid];
220
- const i18nPlugin = strapi.plugin("i18n");
221
278
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
222
279
  if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
223
280
  await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
@@ -230,13 +287,16 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
230
287
  if (!oldContentTypes) {
231
288
  return;
232
289
  }
290
+ const i18nPlugin = strapi.plugin("i18n");
291
+ if (!i18nPlugin) {
292
+ return;
293
+ }
233
294
  for (const uid in contentTypes2) {
234
295
  if (!oldContentTypes[uid]) {
235
296
  continue;
236
297
  }
237
298
  const oldContentType = oldContentTypes[uid];
238
299
  const contentType = contentTypes2[uid];
239
- const i18nPlugin = strapi.plugin("i18n");
240
300
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
241
301
  const { getDefaultLocale } = i18nPlugin.service("locales");
242
302
  if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
@@ -247,11 +307,43 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
247
307
  }
248
308
  }
249
309
  }
250
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
310
+ const addEntryDocumentToReleaseActions = {
311
+ name: "content-releases::5.0.0-add-entry-document-id-to-release-actions",
312
+ async up(trx, db) {
313
+ const hasTable = await trx.schema.hasTable("strapi_release_actions");
314
+ if (!hasTable) {
315
+ return;
316
+ }
317
+ const hasPolymorphicColumn = await trx.schema.hasColumn("strapi_release_actions", "target_id");
318
+ if (hasPolymorphicColumn) {
319
+ const hasEntryDocumentIdColumn = await trx.schema.hasColumn(
320
+ "strapi_release_actions",
321
+ "entry_document_id"
322
+ );
323
+ if (!hasEntryDocumentIdColumn) {
324
+ await trx.schema.alterTable("strapi_release_actions", (table) => {
325
+ table.string("entry_document_id");
326
+ });
327
+ }
328
+ const releaseActions = await trx.select("*").from("strapi_release_actions");
329
+ async.map(releaseActions, async (action) => {
330
+ const { target_type, target_id } = action;
331
+ const entry = await db.query(target_type).findOne({ where: { id: target_id } });
332
+ if (entry) {
333
+ await trx("strapi_release_actions").update({ entry_document_id: entry.documentId }).where("id", action.id);
334
+ }
335
+ });
336
+ }
337
+ },
338
+ async down() {
339
+ throw new Error("not implemented");
340
+ }
341
+ };
251
342
  const register = async ({ strapi: strapi2 }) => {
252
- if (features$2.isEnabled("cms-content-releases")) {
253
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
254
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
343
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
344
+ await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
345
+ strapi2.db.migrations.providers.internal.register(addEntryDocumentToReleaseActions);
346
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
255
347
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
256
348
  }
257
349
  if (strapi2.plugin("graphql")) {
@@ -260,129 +352,134 @@ const register = async ({ strapi: strapi2 }) => {
260
352
  graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
261
353
  }
262
354
  };
263
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
355
+ const updateActionsStatusAndUpdateReleaseStatus = async (contentType, entry) => {
356
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
357
+ where: {
358
+ actions: {
359
+ contentType,
360
+ entryDocumentId: entry.documentId,
361
+ locale: entry.locale
362
+ }
363
+ }
364
+ });
365
+ const entryStatus = await isEntryValid(contentType, entry, { strapi });
366
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
367
+ where: {
368
+ contentType,
369
+ entryDocumentId: entry.documentId,
370
+ locale: entry.locale
371
+ },
372
+ data: {
373
+ isEntryValid: entryStatus
374
+ }
375
+ });
376
+ for (const release2 of releases) {
377
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
378
+ }
379
+ };
380
+ const deleteActionsAndUpdateReleaseStatus = async (params) => {
381
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
382
+ where: {
383
+ actions: params
384
+ }
385
+ });
386
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
387
+ where: params
388
+ });
389
+ for (const release2 of releases) {
390
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
391
+ }
392
+ };
393
+ const deleteActionsOnDelete = async (ctx, next) => {
394
+ if (ctx.action !== "delete") {
395
+ return next();
396
+ }
397
+ if (!contentTypes$1.hasDraftAndPublish(ctx.contentType)) {
398
+ return next();
399
+ }
400
+ const contentType = ctx.contentType.uid;
401
+ const { documentId, locale } = ctx.params;
402
+ const result = await next();
403
+ if (!result) {
404
+ return result;
405
+ }
406
+ try {
407
+ deleteActionsAndUpdateReleaseStatus({
408
+ contentType,
409
+ entryDocumentId: documentId,
410
+ ...locale !== "*" && { locale }
411
+ });
412
+ } catch (error) {
413
+ strapi.log.error("Error while deleting release actions after delete", {
414
+ error
415
+ });
416
+ }
417
+ return result;
418
+ };
419
+ const updateActionsOnUpdate = async (ctx, next) => {
420
+ if (ctx.action !== "update") {
421
+ return next();
422
+ }
423
+ if (!contentTypes$1.hasDraftAndPublish(ctx.contentType)) {
424
+ return next();
425
+ }
426
+ const contentType = ctx.contentType.uid;
427
+ const result = await next();
428
+ if (!result) {
429
+ return result;
430
+ }
431
+ try {
432
+ updateActionsStatusAndUpdateReleaseStatus(contentType, result);
433
+ } catch (error) {
434
+ strapi.log.error("Error while updating release actions after update", {
435
+ error
436
+ });
437
+ }
438
+ return result;
439
+ };
440
+ const deleteReleasesActionsAndUpdateReleaseStatus = async (params) => {
441
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
442
+ where: {
443
+ actions: params
444
+ }
445
+ });
446
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
447
+ where: params
448
+ });
449
+ for (const release2 of releases) {
450
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
451
+ }
452
+ };
264
453
  const bootstrap = async ({ strapi: strapi2 }) => {
265
- if (features$1.isEnabled("cms-content-releases")) {
454
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
266
455
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
456
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
457
  );
269
458
  strapi2.db.lifecycles.subscribe({
270
459
  models: contentTypesWithDraftAndPublish,
271
- async afterDelete(event) {
272
- try {
273
- const { model, result } = event;
274
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
275
- const { id } = result;
276
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
277
- where: {
278
- actions: {
279
- target_type: model.uid,
280
- target_id: id
281
- }
282
- }
283
- });
284
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
285
- where: {
286
- target_type: model.uid,
287
- target_id: id
288
- }
289
- });
290
- for (const release2 of releases) {
291
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
292
- }
293
- }
294
- } catch (error) {
295
- strapi2.log.error("Error while deleting release actions after entry delete", { error });
296
- }
297
- },
298
460
  /**
299
- * deleteMany hook doesn't return the deleted entries ids
300
- * so we need to fetch them before deleting the entries to save the ids on our state
301
- */
302
- async beforeDeleteMany(event) {
303
- const { model, params } = event;
304
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
305
- const { where } = params;
306
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
307
- event.state.entriesToDelete = entriesToDelete;
308
- }
309
- },
310
- /**
311
- * We delete the release actions related to deleted entries
312
- * We make this only after deleteMany is succesfully executed to avoid errors
461
+ * deleteMany is still used outside documents service, for example when deleting a locale
313
462
  */
314
463
  async afterDeleteMany(event) {
315
464
  try {
316
- const { model, state } = event;
317
- const entriesToDelete = state.entriesToDelete;
318
- if (entriesToDelete) {
319
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
320
- where: {
321
- actions: {
322
- target_type: model.uid,
323
- target_id: {
324
- $in: entriesToDelete.map(
325
- (entry) => entry.id
326
- )
327
- }
328
- }
329
- }
330
- });
331
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
332
- where: {
333
- target_type: model.uid,
334
- target_id: {
335
- $in: entriesToDelete.map((entry) => entry.id)
336
- }
337
- }
465
+ const model = strapi2.getModel(event.model.uid);
466
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
467
+ const { where } = event.params;
468
+ deleteReleasesActionsAndUpdateReleaseStatus({
469
+ contentType: model.uid,
470
+ locale: where?.locale ?? null,
471
+ ...where?.documentId && { entryDocumentId: where.documentId }
338
472
  });
339
- for (const release2 of releases) {
340
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
341
- }
342
473
  }
343
474
  } catch (error) {
344
475
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
476
  error
346
477
  });
347
478
  }
348
- },
349
- async afterUpdate(event) {
350
- try {
351
- const { model, result } = event;
352
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
353
- const isEntryValid = await getEntryValidStatus(
354
- model.uid,
355
- result,
356
- {
357
- strapi: strapi2
358
- }
359
- );
360
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
361
- where: {
362
- target_type: model.uid,
363
- target_id: result.id
364
- },
365
- data: {
366
- isEntryValid
367
- }
368
- });
369
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
370
- where: {
371
- actions: {
372
- target_type: model.uid,
373
- target_id: result.id
374
- }
375
- }
376
- });
377
- for (const release2 of releases) {
378
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
379
- }
380
- }
381
- } catch (error) {
382
- strapi2.log.error("Error while updating release actions after entry update", { error });
383
- }
384
479
  }
385
480
  });
481
+ strapi2.documents.use(deleteActionsOnDelete);
482
+ strapi2.documents.use(updateActionsOnUpdate);
386
483
  getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
387
484
  strapi2.log.error(
388
485
  "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
@@ -390,7 +487,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
390
487
  throw err;
391
488
  });
392
489
  Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
393
- strapi2.webhookStore.addAllowedEvent(key, value);
490
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
394
491
  });
395
492
  }
396
493
  };
@@ -474,15 +571,13 @@ const schema = {
474
571
  enum: ["publish", "unpublish"],
475
572
  required: true
476
573
  },
477
- entry: {
478
- type: "relation",
479
- relation: "morphToOne",
480
- configurable: false
481
- },
482
574
  contentType: {
483
575
  type: "string",
484
576
  required: true
485
577
  },
578
+ entryDocumentId: {
579
+ type: "string"
580
+ },
486
581
  locale: {
487
582
  type: "string"
488
583
  },
@@ -504,18 +599,6 @@ const contentTypes = {
504
599
  release: release$1,
505
600
  "release-action": releaseAction$1
506
601
  };
507
- const getGroupName = (queryValue) => {
508
- switch (queryValue) {
509
- case "contentType":
510
- return "contentType.displayName";
511
- case "action":
512
- return "type";
513
- case "locale":
514
- return _.getOr("No locale", "locale.name");
515
- default:
516
- return "contentType.displayName";
517
- }
518
- };
519
602
  const createReleaseService = ({ strapi: strapi2 }) => {
520
603
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
521
604
  strapi2.eventHub.emit(event, {
@@ -524,93 +607,32 @@ const createReleaseService = ({ strapi: strapi2 }) => {
524
607
  release: release2
525
608
  });
526
609
  };
527
- const publishSingleTypeAction = async (uid, actionType, entryId) => {
528
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
529
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
530
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
531
- const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
532
- try {
533
- if (actionType === "publish") {
534
- await entityManagerService.publish(entry, uid);
535
- } else {
536
- await entityManagerService.unpublish(entry, uid);
537
- }
538
- } catch (error) {
539
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
540
- ;
541
- else {
542
- throw error;
543
- }
544
- }
545
- };
546
- const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
547
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
548
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
549
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
550
- const entriesToPublish = await strapi2.entityService.findMany(uid, {
551
- filters: {
552
- id: {
553
- $in: entriesToPublishIds
554
- }
555
- },
556
- populate
557
- });
558
- const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
559
- filters: {
560
- id: {
561
- $in: entriestoUnpublishIds
562
- }
563
- },
564
- populate
565
- });
566
- if (entriesToPublish.length > 0) {
567
- await entityManagerService.publishMany(entriesToPublish, uid);
568
- }
569
- if (entriesToUnpublish.length > 0) {
570
- await entityManagerService.unpublishMany(entriesToUnpublish, uid);
571
- }
572
- };
573
610
  const getFormattedActions = async (releaseId) => {
574
611
  const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
575
612
  where: {
576
613
  release: {
577
614
  id: releaseId
578
615
  }
579
- },
580
- populate: {
581
- entry: {
582
- fields: ["id"]
583
- }
584
616
  }
585
617
  });
586
618
  if (actions.length === 0) {
587
619
  throw new errors.ValidationError("No entries to publish");
588
620
  }
589
- const collectionTypeActions = {};
590
- const singleTypeActions = [];
621
+ const formattedActions = {};
591
622
  for (const action of actions) {
592
623
  const contentTypeUid = action.contentType;
593
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
594
- if (!collectionTypeActions[contentTypeUid]) {
595
- collectionTypeActions[contentTypeUid] = {
596
- entriesToPublishIds: [],
597
- entriesToUnpublishIds: []
598
- };
599
- }
600
- if (action.type === "publish") {
601
- collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
602
- } else {
603
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
604
- }
605
- } else {
606
- singleTypeActions.push({
607
- uid: contentTypeUid,
608
- action: action.type,
609
- id: action.entry.id
610
- });
624
+ if (!formattedActions[contentTypeUid]) {
625
+ formattedActions[contentTypeUid] = {
626
+ publish: [],
627
+ unpublish: []
628
+ };
611
629
  }
630
+ formattedActions[contentTypeUid][action.type].push({
631
+ documentId: action.entryDocumentId,
632
+ locale: action.locale
633
+ });
612
634
  }
613
- return { collectionTypeActions, singleTypeActions };
635
+ return formattedActions;
614
636
  };
615
637
  return {
616
638
  async create(releaseData, { user }) {
@@ -625,7 +647,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
625
647
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
626
648
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
627
649
  ]);
628
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
650
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
629
651
  data: {
630
652
  ...releaseWithCreatorFields,
631
653
  status: "empty"
@@ -639,94 +661,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
639
661
  return release2;
640
662
  },
641
663
  async findOne(id, query = {}) {
642
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
643
- ...query
664
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
665
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
666
+ ...dbQuery,
667
+ where: { id }
644
668
  });
645
669
  return release2;
646
670
  },
647
671
  findPage(query) {
648
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
649
- ...query,
672
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
673
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
674
+ ...dbQuery,
650
675
  populate: {
651
676
  actions: {
652
- // @ts-expect-error Ignore missing properties
653
677
  count: true
654
678
  }
655
679
  }
656
680
  });
657
681
  },
658
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
659
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
660
- where: {
661
- actions: {
662
- target_type: contentTypeUid,
663
- target_id: entryId
664
- },
665
- releasedAt: {
666
- $null: true
667
- }
668
- },
669
- populate: {
670
- // Filter the action to get only the content type entry
671
- actions: {
672
- where: {
673
- target_type: contentTypeUid,
674
- target_id: entryId
675
- }
676
- }
677
- }
678
- });
679
- return releases.map((release2) => {
680
- if (release2.actions?.length) {
681
- const [actionForEntry] = release2.actions;
682
- delete release2.actions;
683
- return {
684
- ...release2,
685
- action: actionForEntry
686
- };
687
- }
688
- return release2;
689
- });
690
- },
691
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
692
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
693
- where: {
694
- releasedAt: {
695
- $null: true
696
- },
697
- actions: {
698
- target_type: contentTypeUid,
699
- target_id: entryId
700
- }
701
- }
702
- });
703
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
704
- where: {
705
- $or: [
706
- {
707
- id: {
708
- $notIn: releasesRelated.map((release2) => release2.id)
709
- }
710
- },
711
- {
712
- actions: null
713
- }
714
- ],
715
- releasedAt: {
716
- $null: true
717
- }
718
- }
719
- });
720
- return releases.map((release2) => {
721
- if (release2.actions?.length) {
722
- const [actionForEntry] = release2.actions;
723
- delete release2.actions;
724
- return {
725
- ...release2,
726
- action: actionForEntry
727
- };
728
- }
729
- return release2;
682
+ findMany(query) {
683
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
684
+ return strapi2.db.query(RELEASE_MODEL_UID).findMany({
685
+ ...dbQuery
730
686
  });
731
687
  },
732
688
  async update(id, releaseData, { user }) {
@@ -741,19 +697,15 @@ const createReleaseService = ({ strapi: strapi2 }) => {
741
697
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
742
698
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
743
699
  ]);
744
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
700
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
745
701
  if (!release2) {
746
702
  throw new errors.NotFoundError(`No release found for id ${id}`);
747
703
  }
748
704
  if (release2.releasedAt) {
749
705
  throw new errors.ValidationError("Release already published");
750
706
  }
751
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
752
- /*
753
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
754
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
755
- */
756
- // @ts-expect-error see above
707
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
708
+ where: { id },
757
709
  data: releaseWithCreatorFields
758
710
  });
759
711
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -766,130 +718,6 @@ const createReleaseService = ({ strapi: strapi2 }) => {
766
718
  strapi2.telemetry.send("didUpdateContentRelease");
767
719
  return updatedRelease;
768
720
  },
769
- async createAction(releaseId, action) {
770
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
771
- strapi: strapi2
772
- });
773
- await Promise.all([
774
- validateEntryContentType(action.entry.contentType),
775
- validateUniqueEntry(releaseId, action)
776
- ]);
777
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
778
- if (!release2) {
779
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
780
- }
781
- if (release2.releasedAt) {
782
- throw new errors.ValidationError("Release already published");
783
- }
784
- const { entry, type } = action;
785
- const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
786
- const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
787
- const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
788
- data: {
789
- type,
790
- contentType: entry.contentType,
791
- locale: entry.locale,
792
- isEntryValid,
793
- entry: {
794
- id: entry.id,
795
- __type: entry.contentType,
796
- __pivot: { field: "entry" }
797
- },
798
- release: releaseId
799
- },
800
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
801
- });
802
- this.updateReleaseStatus(releaseId);
803
- return releaseAction2;
804
- },
805
- async findActions(releaseId, query) {
806
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
807
- fields: ["id"]
808
- });
809
- if (!release2) {
810
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
811
- }
812
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
813
- ...query,
814
- populate: {
815
- entry: {
816
- populate: "*"
817
- }
818
- },
819
- filters: {
820
- release: releaseId
821
- }
822
- });
823
- },
824
- async countActions(query) {
825
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
826
- },
827
- async groupActions(actions, groupBy) {
828
- const contentTypeUids = actions.reduce((acc, action) => {
829
- if (!acc.includes(action.contentType)) {
830
- acc.push(action.contentType);
831
- }
832
- return acc;
833
- }, []);
834
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
835
- contentTypeUids
836
- );
837
- const allLocalesDictionary = await this.getLocalesDataForActions();
838
- const formattedData = actions.map((action) => {
839
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
840
- return {
841
- ...action,
842
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
843
- contentType: {
844
- displayName,
845
- mainFieldValue: action.entry[mainField],
846
- uid: action.contentType
847
- }
848
- };
849
- });
850
- const groupName = getGroupName(groupBy);
851
- return _.groupBy(groupName)(formattedData);
852
- },
853
- async getLocalesDataForActions() {
854
- if (!strapi2.plugin("i18n")) {
855
- return {};
856
- }
857
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
858
- return allLocales.reduce((acc, locale) => {
859
- acc[locale.code] = { name: locale.name, code: locale.code };
860
- return acc;
861
- }, {});
862
- },
863
- async getContentTypesDataForActions(contentTypesUids) {
864
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
865
- const contentTypesData = {};
866
- for (const contentTypeUid of contentTypesUids) {
867
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
868
- uid: contentTypeUid
869
- });
870
- contentTypesData[contentTypeUid] = {
871
- mainField: contentTypeConfig.settings.mainField,
872
- displayName: strapi2.getModel(contentTypeUid).info.displayName
873
- };
874
- }
875
- return contentTypesData;
876
- },
877
- getContentTypeModelsFromActions(actions) {
878
- const contentTypeUids = actions.reduce((acc, action) => {
879
- if (!acc.includes(action.contentType)) {
880
- acc.push(action.contentType);
881
- }
882
- return acc;
883
- }, []);
884
- const contentTypeModelsMap = contentTypeUids.reduce(
885
- (acc, contentTypeUid) => {
886
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
887
- return acc;
888
- },
889
- {}
890
- );
891
- return contentTypeModelsMap;
892
- },
893
721
  async getAllComponents() {
894
722
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
895
723
  const components = await contentManagerComponentsService.findAllComponents();
@@ -903,10 +731,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
903
731
  return componentsMap;
904
732
  },
905
733
  async delete(releaseId) {
906
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
734
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
735
+ where: { id: releaseId },
907
736
  populate: {
908
737
  actions: {
909
- fields: ["id"]
738
+ select: ["id"]
910
739
  }
911
740
  }
912
741
  });
@@ -924,7 +753,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
924
753
  }
925
754
  }
926
755
  });
927
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
756
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
757
+ where: {
758
+ id: releaseId
759
+ }
760
+ });
928
761
  });
929
762
  if (release2.scheduledAt) {
930
763
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -950,22 +783,19 @@ const createReleaseService = ({ strapi: strapi2 }) => {
950
783
  }
951
784
  try {
952
785
  strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
953
- const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
954
- releaseId
786
+ const formattedActions = await getFormattedActions(releaseId);
787
+ await strapi2.db.transaction(
788
+ async () => Promise.all(
789
+ Object.keys(formattedActions).map(async (contentTypeUid) => {
790
+ const contentType = contentTypeUid;
791
+ const { publish, unpublish } = formattedActions[contentType];
792
+ return Promise.all([
793
+ ...publish.map((params) => strapi2.documents(contentType).publish(params)),
794
+ ...unpublish.map((params) => strapi2.documents(contentType).unpublish(params))
795
+ ]);
796
+ })
797
+ )
955
798
  );
956
- await strapi2.db.transaction(async () => {
957
- for (const { uid, action, id } of singleTypeActions) {
958
- await publishSingleTypeAction(uid, action, id);
959
- }
960
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
961
- const uid = contentTypeUid;
962
- await publishCollectionTypeAction(
963
- uid,
964
- collectionTypeActions[uid].entriesToPublishIds,
965
- collectionTypeActions[uid].entriesToUnpublishIds
966
- );
967
- }
968
- });
969
799
  const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
970
800
  where: {
971
801
  id: releaseId
@@ -995,13 +825,226 @@ const createReleaseService = ({ strapi: strapi2 }) => {
995
825
  };
996
826
  }
997
827
  });
998
- if (error) {
999
- throw error;
1000
- }
1001
- return release2;
828
+ if (error instanceof Error) {
829
+ throw error;
830
+ }
831
+ return release2;
832
+ },
833
+ async updateReleaseStatus(releaseId) {
834
+ const releaseActionService = getService("release-action", { strapi: strapi2 });
835
+ const [totalActions, invalidActions] = await Promise.all([
836
+ releaseActionService.countActions({
837
+ filters: {
838
+ release: releaseId
839
+ }
840
+ }),
841
+ releaseActionService.countActions({
842
+ filters: {
843
+ release: releaseId,
844
+ isEntryValid: false
845
+ }
846
+ })
847
+ ]);
848
+ if (totalActions > 0) {
849
+ if (invalidActions > 0) {
850
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
851
+ where: {
852
+ id: releaseId
853
+ },
854
+ data: {
855
+ status: "blocked"
856
+ }
857
+ });
858
+ }
859
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
860
+ where: {
861
+ id: releaseId
862
+ },
863
+ data: {
864
+ status: "ready"
865
+ }
866
+ });
867
+ }
868
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
869
+ where: {
870
+ id: releaseId
871
+ },
872
+ data: {
873
+ status: "empty"
874
+ }
875
+ });
876
+ }
877
+ };
878
+ };
879
+ const getGroupName = (queryValue) => {
880
+ switch (queryValue) {
881
+ case "contentType":
882
+ return "contentType.displayName";
883
+ case "type":
884
+ return "type";
885
+ case "locale":
886
+ return _.getOr("No locale", "locale.name");
887
+ default:
888
+ return "contentType.displayName";
889
+ }
890
+ };
891
+ const createReleaseActionService = ({ strapi: strapi2 }) => {
892
+ const getLocalesDataForActions = async () => {
893
+ if (!strapi2.plugin("i18n")) {
894
+ return {};
895
+ }
896
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
897
+ return allLocales.reduce((acc, locale) => {
898
+ acc[locale.code] = { name: locale.name, code: locale.code };
899
+ return acc;
900
+ }, {});
901
+ };
902
+ const getContentTypesDataForActions = async (contentTypesUids) => {
903
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
904
+ const contentTypesData = {};
905
+ for (const contentTypeUid of contentTypesUids) {
906
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
907
+ uid: contentTypeUid
908
+ });
909
+ contentTypesData[contentTypeUid] = {
910
+ mainField: contentTypeConfig.settings.mainField,
911
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
912
+ };
913
+ }
914
+ return contentTypesData;
915
+ };
916
+ return {
917
+ async create(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
918
+ const { validateEntryData, validateUniqueEntry } = getService("release-validation", {
919
+ strapi: strapi2
920
+ });
921
+ await Promise.all([
922
+ validateEntryData(action.contentType, action.entryDocumentId),
923
+ validateUniqueEntry(releaseId, action)
924
+ ]);
925
+ const model = strapi2.contentType(action.contentType);
926
+ if (model.kind === "singleType") {
927
+ const document = await strapi2.db.query(model.uid).findOne({ select: ["documentId"] });
928
+ if (!document) {
929
+ throw new errors.NotFoundError(`No entry found for contentType ${action.contentType}`);
930
+ }
931
+ action.entryDocumentId = document.documentId;
932
+ }
933
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
934
+ if (!release2) {
935
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
936
+ }
937
+ if (release2.releasedAt) {
938
+ throw new errors.ValidationError("Release already published");
939
+ }
940
+ const actionStatus = action.type === "publish" ? await getDraftEntryValidStatus(
941
+ {
942
+ contentType: action.contentType,
943
+ documentId: action.entryDocumentId,
944
+ locale: action.locale
945
+ },
946
+ {
947
+ strapi: strapi2
948
+ }
949
+ ) : true;
950
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
951
+ data: {
952
+ ...action,
953
+ release: release2.id,
954
+ isEntryValid: actionStatus
955
+ },
956
+ populate: { release: { select: ["id"] } }
957
+ });
958
+ if (!disableUpdateReleaseStatus) {
959
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
960
+ }
961
+ return releaseAction2;
962
+ },
963
+ async findPage(releaseId, query) {
964
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
965
+ where: { id: releaseId },
966
+ select: ["id"]
967
+ });
968
+ if (!release2) {
969
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
970
+ }
971
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
972
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
973
+ ...dbQuery,
974
+ where: {
975
+ release: releaseId
976
+ }
977
+ });
978
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
979
+ const actionsWithEntry = await async.map(actions, async (action) => {
980
+ const populate = await populateBuilderService(action.contentType).populateDeep(Infinity).build();
981
+ const entry = await getEntry(
982
+ {
983
+ contentType: action.contentType,
984
+ documentId: action.entryDocumentId,
985
+ locale: action.locale,
986
+ populate,
987
+ status: action.type === "publish" ? "draft" : "published"
988
+ },
989
+ { strapi: strapi2 }
990
+ );
991
+ return {
992
+ ...action,
993
+ entry,
994
+ status: entry ? await getEntryStatus(action.contentType, entry) : null
995
+ };
996
+ });
997
+ return {
998
+ results: actionsWithEntry,
999
+ pagination
1000
+ };
1001
+ },
1002
+ async groupActions(actions, groupBy) {
1003
+ const contentTypeUids = actions.reduce((acc, action) => {
1004
+ if (!acc.includes(action.contentType)) {
1005
+ acc.push(action.contentType);
1006
+ }
1007
+ return acc;
1008
+ }, []);
1009
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1010
+ const allLocalesDictionary = await getLocalesDataForActions();
1011
+ const formattedData = actions.map((action) => {
1012
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
1013
+ return {
1014
+ ...action,
1015
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
1016
+ contentType: {
1017
+ displayName,
1018
+ mainFieldValue: action.entry[mainField],
1019
+ uid: action.contentType
1020
+ }
1021
+ };
1022
+ });
1023
+ const groupName = getGroupName(groupBy);
1024
+ return _.groupBy(groupName)(formattedData);
1002
1025
  },
1003
- async updateAction(actionId, releaseId, update) {
1004
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1026
+ getContentTypeModelsFromActions(actions) {
1027
+ const contentTypeUids = actions.reduce((acc, action) => {
1028
+ if (!acc.includes(action.contentType)) {
1029
+ acc.push(action.contentType);
1030
+ }
1031
+ return acc;
1032
+ }, []);
1033
+ const contentTypeModelsMap = contentTypeUids.reduce(
1034
+ (acc, contentTypeUid) => {
1035
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
1036
+ return acc;
1037
+ },
1038
+ {}
1039
+ );
1040
+ return contentTypeModelsMap;
1041
+ },
1042
+ async countActions(query) {
1043
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1044
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
1045
+ },
1046
+ async update(actionId, releaseId, update) {
1047
+ const action = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findOne({
1005
1048
  where: {
1006
1049
  id: actionId,
1007
1050
  release: {
@@ -1010,17 +1053,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1010
1053
  $null: true
1011
1054
  }
1012
1055
  }
1013
- },
1014
- data: update
1056
+ }
1015
1057
  });
1016
- if (!updatedAction) {
1058
+ if (!action) {
1017
1059
  throw new errors.NotFoundError(
1018
1060
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1019
1061
  );
1020
1062
  }
1063
+ const actionStatus = update.type === "publish" ? await getDraftEntryValidStatus(
1064
+ {
1065
+ contentType: action.contentType,
1066
+ documentId: action.entryDocumentId,
1067
+ locale: action.locale
1068
+ },
1069
+ {
1070
+ strapi: strapi2
1071
+ }
1072
+ ) : true;
1073
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1074
+ where: {
1075
+ id: actionId,
1076
+ release: {
1077
+ id: releaseId,
1078
+ releasedAt: {
1079
+ $null: true
1080
+ }
1081
+ }
1082
+ },
1083
+ data: {
1084
+ ...update,
1085
+ isEntryValid: actionStatus
1086
+ }
1087
+ });
1088
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1021
1089
  return updatedAction;
1022
1090
  },
1023
- async deleteAction(actionId, releaseId) {
1091
+ async delete(actionId, releaseId) {
1024
1092
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1025
1093
  where: {
1026
1094
  id: actionId,
@@ -1037,51 +1105,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1037
1105
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1038
1106
  );
1039
1107
  }
1040
- this.updateReleaseStatus(releaseId);
1108
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1041
1109
  return deletedAction;
1042
- },
1043
- async updateReleaseStatus(releaseId) {
1044
- const [totalActions, invalidActions] = await Promise.all([
1045
- this.countActions({
1046
- filters: {
1047
- release: releaseId
1048
- }
1049
- }),
1050
- this.countActions({
1051
- filters: {
1052
- release: releaseId,
1053
- isEntryValid: false
1054
- }
1055
- })
1056
- ]);
1057
- if (totalActions > 0) {
1058
- if (invalidActions > 0) {
1059
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1060
- where: {
1061
- id: releaseId
1062
- },
1063
- data: {
1064
- status: "blocked"
1065
- }
1066
- });
1067
- }
1068
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1069
- where: {
1070
- id: releaseId
1071
- },
1072
- data: {
1073
- status: "ready"
1074
- }
1075
- });
1076
- }
1077
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1078
- where: {
1079
- id: releaseId
1080
- },
1081
- data: {
1082
- status: "empty"
1083
- }
1084
- });
1085
1110
  }
1086
1111
  };
1087
1112
  };
@@ -1093,37 +1118,43 @@ class AlreadyOnReleaseError extends errors.ApplicationError {
1093
1118
  }
1094
1119
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1095
1120
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1096
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1097
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1121
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1122
+ where: {
1123
+ id: releaseId
1124
+ },
1125
+ populate: {
1126
+ actions: true
1127
+ }
1098
1128
  });
1099
1129
  if (!release2) {
1100
1130
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1101
1131
  }
1102
1132
  const isEntryInRelease = release2.actions.some(
1103
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1133
+ (action) => action.entryDocumentId === releaseActionArgs.entryDocumentId && action.contentType === releaseActionArgs.contentType && (releaseActionArgs.locale ? action.locale === releaseActionArgs.locale : true)
1104
1134
  );
1105
1135
  if (isEntryInRelease) {
1106
1136
  throw new AlreadyOnReleaseError(
1107
- `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1137
+ `Entry with documentId ${releaseActionArgs.entryDocumentId}${releaseActionArgs.locale ? `( ${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1108
1138
  );
1109
1139
  }
1110
1140
  },
1111
- validateEntryContentType(contentTypeUid) {
1141
+ validateEntryData(contentTypeUid, entryDocumentId) {
1112
1142
  const contentType = strapi2.contentType(contentTypeUid);
1113
1143
  if (!contentType) {
1114
1144
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1115
1145
  }
1116
- if (!contentType.options?.draftAndPublish) {
1146
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
1117
1147
  throw new errors.ValidationError(
1118
1148
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1119
1149
  );
1120
1150
  }
1151
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1152
+ throw new errors.ValidationError("Document id is required for collection type");
1153
+ }
1121
1154
  },
1122
1155
  async validatePendingReleasesLimit() {
1123
- const maximumPendingReleases = (
1124
- // @ts-expect-error - options is not typed into features
1125
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
1126
- );
1156
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1157
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1127
1158
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1128
1159
  filters: {
1129
1160
  releasedAt: {
@@ -1136,8 +1167,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1136
1167
  }
1137
1168
  },
1138
1169
  async validateUniqueNameForPendingRelease(name, id) {
1139
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1140
- filters: {
1170
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1171
+ where: {
1141
1172
  releasedAt: {
1142
1173
  $null: true
1143
1174
  },
@@ -1166,7 +1197,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1166
1197
  }
1167
1198
  const job = scheduleJob(scheduleDate, async () => {
1168
1199
  try {
1169
- await getService("release").publish(releaseId);
1200
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1170
1201
  } catch (error) {
1171
1202
  }
1172
1203
  this.cancel(releaseId);
@@ -1208,85 +1239,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1208
1239
  }
1209
1240
  };
1210
1241
  };
1242
+ const DEFAULT_SETTINGS = {
1243
+ defaultTimezone: null
1244
+ };
1245
+ const createSettingsService = ({ strapi: strapi2 }) => {
1246
+ const getStore = async () => strapi2.store({ type: "core", name: "content-releases" });
1247
+ return {
1248
+ async update({ settings: settings2 }) {
1249
+ const store = await getStore();
1250
+ store.set({ key: "settings", value: settings2 });
1251
+ return settings2;
1252
+ },
1253
+ async find() {
1254
+ const store = await getStore();
1255
+ const settings2 = await store.get({ key: "settings" });
1256
+ return {
1257
+ ...DEFAULT_SETTINGS,
1258
+ ...settings2 || {}
1259
+ };
1260
+ }
1261
+ };
1262
+ };
1211
1263
  const services = {
1212
1264
  release: createReleaseService,
1265
+ "release-action": createReleaseActionService,
1213
1266
  "release-validation": createReleaseValidationService,
1214
- scheduling: createSchedulingService
1267
+ scheduling: createSchedulingService,
1268
+ settings: createSettingsService
1215
1269
  };
1216
- const RELEASE_SCHEMA = yup.object().shape({
1217
- name: yup.string().trim().required(),
1218
- scheduledAt: yup.string().nullable(),
1219
- isScheduled: yup.boolean().optional(),
1220
- time: yup.string().when("isScheduled", {
1221
- is: true,
1222
- then: yup.string().trim().required(),
1223
- otherwise: yup.string().nullable()
1224
- }),
1225
- timezone: yup.string().when("isScheduled", {
1226
- is: true,
1227
- then: yup.string().required().nullable(),
1228
- otherwise: yup.string().nullable()
1229
- }),
1230
- date: yup.string().when("isScheduled", {
1231
- is: true,
1232
- then: yup.string().required().nullable(),
1233
- otherwise: yup.string().nullable()
1270
+ const RELEASE_SCHEMA = yup$1.object().shape({
1271
+ name: yup$1.string().trim().required(),
1272
+ scheduledAt: yup$1.string().nullable(),
1273
+ timezone: yup$1.string().when("scheduledAt", {
1274
+ is: (value) => value !== null && value !== void 0,
1275
+ then: yup$1.string().required(),
1276
+ otherwise: yup$1.string().nullable()
1234
1277
  })
1235
1278
  }).required().noUnknown();
1279
+ const FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA = yup$1.object().shape({
1280
+ contentType: yup$1.string().required(),
1281
+ entryDocumentId: yup$1.string().nullable(),
1282
+ hasEntryAttached: yup$1.string().nullable(),
1283
+ locale: yup$1.string().nullable()
1284
+ }).required().noUnknown();
1236
1285
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1286
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1287
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1288
+ );
1237
1289
  const releaseController = {
1238
- async findMany(ctx) {
1239
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1290
+ /**
1291
+ * Find releases based on documents attached or not to the release.
1292
+ * If `hasEntryAttached` is true, it will return all releases that have the entry attached.
1293
+ * If `hasEntryAttached` is false, it will return all releases that don't have the entry attached.
1294
+ */
1295
+ async findByDocumentAttached(ctx) {
1296
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1240
1297
  ability: ctx.state.userAbility,
1241
1298
  model: RELEASE_MODEL_UID
1242
1299
  });
1243
1300
  await permissionsManager.validateQuery(ctx.query);
1244
1301
  const releaseService = getService("release", { strapi });
1245
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1246
- if (isFindManyForContentTypeEntry) {
1247
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1248
- const contentTypeUid = query.contentTypeUid;
1249
- const entryId = query.entryId;
1250
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1251
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1252
- ctx.body = { data };
1253
- } else {
1254
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1255
- const { results, pagination } = await releaseService.findPage(query);
1256
- const data = results.map((release2) => {
1257
- const { actions, ...releaseData } = release2;
1258
- return {
1259
- ...releaseData,
1302
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1303
+ await validatefindByDocumentAttachedParams(query);
1304
+ const model = strapi.getModel(query.contentType);
1305
+ if (model.kind && model.kind === "singleType") {
1306
+ const document = await strapi.db.query(model.uid).findOne({ select: ["documentId"] });
1307
+ if (!document) {
1308
+ throw new errors.NotFoundError(`No entry found for contentType ${query.contentType}`);
1309
+ }
1310
+ query.entryDocumentId = document.documentId;
1311
+ }
1312
+ const { contentType, hasEntryAttached, entryDocumentId, locale } = query;
1313
+ const isEntryAttached = typeof hasEntryAttached === "string" ? Boolean(JSON.parse(hasEntryAttached)) : false;
1314
+ if (isEntryAttached) {
1315
+ const releases = await releaseService.findMany({
1316
+ where: {
1317
+ releasedAt: null,
1318
+ actions: {
1319
+ contentType,
1320
+ entryDocumentId: entryDocumentId ?? null,
1321
+ locale: locale ?? null
1322
+ }
1323
+ },
1324
+ populate: {
1260
1325
  actions: {
1261
- meta: {
1262
- count: actions.count
1326
+ fields: ["type"],
1327
+ filters: {
1328
+ contentType,
1329
+ entryDocumentId: entryDocumentId ?? null,
1330
+ locale: locale ?? null
1263
1331
  }
1264
1332
  }
1265
- };
1333
+ }
1334
+ });
1335
+ ctx.body = { data: releases };
1336
+ } else {
1337
+ const relatedReleases = await releaseService.findMany({
1338
+ where: {
1339
+ releasedAt: null,
1340
+ actions: {
1341
+ contentType,
1342
+ entryDocumentId: entryDocumentId ?? null,
1343
+ locale: locale ?? null
1344
+ }
1345
+ }
1266
1346
  });
1267
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1347
+ const releases = await releaseService.findMany({
1268
1348
  where: {
1349
+ $or: [
1350
+ {
1351
+ id: {
1352
+ $notIn: relatedReleases.map((release2) => release2.id)
1353
+ }
1354
+ },
1355
+ {
1356
+ actions: null
1357
+ }
1358
+ ],
1269
1359
  releasedAt: null
1270
1360
  }
1271
1361
  });
1272
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1362
+ ctx.body = { data: releases };
1273
1363
  }
1274
1364
  },
1365
+ async findPage(ctx) {
1366
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1367
+ ability: ctx.state.userAbility,
1368
+ model: RELEASE_MODEL_UID
1369
+ });
1370
+ await permissionsManager.validateQuery(ctx.query);
1371
+ const releaseService = getService("release", { strapi });
1372
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1373
+ const { results, pagination } = await releaseService.findPage(query);
1374
+ const data = results.map((release2) => {
1375
+ const { actions, ...releaseData } = release2;
1376
+ return {
1377
+ ...releaseData,
1378
+ actions: {
1379
+ meta: {
1380
+ count: actions.count
1381
+ }
1382
+ }
1383
+ };
1384
+ });
1385
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1386
+ where: {
1387
+ releasedAt: null
1388
+ }
1389
+ });
1390
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1391
+ },
1275
1392
  async findOne(ctx) {
1276
1393
  const id = ctx.params.id;
1277
1394
  const releaseService = getService("release", { strapi });
1395
+ const releaseActionService = getService("release-action", { strapi });
1278
1396
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1279
1397
  if (!release2) {
1280
1398
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
1281
1399
  }
1282
- const count = await releaseService.countActions({
1400
+ const count = await releaseActionService.countActions({
1283
1401
  filters: {
1284
1402
  release: id
1285
1403
  }
1286
1404
  });
1287
1405
  const sanitizedRelease = {
1288
1406
  ...release2,
1289
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1407
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1290
1408
  };
1291
1409
  const data = {
1292
1410
  ...sanitizedRelease,
@@ -1298,19 +1416,63 @@ const releaseController = {
1298
1416
  };
1299
1417
  ctx.body = { data };
1300
1418
  },
1419
+ async mapEntriesToReleases(ctx) {
1420
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1421
+ if (!contentTypeUid || !documentIds) {
1422
+ throw new errors.ValidationError("Missing required query parameters");
1423
+ }
1424
+ const releaseService = getService("release", { strapi });
1425
+ const releasesWithActions = await releaseService.findMany({
1426
+ where: {
1427
+ releasedAt: null,
1428
+ actions: {
1429
+ contentType: contentTypeUid,
1430
+ entryDocumentId: {
1431
+ $in: documentIds
1432
+ },
1433
+ locale
1434
+ }
1435
+ },
1436
+ populate: {
1437
+ actions: true
1438
+ }
1439
+ });
1440
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1441
+ (acc, release2) => {
1442
+ release2.actions.forEach((action) => {
1443
+ if (action.contentType !== contentTypeUid) {
1444
+ return;
1445
+ }
1446
+ if (locale && action.locale !== locale) {
1447
+ return;
1448
+ }
1449
+ if (!acc[action.entryDocumentId]) {
1450
+ acc[action.entryDocumentId] = [{ id: release2.id, name: release2.name }];
1451
+ } else {
1452
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1453
+ }
1454
+ });
1455
+ return acc;
1456
+ },
1457
+ {}
1458
+ );
1459
+ ctx.body = {
1460
+ data: mappedEntriesInReleases
1461
+ };
1462
+ },
1301
1463
  async create(ctx) {
1302
1464
  const user = ctx.state.user;
1303
1465
  const releaseArgs = ctx.request.body;
1304
1466
  await validateRelease(releaseArgs);
1305
1467
  const releaseService = getService("release", { strapi });
1306
1468
  const release2 = await releaseService.create(releaseArgs, { user });
1307
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1469
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1308
1470
  ability: ctx.state.userAbility,
1309
1471
  model: RELEASE_MODEL_UID
1310
1472
  });
1311
- ctx.body = {
1473
+ ctx.created({
1312
1474
  data: await permissionsManager.sanitizeOutput(release2)
1313
- };
1475
+ });
1314
1476
  },
1315
1477
  async update(ctx) {
1316
1478
  const user = ctx.state.user;
@@ -1319,7 +1481,7 @@ const releaseController = {
1319
1481
  await validateRelease(releaseArgs);
1320
1482
  const releaseService = getService("release", { strapi });
1321
1483
  const release2 = await releaseService.update(id, releaseArgs, { user });
1322
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1484
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1323
1485
  ability: ctx.state.userAbility,
1324
1486
  model: RELEASE_MODEL_UID
1325
1487
  });
@@ -1336,18 +1498,18 @@ const releaseController = {
1336
1498
  };
1337
1499
  },
1338
1500
  async publish(ctx) {
1339
- const user = ctx.state.user;
1340
1501
  const id = ctx.params.id;
1341
1502
  const releaseService = getService("release", { strapi });
1342
- const release2 = await releaseService.publish(id, { user });
1503
+ const releaseActionService = getService("release-action", { strapi });
1504
+ const release2 = await releaseService.publish(id);
1343
1505
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1344
- releaseService.countActions({
1506
+ releaseActionService.countActions({
1345
1507
  filters: {
1346
1508
  release: id,
1347
1509
  type: "publish"
1348
1510
  }
1349
1511
  }),
1350
- releaseService.countActions({
1512
+ releaseActionService.countActions({
1351
1513
  filters: {
1352
1514
  release: id,
1353
1515
  type: "unpublish"
@@ -1365,27 +1527,30 @@ const releaseController = {
1365
1527
  }
1366
1528
  };
1367
1529
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
1368
- entry: yup$1.object().shape({
1369
- id: yup$1.strapiID().required(),
1370
- contentType: yup$1.string().required()
1371
- }).required(),
1530
+ contentType: yup$1.string().required(),
1531
+ entryDocumentId: yup$1.strapiID(),
1532
+ locale: yup$1.string(),
1372
1533
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1373
1534
  });
1374
1535
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
1375
1536
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1376
1537
  });
1538
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1539
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1540
+ });
1377
1541
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
1378
1542
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1543
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1379
1544
  const releaseActionController = {
1380
1545
  async create(ctx) {
1381
1546
  const releaseId = ctx.params.releaseId;
1382
1547
  const releaseActionArgs = ctx.request.body;
1383
1548
  await validateReleaseAction(releaseActionArgs);
1384
- const releaseService = getService("release", { strapi });
1385
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1386
- ctx.body = {
1549
+ const releaseActionService = getService("release-action", { strapi });
1550
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1551
+ ctx.created({
1387
1552
  data: releaseAction2
1388
- };
1553
+ });
1389
1554
  },
1390
1555
  async createMany(ctx) {
1391
1556
  const releaseId = ctx.params.releaseId;
@@ -1393,12 +1558,15 @@ const releaseActionController = {
1393
1558
  await Promise.all(
1394
1559
  releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1395
1560
  );
1561
+ const releaseActionService = getService("release-action", { strapi });
1396
1562
  const releaseService = getService("release", { strapi });
1397
1563
  const releaseActions = await strapi.db.transaction(async () => {
1398
1564
  const releaseActions2 = await Promise.all(
1399
1565
  releaseActionsArgs.map(async (releaseActionArgs) => {
1400
1566
  try {
1401
- const action = await releaseService.createAction(releaseId, releaseActionArgs);
1567
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1568
+ disableUpdateReleaseStatus: true
1569
+ });
1402
1570
  return action;
1403
1571
  } catch (error) {
1404
1572
  if (error instanceof AlreadyOnReleaseError) {
@@ -1411,43 +1579,54 @@ const releaseActionController = {
1411
1579
  return releaseActions2;
1412
1580
  });
1413
1581
  const newReleaseActions = releaseActions.filter((action) => action !== null);
1414
- ctx.body = {
1582
+ if (newReleaseActions.length > 0) {
1583
+ releaseService.updateReleaseStatus(releaseId);
1584
+ }
1585
+ ctx.created({
1415
1586
  data: newReleaseActions,
1416
1587
  meta: {
1417
1588
  entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1418
1589
  totalEntries: releaseActions.length
1419
1590
  }
1420
- };
1591
+ });
1421
1592
  },
1422
1593
  async findMany(ctx) {
1423
1594
  const releaseId = ctx.params.releaseId;
1424
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1595
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1425
1596
  ability: ctx.state.userAbility,
1426
1597
  model: RELEASE_ACTION_MODEL_UID
1427
1598
  });
1599
+ await validateFindManyActionsParams(ctx.query);
1600
+ if (ctx.query.groupBy) {
1601
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1602
+ ctx.badRequest("Invalid groupBy parameter");
1603
+ }
1604
+ }
1605
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1606
+ delete ctx.query.groupBy;
1428
1607
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1429
- const releaseService = getService("release", { strapi });
1430
- const { results, pagination } = await releaseService.findActions(releaseId, {
1431
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1608
+ const releaseActionService = getService("release-action", { strapi });
1609
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1432
1610
  ...query
1433
1611
  });
1434
1612
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1435
1613
  if (acc[action.contentType]) {
1436
1614
  return acc;
1437
1615
  }
1438
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1616
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1439
1617
  ability: ctx.state.userAbility,
1440
1618
  model: action.contentType
1441
1619
  });
1442
1620
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1443
1621
  return acc;
1444
1622
  }, {});
1445
- const sanitizedResults = await mapAsync(results, async (action) => ({
1623
+ const sanitizedResults = await async.map(results, async (action) => ({
1446
1624
  ...action,
1447
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1625
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1448
1626
  }));
1449
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1450
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1627
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1628
+ const contentTypes2 = releaseActionService.getContentTypeModelsFromActions(results);
1629
+ const releaseService = getService("release", { strapi });
1451
1630
  const components = await releaseService.getAllComponents();
1452
1631
  ctx.body = {
1453
1632
  data: groupedData,
@@ -1463,8 +1642,8 @@ const releaseActionController = {
1463
1642
  const releaseId = ctx.params.releaseId;
1464
1643
  const releaseActionUpdateArgs = ctx.request.body;
1465
1644
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1466
- const releaseService = getService("release", { strapi });
1467
- const updatedAction = await releaseService.updateAction(
1645
+ const releaseActionService = getService("release-action", { strapi });
1646
+ const updatedAction = await releaseActionService.update(
1468
1647
  actionId,
1469
1648
  releaseId,
1470
1649
  releaseActionUpdateArgs
@@ -1476,17 +1655,71 @@ const releaseActionController = {
1476
1655
  async delete(ctx) {
1477
1656
  const actionId = ctx.params.actionId;
1478
1657
  const releaseId = ctx.params.releaseId;
1479
- const releaseService = getService("release", { strapi });
1480
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1658
+ const releaseActionService = getService("release-action", { strapi });
1659
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1481
1660
  ctx.body = {
1482
1661
  data: deletedReleaseAction
1483
1662
  };
1484
1663
  }
1485
1664
  };
1486
- const controllers = { release: releaseController, "release-action": releaseActionController };
1665
+ const SETTINGS_SCHEMA = yup.object().shape({
1666
+ defaultTimezone: yup.string().nullable().default(null)
1667
+ }).required().noUnknown();
1668
+ const validateSettings = validateYupSchema(SETTINGS_SCHEMA);
1669
+ const settingsController = {
1670
+ async find(ctx) {
1671
+ const settingsService = getService("settings", { strapi });
1672
+ const settings2 = await settingsService.find();
1673
+ ctx.body = { data: settings2 };
1674
+ },
1675
+ async update(ctx) {
1676
+ const settingsBody = ctx.request.body;
1677
+ const settings2 = await validateSettings(settingsBody);
1678
+ const settingsService = getService("settings", { strapi });
1679
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1680
+ ctx.body = { data: updatedSettings };
1681
+ }
1682
+ };
1683
+ const controllers = {
1684
+ release: releaseController,
1685
+ "release-action": releaseActionController,
1686
+ settings: settingsController
1687
+ };
1487
1688
  const release = {
1488
1689
  type: "admin",
1489
1690
  routes: [
1691
+ {
1692
+ method: "GET",
1693
+ path: "/mapEntriesToReleases",
1694
+ handler: "release.mapEntriesToReleases",
1695
+ config: {
1696
+ policies: [
1697
+ "admin::isAuthenticatedAdmin",
1698
+ {
1699
+ name: "admin::hasPermissions",
1700
+ config: {
1701
+ actions: ["plugin::content-releases.read"]
1702
+ }
1703
+ }
1704
+ ]
1705
+ }
1706
+ },
1707
+ {
1708
+ method: "GET",
1709
+ path: "/getByDocumentAttached",
1710
+ handler: "release.findByDocumentAttached",
1711
+ config: {
1712
+ policies: [
1713
+ "admin::isAuthenticatedAdmin",
1714
+ {
1715
+ name: "admin::hasPermissions",
1716
+ config: {
1717
+ actions: ["plugin::content-releases.read"]
1718
+ }
1719
+ }
1720
+ ]
1721
+ }
1722
+ },
1490
1723
  {
1491
1724
  method: "POST",
1492
1725
  path: "/",
@@ -1506,7 +1739,7 @@ const release = {
1506
1739
  {
1507
1740
  method: "GET",
1508
1741
  path: "/",
1509
- handler: "release.findMany",
1742
+ handler: "release.findPage",
1510
1743
  config: {
1511
1744
  policies: [
1512
1745
  "admin::isAuthenticatedAdmin",
@@ -1670,13 +1903,50 @@ const releaseAction = {
1670
1903
  }
1671
1904
  ]
1672
1905
  };
1906
+ const settings = {
1907
+ type: "admin",
1908
+ routes: [
1909
+ {
1910
+ method: "GET",
1911
+ path: "/settings",
1912
+ handler: "settings.find",
1913
+ config: {
1914
+ policies: [
1915
+ "admin::isAuthenticatedAdmin",
1916
+ {
1917
+ name: "admin::hasPermissions",
1918
+ config: {
1919
+ actions: ["plugin::content-releases.settings.read"]
1920
+ }
1921
+ }
1922
+ ]
1923
+ }
1924
+ },
1925
+ {
1926
+ method: "PUT",
1927
+ path: "/settings",
1928
+ handler: "settings.update",
1929
+ config: {
1930
+ policies: [
1931
+ "admin::isAuthenticatedAdmin",
1932
+ {
1933
+ name: "admin::hasPermissions",
1934
+ config: {
1935
+ actions: ["plugin::content-releases.settings.update"]
1936
+ }
1937
+ }
1938
+ ]
1939
+ }
1940
+ }
1941
+ ]
1942
+ };
1673
1943
  const routes = {
1944
+ settings,
1674
1945
  release,
1675
1946
  "release-action": releaseAction
1676
1947
  };
1677
- const { features } = require("@strapi/strapi/dist/utils/ee");
1678
1948
  const getPlugin = () => {
1679
- if (features.isEnabled("cms-content-releases")) {
1949
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1680
1950
  return {
1681
1951
  register,
1682
1952
  bootstrap,