@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
@@ -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) {
@@ -538,6 +586,10 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
538
586
  });
539
587
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
540
588
  });
589
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
590
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
591
+ await schedulingService.cancel(release2.id);
592
+ }
541
593
  return release2;
542
594
  },
543
595
  async publish(releaseId) {
@@ -565,27 +617,53 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
565
617
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
566
618
  throw new utils.errors.ValidationError("No entries to publish");
567
619
  }
568
- const actions = {};
620
+ const collectionTypeActions = {};
621
+ const singleTypeActions = [];
569
622
  for (const action of releaseWithPopulatedActionEntries.actions) {
570
623
  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);
624
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
625
+ if (!collectionTypeActions[contentTypeUid]) {
626
+ collectionTypeActions[contentTypeUid] = {
627
+ entriestoPublishIds: [],
628
+ entriesToUnpublishIds: []
629
+ };
630
+ }
631
+ if (action.type === "publish") {
632
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
633
+ } else {
634
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
635
+ }
579
636
  } else {
580
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
637
+ singleTypeActions.push({
638
+ uid: contentTypeUid,
639
+ action: action.type,
640
+ id: action.entry.id
641
+ });
581
642
  }
582
643
  }
583
644
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
645
  const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
585
646
  await strapi2.db.transaction(async () => {
586
- for (const contentTypeUid of Object.keys(actions)) {
647
+ for (const { uid, action, id } of singleTypeActions) {
648
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
649
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
650
+ try {
651
+ if (action === "publish") {
652
+ await entityManagerService.publish(entry, uid);
653
+ } else {
654
+ await entityManagerService.unpublish(entry, uid);
655
+ }
656
+ } catch (error) {
657
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
658
+ ;
659
+ else {
660
+ throw error;
661
+ }
662
+ }
663
+ }
664
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
587
665
  const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
666
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
589
667
  const entriesToPublish = await strapi2.entityService.findMany(
590
668
  contentTypeUid,
591
669
  {
@@ -711,27 +789,93 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
711
789
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
712
790
  }
713
791
  },
714
- async validateUniqueNameForPendingRelease(name) {
792
+ async validateUniqueNameForPendingRelease(name, id) {
715
793
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
794
  filters: {
717
795
  releasedAt: {
718
796
  $null: true
719
797
  },
720
- name
798
+ name,
799
+ ...id && { id: { $ne: id } }
721
800
  }
722
801
  });
723
802
  const isNameUnique = pendingReleases.length === 0;
724
803
  if (!isNameUnique) {
725
804
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
805
  }
806
+ },
807
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
808
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
809
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
810
+ }
727
811
  }
728
812
  });
813
+ const createSchedulingService = ({ strapi: strapi2 }) => {
814
+ const scheduledJobs = /* @__PURE__ */ new Map();
815
+ return {
816
+ async set(releaseId, scheduleDate) {
817
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
818
+ if (!release2) {
819
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
820
+ }
821
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
822
+ try {
823
+ await getService("release").publish(releaseId);
824
+ } catch (error) {
825
+ }
826
+ this.cancel(releaseId);
827
+ });
828
+ if (scheduledJobs.has(releaseId)) {
829
+ this.cancel(releaseId);
830
+ }
831
+ scheduledJobs.set(releaseId, job);
832
+ return scheduledJobs;
833
+ },
834
+ cancel(releaseId) {
835
+ if (scheduledJobs.has(releaseId)) {
836
+ scheduledJobs.get(releaseId).cancel();
837
+ scheduledJobs.delete(releaseId);
838
+ }
839
+ return scheduledJobs;
840
+ },
841
+ getAll() {
842
+ return scheduledJobs;
843
+ },
844
+ /**
845
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
846
+ * This is useful in case the server was restarted and the scheduled jobs were lost
847
+ * This also could be used to sync different Strapi instances in case of a cluster
848
+ */
849
+ async syncFromDatabase() {
850
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
851
+ where: {
852
+ scheduledAt: {
853
+ $gte: /* @__PURE__ */ new Date()
854
+ },
855
+ releasedAt: null
856
+ }
857
+ });
858
+ for (const release2 of releases) {
859
+ this.set(release2.id, release2.scheduledAt);
860
+ }
861
+ return scheduledJobs;
862
+ }
863
+ };
864
+ };
729
865
  const services = {
730
866
  release: createReleaseService,
731
- "release-validation": createReleaseValidationService
867
+ "release-validation": createReleaseValidationService,
868
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
732
869
  };
733
870
  const RELEASE_SCHEMA = yup__namespace.object().shape({
734
- name: yup__namespace.string().trim().required()
871
+ name: yup__namespace.string().trim().required(),
872
+ // scheduledAt is a date, but we always receive strings from the client
873
+ scheduledAt: yup__namespace.string().nullable(),
874
+ timezone: yup__namespace.string().when("scheduledAt", {
875
+ is: (scheduledAt) => !!scheduledAt,
876
+ then: yup__namespace.string().required(),
877
+ otherwise: yup__namespace.string().nullable()
878
+ })
735
879
  }).required().noUnknown();
736
880
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
737
881
  const releaseController = {
@@ -771,19 +915,18 @@ const releaseController = {
771
915
  const id = ctx.params.id;
772
916
  const releaseService = getService("release", { strapi });
773
917
  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);
918
+ if (!release2) {
919
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
920
+ }
779
921
  const count = await releaseService.countActions({
780
922
  filters: {
781
923
  release: id
782
924
  }
783
925
  });
784
- if (!release2) {
785
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
786
- }
926
+ const sanitizedRelease = {
927
+ ...release2,
928
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
929
+ };
787
930
  const data = {
788
931
  ...sanitizedRelease,
789
932
  actions: {
@@ -836,8 +979,27 @@ const releaseController = {
836
979
  const id = ctx.params.id;
837
980
  const releaseService = getService("release", { strapi });
838
981
  const release2 = await releaseService.publish(id, { user });
982
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
983
+ releaseService.countActions({
984
+ filters: {
985
+ release: id,
986
+ type: "publish"
987
+ }
988
+ }),
989
+ releaseService.countActions({
990
+ filters: {
991
+ release: id,
992
+ type: "unpublish"
993
+ }
994
+ })
995
+ ]);
839
996
  ctx.body = {
840
- data: release2
997
+ data: release2,
998
+ meta: {
999
+ totalEntries: countPublishActions + countUnpublishActions,
1000
+ totalPublishedEntries: countPublishActions,
1001
+ totalUnpublishedEntries: countUnpublishActions
1002
+ }
841
1003
  };
842
1004
  }
843
1005
  };
@@ -1109,6 +1271,7 @@ const getPlugin = () => {
1109
1271
  return {
1110
1272
  register,
1111
1273
  bootstrap,
1274
+ destroy,
1112
1275
  contentTypes,
1113
1276
  services,
1114
1277
  controllers,