@strapi/content-releases 0.0.0-next.d773bfffcd285e7a1e7bf331b6f1224b969dd46b → 0.0.0-next.dad3c50630ca4fd9eccdcbe549ee632fc572e23d

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/dist/_chunks/App-CiZCkScI.mjs +1558 -0
  2. package/dist/_chunks/App-CiZCkScI.mjs.map +1 -0
  3. package/dist/_chunks/App-SGjO5UPV.js +1578 -0
  4. package/dist/_chunks/App-SGjO5UPV.js.map +1 -0
  5. package/dist/_chunks/{PurchaseContentReleases-bpIYXOfu.js → PurchaseContentReleases--qQepXpP.js} +8 -7
  6. package/dist/_chunks/PurchaseContentReleases--qQepXpP.js.map +1 -0
  7. package/dist/_chunks/{PurchaseContentReleases-3tRbmbY3.mjs → PurchaseContentReleases-D-n-w-st.mjs} +9 -8
  8. package/dist/_chunks/PurchaseContentReleases-D-n-w-st.mjs.map +1 -0
  9. package/dist/_chunks/ReleasesSettingsPage-Cto_NLUd.js +178 -0
  10. package/dist/_chunks/ReleasesSettingsPage-Cto_NLUd.js.map +1 -0
  11. package/dist/_chunks/ReleasesSettingsPage-DQT8N3A-.mjs +178 -0
  12. package/dist/_chunks/ReleasesSettingsPage-DQT8N3A-.mjs.map +1 -0
  13. package/dist/_chunks/{en-HrREghh3.js → en-BWPPsSH-.js} +18 -2
  14. package/dist/_chunks/en-BWPPsSH-.js.map +1 -0
  15. package/dist/_chunks/{en-ltT1TlKQ.mjs → en-D9Q4YW03.mjs} +18 -2
  16. package/dist/_chunks/en-D9Q4YW03.mjs.map +1 -0
  17. package/dist/_chunks/index-BjvFfTtA.mjs +1386 -0
  18. package/dist/_chunks/index-BjvFfTtA.mjs.map +1 -0
  19. package/dist/_chunks/index-CyU534vL.js +1404 -0
  20. package/dist/_chunks/index-CyU534vL.js.map +1 -0
  21. package/dist/_chunks/schemas-DBYv9gK8.js +61 -0
  22. package/dist/_chunks/schemas-DBYv9gK8.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/EntryValidationPopover.d.ts +13 -0
  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 +927 -662
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +927 -660
  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 +2111 -0
  90. package/dist/server/src/index.d.ts.map +1 -0
  91. package/dist/server/src/middlewares/documents.d.ts +6 -0
  92. package/dist/server/src/middlewares/documents.d.ts.map +1 -0
  93. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts +9 -0
  94. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts.map +1 -0
  95. package/dist/server/src/migrations/index.d.ts +13 -0
  96. package/dist/server/src/migrations/index.d.ts.map +1 -0
  97. package/dist/server/src/register.d.ts +5 -0
  98. package/dist/server/src/register.d.ts.map +1 -0
  99. package/dist/server/src/routes/index.d.ts +51 -0
  100. package/dist/server/src/routes/index.d.ts.map +1 -0
  101. package/dist/server/src/routes/release-action.d.ts +18 -0
  102. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  103. package/dist/server/src/routes/release.d.ts +18 -0
  104. package/dist/server/src/routes/release.d.ts.map +1 -0
  105. package/dist/server/src/routes/settings.d.ts +18 -0
  106. package/dist/server/src/routes/settings.d.ts.map +1 -0
  107. package/dist/server/src/services/index.d.ts +1824 -0
  108. package/dist/server/src/services/index.d.ts.map +1 -0
  109. package/dist/server/src/services/release-action.d.ts +34 -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 +137 -0
  122. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  123. package/dist/shared/contracts/releases.d.ts +184 -0
  124. package/dist/shared/contracts/releases.d.ts.map +1 -0
  125. package/dist/shared/contracts/settings.d.ts +39 -0
  126. package/dist/shared/contracts/settings.d.ts.map +1 -0
  127. package/dist/shared/types.d.ts +24 -0
  128. package/dist/shared/types.d.ts.map +1 -0
  129. package/package.json +34 -38
  130. package/dist/_chunks/App-dLXY5ei3.js +0 -1353
  131. package/dist/_chunks/App-dLXY5ei3.js.map +0 -1
  132. package/dist/_chunks/App-jrh58sXY.mjs +0 -1330
  133. package/dist/_chunks/App-jrh58sXY.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-HrREghh3.js.map +0 -1
  137. package/dist/_chunks/en-ltT1TlKQ.mjs.map +0 -1
  138. package/dist/_chunks/index-CVO0Rqdm.js +0 -1336
  139. package/dist/_chunks/index-CVO0Rqdm.js.map +0 -1
  140. package/dist/_chunks/index-PiOGBETy.mjs +0 -1315
  141. package/dist/_chunks/index-PiOGBETy.mjs.map +0 -1
  142. package/strapi-server.js +0 -3
@@ -1,8 +1,7 @@
1
- import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, async, setCreatorFields, errors, yup as yup$1, validateYupSchema } from "@strapi/utils";
2
2
  import isEqual from "lodash/isEqual";
3
3
  import { difference, keys } from "lodash";
4
4
  import _ from "lodash/fp";
5
- import EE from "@strapi/strapi/dist/utils/ee";
6
5
  import { scheduleJob } from "node-schedule";
7
6
  import * as yup from "yup";
8
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
@@ -49,21 +48,38 @@ const ACTIONS = [
49
48
  displayName: "Add an entry to a release",
50
49
  uid: "create-action",
51
50
  pluginName: "content-releases"
51
+ },
52
+ // Settings
53
+ {
54
+ uid: "settings.read",
55
+ section: "settings",
56
+ displayName: "Read",
57
+ category: "content releases",
58
+ subCategory: "options",
59
+ pluginName: "content-releases"
60
+ },
61
+ {
62
+ uid: "settings.update",
63
+ section: "settings",
64
+ displayName: "Edit",
65
+ category: "content releases",
66
+ subCategory: "options",
67
+ pluginName: "content-releases"
52
68
  }
53
69
  ];
54
70
  const ALLOWED_WEBHOOK_EVENTS = {
55
71
  RELEASES_PUBLISH: "releases.publish"
56
72
  };
57
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
73
+ const getService = (name, { strapi: strapi2 }) => {
58
74
  return strapi2.plugin("content-releases").service(name);
59
75
  };
60
- const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
76
+ const getDraftEntryValidStatus = async ({ contentType, documentId, locale }, { strapi: strapi2 }) => {
61
77
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
62
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
63
- const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
64
- return entry;
78
+ const populate = await populateBuilderService(contentType).populateDeep(Infinity).build();
79
+ const entry = await getEntry({ contentType, documentId, locale, populate }, { strapi: strapi2 });
80
+ return isEntryValid(contentType, entry, { strapi: strapi2 });
65
81
  };
66
- const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
82
+ const isEntryValid = async (contentTypeUid, entry, { strapi: strapi2 }) => {
67
83
  try {
68
84
  await strapi2.entityValidator.validateEntityCreation(
69
85
  strapi2.getModel(contentTypeUid),
@@ -72,11 +88,54 @@ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } =
72
88
  // @ts-expect-error - FIXME: entity here is unnecessary
73
89
  entry
74
90
  );
91
+ const workflowsService = strapi2.plugin("review-workflows").service("workflows");
92
+ const workflow = await workflowsService.getAssignedWorkflow(contentTypeUid, {
93
+ populate: "stageRequiredToPublish"
94
+ });
95
+ if (workflow?.stageRequiredToPublish) {
96
+ return entry.strapi_stage.id === workflow.stageRequiredToPublish.id;
97
+ }
75
98
  return true;
76
99
  } catch {
77
100
  return false;
78
101
  }
79
102
  };
