@strapi/content-releases 0.0.0-experimental.c5354c231bf2abb4fe353d2ab7812bced4a1c6fa → 0.0.0-experimental.c592deb623aed3f74ef7fdacfad9757ed59d34f7

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-Db0aPjrG.mjs +1374 -0
  3. package/dist/_chunks/App-Db0aPjrG.mjs.map +1 -0
  4. package/dist/_chunks/App-xHZkNTlJ.js +1395 -0
  5. package/dist/_chunks/App-xHZkNTlJ.js.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-C3rlM6Kf.mjs +178 -0
  11. package/dist/_chunks/ReleasesSettingsPage-C3rlM6Kf.mjs.map +1 -0
  12. package/dist/_chunks/ReleasesSettingsPage-DxEYftwd.js +178 -0
  13. package/dist/_chunks/ReleasesSettingsPage-DxEYftwd.js.map +1 -0
  14. package/dist/_chunks/{en-3SGjiVyR.js → en-CmYoEnA7.js} +10 -2
  15. package/dist/_chunks/en-CmYoEnA7.js.map +1 -0
  16. package/dist/_chunks/{en-bpHsnU0n.mjs → en-D0yVZFqf.mjs} +10 -2
  17. package/dist/_chunks/en-D0yVZFqf.mjs.map +1 -0
  18. package/dist/_chunks/index-CulHY6KM.js +1377 -0
  19. package/dist/_chunks/index-CulHY6KM.js.map +1 -0
  20. package/dist/_chunks/index-Dj_bL2s7.mjs +1358 -0
  21. package/dist/_chunks/index-Dj_bL2s7.mjs.map +1 -0
  22. package/dist/_chunks/schemas-63pFihNF.mjs +44 -0
  23. package/dist/_chunks/schemas-63pFihNF.mjs.map +1 -0
  24. package/dist/_chunks/schemas-z5zp-_Gd.js +62 -0
  25. package/dist/_chunks/schemas-z5zp-_Gd.js.map +1 -0
  26. package/dist/admin/index.js +1 -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 +873 -617
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +874 -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 +32 -37
  130. package/dist/_chunks/App-OP70yd5M.js +0 -1338
  131. package/dist/_chunks/App-OP70yd5M.js.map +0 -1
  132. package/dist/_chunks/App-x6Tjj3HN.mjs +0 -1315
  133. package/dist/_chunks/App-x6Tjj3HN.mjs.map +0 -1
  134. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +0 -1
  135. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +0 -1
  136. package/dist/_chunks/en-3SGjiVyR.js.map +0 -1
  137. package/dist/_chunks/en-bpHsnU0n.mjs.map +0 -1
  138. package/dist/_chunks/index-1ejXLtzt.mjs +0 -1203
  139. package/dist/_chunks/index-1ejXLtzt.mjs.map +0 -1
  140. package/dist/_chunks/index-ydocdaZ0.js +0 -1224
  141. package/dist/_chunks/index-ydocdaZ0.js.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,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
  });
@@ -211,13 +261,16 @@ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: cont
211
261
  if (!oldContentTypes) {
212
262
  return;
213
263
  }
264
+ const i18nPlugin = strapi.plugin("i18n");
265
+ if (!i18nPlugin) {
266
+ return;
267
+ }
214
268
  for (const uid in contentTypes2) {
215
269
  if (!oldContentTypes[uid]) {
216
270
  continue;
217
271
  }
218
272
  const oldContentType = oldContentTypes[uid];
219
273
  const contentType = contentTypes2[uid];
220
- const i18nPlugin = strapi.plugin("i18n");
221
274
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
222
275
  if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
223
276
  await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
@@ -230,13 +283,16 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
230
283
  if (!oldContentTypes) {
231
284
  return;
232
285
  }
286
+ const i18nPlugin = strapi.plugin("i18n");
287
+ if (!i18nPlugin) {
288
+ return;
289
+ }
233
290
  for (const uid in contentTypes2) {
234
291
  if (!oldContentTypes[uid]) {
235
292
  continue;
236
293
  }
237
294
  const oldContentType = oldContentTypes[uid];
238
295
  const contentType = contentTypes2[uid];
239
- const i18nPlugin = strapi.plugin("i18n");
240
296
  const { isLocalizedContentType } = i18nPlugin.service("content-types");
241
297
  const { getDefaultLocale } = i18nPlugin.service("locales");
242
298
  if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
@@ -247,11 +303,39 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
247
303
  }
248
304
  }
249
305
  }
250
- 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
+ };
251
334
  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);
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);
255
339
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
256
340
  }
257
341
  if (strapi2.plugin("graphql")) {
@@ -260,129 +344,134 @@ const register = async ({ strapi: strapi2 }) => {
260
344
  graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
261
345
  }
262
346
  };
