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

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