103
+ const getEntry = async ({
104
+ contentType,
105
+ documentId,
106
+ locale,
107
+ populate,
108
+ status = "draft"
109
+ }, { strapi: strapi2 }) => {
110
+ if (documentId) {
111
+ const entry = await strapi2.documents(contentType).findOne({ documentId, locale, populate, status });
112
+ if (status === "published" && !entry) {
113
+ return strapi2.documents(contentType).findOne({ documentId, locale, populate, status: "draft" });
114
+ }
115
+ return entry;
116
+ }
117
+ return strapi2.documents(contentType).findFirst({ locale, populate, status });
118
+ };
119
+ const getEntryStatus = async (contentType, entry) => {
120
+ if (entry.publishedAt) {
121
+ return "published";
122
+ }
123
+ const publishedEntry = await strapi.documents(contentType).findOne({
124
+ documentId: entry.documentId,
125
+ locale: entry.locale,
126
+ status: "published",
127
+ fields: ["updatedAt"]
128
+ });
129
+ if (!publishedEntry) {
130
+ return "draft";
131
+ }
132
+ const entryUpdatedAt = new Date(entry.updatedAt).getTime();
133
+ const publishedEntryUpdatedAt = new Date(publishedEntry.updatedAt).getTime();
134
+ if (entryUpdatedAt > publishedEntryUpdatedAt) {
135
+ return "modified";
136
+ }
137
+ return "published";
138
+ };
80
139
  async function deleteActionsOnDisableDraftAndPublish({
81
140
  oldContentTypes,
82
141
  contentTypes: contentTypes2
@@ -98,7 +157,7 @@ async function deleteActionsOnDisableDraftAndPublish({
98
157
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
99
158
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
100
159
  if (deletedContentTypes.length) {
101
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
160
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
102
161
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
103
162
  });
104
163
  }
@@ -117,25 +176,27 @@ async function migrateIsValidAndStatusReleases() {
117
176
  }
118
177
  }
119
178
  });
120
- mapAsync(releasesWithoutStatus, async (release2) => {
179
+ async.map(releasesWithoutStatus, async (release2) => {
121
180
  const actions = release2.actions;
122
181
  const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
123
182
  for (const action of notValidatedActions) {
124
183
  if (action.entry) {
125
- const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
126
- strapi
184
+ const isEntryValid2 = getDraftEntryValidStatus(
185
+ {
186
+ contentType: action.contentType,
187
+ documentId: action.entryDocumentId,
188
+ locale: action.locale
189
+ },
190
+ { strapi }
191
+ );
192
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
193
+ where: {
194
+ id: action.id
195
+ },
196
+ data: {
197
+ isEntryValid: isEntryValid2
198
+ }
127
199
  });
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
200
  }
140
201
  }
141
202
  return getService("release", { strapi }).updateReleaseStatus(release2.id);
@@ -148,7 +209,7 @@ async function migrateIsValidAndStatusReleases() {
148
209
  }
149
210
  }
150
211
  });
151
- mapAsync(publishedReleases, async (release2) => {
212
+ async.map(publishedReleases, async (release2) => {
152
213
  return strapi.db.query(RELEASE_MODEL_UID).update({
153
214
  where: {
154
215
  id: release2.id
@@ -165,7 +226,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
165
226
  (uid) => oldContentTypes[uid]?.options?.draftAndPublish
166
227
  );
167
228
  const releasesAffected = /* @__PURE__ */ new Set();
168
- mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
229
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
169
230
  const oldContentType = oldContentTypes[contentTypeUID];
170
231
  const contentType = contentTypes2[contentTypeUID];
171
232
  if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
@@ -178,30 +239,30 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
178
239
  release: true
179
240
  }
180
241
  });
181
- await mapAsync(actions, async (action) => {
182
- if (action.entry && action.release) {
183
- const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
- strapi
242
+ await async.map(actions, async (action) => {
243
+ if (action.entry && action.release && action.type === "publish") {
244
+ const isEntryValid2 = await getDraftEntryValidStatus(
245
+ {
246
+ contentType: contentTypeUID,
247
+ documentId: action.entryDocumentId,
248
+ locale: action.locale
249
+ },
250
+ { strapi }
251
+ );
252
+ releasesAffected.add(action.release.id);
253
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
254
+ where: {
255
+ id: action.id
256
+ },
257
+ data: {
258
+ isEntryValid: isEntryValid2
259
+ }
185
260
  });
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
261
  }
201
262
  });
202
263
  }
203
264
  }).then(() => {
204
- mapAsync(releasesAffected, async (releaseId) => {
265
+ async.map(releasesAffected, async (releaseId) => {
205
266
  return getService("release", { strapi }).updateReleaseStatus(releaseId);
206
267
  });
207
268
  });
@@ -253,11 +314,43 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
253
314
  }
254
315
  }
255
316
  }
256
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
317
+ const addEntryDocumentToReleaseActions = {
318
+ name: "content-releases::5.0.0-add-entry-document-id-to-release-actions",
319
+ async up(trx, db) {
320
+ const hasTable = await trx.schema.hasTable("strapi_release_actions");
321
+ if (!hasTable) {
322
+ return;
323
+ }
324
+ const hasPolymorphicColumn = await trx.schema.hasColumn("strapi_release_actions", "target_id");
325
+ if (hasPolymorphicColumn) {
326
+ const hasEntryDocumentIdColumn = await trx.schema.hasColumn(
327
+ "strapi_release_actions",
328
+ "entry_document_id"
329
+ );
330
+ if (!hasEntryDocumentIdColumn) {
331
+ await trx.schema.alterTable("strapi_release_actions", (table) => {
332
+ table.string("entry_document_id");
333
+ });
334
+ }
335
+ const releaseActions = await trx.select("*").from("strapi_release_actions");
336
+ async.map(releaseActions, async (action) => {
337
+ const { target_type, target_id } = action;
338
+ const entry = await db.query(target_type).findOne({ where: { id: target_id } });
339
+ if (entry) {
340
+ await trx("strapi_release_actions").update({ entry_document_id: entry.documentId }).where("id", action.id);
341
+ }
342
+ });
343
+ }
344
+ },
345
+ async down() {
346
+ throw new Error("not implemented");
347
+ }
348
+ };
257
349
  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);
350
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
351
+ await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
352
+ strapi2.db.migrations.providers.internal.register(addEntryDocumentToReleaseActions);
353
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
261
354
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
262
355
  }
263
356
  if (strapi2.plugin("graphql")) {
@@ -266,129 +359,135 @@ const register = async ({ strapi: strapi2 }) => {
266
359
  graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
267
360
  }
268
361
  };
