@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,7 +1,9 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const lodash = require("lodash");
3
4
  const _ = require("lodash/fp");
4
5
  const EE = require("@strapi/strapi/dist/utils/ee");
6
+ const nodeSchedule = require("node-schedule");
5
7
  const yup = require("yup");
6
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
7
9
  function _interopNamespace(e) {
@@ -71,40 +73,47 @@ const ACTIONS = [
71
73
  pluginName: "content-releases"
72
74
  }
73
75
  ];
74
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
75
- return strapi2.plugin("content-releases").service(name);
76
- };
76
+ async function deleteActionsOnDisableDraftAndPublish({
77
+ oldContentTypes,
78
+ contentTypes: contentTypes2
79
+ }) {
80
+ if (!oldContentTypes) {
81
+ return;
82
+ }
83
+ for (const uid in contentTypes2) {
84
+ if (!oldContentTypes[uid]) {
85
+ continue;
86
+ }
87
+ const oldContentType = oldContentTypes[uid];
88
+ const contentType = contentTypes2[uid];
89
+ if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
90
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
91
+ }
92
+ }
93
+ }
94
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
95
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
96
+ if (deletedContentTypes.length) {
97
+ await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
98
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
99
+ });
100
+ }
101
+ }
77
102
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
78
103
  const register = async ({ strapi: strapi2 }) => {
79
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
104
+ if (features$2.isEnabled("cms-content-releases")) {
80
105
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
81
- const releaseActionService = getService("release-action", { strapi: strapi2 });
82
- const eventManager = getService("event-manager", { strapi: strapi2 });
83
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
84
- "content-type.update",
85
- async ({ contentType }) => {
86
- if (contentType.schema.options.draftAndPublish === false) {
87
- await releaseActionService.deleteManyForContentType(contentType.uid);
88
- }
89
- }
90
- );
91
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
92
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
93
- "content-type.delete",
94
- async ({ contentType }) => {
95
- await releaseActionService.deleteManyForContentType(contentType.uid);
96
- }
97
- );
98
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
106
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
107
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
99
108
  }
100
109
  };
101
110
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
102
111
  const bootstrap = async ({ strapi: strapi2 }) => {
103
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
112
+ if (features$1.isEnabled("cms-content-releases")) {
104
113
  strapi2.db.lifecycles.subscribe({
105
114
  afterDelete(event) {
106
115
  const { model, result } = event;
107
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
116
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
108
117
  const { id } = result;
109
118
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
110
119
  where: {
@@ -120,7 +129,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
120
129
  */
121
130
  async beforeDeleteMany(event) {
122
131
  const { model, params } = event;
123
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
132
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
124
133
  const { where } = params;
125
134
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
126
135
  event.state.entriesToDelete = entriesToDelete;
@@ -173,6 +182,9 @@ const schema$1 = {
173
182
  releasedAt: {
174
183
  type: "datetime"
175
184
  },
185
+ scheduledAt: {
186
+ type: "datetime"
187
+ },
176
188
  actions: {
177
189
  type: "relation",
178
190
  relation: "oneToMany",
@@ -235,15 +247,9 @@ const contentTypes = {
235
247
  release: release$1,
236
248
  "release-action": releaseAction$1
237
249
  };
238
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
239
- async deleteManyForContentType(contentTypeUid) {
240
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
241
- where: {
242
- target_type: contentTypeUid
243
- }
244
- });
245
- }
246
- });
250
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
251
+ return strapi2.plugin("content-releases").service(name);
252
+ };
247
253
  const getGroupName = (queryValue) => {
248
254
  switch (queryValue) {
249
255
  case "contentType":
@@ -259,10 +265,24 @@ const getGroupName = (queryValue) => {
259
265
  const createReleaseService = ({ strapi: strapi2 }) => ({
260
266
  async create(releaseData, { user }) {
261
267
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
262
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
263
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
268
+ const {
269
+ validatePendingReleasesLimit,
270
+ validateUniqueNameForPendingRelease,
271
+ validateScheduledAtIsLaterThanNow
272
+ } = getService("release-validation", { strapi: strapi2 });
273
+ await Promise.all([
274
+ validatePendingReleasesLimit(),
275
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
276
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
277
+ ]);
278
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
264
279
  data: releaseWithCreatorFields
265
280
  });
281
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
282
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
283
+ await schedulingService.set(release2.id, release2.scheduledAt);
284
+ }
285
+ return release2;
266
286
  },
267
287
  async findOne(id, query = {}) {
268
288
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
@@ -281,51 +301,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
281
301
  }
282
302
  });
283
303
  },
284
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
285
- hasEntryAttached
286
- } = {
287
- hasEntryAttached: false
288
- }) {
289
- const whereActions = hasEntryAttached ? {
290
- // Find all Releases where the content type entry is present
291
- actions: {
292
- target_type: contentTypeUid,
293
- target_id: entryId
294
- }
295
- } : {
296
- // Find all Releases where the content type entry is not present
297
- $or: [
298
- {
299
- $not: {
300
- actions: {
301
- target_type: contentTypeUid,
302
- target_id: entryId
303
- }
304
- }
304
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
305
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
306
+ where: {
307
+ actions: {
308
+ target_type: contentTypeUid,
309
+ target_id: entryId
305
310
  },
306
- {
307
- actions: null
311
+ releasedAt: {
312
+ $null: true
308
313
  }
309
- ]
310
- };
311
- const populateAttachedAction = hasEntryAttached ? {
312
- // Filter the action to get only the content type entry
313
- actions: {
314
- where: {
314
+ },
315
+ populate: {
316
+ // Filter the action to get only the content type entry
317
+ actions: {
318
+ where: {
319
+ target_type: contentTypeUid,
320
+ target_id: entryId
321
+ }
322
+ }
323
+ }
324
+ });
325
+ return releases.map((release2) => {
326
+ if (release2.actions?.length) {
327
+ const [actionForEntry] = release2.actions;
328
+ delete release2.actions;
329
+ return {
330
+ ...release2,
331
+ action: actionForEntry
332
+ };
333
+ }
334
+ return release2;
335
+ });
336
+ },
337
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
338
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
339
+ where: {
340
+ releasedAt: {
341
+ $null: true
342
+ },
343
+ actions: {
315
344
  target_type: contentTypeUid,
316
345
  target_id: entryId
317
346
  }
318
347
  }
319
- } : {};
348
+ });
320
349
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
321
350
  where: {
322
- ...whereActions,
351
+ $or: [
352
+ {
353
+ id: {
354
+ $notIn: releasesRelated.map((release2) => release2.id)
355
+ }
356
+ },
357
+ {
358
+ actions: null
359
+ }
360
+ ],
323
361
  releasedAt: {
324
362
  $null: true
325
363
  }
326
- },
327
- populate: {
328
- ...populateAttachedAction
329
364
  }
330
365
  });
