@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
@@ -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,242 @@ 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 hasTable = await trx.schema.hasTable("strapi_release_actions");
333
+ if (!hasTable) {
334
+ return;
335
+ }
336
+ const hasPolymorphicColumn = await trx.schema.hasColumn("strapi_release_actions", "target_id");
337
+ if (hasPolymorphicColumn) {
338
+ const hasEntryDocumentIdColumn = await trx.schema.hasColumn(
339
+ "strapi_release_actions",
340
+ "entry_document_id"
341
+ );
342
+ if (!hasEntryDocumentIdColumn) {
343
+ await trx.schema.alterTable("strapi_release_actions", (table) => {
344
+ table.string("entry_document_id");
345
+ });
346
+ }
347
+ const releaseActions = await trx.select("*").from("strapi_release_actions");
348
+ utils.async.map(releaseActions, async (action) => {
349
+ const { target_type, target_id } = action;
350
+ const entry = await db.query(target_type).findOne({ where: { id: target_id } });
351
+ if (entry) {
352
+ await trx("strapi_release_actions").update({ entry_document_id: entry.documentId }).where("id", action.id);
353
+ }
354
+ });
355
+ }
356
+ },
357
+ async down() {
358
+ throw new Error("not implemented");
359
+ }
360
+ };
212
361
  const register = async ({ strapi: strapi2 }) => {
213
362
  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);
363
+ await strapi2.service("admin::permission").actionProvider.registerMany(ACTIONS);
364
+ strapi2.db.migrations.providers.internal.register(addEntryDocumentToReleaseActions);
365
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
366
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
367
+ }
368
+ if (strapi2.plugin("graphql")) {
369
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
370
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
371
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
372
+ }
373
+ };
374
+ const updateActionsStatusAndUpdateReleaseStatus = async (contentType, entry) => {
375
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
376
+ where: {
377
+ actions: {
378
+ contentType,
379
+ entryDocumentId: entry.documentId,
380
+ locale: entry.locale
381
+ }
382
+ }
383
+ });
384
+ const entryStatus = await isEntryValid(contentType, entry, { strapi });
385
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
386
+ where: {
387
+ contentType,
388
+ entryDocumentId: entry.documentId,
389
+ locale: entry.locale
390
+ },
391
+ data: {
392
+ isEntryValid: entryStatus
393
+ }
394
+ });
395
+ for (const release2 of releases) {
396
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
397
+ }
398
+ };
399
+ const deleteActionsAndUpdateReleaseStatus = async (params) => {
400
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
401
+ where: {
402
+ actions: params
403
+ }
404
+ });
405
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
406
+ where: params
407
+ });
408
+ for (const release2 of releases) {
409
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
410
+ }
411
+ };
412
+ const deleteActionsOnDelete = async (ctx, next) => {
413
+ if (ctx.action !== "delete") {
414
+ return next();
415
+ }
416
+ if (!utils.contentTypes.hasDraftAndPublish(ctx.contentType)) {
417
+ return next();
418
+ }
419
+ const contentType = ctx.contentType.uid;
420
+ const { documentId, locale } = ctx.params;
421
+ const result = await next();
422
+ if (!result) {
423
+ return result;
424
+ }
425
+ try {
426
+ deleteActionsAndUpdateReleaseStatus({
427
+ contentType,
428
+ entryDocumentId: documentId,
429
+ ...locale !== "*" && { locale }
430
+ });
431
+ } catch (error) {
432
+ strapi.log.error("Error while deleting release actions after delete", {
433
+ error
434
+ });
435
+ }
436
+ return result;
437
+ };
438
+ const updateActionsOnUpdate = async (ctx, next) => {
439
+ if (ctx.action !== "update") {
440
+ return next();
441
+ }
442
+ if (!utils.contentTypes.hasDraftAndPublish(ctx.contentType)) {
443
+ return next();
444
+ }
445
+ const contentType = ctx.contentType.uid;
446
+ const result = await next();
447
+ if (!result) {
448
+ return result;
449
+ }
450
+ try {
451
+ updateActionsStatusAndUpdateReleaseStatus(contentType, result);
452
+ } catch (error) {
453
+ strapi.log.error("Error while updating release actions after update", {
454
+ error
455
+ });
456
+ }
457
+ return result;
458
+ };
459
+ const deleteReleasesActionsAndUpdateReleaseStatus = async (params) => {
460
+ const releases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
461
+ where: {
462
+ actions: params
463
+ }
464
+ });
465
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
466
+ where: params
467
+ });
468
+ for (const release2 of releases) {
469
+ getService("release", { strapi }).updateReleaseStatus(release2.id);
216
470
  }
217
471
  };
218
472
  const bootstrap = async ({ strapi: strapi2 }) => {
219
473
  if (strapi2.ee.features.isEnabled("cms-content-releases")) {
220
- const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes);
474
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
475
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
476
+ );
221
477
  strapi2.db.lifecycles.subscribe({
222
478
  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
- /**
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
479
  /**
263
- * We delete the release actions related to deleted entries
264
- * We make this only after deleteMany is succesfully executed to avoid errors
480
+ * deleteMany is still used outside documents service, for example when deleting a locale
265
481
  */
266
482
  async afterDeleteMany(event) {
267
483
  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
- }
484
+ const model = strapi2.getModel(event.model.uid);
485
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
486
+ const { where } = event.params;
487
+ deleteReleasesActionsAndUpdateReleaseStatus({
488
+ contentType: model.uid,
489
+ locale: where.locale ?? null,
490
+ ...where.documentId && { entryDocumentId: where.documentId }
290
491
  });
291
- for (const release2 of releases) {
292
- getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
293
- }
294
492
  }
295
493
  } catch (error) {
296
494
  strapi2.log.error("Error while deleting release actions after entry deleteMany", {
297
495
  error
298
496
  });
299
497
  }
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
498
  }
