@strapi/content-releases 0.0.0-next.f4ff842a3cb7b83db540bee67554b704e042b042 → 0.0.0-next.f5312617ca16b870c2bf1adcea2c69b676979e29

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 (141) 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-jrh58sXY.mjs → App-Cne--1Z8.mjs} +602 -558
  5. package/dist/_chunks/App-Cne--1Z8.mjs.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-bpIYXOfu.js → PurchaseContentReleases-Be3acS2L.js} +7 -6
  7. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-3tRbmbY3.mjs → PurchaseContentReleases-_MxP6-Dt.mjs} +8 -7
  9. package/dist/_chunks/PurchaseContentReleases-_MxP6-Dt.mjs.map +1 -0
  10. package/dist/_chunks/ReleasesSettingsPage-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-HrREghh3.js → en-CmYoEnA7.js} +9 -2
  15. package/dist/_chunks/en-CmYoEnA7.js.map +1 -0
  16. package/dist/_chunks/{en-ltT1TlKQ.mjs → en-D0yVZFqf.mjs} +9 -2
  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 +843 -636
  56. package/dist/server/index.js.map +1 -1
  57. package/dist/server/index.mjs +844 -636
  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-dLXY5ei3.js +0 -1353
  131. package/dist/_chunks/App-dLXY5ei3.js.map +0 -1
  132. package/dist/_chunks/App-jrh58sXY.mjs.map +0 -1
  133. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +0 -1
  134. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +0 -1
  135. package/dist/_chunks/en-HrREghh3.js.map +0 -1
  136. package/dist/_chunks/en-ltT1TlKQ.mjs.map +0 -1
  137. package/dist/_chunks/index-CVO0Rqdm.js +0 -1336
  138. package/dist/_chunks/index-CVO0Rqdm.js.map +0 -1
  139. package/dist/_chunks/index-PiOGBETy.mjs +0 -1315
  140. package/dist/_chunks/index-PiOGBETy.mjs.map +0 -1
  141. 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
  });
@@ -277,11 +330,43 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
277
330
  }
278
331
  }
279
332
  }
280
- 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
+ };
281
365
  const register = async ({ strapi: strapi2 }) => {
282
- if (features$2.isEnabled("cms-content-releases")) {
283
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
284
- 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);
285
370
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
286
371
  }
287
372
  if (strapi2.plugin("graphql")) {
@@ -290,129 +375,134 @@ const register = async ({ strapi: strapi2 }) => {
290
375
  graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
291
376
  }
292
377
  };
293
- 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
+ };
294
476
  const bootstrap = async ({ strapi: strapi2 }) => {
295
- if (features$1.isEnabled("cms-content-releases")) {
477
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
296
478
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
297
479
  (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
298
480
  );
299
481
  strapi2.db.lifecycles.subscribe({
300
482
  models: contentTypesWithDraftAndPublish,
301
- async afterDelete(event) {
302
- try {
303
- const { model, result } = event;
304
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
305
- const { id } = result;
306
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
307
- where: {
308
- actions: {
309
- target_type: model.uid,
310
- target_id: id
311
- }
312
- }
313
- });
314
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
315
- where: {
316
- target_type: model.uid,
317
- target_id: id
318
- }
319
- });
320
- for (const release2 of releases) {
321
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
322
- }
323
- }
324
- } catch (error) {
325
- strapi2.log.error("Error while deleting release actions after entry delete", { error });
326
- }
327
- },
328
- /**
329
- * deleteMany hook doesn't return the deleted entries ids
330
- * so we need to fetch them before deleting the entries to save the ids on our state
331
- */
332
- async beforeDeleteMany(event) {
333
- const { model, params } = event;
334
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
335
- const { where } = params;
336
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
337
- event.state.entriesToDelete = entriesToDelete;
338
- }
339
- },
340
483
  /**
341
- * We delete the release actions related to deleted entries
342
- * 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
343
485
  */
344
486
  async afterDeleteMany(event) {
345
487
  try {
346
- const { model, state } = event;
347
- const entriesToDelete = state.entriesToDelete;
348
- if (entriesToDelete) {
349
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
350
- where: {
351
- actions: {
352
- target_type: model.uid,
353
- target_id: {
354
- $in: entriesToDelete.map(
355
- (entry) => entry.id
356
- )
357
- }
358
- }
359
- }
360
- });
361
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
362
- where: {
363
- target_type: model.uid,
364
- target_id: {
365
- $in: entriesToDelete.map((entry) => entry.id)
366
- }
367
- }
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 }
368
495
  });
369
- for (const release2 of releases) {
370
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
371
- }
372
496
  }
