@strapi/content-releases 0.0.0-experimental.e60ec1829240dae21c1e1d29076681c322288813 → 0.0.0-experimental.e8d8fc824d0f6a695b2a9ebaa4680ed21c3645ca

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 (109) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/{App-BsUSTHVD.mjs → App-CiZCkScI.mjs} +643 -428
  3. package/dist/_chunks/App-CiZCkScI.mjs.map +1 -0
  4. package/dist/_chunks/{App-CXRpb2hi.js → App-SGjO5UPV.js} +682 -470
  5. package/dist/_chunks/App-SGjO5UPV.js.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-Be3acS2L.js → PurchaseContentReleases--qQepXpP.js} +2 -2
  7. package/dist/_chunks/PurchaseContentReleases--qQepXpP.js.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-_MxP6-Dt.mjs → PurchaseContentReleases-D-n-w-st.mjs} +2 -2
  9. package/dist/_chunks/{PurchaseContentReleases-_MxP6-Dt.mjs.map → PurchaseContentReleases-D-n-w-st.mjs.map} +1 -1
  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-DtFJ5ViE.js → en-BWPPsSH-.js} +18 -2
  15. package/dist/_chunks/en-BWPPsSH-.js.map +1 -0
  16. package/dist/_chunks/{en-B9Ur3VsE.mjs → en-D9Q4YW03.mjs} +18 -2
  17. package/dist/_chunks/en-D9Q4YW03.mjs.map +1 -0
  18. package/dist/_chunks/{index-DJLIZdZv.mjs → index-BjvFfTtA.mjs} +751 -605
  19. package/dist/_chunks/index-BjvFfTtA.mjs.map +1 -0
  20. package/dist/_chunks/{index-B6-lic1Q.js → index-CyU534vL.js} +739 -596
  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/ReleaseActionMenu.d.ts +3 -3
  30. package/dist/admin/src/components/{CMReleasesContainer.d.ts → ReleaseActionModal.d.ts} +3 -1
  31. package/dist/admin/src/components/ReleaseListCell.d.ts +28 -0
  32. package/dist/admin/src/components/ReleaseModal.d.ts +3 -2
  33. package/dist/admin/src/components/ReleasesPanel.d.ts +3 -0
  34. package/dist/admin/src/constants.d.ts +18 -0
  35. package/dist/admin/src/modules/hooks.d.ts +7 -0
  36. package/dist/admin/src/pages/ReleasesSettingsPage.d.ts +1 -0
  37. package/dist/admin/src/services/release.d.ts +53 -370
  38. package/dist/admin/src/utils/api.d.ts +6 -0
  39. package/dist/admin/src/utils/time.d.ts +9 -0
  40. package/dist/admin/src/validation/schemas.d.ts +6 -0
  41. package/dist/server/index.js +889 -614
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +889 -613
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/server/src/bootstrap.d.ts.map +1 -1
  46. package/dist/server/src/constants.d.ts +11 -2
  47. package/dist/server/src/constants.d.ts.map +1 -1
  48. package/dist/server/src/content-types/index.d.ts +3 -5
  49. package/dist/server/src/content-types/index.d.ts.map +1 -1
  50. package/dist/server/src/content-types/release-action/index.d.ts +3 -5
  51. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -1
  52. package/dist/server/src/content-types/release-action/schema.d.ts +3 -5
  53. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -1
  54. package/dist/server/src/controllers/index.d.ts +6 -1
  55. package/dist/server/src/controllers/index.d.ts.map +1 -1
  56. package/dist/server/src/controllers/release-action.d.ts.map +1 -1
  57. package/dist/server/src/controllers/release.d.ts +7 -1
  58. package/dist/server/src/controllers/release.d.ts.map +1 -1
  59. package/dist/server/src/controllers/settings.d.ts +11 -0
  60. package/dist/server/src/controllers/settings.d.ts.map +1 -0
  61. package/dist/server/src/controllers/validation/release-action.d.ts +7 -1
  62. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -1
  63. package/dist/server/src/controllers/validation/release.d.ts +2 -0
  64. package/dist/server/src/controllers/validation/release.d.ts.map +1 -1
  65. package/dist/server/src/controllers/validation/settings.d.ts +3 -0
  66. package/dist/server/src/controllers/validation/settings.d.ts.map +1 -0
  67. package/dist/server/src/index.d.ts +64 -49
  68. package/dist/server/src/index.d.ts.map +1 -1
  69. package/dist/server/src/middlewares/documents.d.ts +6 -0
  70. package/dist/server/src/middlewares/documents.d.ts.map +1 -0
  71. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts +9 -0
  72. package/dist/server/src/migrations/database/5.0.0-document-id-in-actions.d.ts.map +1 -0
  73. package/dist/server/src/migrations/index.d.ts.map +1 -1
  74. package/dist/server/src/register.d.ts.map +1 -1
  75. package/dist/server/src/routes/index.d.ts +16 -0
  76. package/dist/server/src/routes/index.d.ts.map +1 -1
  77. package/dist/server/src/routes/release.d.ts.map +1 -1
  78. package/dist/server/src/routes/settings.d.ts +18 -0
  79. package/dist/server/src/routes/settings.d.ts.map +1 -0
  80. package/dist/server/src/services/index.d.ts +36 -38
  81. package/dist/server/src/services/index.d.ts.map +1 -1
  82. package/dist/server/src/services/release-action.d.ts +34 -0
  83. package/dist/server/src/services/release-action.d.ts.map +1 -0
  84. package/dist/server/src/services/release.d.ts +6 -41
  85. package/dist/server/src/services/release.d.ts.map +1 -1
  86. package/dist/server/src/services/settings.d.ts +13 -0
  87. package/dist/server/src/services/settings.d.ts.map +1 -0
  88. package/dist/server/src/services/validation.d.ts +1 -1
  89. package/dist/server/src/services/validation.d.ts.map +1 -1
  90. package/dist/server/src/utils/index.d.ts +29 -8
  91. package/dist/server/src/utils/index.d.ts.map +1 -1
  92. package/dist/shared/contracts/release-actions.d.ts +17 -11
  93. package/dist/shared/contracts/release-actions.d.ts.map +1 -1
  94. package/dist/shared/contracts/releases.d.ts +9 -7
  95. package/dist/shared/contracts/releases.d.ts.map +1 -1
  96. package/dist/shared/contracts/settings.d.ts +39 -0
  97. package/dist/shared/contracts/settings.d.ts.map +1 -0
  98. package/package.json +24 -23
  99. package/dist/_chunks/App-BsUSTHVD.mjs.map +0 -1
  100. package/dist/_chunks/App-CXRpb2hi.js.map +0 -1
  101. package/dist/_chunks/PurchaseContentReleases-Be3acS2L.js.map +0 -1
  102. package/dist/_chunks/en-B9Ur3VsE.mjs.map +0 -1
  103. package/dist/_chunks/en-DtFJ5ViE.js.map +0 -1
  104. package/dist/_chunks/index-B6-lic1Q.js.map +0 -1
  105. package/dist/_chunks/index-DJLIZdZv.mjs.map +0 -1
  106. package/dist/admin/src/services/axios.d.ts +0 -29
  107. package/dist/shared/validation-schemas.d.ts +0 -2
  108. package/dist/shared/validation-schemas.d.ts.map +0 -1
  109. 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,6 +70,23 @@ 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 = {
@@ -79,16 +95,13 @@ const ALLOWED_WEBHOOK_EVENTS = {
79
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 }) => {
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 }) => {
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
  }
