@strapi/content-releases 0.0.0-next.37dd1e3ff22e1635b69683abadd444912ae0dbff → 0.0.0-next.6a58621932ad3d83bf9d6928c1871e7906adcd59

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,5 +1,7 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1 } 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";
4
+ import EE from "@strapi/strapi/dist/utils/ee";
3
5
  import * as yup from "yup";
4
6
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
5
7
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -47,40 +49,47 @@ const ACTIONS = [
47
49
  pluginName: "content-releases"
48
50
  }
49
51
  ];
50
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
51
- return strapi2.plugin("content-releases").service(name);
52
- };
52
+ async function deleteActionsOnDisableDraftAndPublish({
53
+ oldContentTypes,
54
+ contentTypes: contentTypes2
55
+ }) {
56
+ if (!oldContentTypes) {
57
+ return;
58
+ }
59
+ for (const uid in contentTypes2) {
60
+ if (!oldContentTypes[uid]) {
61
+ continue;
62
+ }
63
+ const oldContentType = oldContentTypes[uid];
64
+ const contentType = contentTypes2[uid];
65
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
66
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
67
+ }
68
+ }
69
+ }
70
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
71
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
72
+ if (deletedContentTypes.length) {
73
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
74
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
75
+ });
76
+ }
77
+ }
53
78
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
54
79
  const register = async ({ strapi: strapi2 }) => {
55
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
80
+ if (features$2.isEnabled("cms-content-releases")) {
56
81
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
57
- const releaseActionService = getService("release-action", { strapi: strapi2 });
58
- const eventManager = getService("event-manager", { strapi: strapi2 });
59
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
60
- "content-type.update",
61
- async ({ contentType }) => {
62
- if (contentType.schema.options.draftAndPublish === false) {
63
- await releaseActionService.deleteManyForContentType(contentType.uid);
64
- }
65
- }
66
- );
67
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
68
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
69
- "content-type.delete",
70
- async ({ contentType }) => {
71
- await releaseActionService.deleteManyForContentType(contentType.uid);
72
- }
73
- );
74
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
82
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
83
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
75
84
  }
76
85
  };
77
86
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
78
87
  const bootstrap = async ({ strapi: strapi2 }) => {
79
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
88
+ if (features$1.isEnabled("cms-content-releases")) {
80
89
  strapi2.db.lifecycles.subscribe({
81
90
  afterDelete(event) {
82
91
  const { model, result } = event;
83
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
92
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
84
93
  const { id } = result;
85
94
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
86
95
  where: {
@@ -96,7 +105,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
96
105
  */
97
106
  async beforeDeleteMany(event) {
98
107
  const { model, params } = event;
99
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
108
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
100
109
  const { where } = params;
101
110
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
102
111
  event.state.entriesToDelete = entriesToDelete;
@@ -211,30 +220,32 @@ const contentTypes = {
211
220
  release: release$1,
212
221
  "release-action": releaseAction$1
213
222
  };
214
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
215
- async deleteManyForContentType(contentTypeUid) {
216
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
217
- where: {
218
- target_type: contentTypeUid
219
- }
220
- });
221
- }
222
- });
223
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
+ return strapi2.plugin("content-releases").service(name);
225
+ };
223
226
  const getGroupName = (queryValue) => {
224
227
  switch (queryValue) {
225
228
  case "contentType":
226
- return "entry.contentType.displayName";
229
+ return "contentType.displayName";
227
230
  case "action":
228
231
  return "type";
229
232
  case "locale":
230
- return _.getOr("No locale", "entry.locale.name");
233
+ return _.getOr("No locale", "locale.name");
231
234
  default:
232
- return "entry.contentType.displayName";
235
+ return "contentType.displayName";
233
236
  }
234
237
  };
235
238
  const createReleaseService = ({ strapi: strapi2 }) => ({
236
239
  async create(releaseData, { user }) {
237
240
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
241
+ const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
+ "release-validation",
243
+ { strapi: strapi2 }
244
+ );
245
+ await Promise.all([
246
+ validatePendingReleasesLimit(),
247
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
248
+ ]);
238
249
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
239
250
  data: releaseWithCreatorFields
240
251
  });
@@ -256,51 +267,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
256
267
  }
257
268
  });
258
269
  },
