@strapi/content-releases 0.0.0-experimental.fc1ac2acd58c8a5a858679956b6d102ac5ee4011 → 0.0.0-experimental.fed75ee8e64c57dbed0b670b25ef026b69baab10

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