263
- 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
+ };
264
445
  const bootstrap = async ({ strapi: strapi2 }) => {
265
- if (features$1.isEnabled("cms-content-releases")) {
446
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
266
447
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
448
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
449
  );
269
450
  strapi2.db.lifecycles.subscribe({
270
451
  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
452
  /**
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
453
+ * deleteMany is still used outside documents service, for example when deleting a locale
313
454
  */
314
455
  async afterDeleteMany(event) {
315
456
  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
- }
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 }
338
464
  });
339
- for (const release2 of releases) {
340
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
341
- }
342
465
  }
343
466
  } catch (error) {
344
467
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
468
  error
346
469
  });
347
470
  }
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
471
  }
385
472
  });
473
+ strapi2.documents.use(deleteActionsOnDelete);
474
+ strapi2.documents.use(updateActionsOnUpdate);
386
475
  getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
387
476
  strapi2.log.error(
388
477
  "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
@@ -390,7 +479,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
390
479
  throw err;
391
480
  });
392
481
  Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
393
- strapi2.webhookStore.addAllowedEvent(key, value);
482
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
394
483
  });
395
484
  }
396
485
  };
@@ -474,15 +563,13 @@ const schema = {
474
563
  enum: ["publish", "unpublish"],
475
564
  required: true
476
565
  },
477
- entry: {
478
- type: "relation",
479
- relation: "morphToOne",
480
- configurable: false
481
- },
482
566
  contentType: {
483
567
  type: "string",
484
568
  required: true
485
569
  },
570
+ entryDocumentId: {
571
+ type: "string"
572
+ },
486
573
  locale: {
487
574
  type: "string"
488
575
  },
@@ -504,18 +591,6 @@ const contentTypes = {
504
591
  release: release$1,
505
592
  "release-action": releaseAction$1
506
593
  };
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
594
  const createReleaseService = ({ strapi: strapi2 }) => {
520
595
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
521
596
  strapi2.eventHub.emit(event, {
@@ -524,93 +599,32 @@ const createReleaseService = ({ strapi: strapi2 }) => {
524
599
  release: release2
525
600
  });
526
601
  };
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
602
  const getFormattedActions = async (releaseId) => {
574
603
  const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
575
604
  where: {
576
605
  release: {
577
606
  id: releaseId
578
607
  }
579
- },
580
- populate: {
581
- entry: {
582
- fields: ["id"]
583
- }
584
608
  }
585
609
  });
586
610
  if (actions.length === 0) {
587
611
  throw new errors.ValidationError("No entries to publish");
588
612
  }
589
- const collectionTypeActions = {};
590
- const singleTypeActions = [];
613
+ const formattedActions = {};
591
614
  for (const action of actions) {
592
615
  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
- });
616
+ if (!formattedActions[contentTypeUid]) {
617
+ formattedActions[contentTypeUid] = {
618
+ publish: [],
619
+ unpublish: []
620
+ };
611
621
  }
622
+ formattedActions[contentTypeUid][action.type].push({
623
+ documentId: action.entryDocumentId,
624
+ locale: action.locale
625
+ });
612
626
  }
613
- return { collectionTypeActions, singleTypeActions };
627
+ return formattedActions;
614
628
  };
615
629
  return {
616
630
  async create(releaseData, { user }) {
@@ -625,7 +639,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
625
639
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
626
640
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
627
641
  ]);
628
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
642
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
629
643
  data: {
630
644
  ...releaseWithCreatorFields,
631
645
  status: "empty"
@@ -639,94 +653,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
639
653
  return release2;
640
654
  },
641
655
  async findOne(id, query = {}) {
642
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
643
- ...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 }
644
660
  });
645
661
  return release2;
646
662
  },
647
663
  findPage(query) {
648
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
649
- ...query,
664
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
665
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
666
+ ...dbQuery,
650
667
  populate: {
651
668
  actions: {
652
- // @ts-expect-error Ignore missing properties
653
669
  count: true
654
670
  }
655
671
  }
656
672
  });
657
673
  },
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;
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
730
678
  });
731
679
  },
732
680
  async update(id, releaseData, { user }) {
@@ -741,19 +689,15 @@ const createReleaseService = ({ strapi: strapi2 }) => {
741
689
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
742
690
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
743
691
  ]);
744
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
692
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
745
693
  if (!release2) {
746
694
  throw new errors.NotFoundError(`No release found for id ${id}`);
747
695
  }
748
696
  if (release2.releasedAt) {
749
697
  throw new errors.ValidationError("Release already published");
750
698
  }
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
699
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
700
+ where: { id },
757
701
  data: releaseWithCreatorFields
758
702
  });
759
703
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -766,130 +710,6 @@ const createReleaseService = ({ strapi: strapi2 }) => {
766
710
  strapi2.telemetry.send("didUpdateContentRelease");
767
711
  return updatedRelease;
768
712
  },
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
713
  async getAllComponents() {
894
714
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
895
715
  const components = await contentManagerComponentsService.findAllComponents();
@@ -903,10 +723,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
903
723
  return componentsMap;
904
724
  },
