@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,4 @@
1
- import { h } from "../_chunks/index-937f8179.mjs";
1
+ import { j } from "../_chunks/index-_Zsj8MUA.mjs";
2
2
  import "@strapi/helper-plugin";
3
3
  import "@strapi/icons";
4
4
  import "react/jsx-runtime";
@@ -14,6 +14,6 @@ import "yup";
14
14
  import "@reduxjs/toolkit/query/react";
15
15
  import "styled-components";
16
16
  export {
17
- h as default
17
+ j as default
18
18
  };
19
19
  //# sourceMappingURL=index.mjs.map
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const _ = require("lodash/fp");
4
+ const EE = require("@strapi/strapi/dist/utils/ee");
3
5
  const yup = require("yup");
6
+ const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
4
7
  function _interopNamespace(e) {
5
8
  if (e && e.__esModule)
6
9
  return e;
@@ -19,6 +22,8 @@ function _interopNamespace(e) {
19
22
  n.default = e;
20
23
  return Object.freeze(n);
21
24
  }
25
+ const ___default = /* @__PURE__ */ _interopDefault(_);
26
+ const EE__default = /* @__PURE__ */ _interopDefault(EE);
22
27
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
23
28
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
24
29
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -66,10 +71,80 @@ const ACTIONS = [
66
71
  pluginName: "content-releases"
67
72
  }
68
73
  ];
69
- 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");
70
78
  const register = async ({ strapi: strapi2 }) => {
71
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
79
+ if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
72
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") && strapi2.features.future.isEnabled("contentReleases")) {
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
+ });
73
148
  }
74
149
  };
75
150
  const schema$1 = {
@@ -142,6 +217,9 @@ const schema = {
142
217
  type: "string",
143
218
  required: true
144
219
  },
220
+ locale: {
221
+ type: "string"
222
+ },
145
223
  release: {
146
224
  type: "relation",
147
225
  relation: "manyToOne",
@@ -157,12 +235,31 @@ const contentTypes = {
157
235
  release: release$1,
158
236
  "release-action": releaseAction$1
159
237
  };
160
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
161
- return strapi2.plugin("content-releases").service(name);
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
+ });
247
+ const getGroupName = (queryValue) => {
248
+ switch (queryValue) {
249
+ case "contentType":
250
+ return "entry.contentType.displayName";
251
+ case "action":
252
+ return "type";
253
+ case "locale":
254
+ return ___default.default.getOr("No locale", "entry.locale.name");
255
+ default:
256
+ return "entry.contentType.displayName";
257
+ }
162
258
  };
163
259
  const createReleaseService = ({ strapi: strapi2 }) => ({
164
260
  async create(releaseData, { user }) {
165
261
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
262
+ await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
166
263
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
167
264
  data: releaseWithCreatorFields
168
265
  });
@@ -244,19 +341,23 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
244
341
  });
245
342
  },
246
343
  async update(id, releaseData, { user }) {
247
- const updatedRelease = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
248
- 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, {
249
353
  /*
250
354
  * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
251
355
  * is not compatible with the type we are passing here: UpdateRelease.Request['body']
252
356
  */
253
357
  // @ts-expect-error see above
254
- data: updatedRelease
358
+ data: releaseWithCreatorFields
255
359
  });
256
- if (!release2) {
257
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
258
- }
259
- return release2;
360
+ return updatedRelease;
260
361
  },
261
362
  async createAction(releaseId, action) {
262
363
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
@@ -266,11 +367,19 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
266
367
  validateEntryContentType(action.entry.contentType),
267
368
  validateUniqueEntry(releaseId, action)
268
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
+ }
269
377
  const { entry, type } = action;
270
378
  return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
271
379
  data: {
272
380
  type,
273
381
  contentType: entry.contentType,
382
+ locale: entry.locale,
274
383
  entry: {
275
384
  id: entry.id,
276
385
  __type: entry.contentType,
@@ -282,8 +391,10 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
282
391
  });
283
392
  },
284
393
  async findActions(releaseId, query) {
285
- const result = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
286
- if (!result) {
394
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
395
+ fields: ["id"]
396
+ });
397
+ if (!release2) {
287
398
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
288
399
  }
289
400
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
@@ -299,18 +410,40 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
299
410
  async countActions(query) {
300
411
  return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
301
412
  },
