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

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-_20W9dYa.js → App-1hHIqUoZ.js} +253 -191
  2. package/dist/_chunks/App-1hHIqUoZ.js.map +1 -0
  3. package/dist/_chunks/{App-L1jSxCiL.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-c4zRX_sg.mjs → index-gkExFBa0.mjs} +89 -26
  14. package/dist/_chunks/index-gkExFBa0.mjs.map +1 -0
  15. package/dist/_chunks/{index-KJa1Rb5F.js → index-l-FvkQlQ.js} +88 -25
  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 +197 -34
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +197 -34
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +10 -9
  24. package/dist/_chunks/App-L1jSxCiL.mjs.map +0 -1
  25. package/dist/_chunks/App-_20W9dYa.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-KJa1Rb5F.js.map +0 -1
  29. package/dist/_chunks/index-c4zRX_sg.mjs.map +0 -1
@@ -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,12 @@ const schema$1 = {
158
180
  releasedAt: {
159
181
  type: "datetime"
160
182
  },
183
+ scheduledAt: {
184
+ type: "datetime"
185
+ },
186
+ timezone: {
187
+ type: "string"
188
+ },
161
189
  actions: {
162
190
  type: "relation",
163
191
  relation: "oneToMany",
@@ -220,9 +248,6 @@ const contentTypes = {
220
248
  release: release$1,
221
249
  "release-action": releaseAction$1
222
250
  };
223
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
- return strapi2.plugin("content-releases").service(name);
225
- };
226
251
  const getGroupName = (queryValue) => {
227
252
  switch (queryValue) {
228
253
  case "contentType":
@@ -238,17 +263,24 @@ const getGroupName = (queryValue) => {
238
263
  const createReleaseService = ({ strapi: strapi2 }) => ({
239
264
  async create(releaseData, { user }) {
240
265
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
241
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
- "release-validation",
243
- { strapi: strapi2 }
244
- );
266
+ const {
267
+ validatePendingReleasesLimit,
268
+ validateUniqueNameForPendingRelease,
269
+ validateScheduledAtIsLaterThanNow
270
+ } = getService("release-validation", { strapi: strapi2 });
245
271
  await Promise.all([
246
272
  validatePendingReleasesLimit(),
247
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
273
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
274
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
248
275
  ]);
249
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
276
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
250
277
  data: releaseWithCreatorFields
251
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
+ return release2;
252
284
  },
253
285
  async findOne(id, query = {}) {
254
286
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -343,6 +375,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
343
375
  },
344
376
  async update(id, releaseData, { user }) {
345
377
  const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
378
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
379
+ "release-validation",
380
+ { strapi: strapi2 }
381
+ );
382
+ await Promise.all([
383
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
384
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
385
+ ]);
346
386
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
347
387
  if (!release2) {
348
388
  throw new errors.NotFoundError(`No release found for id ${id}`);
@@ -358,6 +398,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
358
398
  // @ts-expect-error see above
359
399
  data: releaseWithCreatorFields
360
400
  });
401
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
402
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
403
+ if (releaseData.scheduledAt) {
404
+ await schedulingService.set(id, releaseData.scheduledAt);
405
+ } else if (release2.scheduledAt) {
406
+ schedulingService.cancel(id);
407
+ }
408
+ }
361
409
  return updatedRelease;
362
410
  },
363
411
  async createAction(releaseId, action) {
@@ -515,6 +563,10 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
515
563
  });
516
564
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
517
565
  });
566
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
567
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
568
+ await schedulingService.cancel(release2.id);
569
+ }
518
570
  return release2;
519
571
  },
520
572
  async publish(releaseId) {
@@ -542,27 +594,53 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
542
594
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
543
595
  throw new errors.ValidationError("No entries to publish");
544
596
  }
545
- const actions = {};
597
+ const collectionTypeActions = {};
598
+ const singleTypeActions = [];
546
599
  for (const action of releaseWithPopulatedActionEntries.actions) {
547
600
  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);
601
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
602
+ if (!collectionTypeActions[contentTypeUid]) {
603
+ collectionTypeActions[contentTypeUid] = {
604
+ entriestoPublishIds: [],
605
+ entriesToUnpublishIds: []
606
+ };
607
+ }
608
+ if (action.type === "publish") {
609
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
610
+ } else {
611
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
612
+ }
556
613
  } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
614
+ singleTypeActions.push({
615
+ uid: contentTypeUid,
616
+ action: action.type,
617
+ id: action.entry.id
618
+ });
558
619
  }
559
620
  }
