@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.
@@ -2,6 +2,7 @@ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, val
2
2
  import { difference, keys } from "lodash";
3
3
  import _ from "lodash/fp";
4
4
  import EE from "@strapi/strapi/dist/utils/ee";
5
+ import { scheduleJob } from "node-schedule";
5
6
  import * as yup from "yup";
6
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
7
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -83,6 +84,9 @@ const register = async ({ strapi: strapi2 }) => {
83
84
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
84
85
  }
85
86
  };
87
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
88
+ return strapi2.plugin("content-releases").service(name);
89
+ };
86
90
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
87
91
  const bootstrap = async ({ strapi: strapi2 }) => {
88
92
  if (features$1.isEnabled("cms-content-releases")) {
@@ -130,6 +134,24 @@ const bootstrap = async ({ strapi: strapi2 }) => {
130
134
  }
131
135
  }
132
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
+ }
133
155
  }
134
156
  };
135
157
  const schema$1 = {
@@ -158,6 +180,9 @@ const schema$1 = {
158
180
  releasedAt: {
159
181
  type: "datetime"
160
182
  },
183
+ scheduledAt: {
184
+ type: "datetime"
185
+ },
161
186
  actions: {
162
187
  type: "relation",
163
188
  relation: "oneToMany",
@@ -220,9 +245,6 @@ const contentTypes = {
220
245
  release: release$1,
221
246
  "release-action": releaseAction$1
222
247
  };
223
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
- return strapi2.plugin("content-releases").service(name);
225
- };
226
248
  const getGroupName = (queryValue) => {
227
249
  switch (queryValue) {
228
250
  case "contentType":
@@ -238,17 +260,24 @@ const getGroupName = (queryValue) => {
238
260
  const createReleaseService = ({ strapi: strapi2 }) => ({
239
261
  async create(releaseData, { user }) {
240
262
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
241
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
- "release-validation",
243
- { strapi: strapi2 }
244
- );
263
+ const {
264
+ validatePendingReleasesLimit,
265
+ validateUniqueNameForPendingRelease,
266
+ validateScheduledAtIsLaterThanNow
267
+ } = getService("release-validation", { strapi: strapi2 });
245
268
  await Promise.all([
246
269
  validatePendingReleasesLimit(),
247
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
270
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
271
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
248
272
  ]);
249
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
273
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
250
274
  data: releaseWithCreatorFields
251
275
  });
276
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
277
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
278
+ await schedulingService.set(release2.id, release2.scheduledAt);
279
+ }
280
+ return release2;
252
281
  },
253
282
  async findOne(id, query = {}) {
254
283
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -343,6 +372,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
343
372
  },
344
373
  async update(id, releaseData, { user }) {
345
374
  const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
375
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
376
+ "release-validation",
377
+ { strapi: strapi2 }
378
+ );
379
+ await Promise.all([
380
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
381
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
382
+ ]);
346
383
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
347
384
  if (!release2) {
348
385
  throw new errors.NotFoundError(`No release found for id ${id}`);
@@ -358,6 +395,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
358
395
  // @ts-expect-error see above
359
396
  data: releaseWithCreatorFields
360
397
  });
398
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
399
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
400
+ if (releaseData.scheduledAt) {
401
+ await schedulingService.set(id, releaseData.scheduledAt);
402
+ } else if (release2.scheduledAt) {
403
+ schedulingService.cancel(id);
404
+ }
405
+ }
361
406
  return updatedRelease;
362
407
  },
363
408
  async createAction(releaseId, action) {
@@ -542,27 +587,53 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
542
587
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
543
588
  throw new errors.ValidationError("No entries to publish");
544
589
  }
545
- const actions = {};
590
+ const collectionTypeActions = {};
591
+ const singleTypeActions = [];
546
592
  for (const action of releaseWithPopulatedActionEntries.actions) {
547
593
  const contentTypeUid = action.contentType;
548
- if (!actions[contentTypeUid]) {
549
- actions[contentTypeUid] = {
550
- entriestoPublishIds: [],
551
- entriesToUnpublishIds: []
552
- };
553
- }
554
- if (action.type === "publish") {
555
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
594
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
595
+ if (!collectionTypeActions[contentTypeUid]) {
596
+ collectionTypeActions[contentTypeUid] = {
597
+ entriestoPublishIds: [],
598
+ entriesToUnpublishIds: []
599
+ };
600
+ }
601
+ if (action.type === "publish") {
602
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
603
+ } else {
604
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
605
+ }
556
606
  } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
607
+ singleTypeActions.push({
608
+ uid: contentTypeUid,
609
+ action: action.type,
610
+ id: action.entry.id
611
+ });
558
612
  }
559
613
  }
