@strapi/content-releases 0.0.0-experimental.e1ede8c55a0e1e22ce20137bf238fc374bd5dd51 → 0.0.0-experimental.e3e48deb89bd0a1b6cc69b698696566fa7854a95

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 (107) hide show
  1. package/dist/_chunks/App-PQlYzNfb.mjs +1337 -0
  2. package/dist/_chunks/App-PQlYzNfb.mjs.map +1 -0
  3. package/dist/_chunks/App-lzeJz92X.js +1360 -0
  4. package/dist/_chunks/App-lzeJz92X.js.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-ngTk74JV.mjs → en-WuuhP6Bn.mjs} +22 -6
  10. package/dist/_chunks/en-WuuhP6Bn.mjs.map +1 -0
  11. package/dist/_chunks/{en-haKSQIo8.js → en-gcJJ5htG.js} +22 -6
  12. package/dist/_chunks/en-gcJJ5htG.js.map +1 -0
  13. package/dist/_chunks/{index-XAQOX_IB.mjs → index--4AgLDzb.mjs} +262 -83
  14. package/dist/_chunks/index--4AgLDzb.mjs.map +1 -0
  15. package/dist/_chunks/{index-EdBmRHRU.js → index-Nf1JfI-m.js} +246 -67
  16. package/dist/_chunks/index-Nf1JfI-m.js.map +1 -0
  17. package/dist/admin/index.js +2 -1
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +3 -2
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/admin/src/components/CMReleasesContainer.d.ts +1 -0
  22. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  23. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  24. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  25. package/dist/admin/src/components/ReleaseModal.d.ts +16 -0
  26. package/dist/admin/src/constants.d.ts +58 -0
  27. package/dist/admin/src/index.d.ts +3 -0
  28. package/dist/admin/src/pages/App.d.ts +1 -0
  29. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  30. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  31. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  32. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  33. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  34. package/dist/admin/src/pluginId.d.ts +1 -0
  35. package/dist/admin/src/services/axios.d.ts +29 -0
  36. package/dist/admin/src/services/release.d.ts +369 -0
  37. package/dist/admin/src/store/hooks.d.ts +7 -0
  38. package/dist/admin/src/utils/time.d.ts +1 -0
  39. package/dist/server/index.js +999 -309
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/index.mjs +999 -310
  42. package/dist/server/index.mjs.map +1 -1
  43. package/dist/server/src/bootstrap.d.ts +5 -0
  44. package/dist/server/src/bootstrap.d.ts.map +1 -0
  45. package/dist/server/src/constants.d.ts +12 -0
  46. package/dist/server/src/constants.d.ts.map +1 -0
  47. package/dist/server/src/content-types/index.d.ts +99 -0
  48. package/dist/server/src/content-types/index.d.ts.map +1 -0
  49. package/dist/server/src/content-types/release/index.d.ts +48 -0
  50. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  51. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  52. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  53. package/dist/server/src/content-types/release-action/index.d.ts +50 -0
  54. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  55. package/dist/server/src/content-types/release-action/schema.d.ts +49 -0
  56. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  57. package/dist/server/src/controllers/index.d.ts +18 -0
  58. package/dist/server/src/controllers/index.d.ts.map +1 -0
  59. package/dist/server/src/controllers/release-action.d.ts +9 -0
  60. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  61. package/dist/server/src/controllers/release.d.ts +11 -0
  62. package/dist/server/src/controllers/release.d.ts.map +1 -0
  63. package/dist/server/src/controllers/validation/release-action.d.ts +3 -0
  64. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  65. package/dist/server/src/controllers/validation/release.d.ts +2 -0
  66. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  67. package/dist/server/src/destroy.d.ts +5 -0
  68. package/dist/server/src/destroy.d.ts.map +1 -0
  69. package/dist/server/src/index.d.ts +2092 -0
  70. package/dist/server/src/index.d.ts.map +1 -0
  71. package/dist/server/src/migrations/index.d.ts +10 -0
  72. package/dist/server/src/migrations/index.d.ts.map +1 -0
  73. package/dist/server/src/register.d.ts +5 -0
  74. package/dist/server/src/register.d.ts.map +1 -0
  75. package/dist/server/src/routes/index.d.ts +35 -0
  76. package/dist/server/src/routes/index.d.ts.map +1 -0
  77. package/dist/server/src/routes/release-action.d.ts +18 -0
  78. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  79. package/dist/server/src/routes/release.d.ts +18 -0
  80. package/dist/server/src/routes/release.d.ts.map +1 -0
  81. package/dist/server/src/services/index.d.ts +1826 -0
  82. package/dist/server/src/services/index.d.ts.map +1 -0
  83. package/dist/server/src/services/release.d.ts +66 -0
  84. package/dist/server/src/services/release.d.ts.map +1 -0
  85. package/dist/server/src/services/scheduling.d.ts +18 -0
  86. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  87. package/dist/server/src/services/validation.d.ts +14 -0
  88. package/dist/server/src/services/validation.d.ts.map +1 -0
  89. package/dist/server/src/utils/index.d.ts +14 -0
  90. package/dist/server/src/utils/index.d.ts.map +1 -0
  91. package/dist/shared/contracts/release-actions.d.ts +105 -0
  92. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  93. package/dist/shared/contracts/releases.d.ts +166 -0
  94. package/dist/shared/contracts/releases.d.ts.map +1 -0
  95. package/dist/shared/types.d.ts +24 -0
  96. package/dist/shared/types.d.ts.map +1 -0
  97. package/dist/shared/validation-schemas.d.ts +2 -0
  98. package/dist/shared/validation-schemas.d.ts.map +1 -0
  99. package/package.json +19 -22
  100. package/dist/_chunks/App-g2P5kbSm.mjs +0 -945
  101. package/dist/_chunks/App-g2P5kbSm.mjs.map +0 -1
  102. package/dist/_chunks/App-o5_WfqR-.js +0 -967
  103. package/dist/_chunks/App-o5_WfqR-.js.map +0 -1
  104. package/dist/_chunks/en-haKSQIo8.js.map +0 -1
  105. package/dist/_chunks/en-ngTk74JV.mjs.map +0 -1
  106. package/dist/_chunks/index-EdBmRHRU.js.map +0 -1
  107. package/dist/_chunks/index-XAQOX_IB.mjs.map +0 -1
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const isEqual = require("lodash/isEqual");
4
+ const lodash = require("lodash");
3
5
  const _ = require("lodash/fp");
