@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,6 +1,8 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const lodash = require("lodash");
3
4
  const _ = require("lodash/fp");
5
+ const EE = require("@strapi/strapi/dist/utils/ee");
4
6
  const yup = require("yup");
5
7
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
8
  function _interopNamespace(e) {
@@ -22,6 +24,7 @@ function _interopNamespace(e) {
22
24
  return Object.freeze(n);
23
25
  }
24
26
  const ___default = /* @__PURE__ */ _interopDefault(_);
27
+ const EE__default = /* @__PURE__ */ _interopDefault(EE);
25
28
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
26
29
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
27
30
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -69,40 +72,47 @@ const ACTIONS = [
69
72
  pluginName: "content-releases"
70
73
  }
71
74
  ];
72
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
73
- return strapi2.plugin("content-releases").service(name);
74
- };
75
+ async function deleteActionsOnDisableDraftAndPublish({
76
+ oldContentTypes,
77
+ contentTypes: contentTypes2
78
+ }) {
79
+ if (!oldContentTypes) {
80
+ return;
81
+ }
82
+ for (const uid in contentTypes2) {
83
+ if (!oldContentTypes[uid]) {
84
+ continue;
85
+ }
86
+ const oldContentType = oldContentTypes[uid];
87
+ const contentType = contentTypes2[uid];
88
+ if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
89
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
90
+ }
91
+ }
92
+ }
93
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
94
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
95
+ if (deletedContentTypes.length) {
96
+ await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
97
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
98
+ });
99
+ }
100
+ }
75
101
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
76
102
  const register = async ({ strapi: strapi2 }) => {
77
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
103
+ if (features$2.isEnabled("cms-content-releases")) {
78
104
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
79
- const releaseActionService = getService("release-action", { strapi: strapi2 });
80
- const eventManager = getService("event-manager", { strapi: strapi2 });
81
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
82
- "content-type.update",
83
- async ({ contentType }) => {
84
- if (contentType.schema.options.draftAndPublish === false) {
85
- await releaseActionService.deleteManyForContentType(contentType.uid);
86
- }
87
- }
88
- );
89
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
90
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
91
- "content-type.delete",
92
- async ({ contentType }) => {
93
- await releaseActionService.deleteManyForContentType(contentType.uid);
94
- }
95
- );
96
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
105
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
106
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
97
107
  }
98
108
  };
99
109
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
100
110
  const bootstrap = async ({ strapi: strapi2 }) => {
101
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
111
+ if (features$1.isEnabled("cms-content-releases")) {
102
112
  strapi2.db.lifecycles.subscribe({
103
113
  afterDelete(event) {
104
114
  const { model, result } = event;
105
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
115
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
106
116
  const { id } = result;
107
117
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
108
118
  where: {
@@ -118,7 +128,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
118
128
  */
119
129
  async beforeDeleteMany(event) {
120
130
  const { model, params } = event;
121
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
131
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
122
132
  const { where } = params;
123
133
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
124
134
  event.state.entriesToDelete = entriesToDelete;
@@ -233,30 +243,32 @@ const contentTypes = {
233
243
  release: release$1,
234
244
  "release-action": releaseAction$1
235
245
  };
236
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
237
- async deleteManyForContentType(contentTypeUid) {
238
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
239
- where: {
240
- target_type: contentTypeUid
241
- }
242
- });
243
- }
244
- });
246
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
+ return strapi2.plugin("content-releases").service(name);
248
+ };
245
249
  const getGroupName = (queryValue) => {
246
250
  switch (queryValue) {
247
251
  case "contentType":
248
- return "entry.contentType.displayName";
252
+ return "contentType.displayName";
249
253
  case "action":
250
254
  return "type";
251
255
  case "locale":
252
- return ___default.default.getOr("No locale", "entry.locale.name");
256
+ return ___default.default.getOr("No locale", "locale.name");
253
257
  default:
254
- return "entry.contentType.displayName";
258
+ return "contentType.displayName";
255
259
  }
256
260
  };
257
261
  const createReleaseService = ({ strapi: strapi2 }) => ({
258
262
  async create(releaseData, { user }) {
259
263
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
264
+ const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
+ "release-validation",
266
+ { strapi: strapi2 }
267
+ );
268
+ await Promise.all([
269
+ validatePendingReleasesLimit(),
270
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
271
+ ]);
260
272
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
261
273
  data: releaseWithCreatorFields
262
274
  });
@@ -278,51 +290,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
278
290
  }
279
291
  });
