@strapi/content-releases 0.0.0-experimental.f7b9b47085e387e97f990d8695971b51d7f7149a → 0.0.0-next.2b10ca9b97a5854909ba0a8d1d5b00f73cae58fa

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,4 +1,6 @@
1
1
  import { setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import _ from "lodash/fp";
3
+ import EE from "@strapi/strapi/dist/utils/ee";
2
4
  import * as yup from "yup";
3
5
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
4
6
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -46,10 +48,80 @@ const ACTIONS = [
46
48
  pluginName: "content-releases"
47
49
  }
48
50
  ];
49
- 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");
50
55
  const register = async ({ strapi: strapi2 }) => {
51
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
56
+ if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
52
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") && strapi2.features.future.isEnabled("contentReleases")) {
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
+ });
53
125
  }
54
126
  };
55
127
  const schema$1 = {
@@ -122,6 +194,9 @@ const schema = {
122
194
  type: "string",
123
195
  required: true
124
196
  },
197
+ locale: {
198
+ type: "string"
199
+ },
125
200
  release: {
126
201
  type: "relation",
127
202
  relation: "manyToOne",
@@ -137,12 +212,31 @@ const contentTypes = {
137
212
  release: release$1,
138
213
  "release-action": releaseAction$1
139
214
  };
140
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
141
- return strapi2.plugin("content-releases").service(name);
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
+ });
224
+ const getGroupName = (queryValue) => {
225
+ switch (queryValue) {
226
+ case "contentType":
227
+ return "entry.contentType.displayName";
228
+ case "action":
229
+ return "type";
230
+ case "locale":
231
+ return _.getOr("No locale", "entry.locale.name");
232
+ default:
233
+ return "entry.contentType.displayName";
234
+ }
142
235
  };
143
236
  const createReleaseService = ({ strapi: strapi2 }) => ({
144
237
  async create(releaseData, { user }) {
145
238
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
+ await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
146
240
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
147
241
  data: releaseWithCreatorFields
148
242
  });
@@ -224,19 +318,23 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
224
318
  });
225
319
  },
226
320
  async update(id, releaseData, { user }) {
227
- const updatedRelease = await setCreatorFields({ user, isEdition: true })(releaseData);
228
- 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, {
229
330
  /*
230
331
  * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
231
332
  * is not compatible with the type we are passing here: UpdateRelease.Request['body']
232
333
  */
233
334
  // @ts-expect-error see above
234
- data: updatedRelease
335
+ data: releaseWithCreatorFields
235
336
  });
236
- if (!release2) {
237
- throw new errors.NotFoundError(`No release found for id ${id}`);
238
- }
239
- return release2;
337
+ return updatedRelease;
240
338
  },
241
339
  async createAction(releaseId, action) {
242
340
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
@@ -246,11 +344,19 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
246
344
  validateEntryContentType(action.entry.contentType),
247
345
  validateUniqueEntry(releaseId, action)
248
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
+ }
249
354
  const { entry, type } = action;
250
355
  return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
251
356
  data: {
252
357
  type,
253
358
  contentType: entry.contentType,
359
+ locale: entry.locale,
254
360
  entry: {
255
361
  id: entry.id,
256
362
  __type: entry.contentType,
@@ -262,8 +368,10 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
262
368
  });
263
369
  },
264
370
  async findActions(releaseId, query) {
265
- const result = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
266
- if (!result) {
371
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
372
+ fields: ["id"]
373
+ });
374
+ if (!release2) {
267
375
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
268
376
  }
269
377
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
@@ -279,18 +387,40 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
279
387
  async countActions(query) {
280
388
  return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
281
389
  },
282
- async getAllContentTypeUids(releaseId) {
283
- const contentTypesFromReleaseActions = await strapi2.db.queryBuilder(RELEASE_ACTION_MODEL_UID).select("content_type").where({
284
- $and: [
285
- {
286
- release: releaseId
390
+ async groupActions(actions, groupBy) {
391
+ const contentTypeUids = actions.reduce((acc, action) => {
392
+ if (!acc.includes(action.contentType)) {
393
+ acc.push(action.contentType);
394
+ }
395
+ return acc;
396
+ }, []);
397
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
398
+ contentTypeUids
399
+ );
400
+ const allLocales = await strapi2.plugin("i18n").service("locales").find();
401
+ const allLocalesDictionary = allLocales.reduce((acc, locale) => {
402
+ acc[locale.code] = { name: locale.name, code: locale.code };
403
+ return acc;
404
+ }, {});
405
+ const formattedData = actions.map((action) => {
406
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
407
+ return {
408
+ ...action,
409
+ entry: {
410
+ id: action.entry.id,
411
+ contentType: {
412
+ displayName,
413
+ mainFieldValue: action.entry[mainField]
414
+ },
415
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
416
+ status: action.entry.publishedAt ? "published" : "draft"
287
417
  }
288
- ]
289
- }).groupBy("content_type").execute();
290
- return contentTypesFromReleaseActions.map(({ contentType: contentTypeUid }) => contentTypeUid);
418
+ };
419
+ });
420
+ const groupName = getGroupName(groupBy);
421
+ return _.groupBy(groupName)(formattedData);
291
422
  },
292
- async getContentTypesDataForActions(releaseId) {
293
- const contentTypesUids = await this.getAllContentTypeUids(releaseId);
423
+ async getContentTypesDataForActions(contentTypesUids) {
294
424
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
295
425
  const contentTypesData = {};
296
426
  for (const contentTypeUid of contentTypesUids) {
@@ -395,13 +525,18 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
395
525
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
396
526
  where: {
397
527
  id: actionId,
398
- release: releaseId
528
+ release: {
529
+ id: releaseId,
530
+ releasedAt: {
531
+ $null: true
532
+ }
533
+ }
399
534
  },
400
535
  data: update
401
536
  });
402
537
  if (!updatedAction) {
403
538
  throw new errors.NotFoundError(
404
- `Action with id ${actionId} not found in release with id ${releaseId}`
539
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
405
540
  );
406
541
  }
407
542
  return updatedAction;
@@ -410,12 +545,17 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
410
545
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
411
546
  where: {
412
547
  id: actionId,
413
- release: releaseId
548
+ release: {
549
+ id: releaseId,
550
+ releasedAt: {
551
+ $null: true
552
+ }
553
+ }
414
554
  }
415
555
  });
