@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,6 +1,7 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
3
  const _ = require("lodash/fp");
4
+ const EE = require("@strapi/strapi/dist/utils/ee");
4
5
  const yup = require("yup");
5
6
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
7
  function _interopNamespace(e) {
@@ -22,6 +23,7 @@ function _interopNamespace(e) {
22
23
  return Object.freeze(n);
23
24
  }
24
25
  const ___default = /* @__PURE__ */ _interopDefault(_);
26
+ const EE__default = /* @__PURE__ */ _interopDefault(EE);
25
27
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
26
28
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
27
29
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -69,10 +71,80 @@ const ACTIONS = [
69
71
  pluginName: "content-releases"
70
72
  }
71
73
  ];
72
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
74
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
75
+ return strapi2.plugin("content-releases").service(name);
76
+ };
77
+ const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
73
78
  const register = async ({ strapi: strapi2 }) => {
74
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
79
+ if (features$2.isEnabled("cms-content-releases")) {
75
80
  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);
99
+ }
100
+ };
101
+ const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
102
+ const bootstrap = async ({ strapi: strapi2 }) => {
103
+ if (features$1.isEnabled("cms-content-releases")) {
104
+ strapi2.db.lifecycles.subscribe({
105
+ afterDelete(event) {
106
+ const { model, result } = event;
107
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
108
+ const { id } = result;
109
+ strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
110
+ where: {
111
+ target_type: model.uid,
112
+ target_id: id
113
+ }
114
+ });
115
+ }
116
+ },
117
+ /**
118
+ * deleteMany hook doesn't return the deleted entries ids
119
+ * so we need to fetch them before deleting the entries to save the ids on our state
120
+ */
121
+ async beforeDeleteMany(event) {
122
+ const { model, params } = event;
123
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
124
+ const { where } = params;
125
+ const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
126
+ event.state.entriesToDelete = entriesToDelete;
127
+ }
128
+ },
129
+ /**
130
+ * We delete the release actions related to deleted entries
131
+ * We make this only after deleteMany is succesfully executed to avoid errors
132
+ */
133
+ async afterDeleteMany(event) {
134
+ const { model, state } = event;
135
+ const entriesToDelete = state.entriesToDelete;
136
+ if (entriesToDelete) {
137
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
138
+ where: {
139
+ target_type: model.uid,
140
+ target_id: {
141
+ $in: entriesToDelete.map((entry) => entry.id)
142
+ }
143
+ }
144
+ });
145
+ }
146
+ }
147
+ });
76
148
  }
77
149
  };
78
150
  const schema$1 = {
@@ -163,24 +235,31 @@ const contentTypes = {
163
235
  release: release$1,
164
236
  "release-action": releaseAction$1
165
237
  };
166
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
167
- return strapi2.plugin("content-releases").service(name);
168
- };
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
+ });
169
247
  const getGroupName = (queryValue) => {
170
248
  switch (queryValue) {
171
249
  case "contentType":
172
- return "entry.contentType.displayName";
250
+ return "contentType.displayName";
173
251
  case "action":
174
252
  return "type";
175
253
  case "locale":
176
- return ___default.default.getOr("No locale", "entry.locale.name");
254
+ return ___default.default.getOr("No locale", "locale.name");
177
255
  default:
178
- return "entry.contentType.displayName";
256
+ return "contentType.displayName";
179
257
  }
180
258
  };
181
259
  const createReleaseService = ({ strapi: strapi2 }) => ({
182
260
  async create(releaseData, { user }) {
183
261
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
262
+ await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
184
263
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
185
264
  data: releaseWithCreatorFields
186
265
  });
@@ -262,19 +341,23 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
262
341
  });
263
342
  },
264
343
  async update(id, releaseData, { user }) {
265
- const updatedRelease = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
266
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
344
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
345
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
346
+ if (!release2) {
347
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
348
+ }
349
+ if (release2.releasedAt) {
350
+ throw new utils.errors.ValidationError("Release already published");
351
+ }
352
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
267
353
  /*
268
354
  * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
269
355
  * is not compatible with the type we are passing here: UpdateRelease.Request['body']
270
356
  */
271
357
  // @ts-expect-error see above
272
- data: updatedRelease
358
+ data: releaseWithCreatorFields
273
359
  });