280
292
  },
281
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
282
- hasEntryAttached
283
- } = {
284
- hasEntryAttached: false
285
- }) {
286
- const whereActions = hasEntryAttached ? {
287
- // Find all Releases where the content type entry is present
288
- actions: {
289
- target_type: contentTypeUid,
290
- target_id: entryId
291
- }
292
- } : {
293
- // Find all Releases where the content type entry is not present
294
- $or: [
295
- {
296
- $not: {
297
- actions: {
298
- target_type: contentTypeUid,
299
- target_id: entryId
300
- }
301
- }
293
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
294
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
295
+ where: {
296
+ actions: {
297
+ target_type: contentTypeUid,
298
+ target_id: entryId
302
299
  },
303
- {
304
- actions: null
300
+ releasedAt: {
301
+ $null: true
305
302
  }
306
- ]
307
- };
308
- const populateAttachedAction = hasEntryAttached ? {
309
- // Filter the action to get only the content type entry
310
- actions: {
311
- where: {
303
+ },
304
+ populate: {
305
+ // Filter the action to get only the content type entry
306
+ actions: {
307
+ where: {
308
+ target_type: contentTypeUid,
309
+ target_id: entryId
310
+ }
311
+ }
312
+ }
313
+ });
314
+ return releases.map((release2) => {
315
+ if (release2.actions?.length) {
316
+ const [actionForEntry] = release2.actions;
317
+ delete release2.actions;
318
+ return {
319
+ ...release2,
320
+ action: actionForEntry
321
+ };
322
+ }
323
+ return release2;
324
+ });
325
+ },
326
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
327
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
328
+ where: {
329
+ releasedAt: {
330
+ $null: true
331
+ },
332
+ actions: {
312
333
  target_type: contentTypeUid,
313
334
  target_id: entryId
314
335
  }
315
336
  }
316
- } : {};
337
+ });
317
338
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
318
339
  where: {
319
- ...whereActions,
340
+ $or: [
341
+ {
342
+ id: {
343
+ $notIn: releasesRelated.map((release2) => release2.id)
344
+ }
345
+ },
346
+ {
347
+ actions: null
348
+ }
349
+ ],
320
350
  releasedAt: {
321
351
  $null: true
322
352
  }
323
- },
324
- populate: {
325
- ...populateAttachedAction
326
353
  }
327
354
  });
328
355
  return releases.map((release2) => {
@@ -397,7 +424,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
397
424
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
398
425
  ...query,
399
426
  populate: {
400
- entry: true
427
+ entry: {
428
+ populate: "*"
429
+ }
401
430
  },
402
431
  filters: {
403
432
  release: releaseId
@@ -417,29 +446,32 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
417
446
  const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
418
447
  contentTypeUids
419
448
  );
420
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
421
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
422
- acc[locale.code] = { name: locale.name, code: locale.code };
423
- return acc;
424
- }, {});
449
+ const allLocalesDictionary = await this.getLocalesDataForActions();
425
450
  const formattedData = actions.map((action) => {
426
451
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
427
452
  return {
428
453
  ...action,
429
- entry: {
430
- id: action.entry.id,
431
- contentType: {
432
- displayName,
433
- mainFieldValue: action.entry[mainField]
434
- },
435
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
436
- status: action.entry.publishedAt ? "published" : "draft"
454
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
455
+ contentType: {
456
+ displayName,
457
+ mainFieldValue: action.entry[mainField],
458
+ uid: action.contentType
437
459
  }
438
460
  };
439
461
  });
440
462
  const groupName = getGroupName(groupBy);
441
463
  return ___default.default.groupBy(groupName)(formattedData);
442
464
  },