259
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
260
- hasEntryAttached
261
- } = {
262
- hasEntryAttached: false
263
- }) {
264
- const whereActions = hasEntryAttached ? {
265
- // Find all Releases where the content type entry is present
266
- actions: {
267
- target_type: contentTypeUid,
268
- target_id: entryId
269
- }
270
- } : {
271
- // Find all Releases where the content type entry is not present
272
- $or: [
273
- {
274
- $not: {
275
- actions: {
276
- target_type: contentTypeUid,
277
- target_id: entryId
278
- }
279
- }
270
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
271
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
272
+ where: {
273
+ actions: {
274
+ target_type: contentTypeUid,
275
+ target_id: entryId
280
276
  },
281
- {
282
- actions: null
277
+ releasedAt: {
278
+ $null: true
283
279
  }
284
- ]
285
- };
286
- const populateAttachedAction = hasEntryAttached ? {
287
- // Filter the action to get only the content type entry
288
- actions: {
289
- where: {
280
+ },
281
+ populate: {
282
+ // Filter the action to get only the content type entry
283
+ actions: {
284
+ where: {
285
+ target_type: contentTypeUid,
286
+ target_id: entryId
287
+ }
288
+ }
289
+ }
290
+ });
291
+ return releases.map((release2) => {
292
+ if (release2.actions?.length) {
293
+ const [actionForEntry] = release2.actions;
294
+ delete release2.actions;
295
+ return {
296
+ ...release2,
297
+ action: actionForEntry
298
+ };
299
+ }
300
+ return release2;
301
+ });
302
+ },
303
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
304
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
+ where: {
306
+ releasedAt: {
307
+ $null: true
308
+ },
309
+ actions: {
290
310
  target_type: contentTypeUid,
291
311
  target_id: entryId
292
312
  }
293
313
  }
294
- } : {};
314
+ });
295
315
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
296
316
  where: {
297
- ...whereActions,
317
+ $or: [
318
+ {
319
+ id: {
320
+ $notIn: releasesRelated.map((release2) => release2.id)
321
+ }
322
+ },
323
+ {
324
+ actions: null
325
+ }
326
+ ],
298
327
  releasedAt: {
299
328
  $null: true
300
329
  }
301
- },
302
- populate: {
303
- ...populateAttachedAction
304
330
  }
305
331
  });
306
332
  return releases.map((release2) => {
@@ -375,7 +401,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
375
401
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
376
402
  ...query,
377
403
  populate: {
378
- entry: true
404
+ entry: {
405
+ populate: "*"
406
+ }
379
407
  },
380
408
  filters: {
381
409
  release: releaseId
@@ -395,29 +423,32 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
395
423
  const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
396
424
  contentTypeUids
397
425
  );
398
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
399
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
400
- acc[locale.code] = { name: locale.name, code: locale.code };
401
- return acc;
402
- }, {});
426
+ const allLocalesDictionary = await this.getLocalesDataForActions();
403
427
  const formattedData = actions.map((action) => {
404
428
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
405
429
  return {
406
430
  ...action,
407
- entry: {
408
- id: action.entry.id,
409
- contentType: {
410
- displayName,
411
- mainFieldValue: action.entry[mainField]
412
- },
413
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
414
- status: action.entry.publishedAt ? "published" : "draft"
431
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
432
+ contentType: {
433
+ displayName,
434
+ mainFieldValue: action.entry[mainField],
435
+ uid: action.contentType
415
436
  }
416
437
  };
417
438
  });
418
439
  const groupName = getGroupName(groupBy);
419
440
  return _.groupBy(groupName)(formattedData);
420
441
  },
442
+ async getLocalesDataForActions() {
443
+ if (!strapi2.plugin("i18n")) {
444
+ return {};
445
+ }
446
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
447
+ return allLocales.reduce((acc, locale) => {
448
+ acc[locale.code] = { name: locale.name, code: locale.code };
449
+ return acc;
450
+ }, {});
451
+ },
421
452
  async getContentTypesDataForActions(contentTypesUids) {
422
453
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
423
454
  const contentTypesData = {};
@@ -432,6 +463,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
432
463
  }
433
464
  return contentTypesData;
434
465
  },
