@strapi/content-releases 0.0.0-experimental.d362bf200f5f9359a4bbd4a549603de5ee1f04ca → 0.0.0-experimental.d3cdf79a0d5f803dfeb6d0f055bb2f3b913bb015

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