560
614
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
615
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
562
616
  await strapi2.db.transaction(async () => {
563
- for (const contentTypeUid of Object.keys(actions)) {
617
+ for (const { uid, action, id } of singleTypeActions) {
618
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
619
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
620
+ try {
621
+ if (action === "publish") {
622
+ await entityManagerService.publish(entry, uid);
623
+ } else {
624
+ await entityManagerService.unpublish(entry, uid);
625
+ }
626
+ } catch (error) {
627
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
628
+ ;
629
+ else {
630
+ throw error;
631
+ }
632
+ }
633
+ }
634
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
564
635
  const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
636
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
566
637
  const entriesToPublish = await strapi2.entityService.findMany(
567
638
  contentTypeUid,
568
639
  {
@@ -688,27 +759,88 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
759
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
760
  }
690
761
  },
691
- async validateUniqueNameForPendingRelease(name) {
762
+ async validateUniqueNameForPendingRelease(name, id) {
692
763
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
764
  filters: {
694
765
  releasedAt: {
695
766
  $null: true
696
767
  },
697
- name
768
+ name,
769
+ ...id && { id: { $ne: id } }
698
770
  }
699
771
  });
700
772
  const isNameUnique = pendingReleases.length === 0;
701
773
  if (!isNameUnique) {
702
774
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
775
  }
776
+ },
777
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
778
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
779
+ throw new errors.ValidationError("Scheduled at must be later than now");
780
+ }
704
781
  }
705
782
  });
783
+ const createSchedulingService = ({ strapi: strapi2 }) => {
784
+ const scheduledJobs = /* @__PURE__ */ new Map();
785
+ return {
786
+ async set(releaseId, scheduleDate) {
787
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
788
+ if (!release2) {
789
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
790
+ }
791
+ const job = scheduleJob(scheduleDate, async () => {
792
+ try {
793
+ await getService("release").publish(releaseId);
794
+ } catch (error) {
795
+ }
796
+ this.cancel(releaseId);
797
+ });
798
+ if (scheduledJobs.has(releaseId)) {
799
+ this.cancel(releaseId);
800
+ }
801
+ scheduledJobs.set(releaseId, job);
802
+ return scheduledJobs;
803
+ },
804
+ cancel(releaseId) {
805
+ if (scheduledJobs.has(releaseId)) {
806
+ scheduledJobs.get(releaseId).cancel();
807
+ scheduledJobs.delete(releaseId);
808
+ }
809
+ return scheduledJobs;
810
+ },
811
+ getAll() {
812
+ return scheduledJobs;
813
+ },
814
+ /**
815
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
816
+ * This is useful in case the server was restarted and the scheduled jobs were lost
817
+ * This also could be used to sync different Strapi instances in case of a cluster
818
+ */
819
+ async syncFromDatabase() {
820
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
821
+ where: {
822
+ scheduledAt: {
823
+ $gte: /* @__PURE__ */ new Date()
824
+ },
825
+ releasedAt: null
826
+ }
827
+ });
828
+ for (const release2 of releases) {
829
+ this.set(release2.id, release2.scheduledAt);
830
+ }
831
+ return scheduledJobs;
832
+ }
833
+ };
834
+ };
706
835
  const services = {
707
836
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
837
+ "release-validation": createReleaseValidationService,
838
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
709
839
  };
710
840
  const RELEASE_SCHEMA = yup.object().shape({
711
- name: yup.string().trim().required()
841
+ name: yup.string().trim().required(),
842
+ // scheduledAt is a date, but we always receive strings from the client
843
+ scheduledAt: yup.string().nullable()
712
844
  }).required().noUnknown();
713
845
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
846
  const releaseController = {
@@ -748,19 +880,18 @@ const releaseController = {
748
880
  const id = ctx.params.id;
749
881
  const releaseService = getService("release", { strapi });
750
882
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
751
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
752
- ability: ctx.state.userAbility,
753
- model: RELEASE_MODEL_UID
754
- });
755
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
883
+ if (!release2) {
884
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
885
+ }
756
886
  const count = await releaseService.countActions({
757
887
  filters: {
758
888
  release: id
759
889
  }
760
890
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
891
+ const sanitizedRelease = {
892
+ ...release2,
893
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
894
+ };
764
895
  const data = {
765
896
  ...sanitizedRelease,
766
897
  actions: {
@@ -813,8 +944,27 @@ const releaseController = {
813
944
  const id = ctx.params.id;
814
945
  const releaseService = getService("release", { strapi });
815
946
  const release2 = await releaseService.publish(id, { user });
947
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
948
+ releaseService.countActions({
949
+ filters: {
950
+ release: id,
951
+ type: "publish"
952
+ }
953
+ }),
954
+ releaseService.countActions({
955
+ filters: {
956
+ release: id,
957
+ type: "unpublish"
958
+ }
959
+ })
960
+ ]);
816
961
  ctx.body = {
817
- data: release2
962
+ data: release2,
963
+ meta: {
964
+ totalEntries: countPublishActions + countUnpublishActions,
965
+ totalPublishedEntries: countPublishActions,
966
+ totalUnpublishedEntries: countUnpublishActions
967
+ }
818
968
  };
819
969
  }
820
970
  };
@@ -1086,6 +1236,7 @@ const getPlugin = () => {
1086
1236
  return {
1087
1237
  register,
1088
1238
  bootstrap,
1239
+ destroy,
1089
1240
  contentTypes,
1090
1241
  services,
1091
1242
  controllers,