6
+ const nodeSchedule = require("node-schedule");
4
7
  const yup = require("yup");
5
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
9
  function _interopNamespace(e) {
@@ -21,6 +24,7 @@ function _interopNamespace(e) {
21
24
  n.default = e;
22
25
  return Object.freeze(n);
23
26
  }
27
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
24
28
  const ___default = /* @__PURE__ */ _interopDefault(_);
25
29
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
26
30
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
@@ -69,10 +73,292 @@ const ACTIONS = [
69
73
  pluginName: "content-releases"
70
74
  }
71
75
  ];
72
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
76
+ const ALLOWED_WEBHOOK_EVENTS = {
77
+ RELEASES_PUBLISH: "releases.publish"
78
+ };
79
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
80
+ return strapi2.plugin("content-releases").service(name);
81
+ };
82
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
83
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
84
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
85
+ const entry = await strapi2.db.query(contentTypeUid).findOne({
86
+ where: { id: entryId },
87
+ populate
88
+ });
89
+ return entry;
90
+ };
91
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
92
+ try {
93
+ await strapi2.entityValidator.validateEntityCreation(
94
+ strapi2.getModel(contentTypeUid),
95
+ entry,
96
+ void 0,
97
+ // @ts-expect-error - FIXME: entity here is unnecessary
98
+ entry
99
+ );
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ };
105
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
106
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
107
+ if (deletedContentTypes.length) {
108
+ await utils.async.map(deletedContentTypes, async (deletedContentTypeUID) => {
109
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
110
+ });
111
+ }
112
+ }
113
+ async function migrateIsValidAndStatusReleases() {
114
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
115
+ where: {
116
+ status: null,
117
+ releasedAt: null
118
+ },
119
+ populate: {
120
+ actions: {
121
+ populate: {
122
+ entry: true
123
+ }
124
+ }
125
+ }
126
+ });
127
+ utils.async.map(releasesWithoutStatus, async (release2) => {
128
+ const actions = release2.actions;
129
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
130
+ for (const action of notValidatedActions) {
131
+ if (action.entry) {
132
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
133
+ strapi
134
+ });
135
+ if (populatedEntry) {
136
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
137
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
138
+ where: {
139
+ id: action.id
140
+ },
141
+ data: {
142
+ isEntryValid
143
+ }
144
+ });
145
+ }
146
+ }
147
+ }
148
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
149
+ });
150
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
151
+ where: {
152
+ status: null,
153
+ releasedAt: {
154
+ $notNull: true
155
+ }
156
+ }
157
+ });
158
+ utils.async.map(publishedReleases, async (release2) => {
159
+ return strapi.db.query(RELEASE_MODEL_UID).update({
160
+ where: {
161
+ id: release2.id
162
+ },
163
+ data: {
164
+ status: "done"
165
+ }
166
+ });
167
+ });
168
+ }
169
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
170
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
171
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes);
172
+ const releasesAffected = /* @__PURE__ */ new Set();
173
+ utils.async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
174
+ const oldContentType = oldContentTypes[contentTypeUID];
175
+ const contentType = contentTypes2[contentTypeUID];
176
+ if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
177
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
178
+ where: {
179
+ contentType: contentTypeUID
180
+ },
181
+ populate: {
182
+ entry: true,
183
+ release: true
184
+ }
185
+ });
186
+ await utils.async.map(actions, async (action) => {
187
+ if (action.entry) {
188
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
189
+ strapi
190
+ });
191
+ if (populatedEntry) {
192
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
193
+ strapi
194
+ });
195
+ releasesAffected.add(action.release.id);
196
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
197
+ where: {
198
+ id: action.id
199
+ },
200
+ data: {
201
+ isEntryValid
202
+ }
203
+ });
204
+ }
205
+ }
206
+ });
207
+ }
208
+ }).then(() => {
209
+ utils.async.map(releasesAffected, async (releaseId) => {
210
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
211
+ });
212
+ });
213
+ }
214
+ }
73
215
  const register = async ({ strapi: strapi2 }) => {
74
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
216
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
75
217
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
218
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
219
+ }
220
+ };
221
+ const bootstrap = async ({ strapi: strapi2 }) => {
222
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
223
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes);
224
+ strapi2.db.lifecycles.subscribe({
225
+ models: contentTypesWithDraftAndPublish,
226
+ async afterDelete(event) {
227
+ try {
228
+ const { model, result } = event;
229
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
230
+ const { id } = result;
231
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
232
+ where: {
233
+ actions: {
234
+ target_type: model.uid,
235
+ target_id: id
236
+ }
237
+ }
238
+ });
239
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
240
+ where: {
241
+ target_type: model.uid,
242
+ target_id: id
243
+ }
244
+ });
245
+ for (const release2 of releases) {
246
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
247
+ }
248
+ }
249
+ } catch (error) {
250
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
251
+ }
252
+ },
253
+ /**
254
+ * deleteMany hook doesn't return the deleted entries ids
255
+ * so we need to fetch them before deleting the entries to save the ids on our state
256
+ */
257
+ async beforeDeleteMany(event) {
258
+ const { model, params } = event;
259
+ if (model.kind === "collectionType") {
260
+ const { where } = params;
261
+ const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
262
+ event.state.entriesToDelete = entriesToDelete;
263
+ }
264
+ },
265
+ /**
266
+ * We delete the release actions related to deleted entries
267
+ * We make this only after deleteMany is succesfully executed to avoid errors
268
+ */
269
+ async afterDeleteMany(event) {
270
+ try {
271
+ const { model, state } = event;
272
+ const entriesToDelete = state.entriesToDelete;
273
+ if (entriesToDelete) {
274
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
275
+ where: {
276
+ actions: {
277
+ target_type: model.uid,
278
+ target_id: {
279
+ $in: entriesToDelete.map(
280
+ (entry) => entry.id
281
+ )
282
+ }
283
+ }
284
+ }
285
+ });
286
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
287
+ where: {
288
+ target_type: model.uid,
289
+ target_id: {
290
+ $in: entriesToDelete.map((entry) => entry.id)
291
+ }
292
+ }
293
+ });
294
+ for (const release2 of releases) {
295
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
296
+ }
297
+ }
298
+ } catch (error) {
299
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
300
+ error
301
+ });
302
+ }
303
+ },
304
+ async afterUpdate(event) {
305
+ try {
306
+ const { model, result } = event;
307
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
308
+ const isEntryValid = await getEntryValidStatus(
309
+ model.uid,
310
+ result,
311
+ {
312
+ strapi: strapi2
313
+ }
314
+ );
315
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
316
+ where: {
317
+ target_type: model.uid,
318
+ target_id: result.id
319
+ },
320
+ data: {
321
+ isEntryValid
322
+ }
323
+ });
324
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
325
+ where: {
326
+ actions: {
327
+ target_type: model.uid,
328
+ target_id: result.id
329
+ }
330
+ }
331
+ });
332
+ for (const release2 of releases) {
333
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
334
+ }
335
+ }
336
+ } catch (error) {
337
+ strapi2.log.error("Error while updating release actions after entry update", { error });
338
+ }
339
+ }
340
+ });
341
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
342
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
343
+ strapi2.log.error(
344
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
345
+ );
346
+ throw err;
347
+ });
348
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
349
+ strapi2.webhookStore.addAllowedEvent(key, value);
350
+ });
351
+ }
352
+ }
353
+ };
354
+ const destroy = async ({ strapi: strapi2 }) => {
355
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
356
+ const scheduledJobs = getService("scheduling", {
357
+ strapi: strapi2
358
+ }).getAll();
359
+ for (const [, job] of scheduledJobs) {
360
+ job.cancel();
361
+ }
76
362
  }