337
499
  });
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
- }
500
+ strapi2.documents.use(deleteActionsOnDelete);
501
+ strapi2.documents.use(updateActionsOnUpdate);
502
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
503
+ strapi2.log.error(
504
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
505
+ );
506
+ throw err;
507
+ });
508
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
509
+ strapi2.get("webhookStore").addAllowedEvent(key, value);
510
+ });
349
511
  }
350
512
  };
351
513
  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
- }
514
+ const scheduledJobs = getService("scheduling", {
515
+ strapi: strapi2
516
+ }).getAll();
517
+ for (const [, job] of scheduledJobs) {
518
+ job.cancel();
359
519
  }
360
520
  };
361
521
  const schema$1 = {
@@ -430,15 +590,13 @@ const schema = {
430
590
  enum: ["publish", "unpublish"],
431
591
  required: true
432
592
  },
433
- entry: {
434
- type: "relation",
435
- relation: "morphToOne",
436
- configurable: false
437
- },
438
593
  contentType: {
439
594
  type: "string",
440
595
  required: true
441
596
  },
597
+ entryDocumentId: {
598
+ type: "string"
599
+ },
442
600
  locale: {
443
601
  type: "string"
444
602
  },
@@ -460,18 +618,6 @@ const contentTypes = {
460
618
  release: release$1,
461
619
  "release-action": releaseAction$1
462
620
  };
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
621
  const createReleaseService = ({ strapi: strapi2 }) => {
476
622
  const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
477
623
  strapi2.eventHub.emit(event, {
@@ -480,6 +626,33 @@ const createReleaseService = ({ strapi: strapi2 }) => {
480
626
  release: release2
481
627
  });
482
628
  };
629
+ const getFormattedActions = async (releaseId) => {
630
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
631
+ where: {
632
+ release: {
633
+ id: releaseId
634
+ }
635
+ }
636
+ });
637
+ if (actions.length === 0) {
638
+ throw new utils.errors.ValidationError("No entries to publish");
639
+ }
640
+ const formattedActions = {};
641
+ for (const action of actions) {
642
+ const contentTypeUid = action.contentType;
643
+ if (!formattedActions[contentTypeUid]) {
644
+ formattedActions[contentTypeUid] = {
645
+ publish: [],
646
+ unpublish: []
647
+ };
648
+ }
649
+ formattedActions[contentTypeUid][action.type].push({
650
+ documentId: action.entryDocumentId,
651
+ locale: action.locale
652
+ });
653
+ }
654
+ return formattedActions;
655
+ };
483
656
  return {
484
657
  async create(releaseData, { user }) {
485
658
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -493,13 +666,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
493
666
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
494
667
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
495
668
  ]);
496
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
669
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
497
670
  data: {
498
671
  ...releaseWithCreatorFields,
499
672
  status: "empty"
500
673
  }
501
674
  });
502
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
675
+ if (releaseWithCreatorFields.scheduledAt) {
503
676
  const schedulingService = getService("scheduling", { strapi: strapi2 });
504
677
  await schedulingService.set(release2.id, release2.scheduledAt);
505
678
  }
@@ -507,94 +680,28 @@ const createReleaseService = ({ strapi: strapi2 }) => {
507
680
  return release2;
508
681
  },
509
682
  async findOne(id, query = {}) {
510
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
511
- ...query
683
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
684
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
685
+ ...dbQuery,
686
+ where: { id }
512
687
  });
513
688
  return release2;
514
689
  },
515
690
  findPage(query) {
516
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
517
- ...query,
691
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
692
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
693
+ ...dbQuery,
518
694
  populate: {
519
695
  actions: {
520
- // @ts-expect-error Ignore missing properties
521
696
  count: true
522
697
  }
523
698
  }
524
699
  });
525
700
  },
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;
701
+ findMany(query) {
702
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
703
+ return strapi2.db.query(RELEASE_MODEL_UID).findMany({
704
+ ...dbQuery
598
705
  });
599
706
  },
600
707
  async update(id, releaseData, { user }) {
@@ -609,90 +716,307 @@ const createReleaseService = ({ strapi: strapi2 }) => {
609
716
  validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
610
717
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
611
718
  ]);
612
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
719
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
613
720
  if (!release2) {
614
721
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
615
722
  }
616
723
  if (release2.releasedAt) {
617
724
  throw new utils.errors.ValidationError("Release already published");
618
725
  }
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
726
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
727
+ where: { id },
625
728
  data: releaseWithCreatorFields
626
729
  });
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
- }
730
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
731
+ if (releaseData.scheduledAt) {
732
+ await schedulingService.set(id, releaseData.scheduledAt);
733
+ } else if (release2.scheduledAt) {
734
+ schedulingService.cancel(id);
634
735
  }
635
736
  this.updateReleaseStatus(id);
636
737
  strapi2.telemetry.send("didUpdateContentRelease");
637
738
  return updatedRelease;
638
739
  },
