@strapi/content-releases 0.0.0-next.56199ab7a5f3320e0debcbe4a24fe0b8cd599e21 → 0.0.0-next.615ae85762cbae9fc80af36685075ef25abd1c88

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 (29) hide show
  1. package/dist/_chunks/{App-YFvVMqB8.js → App-1hHIqUoZ.js} +253 -191
  2. package/dist/_chunks/App-1hHIqUoZ.js.map +1 -0
  3. package/dist/_chunks/{App-8J9a-MD5.mjs → App-U6GbyLIE.mjs} +257 -195
  4. package/dist/_chunks/App-U6GbyLIE.mjs.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-MyLPoISH.mjs → en-GqXgfmzl.mjs} +9 -3
  10. package/dist/_chunks/en-GqXgfmzl.mjs.map +1 -0
  11. package/dist/_chunks/{en-gYDqKYFd.js → en-bDhIlw-B.js} +9 -3
  12. package/dist/_chunks/en-bDhIlw-B.js.map +1 -0
  13. package/dist/_chunks/{index-ej8MzbQl.mjs → index-gkExFBa0.mjs} +92 -29
  14. package/dist/_chunks/index-gkExFBa0.mjs.map +1 -0
  15. package/dist/_chunks/{index-vxli-E-l.js → index-l-FvkQlQ.js} +91 -28
  16. package/dist/_chunks/index-l-FvkQlQ.js.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +1 -1
  19. package/dist/server/index.js +324 -120
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +325 -121
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +10 -9
  24. package/dist/_chunks/App-8J9a-MD5.mjs.map +0 -1
  25. package/dist/_chunks/App-YFvVMqB8.js.map +0 -1
  26. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  27. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  28. package/dist/_chunks/index-ej8MzbQl.mjs.map +0 -1
  29. package/dist/_chunks/index-vxli-E-l.js.map +0 -1
@@ -1,6 +1,8 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1, mapAsync } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import { difference, keys } from "lodash";
2
3
  import _ from "lodash/fp";
3
4
  import EE from "@strapi/strapi/dist/utils/ee";
5
+ import { scheduleJob } from "node-schedule";
4
6
  import * as yup from "yup";
5
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
6
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -48,40 +50,50 @@ const ACTIONS = [
48
50
  pluginName: "content-releases"
49
51
  }
50
52
  ];
51
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
52
- return strapi2.plugin("content-releases").service(name);
53
- };
53
+ async function deleteActionsOnDisableDraftAndPublish({
54
+ oldContentTypes,
55
+ contentTypes: contentTypes2
56
+ }) {
57
+ if (!oldContentTypes) {
58
+ return;
59
+ }
60
+ for (const uid in contentTypes2) {
61
+ if (!oldContentTypes[uid]) {
62
+ continue;
63
+ }
64
+ const oldContentType = oldContentTypes[uid];
65
+ const contentType = contentTypes2[uid];
66
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
67
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
68
+ }
69
+ }
70
+ }
71
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
72
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
73
+ if (deletedContentTypes.length) {
74
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
75
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
76
+ });
77
+ }
78
+ }
54
79
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
55
80
  const register = async ({ strapi: strapi2 }) => {
56
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
81
+ if (features$2.isEnabled("cms-content-releases")) {
57
82
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
58
- const releaseActionService = getService("release-action", { strapi: strapi2 });
59
- const eventManager = getService("event-manager", { strapi: strapi2 });
60
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
61
- "content-type.update",
62
- async ({ contentType }) => {
63
- if (contentType.schema.options.draftAndPublish === false) {
64
- await releaseActionService.deleteManyForContentType(contentType.uid);
65
- }
66
- }
67
- );
68
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
69
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
70
- "content-type.delete",
71
- async ({ contentType }) => {
72
- await releaseActionService.deleteManyForContentType(contentType.uid);
73
- }
74
- );
75
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
83
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
84
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
76
85
  }
77
86
  };