77
363
  };
78
364
  const schema$1 = {
@@ -101,6 +387,17 @@ const schema$1 = {
101
387
  releasedAt: {
102
388
  type: "datetime"
103
389
  },
390
+ scheduledAt: {
391
+ type: "datetime"
392
+ },
393
+ timezone: {
394
+ type: "string"
395
+ },
396
+ status: {
397
+ type: "enumeration",
398
+ enum: ["ready", "blocked", "failed", "done", "empty"],
399
+ required: true
400
+ },
104
401
  actions: {
105
402
  type: "relation",
106
403
  relation: "oneToMany",
@@ -153,6 +450,9 @@ const schema = {
153
450
  relation: "manyToOne",
154
451
  target: RELEASE_MODEL_UID,
155
452
  inversedBy: "actions"
453
+ },
454
+ isEntryValid: {
455
+ type: "boolean"
156
456
  }
157
457
  }
158
458
  };
@@ -163,331 +463,581 @@ const contentTypes = {
163
463
  release: release$1,
164
464
  "release-action": releaseAction$1
165
465
  };
166
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
167
- return strapi2.plugin("content-releases").service(name);
168
- };
169
466
  const getGroupName = (queryValue) => {
170
467
  switch (queryValue) {
171
468
  case "contentType":
172
- return "entry.contentType.displayName";
469
+ return "contentType.displayName";
173
470
  case "action":
174
471
  return "type";
175
472
  case "locale":
176
- return ___default.default.getOr("No locale", "entry.locale.name");
473
+ return ___default.default.getOr("No locale", "locale.name");
177
474
  default:
178
- return "entry.contentType.displayName";
475
+ return "contentType.displayName";
179
476
  }
180
477
  };