269
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
362
+ const updateActionsStatusAndUpdateReleaseStatus = async (contentType, entry) => {
363
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
364
+ where: {
365
+ releasedAt: null,
366
+ actions: {
367
+ contentType,
368
+ entryDocumentId: entry.documentId,
369
+ locale: entry.locale
370
+ }
371
+ }
372
+ });
373
+ const entryStatus = await isEntryValid(contentType, entry, { strapi });
374
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).updateMany({
375
+ where: {
376
+ contentType,
377
+ entryDocumentId: entry.documentId,
378
+ locale: entry.locale
379
+ },
380
+ data: {
381
+ isEntryValid: entryStatus
382
+ }
383
+ });
384
+ for (const release2 of releases) {
385
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
386
+ }
387
+ };
388
+ const deleteActionsAndUpdateReleaseStatus = async (params) => {
389
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
390
+ where: {
391
+ actions: params
392
+ }
393
+ });
394
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
395
+ where: params
396
+ });
397
+ for (const release2 of releases) {
398
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
399
+ }
400
+ };
401
+ const deleteActionsOnDelete = async (ctx, next) => {
402
+ if (ctx.action !== "delete") {
403
+ return next();
404
+ }
405
+ if (!contentTypes$1.hasDraftAndPublish(ctx.contentType)) {
406
+ return next();
407
+ }
408
+ const contentType = ctx.contentType.uid;
409
+ const { documentId, locale } = ctx.params;
410
+ const result = await next();
411
+ if (!result) {
412
+ return result;
413
+ }
414
+ try {
415
+ deleteActionsAndUpdateReleaseStatus({
416
+ contentType,
417
+ entryDocumentId: documentId,
418
+ ...locale !== "*" && { locale }
419
+ });
420
+ } catch (error) {
421
+ strapi.log.error("Error while deleting release actions after delete", {
422
+ error
423
+ });
424
+ }
425
+ return result;
426
+ };
427
+ const updateActionsOnUpdate = async (ctx, next) => {
428
+ if (ctx.action !== "update") {
429
+ return next();
430
+ }
431
+ if (!contentTypes$1.hasDraftAndPublish(ctx.contentType)) {
432
+ return next();
433
+ }
434
+ const contentType = ctx.contentType.uid;
435
+ const result = await next();
436
+ if (!result) {
437
+ return result;
438
+ }
439
+ try {
440
+ updateActionsStatusAndUpdateReleaseStatus(contentType, result);
441
+ } catch (error) {
442
+ strapi.log.error("Error while updating release actions after update", {
443
+ error
444
+ });
445
+ }
446
+ return result;
447
+ };
448
+ const deleteReleasesActionsAndUpdateReleaseStatus = async (params) => {
449
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
450
+ where: {
451
+ actions: params
452
+ }
453
+ });
454
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
455
+ where: params
456
+ });
457
+ for (const release2 of releases) {
458
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
459
+ }
460
+ };
270
461
  const bootstrap = async ({ strapi: strapi2 }) => {
271
- if (features$1.isEnabled("cms-content-releases")) {
462
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
272
463
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
273
464
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
274
465
  );
275
466
  strapi2.db.lifecycles.subscribe({
276
467
  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
468
  /**
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
- /**
317
- * We delete the release actions related to deleted entries
318
- * We make this only after deleteMany is succesfully executed to avoid errors
469
+ * deleteMany is still used outside documents service, for example when deleting a locale
319
470
  */
320
471
  async afterDeleteMany(event) {
321
472
  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
- }
473
+ const model = strapi2.getModel(event.model.uid);
474
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
475
+ const { where } = event.params;
476
+ deleteReleasesActionsAndUpdateReleaseStatus({
477
+ contentType: model.uid,
478
+ locale: where?.locale ?? null,
479
+ ...where?.documentId && { entryDocumentId: where.documentId }
344
480
  });
345
- for (const release2 of releases) {
346
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
347
- }
348
481
  }
349
482
  } catch (error) {
350
483
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
351
484
  error
352
485
  });
353
486
  }
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
487
  }
391
488
  });
489
+ strapi2.documents.use(deleteActionsOnDelete);
490
+ strapi2.documents.use(updateActionsOnUpdate);
392
491
  getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
393
492
  strapi2.log.error(
394
493
  "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
@@ -396,7 +495,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
396
495
  throw err;
397
496
  });
398
497
  Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
399
- strapi2.webhookStore.addAllowedEvent(key, value);
498
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
400
499
  });
401
500
  }
402
501
  };
@@ -480,15 +579,13 @@ const schema = {
480
579
  enum: ["publish", "unpublish"],
481
580
  required: true
482
581
  },
483
- entry: {
484
- type: "relation",
485
- relation: "morphToOne",
486
- configurable: false
487
- },
488
582
  contentType: {
489
583
  type: "string",
490
584
  required: true
491
585
  },
586
+ entryDocumentId: {
587
+ type: "string"
588
+ },
492
589
  locale: {
493
590
  type: "string"
494
591
  },
@@ -510,18 +607,6 @@ const contentTypes = {
510
607
  release: release$1,
511
608
  "release-action": releaseAction$1
512
609
  };
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
610
  const createReleaseService = ({ strapi: strapi2 }) => {
526
611
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
527
612
  strapi2.eventHub.emit(event, {
@@ -530,93 +615,32 @@ const createReleaseService = ({ strapi: strapi2 }) => {
530
615
  release: release2
531
616
  });
532
617
  };
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
618
  const getFormattedActions = async (releaseId) => {
580
619
  const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
581
620
  where: {
582
621
  release: {
583
622
  id: releaseId
584
623
  }
585
- },
586
- populate: {
587
- entry: {
588
- fields: ["id"]
589
- }
590
624
  }
591
625
  });
592
626
  if (actions.length === 0) {
593
627
  throw new errors.ValidationError("No entries to publish");
594
628
  }
595
- const collectionTypeActions = {};
596
- const singleTypeActions = [];
629
+ const formattedActions = {};
597
630
  for (const action of actions) {
598
631
  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
- });
632
+ if (!formattedActions[contentTypeUid]) {
633
+ formattedActions[contentTypeUid] = {
634
+ publish: [],
635
+ unpublish: []
636
+ };
617
637
  }
638
+ formattedActions[contentTypeUid][action.type].push({
639
+ documentId: action.entryDocumentId,
640
+ locale: action.locale
641
+ });
618
642
  }
619
- return { collectionTypeActions, singleTypeActions };
643
+ return formattedActions;
620
644
  };
621
645
  return {
622
646
  async create(releaseData, { user }) {
@@ -631,7 +655,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
631
655
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
632
656
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
633
657
  ]);
634
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
658
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
635
659
  data: {
636
660
  ...releaseWithCreatorFields,
637
661
  status: "empty"
@@ -645,107 +669,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
645
669
  return release2;
646
670
  },
647
671
  async findOne(id, query = {}) {
648
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
649
- ...query
672
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
673
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
674
+ ...dbQuery,
675
+ where: { id }
650
676
  });
651
677
  return release2;
652
678
  },
653
679
  findPage(query) {
654
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
655
- ...query,
680
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
681
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
682
+ ...dbQuery,
656
683
  populate: {
657
684
  actions: {
658
- // @ts-expect-error Ignore missing properties
659
685
  count: true
660
686
  }
661
687
  }
662
688
  });
663
689
  },
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;
690
+ findMany(query) {
691
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
692
+ return strapi2.db.query(RELEASE_MODEL_UID).findMany({
693
+ ...dbQuery
749
694
  });
750
695
  },
751
696
  async update(id, releaseData, { user }) {
@@ -760,19 +705,15 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
705
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
761
706
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
762
707
  ]);
763
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
708
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
764
709
  if (!release2) {
765
710
  throw new errors.NotFoundError(`No release found for id ${id}`);
766
711
  }
767
712
  if (release2.releasedAt) {
768
713
  throw new errors.ValidationError("Release already published");
769
714
  }
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
715
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
716
+ where: { id },
776
717
  data: releaseWithCreatorFields
777
718
  });
778
719
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -785,132 +726,6 @@ const createReleaseService = ({ strapi: strapi2 }) => {
785
726
  strapi2.telemetry.send("didUpdateContentRelease");
786
727
  return updatedRelease;
787
728
  },