373
497
  } catch (error) {
374
498
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
375
499
  error
376
500
  });
377
501
  }
378
- },
379
- async afterUpdate(event) {
380
- try {
381
- const { model, result } = event;
382
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
383
- const isEntryValid = await getEntryValidStatus(
384
- model.uid,
385
- result,
386
- {
387
- strapi: strapi2
388
- }
389
- );
390
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
391
- where: {
392
- target_type: model.uid,
393
- target_id: result.id
394
- },
395
- data: {
396
- isEntryValid
397
- }
398
- });
399
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
400
- where: {
401
- actions: {
402
- target_type: model.uid,
403
- target_id: result.id
404
- }
405
- }
406
- });
407
- for (const release2 of releases) {
408
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
409
- }
410
- }
411
- } catch (error) {
412
- strapi2.log.error("Error while updating release actions after entry update", { error });
413
- }
414
502
  }
415
503
  });
504
+ strapi2.documents.use(deleteActionsOnDelete);
505
+ strapi2.documents.use(updateActionsOnUpdate);
416
506
  getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
417
507
  strapi2.log.error(
418
508
  "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
@@ -420,7 +510,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
420
510
  throw err;
421
511
  });
422
512
  Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
423
- strapi2.webhookStore.addAllowedEvent(key, value);
513
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
424
514
  });
425
515
  }
426
516
  };
@@ -504,15 +594,13 @@ const schema = {
504
594
  enum: ["publish", "unpublish"],
505
595
  required: true
506
596
  },
507
- entry: {
508
- type: "relation",
509
- relation: "morphToOne",
510
- configurable: false
511
- },
512
597
  contentType: {
513
598
  type: "string",
514
599
  required: true
515
600
  },
601
+ entryDocumentId: {
602
+ type: "string"
603
+ },
516
604
  locale: {
517
605
  type: "string"
518
606
  },
@@ -534,18 +622,6 @@ const contentTypes = {
534
622
  release: release$1,
535
623
  "release-action": releaseAction$1
536
624
  };
537
- const getGroupName = (queryValue) => {
538
- switch (queryValue) {
539
- case "contentType":
540
- return "contentType.displayName";
541
- case "action":
542
- return "type";
543
- case "locale":
544
- return ___default.default.getOr("No locale", "locale.name");
545
- default:
546
- return "contentType.displayName";
547
- }
548
- };
549
625
  const createReleaseService = ({ strapi: strapi2 }) => {
550
626
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
551
627
  strapi2.eventHub.emit(event, {
@@ -554,93 +630,32 @@ const createReleaseService = ({ strapi: strapi2 }) => {
554
630
  release: release2
555
631
  });
556
632
  };
557
- const publishSingleTypeAction = async (uid, actionType, entryId) => {
558
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
559
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
560
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
561
- const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
562
- try {
563
- if (actionType === "publish") {
564
- await entityManagerService.publish(entry, uid);
565
- } else {
566
- await entityManagerService.unpublish(entry, uid);
567
- }
568
- } catch (error) {
569
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
570
- ;
571
- else {
572
- throw error;
573
- }
574
- }
575
- };
576
- const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
577
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
578
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
579
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
580
- const entriesToPublish = await strapi2.entityService.findMany(uid, {
581
- filters: {
582
- id: {
583
- $in: entriesToPublishIds
584
- }
585
- },
586
- populate
587
- });
588
- const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
589
- filters: {
590
- id: {
591
- $in: entriestoUnpublishIds
592
- }
593
- },
594
- populate
595
- });
596
- if (entriesToPublish.length > 0) {
597
- await entityManagerService.publishMany(entriesToPublish, uid);
598
- }
599
- if (entriesToUnpublish.length > 0) {
600
- await entityManagerService.unpublishMany(entriesToUnpublish, uid);
601
- }
602
- };
603
633
  const getFormattedActions = async (releaseId) => {
604
634
  const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
605
635
  where: {
606
636
  release: {
607
637
  id: releaseId
608
638
  }
609
- },
610
- populate: {
611
- entry: {
612
- fields: ["id"]
613
- }
614
639
  }
615
640
  });
616
641
  if (actions.length === 0) {
617
642
  throw new utils.errors.ValidationError("No entries to publish");
618
643
  }
