@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,7 +1,9 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const lodash = require("lodash");
3
4
  const _ = require("lodash/fp");
4
5
  const EE = require("@strapi/strapi/dist/utils/ee");
6
+ const nodeSchedule = require("node-schedule");
5
7
  const yup = require("yup");
6
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
7
9
  function _interopNamespace(e) {
@@ -71,40 +73,50 @@ const ACTIONS = [
71
73
  pluginName: "content-releases"
72
74
  }
73
75
  ];
74
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
75
- return strapi2.plugin("content-releases").service(name);
76
- };
76
+ async function deleteActionsOnDisableDraftAndPublish({
77
+ oldContentTypes,
78
+ contentTypes: contentTypes2
79
+ }) {
80
+ if (!oldContentTypes) {
81
+ return;
82
+ }
83
+ for (const uid in contentTypes2) {
84
+ if (!oldContentTypes[uid]) {
85
+ continue;
86
+ }
87
+ const oldContentType = oldContentTypes[uid];
88
+ const contentType = contentTypes2[uid];
89
+ if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
90
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
91
+ }
92
+ }
93
+ }
94
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
95
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
96
+ if (deletedContentTypes.length) {
97
+ await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
98
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
99
+ });
100
+ }
101
+ }
77
102
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
78
103
  const register = async ({ strapi: strapi2 }) => {
79
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
104
+ if (features$2.isEnabled("cms-content-releases")) {
80
105
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
81
- const releaseActionService = getService("release-action", { strapi: strapi2 });
82
- const eventManager = getService("event-manager", { strapi: strapi2 });
83
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
84
- "content-type.update",
85
- async ({ contentType }) => {
86
- if (contentType.schema.options.draftAndPublish === false) {
87
- await releaseActionService.deleteManyForContentType(contentType.uid);
88
- }
89
- }
90
- );
91
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
92
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
93
- "content-type.delete",
94
- async ({ contentType }) => {
95
- await releaseActionService.deleteManyForContentType(contentType.uid);
96
- }
97
- );
98
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
106
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
107
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
99
108
  }
100
109
  };
110
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
111
+ return strapi2.plugin("content-releases").service(name);
112
+ };
101
113
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
102
114
  const bootstrap = async ({ strapi: strapi2 }) => {
103
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
115
+ if (features$1.isEnabled("cms-content-releases")) {
104
116
  strapi2.db.lifecycles.subscribe({
105
117
  afterDelete(event) {
106
118
  const { model, result } = event;
107
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
119
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
108
120
  const { id } = result;
109
121
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
110
122
  where: {
@@ -120,7 +132,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
120
132
  */
121
133
  async beforeDeleteMany(event) {
122
134
  const { model, params } = event;
123
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
135
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
124
136
  const { where } = params;
125
137
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
126
138
  event.state.entriesToDelete = entriesToDelete;
@@ -145,6 +157,24 @@ const bootstrap = async ({ strapi: strapi2 }) => {
145
157
  }
146
158
  }
147
159
  });
160
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
161
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
162
+ strapi2.log.error(
163
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
164
+ );
165
+ throw err;
166
+ });
167
+ }
168
+ }
169
+ };
170
+ const destroy = async ({ strapi: strapi2 }) => {
171
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
172
+ const scheduledJobs = getService("scheduling", {
173
+ strapi: strapi2
174
+ }).getAll();
175
+ for (const [, job] of scheduledJobs) {
176
+ job.cancel();
177
+ }
148
178
  }
149
179
  };
150
180
  const schema$1 = {
@@ -173,6 +203,12 @@ const schema$1 = {
173
203
  releasedAt: {
174
204
  type: "datetime"
175
205
  },
206
+ scheduledAt: {
207
+ type: "datetime"
208
+ },
209
+ timezone: {
210
+ type: "string"
211
+ },
176
212
  actions: {
177
213
  type: "relation",
178
214
  relation: "oneToMany",
@@ -235,15 +271,6 @@ const contentTypes = {
235
271
  release: release$1,
236
272
  "release-action": releaseAction$1
237
273
  };
238
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
239
- async deleteManyForContentType(contentTypeUid) {
240
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
241
- where: {
242
- target_type: contentTypeUid
243
- }
244
- });
245
- }
246
- });
247
274
  const getGroupName = (queryValue) => {
248
275
  switch (queryValue) {
249
276
  case "contentType":
@@ -259,10 +286,25 @@ const getGroupName = (queryValue) => {
259
286
  const createReleaseService = ({ strapi: strapi2 }) => ({
260
287
  async create(releaseData, { user }) {
261
288
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
262
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
263
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
289
+ const {
290
+ validatePendingReleasesLimit,
291
+ validateUniqueNameForPendingRelease,
292
+ validateScheduledAtIsLaterThanNow
293
+ } = getService("release-validation", { strapi: strapi2 });
294
+ await Promise.all([
295
+ validatePendingReleasesLimit(),
296
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
297
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
298
+ ]);
299
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
264
300
  data: releaseWithCreatorFields
265
301
  });
302
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
303
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
304
+ await schedulingService.set(release2.id, release2.scheduledAt);
305
+ }
306
+ strapi2.telemetry.send("didCreateContentRelease");
307
+ return release2;
266
308
  },
267
309
  async findOne(id, query = {}) {
268
310
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -281,51 +323,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
281
323
  }
282
324
  });
