@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
@@ -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,12 @@ const schema$1 = {
181
203
  releasedAt: {
182
204
  type: "datetime"
183
205
  },
206
+ scheduledAt: {
207
+ type: "datetime"
208
+ },
209
+ timezone: {
210
+ type: "string"
211
+ },
184
212
  actions: {
185
213
  type: "relation",
186
214
  relation: "oneToMany",
@@ -243,9 +271,6 @@ const contentTypes = {
243
271
  release: release$1,
244
272
  "release-action": releaseAction$1
245
273
  };
246
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
- return strapi2.plugin("content-releases").service(name);
248
- };
249
274
  const getGroupName = (queryValue) => {
250
275
  switch (queryValue) {
251
276
  case "contentType":
@@ -261,17 +286,24 @@ const getGroupName = (queryValue) => {
261
286
  const createReleaseService = ({ strapi: strapi2 }) => ({
262
287
  async create(releaseData, { user }) {
263
288
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
264
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
- "release-validation",
266
- { strapi: strapi2 }
267
- );
289
+ const {
290
+ validatePendingReleasesLimit,
291
+ validateUniqueNameForPendingRelease,
292
+ validateScheduledAtIsLaterThanNow
293
+ } = getService("release-validation", { strapi: strapi2 });
268
294
  await Promise.all([
269
295
  validatePendingReleasesLimit(),
270
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
296
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
297
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
271
298
  ]);
272
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
299
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
273
300
  data: releaseWithCreatorFields
274
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
+ return release2;
275
307
  },
276
308
  async findOne(id, query = {}) {
277
309
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -366,6 +398,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
366
398
  },
367
399
  async update(id, releaseData, { user }) {
368
400
  const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
401
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
402
+ "release-validation",
403
+ { strapi: strapi2 }
404
+ );
405
+ await Promise.all([
406
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
407
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
408
+ ]);
369
409
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
370
410
  if (!release2) {
371
411
  throw new utils.errors.NotFoundError(`No release found for id ${id}`);
@@ -381,6 +421,14 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
381
421
  // @ts-expect-error see above
382
422
  data: releaseWithCreatorFields
383
423
  });
424
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
425
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
426
+ if (releaseData.scheduledAt) {
427
+ await schedulingService.set(id, releaseData.scheduledAt);
428
+ } else if (release2.scheduledAt) {
429
+ schedulingService.cancel(id);
430
+ }
431
+ }
384
432
  return updatedRelease;
385
433
  },
386
434
  async createAction(releaseId, action) {
@@ -565,27 +613,53 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
565
613
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
566
614
  throw new utils.errors.ValidationError("No entries to publish");
567
615
  }
568
- const actions = {};
616
+ const collectionTypeActions = {};
617
+ const singleTypeActions = [];
569
618
  for (const action of releaseWithPopulatedActionEntries.actions) {
570
619
  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);
620
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
621
+ if (!collectionTypeActions[contentTypeUid]) {
622
+ collectionTypeActions[contentTypeUid] = {
623
+ entriestoPublishIds: [],
624
+ entriesToUnpublishIds: []
625
+ };
626
+ }
627
+ if (action.type === "publish") {
628
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
629
+ } else {
630
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
631
+ }
579
632
  } else {
580
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
633
+ singleTypeActions.push({
634
+ uid: contentTypeUid,
635
+ action: action.type,
636
+ id: action.entry.id
637
+ });
581
638
  }
582
639
  }
583
640
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
641
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
585
642
  await strapi2.db.transaction(async () => {
586
- for (const contentTypeUid of Object.keys(actions)) {
643
+ for (const { uid, action, id } of singleTypeActions) {
644
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
645
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
646
+ try {
647
+ if (action === "publish") {
648
+ await entityManagerService.publish(entry, uid);
649
+ } else {
650
+ await entityManagerService.unpublish(entry, uid);
651
+ }
652
+ } catch (error) {
653
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
654
+ ;
655
+ else {
656
+ throw error;
657
+ }
658
+ }
659
+ }
660
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
587
661
  const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
662
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
589
663
  const entriesToPublish = await strapi2.entityService.findMany(
590
664
  contentTypeUid,
591
665
  {
@@ -711,27 +785,93 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
711
785
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
712
786
  }
713
787
  },
714
- async validateUniqueNameForPendingRelease(name) {
788
+ async validateUniqueNameForPendingRelease(name, id) {
715
789
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
790
  filters: {
717
791
  releasedAt: {
718
792
  $null: true
719
793
  },
720
- name
794
+ name,
795
+ ...id && { id: { $ne: id } }
721
796
  }
722
797
  });
723
798
  const isNameUnique = pendingReleases.length === 0;
724
799
  if (!isNameUnique) {
725
800
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
801
  }
802
+ },
803
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
804
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
805
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
806
+ }
727
807
  }