274
- if (!release2) {
275
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
276
- }
277
- return release2;
360
+ return updatedRelease;
278
361
  },
279
362
  async createAction(releaseId, action) {
280
363
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
@@ -284,6 +367,13 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
284
367
  validateEntryContentType(action.entry.contentType),
285
368
  validateUniqueEntry(releaseId, action)
286
369
  ]);
370
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
371
+ if (!release2) {
372
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
373
+ }
374
+ if (release2.releasedAt) {
375
+ throw new utils.errors.ValidationError("Release already published");
376
+ }
287
377
  const { entry, type } = action;
288
378
  return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
289
379
  data: {
@@ -310,7 +400,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
310
400
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
311
401
  ...query,
312
402
  populate: {
313
- entry: true
403
+ entry: {
404
+ populate: "*"
405
+ }
314
406
  },
315
407
  filters: {
316
408
  release: releaseId
@@ -330,29 +422,32 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
330
422
  const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
331
423
  contentTypeUids
332
424
  );
333
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
334
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
335
- acc[locale.code] = { name: locale.name, code: locale.code };
336
- return acc;
337
- }, {});
425
+ const allLocalesDictionary = await this.getLocalesDataForActions();
338
426
  const formattedData = actions.map((action) => {
339
427
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
340
428
  return {
341
429
  ...action,
342
- entry: {
343
- id: action.entry.id,
344
- contentType: {
345
- displayName,
346
- mainFieldValue: action.entry[mainField]
347
- },
348
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
349
- status: action.entry.publishedAt ? "published" : "draft"
430
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
431
+ contentType: {
432
+ displayName,
433
+ mainFieldValue: action.entry[mainField],
434
+ uid: action.contentType
350
435
  }
351
436
  };
352
437
  });
353
438
  const groupName = getGroupName(groupBy);
354
439
  return ___default.default.groupBy(groupName)(formattedData);
355
440
  },
441
+ async getLocalesDataForActions() {
442
+ if (!strapi2.plugin("i18n")) {
443
+ return {};
444
+ }
445
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
446
+ return allLocales.reduce((acc, locale) => {
447
+ acc[locale.code] = { name: locale.name, code: locale.code };
448
+ return acc;
449
+ }, {});
450
+ },
356
451
  async getContentTypesDataForActions(contentTypesUids) {
357
452
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
358
453
  const contentTypesData = {};
@@ -367,6 +462,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
367
462
  }
368
463
  return contentTypesData;
369
464
  },
465
+ getContentTypeModelsFromActions(actions) {
466
+ const contentTypeUids = actions.reduce((acc, action) => {
467
+ if (!acc.includes(action.contentType)) {
468
+ acc.push(action.contentType);
469
+ }
470
+ return acc;
471
+ }, []);
472
+ const contentTypeModelsMap = contentTypeUids.reduce(
473
+ (acc, contentTypeUid) => {
474
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
475
+ return acc;
476
+ },
477
+ {}
478
+ );
479
+ return contentTypeModelsMap;
480
+ },
481
+ async getAllComponents() {
482
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
483
+ const components = await contentManagerComponentsService.findAllComponents();
484
+ const componentsMap = components.reduce(
485
+ (acc, component) => {
486
+ acc[component.uid] = component;
487
+ return acc;
488
+ },
489
+ {}
490
+ );
491
+ return componentsMap;
492
+ },
370
493
  async delete(releaseId) {
371
494
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
372
495
  populate: {
@@ -458,13 +581,18 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
458
581
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
459
582
  where: {
460
583
  id: actionId,
461
- release: releaseId
584
+ release: {
585
+ id: releaseId,
586
+ releasedAt: {
587
+ $null: true
588
+ }
589
+ }
462
590
  },
463
591
  data: update
464
592
  });
465
593
  if (!updatedAction) {
466
594
  throw new utils.errors.NotFoundError(
467
- `Action with id ${actionId} not found in release with id ${releaseId}`
595
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
468
596
  );
469
597
  }
470
598
  return updatedAction;