788
- async createAction(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
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
- if (!disableUpdateReleaseStatus) {
822
- this.updateReleaseStatus(releaseId);
823
- }
824
- return releaseAction2;
825
- },
826
- async findActions(releaseId, query) {
827
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
828
- fields: ["id"]
829
- });
830
- if (!release2) {
831
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
832
- }
833
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
834
- ...query,
835
- populate: {
836
- entry: {
837
- populate: "*"
838
- }
839
- },
840
- filters: {
841
- release: releaseId
842
- }
843
- });
844
- },
845
- async countActions(query) {
846
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
847
- },
848
- async groupActions(actions, groupBy) {
849
- const contentTypeUids = actions.reduce((acc, action) => {
850
- if (!acc.includes(action.contentType)) {
851
- acc.push(action.contentType);
852
- }
853
- return acc;
854
- }, []);
855
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
856
- contentTypeUids
857
- );
858
- const allLocalesDictionary = await this.getLocalesDataForActions();
859
- const formattedData = actions.map((action) => {
860
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
861
- return {
862
- ...action,
863
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
864
- contentType: {
865
- displayName,
866
- mainFieldValue: action.entry[mainField],
867
- uid: action.contentType
868
- }
869
- };
870
- });
871
- const groupName = getGroupName(groupBy);
872
- return _.groupBy(groupName)(formattedData);
873
- },
874
- async getLocalesDataForActions() {
875
- if (!strapi2.plugin("i18n")) {
876
- return {};
877
- }
878
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
879
- return allLocales.reduce((acc, locale) => {
880
- acc[locale.code] = { name: locale.name, code: locale.code };
881
- return acc;
882
- }, {});
883
- },
884
- async getContentTypesDataForActions(contentTypesUids) {
885
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
886
- const contentTypesData = {};
887
- for (const contentTypeUid of contentTypesUids) {
888
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
889
- uid: contentTypeUid
890
- });
891
- contentTypesData[contentTypeUid] = {
892
- mainField: contentTypeConfig.settings.mainField,
893
- displayName: strapi2.getModel(contentTypeUid).info.displayName
894
- };
895
- }
896
- return contentTypesData;
897
- },
898
- getContentTypeModelsFromActions(actions) {
899
- const contentTypeUids = actions.reduce((acc, action) => {
900
- if (!acc.includes(action.contentType)) {
901
- acc.push(action.contentType);
902
- }
903
- return acc;
904
- }, []);
905
- const contentTypeModelsMap = contentTypeUids.reduce(
906
- (acc, contentTypeUid) => {
907
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
908
- return acc;
909
- },
910
- {}
911
- );
912
- return contentTypeModelsMap;
913
- },
914
729
  async getAllComponents() {
915
730
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
916
731
  const components = await contentManagerComponentsService.findAllComponents();
@@ -924,10 +739,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
924
739
  return componentsMap;
925
740
  },
926
741
  async delete(releaseId) {
927
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
742
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
743
+ where: { id: releaseId },
928
744
  populate: {
929
745
  actions: {
930
- fields: ["id"]
746
+ select: ["id"]
931
747
  }
932
748
  }
933
749
  });
@@ -945,7 +761,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
945
761
  }
946
762
  }
947
763
  });
948
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
764
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
765
+ where: {
766
+ id: releaseId
767
+ }
768
+ });
949
769
  });
950
770
  if (release2.scheduledAt) {
951
771
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -969,60 +789,280 @@ const createReleaseService = ({ strapi: strapi2 }) => {
969
789
  if (lockedRelease.status === "failed") {
970
790
  throw new errors.ValidationError("Release failed to publish");
971
791
  }
972
- try {
973
- strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
974
- const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
975
- releaseId
976
- );
977
- await strapi2.db.transaction(async () => {
978
- for (const { uid, action, id } of singleTypeActions) {
979
- await publishSingleTypeAction(uid, action, id);
980
- }
981
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
982
- const uid = contentTypeUid;
983
- await publishCollectionTypeAction(
984
- uid,
985
- collectionTypeActions[uid].entriesToPublishIds,
986
- collectionTypeActions[uid].entriesToUnpublishIds
987
- );
988
- }
989
- });
990
- const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
991
- where: {
992
- id: releaseId
993
- },
994
- data: {
995
- status: "done",
996
- releasedAt: /* @__PURE__ */ new Date()
997
- }
998
- });
999
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1000
- isPublished: true,
1001
- release: release22
1002
- });
1003
- strapi2.telemetry.send("didPublishContentRelease");
1004
- return { release: release22, error: null };
1005
- } catch (error2) {
1006
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1007
- isPublished: false,
1008
- error: error2
792
+ try {
793
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
794
+ const formattedActions = await getFormattedActions(releaseId);
795
+ await strapi2.db.transaction(
796
+ async () => Promise.all(
797
+ Object.keys(formattedActions).map(async (contentTypeUid) => {
798
+ const contentType = contentTypeUid;
799
+ const { publish, unpublish } = formattedActions[contentType];
800
+ return Promise.all([
801
+ ...publish.map((params) => strapi2.documents(contentType).publish(params)),
802
+ ...unpublish.map((params) => strapi2.documents(contentType).unpublish(params))
803
+ ]);
804
+ })
805
+ )
806
+ );
807
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
808
+ where: {
809
+ id: releaseId
810
+ },
811
+ data: {
812
+ status: "done",
813
+ releasedAt: /* @__PURE__ */ new Date()
814
+ }
815
+ });
816
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
817
+ isPublished: true,
818
+ release: release22
819
+ });
820
+ strapi2.telemetry.send("didPublishContentRelease");
821
+ return { release: release22, error: null };
822
+ } catch (error2) {
823
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
824
+ isPublished: false,
825
+ error: error2
826
+ });
827
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
828
+ status: "failed"
829
+ }).transacting(trx).execute();
830
+ return {
831
+ release: null,
832
+ error: error2
833
+ };
834
+ }
835
+ });
836
+ if (error instanceof Error) {
837
+ throw error;
838
+ }
839
+ return release2;
840
+ },
841
+ async updateReleaseStatus(releaseId) {
842
+ const releaseActionService = getService("release-action", { strapi: strapi2 });
843
+ const [totalActions, invalidActions] = await Promise.all([
844
+ releaseActionService.countActions({
845
+ filters: {
846
+ release: releaseId
847
+ }
848
+ }),
849
+ releaseActionService.countActions({
850
+ filters: {
851
+ release: releaseId,
852
+ isEntryValid: false
853
+ }
854
+ })
855
+ ]);
856
+ if (totalActions > 0) {
857
+ if (invalidActions > 0) {
858
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
859
+ where: {
860
+ id: releaseId
861
+ },
862
+ data: {
863
+ status: "blocked"
864
+ }
865
+ });
866
+ }
867
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
868
+ where: {
869
+ id: releaseId
870
+ },
871
+ data: {
872
+ status: "ready"
873
+ }
874
+ });
875
+ }
876
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
877
+ where: {
878
+ id: releaseId
879
+ },
880
+ data: {
881
+ status: "empty"
882
+ }
883
+ });
884
+ }
885
+ };
886
+ };
887
+ const getGroupName = (queryValue) => {
888
+ switch (queryValue) {
889
+ case "contentType":
890
+ return "contentType.displayName";
891
+ case "type":
892
+ return "type";
893
+ case "locale":
894
+ return _.getOr("No locale", "locale.name");
895
+ default:
896
+ return "contentType.displayName";
897
+ }
898
+ };
899
+ const createReleaseActionService = ({ strapi: strapi2 }) => {
900
+ const getLocalesDataForActions = async () => {
901
+ if (!strapi2.plugin("i18n")) {
902
+ return {};
903
+ }
904
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
905
+ return allLocales.reduce((acc, locale) => {
906
+ acc[locale.code] = { name: locale.name, code: locale.code };
907
+ return acc;
908
+ }, {});
909
+ };
910
+ const getContentTypesDataForActions = async (contentTypesUids) => {
911
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
912
+ const contentTypesData = {};
913
+ for (const contentTypeUid of contentTypesUids) {
914
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
915
+ uid: contentTypeUid
916
+ });
917
+ contentTypesData[contentTypeUid] = {
918
+ mainField: contentTypeConfig.settings.mainField,
919
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
920
+ };
921
+ }
922
+ return contentTypesData;
923
+ };
924
+ return {
925
+ async create(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
926
+ const { validateEntryData, validateUniqueEntry } = getService("release-validation", {
927
+ strapi: strapi2
928
+ });
929
+ await Promise.all([
930
+ validateEntryData(action.contentType, action.entryDocumentId),
931
+ validateUniqueEntry(releaseId, action)
932
+ ]);
933
+ const model = strapi2.contentType(action.contentType);
934
+ if (model.kind === "singleType") {
935
+ const document = await strapi2.db.query(model.uid).findOne({ select: ["documentId"] });
936
+ if (!document) {
937
+ throw new errors.NotFoundError(`No entry found for contentType ${action.contentType}`);
938
+ }
939
+ action.entryDocumentId = document.documentId;
940
+ }
941
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
942
+ if (!release2) {
943
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
944
+ }
945
+ if (release2.releasedAt) {
946
+ throw new errors.ValidationError("Release already published");
947
+ }
948
+ const actionStatus = action.type === "publish" ? await getDraftEntryValidStatus(
949
+ {
950
+ contentType: action.contentType,
951
+ documentId: action.entryDocumentId,
952
+ locale: action.locale
953
+ },
954
+ {
955
+ strapi: strapi2
956
+ }
957
+ ) : true;
958
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
959
+ data: {
960
+ ...action,
961
+ release: release2.id,
962
+ isEntryValid: actionStatus
963
+ },
964
+ populate: { release: { select: ["id"] } }
965
+ });
966
+ if (!disableUpdateReleaseStatus) {
967
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
968
+ }
969
+ return releaseAction2;
970
+ },
971
+ async findPage(releaseId, query) {
972
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
973
+ where: { id: releaseId },
974
+ select: ["id"]
975
+ });
976
+ if (!release2) {
977
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
978
+ }
979
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
980
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
981
+ ...dbQuery,
982
+ where: {
983
+ release: releaseId
984
+ }
985
+ });
986
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
987
+ const actionsWithEntry = await async.map(actions, async (action) => {
988
+ const populate = await populateBuilderService(action.contentType).populateDeep(Infinity).build();
989
+ const entry = await getEntry(
990
+ {
991
+ contentType: action.contentType,
992
+ documentId: action.entryDocumentId,
993
+ locale: action.locale,
994
+ populate,
995
+ status: action.type === "publish" ? "draft" : "published"
996
+ },
997
+ { strapi: strapi2 }
998
+ );
999
+ return {
1000
+ ...action,
1001
+ entry,
1002
+ status: entry ? await getEntryStatus(action.contentType, entry) : null
1003
+ };
1004
+ });
1005
+ return {
1006
+ results: actionsWithEntry,
1007
+ pagination
1008
+ };
1009
+ },
1010
+ async groupActions(actions, groupBy) {
1011
+ const contentTypeUids = actions.reduce((acc, action) => {
1012
+ if (!acc.includes(action.contentType)) {
1013
+ acc.push(action.contentType);
1014
+ }
1015
+ return acc;
1016
+ }, []);
1017
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1018
+ const allLocalesDictionary = await getLocalesDataForActions();
1019
+ const formattedData = actions.map((action) => {
1020
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
1021
+ return {
1022
+ ...action,
1023
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
1024
+ contentType: {
1025
+ displayName,
1026
+ mainFieldValue: action.entry[mainField],
1027
+ uid: action.contentType
1028
+ }
1029
+ };
1030
+ });
1031
+ const groupName = getGroupName(groupBy);
1032
+ return _.groupBy(groupName)(formattedData);
1033
+ },
1034
+ async getContentTypeModelsFromActions(actions) {
1035
+ const contentTypeUids = actions.reduce((acc, action) => {
1036
+ if (!acc.includes(action.contentType)) {
1037
+ acc.push(action.contentType);
1038
+ }
1039
+ return acc;
1040
+ }, []);
1041
+ const workflowsService = strapi2.plugin("review-workflows").service("workflows");
1042
+ const contentTypeModelsMap = await async.reduce(contentTypeUids)(
1043
+ async (accPromise, contentTypeUid) => {
1044
+ const acc = await accPromise;
1045
+ const contentTypeModel = strapi2.getModel(contentTypeUid);
1046
+ const workflow = await workflowsService.getAssignedWorkflow(contentTypeUid, {
1047
+ populate: "stageRequiredToPublish"
1009
1048
  });
1010
- await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1011
- status: "failed"
1012
- }).transacting(trx).execute();
1013
- return {
1014
- release: null,
1015
- error: error2
1049
+ acc[contentTypeUid] = {
1050
+ ...contentTypeModel,
1051
+ hasReviewWorkflow: !!workflow,
1052
+ stageRequiredToPublish: workflow?.stageRequiredToPublish
1016
1053
  };
1017
- }
1018
- });
1019
- if (error) {
1020
- throw error;
1021
- }
1022
- return release2;
1054
+ return acc;
1055
+ },
1056
+ {}
1057
+ );
1058
+ return contentTypeModelsMap;
1023
1059
  },