283
325
  },
284
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
285
- hasEntryAttached
286
- } = {
287
- hasEntryAttached: false
288
- }) {
289
- const whereActions = hasEntryAttached ? {
290
- // Find all Releases where the content type entry is present
291
- actions: {
292
- target_type: contentTypeUid,
293
- target_id: entryId
294
- }
295
- } : {
296
- // Find all Releases where the content type entry is not present
297
- $or: [
298
- {
299
- $not: {
300
- actions: {
301
- target_type: contentTypeUid,
302
- target_id: entryId
303
- }
304
- }
326
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
327
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
328
+ where: {
329
+ actions: {
330
+ target_type: contentTypeUid,
331
+ target_id: entryId
305
332
  },
306
- {
307
- actions: null
333
+ releasedAt: {
334
+ $null: true
308
335
  }
309
- ]
310
- };
311
- const populateAttachedAction = hasEntryAttached ? {
312
- // Filter the action to get only the content type entry
313
- actions: {
314
- where: {
336
+ },
337
+ populate: {
338
+ // Filter the action to get only the content type entry
339
+ actions: {
340
+ where: {
341
+ target_type: contentTypeUid,
342
+ target_id: entryId
343
+ }
344
+ }
345
+ }
346
+ });
347
+ return releases.map((release2) => {
348
+ if (release2.actions?.length) {
349
+ const [actionForEntry] = release2.actions;
350
+ delete release2.actions;
351
+ return {
352
+ ...release2,
353
+ action: actionForEntry
354
+ };
355
+ }
356
+ return release2;
357
+ });
358
+ },
359
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
360
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
361
+ where: {
362
+ releasedAt: {
363
+ $null: true
364
+ },
365
+ actions: {
315
366
  target_type: contentTypeUid,
316
367
  target_id: entryId
317
368
  }
318
369
  }
319
- } : {};
370
+ });
320
371
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
321
372
  where: {
322
- ...whereActions,
373
+ $or: [
374
+ {
375
+ id: {
376
+ $notIn: releasesRelated.map((release2) => release2.id)
377
+ }
378
+ },
379
+ {
380
+ actions: null
381
+ }
382
+ ],
323
383
  releasedAt: {
324
384
  $null: true
325
385
  }
326
- },
327
- populate: {
328
- ...populateAttachedAction
329
386
  }
330
387
  });
331
388
  return releases.map((release2) => {
@@ -342,6 +399,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
342
399
  },
343
400
  async update(id, releaseData, { user }) {
344
401
  const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
402
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
403
+ "release-validation",
404
+ { strapi: strapi2 }
405
+ );
406
+ await Promise.all([
407
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
408
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
409
+ ]);
345
410
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
346
411
  if (!release2) {
347
412
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
@@ -357,6 +422,15 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
357
422
  // @ts-expect-error see above
358
423
  data: releaseWithCreatorFields
359
424
  });
425
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
426
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
427
+ if (releaseData.scheduledAt) {
428
+ await schedulingService.set(id, releaseData.scheduledAt);
429
+ } else if (release2.scheduledAt) {
430
+ schedulingService.cancel(id);
431
+ }
432
+ }
433
+ strapi2.telemetry.send("didUpdateContentRelease");
360
434
  return updatedRelease;
361
435
  },
362
436
  async createAction(releaseId, action) {
@@ -514,6 +588,11 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
514
588
  });
515
589
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
516
590
  });
591
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
592
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
593
+ await schedulingService.cancel(release2.id);
594
+ }
595
+ strapi2.telemetry.send("didDeleteContentRelease");
517
596
  return release2;
518
597
  },
519
598
  async publish(releaseId) {
@@ -524,7 +603,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
524
603
  populate: {
525
604
  actions: {
526
605
  populate: {
527
- entry: true
606
+ entry: {
607
+ fields: ["id"]
608
+ }
528
609
  }
529
610
  }
530
611
  }
@@ -539,30 +620,80 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
539
620
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
540
621
  throw new utils.errors.ValidationError("No entries to publish");
541
622
  }