87
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
88
+ return strapi2.plugin("content-releases").service(name);
89
+ };
78
90
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
79
91
  const bootstrap = async ({ strapi: strapi2 }) => {
80
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
92
+ if (features$1.isEnabled("cms-content-releases")) {
81
93
  strapi2.db.lifecycles.subscribe({
82
94
  afterDelete(event) {
83
95
  const { model, result } = event;
84
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
96
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
85
97
  const { id } = result;
86
98
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
87
99
  where: {
@@ -97,7 +109,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
97
109
  */
98
110
  async beforeDeleteMany(event) {
99
111
  const { model, params } = event;
100
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
112
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
101
113
  const { where } = params;
102
114
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
103
115
  event.state.entriesToDelete = entriesToDelete;
@@ -122,6 +134,24 @@ const bootstrap = async ({ strapi: strapi2 }) => {
122
134
  }
123
135
  }
124
136
  });
137
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
138
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
139
+ strapi2.log.error(
140
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
141
+ );
142
+ throw err;
143
+ });
144
+ }
145
+ }
146
+ };
147
+ const destroy = async ({ strapi: strapi2 }) => {
148
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
149
+ const scheduledJobs = getService("scheduling", {
150
+ strapi: strapi2
151
+ }).getAll();
152
+ for (const [, job] of scheduledJobs) {
153
+ job.cancel();
154
+ }
125
155
  }
126
156
  };
127
157
  const schema$1 = {
@@ -150,6 +180,12 @@ const schema$1 = {
150
180
  releasedAt: {
151
181
  type: "datetime"
152
182
  },
183
+ scheduledAt: {
184
+ type: "datetime"
185
+ },
186
+ timezone: {
187
+ type: "string"
188
+ },
153
189
  actions: {
154
190
  type: "relation",
155
191
  relation: "oneToMany",
@@ -212,15 +248,6 @@ const contentTypes = {
212
248
  release: release$1,
213
249
  "release-action": releaseAction$1
214
250
  };
215
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
216
- async deleteManyForContentType(contentTypeUid) {
217
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
218
- where: {
219
- target_type: contentTypeUid
220
- }
221
- });
222
- }
223
- });
224
251
  const getGroupName = (queryValue) => {
225
252
  switch (queryValue) {
226
253
  case "contentType":
@@ -236,10 +263,25 @@ const getGroupName = (queryValue) => {
236
263
  const createReleaseService = ({ strapi: strapi2 }) => ({
237
264
  async create(releaseData, { user }) {
238
265
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
240
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
266
+ const {
267
+ validatePendingReleasesLimit,
268
+ validateUniqueNameForPendingRelease,
269
+ validateScheduledAtIsLaterThanNow
270
+ } = getService("release-validation", { strapi: strapi2 });
271
+ await Promise.all([
272
+ validatePendingReleasesLimit(),
273
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
274
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
275
+ ]);
276
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
241
277
  data: releaseWithCreatorFields
242
278
  });
279
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
280
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
281
+ await schedulingService.set(release2.id, release2.scheduledAt);
282
+ }
283
+ strapi2.telemetry.send("didCreateContentRelease");
284
+ return release2;
243
285
  },
244
286
  async findOne(id, query = {}) {
245
287
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -258,51 +300,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
258
300
  }
259
301
  });
260
302
  },
