@strapi/content-releases 0.0.0-next.6d59515520a3850456f256fb0e4c54b75054ddf4 → 0.0.0-next.aa7c7ec6724534e157d8a23fe85ee8318dabbf37

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.
@@ -3,6 +3,7 @@ const utils = require("@strapi/utils");
3
3
  const lodash = require("lodash");
4
4
  const _ = require("lodash/fp");
5
5
  const EE = require("@strapi/strapi/dist/utils/ee");
6
+ const nodeSchedule = require("node-schedule");
6
7
  const yup = require("yup");
7
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
8
9
  function _interopNamespace(e) {
@@ -106,6 +107,9 @@ const register = async ({ strapi: strapi2 }) => {
106
107
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
107
108
  }
108
109
  };
110
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
111
+ return strapi2.plugin("content-releases").service(name);
112
+ };
109
113
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
110
114
  const bootstrap = async ({ strapi: strapi2 }) => {
111
115
  if (features$1.isEnabled("cms-content-releases")) {
@@ -153,6 +157,24 @@ const bootstrap = async ({ strapi: strapi2 }) => {
153
157
  }
154
158
  }
155
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
+ }
156
178
  }
157
179
  };
158
180
  const schema$1 = {
@@ -181,6 +203,9 @@ const schema$1 = {
181
203
  releasedAt: {
182
204
  type: "datetime"
183
205
  },
206
+ scheduledAt: {
207
+ type: "datetime"
208
+ },
184
209
  actions: {
185
210
  type: "relation",
186
211
  relation: "oneToMany",
@@ -243,9 +268,6 @@ const contentTypes = {
243
268
  release: release$1,
244
269
  "release-action": releaseAction$1
245
270
  };
246
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
- return strapi2.plugin("content-releases").service(name);
248
- };
249
271
  const getGroupName = (queryValue) => {
250
272
  switch (queryValue) {
251
273
  case "contentType":
@@ -261,17 +283,24 @@ const getGroupName = (queryValue) => {
261
283
  const createReleaseService = ({ strapi: strapi2 }) => ({
262
284
  async create(releaseData, { user }) {
263
285
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
264
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
- "release-validation",
266
- { strapi: strapi2 }
267
- );
286
+ const {
287
+ validatePendingReleasesLimit,
288
+ validateUniqueNameForPendingRelease,
289
+ validateScheduledAtIsLaterThanNow
290
+ } = getService("release-validation", { strapi: strapi2 });
268
291
  await Promise.all([
269
292
  validatePendingReleasesLimit(),
270
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
293
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
294
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
271
295
  ]);
272
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
296
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
273
297
  data: releaseWithCreatorFields
274
298
  });
299
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
300
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
301
+ await schedulingService.set(release2.id, release2.scheduledAt);
302
+ }
303
+ return release2;
275
304
  },
276
305
  async findOne(id, query = {}) {
277
306
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -366,6 +395,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
366
395
  },
367
396
  async update(id, releaseData, { user }) {
368
397
  const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
398
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
399
+ "release-validation",
400
+ { strapi: strapi2 }
401
+ );
402
+ await Promise.all([
403
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
404
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
405
+ ]);
369
406
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
370
407
  if (!release2) {
371
408
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
@@ -381,6 +418,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
381
418
  // @ts-expect-error see above
382
419
  data: releaseWithCreatorFields
383
420
  });
421
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
422
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
423
+ if (releaseData.scheduledAt) {
424
+ await schedulingService.set(id, releaseData.scheduledAt);
425
+ } else if (release2.scheduledAt) {
426
+ schedulingService.cancel(id);
427
+ }
428
+ }
384
429
  return updatedRelease;
385
430
  },
386
431
  async createAction(releaseId, action) {
@@ -565,27 +610,53 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
565
610
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
566
611
  throw new utils.errors.ValidationError("No entries to publish");
567
612
  }
568
- const actions = {};
613
+ const collectionTypeActions = {};
614
+ const singleTypeActions = [];
569
615
  for (const action of releaseWithPopulatedActionEntries.actions) {
570
616
  const contentTypeUid = action.contentType;
571
- if (!actions[contentTypeUid]) {
572
- actions[contentTypeUid] = {
573
- entriestoPublishIds: [],
574
- entriesToUnpublishIds: []
575
- };
576
- }
577
- if (action.type === "publish") {
578
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
617
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
618
+ if (!collectionTypeActions[contentTypeUid]) {
619
+ collectionTypeActions[contentTypeUid] = {
620
+ entriestoPublishIds: [],
621
+ entriesToUnpublishIds: []
622
+ };
623
+ }
624
+ if (action.type === "publish") {
625
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
626
+ } else {
627
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
628
+ }
579
629
  } else {
580
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
630
+ singleTypeActions.push({
631
+ uid: contentTypeUid,
632
+ action: action.type,
633
+ id: action.entry.id
634
+ });
581
635
  }
582
636
  }