905
725
  async delete(releaseId) {
906
- 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 },
907
728
  populate: {
908
729
  actions: {
909
- fields: ["id"]
730
+ select: ["id"]
910
731
  }
911
732
  }
912
733
  });
@@ -924,7 +745,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
924
745
  }
925
746
  }
926
747
  });
927
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
748
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
749
+ where: {
750
+ id: releaseId
751
+ }
752
+ });
928
753
  });
929
754
  if (release2.scheduledAt) {
930
755
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -950,22 +775,19 @@ const createReleaseService = ({ strapi: strapi2 }) => {
950
775
  }
951
776
  try {
952
777
  strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
953
- const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
954
- 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
+ )
955
790
  );
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
791
  const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
970
792
  where: {
971
793
  id: releaseId
@@ -995,13 +817,226 @@ const createReleaseService = ({ strapi: strapi2 }) => {
995
817
  };
996
818
  }
997
819
  });
998
- if (error) {
999
- throw error;
1000
- }
1001
- 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, { disableUpdateReleaseStatus = false } = {}) {
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 model = strapi2.contentType(action.contentType);
918
+ if (model.kind === "singleType") {
919
+ const document = await strapi2.db.query(model.uid).findOne({ select: ["documentId"] });
920
+ if (!document) {
921
+ throw new errors.NotFoundError(`No entry found for contentType ${action.contentType}`);
922
+ }
923
+ action.entryDocumentId = document.documentId;
924
+ }
925
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
926
+ if (!release2) {
927
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
928
+ }
929
+ if (release2.releasedAt) {
930
+ throw new errors.ValidationError("Release already published");
931
+ }
932
+ const actionStatus = action.type === "publish" ? await getDraftEntryValidStatus(
933
+ {
934
+ contentType: action.contentType,
935
+ documentId: action.entryDocumentId,
936
+ locale: action.locale
937
+ },
938
+ {
939
+ strapi: strapi2
940
+ }
941
+ ) : true;
942
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
943
+ data: {
944
+ ...action,
945
+ release: release2.id,
946
+ isEntryValid: actionStatus
947
+ },
948
+ populate: { release: { select: ["id"] } }
949
+ });
950
+ if (!disableUpdateReleaseStatus) {
951
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
952
+ }
953
+ return releaseAction2;
954
+ },
955
+ async findPage(releaseId, query) {
956
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
957
+ where: { id: releaseId },
958
+ select: ["id"]
959
+ });
960
+ if (!release2) {
961
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
962
+ }
963
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
964
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
965
+ ...dbQuery,
966
+ where: {
967
+ release: releaseId
968
+ }
969
+ });
970
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
971
+ const actionsWithEntry = await async.map(actions, async (action) => {
972
+ const populate = await populateBuilderService(action.contentType).populateDeep(Infinity).build();
973
+ const entry = await getEntry(
974
+ {
975
+ contentType: action.contentType,
976
+ documentId: action.entryDocumentId,
977
+ locale: action.locale,
978
+ populate,
979
+ status: action.type === "publish" ? "draft" : "published"
980
+ },
981
+ { strapi: strapi2 }
982
+ );
983
+ return {
984
+ ...action,
985
+ entry,
986
+ status: entry ? await getEntryStatus(action.contentType, entry) : null
987
+ };
988
+ });
989
+ return {
990
+ results: actionsWithEntry,
991
+ pagination
992
+ };
993
+ },
994
+ async groupActions(actions, groupBy) {
995
+ const contentTypeUids = actions.reduce((acc, action) => {
996
+ if (!acc.includes(action.contentType)) {
997
+ acc.push(action.contentType);
998
+ }
999
+ return acc;
1000
+ }, []);
1001
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1002
+ const allLocalesDictionary = await getLocalesDataForActions();
1003
+ const formattedData = actions.map((action) => {
1004
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
1005
+ return {
1006
+ ...action,
1007
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
1008
+ contentType: {
1009
+ displayName,
1010
+ mainFieldValue: action.entry[mainField],
1011
+ uid: action.contentType
1012
+ }
1013
+ };
1014
+ });
1015
+ const groupName = getGroupName(groupBy);
1016
+ return _.groupBy(groupName)(formattedData);
1002
1017
  },