619
- const collectionTypeActions = {};
620
- const singleTypeActions = [];
644
+ const formattedActions = {};
621
645
  for (const action of actions) {
622
646
  const contentTypeUid = action.contentType;
623
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
624
- if (!collectionTypeActions[contentTypeUid]) {
625
- collectionTypeActions[contentTypeUid] = {
626
- entriesToPublishIds: [],
627
- entriesToUnpublishIds: []
628
- };
629
- }
630
- if (action.type === "publish") {
631
- collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
632
- } else {
633
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
634
- }
635
- } else {
636
- singleTypeActions.push({
637
- uid: contentTypeUid,
638
- action: action.type,
639
- id: action.entry.id
640
- });
647
+ if (!formattedActions[contentTypeUid]) {
648
+ formattedActions[contentTypeUid] = {
649
+ publish: [],
650
+ unpublish: []
651
+ };
641
652
  }
653
+ formattedActions[contentTypeUid][action.type].push({
654
+ documentId: action.entryDocumentId,
655
+ locale: action.locale
656
+ });
642
657
  }
643
- return { collectionTypeActions, singleTypeActions };
658
+ return formattedActions;
644
659
  };
645
660
  return {
646
661
  async create(releaseData, { user }) {
@@ -655,7 +670,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
655
670
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
656
671
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
657
672
  ]);
658
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
673
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
659
674
  data: {
660
675
  ...releaseWithCreatorFields,
661
676
  status: "empty"
@@ -669,107 +684,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
669
684
  return release2;
670
685
  },
671
686
  async findOne(id, query = {}) {
672
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
673
- ...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 }
674
691
  });
675
692
  return release2;
676
693
  },
677
694
  findPage(query) {
678
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
679
- ...query,
695
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
696
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
697
+ ...dbQuery,
680
698
  populate: {
681
699
  actions: {
682
- // @ts-expect-error Ignore missing properties
683
700
  count: true
684
701
  }
685
702
  }
686
703
  });
687
704
  },
688
- async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
- let entries = entriesIds;
690
- if (!Array.isArray(entriesIds)) {
691
- entries = [entriesIds];
692
- }
693
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
694
- where: {
695
- actions: {
696
- target_type: contentTypeUid,
697
- target_id: {
698
- $in: entries
699
- }
700
- },
701
- releasedAt: {
702
- $null: true
703
- }
704
- },
705
- populate: {
706
- // Filter the action to get only the content type entry
707
- actions: {
708
- where: {
709
- target_type: contentTypeUid,
710
- target_id: {
711
- $in: entries
712
- }
713
- },
714
- populate: {
715
- entry: {
716
- select: ["id"]
717
- }
718
- }
719
- }
720
- }
721
- });
722
- return releases.map((release2) => {
723
- if (release2.actions?.length) {
724
- const actionsForEntry = release2.actions;
725
- delete release2.actions;
726
- return {
727
- ...release2,
728
- actions: actionsForEntry
729
- };
730
- }
731
- return release2;
732
- });
733
- },
734
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
735
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
736
- where: {
737
- releasedAt: {
738
- $null: true
739
- },
740
- actions: {
741
- target_type: contentTypeUid,
742
- target_id: entryId
743
- }
744
- }
745
- });
746
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
747
- where: {
748
- $or: [
749
- {
750
- id: {
751
- $notIn: releasesRelated.map((release2) => release2.id)
752
- }
753
- },
754
- {
755
- actions: null
756
- }
757
- ],
758
- releasedAt: {
759
- $null: true
760
- }
761
- }
762
- });
763
- return releases.map((release2) => {
764
- if (release2.actions?.length) {
765
- const [actionForEntry] = release2.actions;
766
- delete release2.actions;
767
- return {
768
- ...release2,
769
- action: actionForEntry
770
- };
771
- }
772
- 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
773
709
  });
774
710
  },
775
711
  async update(id, releaseData, { user }) {
@@ -784,19 +720,15 @@ const createReleaseService = ({ strapi: strapi2 }) => {
784
720
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
785
721
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
786
722
  ]);
787
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
723
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
788
724
  if (!release2) {
789
725
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
790
726
  }
791
727
  if (release2.releasedAt) {
792
728
  throw new utils.errors.ValidationError("Release already published");
793
729
  }
794
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
795
- /*
796
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
797
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
798
- */
799
- // @ts-expect-error see above
730
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
731
+ where: { id },
800
732
  data: releaseWithCreatorFields
801
733
  });
802
734
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -809,130 +741,6 @@ const createReleaseService = ({ strapi: strapi2 }) => {
809
741
  strapi2.telemetry.send("didUpdateContentRelease");
810
742
  return updatedRelease;
811
743
  },