@@ -278,9 +336,42 @@ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: conte
278
336
  }
279
337
  }
280
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
+ };
281
371
  const register = async ({ strapi: strapi2 }) => {
282
372
  if (strapi2.ee.features.isEnabled("cms-content-releases")) {
283
373
  await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
374
+ strapi2.db.migrations.providers.internal.register(addEntryDocumentToReleaseActions);
284
375
  strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
285
376
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
286
377
  }
@@ -290,6 +381,105 @@ const register = async ({ strapi: strapi2 }) => {
290
381
  graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
291
382
  }
292
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
+ };
293
483
  const bootstrap = async ({ strapi: strapi2 }) => {
294
484
  if (strapi2.ee.features.isEnabled("cms-content-releases")) {
295
485
  const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
@@ -297,115 +487,29 @@ const bootstrap = async ({ strapi: strapi2 }) => {
297
487
  );
298
488
  strapi2.db.lifecycles.subscribe({
299
489
  models: contentTypesWithDraftAndPublish,
300
- async afterDelete(event) {
301
- try {
302
- const { model, result } = event;
303
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
304
- const { id } = result;
305
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
306
- where: {
307
- actions: {
308
- target_type: model.uid,
309
- target_id: id
310
- }
311
- }
312
- });
313
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
314
- where: {
315
- target_type: model.uid,
316
- target_id: id
317
- }
318
- });
319
- for (const release2 of releases) {
320
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
321
- }
322
- }
323
- } catch (error) {
324
- strapi2.log.error("Error while deleting release actions after entry delete", { error });
325
- }
326
- },
327
- /**
328
- * deleteMany hook doesn't return the deleted entries ids
329
- * so we need to fetch them before deleting the entries to save the ids on our state
330
- */
331
- async beforeDeleteMany(event) {
332
- const { model, params } = event;
333
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
334
- const { where } = params;
335
- const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
336
- event.state.entriesToDelete = entriesToDelete;
337
- }
338
- },
339
490
  /**
340
- * We delete the release actions related to deleted entries
341
- * 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
342
492
  */
343
493
  async afterDeleteMany(event) {
344
494
  try {
345
- const { model, state } = event;
346
- const entriesToDelete = state.entriesToDelete;
347
- if (entriesToDelete) {
348
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
349
- where: {
350
- actions: {
351
- target_type: model.uid,
352
- target_id: {
353
- $in: entriesToDelete.map((entry) => entry.id)
354
- }
355
- }
356
- }
357
- });
358
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
359
- where: {
360
- target_type: model.uid,
361
- target_id: {
362
- $in: entriesToDelete.map((entry) => entry.id)
363
- }
364
- }
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 }
365
502
  });
366
- for (const release2 of releases) {
367
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
368
- }
369
503
  }
370
504
  } catch (error) {
371
505
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
372
506
  error
373
507
  });
374
508
  }
375
- },
376
- async afterUpdate(event) {
377
- try {
378
- const { model, result } = event;
379
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
380
- const isEntryValid = await getEntryValidStatus(model.uid, result, {
381
- strapi: strapi2
382
- });
383
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
384
- where: {
385
- target_type: model.uid,
386
- target_id: result.id
387
- },
388
- data: {
389
- isEntryValid
390
- }
391
- });
392
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
393
- where: {
394
- actions: {
395
- target_type: model.uid,
396
- target_id: result.id
397
- }
398
- }
399
- });
400
- for (const release2 of releases) {
401
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
402
- }
403
- }
404
- } catch (error) {
405
- strapi2.log.error("Error while updating release actions after entry update", { error });
406
- }
407
509
  }
408
510
  });
