@strapi/content-releases 0.0.0-experimental.e1ede8c55a0e1e22ce20137bf238fc374bd5dd51 → 0.0.0-experimental.ee4d311a5e6a131fad03cf07e4696f49fdd9c2e6

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,6 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { setCreatorFields, errors, validateYupSchema, yup as yup$1, mapAsync } from "@strapi/utils";
2
2
  import _ from "lodash/fp";
3
+ import EE from "@strapi/strapi/dist/utils/ee";
3
4
  import * as yup from "yup";
4
5
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
5
6
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -47,10 +48,80 @@ const ACTIONS = [
47
48
  pluginName: "content-releases"
48
49
  }
49
50
  ];
50
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
51
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
52
+ return strapi2.plugin("content-releases").service(name);
53
+ };
54
+ const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
51
55
  const register = async ({ strapi: strapi2 }) => {
52
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
56
+ if (features$2.isEnabled("cms-content-releases")) {
53
57
  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);
76
+ }
77
+ };
78
+ const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
79
+ const bootstrap = async ({ strapi: strapi2 }) => {
80
+ if (features$1.isEnabled("cms-content-releases")) {
81
+ strapi2.db.lifecycles.subscribe({
82
+ afterDelete(event) {
83
+ const { model, result } = event;
84
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
85
+ const { id } = result;
86
+ strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
87
+ where: {
88
+ target_type: model.uid,
89
+ target_id: id
90
+ }
91
+ });
92
+ }
93
+ },
94
+ /**
95
+ * deleteMany hook doesn't return the deleted entries ids
96
+ * so we need to fetch them before deleting the entries to save the ids on our state
97
+ */
98
+ async beforeDeleteMany(event) {
99
+ const { model, params } = event;
100
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
101
+ const { where } = params;
102
+ const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
103
+ event.state.entriesToDelete = entriesToDelete;
104
+ }
105
+ },
106
+ /**
107
+ * We delete the release actions related to deleted entries
108
+ * We make this only after deleteMany is succesfully executed to avoid errors
109
+ */
110
+ async afterDeleteMany(event) {
111
+ const { model, state } = event;
112
+ const entriesToDelete = state.entriesToDelete;
113
+ if (entriesToDelete) {
114
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
115
+ where: {
116
+ target_type: model.uid,
117
+ target_id: {
118
+ $in: entriesToDelete.map((entry) => entry.id)
119
+ }
120
+ }
121
+ });
122
+ }
123
+ }
124
+ });
54
125
  }
55
126
  };
56
127
  const schema$1 = {
@@ -141,24 +212,31 @@ const contentTypes = {
141
212
  release: release$1,
142
213
  "release-action": releaseAction$1
143
214
  };
144
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
145
- return strapi2.plugin("content-releases").service(name);
146
- };
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
+ });
147
224
  const getGroupName = (queryValue) => {
148
225
  switch (queryValue) {
149
226
  case "contentType":
150
- return "entry.contentType.displayName";
227
+ return "contentType.displayName";
151
228
  case "action":
152
229
  return "type";
153
230
  case "locale":
154
- return _.getOr("No locale", "entry.locale.name");
231
+ return _.getOr("No locale", "locale.name");
155
232
  default:
156
- return "entry.contentType.displayName";
233
+ return "contentType.displayName";
157
234
  }
158
235
  };
159
236
  const createReleaseService = ({ strapi: strapi2 }) => ({
160
237
  async create(releaseData, { user }) {
161
238
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
+ await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
162
240
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
163
241
  data: releaseWithCreatorFields
164
242
  });
@@ -240,19 +318,23 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
240
318
  });
241
319
  },
242
320
  async update(id, releaseData, { user }) {
243
- const updatedRelease = await setCreatorFields({ user, isEdition: true })(releaseData);
244
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
321
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
322
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
323
+ if (!release2) {
324
+ throw new errors.NotFoundError(`No release found for id ${id}`);
325
+ }
326
+ if (release2.releasedAt) {
327
+ throw new errors.ValidationError("Release already published");
328
+ }
329
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
245
330
  /*
246
331
  * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
247
332
  * is not compatible with the type we are passing here: UpdateRelease.Request['body']
248
333
  */
249
334
  // @ts-expect-error see above
250
- data: updatedRelease
335
+ data: releaseWithCreatorFields
251
336
  });