639
- async createAction(releaseId, action) {
640
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
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.db.query(RELEASE_MODEL_UID).findOne({
754
+ where: { id: releaseId },
755
+ populate: {
756
+ actions: {
757
+ select: ["id"]
758
+ }
759
+ }
760
+ });
761
+ if (!release2) {
762
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
763
+ }
764
+ if (release2.releasedAt) {
765
+ throw new utils.errors.ValidationError("Release already published");
766
+ }
767
+ await strapi2.db.transaction(async () => {
768
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
769
+ where: {
770
+ id: {
771
+ $in: release2.actions.map((action) => action.id)
772
+ }
773
+ }
774
+ });
775
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
776
+ where: {
777
+ id: releaseId
778
+ }
779
+ });
780
+ });
781
+ if (release2.scheduledAt) {
782
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
783
+ await schedulingService.cancel(release2.id);
784
+ }
785
+ strapi2.telemetry.send("didDeleteContentRelease");
786
+ return release2;
787
+ },
788
+ async publish(releaseId) {
789
+ const {
790
+ release: release2,
791
+ error
792
+ } = await strapi2.db.transaction(async ({ trx }) => {
793
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
794
+ if (!lockedRelease) {
795
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
796
+ }
797
+ if (lockedRelease.releasedAt) {
798
+ throw new utils.errors.ValidationError("Release already published");
799
+ }
800
+ if (lockedRelease.status === "failed") {
801
+ throw new utils.errors.ValidationError("Release failed to publish");
802
+ }
803
+ try {
804
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
805
+ const formattedActions = await getFormattedActions(releaseId);
806
+ await strapi2.db.transaction(
807
+ async () => Promise.all(
808
+ Object.keys(formattedActions).map(async (contentTypeUid) => {
809
+ const contentType = contentTypeUid;
810
+ const { publish, unpublish } = formattedActions[contentType];
811
+ return Promise.all([
812
+ ...publish.map((params) => strapi2.documents(contentType).publish(params)),
813
+ ...unpublish.map((params) => strapi2.documents(contentType).unpublish(params))
814
+ ]);
815
+ })
816
+ )
817
+ );
818
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
819
+ where: {
820
+ id: releaseId
821
+ },
822
+ data: {
823
+ status: "done",
824
+ releasedAt: /* @__PURE__ */ new Date()
825
+ }
826
+ });
827
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
828
+ isPublished: true,
829
+ release: release22
830
+ });
831
+ strapi2.telemetry.send("didPublishContentRelease");
832
+ return { release: release22, error: null };
833
+ } catch (error2) {
834
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
835
+ isPublished: false,
836
+ error: error2
837
+ });
838
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
839
+ status: "failed"
840
+ }).transacting(trx).execute();
841
+ return {
842
+ release: null,
843
+ error: error2
844
+ };
845
+ }
846
+ });
847
+ if (error instanceof Error) {
848
+ throw error;
849
+ }
850
+ return release2;
851
+ },
852
+ async updateReleaseStatus(releaseId) {
853
+ const releaseActionService = getService("release-action", { strapi: strapi2 });
854
+ const [totalActions, invalidActions] = await Promise.all([
855
+ releaseActionService.countActions({
856
+ filters: {
857
+ release: releaseId
858
+ }
859
+ }),
860
+ releaseActionService.countActions({
861
+ filters: {
862
+ release: releaseId,
863
+ isEntryValid: false
864
+ }
865
+ })
866
+ ]);
867
+ if (totalActions > 0) {
868
+ if (invalidActions > 0) {
869
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
870
+ where: {
871
+ id: releaseId
872
+ },
873
+ data: {
874
+ status: "blocked"
875
+ }
876
+ });
877
+ }
878
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
879
+ where: {
880
+ id: releaseId
881
+ },
882
+ data: {
883
+ status: "ready"
884
+ }
885
+ });
886
+ }
887
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
888
+ where: {
889
+ id: releaseId
890
+ },
891
+ data: {
892
+ status: "empty"
893
+ }
894
+ });
895
+ }
896
+ };
897
+ };
898
+ const getGroupName = (queryValue) => {
899
+ switch (queryValue) {
900
+ case "contentType":
901
+ return "contentType.displayName";
902
+ case "type":
903
+ return "type";
904
+ case "locale":
905
+ return ___default.default.getOr("No locale", "locale.name");
906
+ default:
907
+ return "contentType.displayName";
908
+ }
909
+ };
910
+ const createReleaseActionService = ({ strapi: strapi2 }) => {
911
+ const getLocalesDataForActions = async () => {
912
+ if (!strapi2.plugin("i18n")) {
913
+ return {};
914
+ }
915
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
916
+ return allLocales.reduce((acc, locale) => {
917
+ acc[locale.code] = { name: locale.name, code: locale.code };
918
+ return acc;
919
+ }, {});
920
+ };
921
+ const getContentTypesDataForActions = async (contentTypesUids) => {
922
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
923
+ const contentTypesData = {};
924
+ for (const contentTypeUid of contentTypesUids) {
925
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
926
+ uid: contentTypeUid
927
+ });
928
+ contentTypesData[contentTypeUid] = {
929
+ mainField: contentTypeConfig.settings.mainField,
930
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
931
+ };
932
+ }
933
+ return contentTypesData;
934
+ };
935
+ return {
936
+ async create(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
937
+ const { validateEntryData, validateUniqueEntry } = getService("release-validation", {
641
938
  strapi: strapi2
642
939
  });
643
940
  await Promise.all([
644
- validateEntryContentType(action.entry.contentType),
941
+ validateEntryData(action.contentType, action.entryDocumentId),
645
942
  validateUniqueEntry(releaseId, action)
646
943
  ]);
647
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
944
+ const model = strapi2.contentType(action.contentType);
945
+ if (model.kind === "singleType") {
946
+ const document = await strapi2.db.query(model.uid).findOne({ select: ["documentId"] });
947
+ if (!document) {
948
+ throw new utils.errors.NotFoundError(`No entry found for contentType ${action.contentType}`);
949
+ }
950
+ action.entryDocumentId = document.documentId;
951
+ }
952
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
648
953
  if (!release2) {
649
954
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
650
955
  }
651
956
  if (release2.releasedAt) {
652
957
  throw new utils.errors.ValidationError("Release already published");
653
958
  }
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, {
959
+ const actionStatus = action.type === "publish" ? await getDraftEntryValidStatus(
960
+ {
961
+ contentType: action.contentType,
962
+ documentId: action.entryDocumentId,
963
+ locale: action.locale
964
+ },
965
+ {
966
+ strapi: strapi2
967
+ }
968
+ ) : true;
969
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
658
970
  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
971
+ ...action,
972
+ release: release2.id,
973
+ isEntryValid: actionStatus
669
974
  },
670
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
975
+ populate: { release: { select: ["id"] } }
671
976
  });
672
- this.updateReleaseStatus(releaseId);
977
+ if (!disableUpdateReleaseStatus) {
978
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
979
+ }
673
980
  return releaseAction2;
674
981
  },
675
- async findActions(releaseId, query) {
676
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
677
- fields: ["id"]
982
+ async findPage(releaseId, query) {
983
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
984
+ where: { id: releaseId },
985
+ select: ["id"]
678
986
  });
679
987
  if (!release2) {
680
988
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
681
989
  }