728
808
  });
809
+ const createSchedulingService = ({ strapi: strapi2 }) => {
810
+ const scheduledJobs = /* @__PURE__ */ new Map();
811
+ return {
812
+ async set(releaseId, scheduleDate) {
813
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
814
+ if (!release2) {
815
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
816
+ }
817
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
818
+ try {
819
+ await getService("release").publish(releaseId);
820
+ } catch (error) {
821
+ }
822
+ this.cancel(releaseId);
823
+ });
824
+ if (scheduledJobs.has(releaseId)) {
825
+ this.cancel(releaseId);
826
+ }
827
+ scheduledJobs.set(releaseId, job);
828
+ return scheduledJobs;
829
+ },
830
+ cancel(releaseId) {
831
+ if (scheduledJobs.has(releaseId)) {
832
+ scheduledJobs.get(releaseId).cancel();
833
+ scheduledJobs.delete(releaseId);
834
+ }
835
+ return scheduledJobs;
836
+ },
837
+ getAll() {
838
+ return scheduledJobs;
839
+ },
840
+ /**
841
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
842
+ * This is useful in case the server was restarted and the scheduled jobs were lost
843
+ * This also could be used to sync different Strapi instances in case of a cluster
844
+ */
845
+ async syncFromDatabase() {
846
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
847
+ where: {
848
+ scheduledAt: {
849
+ $gte: /* @__PURE__ */ new Date()
850
+ },
851
+ releasedAt: null
852
+ }
853
+ });
854
+ for (const release2 of releases) {
855
+ this.set(release2.id, release2.scheduledAt);
856
+ }
857
+ return scheduledJobs;
858
+ }
859
+ };
860
+ };
729
861
  const services = {
730
862
  release: createReleaseService,
731
- "release-validation": createReleaseValidationService
863
+ "release-validation": createReleaseValidationService,
864
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
732
865
  };
733
866
  const RELEASE_SCHEMA = yup__namespace.object().shape({
734
- name: yup__namespace.string().trim().required()
867
+ name: yup__namespace.string().trim().required(),
868
+ // scheduledAt is a date, but we always receive strings from the client
869
+ scheduledAt: yup__namespace.string().nullable(),
870
+ timezone: yup__namespace.string().when("scheduledAt", {
871
+ is: (scheduledAt) => !!scheduledAt,
872
+ then: yup__namespace.string().required(),
873
+ otherwise: yup__namespace.string().nullable()
874
+ })
735
875
  }).required().noUnknown();
736
876
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
737
877
  const releaseController = {
@@ -771,19 +911,18 @@ const releaseController = {
771
911
  const id = ctx.params.id;
772
912
  const releaseService = getService("release", { strapi });
773
913
  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);
914
+ if (!release2) {
915
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
916
+ }
779
917
  const count = await releaseService.countActions({
780
918
  filters: {
781
919
  release: id
782
920
  }
783
921
  });
784
- if (!release2) {
785
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
786
- }
922
+ const sanitizedRelease = {
923
+ ...release2,
924
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
925
+ };
787
926
  const data = {
788
927
  ...sanitizedRelease,
789
928
  actions: {
@@ -836,8 +975,27 @@ const releaseController = {
836
975
  const id = ctx.params.id;
837
976
  const releaseService = getService("release", { strapi });
838
977
  const release2 = await releaseService.publish(id, { user });
978
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
979
+ releaseService.countActions({
980
+ filters: {
981
+ release: id,
982
+ type: "publish"
983
+ }
984
+ }),
985
+ releaseService.countActions({
986
+ filters: {
987
+ release: id,
988
+ type: "unpublish"
989
+ }
990
+ })
991
+ ]);
839
992
  ctx.body = {
840
- data: release2
993
+ data: release2,
994
+ meta: {
995
+ totalEntries: countPublishActions + countUnpublishActions,
996
+ totalPublishedEntries: countPublishActions,
997
+ totalUnpublishedEntries: countUnpublishActions
998
+ }
841
999
  };
842
1000
  }
843
1001
  };
@@ -1109,6 +1267,7 @@ const getPlugin = () => {
1109
1267
  return {
1110
1268
  register,
1111
1269
  bootstrap,
1270
+ destroy,
1112
1271
  contentTypes,
1113
1272
  services,
1114
1273
  controllers,