542
- const actions = {};
623
+ const collectionTypeActions = {};
624
+ const singleTypeActions = [];
543
625
  for (const action of releaseWithPopulatedActionEntries.actions) {
544
626
  const contentTypeUid = action.contentType;
545
- if (!actions[contentTypeUid]) {
546
- actions[contentTypeUid] = {
547
- publish: [],
548
- unpublish: []
549
- };
550
- }
551
- if (action.type === "publish") {
552
- actions[contentTypeUid].publish.push(action.entry);
627
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
628
+ if (!collectionTypeActions[contentTypeUid]) {
629
+ collectionTypeActions[contentTypeUid] = {
630
+ entriestoPublishIds: [],
631
+ entriesToUnpublishIds: []
632
+ };
633
+ }
634
+ if (action.type === "publish") {
635
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
636
+ } else {
637
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
638
+ }
553
639
  } else {
554
- actions[contentTypeUid].unpublish.push(action.entry);
640
+ singleTypeActions.push({
641
+ uid: contentTypeUid,
642
+ action: action.type,
643
+ id: action.entry.id
644
+ });
555
645
  }
556
646
  }
557
647
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
648
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
558
649
  await strapi2.db.transaction(async () => {
559
- for (const contentTypeUid of Object.keys(actions)) {
560
- const { publish, unpublish } = actions[contentTypeUid];
561
- if (publish.length > 0) {
562
- await entityManagerService.publishMany(publish, contentTypeUid);
650
+ for (const { uid, action, id } of singleTypeActions) {
651
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
652
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
653
+ try {
654
+ if (action === "publish") {
655
+ await entityManagerService.publish(entry, uid);
656
+ } else {
657
+ await entityManagerService.unpublish(entry, uid);
658
+ }
659
+ } catch (error) {
660
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
661
+ ;
662
+ else {
663
+ throw error;
664
+ }
665
+ }
666
+ }
667
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
668
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
669
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
670
+ const entriesToPublish = await strapi2.entityService.findMany(
671
+ contentTypeUid,
672
+ {
673
+ filters: {
674
+ id: {
675
+ $in: entriestoPublishIds
676
+ }
677
+ },
678
+ populate
679
+ }
680
+ );
681
+ const entriesToUnpublish = await strapi2.entityService.findMany(
682
+ contentTypeUid,
683
+ {
684
+ filters: {
685
+ id: {
686
+ $in: entriesToUnpublishIds
687
+ }
688
+ },
689
+ populate
690
+ }
691
+ );
692
+ if (entriesToPublish.length > 0) {
693
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
563
694
  }
564
- if (unpublish.length > 0) {
565
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
695
+ if (entriesToUnpublish.length > 0) {
696
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
566
697
  }
567
698
  }
568
699
  });
@@ -575,6 +706,7 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
575
706
  releasedAt: /* @__PURE__ */ new Date()
576
707
  }
577
708
  });
709
+ strapi2.telemetry.send("didPublishContentRelease");
578
710
  return release2;
579
711
  },
580
712
  async updateAction(actionId, releaseId, update) {
@@ -660,34 +792,94 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
660
792
  if (pendingReleasesCount >= maximumPendingReleases) {
661
793
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
662
794
  }
795
+ },
796
+ async validateUniqueNameForPendingRelease(name, id) {
797
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
798
+ filters: {
799
+ releasedAt: {
800
+ $null: true
801
+ },
802
+ name,
803
+ ...id && { id: { $ne: id } }
804
+ }
805
+ });
806
+ const isNameUnique = pendingReleases.length === 0;
807
+ if (!isNameUnique) {
808
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
809
+ }
810
+ },
811
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
812
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
813
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
814
+ }
663
815
  }
664
816
  });