682
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
683
- ...query,
684
- populate: {
685
- entry: {
686
- populate: "*"
687
- }
688
- },
689
- filters: {
990
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
991
+ const { results: actions, pagination } = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
992
+ ...dbQuery,
993
+ where: {
690
994
  release: releaseId
691
995
  }
692
996
  });
693
- },
694
- async countActions(query) {
695
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
997
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
998
+ const actionsWithEntry = await utils.async.map(actions, async (action) => {
999
+ const populate = await populateBuilderService(action.contentType).populateDeep(Infinity).build();
1000
+ const entry = await getEntry(
1001
+ {
1002
+ contentType: action.contentType,
1003
+ documentId: action.entryDocumentId,
1004
+ locale: action.locale,
1005
+ populate,
1006
+ status: action.type === "publish" ? "draft" : "published"
1007
+ },
1008
+ { strapi: strapi2 }
1009
+ );
1010
+ return {
1011
+ ...action,
1012
+ entry,
1013
+ status: entry ? await getEntryStatus(action.contentType, entry) : null
1014
+ };
1015
+ });
1016
+ return {
1017
+ results: actionsWithEntry,
1018
+ pagination
1019
+ };
696
1020
  },
697
1021
  async groupActions(actions, groupBy) {
698
1022
  const contentTypeUids = actions.reduce((acc, action) => {
@@ -701,10 +1025,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
701
1025
  }
702
1026
  return acc;
703
1027
  }, []);
704
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
705
- contentTypeUids
706
- );
707
- const allLocalesDictionary = await this.getLocalesDataForActions();
1028
+ const allReleaseContentTypesDictionary = await getContentTypesDataForActions(contentTypeUids);
1029
+ const allLocalesDictionary = await getLocalesDataForActions();
708
1030
  const formattedData = actions.map((action) => {
709
1031
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
710
1032
  return {
@@ -720,30 +1042,6 @@ const createReleaseService = ({ strapi: strapi2 }) => {
720
1042
  const groupName = getGroupName(groupBy);
721
1043
  return ___default.default.groupBy(groupName)(formattedData);
722
1044
  },
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
1045
  getContentTypeModelsFromActions(actions) {
748
1046
  const contentTypeUids = actions.reduce((acc, action) => {
749
1047
  if (!acc.includes(action.contentType)) {
@@ -760,191 +1058,37 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1058
  );
761
1059
  return contentTypeModelsMap;
762
1060
  },
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;
1061
+ async countActions(query) {
1062
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
1063
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
805
1064
  },
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
1065
+ async update(actionId, releaseId, update) {
1066
+ const action = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findOne({
1067
+ where: {
1068
+ id: actionId,
1069
+ release: {
1070
+ id: releaseId,
1071
+ releasedAt: {
1072
+ $null: true
920
1073
  }
921
1074
  }
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
1075
  }
938
- strapi2.db.query(RELEASE_MODEL_UID).update({
939
- where: { id: releaseId },
940
- data: {
941
- status: "failed"
942
- }
943
- });
944
- throw error;
1076
+ });
1077
+ if (!action) {
1078
+ throw new utils.errors.NotFoundError(
1079
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1080
+ );
945
1081
  }
946
- },
947
- async updateAction(actionId, releaseId, update) {
1082
+ const actionStatus = update.type === "publish" ? await getDraftEntryValidStatus(
1083
+ {
1084
+ contentType: action.contentType,
1085
+ documentId: action.entryDocumentId,
1086
+ locale: action.locale
1087
+ },
1088
+ {
1089
+ strapi: strapi2
1090
+ }
1091
+ ) : true;
948
1092
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
949
1093
  where: {
950
1094
  id: actionId,
@@ -955,16 +1099,15 @@ const createReleaseService = ({ strapi: strapi2 }) => {
955
1099
  }
956
1100
  }
957
1101
  },
958
- data: update
1102
+ data: {
1103
+ ...update,
1104
+ isEntryValid: actionStatus
1105
+ }
959
1106
  });
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
- }
1107
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
965
1108
  return updatedAction;
966
1109
  },
967
- async deleteAction(actionId, releaseId) {
1110
+ async delete(actionId, releaseId) {
968
1111
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
969
1112
  where: {
970
1113
  id: actionId,
@@ -981,79 +1124,56 @@ const createReleaseService = ({ strapi: strapi2 }) => {
981
1124
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
982
1125
  );
983
1126
  }
984
- this.updateReleaseStatus(releaseId);
1127
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(releaseId);
985
1128
  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
1129
  }
1030
1130
  };
1031
1131
  };
1132
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1133
+ constructor(message) {
1134
+ super(message);
1135
+ this.name = "AlreadyOnReleaseError";
1136
+ }
1137
+ }
1032
1138
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1033
1139
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1034
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
1035
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1140
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1141
+ where: {
1142
+ id: releaseId
1143
+ },
1144
+ populate: {
1145
+ actions: true
1146
+ }
1036
1147
  });
1037
1148
  if (!release2) {
1038
1149
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1039
1150
  }
1040
1151
  const isEntryInRelease = release2.actions.some(
1041
- (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1152
+ (action) => action.entryDocumentId === releaseActionArgs.entryDocumentId && action.contentType === releaseActionArgs.contentType && (releaseActionArgs.locale ? action.locale === releaseActionArgs.locale : true)
1042
1153
  );
1043
1154
  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}`
1155
+ throw new AlreadyOnReleaseError(
1156
+ `Entry with documentId ${releaseActionArgs.entryDocumentId}${releaseActionArgs.locale ? `( ${releaseActionArgs.locale})` : ""} and contentType ${releaseActionArgs.contentType} already exists in release with id ${releaseId}`
1046
1157
  );
1047
1158
  }
1048
1159
  },
1049
- validateEntryContentType(contentTypeUid) {
1160
+ validateEntryData(contentTypeUid, entryDocumentId) {
1050
1161
  const contentType = strapi2.contentType(contentTypeUid);
1051
1162
  if (!contentType) {
1052
1163
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
1053
1164
  }
1165
+ if (!utils.contentTypes.hasDraftAndPublish(contentType)) {
1166
+ throw new utils.errors.ValidationError(
1167
+ `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
1168
+ );
1169
+ }
1170
+ if (contentType.kind === "collectionType" && !entryDocumentId) {
1171
+ throw new utils.errors.ValidationError("Document id is required for collection type");
1172
+ }
1054
1173
  },