511
+ strapi2.documents.use(deleteActionsOnDelete);
512
+ strapi2.documents.use(updateActionsOnUpdate);
409
513
  getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
410
514
  strapi2.log.error(
411
515
  "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
@@ -497,15 +601,13 @@ const schema = {
497
601
  enum: ["publish", "unpublish"],
498
602
  required: true
499
603
  },
500
- entry: {
501
- type: "relation",
502
- relation: "morphToOne",
503
- configurable: false
504
- },
505
604
  contentType: {
506
605
  type: "string",
507
606
  required: true
508
607
  },
608
+ entryDocumentId: {
609
+ type: "string"
610
+ },
509
611
  locale: {
510
612
  type: "string"
511
613
  },
@@ -527,18 +629,6 @@ const contentTypes = {
527
629
  release: release$1,
528
630
  "release-action": releaseAction$1
529
631
  };
530
- const getGroupName = (queryValue) => {
531
- switch (queryValue) {
532
- case "contentType":
533
- return "contentType.displayName";
534
- case "action":
535
- return "type";
536
- case "locale":
537
- return ___default.default.getOr("No locale", "locale.name");
538
- default:
539
- return "contentType.displayName";
540
- }
541
- };
542
632
  const createReleaseService = ({ strapi: strapi2 }) => {
543
633
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
544
634
  strapi2.eventHub.emit(event, {
@@ -547,93 +637,32 @@ const createReleaseService = ({ strapi: strapi2 }) => {
547
637
  release: release2
548
638
  });
549
639
  };
550
- const publishSingleTypeAction = async (uid, actionType, entryId) => {
551
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
552
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
553
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
554
- const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
555
- try {
556
- if (actionType === "publish") {
557
- await entityManagerService.publish(entry, uid);
558
- } else {
559
- await entityManagerService.unpublish(entry, uid);
560
- }
561
- } catch (error) {
562
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
563
- ;
564
- else {
565
- throw error;
566
- }
567
- }
568
- };
569
- const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
570
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
571
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
572
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
573
- const entriesToPublish = await strapi2.entityService.findMany(uid, {
574
- filters: {
575
- id: {
576
- $in: entriesToPublishIds
577
- }
578
- },
579
- populate
580
- });
581
- const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
582
- filters: {
583
- id: {
584
- $in: entriestoUnpublishIds
585
- }
586
- },
587
- populate
588
- });
589
- if (entriesToPublish.length > 0) {
590
- await entityManagerService.publishMany(entriesToPublish, uid);
591
- }
592
- if (entriesToUnpublish.length > 0) {
593
- await entityManagerService.unpublishMany(entriesToUnpublish, uid);
594
- }
595
- };
596
640
  const getFormattedActions = async (releaseId) => {
597
641
  const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
598
642
  where: {
599
643
  release: {
600
644
  id: releaseId
601
645
  }
602
- },
603
- populate: {
604
- entry: {
605
- fields: ["id"]
606
- }
607
646
  }
608
647
  });
609
648
  if (actions.length === 0) {
610
649
  throw new utils.errors.ValidationError("No entries to publish");
611
650
  }
612
- const collectionTypeActions = {};
613
- const singleTypeActions = [];
651
+ const formattedActions = {};
614
652
  for (const action of actions) {
615
653
  const contentTypeUid = action.contentType;
616
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
617
- if (!collectionTypeActions[contentTypeUid]) {
618
- collectionTypeActions[contentTypeUid] = {
619
- entriesToPublishIds: [],
620
- entriesToUnpublishIds: []
621
- };
622
- }
623
- if (action.type === "publish") {
624
- collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
625
- } else {
626
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
627
- }
628
- } else {
629
- singleTypeActions.push({
630
- uid: contentTypeUid,
631
- action: action.type,
632
- id: action.entry.id
633
- });
654
+ if (!formattedActions[contentTypeUid]) {
655
+ formattedActions[contentTypeUid] = {
656
+ publish: [],
657
+ unpublish: []
658
+ };
634
659
  }
660
+ formattedActions[contentTypeUid][action.type].push({
661
+ documentId: action.entryDocumentId,
662
+ locale: action.locale
663
+ });
635
664
  }
636
- return { collectionTypeActions, singleTypeActions };
665
+ return formattedActions;
637
666
  };
638
667
  return {
639
668
  async create(releaseData, { user }) {
@@ -680,91 +709,10 @@ const createReleaseService = ({ strapi: strapi2 }) => {
680
709
  }
681
710
  });
682
711
  },
683
- async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
684
- let entries = entriesIds;
685
- if (!Array.isArray(entriesIds)) {
686
- entries = [entriesIds];
687
- }
688
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
689
- where: {
690
- actions: {
691
- target_type: contentTypeUid,
692
- target_id: {
693
- $in: entries
694
- }
695
- },
696
- releasedAt: {
697
- $null: true
698
- }
699
- },
700
- populate: {
701
- // Filter the action to get only the content type entry
702
- actions: {
703
- where: {
704
- target_type: contentTypeUid,
705
- target_id: {
706
- $in: entries
707
- }
708
- },
709
- populate: {
710
- entry: {
711
- select: ["id"]
712
- }
713
- }
714
- }
715
- }
716
- });
717
- return releases.map((release2) => {
718
- if (release2.actions?.length) {
719
- const actionsForEntry = release2.actions;
720
- delete release2.actions;
721
- return {
722
- ...release2,
723
- actions: actionsForEntry
724
- };
725
- }
726
- return release2;
727
- });
728
- },
729
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
730
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
731
- where: {
732
- releasedAt: {
733
- $null: true
734
- },
735
- actions: {
736
- target_type: contentTypeUid,
737
- target_id: entryId
738
- }
739
- }
740
- });
741
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
742
- where: {
743
- $or: [
744
- {
745
- id: {
746
- $notIn: releasesRelated.map((release2) => release2.id)
747
- }
748
- },
749
- {
750
- actions: null
751
- }
752
- ],
753
- releasedAt: {
754
- $null: true
755
- }
756
- }
757
- });
758
- return releases.map((release2) => {
759
- if (release2.actions?.length) {
760
- const [actionForEntry] = release2.actions;
761
- delete release2.actions;
762
- return {
763
- ...release2,
764
- action: actionForEntry
765
- };
766
- }
767
- 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
768
716
  });