465
+ async getLocalesDataForActions() {
466
+ if (!strapi2.plugin("i18n")) {
467
+ return {};
468
+ }
469
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
470
+ return allLocales.reduce((acc, locale) => {
471
+ acc[locale.code] = { name: locale.name, code: locale.code };
472
+ return acc;
473
+ }, {});
474
+ },
443
475
  async getContentTypesDataForActions(contentTypesUids) {
444
476
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
445
477
  const contentTypesData = {};
@@ -454,6 +486,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
454
486
  }
455
487
  return contentTypesData;
456
488
  },
489
+ getContentTypeModelsFromActions(actions) {
490
+ const contentTypeUids = actions.reduce((acc, action) => {
491
+ if (!acc.includes(action.contentType)) {
492
+ acc.push(action.contentType);
493
+ }
494
+ return acc;
495
+ }, []);
496
+ const contentTypeModelsMap = contentTypeUids.reduce(
497
+ (acc, contentTypeUid) => {
498
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
499
+ return acc;
500
+ },
501
+ {}
502
+ );
503
+ return contentTypeModelsMap;
504
+ },
505
+ async getAllComponents() {
506
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
507
+ const components = await contentManagerComponentsService.findAllComponents();
508
+ const componentsMap = components.reduce(
509
+ (acc, component) => {
510
+ acc[component.uid] = component;
511
+ return acc;
512
+ },
513
+ {}
514
+ );
515
+ return componentsMap;
516
+ },
457
517
  async delete(releaseId) {
458
518
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
459
519
  populate: {
@@ -488,7 +548,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
488
548
  populate: {
489
549
  actions: {
490
550
  populate: {
491
- entry: true
551
+ entry: {
552
+ fields: ["id"]
553
+ }
492
554
  }
493
555
  }
494
556
  }
@@ -508,25 +570,49 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
508
570
  const contentTypeUid = action.contentType;
509
571
  if (!actions[contentTypeUid]) {
510
572
  actions[contentTypeUid] = {
511
- publish: [],
512
- unpublish: []
573
+ entriestoPublishIds: [],
574
+ entriesToUnpublishIds: []
513
575
  };
514
576
  }
515
577
  if (action.type === "publish") {
516
- actions[contentTypeUid].publish.push(action.entry);
578
+ actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
517
579
  } else {
518
- actions[contentTypeUid].unpublish.push(action.entry);
580
+ actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
519
581
  }
520
582
  }
521
583
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
522
585
  await strapi2.db.transaction(async () => {
523
586
  for (const contentTypeUid of Object.keys(actions)) {
524
- const { publish, unpublish } = actions[contentTypeUid];
525
- if (publish.length > 0) {
526
- await entityManagerService.publishMany(publish, contentTypeUid);
587
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
+ const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
589
+ const entriesToPublish = await strapi2.entityService.findMany(
590
+ contentTypeUid,
591
+ {
592
+ filters: {
593
+ id: {
594
+ $in: entriestoPublishIds
595
+ }
596
+ },
597
+ populate
598
+ }
599
+ );
600
+ const entriesToUnpublish = await strapi2.entityService.findMany(
601
+ contentTypeUid,
602
+ {
603
+ filters: {
604
+ id: {
605
+ $in: entriesToUnpublishIds
606
+ }
607
+ },
608
+ populate
609
+ }
610
+ );
611
+ if (entriesToPublish.length > 0) {
612
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
527
613
  }
528
- if (unpublish.length > 0) {
529
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
614
+ if (entriesToUnpublish.length > 0) {
615
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
530
616
  }
531
617
  }
532
618
  });
@@ -608,31 +694,41 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
608
694
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
609
695
  );
610
696
  }
611
- }
612
- });
613
- const createEventManagerService = () => {
614
- const state = {
615
- destroyListenerCallbacks: []
616
- };
617
- return {
618
- addDestroyListenerCallback(destroyListenerCallback) {
619
- state.destroyListenerCallbacks.push(destroyListenerCallback);
620
- },
621
- destroyAllListeners() {
622
- if (!state.destroyListenerCallbacks.length) {
623
- return;
697
+ },
698
+ async validatePendingReleasesLimit() {
699
+ const maximumPendingReleases = (
700
+ // @ts-expect-error - options is not typed into features
701
+ EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
702
+ );
703
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
704
+ filters: {
705
+ releasedAt: {
706
+ $null: true
707
+ }
624
708
  }
625
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
626
- destroyListenerCallback();
627
- });
709
+ });
710
+ if (pendingReleasesCount >= maximumPendingReleases) {
711
+ throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
628
712
  }