252
- if (!release2) {
253
- throw new errors.NotFoundError(`No release found for id ${id}`);
254
- }
255
- return release2;
337
+ return updatedRelease;
256
338
  },
257
339
  async createAction(releaseId, action) {
258
340
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
@@ -262,6 +344,13 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
262
344
  validateEntryContentType(action.entry.contentType),
263
345
  validateUniqueEntry(releaseId, action)
264
346
  ]);
347
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
348
+ if (!release2) {
349
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
350
+ }
351
+ if (release2.releasedAt) {
352
+ throw new errors.ValidationError("Release already published");
353
+ }
265
354
  const { entry, type } = action;
266
355
  return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
267
356
  data: {
@@ -288,7 +377,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
288
377
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
289
378
  ...query,
290
379
  populate: {
291
- entry: true
380
+ entry: {
381
+ populate: "*"
382
+ }
292
383
  },
293
384
  filters: {
294
385
  release: releaseId
@@ -308,29 +399,32 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
308
399
  const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
309
400
  contentTypeUids
310
401
  );
311
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
312
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
313
- acc[locale.code] = { name: locale.name, code: locale.code };
314
- return acc;
315
- }, {});
402
+ const allLocalesDictionary = await this.getLocalesDataForActions();
316
403
  const formattedData = actions.map((action) => {
317
404
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
318
405
  return {
319
406
  ...action,
320
- entry: {
321
- id: action.entry.id,
322
- contentType: {
323
- displayName,
324
- mainFieldValue: action.entry[mainField]
325
- },
326
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
327
- status: action.entry.publishedAt ? "published" : "draft"
407
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
408
+ contentType: {
409
+ displayName,
410
+ mainFieldValue: action.entry[mainField],
411
+ uid: action.contentType
328
412
  }
329
413
  };
330
414
  });
331
415
  const groupName = getGroupName(groupBy);
332
416
  return _.groupBy(groupName)(formattedData);
333
417
  },
418
+ async getLocalesDataForActions() {
419
+ if (!strapi2.plugin("i18n")) {
420
+ return {};
421
+ }
422
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
423
+ return allLocales.reduce((acc, locale) => {
424
+ acc[locale.code] = { name: locale.name, code: locale.code };
425
+ return acc;
426
+ }, {});
427
+ },
334
428
  async getContentTypesDataForActions(contentTypesUids) {
335
429
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
336
430
  const contentTypesData = {};
@@ -345,6 +439,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
345
439
  }
346
440
  return contentTypesData;
347
441
  },
442
+ getContentTypeModelsFromActions(actions) {
443
+ const contentTypeUids = actions.reduce((acc, action) => {
444
+ if (!acc.includes(action.contentType)) {
445
+ acc.push(action.contentType);
446
+ }
447
+ return acc;
448
+ }, []);
449
+ const contentTypeModelsMap = contentTypeUids.reduce(
450
+ (acc, contentTypeUid) => {
451
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
452
+ return acc;
453
+ },
454
+ {}
455
+ );
456
+ return contentTypeModelsMap;
457
+ },
458
+ async getAllComponents() {
459
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
460
+ const components = await contentManagerComponentsService.findAllComponents();
461
+ const componentsMap = components.reduce(
462
+ (acc, component) => {
463
+ acc[component.uid] = component;
464
+ return acc;
465
+ },
466
+ {}
467
+ );
468
+ return componentsMap;
469
+ },
348
470
  async delete(releaseId) {
349
471
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
350
472
  populate: {
@@ -436,13 +558,18 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
436
558
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
437
559
  where: {
438
560
  id: actionId,
439
- release: releaseId
561
+ release: {
562
+ id: releaseId,
563
+ releasedAt: {
564
+ $null: true
565
+ }
566
+ }
440
567
  },
441
568
  data: update
442
569
  });
443
570
  if (!updatedAction) {
444
571
  throw new errors.NotFoundError(
445
- `Action with id ${actionId} not found in release with id ${releaseId}`
572
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
446
573
  );
447
574
  }