769
717
  },
770
718
  async update(id, releaseData, { user }) {
@@ -800,14 +748,218 @@ const createReleaseService = ({ strapi: strapi2 }) => {
800
748
  strapi2.telemetry.send("didUpdateContentRelease");
801
749
  return updatedRelease;
802
750
  },
803
- async createAction(releaseId, action) {
804
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
751
+ async getAllComponents() {
752
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
753
+ const components = await contentManagerComponentsService.findAllComponents();
754
+ const componentsMap = components.reduce(
755
+ (acc, component) => {
756
+ acc[component.uid] = component;
757
+ return acc;
758
+ },
759
+ {}
760
+ );
761
+ return componentsMap;
762
+ },
763
+ async delete(releaseId) {
764
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
765
+ where: { id: releaseId },
766
+ populate: {
767
+ actions: {
768
+ select: ["id"]
769
+ }
770
+ }
771
+ });
772
+ if (!release2) {
773
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
774
+ }
775
+ if (release2.releasedAt) {
776
+ throw new utils.errors.ValidationError("Release already published");
777
+ }
778
+ await strapi2.db.transaction(async () => {
779
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
780
+ where: {
781
+ id: {
782
+ $in: release2.actions.map((action) => action.id)
783
+ }
784
+ }
785
+ });
786
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
787
+ where: {
788
+ id: releaseId
789
+ }
790
+ });
791
+ });
792
+ if (release2.scheduledAt) {
793
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
794
+ await schedulingService.cancel(release2.id);
795
+ }
796
+ strapi2.telemetry.send("didDeleteContentRelease");
797
+ return release2;
798
+ },
799
+ async publish(releaseId) {
800
+ const {
801
+ release: release2,
802
+ error
803
+ } = await strapi2.db.transaction(async ({ trx }) => {
804
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
805
+ if (!lockedRelease) {
806
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
807
+ }
808
+ if (lockedRelease.releasedAt) {
809
+ throw new utils.errors.ValidationError("Release already published");
810
+ }
811
+ if (lockedRelease.status === "failed") {
812
+ throw new utils.errors.ValidationError("Release failed to publish");
813
+ }
814
+ try {
815
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
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
+ )
828
+ );
829
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
830
+ where: {
831
+ id: releaseId
832
+ },
833
+ data: {
834
+ status: "done",
835
+ releasedAt: /* @__PURE__ */ new Date()
836
+ }
837
+ });
838
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
839
+ isPublished: true,
840
+ release: release22
841
+ });
842
+ strapi2.telemetry.send("didPublishContentRelease");
843
+ return { release: release22, error: null };
844
+ } catch (error2) {
845
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
846
+ isPublished: false,
847
+ error: error2
848
+ });
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", {
805
949
  strapi: strapi2
806
950
  });
807
951
  await Promise.all([
808
- validateEntryContentType(action.entry.contentType),
952
+ validateEntryData(action.contentType, action.entryDocumentId),
809
953
  validateUniqueEntry(releaseId, action)
810
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
+ }
811
963
  const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
812
964
  if (!release2) {
813
965
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
@@ -815,28 +967,30 @@ const createReleaseService = ({ strapi: strapi2 }) => {
815
967
  if (release2.releasedAt) {
816
968
  throw new utils.errors.ValidationError("Release already published");
817
969
  }
818
- const { entry, type } = action;
819
- const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
820
- const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
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;
821
980
  const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
822
981
  data: {
823
- type,
824
- contentType: entry.contentType,
825
- locale: entry.locale,
826
- isEntryValid,
827
- entry: {
828
- id: entry.id,
829
- __type: entry.contentType,
830
- __pivot: { field: "entry" }
831
- },
832
- release: releaseId
982
+ ...action,
983
+ release: release2.id,
984
+ isEntryValid: actionStatus
833
985
  },
834
- populate: { release: { select: ["id"] }, entry: { select: ["id"] } }
986
+ populate: { release: { select: ["id"] } }
835
987
  });
836
- this.updateReleaseStatus(releaseId);
988
+ if (!disableUpdateReleaseStatus) {
989
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
990
+ }
837
991
  return releaseAction2;
838
992
  },
839
- async findActions(releaseId, query) {
993
+ async findPage(releaseId, query) {
840
994
  const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
841
995
  where: { id: releaseId },
842
996
  select: ["id"]
@@ -845,21 +999,35 @@ const createReleaseService = ({ strapi: strapi2 }) => {
845
999
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
846
1000
  }
847
1001
  const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
848
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
1002
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
849
1003
  ...dbQuery,
850
- populate: {
851
- entry: {
852
- populate: "*"
853
- }
854
- },
855
1004
  where: {
856
1005
  release: releaseId
857
1006
  }
858
1007
  });
859
- },
860
- async countActions(query) {
861
- const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
862
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
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
+ };
863
1031
  },