629
- };
630
- };
713
+ },
714
+ async validateUniqueNameForPendingRelease(name) {
715
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
+ filters: {
717
+ releasedAt: {
718
+ $null: true
719
+ },
720
+ name
721
+ }
722
+ });
723
+ const isNameUnique = pendingReleases.length === 0;
724
+ if (!isNameUnique) {
725
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
+ }
727
+ }
728
+ });
631
729
  const services = {
632
730
  release: createReleaseService,
633
- "release-action": createReleaseActionService,
634
- "release-validation": createReleaseValidationService,
635
- "event-manager": createEventManagerService
731
+ "release-validation": createReleaseValidationService
636
732
  };
637
733
  const RELEASE_SCHEMA = yup__namespace.object().shape({
638
734
  name: yup__namespace.string().trim().required()
@@ -652,9 +748,7 @@ const releaseController = {
652
748
  const contentTypeUid = query.contentTypeUid;
653
749
  const entryId = query.entryId;
654
750
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
655
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
656
- hasEntryAttached
657
- });
751
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
658
752
  ctx.body = { data };
659
753
  } else {
660
754
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -782,11 +876,30 @@ const releaseActionController = {
782
876
  sort: query.groupBy === "action" ? "type" : query.groupBy,
783
877
  ...query
784
878
  });
785
- const groupedData = await releaseService.groupActions(results, query.groupBy);
879
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
880
+ if (acc[action.contentType]) {
881
+ return acc;
882
+ }
883
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
884
+ ability: ctx.state.userAbility,
885
+ model: action.contentType
886
+ });
887
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
888
+ return acc;
889
+ }, {});
890
+ const sanitizedResults = await utils.mapAsync(results, async (action) => ({
891
+ ...action,
892
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
893
+ }));
894
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
895
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
896
+ const components = await releaseService.getAllComponents();
786
897
  ctx.body = {
787
898
  data: groupedData,
788
899
  meta: {
789
- pagination
900
+ pagination,
901
+ contentTypes: contentTypes2,
902
+ components
790
903
  }
791
904
  };
792
905
  },
@@ -992,19 +1105,14 @@ const routes = {
992
1105
  };
993
1106
  const { features } = require("@strapi/strapi/dist/utils/ee");
994
1107
  const getPlugin = () => {
995
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1108
+ if (features.isEnabled("cms-content-releases")) {
996
1109
  return {
997
1110
  register,
998
1111
  bootstrap,
999
1112
  contentTypes,
1000
1113
  services,
1001
1114
  controllers,
1002
- routes,
1003
- destroy() {
1004
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1005
- getService("event-manager").destroyAllListeners();
1006
- }
1007
- }
1115
+ routes
1008
1116
  };
1009
1117
  }
1010
1118
  return {