812
- async createAction(releaseId, action) {
813
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
814
- strapi: strapi2
815
- });
816
- await Promise.all([
817
- validateEntryContentType(action.entry.contentType),
818
- validateUniqueEntry(releaseId, action)
819
- ]);
820
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
821
- if (!release2) {
822
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
823
- }
824
- if (release2.releasedAt) {
825
- throw new utils.errors.ValidationError("Release already published");
826
- }
827
- const { entry, type } = action;
828
- const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
829
- const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
830
- const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
831
- data: {
832
- type,
833
- contentType: entry.contentType,
834
- locale: entry.locale,
835
- isEntryValid,
836
- entry: {
837
- id: entry.id,
838
- __type: entry.contentType,
839
- __pivot: { field: "entry" }
840
- },
841
- release: releaseId
842
- },
843
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
844
- });
845
- this.updateReleaseStatus(releaseId);
846
- return releaseAction2;
847
- },
848
- async findActions(releaseId, query) {
849
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
850
- fields: ["id"]
851
- });
852
- if (!release2) {
853
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
854
- }
855
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
856
- ...query,
857
- populate: {
858
- entry: {
859
- populate: "*"
860
- }
861
- },
862
- filters: {
863
- release: releaseId
864
- }
865
- });
866
- },
867
- async countActions(query) {
868
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
869
- },
870
- async groupActions(actions, groupBy) {
871
- const contentTypeUids = actions.reduce((acc, action) => {
872
- if (!acc.includes(action.contentType)) {
873
- acc.push(action.contentType);
874
- }
875
- return acc;
876
- }, []);
877
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
878
- contentTypeUids
879
- );
880
- const allLocalesDictionary = await this.getLocalesDataForActions();
881
- const formattedData = actions.map((action) => {
882
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
883
- return {
884
- ...action,
885
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
886
- contentType: {
887
- displayName,
888
- mainFieldValue: action.entry[mainField],
889
- uid: action.contentType
890
- }
891
- };
892
- });
893
- const groupName = getGroupName(groupBy);
894
- return ___default.default.groupBy(groupName)(formattedData);
895
- },
896
- async getLocalesDataForActions() {
897
- if (!strapi2.plugin("i18n")) {
898
- return {};
899
- }
900
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
901
- return allLocales.reduce((acc, locale) => {
902
- acc[locale.code] = { name: locale.name, code: locale.code };
903
- return acc;
904
- }, {});
905
- },
906
- async getContentTypesDataForActions(contentTypesUids) {
907
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
908
- const contentTypesData = {};
909
- for (const contentTypeUid of contentTypesUids) {
910
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
911
- uid: contentTypeUid
912
- });
913
- contentTypesData[contentTypeUid] = {
914
- mainField: contentTypeConfig.settings.mainField,
915
- displayName: strapi2.getModel(contentTypeUid).info.displayName
916
- };
917
- }
918
- return contentTypesData;
919
- },
920
- getContentTypeModelsFromActions(actions) {
921
- const contentTypeUids = actions.reduce((acc, action) => {
922
- if (!acc.includes(action.contentType)) {
923
- acc.push(action.contentType);
924
- }
925
- return acc;
926
- }, []);
927
- const contentTypeModelsMap = contentTypeUids.reduce(
928
- (acc, contentTypeUid) => {
929
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
930
- return acc;
931
- },
932
- {}
933
- );
934
- return contentTypeModelsMap;
935
- },
936
744
  async getAllComponents() {
937
745
  const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
938
746
  const components = await contentManagerComponentsService.findAllComponents();
@@ -946,10 +754,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
946
754
  return componentsMap;
947
755
  },
948
756
  async delete(releaseId) {
949
- 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 },
950
759
  populate: {
951
760
  actions: {
952
- fields: ["id"]
761
+ select: ["id"]
953
762
  }
954
763
  }
955
764
  });
@@ -967,7 +776,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
967
776
  }
968
777
  }
969
778
  });
970
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
779
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
780
+ where: {
781
+ id: releaseId
782
+ }
783
+ });
971
784
  });
972
785
  if (release2.scheduledAt) {
973
786
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -993,22 +806,19 @@ const createReleaseService = ({ strapi: strapi2 }) => {
993
806
  }
994
807
  try {
995
808
  strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
996
- const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
997
- 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
+ )
998
821
  );
999
- await strapi2.db.transaction(async () => {
1000
- for (const { uid, action, id } of singleTypeActions) {
1001
- await publishSingleTypeAction(uid, action, id);
1002
- }
1003
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
1004
- const uid = contentTypeUid;
1005
- await publishCollectionTypeAction(
1006
- uid,
1007
- collectionTypeActions[uid].entriesToPublishIds,
1008
- collectionTypeActions[uid].entriesToUnpublishIds
1009
- );
1010
- }
1011
- });
1012
822
  const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