665
- const createEventManagerService = () => {
666
- const state = {
667
- destroyListenerCallbacks: []
668
- };
817
+ const createSchedulingService = ({ strapi: strapi2 }) => {
818
+ const scheduledJobs = /* @__PURE__ */ new Map();
669
819
  return {
670
- addDestroyListenerCallback(destroyListenerCallback) {
671
- state.destroyListenerCallbacks.push(destroyListenerCallback);
820
+ async set(releaseId, scheduleDate) {
821
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
822
+ if (!release2) {
823
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
824
+ }
825
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
826
+ try {
827
+ await getService("release").publish(releaseId);
828
+ } catch (error) {
829
+ }
830
+ this.cancel(releaseId);
831
+ });
832
+ if (scheduledJobs.has(releaseId)) {
833
+ this.cancel(releaseId);
834
+ }
835
+ scheduledJobs.set(releaseId, job);
836
+ return scheduledJobs;
672
837
  },
673
- destroyAllListeners() {
674
- if (!state.destroyListenerCallbacks.length) {
675
- return;
838
+ cancel(releaseId) {
839
+ if (scheduledJobs.has(releaseId)) {
840
+ scheduledJobs.get(releaseId).cancel();
841
+ scheduledJobs.delete(releaseId);
676
842
  }
677
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
678
- destroyListenerCallback();
843
+ return scheduledJobs;
844
+ },
845
+ getAll() {
846
+ return scheduledJobs;
847
+ },
848
+ /**
849
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
850
+ * This is useful in case the server was restarted and the scheduled jobs were lost
851
+ * This also could be used to sync different Strapi instances in case of a cluster
852
+ */
853
+ async syncFromDatabase() {
854
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
855
+ where: {
856
+ scheduledAt: {
857
+ $gte: /* @__PURE__ */ new Date()
858
+ },
859
+ releasedAt: null
860
+ }
679
861
  });
862
+ for (const release2 of releases) {
863
+ this.set(release2.id, release2.scheduledAt);
864
+ }
865
+ return scheduledJobs;
680
866
  }
681
867
  };
682
868
  };
683
869
  const services = {
684
870
  release: createReleaseService,
685
- "release-action": createReleaseActionService,
686
871
  "release-validation": createReleaseValidationService,
687
- "event-manager": createEventManagerService
872
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
688
873
  };
689
874
  const RELEASE_SCHEMA = yup__namespace.object().shape({
690
- name: yup__namespace.string().trim().required()
875
+ name: yup__namespace.string().trim().required(),
876
+ // scheduledAt is a date, but we always receive strings from the client
877
+ scheduledAt: yup__namespace.string().nullable(),
878
+ timezone: yup__namespace.string().when("scheduledAt", {
879
+ is: (scheduledAt) => !!scheduledAt,
880
+ then: yup__namespace.string().required(),
881
+ otherwise: yup__namespace.string().nullable()
882
+ })
691
883
  }).required().noUnknown();
692
884
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
693
885
  const releaseController = {
@@ -704,9 +896,7 @@ const releaseController = {
704
896
  const contentTypeUid = query.contentTypeUid;
705
897
  const entryId = query.entryId;
706
898
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
707
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
708
- hasEntryAttached
709
- });
899
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
710
900
  ctx.body = { data };
711
901
  } else {
712
902
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -729,19 +919,18 @@ const releaseController = {
729
919
  const id = ctx.params.id;
730
920
  const releaseService = getService("release", { strapi });
731
921
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
732
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
733
- ability: ctx.state.userAbility,
734
- model: RELEASE_MODEL_UID
735
- });
736
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
922
+ if (!release2) {
923
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
924
+ }
737
925
  const count = await releaseService.countActions({
738
926
  filters: {
739
927
  release: id
740
928
  }
741
929
  });
742
- if (!release2) {
743
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
744
- }
930
+ const sanitizedRelease = {
931
+ ...release2,
932
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
933
+ };
745
934
  const data = {
746
935
  ...sanitizedRelease,
747
936
  actions: {
@@ -794,8 +983,27 @@ const releaseController = {
794
983
  const id = ctx.params.id;
795
984
  const releaseService = getService("release", { strapi });
796
985
  const release2 = await releaseService.publish(id, { user });
986
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
987
+ releaseService.countActions({
988
+ filters: {
989
+ release: id,
990
+ type: "publish"
991
+ }
992
+ }),
993
+ releaseService.countActions({
994
+ filters: {
995
+ release: id,
996
+ type: "unpublish"
997
+ }
998
+ })
999
+ ]);
797
1000
  ctx.body = {
798
- data: release2
1001
+ data: release2,
1002
+ meta: {
1003
+ totalEntries: countPublishActions + countUnpublishActions,
1004
+ totalPublishedEntries: countPublishActions,
1005
+ totalUnpublishedEntries: countUnpublishActions
1006
+ }
799
1007
  };
800
1008
  }
801
1009
  };
@@ -1063,19 +1271,15 @@ const routes = {
1063
1271
  };
1064
1272
  const { features } = require("@strapi/strapi/dist/utils/ee");
1065
1273
  const getPlugin = () => {
1066
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1274
+ if (features.isEnabled("cms-content-releases")) {
1067
1275
  return {
1068
1276
  register,
1069
1277
  bootstrap,
1278
+ destroy,
1070
1279
  contentTypes,
1071
1280
  services,
1072
1281
  controllers,
1073
- routes,
1074
- destroy() {
1075
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1076
- getService("event-manager").destroyAllListeners();
1077
- }
1078
- }
1282
+ routes
1079
1283
  };
1080
1284
  }
1081
1285
  return {