181
- const createReleaseService = ({ strapi: strapi2 }) => ({
182
- async create(releaseData, { user }) {
183
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
184
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
185
- data: releaseWithCreatorFields
186
- });
187
- },
188
- async findOne(id, query = {}) {
189
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
190
- ...query
478
+ const createReleaseService = ({ strapi: strapi2 }) => {
479
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
480
+ strapi2.eventHub.emit(event, {
481
+ isPublished,
482
+ error,
483
+ release: release2
191
484
  });
192
- return release2;
193
- },
194
- findPage(query) {
195
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
196
- ...query,
197
- populate: {
198
- actions: {
199
- // @ts-expect-error Ignore missing properties
200
- count: true
485
+ };
486
+ return {
487
+ async create(releaseData, { user }) {
488
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
489
+ const {
490
+ validatePendingReleasesLimit,
491
+ validateUniqueNameForPendingRelease,
492
+ validateScheduledAtIsLaterThanNow
493
+ } = getService("release-validation", { strapi: strapi2 });
494
+ await Promise.all([
495
+ validatePendingReleasesLimit(),
496
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
497
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
498
+ ]);
499
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
500
+ data: {
501
+ ...releaseWithCreatorFields,
502
+ status: "empty"
201
503
  }
504
+ });
505
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
506
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
507
+ await schedulingService.set(release2.id, release2.scheduledAt);
202
508
  }
203
- });
204
- },
205
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
206
- hasEntryAttached
207
- } = {
208
- hasEntryAttached: false
209
- }) {
210
- const whereActions = hasEntryAttached ? {
211
- // Find all Releases where the content type entry is present
212
- actions: {
213
- target_type: contentTypeUid,
214
- target_id: entryId
215
- }
216
- } : {
217
- // Find all Releases where the content type entry is not present
218
- $or: [
219
- {
220
- $not: {
221
- actions: {
509
+ strapi2.telemetry.send("didCreateContentRelease");
510
+ return release2;
511
+ },
512
+ async findOne(id, query = {}) {
513
+ const dbQuery = utils.convertQueryParams.transformParamsToQuery(RELEASE_MODEL_UID, query);
514
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
515
+ ...dbQuery,
516
+ where: { id }
517
+ });
518
+ return release2;
519
+ },
520
+ findPage(query) {
521
+ const dbQuery = utils.convertQueryParams.transformParamsToQuery(RELEASE_MODEL_UID, query ?? {});
522
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
523
+ ...dbQuery,
524
+ populate: {
525
+ actions: {
526
+ count: true
527
+ }
528
+ }
529
+ });
530
+ },
531
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
532
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
533
+ where: {
534
+ actions: {
535
+ target_type: contentTypeUid,
536
+ target_id: entryId
537
+ },
538
+ releasedAt: {
539
+ $null: true
540
+ }
541
+ },
542
+ populate: {
543
+ // Filter the action to get only the content type entry
544
+ actions: {
545
+ where: {
222
546
  target_type: contentTypeUid,
223
547
  target_id: entryId
224
548
  }
225
549
  }
226
- },
227
- {
228
- actions: null
229
550
  }
230
- ]
231
- };
232
- const populateAttachedAction = hasEntryAttached ? {
233
- // Filter the action to get only the content type entry
234
- actions: {
551
+ });
552
+ return releases.map((release2) => {
553
+ if (release2.actions?.length) {
554
+ const [actionForEntry] = release2.actions;
555
+ delete release2.actions;
556
+ return {
557
+ ...release2,
558
+ action: actionForEntry
559
+ };
560
+ }
561
+ return release2;
562
+ });
563
+ },
564
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
565
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
566
+ where: {
567
+ releasedAt: {
568
+ $null: true
569
+ },
570
+ actions: {
571
+ target_type: contentTypeUid,
572
+ target_id: entryId
573
+ }
574
+ }
575
+ });
576
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
235
577
  where: {
236
- target_type: contentTypeUid,
237
- target_id: entryId
578
+ $or: [
579
+ {
580
+ id: {
581
+ $notIn: releasesRelated.map((release2) => release2.id)
582
+ }
583
+ },
584
+ {
585
+ actions: null
586
+ }
587
+ ],
588
+ releasedAt: {
589
+ $null: true
590
+ }
238
591
  }
239
- }
240
- } : {};
241
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
242
- where: {
243
- ...whereActions,
244
- releasedAt: {
245
- $null: true
592
+ });
593
+ return releases.map((release2) => {
594
+ if (release2.actions?.length) {
595
+ const [actionForEntry] = release2.actions;
596
+ delete release2.actions;
597
+ return {
598
+ ...release2,
599
+ action: actionForEntry
600
+ };
246
601
  }
247
- },
248
- populate: {
249
- ...populateAttachedAction
602
+ return release2;
603
+ });
604
+ },
605
+ async update(id, releaseData, { user }) {
606
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
607
+ releaseData
608
+ );
609
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
610
+ "release-validation",
611
+ { strapi: strapi2 }
612
+ );
613
+ await Promise.all([
614
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
615
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
616
+ ]);
617
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
618
+ if (!release2) {
619
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
250
620
  }
251
- });
252
- return releases.map((release2) => {
253
- if (release2.actions?.length) {
254
- const [actionForEntry] = release2.actions;
255
- delete release2.actions;
256
- return {
257
- ...release2,
258
- action: actionForEntry
259
- };
621
+ if (release2.releasedAt) {
622
+ throw new utils.errors.ValidationError("Release already published");
260
623
  }
261
- return release2;
262
- });
263
- },
264
- async update(id, releaseData, { user }) {
265
- const updatedRelease = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
266
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
267
- /*
268
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
269
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
270
- */
271
- // @ts-expect-error see above
272
- data: updatedRelease
273
- });
274
- if (!release2) {
275
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
276
- }
277
- return release2;
278
- },
279
- async createAction(releaseId, action) {
280
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
281
- strapi: strapi2
282
- });
283
- await Promise.all([
284
- validateEntryContentType(action.entry.contentType),
285
- validateUniqueEntry(releaseId, action)
286
- ]);
287
- const { entry, type } = action;
288
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
289
- data: {
290
- type,
291
- contentType: entry.contentType,
292
- locale: entry.locale,
293
- entry: {
294
- id: entry.id,
295
- __type: entry.contentType,
296
- __pivot: { field: "entry" }
297
- },
298
- release: releaseId
299
- },
300
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
301
- });
302
- },
303
- async findActions(releaseId, query) {
304
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
305
- fields: ["id"]
306
- });
307
- if (!release2) {
308
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
309
- }
310
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
311
- ...query,
312
- populate: {
313
- entry: true
314
- },
315
- filters: {
316
- release: releaseId
624
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
625
+ where: { id },
626
+ data: releaseWithCreatorFields
627
+ });
628
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
629
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
630
+ if (releaseData.scheduledAt) {
631
+ await schedulingService.set(id, releaseData.scheduledAt);
632
+ } else if (release2.scheduledAt) {
633
+ schedulingService.cancel(id);
634
+ }
317
635
  }
