@strapi/content-releases 0.0.0-next.56199ab7a5f3320e0debcbe4a24fe0b8cd599e21 → 0.0.0-next.6d384ed205b7f0792d9bea79195f01b30463cfa0

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.
@@ -1,6 +1,8 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1, mapAsync } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import { difference, keys } from "lodash";
2
3
  import _ from "lodash/fp";
3
4
  import EE from "@strapi/strapi/dist/utils/ee";
5
+ import { scheduleJob } from "node-schedule";
4
6
  import * as yup from "yup";
5
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
6
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -48,40 +50,47 @@ const ACTIONS = [
48
50
  pluginName: "content-releases"
49
51
  }
50
52
  ];
51
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
52
- return strapi2.plugin("content-releases").service(name);
53
- };
53
+ async function deleteActionsOnDisableDraftAndPublish({
54
+ oldContentTypes,
55
+ contentTypes: contentTypes2
56
+ }) {
57
+ if (!oldContentTypes) {
58
+ return;
59
+ }
60
+ for (const uid in contentTypes2) {
61
+ if (!oldContentTypes[uid]) {
62
+ continue;
63
+ }
64
+ const oldContentType = oldContentTypes[uid];
65
+ const contentType = contentTypes2[uid];
66
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
67
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
68
+ }
69
+ }
70
+ }
71
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
72
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
73
+ if (deletedContentTypes.length) {
74
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
75
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
76
+ });
77
+ }
78
+ }
54
79
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
55
80
  const register = async ({ strapi: strapi2 }) => {
56
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
81
+ if (features$2.isEnabled("cms-content-releases")) {
57
82
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
58
- const releaseActionService = getService("release-action", { strapi: strapi2 });
59
- const eventManager = getService("event-manager", { strapi: strapi2 });
60
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
61
- "content-type.update",
62
- async ({ contentType }) => {
63
- if (contentType.schema.options.draftAndPublish === false) {
64
- await releaseActionService.deleteManyForContentType(contentType.uid);
65
- }
66
- }
67
- );
68
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
69
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
70
- "content-type.delete",
71
- async ({ contentType }) => {
72
- await releaseActionService.deleteManyForContentType(contentType.uid);
73
- }
74
- );
75
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
83
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
84
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
76
85
  }
77
86
  };