448
575
  return updatedAction;
@@ -451,12 +578,17 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
451
578
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
452
579
  where: {
453
580
  id: actionId,
454
- release: releaseId
581
+ release: {
582
+ id: releaseId,
583
+ releasedAt: {
584
+ $null: true
585
+ }
586
+ }
455
587
  }
456
588
  });
457
589
  if (!deletedAction) {
458
590
  throw new errors.NotFoundError(
459
- `Action with id ${actionId} not found in release with id ${releaseId}`
591
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
460
592
  );
461
593
  }
462
594
  return deletedAction;
@@ -489,9 +621,48 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
489
621
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
490
622
  );
491
623
  }
624
+ },
625
+ async validatePendingReleasesLimit() {
626
+ const maximumPendingReleases = (
627
+ // @ts-expect-error - options is not typed into features
628
+ EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
629
+ );
630
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
631
+ filters: {
632
+ releasedAt: {
633
+ $null: true
634
+ }
635
+ }
636
+ });
637
+ if (pendingReleasesCount >= maximumPendingReleases) {
638
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
639
+ }
492
640
  }
493
641
  });
494
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
642
+ const createEventManagerService = () => {
643
+ const state = {
644
+ destroyListenerCallbacks: []
645
+ };
646
+ return {
647
+ addDestroyListenerCallback(destroyListenerCallback) {
648
+ state.destroyListenerCallbacks.push(destroyListenerCallback);
649
+ },
650
+ destroyAllListeners() {
651
+ if (!state.destroyListenerCallbacks.length) {
652
+ return;
653
+ }
654
+ state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
655
+ destroyListenerCallback();
656
+ });
657
+ }
658
+ };
659
+ };
660
+ const services = {
661
+ release: createReleaseService,
662
+ "release-action": createReleaseActionService,
663
+ "release-validation": createReleaseValidationService,
664
+ "event-manager": createEventManagerService
665
+ };
495
666
  const RELEASE_SCHEMA = yup.object().shape({
496
667
  name: yup.string().trim().required()
497
668
  }).required().noUnknown();
@@ -640,11 +811,30 @@ const releaseActionController = {
640
811
  sort: query.groupBy === "action" ? "type" : query.groupBy,
641
812
  ...query
642
813
  });
643
- const groupedData = await releaseService.groupActions(results, query.groupBy);
814
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
815
+ if (acc[action.contentType]) {
816
+ return acc;
817
+ }
818
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
819
+ ability: ctx.state.userAbility,
820
+ model: action.contentType
821
+ });
822
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
823
+ return acc;
824
+ }, {});
825
+ const sanitizedResults = await mapAsync(results, async (action) => ({
826
+ ...action,
827
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
828
+ }));
829
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
830
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
831
+ const components = await releaseService.getAllComponents();
644
832
  ctx.body = {
645
833
  data: groupedData,
646
834
  meta: {
647
- pagination
835
+ pagination,
836
+ contentTypes: contentTypes2,
837
+ components
648
838
  }
649
839
  };
650
840
  },
@@ -666,10 +856,8 @@ const releaseActionController = {
666
856
  async delete(ctx) {
667
857
  const actionId = ctx.params.actionId;
668
858
  const releaseId = ctx.params.releaseId;
669
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
670
- actionId,
671
- releaseId
672
- );
859
+ const releaseService = getService("release", { strapi });
860
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
673
861
  ctx.body = {
674
862
  data: deletedReleaseAction
675
863
  };
@@ -852,13 +1040,19 @@ const routes = {
852
1040
  };
853
1041
  const { features } = require("@strapi/strapi/dist/utils/ee");
854
1042
  const getPlugin = () => {
855
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1043
+ if (features.isEnabled("cms-content-releases")) {
856
1044
  return {
857
1045
  register,
1046
+ bootstrap,
858
1047
  contentTypes,
859
1048
  services,
860
1049
  controllers,
861
- routes
1050
+ routes,
1051
+ destroy() {
1052
+ if (features.isEnabled("cms-content-releases")) {
1053
+ getService("event-manager").destroyAllListeners();
1054
+ }
1055
+ }
862
1056
  };
863
1057
  }
864
1058
  return {