1055
1174
  async validatePendingReleasesLimit() {
1056
- const maximumPendingReleases = strapi2.ee.features.get("cms-content-releases")?.options?.maximumReleases || 3;
1175
+ const featureCfg = strapi2.ee.features.get("cms-content-releases");
1176
+ const maximumPendingReleases = typeof featureCfg === "object" && featureCfg?.options?.maximumReleases || 3;
1057
1177
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1058
1178
  filters: {
1059
1179
  releasedAt: {
@@ -1066,8 +1186,8 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1066
1186
  }
1067
1187
  },
1068
1188
  async validateUniqueNameForPendingRelease(name, id) {
1069
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1070
- filters: {
1189
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1190
+ where: {
1071
1191
  releasedAt: {
1072
1192
  $null: true
1073
1193
  },
@@ -1096,7 +1216,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1096
1216
  }
1097
1217
  const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1098
1218
  try {
1099
- await getService("release").publish(releaseId);
1219
+ await getService("release", { strapi: strapi2 }).publish(releaseId);
1100
1220
  } catch (error) {
1101
1221
  }
1102
1222
  this.cancel(releaseId);
@@ -1138,85 +1258,172 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1138
1258
  }
1139
1259
  };
1140
1260
  };
1261
+ const DEFAULT_SETTINGS = {
1262
+ defaultTimezone: null
1263
+ };
1264
+ const createSettingsService = ({ strapi: strapi2 }) => {
1265
+ const getStore = async () => strapi2.store({ type: "core", name: "content-releases" });
1266
+ return {
1267
+ async update({ settings: settings2 }) {
1268
+ const store = await getStore();
1269
+ store.set({ key: "settings", value: settings2 });
1270
+ return settings2;
1271
+ },
1272
+ async find() {
1273
+ const store = await getStore();
1274
+ const settings2 = await store.get({ key: "settings" });
1275
+ return {
1276
+ ...DEFAULT_SETTINGS,
1277
+ ...settings2 || {}
1278
+ };
1279
+ }
1280
+ };
1281
+ };
1141
1282
  const services = {
1142
1283
  release: createReleaseService,
1284
+ "release-action": createReleaseActionService,
1143
1285
  "release-validation": createReleaseValidationService,
1144
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1286
+ scheduling: createSchedulingService,
1287
+ settings: createSettingsService
1145
1288
  };
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()
1289
+ const RELEASE_SCHEMA = utils.yup.object().shape({
1290
+ name: utils.yup.string().trim().required(),
1291
+ scheduledAt: utils.yup.string().nullable(),
1292
+ timezone: utils.yup.string().when("scheduledAt", {
1293
+ is: (value) => value !== null && value !== void 0,
1294
+ then: utils.yup.string().required(),
1295
+ otherwise: utils.yup.string().nullable()
1164
1296
  })
1165
1297
  }).required().noUnknown();
1298
+ const FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA = utils.yup.object().shape({
1299
+ contentType: utils.yup.string().required(),
1300
+ entryDocumentId: utils.yup.string().nullable(),
1301
+ hasEntryAttached: utils.yup.string().nullable(),
1302
+ locale: utils.yup.string().nullable()
1303
+ }).required().noUnknown();
1166
1304
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
1305
+ const validatefindByDocumentAttachedParams = utils.validateYupSchema(
1306
+ FIND_BY_DOCUMENT_ATTACHED_PARAMS_SCHEMA
1307
+ );
1167
1308
  const releaseController = {
1168
- async findMany(ctx) {
1169
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1309
+ /**
1310
+ * Find releases based on documents attached or not to the release.
1311
+ * If `hasEntryAttached` is true, it will return all releases that have the entry attached.
1312
+ * If `hasEntryAttached` is false, it will return all releases that don't have the entry attached.
1313
+ */
1314
+ async findByDocumentAttached(ctx) {
1315
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1170
1316
  ability: ctx.state.userAbility,
1171
1317
  model: RELEASE_MODEL_UID
1172
1318
  });
1173
1319
  await permissionsManager.validateQuery(ctx.query);
1174
1320
  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,
1321
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1322
+ await validatefindByDocumentAttachedParams(query);
1323
+ const model = strapi.getModel(query.contentType);
1324
+ if (model.kind && model.kind === "singleType") {
1325
+ const document = await strapi.db.query(model.uid).findOne({ select: ["documentId"] });
1326
+ if (!document) {
1327
+ throw new utils.errors.NotFoundError(`No entry found for contentType ${query.contentType}`);
1328
+ }
1329
+ query.entryDocumentId = document.documentId;
1330
+ }
1331
+ const { contentType, hasEntryAttached, entryDocumentId, locale } = query;
1332
+ const isEntryAttached = typeof hasEntryAttached === "string" ? Boolean(JSON.parse(hasEntryAttached)) : false;
1333
+ if (isEntryAttached) {
1334
+ const releases = await releaseService.findMany({
1335
+ where: {
1336
+ releasedAt: null,
1190
1337
  actions: {
1191
- meta: {
1192
- count: actions.count
1338
+ contentType,
1339
+ entryDocumentId: entryDocumentId ?? null,
1340
+ locale: locale ?? null
1341
+ }
1342
+ },
1343
+ populate: {
1344
+ actions: {
1345
+ fields: ["type"],
1346
+ filters: {
1347
+ contentType,
1348
+ entryDocumentId: entryDocumentId ?? null,
1349
+ locale: locale ?? null
1193
1350
  }
1194
1351
  }
1195
- };
1352
+ }
1353
+ });
1354
+ ctx.body = { data: releases };
1355
+ } else {
1356
+ const relatedReleases = await releaseService.findMany({
1357
+ where: {
1358
+ releasedAt: null,
1359
+ actions: {
1360
+ contentType,
1361
+ entryDocumentId: entryDocumentId ?? null,
1362
+ locale: locale ?? null
1363
+ }
1364
+ }
1196
1365
  });
1197
- const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1366
+ const releases = await releaseService.findMany({
1198
1367
  where: {
1368
+ $or: [
1369
+ {
1370
+ id: {
1371
+ $notIn: relatedReleases.map((release2) => release2.id)
1372
+ }
1373
+ },
1374
+ {
1375
+ actions: null
1376
+ }
1377
+ ],
1199
1378
  releasedAt: null
1200
1379
  }
1201
1380
  });
1202
- ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1381
+ ctx.body = { data: releases };
1203
1382
  }
1204
1383
  },