331
366
  return releases.map((release2) => {
@@ -524,7 +559,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
524
559
  populate: {
525
560
  actions: {
526
561
  populate: {
527
- entry: true
562
+ entry: {
563
+ fields: ["id"]
564
+ }
528
565
  }
529
566
  }
530
567
  }
@@ -539,30 +576,80 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
539
576
  if (releaseWithPopulatedActionEntries.actions.length === 0) {
540
577
  throw new utils.errors.ValidationError("No entries to publish");
541
578
  }
542
- const actions = {};
579
+ const collectionTypeActions = {};
580
+ const singleTypeActions = [];
543
581
  for (const action of releaseWithPopulatedActionEntries.actions) {
544
582
  const contentTypeUid = action.contentType;
545
- if (!actions[contentTypeUid]) {
546
- actions[contentTypeUid] = {
547
- publish: [],
548
- unpublish: []
549
- };
550
- }
551
- if (action.type === "publish") {
552
- actions[contentTypeUid].publish.push(action.entry);
583
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
584
+ if (!collectionTypeActions[contentTypeUid]) {
585
+ collectionTypeActions[contentTypeUid] = {
586
+ entriestoPublishIds: [],
587
+ entriesToUnpublishIds: []
588
+ };
589
+ }
590
+ if (action.type === "publish") {
591
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
592
+ } else {
593
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
594
+ }
553
595
  } else {
554
- actions[contentTypeUid].unpublish.push(action.entry);
596
+ singleTypeActions.push({
597
+ uid: contentTypeUid,
598
+ action: action.type,
599
+ id: action.entry.id
600
+ });
555
601
  }
556
602
  }
557
603
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
604
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
558
605
  await strapi2.db.transaction(async () => {
559
- for (const contentTypeUid of Object.keys(actions)) {
560
- const { publish, unpublish } = actions[contentTypeUid];
561
- if (publish.length > 0) {
562
- await entityManagerService.publishMany(publish, contentTypeUid);
606
+ for (const { uid, action, id } of singleTypeActions) {
607
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
608
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
609
+ try {
610
+ if (action === "publish") {
611
+ await entityManagerService.publish(entry, uid);
612
+ } else {
613
+ await entityManagerService.unpublish(entry, uid);
614
+ }
615
+ } catch (error) {
616
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
617
+ ;
618
+ else {
619
+ throw error;
620
+ }
621
+ }
622
+ }
623
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
624
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
625
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
626
+ const entriesToPublish = await strapi2.entityService.findMany(
627
+ contentTypeUid,
628
+ {
629
+ filters: {
630
+ id: {
631
+ $in: entriestoPublishIds
632
+ }
633
+ },
634
+ populate
635
+ }
636
+ );
637
+ const entriesToUnpublish = await strapi2.entityService.findMany(
638
+ contentTypeUid,
639
+ {
640
+ filters: {
641
+ id: {
642
+ $in: entriesToUnpublishIds
643
+ }
644
+ },
645
+ populate
646
+ }
647
+ );
648
+ if (entriesToPublish.length > 0) {
649
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
563
650
  }
564
- if (unpublish.length > 0) {
565
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
651
+ if (entriesToUnpublish.length > 0) {
652
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
566
653
  }
567
654
  }
568
655
  });
@@ -660,34 +747,66 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
660
747
  if (pendingReleasesCount >= maximumPendingReleases) {
661
748
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
662
749
  }
750
+ },
751
+ async validateUniqueNameForPendingRelease(name) {
752
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
753
+ filters: {
754
+ releasedAt: {
755
+ $null: true
756
+ },
757
+ name
758
+ }
759
+ });
760
+ const isNameUnique = pendingReleases.length === 0;
761
+ if (!isNameUnique) {
762
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
763
+ }
764
+ },
765
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
766
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
767
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
768
+ }
663
769
  }