318
- });
319
- },
320
- async countActions(query) {
321
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
322
- },
323
- async groupActions(actions, groupBy) {
324
- const contentTypeUids = actions.reduce((acc, action) => {
325
- if (!acc.includes(action.contentType)) {
326
- acc.push(action.contentType);
636
+ this.updateReleaseStatus(id);
637
+ strapi2.telemetry.send("didUpdateContentRelease");
638
+ return updatedRelease;
639
+ },
640
+ async createAction(releaseId, action) {
641
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
642
+ strapi: strapi2
643
+ });
644
+ await Promise.all([
645
+ validateEntryContentType(action.entry.contentType),
646
+ validateUniqueEntry(releaseId, action)
647
+ ]);
648
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
649
+ if (!release2) {
650
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
327
651
  }
328
- return acc;
329
- }, []);
330
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
331
- contentTypeUids
332
- );
333
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
334
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
335
- acc[locale.code] = { name: locale.name, code: locale.code };
336
- return acc;
337
- }, {});
338
- const formattedData = actions.map((action) => {
339
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
340
- return {
341
- ...action,
342
- entry: {
343
- id: action.entry.id,
344
- contentType: {
345
- displayName,
346
- mainFieldValue: action.entry[mainField]
652
+ if (release2.releasedAt) {
653
+ throw new utils.errors.ValidationError("Release already published");
654
+ }
655
+ const { entry, type } = action;
656
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
657
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
658
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
659
+ data: {
660
+ type,
661
+ contentType: entry.contentType,
662
+ locale: entry.locale,
663
+ isEntryValid,
664
+ entry: {
665
+ id: entry.id,
666
+ __type: entry.contentType,
667
+ __pivot: { field: "entry" }
347
668
  },
348
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
349
- status: action.entry.publishedAt ? "published" : "draft"
350
- }
351
- };
352
- });
353
- const groupName = getGroupName(groupBy);
354
- return ___default.default.groupBy(groupName)(formattedData);
355
- },
356
- async getContentTypesDataForActions(contentTypesUids) {
357
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
358
- const contentTypesData = {};
359
- for (const contentTypeUid of contentTypesUids) {
360
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
361
- uid: contentTypeUid
669
+ release: releaseId
670
+ },
671
+ populate: { release: { select: ["id"] }, entry: { select: ["id"] } }
362
672
  });
363
- contentTypesData[contentTypeUid] = {
364
- mainField: contentTypeConfig.settings.mainField,
365
- displayName: strapi2.getModel(contentTypeUid).info.displayName
366
- };
367
- }
368
- return contentTypesData;
369
- },
370
- async delete(releaseId) {
371
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
372
- populate: {
373
- actions: {
374
- fields: ["id"]
375
- }
673
+ this.updateReleaseStatus(releaseId);
674
+ return releaseAction2;
675
+ },
676
+ async findActions(releaseId, query) {
677
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
678
+ where: { id: releaseId },
679
+ select: ["id"]
680
+ });
681
+ if (!release2) {
682
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
376
683
  }
377
- });
378
- if (!release2) {
379
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
380
- }
381
- if (release2.releasedAt) {
382
- throw new utils.errors.ValidationError("Release already published");
383
- }
384
- await strapi2.db.transaction(async () => {
385
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
386
- where: {
387
- id: {
388
- $in: release2.actions.map((action) => action.id)
684
+ const dbQuery = utils.convertQueryParams.transformParamsToQuery(
685
+ RELEASE_ACTION_MODEL_UID,
686
+ query ?? {}
687
+ );
688
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
689
+ ...dbQuery,
690
+ populate: {
691
+ entry: {
692
+ populate: "*"
389
693
  }
694
+ },
695
+ where: {
696
+ release: releaseId
390
697
  }
391
698
  });
392
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
393
- });
394
- return release2;
395
- },
396
- async publish(releaseId) {
397
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
398
- RELEASE_MODEL_UID,
399
- releaseId,
400
- {
699
+ },
700
+ async countActions(query) {
701
+ const dbQuery = utils.convertQueryParams.transformParamsToQuery(
702
+ RELEASE_ACTION_MODEL_UID,
703
+ query ?? {}
704
+ );
705
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
706
+ },
707
+ async groupActions(actions, groupBy) {
708
+ const contentTypeUids = actions.reduce((acc, action) => {
709
+ if (!acc.includes(action.contentType)) {
710
+ acc.push(action.contentType);
711
+ }
712
+ return acc;
713
+ }, []);
714
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
715
+ contentTypeUids
716
+ );
717
+ const allLocalesDictionary = await this.getLocalesDataForActions();
718
+ const formattedData = actions.map((action) => {
719
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
720
+ return {
721
+ ...action,
722
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
723
+ contentType: {
724
+ displayName,
725
+ mainFieldValue: action.entry[mainField],
726
+ uid: action.contentType
727
+ }
728
+ };
729
+ });
730
+ const groupName = getGroupName(groupBy);
731
+ return ___default.default.groupBy(groupName)(formattedData);
732
+ },
733
+ async getLocalesDataForActions() {
734
+ if (!strapi2.plugin("i18n")) {
735
+ return {};
736
+ }
737
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
738
+ return allLocales.reduce((acc, locale) => {
739
+ acc[locale.code] = { name: locale.name, code: locale.code };
740
+ return acc;
741
+ }, {});
742
+ },
743
+ async getContentTypesDataForActions(contentTypesUids) {
744
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
745
+ const contentTypesData = {};
746
+ for (const contentTypeUid of contentTypesUids) {
747
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
748
+ uid: contentTypeUid
749
+ });
750
+ contentTypesData[contentTypeUid] = {
751
+ mainField: contentTypeConfig.settings.mainField,
752
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
753
+ };
754
+ }
755
+ return contentTypesData;
756
+ },
757
+ getContentTypeModelsFromActions(actions) {
758
+ const contentTypeUids = actions.reduce((acc, action) => {
759
+ if (!acc.includes(action.contentType)) {
760
+ acc.push(action.contentType);
761
+ }
762
+ return acc;
763
+ }, []);
764
+ const contentTypeModelsMap = contentTypeUids.reduce(
765
+ (acc, contentTypeUid) => {
766
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
767
+ return acc;
768
+ },
769
+ {}
770
+ );
771
+ return contentTypeModelsMap;
772
+ },
773
+ async getAllComponents() {
774
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
775
+ const components = await contentManagerComponentsService.findAllComponents();
776
+ const componentsMap = components.reduce(
777
+ (acc, component) => {
778
+ acc[component.uid] = component;
779
+ return acc;
780
+ },
781
+ {}
782
+ );
783
+ return componentsMap;
784
+ },
785
+ async delete(releaseId) {
786
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
787
+ where: { id: releaseId },
401
788
  populate: {
402
789
  actions: {
403
- populate: {
404
- entry: true
405
- }
790
+ select: ["id"]
406
791
  }
407
792
  }
793
+ });
794
+ if (!release2) {
795
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
408
796
  }