302
- async getAllContentTypeUids(releaseId) {
303
- const contentTypesFromReleaseActions = await strapi2.db.queryBuilder(RELEASE_ACTION_MODEL_UID).select("content_type").where({
304
- $and: [
305
- {
306
- release: releaseId
413
+ async groupActions(actions, groupBy) {
414
+ const contentTypeUids = actions.reduce((acc, action) => {
415
+ if (!acc.includes(action.contentType)) {
416
+ acc.push(action.contentType);
417
+ }
418
+ return acc;
419
+ }, []);
420
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
421
+ contentTypeUids
422
+ );
423
+ const allLocales = await strapi2.plugin("i18n").service("locales").find();
424
+ const allLocalesDictionary = allLocales.reduce((acc, locale) => {
425
+ acc[locale.code] = { name: locale.name, code: locale.code };
426
+ return acc;
427
+ }, {});
428
+ const formattedData = actions.map((action) => {
429
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
430
+ return {
431
+ ...action,
432
+ entry: {
433
+ id: action.entry.id,
434
+ contentType: {
435
+ displayName,
436
+ mainFieldValue: action.entry[mainField]
437
+ },
438
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
439
+ status: action.entry.publishedAt ? "published" : "draft"
307
440
  }
308
- ]
309
- }).groupBy("content_type").execute();
310
- return contentTypesFromReleaseActions.map(({ contentType: contentTypeUid }) => contentTypeUid);
441
+ };
442
+ });
443
+ const groupName = getGroupName(groupBy);
444
+ return ___default.default.groupBy(groupName)(formattedData);
311
445
  },
312
- async getContentTypesDataForActions(releaseId) {
313
- const contentTypesUids = await this.getAllContentTypeUids(releaseId);
446
+ async getContentTypesDataForActions(contentTypesUids) {
314
447
  const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
315
448
  const contentTypesData = {};
316
449
  for (const contentTypeUid of contentTypesUids) {
@@ -415,13 +548,18 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
415
548
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
416
549
  where: {
417
550
  id: actionId,
418
- release: releaseId
551
+ release: {
552
+ id: releaseId,
553
+ releasedAt: {
554
+ $null: true
555
+ }
556
+ }
419
557
  },
420
558
  data: update
421
559
  });
422
560
  if (!updatedAction) {
423
561
  throw new utils.errors.NotFoundError(
424
- `Action with id ${actionId} not found in release with id ${releaseId}`
562
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
425
563
  );
426
564
  }
427
565
  return updatedAction;
@@ -430,12 +568,17 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
430
568
  const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
431
569
  where: {
432
570
  id: actionId,
433
- release: releaseId
571
+ release: {
572
+ id: releaseId,
573
+ releasedAt: {
574
+ $null: true
575
+ }
576
+ }
434
577
  }
435
578
  });
436
579
  if (!deletedAction) {
437
580
  throw new utils.errors.NotFoundError(
438
- `Action with id ${actionId} not found in release with id ${releaseId}`
581
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
439
582
  );
440
583
  }
441
584
  return deletedAction;
@@ -468,9 +611,48 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
468
611
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
469
612
  );
470
613
  }
614
+ },
615
+ async validatePendingReleasesLimit() {
616
+ const maximumPendingReleases = (
617
+ // @ts-expect-error - options is not typed into features
618
+ EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
619
+ );
620
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
621
+ filters: {
622
+ releasedAt: {
623
+ $null: true
624
+ }
625
+ }
626
+ });
627
+ if (pendingReleasesCount >= maximumPendingReleases) {
628
+ throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
629
+ }
471
630
  }
472
631
  });
473
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
632
+ const createEventManagerService = () => {
633
+ const state = {
634
+ destroyListenerCallbacks: []
635
+ };
636
+ return {
637
+ addDestroyListenerCallback(destroyListenerCallback) {
638
+ state.destroyListenerCallbacks.push(destroyListenerCallback);
639
+ },
640
+ destroyAllListeners() {
641
+ if (!state.destroyListenerCallbacks.length) {
642
+ return;
643
+ }
644
+ state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
645
+ destroyListenerCallback();
646
+ });
647
+ }
648
+ };
649
+ };
650
+ const services = {
651
+ release: createReleaseService,
652
+ "release-action": createReleaseActionService,
653
+ "release-validation": createReleaseValidationService,
654
+ "event-manager": createEventManagerService
655
+ };
474
656
  const RELEASE_SCHEMA = yup__namespace.object().shape({
475
657
  name: yup__namespace.string().trim().required()
476
658
  }).required().noUnknown();
@@ -615,31 +797,13 @@ const releaseActionController = {
615
797
  });
616
798
  const query = await permissionsManager.sanitizeQuery(ctx.query);
617
799
  const releaseService = getService("release", { strapi });
618
- const { results, pagination } = await releaseService.findActions(releaseId, query);
619
- const allReleaseContentTypesDictionary = await releaseService.getContentTypesDataForActions(
620
- releaseId
621
- );
622
- const allLocales = await strapi.plugin("i18n").service("locales").find();
623
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
624
- acc[locale.code] = { name: locale.name, code: locale.code };
625
- return acc;
626
- }, {});
627
- const data = results.map((action) => {
628
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
629
- return {
630
- ...action,
631
- entry: {
632
- id: action.entry.id,
633
- contentType: {
634
- displayName,
635
- mainFieldValue: action.entry[mainField]
636
- },
637
- locale: allLocalesDictionary[action.entry.locale]
638
- }
639
- };
800
+ const { results, pagination } = await releaseService.findActions(releaseId, {
801
+ sort: query.groupBy === "action" ? "type" : query.groupBy,
802
+ ...query
640
803
  });
804
+ const groupedData = await releaseService.groupActions(results, query.groupBy);
641
805
  ctx.body = {
642
- data,
806
+ data: groupedData,
643
807
  meta: {
644
808
  pagination
645
809
  }
@@ -663,10 +827,8 @@ const releaseActionController = {
663
827
  async delete(ctx) {
664
828
  const actionId = ctx.params.actionId;
665
829
  const releaseId = ctx.params.releaseId;
666
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
667
- actionId,
668
- releaseId
669
- );
830
+ const releaseService = getService("release", { strapi });
831
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
670
832
  ctx.body = {
671
833
  data: deletedReleaseAction
672
834
  };
@@ -852,10 +1014,16 @@ const getPlugin = () => {
852
1014
  if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
853
1015
  return {
854
1016
  register,
1017
+ bootstrap,
855
1018
  contentTypes,
856
1019
  services,
857
1020
  controllers,
858
- routes
1021
+ routes,
1022
+ destroy() {
1023
+ if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1024
+ getService("event-manager").destroyAllListeners();
1025
+ }
1026
+ }
859
1027
  };
860
1028
  }
861
1029
  return {