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