409
- );
410
- if (!releaseWithPopulatedActionEntries) {
411
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
412
- }
413
- if (releaseWithPopulatedActionEntries.releasedAt) {
414
- throw new utils.errors.ValidationError("Release already published");
415
- }
416
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
417
- throw new utils.errors.ValidationError("No entries to publish");
418
- }
419
- const actions = {};
420
- for (const action of releaseWithPopulatedActionEntries.actions) {
421
- const contentTypeUid = action.contentType;
422
- if (!actions[contentTypeUid]) {
423
- actions[contentTypeUid] = {
424
- publish: [],
425
- unpublish: []
426
- };
797
+ if (release2.releasedAt) {
798
+ throw new utils.errors.ValidationError("Release already published");
427
799
  }
428
- if (action.type === "publish") {
429
- actions[contentTypeUid].publish.push(action.entry);
430
- } else {
431
- actions[contentTypeUid].unpublish.push(action.entry);
800
+ await strapi2.db.transaction(async () => {
801
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
802
+ where: {
803
+ id: {
804
+ $in: release2.actions.map((action) => action.id)
805
+ }
806
+ }
807
+ });
808
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
809
+ where: {
810
+ id: releaseId
811
+ }
812
+ });
813
+ });
814
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
815
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
816
+ await schedulingService.cancel(release2.id);
432
817
  }
433
- }
434
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
435
- await strapi2.db.transaction(async () => {
436
- for (const contentTypeUid of Object.keys(actions)) {
437
- const { publish, unpublish } = actions[contentTypeUid];
438
- if (publish.length > 0) {
439
- await entityManagerService.publishMany(publish, contentTypeUid);
818
+ strapi2.telemetry.send("didDeleteContentRelease");
819
+ return release2;
820
+ },
821
+ async publish(releaseId) {
822
+ try {
823
+ const releaseWithPopulatedActionEntries = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
824
+ where: { id: releaseId },
825
+ populate: {
826
+ actions: {
827
+ populate: {
828
+ entry: {
829
+ select: ["id"]
830
+ }
831
+ }
832
+ }
833
+ }
834
+ });
835
+ if (!releaseWithPopulatedActionEntries) {
836
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
837
+ }
838
+ if (releaseWithPopulatedActionEntries.releasedAt) {
839
+ throw new utils.errors.ValidationError("Release already published");
840
+ }
841
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
842
+ throw new utils.errors.ValidationError("No entries to publish");
843
+ }
844
+ const collectionTypeActions = {};
845
+ const singleTypeActions = [];
846
+ for (const action of releaseWithPopulatedActionEntries.actions) {
847
+ const contentTypeUid = action.contentType;
848
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
849
+ if (!collectionTypeActions[contentTypeUid]) {
850
+ collectionTypeActions[contentTypeUid] = {
851
+ entriestoPublishIds: [],
852
+ entriesToUnpublishIds: []
853
+ };
854
+ }
855
+ if (action.type === "publish") {
856
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
857
+ } else {
858
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
859
+ }
860
+ } else {
861
+ singleTypeActions.push({
862
+ uid: contentTypeUid,
863
+ action: action.type,
864
+ id: action.entry.id
865
+ });
866
+ }
867
+ }
868
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
869
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
870
+ await strapi2.db.transaction(async () => {
871
+ for (const { uid, action, id } of singleTypeActions) {
872
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
873
+ const entry = await strapi2.db.query(uid).findOne({ where: { id }, populate });
874
+ try {
875
+ if (action === "publish") {
876
+ await entityManagerService.publish(entry, uid);
877
+ } else {
878
+ await entityManagerService.unpublish(entry, uid);
879
+ }
880
+ } catch (error) {
881
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
882
+ } else {
883
+ throw error;
884
+ }
885
+ }
886
+ }
887
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
888
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
889
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
890
+ const entriesToPublish = await strapi2.db.query(contentTypeUid).findMany({
891
+ where: {
892
+ id: {
893
+ $in: entriestoPublishIds
894
+ }
895
+ },
896
+ populate
897
+ });
898
+ const entriesToUnpublish = await strapi2.db.query(contentTypeUid).findMany({
899
+ where: {
900
+ id: {
901
+ $in: entriesToUnpublishIds
902
+ }
903
+ },
904
+ populate
905
+ });
906
+ if (entriesToPublish.length > 0) {
907
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
908
+ }
909
+ if (entriesToUnpublish.length > 0) {
910
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
911
+ }
912
+ }
913
+ });
914
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).update({
915
+ where: { id: releaseId },
916
+ data: {
917
+ releasedAt: /* @__PURE__ */ new Date()
918
+ },
919
+ populate: {
920
+ actions: {
921
+ count: true
922
+ }
923
+ }
924
+ });
925
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
926
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
927
+ isPublished: true,
928
+ release: release2
929
+ });
440
930
  }
441
- if (unpublish.length > 0) {
442
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
931
+ strapi2.telemetry.send("didPublishContentRelease");
932
+ return release2;
933
+ } catch (error) {
934
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
935
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
936
+ isPublished: false,
937
+ error
938
+ });
443
939
  }
940
+ strapi2.db.query(RELEASE_MODEL_UID).update({
941
+ where: { id: releaseId },
942
+ data: {
943
+ status: "failed"
944
+ }
945
+ });
946
+ throw error;
444
947
  }