416
556
  if (!deletedAction) {
417
557
  throw new errors.NotFoundError(
418
- `Action with id ${actionId} not found in release with id ${releaseId}`
558
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
419
559
  );
420
560
  }
421
561
  return deletedAction;
@@ -448,9 +588,48 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
448
588
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
449
589
  );
450
590
  }
591
+ },
592
+ async validatePendingReleasesLimit() {
593
+ const maximumPendingReleases = (
594
+ // @ts-expect-error - options is not typed into features
595
+ EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
596
+ );
597
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
598
+ filters: {
599
+ releasedAt: {
600
+ $null: true
601
+ }
602
+ }
603
+ });
604
+ if (pendingReleasesCount >= maximumPendingReleases) {
605
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
606
+ }
451
607
  }
452
608
  });
453
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
609
+ const createEventManagerService = () => {
610
+ const state = {
611
+ destroyListenerCallbacks: []
612
+ };
613
+ return {
614
+ addDestroyListenerCallback(destroyListenerCallback) {
615
+ state.destroyListenerCallbacks.push(destroyListenerCallback);
616
+ },
617
+ destroyAllListeners() {
618
+ if (!state.destroyListenerCallbacks.length) {
619
+ return;
620
+ }
621
+ state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
622
+ destroyListenerCallback();
623
+ });
624
+ }
625
+ };
626
+ };
627
+ const services = {
628
+ release: createReleaseService,
629
+ "release-action": createReleaseActionService,
630
+ "release-validation": createReleaseValidationService,
631
+ "event-manager": createEventManagerService
632
+ };
454
633
  const RELEASE_SCHEMA = yup.object().shape({
455
634
  name: yup.string().trim().required()
456
635
  }).required().noUnknown();
@@ -595,31 +774,13 @@ const releaseActionController = {
595
774
  });
596
775
  const query = await permissionsManager.sanitizeQuery(ctx.query);
597
776
  const releaseService = getService("release", { strapi });
598
- const { results, pagination } = await releaseService.findActions(releaseId, query);
599
- const allReleaseContentTypesDictionary = await releaseService.getContentTypesDataForActions(
600
- releaseId
601
- );
602
- const allLocales = await strapi.plugin("i18n").service("locales").find();
603
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
604
- acc[locale.code] = { name: locale.name, code: locale.code };
605
- return acc;
606
- }, {});
607
- const data = results.map((action) => {
608
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
609
- return {
610
- ...action,
611
- entry: {
612
- id: action.entry.id,
613
- contentType: {
614
- displayName,
615
- mainFieldValue: action.entry[mainField]
616
- },
617
- locale: allLocalesDictionary[action.entry.locale]
618
- }
619
- };
777
+ const { results, pagination } = await releaseService.findActions(releaseId, {
778
+ sort: query.groupBy === "action" ? "type" : query.groupBy,
779
+ ...query
620
780
  });
781
+ const groupedData = await releaseService.groupActions(results, query.groupBy);
621
782
  ctx.body = {
622
- data,
783
+ data: groupedData,
623
784
  meta: {
624
785
  pagination
625
786
  }
@@ -643,10 +804,8 @@ const releaseActionController = {
643
804
  async delete(ctx) {
644
805
  const actionId = ctx.params.actionId;
645
806
  const releaseId = ctx.params.releaseId;
646
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
647
- actionId,
648
- releaseId
649
- );
807
+ const releaseService = getService("release", { strapi });
808
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
650
809
  ctx.body = {
651
810
  data: deletedReleaseAction
652
811
  };
@@ -832,10 +991,16 @@ const getPlugin = () => {
832
991
  if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
833
992
  return {
834
993
  register,
994
+ bootstrap,
835
995
  contentTypes,
836
996
  services,
837
997
  controllers,
838
- routes
998
+ routes,
999
+ destroy() {
1000
+ if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1001
+ getService("event-manager").destroyAllListeners();
1002
+ }
1003
+ }
839
1004
  };
840
1005
  }
841
1006
  return {