864
1032
  async groupActions(actions, groupBy) {
865
1033
  const contentTypeUids = actions.reduce((acc, action) => {
@@ -868,8 +1036,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
868
1036
  }
869
1037
  return acc;
870
1038
  }, []);
871
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(contentTypeUids);
872
- const allLocalesDictionary = await this.getLocalesDataForActions();
1039
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1040
+ const allLocalesDictionary = await getLocalesDataForActions();
873
1041
  const formattedData = actions.map((action) => {
874
1042
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
875
1043
  return {
@@ -881,165 +1049,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
881
1049
  uid: action.contentType
882
1050
  }
883
1051
  };
884
- });
885
- const groupName = getGroupName(groupBy);
886
- return ___default.default.groupBy(groupName)(formattedData);
887
- },
888
- async getLocalesDataForActions() {
889
- if (!strapi2.plugin("i18n")) {
890
- return {};
891
- }
892
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
893
- return allLocales.reduce((acc, locale) => {
894
- acc[locale.code] = { name: locale.name, code: locale.code };
895
- return acc;
896
- }, {});
897
- },
898
- async getContentTypesDataForActions(contentTypesUids) {
899
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
900
- const contentTypesData = {};
901
- for (const contentTypeUid of contentTypesUids) {
902
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
903
- uid: contentTypeUid
904
- });
905
- contentTypesData[contentTypeUid] = {
906
- mainField: contentTypeConfig.settings.mainField,
907
- displayName: strapi2.getModel(contentTypeUid).info.displayName
908
- };
909
- }
910
- return contentTypesData;
911
- },
912
- getContentTypeModelsFromActions(actions) {
913
- const contentTypeUids = actions.reduce((acc, action) => {
914
- if (!acc.includes(action.contentType)) {
915
- acc.push(action.contentType);
916
- }
917
- return acc;
918
- }, []);
919
- const contentTypeModelsMap = contentTypeUids.reduce(
920
- (acc, contentTypeUid) => {
921
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
922
- return acc;
923
- },
924
- {}
925
- );
926
- return contentTypeModelsMap;
927
- },
928
- async getAllComponents() {
929
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
930
- const components = await contentManagerComponentsService.findAllComponents();
931
- const componentsMap = components.reduce(
932
- (acc, component) => {
933
- acc[component.uid] = component;
934
- return acc;
935
- },
936
- {}
937
- );
938
- return componentsMap;
939
- },
940
- async delete(releaseId) {
941
- const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
942
- where: { id: releaseId },
943
- populate: {
944
- actions: {
945
- select: ["id"]
946
- }
947
- }
948
- });
949
- if (!release2) {
950
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
951
- }
952
- if (release2.releasedAt) {
953
- throw new utils.errors.ValidationError("Release already published");
954
- }
955
- await strapi2.db.transaction(async () => {
956
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
957
- where: {
958
- id: {
959
- $in: release2.actions.map((action) => action.id)
960
- }
961
- }
962
- });
963
- await strapi2.db.query(RELEASE_MODEL_UID).delete({
964
- where: {
965
- id: releaseId
966
- }
967
- });
968
- });
969
- if (release2.scheduledAt) {
970
- const schedulingService = getService("scheduling", { strapi: strapi2 });
971
- await schedulingService.cancel(release2.id);
972
- }
973
- strapi2.telemetry.send("didDeleteContentRelease");
974
- return release2;
1052
+ });
1053
+ const groupName = getGroupName(groupBy);
1054
+ return ___default.default.groupBy(groupName)(formattedData);
975
1055
  },
976
- async publish(releaseId) {
977
- const {
978
- release: release2,
979
- error
980
- } = await strapi2.db.transaction(async ({ trx }) => {
981
- const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
982
- if (!lockedRelease) {
983
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
984
- }
985
- if (lockedRelease.releasedAt) {
986
- throw new utils.errors.ValidationError("Release already published");
987
- }
988
- if (lockedRelease.status === "failed") {
989
- throw new utils.errors.ValidationError("Release failed to publish");
1056
+ async getContentTypeModelsFromActions(actions) {
1057
+ const contentTypeUids = actions.reduce((acc, action) => {
1058
+ if (!acc.includes(action.contentType)) {
1059
+ acc.push(action.contentType);
990
1060
  }
991
- try {
992
- strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
993
- const { collectionTypeActions, singleTypeActions } = await getFormattedActions(releaseId);
994
- await strapi2.db.transaction(async () => {
995
- for (const { uid, action, id } of singleTypeActions) {
996
- await publishSingleTypeAction(uid, action, id);
997
- }
998
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
999
- const uid = contentTypeUid;
1000
- await publishCollectionTypeAction(
1001
- uid,
1002
- collectionTypeActions[uid].entriesToPublishIds,
1003
- collectionTypeActions[uid].entriesToUnpublishIds
1004
- );
1005
- }
1006
- });
1007
- const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
1008
- where: {
1009
- id: releaseId
1010
- },
1011
- data: {
1012
- status: "done",
1013
- releasedAt: /* @__PURE__ */ new Date()
1014
- }
1015
- });
1016
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1017
- isPublished: true,
1018
- release: release22
1019
- });
1020
- strapi2.telemetry.send("didPublishContentRelease");
1021
- return { release: release22, error: null };
1022
- } catch (error2) {
1023
- dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1024
- isPublished: false,
1025
- error: error2
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"
1026
1070
  });
1027
- await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1028
- status: "failed"
1029
- }).transacting(trx).execute();
1030
- return {
1031
- release: null,
1032
- error: error2
1071
+ acc[contentTypeUid] = {
1072
+ ...contentTypeModel,
1073
+ hasReviewWorkflow: !!workflow,
1074
+ stageRequiredToPublish: workflow?.stageRequiredToPublish
1033
1075
  };
1034
- }
1035
- });
1036
- if (error instanceof Error) {
1037
- throw error;
1038
- }
1039
- return release2;
1076
+ return acc;
1077
+ },
1078
+ {}
1079
+ );
1080
+ return contentTypeModelsMap;
1040
1081
  },