445
- });
446
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
447
- data: {
448
- /*
449
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
450
- */
451
- // @ts-expect-error see above
452
- releasedAt: /* @__PURE__ */ new Date()
948
+ },
949
+ async updateAction(actionId, releaseId, update) {
950
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
951
+ where: {
952
+ id: actionId,
953
+ release: {
954
+ id: releaseId,
955
+ releasedAt: {
956
+ $null: true
957
+ }
958
+ }
959
+ },
960
+ data: update
961
+ });
962
+ if (!updatedAction) {
963
+ throw new utils.errors.NotFoundError(
964
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
965
+ );
453
966
  }
454
- });
455
- return release2;
456
- },
457
- async updateAction(actionId, releaseId, update) {
458
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
459
- where: {
460
- id: actionId,
461
- release: releaseId
462
- },
463
- data: update
464
- });
465
- if (!updatedAction) {
466
- throw new utils.errors.NotFoundError(
467
- `Action with id ${actionId} not found in release with id ${releaseId}`
468
- );
469
- }
470
- return updatedAction;
471
- },
472
- async deleteAction(actionId, releaseId) {
473
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
474
- where: {
475
- id: actionId,
476
- release: releaseId
967
+ return updatedAction;
968
+ },
969
+ async deleteAction(actionId, releaseId) {
970
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
971
+ where: {
972
+ id: actionId,
973
+ release: {
974
+ id: releaseId,
975
+ releasedAt: {
976
+ $null: true
977
+ }
978
+ }
979
+ }
980
+ });
981
+ if (!deletedAction) {
982
+ throw new utils.errors.NotFoundError(
983
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
984
+ );
477
985
  }
478
- });
479
- if (!deletedAction) {
480
- throw new utils.errors.NotFoundError(
481
- `Action with id ${actionId} not found in release with id ${releaseId}`
482
- );
986
+ this.updateReleaseStatus(releaseId);
987
+ return deletedAction;
988
+ },
989
+ async updateReleaseStatus(releaseId) {
990
+ const [totalActions, invalidActions] = await Promise.all([
991
+ this.countActions({
992
+ filters: {
993
+ release: releaseId
994
+ }
995
+ }),
996
+ this.countActions({
997
+ filters: {
998
+ release: releaseId,
999
+ isEntryValid: false
1000
+ }
1001
+ })
1002
+ ]);
1003
+ if (totalActions > 0) {
1004
+ if (invalidActions > 0) {
1005
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1006
+ where: {
1007
+ id: releaseId
1008
+ },
1009
+ data: {
1010
+ status: "blocked"
1011
+ }
1012
+ });
1013
+ }
1014
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1015
+ where: {
1016
+ id: releaseId
1017
+ },
1018
+ data: {
1019
+ status: "ready"
1020
+ }
1021
+ });
1022
+ }
1023
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1024
+ where: {
1025
+ id: releaseId
1026
+ },
1027
+ data: {
1028
+ status: "empty"
1029
+ }
1030
+ });
483
1031
  }
484
- return deletedAction;
485
- }
486
- });
1032
+ };
1033
+ };
487
1034
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
488
1035
  async validateUniqueEntry(releaseId, releaseActionArgs) {
489
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
490
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1036
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1037
+ where: {
1038
+ id: releaseId
1039
+ },
1040
+ populate: { actions: { populate: { entry: { select: ["id"] } } } }
491
1041
  });