78
87
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
79
88
  const bootstrap = async ({ strapi: strapi2 }) => {
80
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
89
+ if (features$1.isEnabled("cms-content-releases")) {
81
90
  strapi2.db.lifecycles.subscribe({
82
91
  afterDelete(event) {
83
92
  const { model, result } = event;
84
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
93
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
85
94
  const { id } = result;
86
95
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
87
96
  where: {
@@ -97,7 +106,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
97
106
  */
98
107
  async beforeDeleteMany(event) {
99
108
  const { model, params } = event;
100
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
109
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
101
110
  const { where } = params;
102
111
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
103
112
  event.state.entriesToDelete = entriesToDelete;
@@ -150,6 +159,9 @@ const schema$1 = {
150
159
  releasedAt: {
151
160
  type: "datetime"
152
161
  },
162
+ scheduledAt: {
163
+ type: "datetime"
164
+ },
153
165
  actions: {
154
166
  type: "relation",
155
167
  relation: "oneToMany",
@@ -212,15 +224,9 @@ const contentTypes = {
212
224
  release: release$1,
213
225
  "release-action": releaseAction$1
214
226
  };
215
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
216
- async deleteManyForContentType(contentTypeUid) {
217
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
218
- where: {
219
- target_type: contentTypeUid
220
- }
221
- });
222
- }
223
- });
227
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
228
+ return strapi2.plugin("content-releases").service(name);
229
+ };
224
230
  const getGroupName = (queryValue) => {
225
231
  switch (queryValue) {
226
232
  case "contentType":
@@ -236,10 +242,24 @@ const getGroupName = (queryValue) => {
236
242
  const createReleaseService = ({ strapi: strapi2 }) => ({
237
243
  async create(releaseData, { user }) {
238
244
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
240
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
245
+ const {
246
+ validatePendingReleasesLimit,
247
+ validateUniqueNameForPendingRelease,
248
+ validateScheduledAtIsLaterThanNow
249
+ } = getService("release-validation", { strapi: strapi2 });
250
+ await Promise.all([
251
+ validatePendingReleasesLimit(),
252
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
253
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
254
+ ]);
255
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
241
256
  data: releaseWithCreatorFields
242
257
  });
258
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
259
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
260
+ await schedulingService.set(release2.id, release2.scheduledAt);
261
+ }
262
+ return release2;
243
263
  },
244
264
  async findOne(id, query = {}) {
245
265
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -258,51 +278,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
258
278
  }
259
279
  });
260
280
  },
261
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
262
- hasEntryAttached
263
- } = {
264
- hasEntryAttached: false
265
- }) {
266
- const whereActions = hasEntryAttached ? {
267
- // Find all Releases where the content type entry is present
268
- actions: {
269
- target_type: contentTypeUid,
270
- target_id: entryId
271
- }
272
- } : {
273
- // Find all Releases where the content type entry is not present
274
- $or: [
275
- {
276
- $not: {
277
- actions: {
278
- target_type: contentTypeUid,
279
- target_id: entryId
280
- }
281
- }
281
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
282
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
283
+ where: {
284
+ actions: {
285
+ target_type: contentTypeUid,
286
+ target_id: entryId
282
287
  },
283
- {
284
- actions: null
288
+ releasedAt: {
289
+ $null: true
285
290
  }
286
- ]
287
- };
288
- const populateAttachedAction = hasEntryAttached ? {
289
- // Filter the action to get only the content type entry
290
- actions: {
291
- where: {
291
+ },
292
+ populate: {
293
+ // Filter the action to get only the content type entry
294
+ actions: {
295
+ where: {
296
+ target_type: contentTypeUid,
297
+ target_id: entryId
298
+ }
299
+ }
300
+ }
301
+ });
302
+ return releases.map((release2) => {
303
+ if (release2.actions?.length) {
304
+ const [actionForEntry] = release2.actions;
305
+ delete release2.actions;
306
+ return {
307
+ ...release2,
308
+ action: actionForEntry
309
+ };
310
+ }
311
+ return release2;
312
+ });
313
+ },
314
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
315
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
316
+ where: {
317
+ releasedAt: {
318
+ $null: true
319
+ },
320
+ actions: {
292
321
  target_type: contentTypeUid,
293
322
  target_id: entryId
294
323
  }
295
324
  }
296
- } : {};
325
+ });
297
326
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
298
327
  where: {
299
- ...whereActions,
328
+ $or: [
329
+ {
330
+ id: {
331
+ $notIn: releasesRelated.map((release2) => release2.id)
332
+ }
333
+ },
334
+ {
335
+ actions: null
336
+ }
337
+ ],
300
338
  releasedAt: {
301
339
  $null: true
302
340
  }
303
- },
304
- populate: {
305
- ...populateAttachedAction
306
341
  }
307
342
  });