1003
- async updateAction(actionId, releaseId, update) {
1004
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1018
+ getContentTypeModelsFromActions(actions) {
1019
+ const contentTypeUids = actions.reduce((acc, action) => {
1020
+ if (!acc.includes(action.contentType)) {
1021
+ acc.push(action.contentType);
1022
+ }
1023
+ return acc;
1024
+ }, []);
1025
+ const contentTypeModelsMap = contentTypeUids.reduce(
1026
+ (acc, contentTypeUid) => {
1027
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
1028
+ return acc;
1029
+ },
1030
+ {}
1031
+ );
1032
+ return contentTypeModelsMap;
1033
+ },
1034
+ async countActions(query) {
1035
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1036
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
1037
+ },
1038
+ async update(actionId, releaseId, update) {
1039
+ const action = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findOne({
1005
1040
  where: {
1006
1041
  id: actionId,
1007
1042
  release: {
@@ -1010,17 +1045,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1010
1045
  $null: true
1011
1046
  }
1012
1047
  }
1013
- },
1014
- data: update
1048
+ }
1015
1049
  });
1016
- if (!updatedAction) {
1050
+ if (!action) {
1017
1051
  throw new errors.NotFoundError(
1018
1052
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1019
1053
  );
1020
1054
  }
1055
+ const actionStatus = update.type === "publish" ? getDraftEntryValidStatus(
1056
+ {
1057
+ contentType: action.contentType,
1058
+ documentId: action.entryDocumentId,
1059
+ locale: action.locale
1060
+ },
1061
+ {
1062
+ strapi: strapi2
1063
+ }
1064
+ ) : true;
1065
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1066
+ where: {
1067
+ id: actionId,
1068
+ release: {
1069
+ id: releaseId,
1070
+ releasedAt: {
1071
+ $null: true
1072
+ }
1073
+ }
1074
+ },
1075
+ data: {
1076
+ ...update,
1077
+ isEntryValid: actionStatus
1078
+ }
1079
+ });
1080
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1021
1081
  return updatedAction;
1022
1082
  },
1023
- async deleteAction(actionId, releaseId) {
1083
+ async delete(actionId, releaseId) {
1024
1084
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1025
1085
  where: {
1026
1086
  id: actionId,
@@ -1037,51 +1097,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1037
1097
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1038
1098
  );
1039
1099
  }
1040
- this.updateReleaseStatus(releaseId);
1100
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1041
1101
  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
1102
  }
1086
1103
  };
1087
1104
  };
@@ -1093,37 +1110,43 @@ class AlreadyOnReleaseError extends errors.ApplicationError {
1093
1110
  }
1094
1111
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1095
1112
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1096
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1097
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1113
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1114
+ where: {
1115
+ id: releaseId
1116
+ },
1117
+ populate: {
1118
+ actions: true
1119
+ }
1098
1120
  });
1099
1121
  if (!release2) {
1100
1122
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1101
1123
  }
1102
1124
  const isEntryInRelease = release2.actions.some(
1103
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1125
+ (action) => action.entryDocumentId === releaseActionArgs.entryDocumentId && action.contentType === releaseActionArgs.contentType && (releaseActionArgs.locale ? action.locale === releaseActionArgs.locale : true)
1104
1126
  );