1041
- async updateAction(actionId, releaseId, update) {
1042
- 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({
1043
1088
  where: {
1044
1089
  id: actionId,
1045
1090
  release: {
@@ -1048,17 +1093,42 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1048
1093
  $null: true
1049
1094
  }
1050
1095
  }
1051
- },
1052
- data: update
1096
+ }
1053
1097
  });
1054
- if (!updatedAction) {
1098
+ if (!action) {
1055
1099
  throw new utils.errors.NotFoundError(
1056
1100
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1057
1101
  );
1058
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);
1059
1129
  return updatedAction;
1060
1130
  },
1061
- async deleteAction(actionId, releaseId) {
1131
+ async delete(actionId, releaseId) {
1062
1132
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1063
1133
  where: {
1064
1134
  id: actionId,
@@ -1075,51 +1145,56 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1075
1145
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1076
1146
  );
1077
1147
  }
1078
- this.updateReleaseStatus(releaseId);
1148
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1079
1149
  return deletedAction;
1080
1150
  },
1081
- async updateReleaseStatus(releaseId) {
1082
- const [totalActions, invalidActions] = await Promise.all([
1083
- this.countActions({
1084
- filters: {
1085
- release: releaseId
1086
- }
1087
- }),
1088
- this.countActions({
1089
- filters: {
1090
- release: releaseId,
1091
- isEntryValid: false
1092
- }
1093
- })
1094
- ]);
1095
- if (totalActions > 0) {
1096
- if (invalidActions > 0) {
1097
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1098
- where: {
1099
- id: releaseId
1100
- },
1101
- data: {
1102
- 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
1103
1162
  }
1104
- });
1105
- }
1106
- 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({
1107
1178
  where: {
1108
- id: releaseId
1179
+ id: action.id
1109
1180
  },
1110
1181
  data: {
1111
- status: "ready"
1182
+ isEntryValid: isValid
1112
1183
  }
1113
1184
  });
1114
- }
1115
- return strapi2.db.query(RELEASE_MODEL_UID).update({
1116
- where: {
1117
- id: releaseId
1118
- },
1119
- data: {
1120
- status: "empty"
1185
+ if (!releasesUpdated.includes(action.release.id)) {
1186
+ releasesUpdated.push(action.release.id);
1121
1187
  }
1188
+ return {
1189
+ id: action.id,
1190
+ isEntryValid: isValid
1191
+ };
1122
1192
  });
1193
+ if (releasesUpdated.length > 0) {
1194
+ await utils.async.map(releasesUpdated, async (releaseId) => {
1195
+ await getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
1196
+ });
1197
+ }
1123
1198
  }
1124
1199
  };
1125
1200
  };
@@ -1135,30 +1210,35 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1135
1210
  where: {
1136
1211
  id: releaseId
1137
1212
  },
1138
- populate: { actions: { populate: { entry: { select: ["id"] } } } }
1213
+ populate: {
1214
+ actions: true
1215
+ }
1139
1216
  });
1140
1217
  if (!release2) {
1141
1218
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1142
1219
  }
1143
1220
  const isEntryInRelease = release2.actions.some(
1144
- (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)
1145
1222
  );
1146
1223
  if (isEntryInRelease) {
1147
1224
  throw new AlreadyOnReleaseError(
1148
- `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}`
1149
1226
  );
1150
1227
  }
1151
1228
  },
1152
- validateEntryContentType(contentTypeUid) {
1229
+ validateEntryData(contentTypeUid, entryDocumentId) {
1153
1230
  const contentType = strapi2.contentType(contentTypeUid);
1154
1231
  if (!contentType) {
1155
1232
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1156
1233
  }
1157
- if (!contentType.options?.draftAndPublish) {
1234
+ if (!utils.contentTypes.hasDraftAndPublish(contentType)) {
1158
1235
  throw new utils.errors.ValidationError(
1159
1236
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1160
1237
  );
1161
1238
  }
1239
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1240
+ throw new utils.errors.ValidationError("Document id is required for collection type");
1241
+ }
1162
1242
  },
1163
1243
  async validatePendingReleasesLimit() {
1164
1244
  const featureCfg = strapi2.ee.features.get("cms-content-releases");
@@ -1247,78 +1327,165 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1247
1327
  }
1248
1328
  };
1249
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
+ };
1250
1351
  const services = {
1251
1352
  release: createReleaseService,
1353
+ "release-action": createReleaseActionService,
1252
1354
  "release-validation": createReleaseValidationService,
1253
- scheduling: createSchedulingService
1355
+ scheduling: createSchedulingService,
1356
+ settings: createSettingsService
1254
1357
  };