560
621
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
622
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
562
623
  await strapi2.db.transaction(async () => {
563
- for (const contentTypeUid of Object.keys(actions)) {
624
+ for (const { uid, action, id } of singleTypeActions) {
625
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
626
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
627
+ try {
628
+ if (action === "publish") {
629
+ await entityManagerService.publish(entry, uid);
630
+ } else {
631
+ await entityManagerService.unpublish(entry, uid);
632
+ }
633
+ } catch (error) {
634
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
635
+ ;
636
+ else {
637
+ throw error;
638
+ }
639
+ }
640
+ }
641
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
564
642
  const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
643
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
566
644
  const entriesToPublish = await strapi2.entityService.findMany(
567
645
  contentTypeUid,
568
646
  {
@@ -688,27 +766,93 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
766
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
767
  }
690
768
  },
691
- async validateUniqueNameForPendingRelease(name) {
769
+ async validateUniqueNameForPendingRelease(name, id) {
692
770
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
771
  filters: {
694
772
  releasedAt: {
695
773
  $null: true
696
774
  },
697
- name
775
+ name,
776
+ ...id && { id: { $ne: id } }
698
777
  }
699
778
  });
700
779
  const isNameUnique = pendingReleases.length === 0;
701
780
  if (!isNameUnique) {
702
781
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
782
  }
783
+ },
784
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
785
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
786
+ throw new errors.ValidationError("Scheduled at must be later than now");
787
+ }
704
788
  }
705
789
  });
790
+ const createSchedulingService = ({ strapi: strapi2 }) => {
791
+ const scheduledJobs = /* @__PURE__ */ new Map();
792
+ return {
793
+ async set(releaseId, scheduleDate) {
794
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
795
+ if (!release2) {
796
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
797
+ }
798
+ const job = scheduleJob(scheduleDate, async () => {
799
+ try {
800
+ await getService("release").publish(releaseId);
801
+ } catch (error) {
802
+ }
803
+ this.cancel(releaseId);
804
+ });
805
+ if (scheduledJobs.has(releaseId)) {
806
+ this.cancel(releaseId);
807
+ }
808
+ scheduledJobs.set(releaseId, job);
809
+ return scheduledJobs;
810
+ },
811
+ cancel(releaseId) {
812
+ if (scheduledJobs.has(releaseId)) {
813
+ scheduledJobs.get(releaseId).cancel();
814
+ scheduledJobs.delete(releaseId);
815
+ }
816
+ return scheduledJobs;
817
+ },
818
+ getAll() {
819
+ return scheduledJobs;
820
+ },
821
+ /**
822
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
823
+ * This is useful in case the server was restarted and the scheduled jobs were lost
824
+ * This also could be used to sync different Strapi instances in case of a cluster
825
+ */
826
+ async syncFromDatabase() {
827
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
828
+ where: {
829
+ scheduledAt: {
830
+ $gte: /* @__PURE__ */ new Date()
831
+ },
832
+ releasedAt: null
833
+ }
834
+ });
835
+ for (const release2 of releases) {
836
+ this.set(release2.id, release2.scheduledAt);
837
+ }
838
+ return scheduledJobs;
839
+ }
840
+ };
841
+ };
706
842
  const services = {
707
843
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
844
+ "release-validation": createReleaseValidationService,
845
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
709
846
  };
710
847
  const RELEASE_SCHEMA = yup.object().shape({
711
- name: yup.string().trim().required()
848
+ name: yup.string().trim().required(),
849
+ // scheduledAt is a date, but we always receive strings from the client
850
+ scheduledAt: yup.string().nullable(),
851
+ timezone: yup.string().when("scheduledAt", {
852
+ is: (scheduledAt) => !!scheduledAt,
853
+ then: yup.string().required(),
854
+ otherwise: yup.string().nullable()
855
+ })
712
856
  }).required().noUnknown();
713
857
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
858
  const releaseController = {
@@ -748,19 +892,18 @@ const releaseController = {
748
892
  const id = ctx.params.id;
749
893
  const releaseService = getService("release", { strapi });
750
894
  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);
895
+ if (!release2) {
896
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
897
+ }
756
898
  const count = await releaseService.countActions({
757
899
  filters: {
758
900
  release: id
759
901
  }
760
902
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
903
+ const sanitizedRelease = {
904
+ ...release2,
905
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
906
+ };
764
907
  const data = {
765
908
  ...sanitizedRelease,
766
909
  actions: {
@@ -813,8 +956,27 @@ const releaseController = {
813
956
  const id = ctx.params.id;
814
957
  const releaseService = getService("release", { strapi });
815
958
  const release2 = await releaseService.publish(id, { user });
959
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
960
+ releaseService.countActions({
961
+ filters: {
962
+ release: id,
963
+ type: "publish"
964
+ }
965
+ }),
966
+ releaseService.countActions({
967
+ filters: {
968
+ release: id,
969
+ type: "unpublish"
970
+ }
971
+ })
972
+ ]);
816
973
  ctx.body = {
817
- data: release2
974
+ data: release2,
975
+ meta: {
976
+ totalEntries: countPublishActions + countUnpublishActions,
977
+ totalPublishedEntries: countPublishActions,
978
+ totalUnpublishedEntries: countUnpublishActions
979
+ }
818
980
  };
819
981
  }
820
982
  };
@@ -1086,6 +1248,7 @@ const getPlugin = () => {
1086
1248
  return {
1087
1249
  register,
1088
1250
  bootstrap,
1251
+ destroy,
1089
1252
  contentTypes,
1090
1253
  services,
1091
1254
  controllers,