1105
1127
  if (isEntryInRelease) {
1106
1128
  throw new AlreadyOnReleaseError(
1107
- `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1129
+ `Entry with documentId ${releaseActionArgs.entryDocumentId}${releaseActionArgs.locale ? `( ${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1108
1130
  );
1109
1131
  }
1110
1132
  },
1111
- validateEntryContentType(contentTypeUid) {
1133
+ validateEntryData(contentTypeUid, entryDocumentId) {
1112
1134
  const contentType = strapi2.contentType(contentTypeUid);
1113
1135
  if (!contentType) {
1114
1136
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1115
1137
  }
1116
- if (!contentType.options?.draftAndPublish) {
1138
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
1117
1139
  throw new errors.ValidationError(
1118
1140
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1119
1141
  );
1120
1142
  }
1143
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1144
+ throw new errors.ValidationError("Document id is required for collection type");
1145
+ }
1121
1146
  },
1122
1147
  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
- );
1148
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1149
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1127
1150
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1128
1151
  filters: {
1129
1152
  releasedAt: {
@@ -1136,8 +1159,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1136
1159
  }
1137
1160
  },
1138
1161
  async validateUniqueNameForPendingRelease(name, id) {
1139
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1140
- filters: {
1162
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1163
+ where: {
1141
1164
  releasedAt: {
1142
1165
  $null: true
1143
1166
  },
@@ -1166,7 +1189,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1166
1189
  }
1167
1190
  const job = scheduleJob(scheduleDate, async () => {
1168
1191
  try {
1169
- await getService("release").publish(releaseId);
1192
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1170
1193
  } catch (error) {
1171
1194
  }
1172
1195
  this.cancel(releaseId);
@@ -1208,85 +1231,167 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1208
1231
  }
1209
1232
  };
1210
1233
  };
1234
+ const DEFAULT_SETTINGS = {
1235
+ defaultTimezone: null
1236
+ };
1237
+ const createSettingsService = ({ strapi: strapi2 }) => {
1238
+ const getStore = async () => strapi2.store({ type: "core", name: "content-releases" });
1239
+ return {
1240
+ async update({ settings: settings2 }) {
1241
+ const store = await getStore();
1242
+ store.set({ key: "settings", value: settings2 });
1243
+ return settings2;
1244
+ },
1245
+ async find() {
1246
+ const store = await getStore();
1247
+ const settings2 = await store.get({ key: "settings" });
1248
+ return {
1249
+ ...DEFAULT_SETTINGS,
1250
+ ...settings2 || {}
1251
+ };
1252
+ }
1253
+ };
1254
+ };
1211
1255
  const services = {
1212
1256
  release: createReleaseService,
1257
+ "release-action": createReleaseActionService,
1213
1258
  "release-validation": createReleaseValidationService,
1214
- scheduling: createSchedulingService
1259
+ scheduling: createSchedulingService,
1260
+ settings: createSettingsService
1215
1261
  };
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()
1262
+ const RELEASE_SCHEMA = yup$1.object().shape({
1263
+ name: yup$1.string().trim().required(),
1264
+ scheduledAt: yup$1.string().nullable(),
1265
+ timezone: yup$1.string().when("scheduledAt", {
1266
+ is: (value) => value !== null && value !== void 0,
1267
+ then: yup$1.string().required(),
1268
+ otherwise: yup$1.string().nullable()
1234
1269
  })
1235
1270
  }).required().noUnknown();
1271
+ const FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA = yup$1.object().shape({
1272
+ contentType: yup$1.string().required(),
1273
+ entryDocumentId: yup$1.string().nullable(),
1274
+ hasEntryAttached: yup$1.string().nullable(),
1275
+ locale: yup$1.string().nullable()
1276
+ }).required().noUnknown();
1236
1277
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1278
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1279
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1280
+ );
1237
1281
  const releaseController = {
1238
- async findMany(ctx) {
1239
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1282
+ /**
1283
+ * Find releases based on documents attached or not to the release.
1284
+ * If `hasEntryAttached` is true, it will return all releases that have the entry attached.
1285
+ * If `hasEntryAttached` is false, it will return all releases that don't have the entry attached.
1286
+ */
1287
+ async findByDocumentAttached(ctx) {
1288
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1240
1289
  ability: ctx.state.userAbility,
1241
1290
  model: RELEASE_MODEL_UID
1242
1291
  });
1243
1292
  await permissionsManager.validateQuery(ctx.query);
1244
1293
  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 };
1294
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1295
+ await validatefindByDocumentAttachedParams(query);
1296
+ const model = strapi.getModel(query.contentType);
1297
+ if (model.kind && model.kind === "singleType") {
1298
+ const document = await strapi.db.query(model.uid).findOne({ select: ["documentId"] });
1299
+ if (!document) {
1300
+ throw new errors.NotFoundError(`No entry found for contentType ${query.contentType}`);
1301
+ }
1302
+ query.entryDocumentId = document.documentId;
1303
+ }
1304
+ const { contentType, hasEntryAttached, entryDocumentId, locale } = query;
1305
+ const isEntryAttached = typeof hasEntryAttached === "string" ? Boolean(JSON.parse(hasEntryAttached)) : false;
1306
+ if (isEntryAttached) {
1307
+ const releases = await releaseService.findMany({
1308
+ where: {
1309
+ releasedAt: null,
1310
+ actions: {
1311
+ contentType,
1312
+ entryDocumentId: entryDocumentId ?? null,
1313
+ locale: locale ?? null
1314
+ }
1315
+ },
1316
+ populate: {
1317
+ actions: {
1318
+ fields: ["type"]
1319
+ }
1320
+ }
1321
+ });
1322
+ ctx.body = { data: releases };
1253
1323
  } 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,
1324
+ const relatedReleases = await releaseService.findMany({
1325
+ where: {
1326
+ releasedAt: null,
1260
1327
  actions: {
1261
- meta: {
1262
- count: actions.count
1263
- }
1328
+ contentType,
1329
+ entryDocumentId: entryDocumentId ?? null,
1330
+ locale: locale ?? null
1264
1331
  }
1265
- };
1332
+ }
1266
1333
  });
1267
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1334
+ const releases = await releaseService.findMany({
1268
1335
  where: {
1336
+ $or: [
1337
+ {
1338
+ id: {
1339
+ $notIn: relatedReleases.map((release2) => release2.id)
1340
+ }
1341
+ },
1342
+ {
1343
+ actions: null
1344
+ }
1345
+ ],
1269
1346
  releasedAt: null
1270
1347
  }
1271
1348
  });
1272
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1349
+ ctx.body = { data: releases };
1273
1350
  }
1274
1351
  },
1352
+ async findPage(ctx) {
1353
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1354
+ ability: ctx.state.userAbility,
1355
+ model: RELEASE_MODEL_UID
1356
+ });
1357
+ await permissionsManager.validateQuery(ctx.query);
1358
+ const releaseService = getService("release", { strapi });
1359
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1360
+ const { results, pagination } = await releaseService.findPage(query);
1361
+ const data = results.map((release2) => {
1362
+ const { actions, ...releaseData } = release2;
1363
+ return {
1364
+ ...releaseData,
1365
+ actions: {
1366
+ meta: {
1367
+ count: actions.count
1368
+ }
1369
+ }
1370
+ };
1371
+ });
1372
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1373
+ where: {
1374
+ releasedAt: null
1375
+ }
1376
+ });
1377
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1378
+ },
1275
1379
  async findOne(ctx) {
1276
1380
  const id = ctx.params.id;
1277
1381
  const releaseService = getService("release", { strapi });
1382
+ const releaseActionService = getService("release-action", { strapi });
1278
1383
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1279
1384
  if (!release2) {
1280
1385
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
1281
1386
  }
1282
- const count = await releaseService.countActions({
1387
+ const count = await releaseActionService.countActions({
1283
1388
  filters: {
1284
1389
  release: id
1285
1390
  }
1286
1391
  });
1287
1392
  const sanitizedRelease = {
1288
1393
  ...release2,
1289
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1394
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1290
1395
  };
1291
1396
  const data = {
1292
1397
  ...sanitizedRelease,
@@ -1298,19 +1403,63 @@ const releaseController = {
1298
1403
  };
1299
1404
  ctx.body = { data };
1300
1405
  },
1406
+ async mapEntriesToReleases(ctx) {
1407
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1408
+ if (!contentTypeUid || !documentIds) {
1409
+ throw new errors.ValidationError("Missing required query parameters");
1410
+ }
1411
+ const releaseService = getService("release", { strapi });
1412
+ const releasesWithActions = await releaseService.findMany({
1413
+ where: {
1414
+ releasedAt: null,
1415
+ actions: {
1416
+ contentType: contentTypeUid,
1417
+ entryDocumentId: {
1418
+ $in: documentIds
1419
+ },
1420
+ locale
1421
+ }
1422
+ },
1423
+ populate: {
1424
+ actions: true
1425
+ }
1426
+ });
1427
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1428
+ (acc, release2) => {
1429
+ release2.actions.forEach((action) => {
1430
+ if (action.contentType !== contentTypeUid) {
1431
+ return;
1432
+ }
1433
+ if (locale && action.locale !== locale) {
1434
+ return;
1435
+ }
1436
+ if (!acc[action.entryDocumentId]) {
1437
+ acc[action.entryDocumentId] = [{ id: release2.id, name: release2.name }];
1438
+ } else {
1439
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1440
+ }
1441
+ });
1442
+ return acc;
1443
+ },
1444
+ {}
1445
+ );
1446
+ ctx.body = {
1447
+ data: mappedEntriesInReleases
1448
+ };
1449
+ },
1301
1450
  async create(ctx) {
1302
1451
  const user = ctx.state.user;
1303
1452
  const releaseArgs = ctx.request.body;
1304
1453
  await validateRelease(releaseArgs);
1305
1454
  const releaseService = getService("release", { strapi });
1306
1455
  const release2 = await releaseService.create(releaseArgs, { user });
1307
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1456
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1308
1457
  ability: ctx.state.userAbility,
1309
1458
  model: RELEASE_MODEL_UID
1310
1459
  });
1311
- ctx.body = {
1460
+ ctx.created({
1312
1461
  data: await permissionsManager.sanitizeOutput(release2)
1313
- };
1462
+ });
1314
1463
  },
1315
1464
  async update(ctx) {
1316
1465
  const user = ctx.state.user;
@@ -1319,7 +1468,7 @@ const releaseController = {
1319
1468
  await validateRelease(releaseArgs);
1320
1469
  const releaseService = getService("release", { strapi });
1321
1470
  const release2 = await releaseService.update(id, releaseArgs, { user });
1322
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1471
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1323
1472
  ability: ctx.state.userAbility,
1324
1473
  model: RELEASE_MODEL_UID
1325
1474
  });
@@ -1336,18 +1485,18 @@ const releaseController = {
1336
1485
  };
1337
1486
  },
1338
1487
  async publish(ctx) {
1339
- const user = ctx.state.user;
1340
1488
  const id = ctx.params.id;
1341
1489
  const releaseService = getService("release", { strapi });
1342
- const release2 = await releaseService.publish(id, { user });
1490
+ const releaseActionService = getService("release-action", { strapi });
1491
+ const release2 = await releaseService.publish(id);
1343
1492
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1344
- releaseService.countActions({
1493
+ releaseActionService.countActions({
1345
1494
  filters: {
1346
1495
  release: id,
1347
1496
  type: "publish"
1348
1497
  }
1349
1498
  }),
1350
- releaseService.countActions({
1499
+ releaseActionService.countActions({
1351
1500
  filters: {
1352
1501
  release: id,
1353
1502
  type: "unpublish"
@@ -1365,27 +1514,30 @@ const releaseController = {
1365
1514
  }
1366
1515
  };
1367
1516
  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(),
1517
+ contentType: yup$1.string().required(),
1518
+ entryDocumentId: yup$1.strapiID(),
1519
+ locale: yup$1.string(),
1372
1520
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1373
1521
  });
1374
1522
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
1375
1523
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1376
1524
  });
1525
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1526
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1527
+ });
1377
1528
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
1378
1529
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1530
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1379
1531
  const releaseActionController = {
1380
1532
  async create(ctx) {
1381
1533
  const releaseId = ctx.params.releaseId;
1382
1534
  const releaseActionArgs = ctx.request.body;
1383
1535
  await validateReleaseAction(releaseActionArgs);
1384
- const releaseService = getService("release", { strapi });
1385
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1386
- ctx.body = {
1536
+ const releaseActionService = getService("release-action", { strapi });
1537
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1538
+ ctx.created({
1387
1539
  data: releaseAction2
1388
- };
1540
+ });
1389
1541
  },
1390
1542
  async createMany(ctx) {
1391
1543
  const releaseId = ctx.params.releaseId;
@@ -1393,12 +1545,15 @@ const releaseActionController = {
1393
1545
  await Promise.all(
1394
1546
  releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1395
1547
  );
1548
+ const releaseActionService = getService("release-action", { strapi });
1396
1549
  const releaseService = getService("release", { strapi });
1397
1550
  const releaseActions = await strapi.db.transaction(async () => {
1398
1551
  const releaseActions2 = await Promise.all(
1399
1552
  releaseActionsArgs.map(async (releaseActionArgs) => {
1400
1553
  try {
1401
- const action = await releaseService.createAction(releaseId, releaseActionArgs);
1554
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1555
+ disableUpdateReleaseStatus: true
1556
+ });
1402
1557
  return action;
1403
1558
  } catch (error) {
1404
1559
  if (error instanceof AlreadyOnReleaseError) {
@@ -1411,43 +1566,54 @@ const releaseActionController = {
1411
1566
  return releaseActions2;
1412
1567
  });
1413
1568
  const newReleaseActions = releaseActions.filter((action) => action !== null);
1414
- ctx.body = {
1569
+ if (newReleaseActions.length > 0) {
1570
+ releaseService.updateReleaseStatus(releaseId);
1571
+ }
1572
+ ctx.created({
1415
1573
  data: newReleaseActions,
1416
1574
  meta: {
1417
1575
  entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1418
1576
  totalEntries: releaseActions.length
1419
1577
  }
1420
- };
1578
+ });
1421
1579
  },
1422
1580
  async findMany(ctx) {
1423
1581
  const releaseId = ctx.params.releaseId;
1424
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1582
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1425
1583
  ability: ctx.state.userAbility,
1426
1584
  model: RELEASE_ACTION_MODEL_UID
1427
1585
  });
1586
+ await validateFindManyActionsParams(ctx.query);
1587
+ if (ctx.query.groupBy) {
1588
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1589
+ ctx.badRequest("Invalid groupBy parameter");
1590
+ }
1591
+ }
1592
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1593
+ delete ctx.query.groupBy;
1428
1594
  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,
1595
+ const releaseActionService = getService("release-action", { strapi });
1596
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1432
1597
  ...query
1433
1598
  });
1434
1599
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1435
1600
  if (acc[action.contentType]) {
1436
1601
  return acc;
1437
1602
  }
1438
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1603
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1439
1604
  ability: ctx.state.userAbility,
1440
1605
  model: action.contentType
1441
1606
  });
1442
1607
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1443
1608
  return acc;
1444
1609
  }, {});
1445
- const sanitizedResults = await mapAsync(results, async (action) => ({
1610
+ const sanitizedResults = await async.map(results, async (action) => ({
1446
1611
  ...action,
1447
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1612
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1448
1613
  }));
1449
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1450
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1614
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1615
+ const contentTypes2 = releaseActionService.getContentTypeModelsFromActions(results);
1616
+ const releaseService = getService("release", { strapi });
1451
1617
  const components = await releaseService.getAllComponents();
1452
1618
  ctx.body = {
1453
1619
  data: groupedData,
@@ -1463,8 +1629,8 @@ const releaseActionController = {
1463
1629
  const releaseId = ctx.params.releaseId;
1464
1630
  const releaseActionUpdateArgs = ctx.request.body;
1465
1631
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1466
- const releaseService = getService("release", { strapi });
1467
- const updatedAction = await releaseService.updateAction(
1632
+ const releaseActionService = getService("release-action", { strapi });
1633
+ const updatedAction = await releaseActionService.update(
1468
1634
  actionId,
1469
1635
  releaseId,
1470
1636
  releaseActionUpdateArgs
@@ -1476,17 +1642,71 @@ const releaseActionController = {
1476
1642
  async delete(ctx) {
1477
1643
  const actionId = ctx.params.actionId;
1478
1644
  const releaseId = ctx.params.releaseId;
1479
- const releaseService = getService("release", { strapi });
1480
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1645
+ const releaseActionService = getService("release-action", { strapi });
1646
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1481
1647
  ctx.body = {
1482
1648
  data: deletedReleaseAction
1483
1649
  };
1484
1650
  }
1485
1651
  };
1486
- const controllers = { release: releaseController, "release-action": releaseActionController };
1652
+ const SETTINGS_SCHEMA = yup.object().shape({
1653
+ defaultTimezone: yup.string().nullable().default(null)
1654
+ }).required().noUnknown();
1655
+ const validateSettings = validateYupSchema(SETTINGS_SCHEMA);
1656
+ const settingsController = {
1657
+ async find(ctx) {
1658
+ const settingsService = getService("settings", { strapi });
1659
+ const settings2 = await settingsService.find();
1660
+ ctx.body = { data: settings2 };
1661
+ },
1662
+ async update(ctx) {
1663
+ const settingsBody = ctx.request.body;
1664
+ const settings2 = await validateSettings(settingsBody);
1665
+ const settingsService = getService("settings", { strapi });
1666
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1667
+ ctx.body = { data: updatedSettings };
1668
+ }
1669
+ };
1670
+ const controllers = {
1671
+ release: releaseController,
1672
+ "release-action": releaseActionController,
1673
+ settings: settingsController
1674
+ };
1487
1675
  const release = {
1488
1676
  type: "admin",
1489
1677
  routes: [
1678
+ {
1679
+ method: "GET",
1680
+ path: "/mapEntriesToReleases",
1681
+ handler: "release.mapEntriesToReleases",
1682
+ config: {
1683
+ policies: [
1684
+ "admin::isAuthenticatedAdmin",
1685
+ {
1686
+ name: "admin::hasPermissions",
1687
+ config: {
1688
+ actions: ["plugin::content-releases.read"]
1689
+ }
1690
+ }
1691
+ ]
1692
+ }
1693
+ },
1694
+ {
1695
+ method: "GET",
1696
+ path: "/getByDocumentAttached",
1697
+ handler: "release.findByDocumentAttached",
1698
+ config: {
1699
+ policies: [
1700
+ "admin::isAuthenticatedAdmin",
1701
+ {
1702
+ name: "admin::hasPermissions",
1703
+ config: {
1704
+ actions: ["plugin::content-releases.read"]
1705
+ }
1706
+ }
1707
+ ]
1708
+ }
1709
+ },
1490
1710
  {
1491
1711
  method: "POST",
1492
1712
  path: "/",
@@ -1506,7 +1726,7 @@ const release = {
1506
1726
  {
1507
1727
  method: "GET",
1508
1728
  path: "/",
1509
- handler: "release.findMany",
1729
+ handler: "release.findPage",
1510
1730
  config: {
1511
1731
  policies: [
1512
1732
  "admin::isAuthenticatedAdmin",
@@ -1670,13 +1890,50 @@ const releaseAction = {
1670
1890
  }
1671
1891
  ]
1672
1892
  };
1893
+ const settings = {
1894
+ type: "admin",
1895
+ routes: [
1896
+ {
1897
+ method: "GET",
1898
+ path: "/settings",
1899
+ handler: "settings.find",
1900
+ config: {
1901
+ policies: [
1902
+ "admin::isAuthenticatedAdmin",
1903
+ {
1904
+ name: "admin::hasPermissions",
1905
+ config: {
1906
+ actions: ["plugin::content-releases.settings.read"]
1907
+ }
1908
+ }
1909
+ ]
1910
+ }
1911
+ },
1912
+ {
1913
+ method: "PUT",
1914
+ path: "/settings",
1915
+ handler: "settings.update",
1916
+ config: {
1917
+ policies: [
1918
+ "admin::isAuthenticatedAdmin",
1919
+ {
1920
+ name: "admin::hasPermissions",
1921
+ config: {
1922
+ actions: ["plugin::content-releases.settings.update"]
1923
+ }
1924
+ }
1925
+ ]
1926
+ }
1927
+ }
1928
+ ]
1929
+ };
1673
1930
  const routes = {
1931
+ settings,
1674
1932
  release,
1675
1933
  "release-action": releaseAction
1676
1934
  };
1677
- const { features } = require("@strapi/strapi/dist/utils/ee");
1678
1935
  const getPlugin = () => {
1679
- if (features.isEnabled("cms-content-releases")) {
1936
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1680
1937
  return {
1681
1938
  register,
1682
1939
  bootstrap,