1024
- async updateAction(actionId, releaseId, update) {
1025
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1060
+ async countActions(query) {
1061
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1062
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
1063
+ },
1064
+ async update(actionId, releaseId, update) {
1065
+ const action = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findOne({
1026
1066
  where: {
1027
1067
  id: actionId,
1028
1068
  release: {
@@ -1031,17 +1071,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1031
1071
  $null: true
1032
1072
  }
1033
1073
  }
1034
- },
1035
- data: update
1074
+ }
1036
1075
  });
1037
- if (!updatedAction) {
1076
+ if (!action) {
1038
1077
  throw new errors.NotFoundError(
1039
1078
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1040
1079
  );
1041
1080
  }
1081
+ const actionStatus = update.type === "publish" ? await getDraftEntryValidStatus(
1082
+ {
1083
+ contentType: action.contentType,
1084
+ documentId: action.entryDocumentId,
1085
+ locale: action.locale
1086
+ },
1087
+ {
1088
+ strapi: strapi2
1089
+ }
1090
+ ) : true;
1091
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1092
+ where: {
1093
+ id: actionId,
1094
+ release: {
1095
+ id: releaseId,
1096
+ releasedAt: {
1097
+ $null: true
1098
+ }
1099
+ }
1100
+ },
1101
+ data: {
1102
+ ...update,
1103
+ isEntryValid: actionStatus
1104
+ }
1105
+ });
1106
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1042
1107
  return updatedAction;
1043
1108
  },
1044
- async deleteAction(actionId, releaseId) {
1109
+ async delete(actionId, releaseId) {
1045
1110
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1046
1111
  where: {
1047
1112
  id: actionId,
@@ -1058,51 +1123,56 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1058
1123
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1059
1124
  );
1060
1125
  }
1061
- this.updateReleaseStatus(releaseId);
1126
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1062
1127
  return deletedAction;
1063
1128
  },
1064
- async updateReleaseStatus(releaseId) {
1065
- const [totalActions, invalidActions] = await Promise.all([
1066
- this.countActions({
1067
- filters: {
1068
- release: releaseId
1069
- }
1070
- }),
1071
- this.countActions({
1072
- filters: {
1073
- release: releaseId,
1074
- isEntryValid: false
1075
- }
1076
- })
1077
- ]);
1078
- if (totalActions > 0) {
1079
- if (invalidActions > 0) {
1080
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1081
- where: {
1082
- id: releaseId
1083
- },
1084
- data: {
1085
- status: "blocked"
1129
+ async validateActionsByContentTypes(contentTypeUids) {
1130
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
1131
+ where: {
1132
+ contentType: {
1133
+ $in: contentTypeUids
1134
+ },
1135
+ // We only want to validate actions that are going to be published
1136
+ type: "publish",
1137
+ release: {
1138
+ releasedAt: {
1139
+ $null: true
1086
1140
  }
1087
- });
1088
- }
1089
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1141
+ }
1142
+ },
1143
+ populate: { release: true }
1144
+ });
1145
+ const releasesUpdated = [];
1146
+ await async.map(actions, async (action) => {
1147
+ const isValid = await getDraftEntryValidStatus(
1148
+ {
1149
+ contentType: action.contentType,
1150
+ documentId: action.entryDocumentId,
1151
+ locale: action.locale
1152
+ },
1153
+ { strapi: strapi2 }
1154
+ );
1155
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1090
1156
  where: {
1091
- id: releaseId
1157
+ id: action.id
1092
1158
  },
1093
1159
  data: {
1094
- status: "ready"
1160
+ isEntryValid: isValid
1095
1161
  }
1096
1162
  });
1097
- }
1098
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1099
- where: {
1100
- id: releaseId
1101
- },
1102
- data: {
1103
- status: "empty"
1163
+ if (!releasesUpdated.includes(action.release.id)) {
1164
+ releasesUpdated.push(action.release.id);
1104
1165
  }
1166
+ return {
1167
+ id: action.id,
1168
+ isEntryValid: isValid
1169
+ };
1105
1170
  });
1171
+ if (releasesUpdated.length > 0) {
1172
+ await async.map(releasesUpdated, async (releaseId) => {
1173
+ await getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1174
+ });
1175
+ }
1106
1176
  }
