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

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