466
+ getContentTypeModelsFromActions(actions) {
467
+ const contentTypeUids = actions.reduce((acc, action) => {
468
+ if (!acc.includes(action.contentType)) {
469
+ acc.push(action.contentType);
470
+ }
471
+ return acc;
472
+ }, []);
473
+ const contentTypeModelsMap = contentTypeUids.reduce(
474
+ (acc, contentTypeUid) => {
475
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
476
+ return acc;
477
+ },
478
+ {}
479
+ );
480
+ return contentTypeModelsMap;
481
+ },
482
+ async getAllComponents() {
483
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
484
+ const components = await contentManagerComponentsService.findAllComponents();
485
+ const componentsMap = components.reduce(
486
+ (acc, component) => {
487
+ acc[component.uid] = component;
488
+ return acc;
489
+ },
490
+ {}
491
+ );
492
+ return componentsMap;
493
+ },
435
494
  async delete(releaseId) {
436
495
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
437
496
  populate: {
@@ -466,7 +525,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
466
525
  populate: {
467
526
  actions: {
468
527
  populate: {
469
- entry: true
528
+ entry: {
529
+ fields: ["id"]
530
+ }
470
531
  }
471
532
  }
472
533
  }
@@ -486,25 +547,49 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
486
547
  const contentTypeUid = action.contentType;
487
548
  if (!actions[contentTypeUid]) {
488
549
  actions[contentTypeUid] = {
489
- publish: [],
490
- unpublish: []
550
+ entriestoPublishIds: [],
551
+ entriesToUnpublishIds: []
491
552
  };
492
553
  }
493
554
  if (action.type === "publish") {
494
- actions[contentTypeUid].publish.push(action.entry);
555
+ actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
495
556
  } else {
496
- actions[contentTypeUid].unpublish.push(action.entry);
557
+ actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
497
558
  }
498
559
  }
499
560
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
500
562
  await strapi2.db.transaction(async () => {
501
563
  for (const contentTypeUid of Object.keys(actions)) {
502
- const { publish, unpublish } = actions[contentTypeUid];
503
- if (publish.length > 0) {
504
- await entityManagerService.publishMany(publish, contentTypeUid);
564
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
+ const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
566
+ const entriesToPublish = await strapi2.entityService.findMany(
567
+ contentTypeUid,
568
+ {
569
+ filters: {
570
+ id: {
571
+ $in: entriestoPublishIds
572
+ }
573
+ },
574
+ populate
575
+ }
576
+ );
577
+ const entriesToUnpublish = await strapi2.entityService.findMany(
578
+ contentTypeUid,
579
+ {
580
+ filters: {
581
+ id: {
582
+ $in: entriesToUnpublishIds
583
+ }
584
+ },
585
+ populate
586
+ }
587
+ );
588
+ if (entriesToPublish.length > 0) {
589
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
505
590
  }
506
- if (unpublish.length > 0) {
507
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
591
+ if (entriesToUnpublish.length > 0) {
592
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
508
593
  }
509
594
  }
510
595
  });
@@ -586,31 +671,41 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
586
671
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
587
672
  );
588
673
  }
589
- }
590
- });
591
- const createEventManagerService = () => {
592
- const state = {
593
- destroyListenerCallbacks: []
594
- };
595
- return {
596
- addDestroyListenerCallback(destroyListenerCallback) {
597
- state.destroyListenerCallbacks.push(destroyListenerCallback);
598
- },
599
- destroyAllListeners() {
600
- if (!state.destroyListenerCallbacks.length) {
601
- return;
674
+ },
675
+ async validatePendingReleasesLimit() {
676
+ const maximumPendingReleases = (
677
+ // @ts-expect-error - options is not typed into features
678
+ EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
679
+ );
680
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
681
+ filters: {
682
+ releasedAt: {
683
+ $null: true
684
+ }
602
685
  }
603
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
604
- destroyListenerCallback();
605
- });
686
+ });
687
+ if (pendingReleasesCount >= maximumPendingReleases) {
688
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
606
689
  }
607
- };
608
- };
690
+ },
691
+ async validateUniqueNameForPendingRelease(name) {
692
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
+ filters: {
694
+ releasedAt: {
695
+ $null: true
696
+ },
697
+ name
698
+ }
699
+ });
700
+ const isNameUnique = pendingReleases.length === 0;
701
+ if (!isNameUnique) {
702
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
703
+ }
704
+ }
705
+ });
609
706
  const services = {
610
707
  release: createReleaseService,
611
- "release-action": createReleaseActionService,
612
- "release-validation": createReleaseValidationService,
613
- "event-manager": createEventManagerService
708
+ "release-validation": createReleaseValidationService
614
709
  };
615
710
  const RELEASE_SCHEMA = yup.object().shape({
616
711
  name: yup.string().trim().required()
@@ -630,9 +725,7 @@ const releaseController = {
630
725
  const contentTypeUid = query.contentTypeUid;
631
726
  const entryId = query.entryId;
632
727
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
633
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
634
- hasEntryAttached
635
- });
728
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
636
729
  ctx.body = { data };
637
730
  } else {
638
731
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -760,11 +853,30 @@ const releaseActionController = {
760
853
  sort: query.groupBy === "action" ? "type" : query.groupBy,
761
854
  ...query
762
855
  });
763
- const groupedData = await releaseService.groupActions(results, query.groupBy);
856
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
857
+ if (acc[action.contentType]) {
858
+ return acc;
859
+ }
860
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
861
+ ability: ctx.state.userAbility,
862
+ model: action.contentType
863
+ });
864
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
865
+ return acc;
866
+ }, {});
867
+ const sanitizedResults = await mapAsync(results, async (action) => ({
868
+ ...action,
869
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
870
+ }));
871
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
872
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
873
+ const components = await releaseService.getAllComponents();
764
874
  ctx.body = {
765
875
  data: groupedData,
766
876
  meta: {
767
- pagination
877
+ pagination,
878
+ contentTypes: contentTypes2,
879
+ components
768
880
  }
769
881
  };
770
882
  },
@@ -970,19 +1082,14 @@ const routes = {
970
1082
  };
971
1083
  const { features } = require("@strapi/strapi/dist/utils/ee");
972
1084
  const getPlugin = () => {
973
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1085
+ if (features.isEnabled("cms-content-releases")) {
974
1086
  return {
975
1087
  register,
976
1088
  bootstrap,
977
1089
  contentTypes,
978
1090
  services,
979
1091
  controllers,
980
- routes,
981
- destroy() {
982
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
983
- getService("event-manager").destroyAllListeners();
984
- }
985
- }
1092
+ routes
986
1093
  };
987
1094
  }
988
1095
  return {