1255
- const RELEASE_SCHEMA = yup__namespace.object().shape({
1256
- name: yup__namespace.string().trim().required(),
1257
- scheduledAt: yup__namespace.string().nullable(),
1258
- isScheduled: yup__namespace.boolean().optional(),
1259
- time: yup__namespace.string().when("isScheduled", {
1260
- is: true,
1261
- then: yup__namespace.string().trim().required(),
1262
- otherwise: yup__namespace.string().nullable()
1263
- }),
1264
- timezone: yup__namespace.string().when("isScheduled", {
1265
- is: true,
1266
- then: yup__namespace.string().required().nullable(),
1267
- otherwise: yup__namespace.string().nullable()
1268
- }),
1269
- date: yup__namespace.string().when("isScheduled", {
1270
- is: true,
1271
- then: yup__namespace.string().required().nullable(),
1272
- 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()
1273
1365
  })
1274
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();
1275
1373
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
1374
+ const validatefindByDocumentAttachedParams = utils.validateYupSchema(
1375
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1376
+ );
1276
1377
  const releaseController = {
1277
- async findMany(ctx) {
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) {
1278
1384
  const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1279
1385
  ability: ctx.state.userAbility,
1280
1386
  model: RELEASE_MODEL_UID
1281
1387
  });
1282
1388
  await permissionsManager.validateQuery(ctx.query);
1283
1389
  const releaseService = getService("release", { strapi });
1284
- const isFindManyForContentTypeEntry = Boolean(ctx.query?.contentTypeUid && ctx.query?.entryId);
1285
- if (isFindManyForContentTypeEntry) {
1286
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1287
- const contentTypeUid = query.contentTypeUid;
1288
- const entryId = query.entryId;
1289
- const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
1290
- const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
1291
- ctx.body = { data };
1292
- } else {
1293
- const query = await permissionsManager.sanitizeQuery(ctx.query);
1294
- const { results, pagination } = await releaseService.findPage(query);
1295
- const data = results.map((release2) => {
1296
- const { actions, ...releaseData } = release2;
1297
- return {
1298
- ...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: {
1299
1413
  actions: {
1300
- meta: {
1301
- count: actions.count
1414
+ fields: ["type"],
1415
+ filters: {
1416
+ contentType,
1417
+ entryDocumentId: entryDocumentId ?? null,
1418
+ locale: locale ?? null
1302
1419
  }
1303
1420
  }
1304
- };
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
+ }
1305
1434
  });
1306
- const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1435
+ const releases = await releaseService.findMany({
1307
1436
  where: {
1437
+ $or: [
1438
+ {
1439
+ id: {
1440
+ $notIn: relatedReleases.map((release2) => release2.id)
1441
+ }
1442
+ },
1443
+ {
1444
+ actions: null
1445
+ }
1446
+ ],
1308
1447
  releasedAt: null
1309
1448
  }
1310
1449
  });
1311
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1450
+ ctx.body = { data: releases };
1312
1451
  }
1313
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
+ },
1314
1480
  async findOne(ctx) {
1315
1481
  const id = ctx.params.id;
1316
1482
  const releaseService = getService("release", { strapi });
1483
+ const releaseActionService = getService("release-action", { strapi });
1317
1484
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1318
1485
  if (!release2) {
1319
1486
  throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1320
1487
  }
1321
- const count = await releaseService.countActions({
1488
+ const count = await releaseActionService.countActions({
1322
1489
  filters: {
1323
1490
  release: id
1324
1491
  }
@@ -1338,28 +1505,43 @@ const releaseController = {
1338
1505
  ctx.body = { data };
1339
1506
  },
1340
1507
  async mapEntriesToReleases(ctx) {
1341
- const { contentTypeUid, entriesIds } = ctx.query;
1342
- if (!contentTypeUid || !entriesIds) {
1508
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1509
+ if (!contentTypeUid || !documentIds) {
1343
1510
  throw new utils.errors.ValidationError("Missing required query parameters");
1344
1511
  }
1345
1512
  const releaseService = getService("release", { strapi });
1346
- const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1347
- contentTypeUid,
1348
- entriesIds
1349
- );
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
+ });
1350
1528
  const mappedEntriesInReleases = releasesWithActions.reduce(
1351
- // TODO: Fix for v5 removed mappedEntriedToRelease
1352
1529
  (acc, release2) => {
1353
1530
  release2.actions.forEach((action) => {
1354
- if (!acc[action.entry.id]) {
1355
- acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
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 }];
1356
1539
  } else {
1357
- acc[action.entry.id].push({ id: release2.id, name: release2.name });
1540
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1358
1541
  }
1359
1542
  });
1360
1543
  return acc;
1361
1544
  },
1362
- // TODO: Fix for v5 removed mappedEntriedToRelease
1363
1545
  {}
1364
1546
  );
1365
1547
  ctx.body = {
@@ -1404,18 +1586,18 @@ const releaseController = {
1404
1586
  };
1405
1587
  },
1406
1588
  async publish(ctx) {
1407
- const user = ctx.state.user;
1408
1589
  const id = ctx.params.id;
1409
1590
  const releaseService = getService("release", { strapi });
1410
- const release2 = await releaseService.publish(id, { user });
1591
+ const releaseActionService = getService("release-action", { strapi });
1592
+ const release2 = await releaseService.publish(id);
1411
1593
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1412
- releaseService.countActions({
1594
+ releaseActionService.countActions({
1413
1595
  filters: {
1414
1596
  release: id,
1415
1597
  type: "publish"
1416
1598
  }
1417
1599
  }),
1418
- releaseService.countActions({
1600
+ releaseActionService.countActions({
1419
1601
  filters: {
1420
1602
  release: id,
1421
1603
  type: "unpublish"
@@ -1433,24 +1615,27 @@ const releaseController = {
1433
1615
  }
1434
1616
  };
1435
1617
  const RELEASE_ACTION_SCHEMA = utils.yup.object().shape({
1436
- entry: utils.yup.object().shape({
1437
- id: utils.yup.strapiID().required(),
1438
- contentType: utils.yup.string().required()
1439
- }).required(),
1618
+ contentType: utils.yup.string().required(),
1619
+ entryDocumentId: utils.yup.strapiID(),
1620
+ locale: utils.yup.string(),
1440
1621
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1441
1622
  });
1442
1623
  const RELEASE_ACTION_UPDATE_SCHEMA = utils.yup.object().shape({
1443
1624
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1444
1625
  });
1626
+ const FIND_MANY_ACTIONS_PARAMS = utils.yup.object().shape({
1627
+ groupBy: utils.yup.string().oneOf(["action", "contentType", "locale"])
1628
+ });
1445
1629
  const validateReleaseAction = utils.validateYupSchema(RELEASE_ACTION_SCHEMA);
1446
1630
  const validateReleaseActionUpdateSchema = utils.validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1631
+ const validateFindManyActionsParams = utils.validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1447
1632
  const releaseActionController = {
1448
1633
  async create(ctx) {
1449
1634
  const releaseId = ctx.params.releaseId;
1450
1635
  const releaseActionArgs = ctx.request.body;
1451
1636
  await validateReleaseAction(releaseActionArgs);
1452
- const releaseService = getService("release", { strapi });
1453
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1637
+ const releaseActionService = getService("release-action", { strapi });
1638
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1454
1639
  ctx.created({
1455
1640
  data: releaseAction2
1456
1641
  });
@@ -1461,12 +1646,15 @@ const releaseActionController = {
1461
1646
  await Promise.all(
1462
1647
  releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1463
1648
  );
1649
+ const releaseActionService = getService("release-action", { strapi });
1464
1650
  const releaseService = getService("release", { strapi });
1465
1651
  const releaseActions = await strapi.db.transaction(async () => {
1466
1652
  const releaseActions2 = await Promise.all(
1467
1653
  releaseActionsArgs.map(async (releaseActionArgs) => {
1468
1654
  try {
1469
- const action = await releaseService.createAction(releaseId, releaseActionArgs);
1655
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1656
+ disableUpdateReleaseStatus: true
1657
+ });
1470
1658
  return action;
1471
1659
  } catch (error) {
1472
1660
  if (error instanceof AlreadyOnReleaseError) {
@@ -1479,6 +1667,9 @@ const releaseActionController = {
1479
1667
  return releaseActions2;
1480
1668
  });
1481
1669
  const newReleaseActions = releaseActions.filter((action) => action !== null);
1670
+ if (newReleaseActions.length > 0) {
1671
+ releaseService.updateReleaseStatus(releaseId);
1672
+ }
1482
1673
  ctx.created({
1483
1674
  data: newReleaseActions,
1484
1675
  meta: {
@@ -1493,10 +1684,17 @@ const releaseActionController = {
1493
1684
  ability: ctx.state.userAbility,
1494
1685
  model: RELEASE_ACTION_MODEL_UID
1495
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;
1496
1695
  const query = await permissionsManager.sanitizeQuery(ctx.query);
1497
- const releaseService = getService("release", { strapi });
1498
- const { results, pagination } = await releaseService.findActions(releaseId, {
1499
- sort: query.groupBy === "action" ? "type" : query.groupBy,
1696
+ const releaseActionService = getService("release-action", { strapi });
1697
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1500
1698
  ...query
1501
1699
  });
1502
1700
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
@@ -1512,10 +1710,11 @@ const releaseActionController = {
1512
1710
  }, {});
1513
1711
  const sanitizedResults = await utils.async.map(results, async (action) => ({
1514
1712
  ...action,
1515
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1713
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1516
1714
  }));
1517
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1518
- 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 });
1519
1718
  const components = await releaseService.getAllComponents();
1520
1719
  ctx.body = {
1521
1720
  data: groupedData,
@@ -1531,8 +1730,8 @@ const releaseActionController = {
1531
1730
  const releaseId = ctx.params.releaseId;
1532
1731
  const releaseActionUpdateArgs = ctx.request.body;
1533
1732
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1534
- const releaseService = getService("release", { strapi });
1535
- const updatedAction = await releaseService.updateAction(
1733
+ const releaseActionService = getService("release-action", { strapi });
1734
+ const updatedAction = await releaseActionService.update(
1536
1735
  actionId,
1537
1736
  releaseId,
1538
1737
  releaseActionUpdateArgs
@@ -1544,14 +1743,36 @@ const releaseActionController = {
1544
1743
  async delete(ctx) {
1545
1744
  const actionId = ctx.params.actionId;
1546
1745
  const releaseId = ctx.params.releaseId;
1547
- const releaseService = getService("release", { strapi });
1548
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1746
+ const releaseActionService = getService("release-action", { strapi });
1747
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1549
1748
  ctx.body = {
1550
1749
  data: deletedReleaseAction
1551
1750
  };
1552
1751
  }
1553
1752
  };
1554
- 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
+ };
1555
1776
  const release = {
1556
1777
  type: "admin",
1557
1778
  routes: [
@@ -1571,6 +1792,22 @@ const release = {
1571
1792
  ]
1572
1793
  }
1573
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
+ },
1574
1811
  {
1575
1812
  method: "POST",
1576
1813
  path: "/",
@@ -1590,7 +1827,7 @@ const release = {
1590
1827
  {
1591
1828
  method: "GET",
1592
1829
  path: "/",
1593
- handler: "release.findMany",
1830
+ handler: "release.findPage",
1594
1831
  config: {
1595
1832
  policies: [
1596
1833
  "admin::isAuthenticatedAdmin",
@@ -1754,7 +1991,45 @@ const releaseAction = {
1754
1991
  }
1755
1992
  ]
1756
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
+ };
1757
2031
  const routes = {
2032
+ settings,
1758
2033
  release,
1759
2034
  "release-action": releaseAction
1760
2035
  };