308
343
  return releases.map((release2) => {
@@ -501,7 +536,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
501
536
  populate: {
502
537
  actions: {
503
538
  populate: {
504
- entry: true
539
+ entry: {
540
+ fields: ["id"]
541
+ }
505
542
  }
506
543
  }
507
544
  }
@@ -516,30 +553,80 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
516
553
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
517
554
  throw new errors.ValidationError("No entries to publish");
518
555
  }
519
- const actions = {};
556
+ const collectionTypeActions = {};
557
+ const singleTypeActions = [];
520
558
  for (const action of releaseWithPopulatedActionEntries.actions) {
521
559
  const contentTypeUid = action.contentType;
522
- if (!actions[contentTypeUid]) {
523
- actions[contentTypeUid] = {
524
- publish: [],
525
- unpublish: []
526
- };
527
- }
528
- if (action.type === "publish") {
529
- actions[contentTypeUid].publish.push(action.entry);
560
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
561
+ if (!collectionTypeActions[contentTypeUid]) {
562
+ collectionTypeActions[contentTypeUid] = {
563
+ entriestoPublishIds: [],
564
+ entriesToUnpublishIds: []
565
+ };
566
+ }
567
+ if (action.type === "publish") {
568
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
569
+ } else {
570
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
571
+ }
530
572
  } else {
531
- actions[contentTypeUid].unpublish.push(action.entry);
573
+ singleTypeActions.push({
574
+ uid: contentTypeUid,
575
+ action: action.type,
576
+ id: action.entry.id
577
+ });
532
578
  }
533
579
  }
534
580
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
581
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
535
582
  await strapi2.db.transaction(async () => {
536
- for (const contentTypeUid of Object.keys(actions)) {
537
- const { publish, unpublish } = actions[contentTypeUid];
538
- if (publish.length > 0) {
539
- await entityManagerService.publishMany(publish, contentTypeUid);
583
+ for (const { uid, action, id } of singleTypeActions) {
584
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
585
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
586
+ try {
587
+ if (action === "publish") {
588
+ await entityManagerService.publish(entry, uid);
589
+ } else {
590
+ await entityManagerService.unpublish(entry, uid);
591
+ }
592
+ } catch (error) {
593
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
594
+ ;
595
+ else {
596
+ throw error;
597
+ }
540
598
  }
541
- if (unpublish.length > 0) {
542
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
599
+ }
600
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
601
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
602
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
603
+ const entriesToPublish = await strapi2.entityService.findMany(
604
+ contentTypeUid,
605
+ {
606
+ filters: {
607
+ id: {
608
+ $in: entriestoPublishIds
609
+ }
610
+ },
611
+ populate
612
+ }
613
+ );
614
+ const entriesToUnpublish = await strapi2.entityService.findMany(
615
+ contentTypeUid,
616
+ {
617
+ filters: {
618
+ id: {
619
+ $in: entriesToUnpublishIds
620
+ }
621
+ },
622
+ populate
623
+ }
624
+ );
625
+ if (entriesToPublish.length > 0) {
626
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
627
+ }
628
+ if (entriesToUnpublish.length > 0) {
629
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
543
630
  }
544
631
  }
545
632
  });
@@ -637,34 +724,66 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
637
724
  if (pendingReleasesCount >= maximumPendingReleases) {
638
725
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
639
726
  }
727
+ },
728
+ async validateUniqueNameForPendingRelease(name) {
729
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
730
+ filters: {
731
+ releasedAt: {
732
+ $null: true
733
+ },
734
+ name
735
+ }
736
+ });
737
+ const isNameUnique = pendingReleases.length === 0;
738
+ if (!isNameUnique) {
739
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
740
+ }
741
+ },
742
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
743
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
744
+ throw new errors.ValidationError("Scheduled at must be later than now");
745
+ }
640
746
  }
641
747
  });