664
770
  });
665
- const createEventManagerService = () => {
666
- const state = {
667
- destroyListenerCallbacks: []
668
- };
771
+ const createSchedulingService = ({ strapi: strapi2 }) => {
772
+ const scheduledJobs = /* @__PURE__ */ new Map();
669
773
  return {
670
- addDestroyListenerCallback(destroyListenerCallback) {
671
- state.destroyListenerCallbacks.push(destroyListenerCallback);
672
- },
673
- destroyAllListeners() {
674
- if (!state.destroyListenerCallbacks.length) {
675
- return;
774
+ async set(releaseId, scheduleDate) {
775
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
776
+ if (!release2) {
777
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
676
778
  }
677
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
678
- destroyListenerCallback();
779
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
780
+ try {
781
+ await getService("release").publish(releaseId);
782
+ } catch (error) {
783
+ }
784
+ this.cancel(releaseId);
679
785
  });
786
+ if (scheduledJobs.has(releaseId)) {
787
+ this.cancel(releaseId);
788
+ }
789
+ scheduledJobs.set(releaseId, job);
790
+ return scheduledJobs;
791
+ },
792
+ cancel(releaseId) {
793
+ if (scheduledJobs.has(releaseId)) {
794
+ scheduledJobs.get(releaseId).cancel();
795
+ scheduledJobs.delete(releaseId);
796
+ }
797
+ return scheduledJobs;
680
798
  }
681
799
  };
682
800
  };
683
801
  const services = {
684
802
  release: createReleaseService,
685
- "release-action": createReleaseActionService,
686
803
  "release-validation": createReleaseValidationService,
687
- "event-manager": createEventManagerService
804
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
688
805
  };
689
806
  const RELEASE_SCHEMA = yup__namespace.object().shape({
690
- name: yup__namespace.string().trim().required()
807
+ name: yup__namespace.string().trim().required(),
808
+ // scheduledAt is a date, but we always receive strings from the client
809
+ scheduledAt: yup__namespace.string()
691
810
  }).required().noUnknown();
692
811
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
693
812
  const releaseController = {
@@ -704,9 +823,7 @@ const releaseController = {
704
823
  const contentTypeUid = query.contentTypeUid;
705
824
  const entryId = query.entryId;
706
825
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
707
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
708
- hasEntryAttached
709
- });
826
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
710
827
  ctx.body = { data };
711
828
  } else {
712
829
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -729,19 +846,18 @@ const releaseController = {
729
846
  const id = ctx.params.id;
730
847
  const releaseService = getService("release", { strapi });
731
848
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
732
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
733
- ability: ctx.state.userAbility,
734
- model: RELEASE_MODEL_UID
735
- });
736
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
849
+ if (!release2) {
850
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
851
+ }
737
852
  const count = await releaseService.countActions({
738
853
  filters: {
739
854
  release: id
740
855
  }
741
856
  });
742
- if (!release2) {
743
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
744
- }
857
+ const sanitizedRelease = {
858
+ ...release2,
859
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
860
+ };
745
861
  const data = {
746
862
  ...sanitizedRelease,
747
863
  actions: {
@@ -794,8 +910,27 @@ const releaseController = {
794
910
  const id = ctx.params.id;
795
911
  const releaseService = getService("release", { strapi });
796
912
  const release2 = await releaseService.publish(id, { user });
913
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
914
+ releaseService.countActions({
915
+ filters: {
916
+ release: id,
917
+ type: "publish"
918
+ }
919
+ }),
920
+ releaseService.countActions({
921
+ filters: {
922
+ release: id,
923
+ type: "unpublish"
924
+ }
925
+ })
926
+ ]);
797
927
  ctx.body = {
798
- data: release2
928
+ data: release2,
929
+ meta: {
930
+ totalEntries: countPublishActions + countUnpublishActions,
931
+ totalPublishedEntries: countPublishActions,
932
+ totalUnpublishedEntries: countUnpublishActions
933
+ }
799
934
  };
800
935
  }
801
936
  };
@@ -1063,19 +1198,14 @@ const routes = {
1063
1198
  };
1064
1199
  const { features } = require("@strapi/strapi/dist/utils/ee");
1065
1200
  const getPlugin = () => {
1066
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1201
+ if (features.isEnabled("cms-content-releases")) {
1067
1202
  return {
1068
1203
  register,
1069
1204
  bootstrap,
1070
1205
  contentTypes,
1071
1206
  services,
1072
1207
  controllers,
1073
- routes,
1074
- destroy() {
1075
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1076
- getService("event-manager").destroyAllListeners();
1077
- }
1078
- }
1208
+ routes
1079
1209
  };
1080
1210
  }
1081
1211
  return {