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

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