261
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
262
- hasEntryAttached
263
- } = {
264
- hasEntryAttached: false
265
- }) {
266
- const whereActions = hasEntryAttached ? {
267
- // Find all Releases where the content type entry is present
268
- actions: {
269
- target_type: contentTypeUid,
270
- target_id: entryId
271
- }
272
- } : {
273
- // Find all Releases where the content type entry is not present
274
- $or: [
275
- {
276
- $not: {
277
- actions: {
278
- target_type: contentTypeUid,
279
- target_id: entryId
280
- }
281
- }
303
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
304
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
+ where: {
306
+ actions: {
307
+ target_type: contentTypeUid,
308
+ target_id: entryId
282
309
  },
283
- {
284
- actions: null
310
+ releasedAt: {
311
+ $null: true
285
312
  }
286
- ]
287
- };
288
- const populateAttachedAction = hasEntryAttached ? {
289
- // Filter the action to get only the content type entry
290
- actions: {
291
- where: {
313
+ },
314
+ populate: {
315
+ // Filter the action to get only the content type entry
316
+ actions: {
317
+ where: {
318
+ target_type: contentTypeUid,
319
+ target_id: entryId
320
+ }
321
+ }
322
+ }
323
+ });
324
+ return releases.map((release2) => {
325
+ if (release2.actions?.length) {
326
+ const [actionForEntry] = release2.actions;
327
+ delete release2.actions;
328
+ return {
329
+ ...release2,
330
+ action: actionForEntry
331
+ };
332
+ }
333
+ return release2;
334
+ });
335
+ },
336
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
337
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
338
+ where: {
339
+ releasedAt: {
340
+ $null: true
341
+ },
342
+ actions: {
292
343
  target_type: contentTypeUid,
293
344
  target_id: entryId
294
345
  }
295
346
  }
296
- } : {};
347
+ });
297
348
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
298
349
  where: {
299
- ...whereActions,
350
+ $or: [
351
+ {
352
+ id: {
353
+ $notIn: releasesRelated.map((release2) => release2.id)
354
+ }
355
+ },
356
+ {
357
+ actions: null
358
+ }
359
+ ],
300
360
  releasedAt: {
301
361
  $null: true
302
362
  }
303
- },
304
- populate: {
305
- ...populateAttachedAction
306
363
  }
307
364
  });
308
365
  return releases.map((release2) => {
@@ -319,6 +376,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
319
376
  },
320
377
  async update(id, releaseData, { user }) {
321
378
  const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
379
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
380
+ "release-validation",
381
+ { strapi: strapi2 }
382
+ );
383
+ await Promise.all([
384
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
385
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
386
+ ]);
322
387
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
323
388
  if (!release2) {
324
389
  throw new errors.NotFoundError(`No release found for id ${id}`);
@@ -334,6 +399,15 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
334
399
  // @ts-expect-error see above
335
400
  data: releaseWithCreatorFields
336
401
  });
402
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
403
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
404
+ if (releaseData.scheduledAt) {
405
+ await schedulingService.set(id, releaseData.scheduledAt);
406
+ } else if (release2.scheduledAt) {
407
+ schedulingService.cancel(id);
408
+ }
409
+ }
410
+ strapi2.telemetry.send("didUpdateContentRelease");
337
411
  return updatedRelease;
338
412
  },
339
413
  async createAction(releaseId, action) {
@@ -491,6 +565,11 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
491
565
  });
492
566
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
493
567
  });
568
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
569
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
570
+ await schedulingService.cancel(release2.id);
571
+ }
572
+ strapi2.telemetry.send("didDeleteContentRelease");
494
573
  return release2;
495
574
  },
496
575
  async publish(releaseId) {
@@ -501,7 +580,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
501
580
  populate: {
502
581
  actions: {
503
582
  populate: {
504
- entry: true
583
+ entry: {
584
+ fields: ["id"]
585
+ }
505
586
  }
506
587
  }
507
588
  }
@@ -516,30 +597,80 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
516
597
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
517
598
  throw new errors.ValidationError("No entries to publish");
518
599
  }
519
- const actions = {};
600
+ const collectionTypeActions = {};
601
+ const singleTypeActions = [];
520
602
  for (const action of releaseWithPopulatedActionEntries.actions) {
521
603
  const contentTypeUid = action.contentType;
522
- if (!actions[contentTypeUid]) {
523
- actions[contentTypeUid] = {
524
- publish: [],
525
- unpublish: []
526
- };
527
- }
528
- if (action.type === "publish") {
529
- actions[contentTypeUid].publish.push(action.entry);
604
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
605
+ if (!collectionTypeActions[contentTypeUid]) {
606
+ collectionTypeActions[contentTypeUid] = {
607
+ entriestoPublishIds: [],
608
+ entriesToUnpublishIds: []
609
+ };
610
+ }
611
+ if (action.type === "publish") {
612
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
613
+ } else {
614
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
615
+ }
530
616
  } else {
531
- actions[contentTypeUid].unpublish.push(action.entry);
617
+ singleTypeActions.push({
618
+ uid: contentTypeUid,
619
+ action: action.type,
620
+ id: action.entry.id
621
+ });
532
622
  }
533
623
  }