583
637
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
638
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
585
639
  await strapi2.db.transaction(async () => {
586
- for (const contentTypeUid of Object.keys(actions)) {
640
+ for (const { uid, action, id } of singleTypeActions) {
641
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
642
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
643
+ try {
644
+ if (action === "publish") {
645
+ await entityManagerService.publish(entry, uid);
646
+ } else {
647
+ await entityManagerService.unpublish(entry, uid);
648
+ }
649
+ } catch (error) {
650
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
651
+ ;
652
+ else {
653
+ throw error;
654
+ }
655
+ }
656
+ }
657
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
587
658
  const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
659
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
589
660
  const entriesToPublish = await strapi2.entityService.findMany(
590
661
  contentTypeUid,
591
662
  {
@@ -711,27 +782,88 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
711
782
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
712
783
  }
713
784
  },
714
- async validateUniqueNameForPendingRelease(name) {
785
+ async validateUniqueNameForPendingRelease(name, id) {
715
786
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
787
  filters: {
717
788
  releasedAt: {
718
789
  $null: true
719
790
  },
720
- name
791
+ name,
792
+ ...id && { id: { $ne: id } }
721
793
  }
722
794
  });
723
795
  const isNameUnique = pendingReleases.length === 0;
724
796
  if (!isNameUnique) {
725
797
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
798
  }
799
+ },
800
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
801
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
802
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
803
+ }
727
804
  }
728
805
  });
806
+ const createSchedulingService = ({ strapi: strapi2 }) => {
807
+ const scheduledJobs = /* @__PURE__ */ new Map();
808
+ return {
809
+ async set(releaseId, scheduleDate) {
810
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
811
+ if (!release2) {
812
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
813
+ }
814
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
815
+ try {
816
+ await getService("release").publish(releaseId);
817
+ } catch (error) {
818
+ }
819
+ this.cancel(releaseId);
820
+ });
821
+ if (scheduledJobs.has(releaseId)) {
822
+ this.cancel(releaseId);
823
+ }
824
+ scheduledJobs.set(releaseId, job);
825
+ return scheduledJobs;
826
+ },
827
+ cancel(releaseId) {
828
+ if (scheduledJobs.has(releaseId)) {
829
+ scheduledJobs.get(releaseId).cancel();
830
+ scheduledJobs.delete(releaseId);
831
+ }
832
+ return scheduledJobs;
833
+ },
834
+ getAll() {
835
+ return scheduledJobs;
836
+ },
837
+ /**
838
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
839
+ * This is useful in case the server was restarted and the scheduled jobs were lost
840
+ * This also could be used to sync different Strapi instances in case of a cluster
841
+ */
842
+ async syncFromDatabase() {
843
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
844
+ where: {
845
+ scheduledAt: {
846
+ $gte: /* @__PURE__ */ new Date()
847
+ },
848
+ releasedAt: null
849
+ }
850
+ });
851
+ for (const release2 of releases) {
852
+ this.set(release2.id, release2.scheduledAt);
853
+ }
854
+ return scheduledJobs;
855
+ }
856
+ };
857
+ };
729
858
  const services = {
730
859
  release: createReleaseService,
731
- "release-validation": createReleaseValidationService
860
+ "release-validation": createReleaseValidationService,
861
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
732
862
  };
733
863
  const RELEASE_SCHEMA = yup__namespace.object().shape({
734
- name: yup__namespace.string().trim().required()
864
+ name: yup__namespace.string().trim().required(),
865
+ // scheduledAt is a date, but we always receive strings from the client
866
+ scheduledAt: yup__namespace.string().nullable()
735
867
  }).required().noUnknown();
736
868
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
737
869
  const releaseController = {
@@ -771,19 +903,18 @@ const releaseController = {
771
903
  const id = ctx.params.id;
772
904
  const releaseService = getService("release", { strapi });
773
905
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
774
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
775
- ability: ctx.state.userAbility,
776
- model: RELEASE_MODEL_UID
777
- });
778
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
906
+ if (!release2) {
907
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
908
+ }
779
909
  const count = await releaseService.countActions({
780
910
  filters: {
781
911
  release: id
782
912
  }
783
913
  });
784
- if (!release2) {
785
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
786
- }
914
+ const sanitizedRelease = {
915
+ ...release2,
916
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
917
+ };
787
918
  const data = {
788
919
  ...sanitizedRelease,
789
920
  actions: {
@@ -836,8 +967,27 @@ const releaseController = {
836
967
  const id = ctx.params.id;
837
968
  const releaseService = getService("release", { strapi });
838
969
  const release2 = await releaseService.publish(id, { user });
970
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
971
+ releaseService.countActions({
972
+ filters: {
973
+ release: id,
974
+ type: "publish"
975
+ }
976
+ }),
977
+ releaseService.countActions({
978
+ filters: {
979
+ release: id,
980
+ type: "unpublish"
981
+ }
982
+ })
983
+ ]);
839
984
  ctx.body = {
840
- data: release2
985
+ data: release2,
986
+ meta: {
987
+ totalEntries: countPublishActions + countUnpublishActions,
988
+ totalPublishedEntries: countPublishActions,
989
+ totalUnpublishedEntries: countUnpublishActions
990
+ }
841
991
  };
842
992
  }
843
993
  };
@@ -1109,6 +1259,7 @@ const getPlugin = () => {
1109
1259
  return {
1110
1260
  register,
1111
1261
  bootstrap,
1262
+ destroy,
1112
1263
  contentTypes,
1113
1264
  services,
1114
1265
  controllers,