@strapi/content-releases 4.19.1 → 4.20.1

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 +193 -34
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +193 -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) {
@@ -542,27 +590,53 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
542
590
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
543
591
  throw new errors.ValidationError("No entries to publish");
544
592
  }
545
- const actions = {};
593
+ const collectionTypeActions = {};
594
+ const singleTypeActions = [];
546
595
  for (const action of releaseWithPopulatedActionEntries.actions) {
547
596
  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);
597
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
598
+ if (!collectionTypeActions[contentTypeUid]) {
599
+ collectionTypeActions[contentTypeUid] = {
600
+ entriestoPublishIds: [],
601
+ entriesToUnpublishIds: []
602
+ };
603
+ }
604
+ if (action.type === "publish") {
605
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
606
+ } else {
607
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
608
+ }
556
609
  } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
610
+ singleTypeActions.push({
611
+ uid: contentTypeUid,
612
+ action: action.type,
613
+ id: action.entry.id
614
+ });
558
615
  }
559
616
  }
560
617
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
618
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
562
619
  await strapi2.db.transaction(async () => {
563
- for (const contentTypeUid of Object.keys(actions)) {
620
+ for (const { uid, action, id } of singleTypeActions) {
621
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
622
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
623
+ try {
624
+ if (action === "publish") {
625
+ await entityManagerService.publish(entry, uid);
626
+ } else {
627
+ await entityManagerService.unpublish(entry, uid);
628
+ }
629
+ } catch (error) {
630
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
631
+ ;
632
+ else {
633
+ throw error;
634
+ }
635
+ }
636
+ }
637
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
564
638
  const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
639
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
566
640
  const entriesToPublish = await strapi2.entityService.findMany(
567
641
  contentTypeUid,
568
642
  {
@@ -688,27 +762,93 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
762
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
763
  }
690
764
  },
691
- async validateUniqueNameForPendingRelease(name) {
765
+ async validateUniqueNameForPendingRelease(name, id) {
692
766
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
767
  filters: {
694
768
  releasedAt: {
695
769
  $null: true
696
770
  },
697
- name
771
+ name,
772
+ ...id && { id: { $ne: id } }
698
773
  }
699
774
  });
700
775
  const isNameUnique = pendingReleases.length === 0;
701
776
  if (!isNameUnique) {
702
777
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
778
  }
779
+ },
780
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
781
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
782
+ throw new errors.ValidationError("Scheduled at must be later than now");
783
+ }
704
784
  }
705
785
  });
786
+ const createSchedulingService = ({ strapi: strapi2 }) => {
787
+ const scheduledJobs = /* @__PURE__ */ new Map();
788
+ return {
789
+ async set(releaseId, scheduleDate) {
790
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
791
+ if (!release2) {
792
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
793
+ }
794
+ const job = scheduleJob(scheduleDate, async () => {
795
+ try {
796
+ await getService("release").publish(releaseId);
797
+ } catch (error) {
798
+ }
799
+ this.cancel(releaseId);
800
+ });
801
+ if (scheduledJobs.has(releaseId)) {
802
+ this.cancel(releaseId);
803
+ }
804
+ scheduledJobs.set(releaseId, job);
805
+ return scheduledJobs;
806
+ },
807
+ cancel(releaseId) {
808
+ if (scheduledJobs.has(releaseId)) {
809
+ scheduledJobs.get(releaseId).cancel();
810
+ scheduledJobs.delete(releaseId);
811
+ }
812
+ return scheduledJobs;
813
+ },
814
+ getAll() {
815
+ return scheduledJobs;
816
+ },
817
+ /**
818
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
819
+ * This is useful in case the server was restarted and the scheduled jobs were lost
820
+ * This also could be used to sync different Strapi instances in case of a cluster
821
+ */
822
+ async syncFromDatabase() {
823
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
824
+ where: {
825
+ scheduledAt: {
826
+ $gte: /* @__PURE__ */ new Date()
827
+ },
828
+ releasedAt: null
829
+ }
830
+ });
831
+ for (const release2 of releases) {
832
+ this.set(release2.id, release2.scheduledAt);
833
+ }
834
+ return scheduledJobs;
835
+ }
836
+ };
837
+ };
706
838
  const services = {
707
839
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
840
+ "release-validation": createReleaseValidationService,
841
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
709
842
  };
710
843
  const RELEASE_SCHEMA = yup.object().shape({
711
- name: yup.string().trim().required()
844
+ name: yup.string().trim().required(),
845
+ // scheduledAt is a date, but we always receive strings from the client
846
+ scheduledAt: yup.string().nullable(),
847
+ timezone: yup.string().when("scheduledAt", {
848
+ is: (scheduledAt) => !!scheduledAt,
849
+ then: yup.string().required(),
850
+ otherwise: yup.string().nullable()
851
+ })
712
852
  }).required().noUnknown();
713
853
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
854
  const releaseController = {
@@ -748,19 +888,18 @@ const releaseController = {
748
888
  const id = ctx.params.id;
749
889
  const releaseService = getService("release", { strapi });
750
890
  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);
891
+ if (!release2) {
892
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
893
+ }
756
894
  const count = await releaseService.countActions({
757
895
  filters: {
758
896
  release: id
759
897
  }
760
898
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
899
+ const sanitizedRelease = {
900
+ ...release2,
901
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
902
+ };
764
903
  const data = {
765
904
  ...sanitizedRelease,
766
905
  actions: {
@@ -813,8 +952,27 @@ const releaseController = {
813
952
  const id = ctx.params.id;
814
953
  const releaseService = getService("release", { strapi });
815
954
  const release2 = await releaseService.publish(id, { user });
955
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
956
+ releaseService.countActions({
957
+ filters: {
958
+ release: id,
959
+ type: "publish"
960
+ }
961
+ }),
962
+ releaseService.countActions({
963
+ filters: {
964
+ release: id,
965
+ type: "unpublish"
966
+ }
967
+ })
968
+ ]);
816
969
  ctx.body = {
817
- data: release2
970
+ data: release2,
971
+ meta: {
972
+ totalEntries: countPublishActions + countUnpublishActions,
973
+ totalPublishedEntries: countPublishActions,
974
+ totalUnpublishedEntries: countUnpublishActions
975
+ }
818
976
  };
819
977
  }
820
978
  };
@@ -1086,6 +1244,7 @@ const getPlugin = () => {
1086
1244
  return {
1087
1245
  register,
1088
1246
  bootstrap,
1247
+ destroy,
1089
1248
  contentTypes,
1090
1249
  services,
1091
1250
  controllers,