534
624
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
625
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
535
626
  await strapi2.db.transaction(async () => {
536
- for (const contentTypeUid of Object.keys(actions)) {
537
- const { publish, unpublish } = actions[contentTypeUid];
538
- if (publish.length > 0) {
539
- await entityManagerService.publishMany(publish, contentTypeUid);
627
+ for (const { uid, action, id } of singleTypeActions) {
628
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
629
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
630
+ try {
631
+ if (action === "publish") {
632
+ await entityManagerService.publish(entry, uid);
633
+ } else {
634
+ await entityManagerService.unpublish(entry, uid);
635
+ }
636
+ } catch (error) {
637
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
638
+ ;
639
+ else {
640
+ throw error;
641
+ }
642
+ }
643
+ }
644
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
645
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
646
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
647
+ const entriesToPublish = await strapi2.entityService.findMany(
648
+ contentTypeUid,
649
+ {
650
+ filters: {
651
+ id: {
652
+ $in: entriestoPublishIds
653
+ }
654
+ },
655
+ populate
656
+ }
657
+ );
658
+ const entriesToUnpublish = await strapi2.entityService.findMany(
659
+ contentTypeUid,
660
+ {
661
+ filters: {
662
+ id: {
663
+ $in: entriesToUnpublishIds
664
+ }
665
+ },
666
+ populate
667
+ }
668
+ );
669
+ if (entriesToPublish.length > 0) {
670
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
540
671
  }
541
- if (unpublish.length > 0) {
542
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
672
+ if (entriesToUnpublish.length > 0) {
673
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
543
674
  }
544
675
  }
545
676
  });
@@ -552,6 +683,7 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
552
683
  releasedAt: /* @__PURE__ */ new Date()
553
684
  }
554
685
  });
686
+ strapi2.telemetry.send("didPublishContentRelease");
555
687
  return release2;
556
688
  },
557
689
  async updateAction(actionId, releaseId, update) {
@@ -637,34 +769,94 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
637
769
  if (pendingReleasesCount >= maximumPendingReleases) {
638
770
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
639
771
  }
772
+ },
773
+ async validateUniqueNameForPendingRelease(name, id) {
774
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
775
+ filters: {
776
+ releasedAt: {
777
+ $null: true
778
+ },
779
+ name,
780
+ ...id && { id: { $ne: id } }
781
+ }
782
+ });
783
+ const isNameUnique = pendingReleases.length === 0;
784
+ if (!isNameUnique) {
785
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
786
+ }
787
+ },
788
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
789
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
790
+ throw new errors.ValidationError("Scheduled at must be later than now");
791
+ }
640
792
  }
641
793
  });