1013
823
  where: {
1014
824
  id: releaseId
@@ -1038,13 +848,226 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1038
848
  };
1039
849
  }
1040
850
  });
1041
- if (error) {
1042
- throw error;
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);
1043
983
  }
1044
- return release2;
984
+ return releaseAction2;
1045
985
  },
1046
- async updateAction(actionId, releaseId, update) {
1047
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
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);
1048
+ },
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({
1048
1071
  where: {
1049
1072
  id: actionId,
1050
1073
  release: {
@@ -1053,17 +1076,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1053
1076
  $null: true
1054
1077
  }
1055
1078
  }
1056
- },
1057
- data: update
1079
+ }
1058
1080
  });
1059
- if (!updatedAction) {
1081
+ if (!action) {
1060
1082
  throw new utils.errors.NotFoundError(
1061
1083
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1062
1084
  );
1063
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);
1064
1112
  return updatedAction;
1065
1113
  },
1066
- async deleteAction(actionId, releaseId) {
1114
+ async delete(actionId, releaseId) {
1067
1115
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1068
1116
  where: {
1069
1117
  id: actionId,
@@ -1080,51 +1128,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1080
1128
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1081
1129
  );
1082
1130
  }
1083
- this.updateReleaseStatus(releaseId);
1131
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1084
1132
  return deletedAction;
1085
- },
1086
- async updateReleaseStatus(releaseId) {
1087
- const [totalActions, invalidActions] = await Promise.all([
1088
- this.countActions({
1089
- filters: {
1090
- release: releaseId
1091
- }
1092
- }),
1093
- this.countActions({
1094
- filters: {
1095
- release: releaseId,
1096
- isEntryValid: false
1097
- }
1098
- })
1099
- ]);
1100
- if (totalActions > 0) {
1101
- if (invalidActions > 0) {
1102
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1103
- where: {
1104
- id: releaseId
1105
- },
1106
- data: {
1107
- status: "blocked"
1108
- }
1109
- });
1110
- }
1111
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1112
- where: {
1113
- id: releaseId
1114
- },
1115
- data: {
1116
- status: "ready"
1117
- }
1118
- });
1119
- }
1120
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1121
- where: {
1122
- id: releaseId
1123
- },
1124
- data: {
1125
- status: "empty"
1126
- }
1127
- });
1128
1133
  }
1129
1134
  };
1130
1135
  };
@@ -1136,37 +1141,43 @@ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1136
1141
  }
1137
1142
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1138
1143
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1139
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1140
- 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
+ }
1141
1151
  });
1142
1152
  if (!release2) {
1143
1153
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1144
1154
  }
1145
1155
  const isEntryInRelease = release2.actions.some(
1146
- (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)
1147
1157
  );
1148
1158
  if (isEntryInRelease) {
1149
1159
  throw new AlreadyOnReleaseError(
1150
- `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}`
1151
1161
  );
1152
1162
  }
1153
1163
  },
