@strapi/content-releases 0.0.0-next.e9b6852d1c05518ff6e37d599321f7aa7aa0683b → 0.0.0-next.ee56af7ae29770097422de95c0d5500908dce15c

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