642
- const createEventManagerService = () => {
643
- const state = {
644
- destroyListenerCallbacks: []
645
- };
794
+ const createSchedulingService = ({ strapi: strapi2 }) => {
795
+ const scheduledJobs = /* @__PURE__ */ new Map();
646
796
  return {
647
- addDestroyListenerCallback(destroyListenerCallback) {
648
- state.destroyListenerCallbacks.push(destroyListenerCallback);
797
+ async set(releaseId, scheduleDate) {
798
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
799
+ if (!release2) {
800
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
801
+ }
802
+ const job = scheduleJob(scheduleDate, async () => {
803
+ try {
804
+ await getService("release").publish(releaseId);
805
+ } catch (error) {
806
+ }
807
+ this.cancel(releaseId);
808
+ });
809
+ if (scheduledJobs.has(releaseId)) {
810
+ this.cancel(releaseId);
811
+ }
812
+ scheduledJobs.set(releaseId, job);
813
+ return scheduledJobs;
649
814
  },
650
- destroyAllListeners() {
651
- if (!state.destroyListenerCallbacks.length) {
652
- return;
815
+ cancel(releaseId) {
816
+ if (scheduledJobs.has(releaseId)) {
817
+ scheduledJobs.get(releaseId).cancel();
818
+ scheduledJobs.delete(releaseId);
653
819
  }
654
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
655
- destroyListenerCallback();
820
+ return scheduledJobs;
821
+ },
822
+ getAll() {
823
+ return scheduledJobs;
824
+ },
825
+ /**
826
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
827
+ * This is useful in case the server was restarted and the scheduled jobs were lost
828
+ * This also could be used to sync different Strapi instances in case of a cluster
829
+ */
830
+ async syncFromDatabase() {
831
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
832
+ where: {
833
+ scheduledAt: {
834
+ $gte: /* @__PURE__ */ new Date()
835
+ },
836
+ releasedAt: null
837
+ }
656
838
  });
839
+ for (const release2 of releases) {
840
+ this.set(release2.id, release2.scheduledAt);
841
+ }
842
+ return scheduledJobs;
657
843
  }
658
844
  };
659
845
  };
660
846
  const services = {
661
847
  release: createReleaseService,
662
- "release-action": createReleaseActionService,
663
848
  "release-validation": createReleaseValidationService,
664
- "event-manager": createEventManagerService
849
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
665
850
  };
666
851
  const RELEASE_SCHEMA = yup.object().shape({
667
- name: yup.string().trim().required()
852
+ name: yup.string().trim().required(),
853
+ // scheduledAt is a date, but we always receive strings from the client
854
+ scheduledAt: yup.string().nullable(),
855
+ timezone: yup.string().when("scheduledAt", {
856
+ is: (scheduledAt) => !!scheduledAt,
857
+ then: yup.string().required(),
858
+ otherwise: yup.string().nullable()
859
+ })
668
860
  }).required().noUnknown();
669
861
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
670
862
  const releaseController = {
@@ -681,9 +873,7 @@ const releaseController = {
681
873
  const contentTypeUid = query.contentTypeUid;
682
874
  const entryId = query.entryId;
683
875
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
684
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
685
- hasEntryAttached
686
- });
876
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
687
877
  ctx.body = { data };
688
878
  } else {
689
879
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -706,19 +896,18 @@ const releaseController = {
706
896
  const id = ctx.params.id;
707
897
  const releaseService = getService("release", { strapi });
708
898
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
709
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
710
- ability: ctx.state.userAbility,
711
- model: RELEASE_MODEL_UID
712
- });
713
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
899
+ if (!release2) {
900
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
901
+ }
714
902
  const count = await releaseService.countActions({
715
903
  filters: {
716
904
  release: id
717
905
  }
718
906
  });
719
- if (!release2) {
720
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
721
- }
907
+ const sanitizedRelease = {
908
+ ...release2,
909
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
910
+ };
722
911
  const data = {
723
912
  ...sanitizedRelease,
724
913
  actions: {
@@ -771,8 +960,27 @@ const releaseController = {
771
960
  const id = ctx.params.id;
772
961
  const releaseService = getService("release", { strapi });
773
962
  const release2 = await releaseService.publish(id, { user });
963
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
964
+ releaseService.countActions({
965
+ filters: {
966
+ release: id,
967
+ type: "publish"
968
+ }
969
+ }),
970
+ releaseService.countActions({
971
+ filters: {
972
+ release: id,
973
+ type: "unpublish"
974
+ }
975
+ })
976
+ ]);
774
977
  ctx.body = {
775
- data: release2
978
+ data: release2,
979
+ meta: {
980
+ totalEntries: countPublishActions + countUnpublishActions,
981
+ totalPublishedEntries: countPublishActions,
982
+ totalUnpublishedEntries: countUnpublishActions
983
+ }
776
984
  };
777
985
  }
778
986
  };
@@ -1040,19 +1248,15 @@ const routes = {
1040
1248
  };
1041
1249
  const { features } = require("@strapi/strapi/dist/utils/ee");
1042
1250
  const getPlugin = () => {
1043
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1251
+ if (features.isEnabled("cms-content-releases")) {
1044
1252
  return {
1045
1253
  register,
1046
1254
  bootstrap,
1255
+ destroy,
1047
1256
  contentTypes,
1048
1257
  services,
1049
1258
  controllers,
1050
- routes,
1051
- destroy() {
1052
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1053
- getService("event-manager").destroyAllListeners();
1054
- }
1055
- }
1259
+ routes
1056
1260
  };
1057
1261
  }
1058
1262
  return {