1154
- validateEntryContentType(contentTypeUid) {
1164
+ validateEntryData(contentTypeUid, entryDocumentId) {
1155
1165
  const contentType = strapi2.contentType(contentTypeUid);
1156
1166
  if (!contentType) {
1157
1167
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1158
1168
  }
1159
- if (!contentType.options?.draftAndPublish) {
1169
+ if (!utils.contentTypes.hasDraftAndPublish(contentType)) {
1160
1170
  throw new utils.errors.ValidationError(
1161
1171
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1162
1172
  );
1163
1173
  }
1174
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1175
+ throw new utils.errors.ValidationError("Document id is required for collection type");
1176
+ }
1164
1177
  },
1165
1178
  async validatePendingReleasesLimit() {
1166
- const maximumPendingReleases = (
1167
- // @ts-expect-error - options is not typed into features
1168
- EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
1169
- );
1179
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1180
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1170
1181
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1171
1182
  filters: {
1172
1183
  releasedAt: {
@@ -1179,8 +1190,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1179
1190
  }
1180
1191
  },
1181
1192
  async validateUniqueNameForPendingRelease(name, id) {
1182
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1183
- filters: {
1193
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1194
+ where: {
1184
1195
  releasedAt: {
1185
1196
  $null: true
1186
1197
  },
@@ -1209,7 +1220,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1209
1220
  }
1210
1221
  const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1211
1222
  try {
1212
- await getService("release").publish(releaseId);
1223
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1213
1224
  } catch (error) {
1214
1225
  }
1215
1226
  this.cancel(releaseId);
@@ -1251,85 +1262,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1251
1262
  }
1252
1263
  };
1253
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
+ };
1254
1286
  const services = {
1255
1287
  release: createReleaseService,
1288
+ "release-action": createReleaseActionService,
1256
1289
  "release-validation": createReleaseValidationService,
1257
- scheduling: createSchedulingService
1290
+ scheduling: createSchedulingService,
1291
+ settings: createSettingsService
1258
1292
  };
1259
- const RELEASE_SCHEMA = yup__namespace.object().shape({
1260
- name: yup__namespace.string().trim().required(),
1261
- scheduledAt: yup__namespace.string().nullable(),
1262
- isScheduled: yup__namespace.boolean().optional(),
1263
- time: yup__namespace.string().when("isScheduled", {
1264
- is: true,
1265
- then: yup__namespace.string().trim().required(),
1266
- otherwise: yup__namespace.string().nullable()
1267
- }),
1268
- timezone: yup__namespace.string().when("isScheduled", {
1269
- is: true,
1270
- then: yup__namespace.string().required().nullable(),
1271
- otherwise: yup__namespace.string().nullable()
1272
- }),
1273
- date: yup__namespace.string().when("isScheduled", {
1274
- is: true,
1275
- then: yup__namespace.string().required().nullable(),
1276
- 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()
1277
1300
  })
1278
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();
1279
1308
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
1309
+ const validatefindByDocumentAttachedParams = utils.validateYupSchema(
1310
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1311
+ );
1280
1312
  const releaseController = {
1281
- async findMany(ctx) {
1282
- 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({
1283
1320
  ability: ctx.state.userAbility,
1284
1321
  model: RELEASE_MODEL_UID
1285
1322
  });
1286
1323
  await permissionsManager.validateQuery(ctx.query);
1287
1324
  const releaseService = getService("release", { strapi });
1288
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1289
- if (isFindManyForContentTypeEntry) {
1290
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1291
- const contentTypeUid = query.contentTypeUid;
1292
- const entryId = query.entryId;
1293
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1294
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1295
- ctx.body = { data };
1296
- } else {
1297
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1298
- const { results, pagination } = await releaseService.findPage(query);
1299
- const data = results.map((release2) => {
1300
- const { actions, ...releaseData } = release2;
1301
- return {
1302
- ...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: {
1303
1348
  actions: {
1304
- meta: {
1305
- count: actions.count
1349
+ fields: ["type"],
1350
+ filters: {
1351
+ contentType,
1352
+ entryDocumentId: entryDocumentId ?? null,
1353
+ locale: locale ?? null
1306
1354
  }
1307
1355
  }
1308
- };
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
+ }
1309
1369
  });
1310
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1370
+ const releases = await releaseService.findMany({
1311
1371
  where: {
1372
+ $or: [
1373
+ {
1374
+ id: {
1375
+ $notIn: relatedReleases.map((release2) => release2.id)
1376
+ }
1377
+ },
1378
+ {
1379
+ actions: null
1380
+ }
1381
+ ],
1312
1382
  releasedAt: null
1313
1383
  }
1314
1384
  });
1315
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1385
+ ctx.body = { data: releases };
1316
1386
  }
1317
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
+ },
1318
1415
  async findOne(ctx) {
1319
1416
  const id = ctx.params.id;
1320
1417
  const releaseService = getService("release", { strapi });
1418
+ const releaseActionService = getService("release-action", { strapi });
1321
1419
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1322
1420
  if (!release2) {
1323
1421
  throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1324
1422
  }
1325
- const count = await releaseService.countActions({
1423
+ const count = await releaseActionService.countActions({
1326
1424
  filters: {
1327
1425
  release: id
1328
1426
  }
1329
1427
  });
1330
1428
  const sanitizedRelease = {
1331
1429
  ...release2,
1332
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1430
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1333
1431
  };
1334
1432
  const data = {
1335
1433
  ...sanitizedRelease,
@@ -1342,22 +1440,39 @@ const releaseController = {
1342
1440
  ctx.body = { data };
1343
1441
  },
1344
1442
  async mapEntriesToReleases(ctx) {
1345
- const { contentTypeUid, entriesIds } = ctx.query;
1346
- if (!contentTypeUid || !entriesIds) {
1443
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1444
+ if (!contentTypeUid || !documentIds) {
1347
1445
  throw new utils.errors.ValidationError("Missing required query parameters");
1348
1446
  }
1349
1447
  const releaseService = getService("release", { strapi });
1350
- const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1351
- contentTypeUid,
1352
- entriesIds
1353
- );
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
+ });
1354
1463
  const mappedEntriesInReleases = releasesWithActions.reduce(
1355
1464
  (acc, release2) => {
1356
1465
  release2.actions.forEach((action) => {
1357
- if (!acc[action.entry.id]) {
1358
- acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
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 }];
1359
1474
  } else {
1360
- acc[action.entry.id].push({ id: release2.id, name: release2.name });
1475
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1361
1476
  }
1362
1477
  });
1363
1478
  return acc;
@@ -1374,13 +1489,13 @@ const releaseController = {
1374
1489
  await validateRelease(releaseArgs);
1375
1490
  const releaseService = getService("release", { strapi });
1376
1491
  const release2 = await releaseService.create(releaseArgs, { user });
1377
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1492
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1378
1493
  ability: ctx.state.userAbility,
1379
1494
  model: RELEASE_MODEL_UID
1380
1495
  });
1381
- ctx.body = {
1496
+ ctx.created({
1382
1497
  data: await permissionsManager.sanitizeOutput(release2)
1383
- };
1498
+ });
1384
1499
  },
1385
1500
  async update(ctx) {
1386
1501
  const user = ctx.state.user;
@@ -1389,7 +1504,7 @@ const releaseController = {
1389
1504
  await validateRelease(releaseArgs);
1390
1505
  const releaseService = getService("release", { strapi });
1391
1506
  const release2 = await releaseService.update(id, releaseArgs, { user });
1392
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1507
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1393
1508
  ability: ctx.state.userAbility,
1394
1509
  model: RELEASE_MODEL_UID
1395
1510
  });
@@ -1406,18 +1521,18 @@ const releaseController = {
1406
1521
  };
1407
1522
  },
1408
1523
  async publish(ctx) {
1409
- const user = ctx.state.user;
1410
1524
  const id = ctx.params.id;
1411
1525
  const releaseService = getService("release", { strapi });
1412
- const release2 = await releaseService.publish(id, { user });
1526
+ const releaseActionService = getService("release-action", { strapi });
1527
+ const release2 = await releaseService.publish(id);
1413
1528
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1414
- releaseService.countActions({
1529
+ releaseActionService.countActions({
1415
1530
  filters: {
1416
1531
  release: id,
1417
1532
  type: "publish"
1418
1533
  }
1419
1534
  }),
1420
- releaseService.countActions({
1535
+ releaseActionService.countActions({
1421
1536
  filters: {
1422
1537
  release: id,
1423
1538
  type: "unpublish"
@@ -1435,27 +1550,30 @@ const releaseController = {
1435
1550
  }
1436
1551
  };
1437
1552
  const RELEASE_ACTION_SCHEMA = utils.yup.object().shape({
1438
- entry: utils.yup.object().shape({
1439
- id: utils.yup.strapiID().required(),
1440
- contentType: utils.yup.string().required()
1441
- }).required(),
1553
+ contentType: utils.yup.string().required(),
1554
+ entryDocumentId: utils.yup.strapiID(),
1555
+ locale: utils.yup.string(),
1442
1556
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1443
1557
  });
1444
1558
  const RELEASE_ACTION_UPDATE_SCHEMA = utils.yup.object().shape({
1445
1559
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1446
1560
  });
1561
+ const FIND_MANY_ACTIONS_PARAMS = utils.yup.object().shape({
1562
+ groupBy: utils.yup.string().oneOf(["action", "contentType", "locale"])
1563
+ });
1447
1564
  const validateReleaseAction = utils.validateYupSchema(RELEASE_ACTION_SCHEMA);
1448
1565
  const validateReleaseActionUpdateSchema = utils.validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1566
+ const validateFindManyActionsParams = utils.validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1449
1567
  const releaseActionController = {
1450
1568
  async create(ctx) {
1451
1569
  const releaseId = ctx.params.releaseId;
1452
1570
  const releaseActionArgs = ctx.request.body;
1453
1571
  await validateReleaseAction(releaseActionArgs);
1454
- const releaseService = getService("release", { strapi });
1455
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1456
- ctx.body = {
1572
+ const releaseActionService = getService("release-action", { strapi });
1573
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1574
+ ctx.created({
1457
1575
  data: releaseAction2
1458
- };
1576
+ });
1459
1577
  },
1460
1578
  async createMany(ctx) {
1461
1579
  const releaseId = ctx.params.releaseId;
@@ -1463,12 +1581,15 @@ const releaseActionController = {
1463
1581
  await Promise.all(
1464
1582
  releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1465
1583
  );
1584
+ const releaseActionService = getService("release-action", { strapi });
1466
1585
  const releaseService = getService("release", { strapi });
1467
1586
  const releaseActions = await strapi.db.transaction(async () => {
1468
1587
  const releaseActions2 = await Promise.all(
1469
1588
  releaseActionsArgs.map(async (releaseActionArgs) => {
1470
1589
  try {
1471
- const action = await releaseService.createAction(releaseId, releaseActionArgs);
1590
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1591
+ disableUpdateReleaseStatus: true
1592
+ });
1472
1593
  return action;
1473
1594
  } catch (error) {
1474
1595
  if (error instanceof AlreadyOnReleaseError) {
@@ -1481,43 +1602,54 @@ const releaseActionController = {
1481
1602
  return releaseActions2;
1482
1603
  });
1483
1604
  const newReleaseActions = releaseActions.filter((action) => action !== null);
1484
- ctx.body = {
1605
+ if (newReleaseActions.length > 0) {
1606
+ releaseService.updateReleaseStatus(releaseId);
1607
+ }
1608
+ ctx.created({
1485
1609
  data: newReleaseActions,
1486
1610
  meta: {
1487
1611
  entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1488
1612
  totalEntries: releaseActions.length
1489
1613
  }
1490
- };
1614
+ });
1491
1615
  },
1492
1616
  async findMany(ctx) {
1493
1617
  const releaseId = ctx.params.releaseId;
1494
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1618
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1495
1619
  ability: ctx.state.userAbility,
1496
1620
  model: RELEASE_ACTION_MODEL_UID
1497
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;
1498
1630
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1499
- const releaseService = getService("release", { strapi });
1500
- const { results, pagination } = await releaseService.findActions(releaseId, {
1501
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1631
+ const releaseActionService = getService("release-action", { strapi });
1632
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1502
1633
  ...query
1503
1634
  });
1504
1635
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1505
1636
  if (acc[action.contentType]) {
1506
1637
  return acc;
1507
1638
  }
1508
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1639
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1509
1640
  ability: ctx.state.userAbility,
1510
1641
  model: action.contentType
1511
1642
  });
1512
1643
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1513
1644
  return acc;
1514
1645
  }, {});
1515
- const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1646
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
1516
1647
  ...action,
1517
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1648
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1518
1649
  }));
1519
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1520
- 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 });
1521
1653
  const components = await releaseService.getAllComponents();
1522
1654
  ctx.body = {
1523
1655
  data: groupedData,
@@ -1533,8 +1665,8 @@ const releaseActionController = {
1533
1665
  const releaseId = ctx.params.releaseId;
1534
1666
  const releaseActionUpdateArgs = ctx.request.body;
1535
1667
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1536
- const releaseService = getService("release", { strapi });
1537
- const updatedAction = await releaseService.updateAction(
1668
+ const releaseActionService = getService("release-action", { strapi });
1669
+ const updatedAction = await releaseActionService.update(
1538
1670
  actionId,
1539
1671
  releaseId,
1540
1672
  releaseActionUpdateArgs
@@ -1546,14 +1678,36 @@ const releaseActionController = {
1546
1678
  async delete(ctx) {
1547
1679
  const actionId = ctx.params.actionId;
1548
1680
  const releaseId = ctx.params.releaseId;
1549
- const releaseService = getService("release", { strapi });
1550
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1681
+ const releaseActionService = getService("release-action", { strapi });
1682
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1551
1683
  ctx.body = {
1552
1684
  data: deletedReleaseAction
1553
1685
  };
1554
1686
  }
1555
1687
  };
1556
- 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
+ };
1557
1711
  const release = {
1558
1712
  type: "admin",
1559
1713
  routes: [
@@ -1573,6 +1727,22 @@ const release = {
1573
1727
  ]
1574
1728
  }
1575
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
+ },
1576
1746
  {
1577
1747
  method: "POST",
1578
1748
  path: "/",
@@ -1592,7 +1762,7 @@ const release = {
1592
1762
  {
1593
1763
  method: "GET",
1594
1764
  path: "/",
1595
- handler: "release.findMany",
1765
+ handler: "release.findPage",
1596
1766
  config: {
1597
1767
  policies: [
1598
1768
  "admin::isAuthenticatedAdmin",
@@ -1756,13 +1926,50 @@ const releaseAction = {
1756
1926
  }
1757
1927
  ]
1758
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
+ };
1759
1966
  const routes = {
1967
+ settings,
1760
1968
  release,
1761
1969
  "release-action": releaseAction
1762
1970
  };
1763
- const { features } = require("@strapi/strapi/dist/utils/ee");
1764
1971
  const getPlugin = () => {
1765
- if (features.isEnabled("cms-content-releases")) {
1972
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1766
1973
  return {
1767
1974
  register,
1768
1975
  bootstrap,