@@ -473,12 +601,17 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
473
601
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
474
602
  where: {
475
603
  id: actionId,
476
- release: releaseId
604
+ release: {
605
+ id: releaseId,
606
+ releasedAt: {
607
+ $null: true
608
+ }
609
+ }
477
610
  }
478
611
  });
479
612
  if (!deletedAction) {
480
613
  throw new utils.errors.NotFoundError(
481
- `Action with id ${actionId} not found in release with id ${releaseId}`
614
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
482
615
  );
483
616
  }
484
617
  return deletedAction;
@@ -511,9 +644,48 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
511
644
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
512
645
  );
513
646
  }
647
+ },
648
+ async validatePendingReleasesLimit() {
649
+ const maximumPendingReleases = (
650
+ // @ts-expect-error - options is not typed into features
651
+ EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
652
+ );
653
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
654
+ filters: {
655
+ releasedAt: {
656
+ $null: true
657
+ }
658
+ }
659
+ });
660
+ if (pendingReleasesCount >= maximumPendingReleases) {
661
+ throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
662
+ }
514
663
  }
515
664
  });
516
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
665
+ const createEventManagerService = () => {
666
+ const state = {
667
+ destroyListenerCallbacks: []
668
+ };
669
+ return {
670
+ addDestroyListenerCallback(destroyListenerCallback) {
671
+ state.destroyListenerCallbacks.push(destroyListenerCallback);
672
+ },
673
+ destroyAllListeners() {
674
+ if (!state.destroyListenerCallbacks.length) {
675
+ return;
676
+ }
677
+ state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
678
+ destroyListenerCallback();
679
+ });
680
+ }
681
+ };
682
+ };
683
+ const services = {
684
+ release: createReleaseService,
685
+ "release-action": createReleaseActionService,
686
+ "release-validation": createReleaseValidationService,
687
+ "event-manager": createEventManagerService
688
+ };
517
689
  const RELEASE_SCHEMA = yup__namespace.object().shape({
518
690
  name: yup__namespace.string().trim().required()
519
691
  }).required().noUnknown();
@@ -662,11 +834,30 @@ const releaseActionController = {
662
834
  sort: query.groupBy === "action" ? "type" : query.groupBy,
663
835
  ...query
664
836
  });
665
- const groupedData = await releaseService.groupActions(results, query.groupBy);
837
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
838
+ if (acc[action.contentType]) {
839
+ return acc;
840
+ }
841
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
842
+ ability: ctx.state.userAbility,
843
+ model: action.contentType
844
+ });
845
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
846
+ return acc;
847
+ }, {});
848
+ const sanitizedResults = await utils.mapAsync(results, async (action) => ({
849
+ ...action,
850
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
851
+ }));
852
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
853
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
854
+ const components = await releaseService.getAllComponents();
666
855
  ctx.body = {
667
856
  data: groupedData,
668
857
  meta: {
669
- pagination
858
+ pagination,
859
+ contentTypes: contentTypes2,
860
+ components
670
861
  }
671
862
  };
672
863
  },
@@ -688,10 +879,8 @@ const releaseActionController = {
688
879
  async delete(ctx) {
689
880
  const actionId = ctx.params.actionId;
690
881
  const releaseId = ctx.params.releaseId;
691
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
692
- actionId,
693
- releaseId
694
- );
882
+ const releaseService = getService("release", { strapi });
883
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
695
884
  ctx.body = {
696
885
  data: deletedReleaseAction
697
886
  };
@@ -874,13 +1063,19 @@ const routes = {
874
1063
  };
875
1064
  const { features } = require("@strapi/strapi/dist/utils/ee");
876
1065
  const getPlugin = () => {
877
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1066
+ if (features.isEnabled("cms-content-releases")) {
878
1067
  return {
879
1068
  register,
1069
+ bootstrap,
880
1070
  contentTypes,
881
1071
  services,
882
1072
  controllers,
883
- routes
1073
+ routes,
1074
+ destroy() {
1075
+ if (features.isEnabled("cms-content-releases")) {
1076
+ getService("event-manager").destroyAllListeners();
1077
+ }
1078
+ }
884
1079
  };
885
1080
  }
886
1081
  return {