1384
+ async findPage(ctx) {
1385
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1386
+ ability: ctx.state.userAbility,
1387
+ model: RELEASE_MODEL_UID
1388
+ });
1389
+ await permissionsManager.validateQuery(ctx.query);
1390
+ const releaseService = getService("release", { strapi });
1391
+ const query = await permissionsManager.sanitizeQuery(ctx.query);
1392
+ const { results, pagination } = await releaseService.findPage(query);
1393
+ const data = results.map((release2) => {
1394
+ const { actions, ...releaseData } = release2;
1395
+ return {
1396
+ ...releaseData,
1397
+ actions: {
1398
+ meta: {
1399
+ count: actions.count
1400
+ }
1401
+ }
1402
+ };
1403
+ });
1404
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1405
+ where: {
1406
+ releasedAt: null
1407
+ }
1408
+ });
1409
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
1410
+ },
1205
1411
  async findOne(ctx) {
1206
1412
  const id = ctx.params.id;
1207
1413
  const releaseService = getService("release", { strapi });
1414
+ const releaseActionService = getService("release-action", { strapi });
1208
1415
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
1209
1416
  if (!release2) {
1210
1417
  throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1211
1418
  }
1212
- const count = await releaseService.countActions({
1419
+ const count = await releaseActionService.countActions({
1213
1420
  filters: {
1214
1421
  release: id
1215
1422
  }
1216
1423
  });
1217
1424
  const sanitizedRelease = {
1218
1425
  ...release2,
1219
- createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1426
+ createdBy: release2.createdBy ? strapi.service("admin::user").sanitizeUser(release2.createdBy) : null
1220
1427
  };
1221
1428
  const data = {
1222
1429
  ...sanitizedRelease,
@@ -1228,19 +1435,63 @@ const releaseController = {
1228
1435
  };
1229
1436
  ctx.body = { data };
1230
1437
  },
1438
+ async mapEntriesToReleases(ctx) {
1439
+ const { contentTypeUid, documentIds, locale } = ctx.query;
1440
+ if (!contentTypeUid || !documentIds) {
1441
+ throw new utils.errors.ValidationError("Missing required query parameters");
1442
+ }
1443
+ const releaseService = getService("release", { strapi });
1444
+ const releasesWithActions = await releaseService.findMany({
1445
+ where: {
1446
+ releasedAt: null,
1447
+ actions: {
1448
+ contentType: contentTypeUid,
1449
+ entryDocumentId: {
1450
+ $in: documentIds
1451
+ },
1452
+ locale
1453
+ }
1454
+ },
1455
+ populate: {
1456
+ actions: true
1457
+ }
1458
+ });
1459
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1460
+ (acc, release2) => {
1461
+ release2.actions.forEach((action) => {
1462
+ if (action.contentType !== contentTypeUid) {
1463
+ return;
1464
+ }
1465
+ if (locale && action.locale !== locale) {
1466
+ return;
1467
+ }
1468
+ if (!acc[action.entryDocumentId]) {
1469
+ acc[action.entryDocumentId] = [{ id: release2.id, name: release2.name }];
1470
+ } else {
1471
+ acc[action.entryDocumentId].push({ id: release2.id, name: release2.name });
1472
+ }
1473
+ });
1474
+ return acc;
1475
+ },
1476
+ {}
1477
+ );
1478
+ ctx.body = {
1479
+ data: mappedEntriesInReleases
1480
+ };
1481
+ },
1231
1482
  async create(ctx) {
1232
1483
  const user = ctx.state.user;
1233
1484
  const releaseArgs = ctx.request.body;
1234
1485
  await validateRelease(releaseArgs);
1235
1486
  const releaseService = getService("release", { strapi });
1236
1487
  const release2 = await releaseService.create(releaseArgs, { user });
1237
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1488
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1238
1489
  ability: ctx.state.userAbility,
1239
1490
  model: RELEASE_MODEL_UID
1240
1491
  });
1241
- ctx.body = {
1492
+ ctx.created({
1242
1493
  data: await permissionsManager.sanitizeOutput(release2)
1243
- };
1494
+ });
1244
1495
  },
1245
1496
  async update(ctx) {
1246
1497
  const user = ctx.state.user;
@@ -1249,7 +1500,7 @@ const releaseController = {
1249
1500
  await validateRelease(releaseArgs);
1250
1501
  const releaseService = getService("release", { strapi });
1251
1502
  const release2 = await releaseService.update(id, releaseArgs, { user });
1252
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1503
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1253
1504
  ability: ctx.state.userAbility,
1254
1505
  model: RELEASE_MODEL_UID
1255
1506
  });
@@ -1266,18 +1517,18 @@ const releaseController = {
1266
1517
  };
1267
1518
  },