642
- const createEventManagerService = () => {
643
- const state = {
644
- destroyListenerCallbacks: []
645
- };
748
+ const createSchedulingService = ({ strapi: strapi2 }) => {
749
+ const scheduledJobs = /* @__PURE__ */ new Map();
646
750
  return {
647
- addDestroyListenerCallback(destroyListenerCallback) {
648
- state.destroyListenerCallbacks.push(destroyListenerCallback);
649
- },
650
- destroyAllListeners() {
651
- if (!state.destroyListenerCallbacks.length) {
652
- return;
751
+ async set(releaseId, scheduleDate) {
752
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
753
+ if (!release2) {
754
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
653
755
  }
654
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
655
- destroyListenerCallback();
756
+ const job = scheduleJob(scheduleDate, async () => {
757
+ try {
758
+ await getService("release").publish(releaseId);
759
+ } catch (error) {
760
+ }
761
+ this.cancel(releaseId);
656
762
  });
763
+ if (scheduledJobs.has(releaseId)) {
764
+ this.cancel(releaseId);
765
+ }
766
+ scheduledJobs.set(releaseId, job);
767
+ return scheduledJobs;
768
+ },
769
+ cancel(releaseId) {
770
+ if (scheduledJobs.has(releaseId)) {
771
+ scheduledJobs.get(releaseId).cancel();
772
+ scheduledJobs.delete(releaseId);
773
+ }
774
+ return scheduledJobs;
657
775
  }
658
776
  };
659
777
  };
660
778
  const services = {
661
779
  release: createReleaseService,
662
- "release-action": createReleaseActionService,
663
780
  "release-validation": createReleaseValidationService,
664
- "event-manager": createEventManagerService
781
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
665
782
  };
666
783
  const RELEASE_SCHEMA = yup.object().shape({
667
- name: yup.string().trim().required()
784
+ name: yup.string().trim().required(),
785
+ // scheduledAt is a date, but we always receive strings from the client
786
+ scheduledAt: yup.string()
668
787
  }).required().noUnknown();
669
788
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
670
789
  const releaseController = {
@@ -681,9 +800,7 @@ const releaseController = {
681
800
  const contentTypeUid = query.contentTypeUid;
682
801
  const entryId = query.entryId;
683
802
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
684
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
685
- hasEntryAttached
686
- });
803
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
687
804
  ctx.body = { data };
688
805
  } else {
689
806
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -706,19 +823,18 @@ const releaseController = {
706
823
  const id = ctx.params.id;
707
824
  const releaseService = getService("release", { strapi });
708
825
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
709
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
710
- ability: ctx.state.userAbility,
711
- model: RELEASE_MODEL_UID
712
- });
713
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
826
+ if (!release2) {
827
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
828
+ }
714
829
  const count = await releaseService.countActions({
715
830
  filters: {
716
831
  release: id
717
832
  }
718
833
  });
719
- if (!release2) {
720
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
721
- }
834
+ const sanitizedRelease = {
835
+ ...release2,
836
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
837
+ };
722
838
  const data = {
723
839
  ...sanitizedRelease,
724
840
  actions: {
@@ -771,8 +887,27 @@ const releaseController = {
771
887
  const id = ctx.params.id;
772
888
  const releaseService = getService("release", { strapi });
773
889
  const release2 = await releaseService.publish(id, { user });
890
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
891
+ releaseService.countActions({
892
+ filters: {
893
+ release: id,
894
+ type: "publish"
895
+ }
896
+ }),
897
+ releaseService.countActions({
898
+ filters: {
899
+ release: id,
900
+ type: "unpublish"
901
+ }
902
+ })
903
+ ]);
774
904
  ctx.body = {
775
- data: release2
905
+ data: release2,
906
+ meta: {
907
+ totalEntries: countPublishActions + countUnpublishActions,
908
+ totalPublishedEntries: countPublishActions,
909
+ totalUnpublishedEntries: countUnpublishActions
910
+ }
776
911
  };
777
912
  }
778
913
  };
@@ -1040,19 +1175,14 @@ const routes = {
1040
1175
  };
1041
1176
  const { features } = require("@strapi/strapi/dist/utils/ee");
1042
1177
  const getPlugin = () => {
1043
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1178
+ if (features.isEnabled("cms-content-releases")) {
1044
1179
  return {
1045
1180
  register,
1046
1181
  bootstrap,
1047
1182
  contentTypes,
1048
1183
  services,
1049
1184
  controllers,
1050
- routes,
1051
- destroy() {
1052
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1053
- getService("event-manager").destroyAllListeners();
1054
- }
1055
- }
1185
+ routes
1056
1186
  };
1057
1187
  }
1058
1188
  return {