1107
1177
  };
1108
1178
  };
@@ -1114,37 +1184,43 @@ class AlreadyOnReleaseError extends errors.ApplicationError {
1114
1184
  }
1115
1185
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1116
1186
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1117
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1118
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1187
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1188
+ where: {
1189
+ id: releaseId
1190
+ },
1191
+ populate: {
1192
+ actions: true
1193
+ }
1119
1194
  });
1120
1195
  if (!release2) {
1121
1196
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1122
1197
  }
1123
1198
  const isEntryInRelease = release2.actions.some(
1124
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1199
+ (action) => action.entryDocumentId === releaseActionArgs.entryDocumentId && action.contentType === releaseActionArgs.contentType && (releaseActionArgs.locale ? action.locale === releaseActionArgs.locale : true)
1125
1200
  );
1126
1201
  if (isEntryInRelease) {
1127
1202
  throw new AlreadyOnReleaseError(
1128
- `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1203
+ `Entry with documentId ${releaseActionArgs.entryDocumentId}${releaseActionArgs.locale ? `( ${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1129
1204
  );
1130
1205
  }
1131
1206
  },
1132
- validateEntryContentType(contentTypeUid) {
1207
+ validateEntryData(contentTypeUid, entryDocumentId) {
1133
1208
  const contentType = strapi2.contentType(contentTypeUid);
1134
1209
  if (!contentType) {
1135
1210
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1136
1211
  }
1137
- if (!contentType.options?.draftAndPublish) {
1212
+ if (!contentTypes$1.hasDraftAndPublish(contentType)) {
1138
1213
  throw new errors.ValidationError(
1139
1214
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1140
1215
  );
1141
1216
  }
1217
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1218
+ throw new errors.ValidationError("Document id is required for collection type");
1219
+ }
1142
1220
  },
1143
1221
  async validatePendingReleasesLimit() {
1144
- const maximumPendingReleases = (
1145
- // @ts-expect-error - options is not typed into features
1146
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
1147
- );
1222
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1223
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1148
1224
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1149
1225
  filters: {
1150
1226
  releasedAt: {
@@ -1157,8 +1233,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1157
1233
  }
1158
1234
  },
1159
1235
  async validateUniqueNameForPendingRelease(name, id) {
1160
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1161
- filters: {
1236
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1237
+ where: {
1162
1238
  releasedAt: {
1163
1239
  $null: true
1164
1240
  },
@@ -1187,7 +1263,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1187
1263
  }
1188
1264
  const job = scheduleJob(scheduleDate, async () => {
1189
1265
  try {
1190
- await getService("release").publish(releaseId);
1266
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1191
1267
  } catch (error) {
1192
1268
  }
1193
1269
  this.cancel(releaseId);
@@ -1229,85 +1305,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1229
1305
  }
1230
1306
  };
1231
1307
  };
1308
+ const DEFAULT_SETTINGS = {
1309
+ defaultTimezone: null
1310
+ };
1311
+ const createSettingsService = ({ strapi: strapi2 }) => {
1312
+ const getStore = async () => strapi2.store({ type: "core", name: "content-releases" });
1313
+ return {
1314
+ async update({ settings: settings2 }) {
1315
+ const store = await getStore();
1316
+ store.set({ key: "settings", value: settings2 });
1317
+ return settings2;
1318
+ },
1319
+ async find() {
1320
+ const store = await getStore();
1321
+ const settings2 = await store.get({ key: "settings" });
1322
+ return {
1323
+ ...DEFAULT_SETTINGS,
1324
+ ...settings2 || {}
1325
+ };
1326
+ }
1327
+ };
1328
+ };
1232
1329
  const services = {
1233
1330
  release: createReleaseService,
1331
+ "release-action": createReleaseActionService,
1234
1332
  "release-validation": createReleaseValidationService,
1235
- scheduling: createSchedulingService
1333
+ scheduling: createSchedulingService,
1334
+ settings: createSettingsService
1236
1335
  };
1237
- const RELEASE_SCHEMA = yup.object().shape({
1238
- name: yup.string().trim().required(),
1239
- scheduledAt: yup.string().nullable(),
1240
- isScheduled: yup.boolean().optional(),
1241
- time: yup.string().when("isScheduled", {
1242
- is: true,
1243
- then: yup.string().trim().required(),
1244
- otherwise: yup.string().nullable()
1245
- }),
1246
- timezone: yup.string().when("isScheduled", {
1247
- is: true,
1248
- then: yup.string().required().nullable(),
1249
- otherwise: yup.string().nullable()
1250
- }),
1251
- date: yup.string().when("isScheduled", {
1252
- is: true,
1253
- then: yup.string().required().nullable(),
1254
- otherwise: yup.string().nullable()
1336
+ const RELEASE_SCHEMA = yup$1.object().shape({
1337
+ name: yup$1.string().trim().required(),
1338
+ scheduledAt: yup$1.string().nullable(),
1339
+ timezone: yup$1.string().when("scheduledAt", {
1340
+ is: (value) => value !== null && value !== void 0,
1341
+ then: yup$1.string().required(),
1342
+ otherwise: yup$1.string().nullable()
1255
1343
  })
1256
1344
  }).required().noUnknown();
1345
+ const FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA = yup$1.object().shape({
1346
+ contentType: yup$1.string().required(),
1347
+ entryDocumentId: yup$1.string().nullable(),
1348
+ hasEntryAttached: yup$1.string().nullable(),
1349
+ locale: yup$1.string().nullable()
1350
+ }).required().noUnknown();
1257
1351
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
1352
+ const validatefindByDocumentAttachedParams = validateYupSchema(
1353
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1354
+ );
1258
1355
  const releaseController = {
1259
- async findMany(ctx) {
1260
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1356
+ /**
1357
+ * Find releases based on documents attached or not to the release.
1358
+ * If `hasEntryAttached` is true, it will return all releases that have the entry attached.
1359
+ * If `hasEntryAttached` is false, it will return all releases that don't have the entry attached.
1360
+ */
1361
+ async findByDocumentAttached(ctx) {
1362
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1261
1363
  ability: ctx.state.userAbility,
1262
1364
  model: RELEASE_MODEL_UID
1263
1365
  });
1264
1366
  await permissionsManager.validateQuery(ctx.query);
1265
1367
  const releaseService = getService("release", { strapi });
1266
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1267
- if (isFindManyForContentTypeEntry) {
1268
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1269
- const contentTypeUid = query.contentTypeUid;
1270
- const entryId = query.entryId;
1271
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1272
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1273
- ctx.body = { data };
1274
- } else {
1275
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1276
- const { results, pagination } = await releaseService.findPage(query);
1277
- const data = results.map((release2) => {
1278
- const { actions, ...releaseData } = release2;
1279
- return {
1280
- ...releaseData,
1368
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1369
+ await validatefindByDocumentAttachedParams(query);
1370
+ const model = strapi.getModel(query.contentType);
1371
+ if (model.kind && model.kind === "singleType") {
1372
+ const document = await strapi.db.query(model.uid).findOne({ select: ["documentId"] });
1373
+ if (!document) {
1374
+ throw new errors.NotFoundError(`No entry found for contentType ${query.contentType}`);
1375
+ }
1376
+ query.entryDocumentId = document.documentId;
1377
+ }
1378
+ const { contentType, hasEntryAttached, entryDocumentId, locale } = query;
1379
+ const isEntryAttached = typeof hasEntryAttached === "string" ? Boolean(JSON.parse(hasEntryAttached)) : false;
1380
+ if (isEntryAttached) {
1381
+ const releases = await releaseService.findMany({
1382
+ where: {
1383
+ releasedAt: null,
1281
1384
  actions: {
1282
- meta: {
1283
- count: actions.count
1385
+ contentType,
1386
+ entryDocumentId: entryDocumentId ?? null,
1387
+ locale: locale ?? null
1388
+ }
1389
+ },
1390
+ populate: {
1391
+ actions: {
1392
+ fields: ["type"],
1393
+ filters: {
1394
+ contentType,
1395
+ entryDocumentId: entryDocumentId ?? null,
1396
+ locale: locale ?? null
1284
1397
  }
1285
1398
  }
1286
- };
1399
+ }
1400
+ });
1401
+ ctx.body = { data: releases };
1402
+ } else {
1403
+ const relatedReleases = await releaseService.findMany({
1404
+ where: {
1405
+ releasedAt: null,
1406
+ actions: {
1407
+ contentType,
1408
+ entryDocumentId: entryDocumentId ?? null,
1409
+ locale: locale ?? null
1410
+ }
1411
+ }
1287
1412
  });
1288
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1413
+ const releases = await releaseService.findMany({
1289
1414
  where: {
1415
+ $or: [
1416
+ {
1417
+ id: {
1418
+ $notIn: relatedReleases.map((release2) => release2.id)
1419
+ }
1420
+ },
1421
+ {
1422
+ actions: null
1423
+ }
1424
+ ],
1290
1425
  releasedAt: null
1291
1426
  }
1292
1427
  });
1293
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1428
+ ctx.body = { data: releases };
1294
1429
  }
1295
1430
  },
1431
+ async findPage(ctx) {
1432
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1433
+ ability: ctx.state.userAbility,
1434
+ model: RELEASE_MODEL_UID
1435
+ });
1436
+ await permissionsManager.validateQuery(ctx.query);
1437
+ const releaseService = getService("release", { strapi });
1438
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1439
+ const { results, pagination } = await releaseService.findPage(query);
1440
+ const data = results.map((release2) => {
1441
+ const { actions, ...releaseData } = release2;
1442
+ return {
1443
+ ...releaseData,
1444
+ actions: {
1445
+ meta: {
1446
+ count: actions.count
1447
+ }
1448
+ }
1449
+ };
1450
+ });
1451
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1452
+ where: {
1453
+ releasedAt: null
1454
+ }
1455
+ });
1456
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1457
+ },
1296
1458
  async findOne(ctx) {
1297
1459
  const id = ctx.params.id;
1298
1460
  const releaseService = getService("release", { strapi });
1461
+ const releaseActionService = getService("release-action", { strapi });
1299
1462
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1300
1463
  if (!release2) {
1301
1464
  throw new errors.NotFoundError(`Release not found for id: ${id}`);
1302
1465
  }
1303
- const count = await releaseService.countActions({
1466
+ const count = await releaseActionService.countActions({
1304
1467
  filters: {
1305
1468
  release: id
1306
1469
  }
1307
1470
  });
1308
1471
  const sanitizedRelease = {
1309
1472
  ...release2,
1310
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1473
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1311
1474
  };
1312
1475
  const data = {
1313
1476
  ...sanitizedRelease,
@@ -1320,22 +1483,39 @@ const releaseController = {
1320
1483
  ctx.body = { data };
1321
1484
  },
1322
1485
  async mapEntriesToReleases(ctx) {
1323
- const { contentTypeUid, entriesIds } = ctx.query;
1324
- if (!contentTypeUid || !entriesIds) {
1486
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1487
+ if (!contentTypeUid || !documentIds) {
1325
1488
  throw new errors.ValidationError("Missing required query parameters");
1326
1489
  }
1327
1490
  const releaseService = getService("release", { strapi });
1328
- const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1329
- contentTypeUid,
1330
- entriesIds
1331
- );
1491
+ const releasesWithActions = await releaseService.findMany({
1492
+ where: {
1493
+ releasedAt: null,
1494
+ actions: {
1495
+ contentType: contentTypeUid,
1496
+ entryDocumentId: {
1497
+ $in: documentIds
1498
+ },
1499
+ locale
1500
+ }
1501
+ },
1502
+ populate: {
1503
+ actions: true
1504
+ }
1505
+ });
1332
1506
  const mappedEntriesInReleases = releasesWithActions.reduce(
1333
1507
  (acc, release2) => {
1334
1508
  release2.actions.forEach((action) => {
1335
- if (!acc[action.entry.id]) {
1336
- acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1509
+ if (action.contentType !== contentTypeUid) {
1510
+ return;
1511
+ }
1512
+ if (locale && action.locale !== locale) {
1513
+ return;
1514
+ }
1515
+ if (!acc[action.entryDocumentId]) {
1516
+ acc[action.entryDocumentId] = [{ id: release2.id, name: release2.name }];
1337
1517
  } else {
1338
- acc[action.entry.id].push({ id: release2.id, name: release2.name });
1518
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1339
1519
  }
1340
1520
  });
1341
1521
  return acc;
@@ -1352,13 +1532,13 @@ const releaseController = {
1352
1532
  await validateRelease(releaseArgs);
1353
1533
  const releaseService = getService("release", { strapi });
1354
1534
  const release2 = await releaseService.create(releaseArgs, { user });
1355
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1535
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1356
1536
  ability: ctx.state.userAbility,
1357
1537
  model: RELEASE_MODEL_UID
1358
1538
  });
1359
- ctx.body = {
1539
+ ctx.created({
1360
1540
  data: await permissionsManager.sanitizeOutput(release2)
1361
- };
1541
+ });
1362
1542
  },
1363
1543
  async update(ctx) {
1364
1544
  const user = ctx.state.user;
@@ -1367,7 +1547,7 @@ const releaseController = {
1367
1547
  await validateRelease(releaseArgs);
1368
1548
  const releaseService = getService("release", { strapi });
1369
1549
  const release2 = await releaseService.update(id, releaseArgs, { user });
1370
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1550
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1371
1551
  ability: ctx.state.userAbility,
1372
1552
  model: RELEASE_MODEL_UID
1373
1553
  });
@@ -1384,18 +1564,18 @@ const releaseController = {
1384
1564
  };
1385
1565
  },
1386
1566
  async publish(ctx) {
1387
- const user = ctx.state.user;
1388
1567
  const id = ctx.params.id;
1389
1568
  const releaseService = getService("release", { strapi });
1390
- const release2 = await releaseService.publish(id, { user });
1569
+ const releaseActionService = getService("release-action", { strapi });
1570
+ const release2 = await releaseService.publish(id);
1391
1571
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1392
- releaseService.countActions({
1572
+ releaseActionService.countActions({
1393
1573
  filters: {
1394
1574
  release: id,
1395
1575
  type: "publish"
1396
1576
  }
1397
1577
  }),
1398
- releaseService.countActions({
1578
+ releaseActionService.countActions({
1399
1579
  filters: {
1400
1580
  release: id,
1401
1581
  type: "unpublish"
@@ -1413,27 +1593,30 @@ const releaseController = {
1413
1593
  }
1414
1594
  };
1415
1595
  const RELEASE_ACTION_SCHEMA = yup$1.object().shape({
1416
- entry: yup$1.object().shape({
1417
- id: yup$1.strapiID().required(),
1418
- contentType: yup$1.string().required()
1419
- }).required(),
1596
+ contentType: yup$1.string().required(),
1597
+ entryDocumentId: yup$1.strapiID(),
1598
+ locale: yup$1.string(),
1420
1599
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1421
1600
  });
1422
1601
  const RELEASE_ACTION_UPDATE_SCHEMA = yup$1.object().shape({
1423
1602
  type: yup$1.string().oneOf(["publish", "unpublish"]).required()
1424
1603
  });
1604
+ const FIND_MANY_ACTIONS_PARAMS = yup$1.object().shape({
1605
+ groupBy: yup$1.string().oneOf(["action", "contentType", "locale"])
1606
+ });
1425
1607
  const validateReleaseAction = validateYupSchema(RELEASE_ACTION_SCHEMA);
1426
1608
  const validateReleaseActionUpdateSchema = validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1609
+ const validateFindManyActionsParams = validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1427
1610
  const releaseActionController = {
1428
1611
  async create(ctx) {
1429
1612
  const releaseId = ctx.params.releaseId;
1430
1613
  const releaseActionArgs = ctx.request.body;
1431
1614
  await validateReleaseAction(releaseActionArgs);
1432
- const releaseService = getService("release", { strapi });
1433
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1434
- ctx.body = {
1615
+ const releaseActionService = getService("release-action", { strapi });
1616
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1617
+ ctx.created({
1435
1618
  data: releaseAction2
1436
- };
1619
+ });
1437
1620
  },
1438
1621
  async createMany(ctx) {
1439
1622
  const releaseId = ctx.params.releaseId;
@@ -1441,12 +1624,13 @@ const releaseActionController = {
1441
1624
  await Promise.all(
1442
1625
  releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1443
1626
  );
1627
+ const releaseActionService = getService("release-action", { strapi });
1444
1628
  const releaseService = getService("release", { strapi });
1445
1629
  const releaseActions = await strapi.db.transaction(async () => {
1446
1630
  const releaseActions2 = await Promise.all(
1447
1631
  releaseActionsArgs.map(async (releaseActionArgs) => {
1448
1632
  try {
1449
- const action = await releaseService.createAction(releaseId, releaseActionArgs, {
1633
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1450
1634
  disableUpdateReleaseStatus: true
1451
1635
  });
1452
1636
  return action;
@@ -1464,43 +1648,51 @@ const releaseActionController = {
1464
1648
  if (newReleaseActions.length > 0) {
1465
1649
  releaseService.updateReleaseStatus(releaseId);
1466
1650
  }
1467
- ctx.body = {
1651
+ ctx.created({
1468
1652
  data: newReleaseActions,
1469
1653
  meta: {
1470
1654
  entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1471
1655
  totalEntries: releaseActions.length
1472
1656
  }
1473
- };
1657
+ });
1474
1658
  },
1475
1659
  async findMany(ctx) {
1476
1660
  const releaseId = ctx.params.releaseId;
1477
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1661
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1478
1662
  ability: ctx.state.userAbility,
1479
1663
  model: RELEASE_ACTION_MODEL_UID
1480
1664
  });
1665
+ await validateFindManyActionsParams(ctx.query);
1666
+ if (ctx.query.groupBy) {
1667
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1668
+ ctx.badRequest("Invalid groupBy parameter");
1669
+ }
1670
+ }
1671
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1672
+ delete ctx.query.groupBy;
1481
1673
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1482
- const releaseService = getService("release", { strapi });
1483
- const { results, pagination } = await releaseService.findActions(releaseId, {
1484
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1674
+ const releaseActionService = getService("release-action", { strapi });
1675
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1485
1676
  ...query
1486
1677
  });
1487
1678
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1488
1679
  if (acc[action.contentType]) {
1489
1680
  return acc;
1490
1681
  }
1491
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1682
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1492
1683
  ability: ctx.state.userAbility,
1493
1684
  model: action.contentType
1494
1685
  });
1495
1686
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1496
1687
  return acc;
1497
1688
  }, {});
1498
- const sanitizedResults = await mapAsync(results, async (action) => ({
1689
+ const sanitizedResults = await async.map(results, async (action) => ({
1499
1690
  ...action,
1500
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1691
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1501
1692
  }));
1502
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1503
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1693
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1694
+ const contentTypes2 = await releaseActionService.getContentTypeModelsFromActions(results);
1695
+ const releaseService = getService("release", { strapi });
1504
1696
  const components = await releaseService.getAllComponents();
1505
1697
  ctx.body = {
1506
1698
  data: groupedData,
@@ -1516,8 +1708,8 @@ const releaseActionController = {
1516
1708
  const releaseId = ctx.params.releaseId;
1517
1709
  const releaseActionUpdateArgs = ctx.request.body;
1518
1710
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1519
- const releaseService = getService("release", { strapi });
1520
- const updatedAction = await releaseService.updateAction(
1711
+ const releaseActionService = getService("release-action", { strapi });
1712
+ const updatedAction = await releaseActionService.update(
1521
1713
  actionId,
1522
1714
  releaseId,
1523
1715
  releaseActionUpdateArgs
@@ -1529,14 +1721,36 @@ const releaseActionController = {
1529
1721
  async delete(ctx) {
1530
1722
  const actionId = ctx.params.actionId;
1531
1723
  const releaseId = ctx.params.releaseId;
1532
- const releaseService = getService("release", { strapi });
1533
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1724
+ const releaseActionService = getService("release-action", { strapi });
1725
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1534
1726
  ctx.body = {
1535
1727
  data: deletedReleaseAction
1536
1728
  };
1537
1729
  }
1538
1730
  };
1539
- const controllers = { release: releaseController, "release-action": releaseActionController };
1731
+ const SETTINGS_SCHEMA = yup.object().shape({
1732
+ defaultTimezone: yup.string().nullable().default(null)
1733
+ }).required().noUnknown();
1734
+ const validateSettings = validateYupSchema(SETTINGS_SCHEMA);
1735
+ const settingsController = {
1736
+ async find(ctx) {
1737
+ const settingsService = getService("settings", { strapi });
1738
+ const settings2 = await settingsService.find();
1739
+ ctx.body = { data: settings2 };
1740
+ },
1741
+ async update(ctx) {
1742
+ const settingsBody = ctx.request.body;
1743
+ const settings2 = await validateSettings(settingsBody);
1744
+ const settingsService = getService("settings", { strapi });
1745
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1746
+ ctx.body = { data: updatedSettings };
1747
+ }
1748
+ };
1749
+ const controllers = {
1750
+ release: releaseController,
1751
+ "release-action": releaseActionController,
1752
+ settings: settingsController
1753
+ };
1540
1754
  const release = {
1541
1755
  type: "admin",
1542
1756
  routes: [
@@ -1556,6 +1770,22 @@ const release = {
1556
1770
  ]
1557
1771
  }
1558
1772
  },
1773
+ {
1774
+ method: "GET",
1775
+ path: "/getByDocumentAttached",
1776
+ handler: "release.findByDocumentAttached",
1777
+ config: {
1778
+ policies: [
1779
+ "admin::isAuthenticatedAdmin",
1780
+ {
1781
+ name: "admin::hasPermissions",
1782
+ config: {
1783
+ actions: ["plugin::content-releases.read"]
1784
+ }
1785
+ }
1786
+ ]
1787
+ }
1788
+ },
1559
1789
  {
1560
1790
  method: "POST",
1561
1791
  path: "/",
@@ -1575,7 +1805,7 @@ const release = {
1575
1805
  {
1576
1806
  method: "GET",
1577
1807
  path: "/",
1578
- handler: "release.findMany",
1808
+ handler: "release.findPage",
1579
1809
  config: {
1580
1810
  policies: [
1581
1811
  "admin::isAuthenticatedAdmin",
@@ -1739,13 +1969,50 @@ const releaseAction = {
1739
1969
  }
1740
1970
  ]
1741
1971
  };
1972
+ const settings = {
1973
+ type: "admin",
1974
+ routes: [
1975
+ {
1976
+ method: "GET",
1977
+ path: "/settings",
1978
+ handler: "settings.find",
1979
+ config: {
1980
+ policies: [
1981
+ "admin::isAuthenticatedAdmin",
1982
+ {
1983
+ name: "admin::hasPermissions",
1984
+ config: {
1985
+ actions: ["plugin::content-releases.settings.read"]
1986
+ }
1987
+ }
1988
+ ]
1989
+ }
1990
+ },
1991
+ {
1992
+ method: "PUT",
1993
+ path: "/settings",
1994
+ handler: "settings.update",
1995
+ config: {
1996
+ policies: [
1997
+ "admin::isAuthenticatedAdmin",
1998
+ {
1999
+ name: "admin::hasPermissions",
2000
+ config: {
2001
+ actions: ["plugin::content-releases.settings.update"]
2002
+ }
2003
+ }
2004
+ ]
2005
+ }
2006
+ }
2007
+ ]
2008
+ };
1742
2009
  const routes = {
2010
+ settings,
1743
2011
  release,
1744
2012
  "release-action": releaseAction
1745
2013
  };
1746
- const { features } = require("@strapi/strapi/dist/utils/ee");
1747
2014
  const getPlugin = () => {
1748
- if (features.isEnabled("cms-content-releases")) {
2015
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1749
2016
  return {
1750
2017
  register,
1751
2018
  bootstrap,