1268
1519
  async publish(ctx) {
1269
- const user = ctx.state.user;
1270
1520
  const id = ctx.params.id;
1271
1521
  const releaseService = getService("release", { strapi });
1272
- const release2 = await releaseService.publish(id, { user });
1522
+ const releaseActionService = getService("release-action", { strapi });
1523
+ const release2 = await releaseService.publish(id);
1273
1524
  const [countPublishActions, countUnpublishActions] = await Promise.all([
1274
- releaseService.countActions({
1525
+ releaseActionService.countActions({
1275
1526
  filters: {
1276
1527
  release: id,
1277
1528
  type: "publish"
1278
1529
  }
1279
1530
  }),
1280
- releaseService.countActions({
1531
+ releaseActionService.countActions({
1281
1532
  filters: {
1282
1533
  release: id,
1283
1534
  type: "unpublish"
@@ -1295,45 +1546,93 @@ const releaseController = {
1295
1546
  }
1296
1547
  };
1297
1548
  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(),
1549
+ contentType: utils.yup.string().required(),
1550
+ entryDocumentId: utils.yup.strapiID(),
1551
+ locale: utils.yup.string(),
1302
1552
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1303
1553
  });
1304
1554
  const RELEASE_ACTION_UPDATE_SCHEMA = utils.yup.object().shape({
1305
1555
  type: utils.yup.string().oneOf(["publish", "unpublish"]).required()
1306
1556
  });
1557
+ const FIND_MANY_ACTIONS_PARAMS = utils.yup.object().shape({
1558
+ groupBy: utils.yup.string().oneOf(["action", "contentType", "locale"])
1559
+ });
1307
1560
  const validateReleaseAction = utils.validateYupSchema(RELEASE_ACTION_SCHEMA);
1308
1561
  const validateReleaseActionUpdateSchema = utils.validateYupSchema(RELEASE_ACTION_UPDATE_SCHEMA);
1562
+ const validateFindManyActionsParams = utils.validateYupSchema(FIND_MANY_ACTIONS_PARAMS);
1309
1563
  const releaseActionController = {
1310
1564
  async create(ctx) {
1311
1565
  const releaseId = ctx.params.releaseId;
1312
1566
  const releaseActionArgs = ctx.request.body;
1313
1567
  await validateReleaseAction(releaseActionArgs);
1314
- const releaseService = getService("release", { strapi });
1315
- const releaseAction2 = await releaseService.createAction(releaseId, releaseActionArgs);
1316
- ctx.body = {
1568
+ const releaseActionService = getService("release-action", { strapi });
1569
+ const releaseAction2 = await releaseActionService.create(releaseId, releaseActionArgs);
1570
+ ctx.created({
1317
1571
  data: releaseAction2
1318
- };
1572
+ });
1573
+ },
1574
+ async createMany(ctx) {
1575
+ const releaseId = ctx.params.releaseId;
1576
+ const releaseActionsArgs = ctx.request.body;
1577
+ await Promise.all(
1578
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1579
+ );
1580
+ const releaseActionService = getService("release-action", { strapi });
1581
+ const releaseService = getService("release", { strapi });
1582
+ const releaseActions = await strapi.db.transaction(async () => {
1583
+ const releaseActions2 = await Promise.all(
1584
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1585
+ try {
1586
+ const action = await releaseActionService.create(releaseId, releaseActionArgs, {
1587
+ disableUpdateReleaseStatus: true
1588
+ });
1589
+ return action;
1590
+ } catch (error) {
1591
+ if (error instanceof AlreadyOnReleaseError) {
1592
+ return null;
1593
+ }
1594
+ throw error;
1595
+ }
1596
+ })
1597
+ );
1598
+ return releaseActions2;
1599
+ });
1600
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1601
+ if (newReleaseActions.length > 0) {
1602
+ releaseService.updateReleaseStatus(releaseId);
1603
+ }
1604
+ ctx.created({
1605
+ data: newReleaseActions,
1606
+ meta: {
1607
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1608
+ totalEntries: releaseActions.length
1609
+ }
1610
+ });
1319
1611
  },
1320
1612
  async findMany(ctx) {
1321
1613
  const releaseId = ctx.params.releaseId;
1322
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
1614
+ const permissionsManager = strapi.service("admin::permission").createPermissionsManager({
1323
1615
  ability: ctx.state.userAbility,
1324
1616
  model: RELEASE_ACTION_MODEL_UID
1325
1617
  });
1618
+ await validateFindManyActionsParams(ctx.query);
1619
+ if (ctx.query.groupBy) {
1620
+ if (!["action", "contentType", "locale"].includes(ctx.query.groupBy)) {
1621
+ ctx.badRequest("Invalid groupBy parameter");
1622
+ }
1623
+ }
1624
+ ctx.query.sort = ctx.query.groupBy === "action" ? "type" : ctx.query.groupBy;
1625
+ delete ctx.query.groupBy;
1326
1626
  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,
1627
+ const releaseActionService = getService("release-action", { strapi });
1628
+ const { results, pagination } = await releaseActionService.findPage(releaseId, {
1330
1629
  ...query
1331
1630
  });
1332
1631
  const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1333
1632
  if (acc[action.contentType]) {
1334
1633
  return acc;
1335
1634
  }
1336
- const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1635
+ const contentTypePermissionsManager = strapi.service("admin::permission").createPermissionsManager({
1337
1636
  ability: ctx.state.userAbility,
1338
1637
  model: action.contentType
1339
1638
  });
@@ -1342,10 +1641,11 @@ const releaseActionController = {
1342
1641
  }, {});
1343
1642
  const sanitizedResults = await utils.async.map(results, async (action) => ({
1344
1643
  ...action,
1345
- entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1644
+ entry: action.entry ? await contentTypeOutputSanitizers[action.contentType](action.entry) : {}
1346
1645
  }));
1347
- const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1348
- const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1646
+ const groupedData = await releaseActionService.groupActions(sanitizedResults, query.sort);
1647
+ const contentTypes2 = releaseActionService.getContentTypeModelsFromActions(results);
1648
+ const releaseService = getService("release", { strapi });
1349
1649
  const components = await releaseService.getAllComponents();
1350
1650
  ctx.body = {
1351
1651
  data: groupedData,
@@ -1361,8 +1661,8 @@ const releaseActionController = {
1361
1661
  const releaseId = ctx.params.releaseId;
1362
1662
  const releaseActionUpdateArgs = ctx.request.body;
1363
1663
  await validateReleaseActionUpdateSchema(releaseActionUpdateArgs);
1364
- const releaseService = getService("release", { strapi });
1365
- const updatedAction = await releaseService.updateAction(
1664
+ const releaseActionService = getService("release-action", { strapi });
1665
+ const updatedAction = await releaseActionService.update(
1366
1666
  actionId,
1367
1667
  releaseId,
1368
1668
  releaseActionUpdateArgs
@@ -1374,17 +1674,71 @@ const releaseActionController = {
1374
1674
  async delete(ctx) {
1375
1675
  const actionId = ctx.params.actionId;
1376
1676
  const releaseId = ctx.params.releaseId;
1377
- const releaseService = getService("release", { strapi });
1378
- const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
1677
+ const releaseActionService = getService("release-action", { strapi });
1678
+ const deletedReleaseAction = await releaseActionService.delete(actionId, releaseId);
1379
1679
  ctx.body = {
1380
1680
  data: deletedReleaseAction
1381
1681
  };
1382
1682
  }
1383
1683
  };
1384
- const controllers = { release: releaseController, "release-action": releaseActionController };
1684
+ const SETTINGS_SCHEMA = yup__namespace.object().shape({
1685
+ defaultTimezone: yup__namespace.string().nullable().default(null)
1686
+ }).required().noUnknown();
1687
+ const validateSettings = utils.validateYupSchema(SETTINGS_SCHEMA);
1688
+ const settingsController = {
1689
+ async find(ctx) {
1690
+ const settingsService = getService("settings", { strapi });
1691
+ const settings2 = await settingsService.find();
1692
+ ctx.body = { data: settings2 };
1693
+ },
1694
+ async update(ctx) {
1695
+ const settingsBody = ctx.request.body;
1696
+ const settings2 = await validateSettings(settingsBody);
1697
+ const settingsService = getService("settings", { strapi });
1698
+ const updatedSettings = await settingsService.update({ settings: settings2 });
1699
+ ctx.body = { data: updatedSettings };
1700
+ }
1701
+ };
1702
+ const controllers = {
1703
+ release: releaseController,
1704
+ "release-action": releaseActionController,
1705
+ settings: settingsController
1706
+ };
1385
1707
  const release = {
1386
1708
  type: "admin",
1387
1709
  routes: [
1710
+ {
1711
+ method: "GET",
1712
+ path: "/mapEntriesToReleases",
1713
+ handler: "release.mapEntriesToReleases",
1714
+ config: {
1715
+ policies: [
1716
+ "admin::isAuthenticatedAdmin",
1717
+ {
1718
+ name: "admin::hasPermissions",
1719
+ config: {
1720
+ actions: ["plugin::content-releases.read"]
1721
+ }
1722
+ }
1723
+ ]
1724
+ }
1725
+ },
1726
+ {
1727
+ method: "GET",
1728
+ path: "/getByDocumentAttached",
1729
+ handler: "release.findByDocumentAttached",
1730
+ config: {
1731
+ policies: [
1732
+ "admin::isAuthenticatedAdmin",
1733
+ {
1734
+ name: "admin::hasPermissions",
1735
+ config: {
1736
+ actions: ["plugin::content-releases.read"]
1737
+ }
1738
+ }
1739
+ ]
1740
+ }
1741
+ },
1388
1742
  {
1389
1743
  method: "POST",
1390
1744
  path: "/",
@@ -1404,7 +1758,7 @@ const release = {
1404
1758
  {
1405
1759
  method: "GET",
1406
1760
  path: "/",
1407
- handler: "release.findMany",
1761
+ handler: "release.findPage",
1408
1762
  config: {
1409
1763
  policies: [
1410
1764
  "admin::isAuthenticatedAdmin",
@@ -1502,6 +1856,22 @@ const releaseAction = {
1502
1856
  ]
1503
1857
  }
1504
1858
  },
1859
+ {
1860
+ method: "POST",
1861
+ path: "/:releaseId/actions/bulk",
1862
+ handler: "release-action.createMany",
1863
+ config: {
1864
+ policies: [
1865
+ "admin::isAuthenticatedAdmin",
1866
+ {
1867
+ name: "admin::hasPermissions",
1868
+ config: {
1869
+ actions: ["plugin::content-releases.create-action"]
1870
+ }
1871
+ }
1872
+ ]
1873
+ }
1874
+ },
1505
1875
  {
1506
1876
  method: "GET",
1507
1877
  path: "/:releaseId/actions",
@@ -1552,7 +1922,45 @@ const releaseAction = {
1552
1922
  }
1553
1923
  ]
1554
1924
  };
1925
+ const settings = {
1926
+ type: "admin",
1927
+ routes: [
1928
+ {
1929
+ method: "GET",
1930
+ path: "/settings",
1931
+ handler: "settings.find",
1932
+ config: {
1933
+ policies: [
1934
+ "admin::isAuthenticatedAdmin",
1935
+ {
1936
+ name: "admin::hasPermissions",
1937
+ config: {
1938
+ actions: ["plugin::content-releases.settings.read"]
1939
+ }
1940
+ }
1941
+ ]
1942
+ }
1943
+ },
1944
+ {
1945
+ method: "PUT",
1946
+ path: "/settings",
1947
+ handler: "settings.update",
1948
+ config: {
1949
+ policies: [
1950
+ "admin::isAuthenticatedAdmin",
1951
+ {
1952
+ name: "admin::hasPermissions",
1953
+ config: {
1954
+ actions: ["plugin::content-releases.settings.update"]
1955
+ }
1956
+ }
1957
+ ]
1958
+ }
1959
+ }
1960
+ ]
1961
+ };
1555
1962
  const routes = {
1963
+ settings,
1556
1964
  release,
1557
1965
  "release-action": releaseAction
1558
1966
  };
@@ -1569,6 +1977,9 @@ const getPlugin = () => {
1569
1977
  };
1570
1978
  }
1571
1979
  return {
1980
+ // Always return register, it handles its own feature check
1981
+ register,
1982
+ // Always return contentTypes to avoid losing data when the feature is disabled
1572
1983
  contentTypes
1573
1984
  };
1574
1985
  };