492
1042
  if (!release2) {
493
1043
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
@@ -506,16 +1056,117 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
506
1056
  if (!contentType) {
507
1057
  throw new utils.errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
508
1058
  }
509
- if (!contentType.options?.draftAndPublish) {
510
- throw new utils.errors.ValidationError(
511
- `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
512
- );
1059
+ },
1060
+ async validatePendingReleasesLimit() {
1061
+ const maximumPendingReleases = strapi2.ee.features.get("cms-content-releases")?.options?.maximumReleases || 3;
1062
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1063
+ filters: {
1064
+ releasedAt: {
1065
+ $null: true
1066
+ }
1067
+ }
1068
+ });
1069
+ if (pendingReleasesCount >= maximumPendingReleases) {
1070
+ throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
1071
+ }
1072
+ },
1073
+ async validateUniqueNameForPendingRelease(name, id) {
1074
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1075
+ where: {
1076
+ releasedAt: {
1077
+ $null: true
1078
+ },
1079
+ name,
1080
+ ...id && { id: { $ne: id } }
1081
+ }
1082
+ });
1083
+ const isNameUnique = pendingReleases.length === 0;
1084
+ if (!isNameUnique) {
1085
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
1086
+ }
1087
+ },
1088
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1089
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1090
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
513
1091
  }
514
1092
  }
515
1093
  });
516
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
1094
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1095
+ const scheduledJobs = /* @__PURE__ */ new Map();
1096
+ return {
1097
+ async set(releaseId, scheduleDate) {
1098
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1099
+ if (!release2) {
1100
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1101
+ }
1102
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1103
+ try {
1104
+ await getService("release").publish(releaseId);
1105
+ } catch (error) {
1106
+ }
1107
+ this.cancel(releaseId);
1108
+ });
1109
+ if (scheduledJobs.has(releaseId)) {
1110
+ this.cancel(releaseId);
1111
+ }
1112
+ scheduledJobs.set(releaseId, job);
1113
+ return scheduledJobs;
1114
+ },
1115
+ cancel(releaseId) {
1116
+ if (scheduledJobs.has(releaseId)) {
1117
+ scheduledJobs.get(releaseId).cancel();
1118
+ scheduledJobs.delete(releaseId);
1119
+ }
1120
+ return scheduledJobs;
1121
+ },
1122
+ getAll() {
1123
+ return scheduledJobs;
1124
+ },
1125
+ /**
1126
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1127
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1128
+ * This also could be used to sync different Strapi instances in case of a cluster
1129
+ */
1130
+ async syncFromDatabase() {
1131
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1132
+ where: {
1133
+ scheduledAt: {
1134
+ $gte: /* @__PURE__ */ new Date()
1135
+ },
1136
+ releasedAt: null
1137
+ }
1138
+ });
1139
+ for (const release2 of releases) {
1140
+ this.set(release2.id, release2.scheduledAt);
1141
+ }
1142
+ return scheduledJobs;
1143
+ }
1144
+ };
1145
+ };
1146
+ const services = {
1147
+ release: createReleaseService,
1148
+ "release-validation": createReleaseValidationService,
1149
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1150
+ };
517
1151
  const RELEASE_SCHEMA = yup__namespace.object().shape({
518
- name: yup__namespace.string().trim().required()
1152
+ name: yup__namespace.string().trim().required(),
1153
+ scheduledAt: yup__namespace.string().nullable(),
1154
+ isScheduled: yup__namespace.boolean().optional(),
1155
+ time: yup__namespace.string().when("isScheduled", {
1156
+ is: true,
1157
+ then: yup__namespace.string().trim().required(),
1158
+ otherwise: yup__namespace.string().nullable()
1159
+ }),
1160
+ timezone: yup__namespace.string().when("isScheduled", {
1161
+ is: true,
1162
+ then: yup__namespace.string().required().nullable(),
1163
+ otherwise: yup__namespace.string().nullable()
1164
+ }),
1165
+ date: yup__namespace.string().when("isScheduled", {
1166
+ is: true,
1167
+ then: yup__namespace.string().required().nullable(),
1168
+ otherwise: yup__namespace.string().nullable()
1169
+ })
519
1170
  }).required().noUnknown();
520
1171
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
521
1172
  const releaseController = {
@@ -532,9 +1183,7 @@ const releaseController = {
532
1183
  const contentTypeUid = query.contentTypeUid;
533
1184
  const entryId = query.entryId;
534
1185
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
535
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
536
- hasEntryAttached
537
- });
1186
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
538
1187
  ctx.body = { data };
539
1188
  } else {
540
1189
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -550,26 +1199,30 @@ const releaseController = {
550
1199
  }
551
1200
  };
552
1201
  });
553
- ctx.body = { data, meta: { pagination } };
1202
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1203
+ where: {
1204
+ releasedAt: null
1205
+ }
1206
+ });
1207
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
554
1208
  }
555
1209
  },
556
1210
  async findOne(ctx) {
557
1211
  const id = ctx.params.id;
558
1212
  const releaseService = getService("release", { strapi });
559
1213
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
560
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
561
- ability: ctx.state.userAbility,
562
- model: RELEASE_MODEL_UID
563
- });
564
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1214
+ if (!release2) {
1215
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1216
+ }
565
1217
  const count = await releaseService.countActions({
566
1218
  filters: {
567
1219
  release: id
568
1220
  }
569
1221
  });
570
- if (!release2) {
571
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
572
- }
1222
+ const sanitizedRelease = {
1223
+ ...release2,
1224
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1225
+ };
573
1226
  const data = {
574
1227
  ...sanitizedRelease,
575
1228
  actions: {
@@ -622,8 +1275,27 @@ const releaseController = {
622
1275
  const id = ctx.params.id;
623
1276
  const releaseService = getService("release", { strapi });
624
1277
  const release2 = await releaseService.publish(id, { user });
1278
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1279
+ releaseService.countActions({
1280
+ filters: {
1281
+ release: id,
1282
+ type: "publish"
1283
+ }
1284
+ }),
1285
+ releaseService.countActions({
1286
+ filters: {
1287
+ release: id,
1288
+ type: "unpublish"
1289
+ }
1290
+ })
1291
+ ]);
625
1292
  ctx.body = {
626
- data: release2
1293
+ data: release2,
1294
+ meta: {
1295
+ totalEntries: countPublishActions + countUnpublishActions,
1296
+ totalPublishedEntries: countPublishActions,
1297
+ totalUnpublishedEntries: countUnpublishActions
1298
+ }
627
1299
  };
628
1300
  }
629
1301
  };
@@ -662,11 +1334,30 @@ const releaseActionController = {
662
1334
  sort: query.groupBy === "action" ? "type" : query.groupBy,
663
1335
  ...query
664
1336
  });
665
- const groupedData = await releaseService.groupActions(results, query.groupBy);
1337
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1338
+ if (acc[action.contentType]) {
1339
+ return acc;
1340
+ }
1341
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1342
+ ability: ctx.state.userAbility,
1343
+ model: action.contentType
1344
+ });
1345
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1346
+ return acc;
1347
+ }, {});
1348
+ const sanitizedResults = await utils.async.map(results, async (action) => ({
1349
+ ...action,
1350
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1351
+ }));
1352
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1353
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1354
+ const components = await releaseService.getAllComponents();
666
1355
  ctx.body = {
667
1356
  data: groupedData,
668
1357
  meta: {
669
- pagination
1358
+ pagination,
1359
+ contentTypes: contentTypes2,
1360
+ components
670
1361
  }
671
1362
  };
672
1363
  },
@@ -688,10 +1379,8 @@ const releaseActionController = {
688
1379
  async delete(ctx) {
689
1380
  const actionId = ctx.params.actionId;
690
1381
  const releaseId = ctx.params.releaseId;
691
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
692
- actionId,
693
- releaseId
694
- );
1382
+ const releaseService = getService("release", { strapi });
1383
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
695
1384
  ctx.body = {
696
1385
  data: deletedReleaseAction
697
1386
  };
@@ -872,11 +1561,12 @@ const routes = {
872
1561
  release,
873
1562
  "release-action": releaseAction
874
1563
  };
875
- const { features } = require("@strapi/strapi/dist/utils/ee");
876
1564
  const getPlugin = () => {
877
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1565
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
878
1566
  return {
879
1567
  register,
1568
+ bootstrap,
1569
+ destroy,
880
1570
  contentTypes,
881
1571
  services,
882
1572
  controllers,