@strapi/content-releases 0.0.0-experimental.b5b7b8260a4549f3bd7443fbd68be5ccc9857cd7 → 0.0.0-experimental.cae3a5a17d131a6f59673b62d01cfac869ea9cc2

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,5 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import { difference, keys } from "lodash";
2
3
  import _ from "lodash/fp";
3
4
  import EE from "@strapi/strapi/dist/utils/ee";
4
5
  import * as yup from "yup";
@@ -48,40 +49,47 @@ const ACTIONS = [
48
49
  pluginName: "content-releases"
49
50
  }
50
51
  ];
51
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
52
- return strapi2.plugin("content-releases").service(name);
53
- };
52
+ async function deleteActionsOnDisableDraftAndPublish({
53
+ oldContentTypes,
54
+ contentTypes: contentTypes2
55
+ }) {
56
+ if (!oldContentTypes) {
57
+ return;
58
+ }
59
+ for (const uid in contentTypes2) {
60
+ if (!oldContentTypes[uid]) {
61
+ continue;
62
+ }
63
+ const oldContentType = oldContentTypes[uid];
64
+ const contentType = contentTypes2[uid];
65
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
66
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
67
+ }
68
+ }
69
+ }
70
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
71
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
72
+ if (deletedContentTypes.length) {
73
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
74
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
75
+ });
76
+ }
77
+ }
54
78
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
55
79
  const register = async ({ strapi: strapi2 }) => {
56
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
80
+ if (features$2.isEnabled("cms-content-releases")) {
57
81
  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);
82
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
83
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
76
84
  }
77
85
  };
78
86
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
79
87
  const bootstrap = async ({ strapi: strapi2 }) => {
80
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
88
+ if (features$1.isEnabled("cms-content-releases")) {
81
89
  strapi2.db.lifecycles.subscribe({
82
90
  afterDelete(event) {
83
91
  const { model, result } = event;
84
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
92
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
85
93
  const { id } = result;
86
94
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
87
95
  where: {
@@ -97,7 +105,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
97
105
  */
98
106
  async beforeDeleteMany(event) {
99
107
  const { model, params } = event;
100
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
108
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
101
109
  const { where } = params;
102
110
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
103
111
  event.state.entriesToDelete = entriesToDelete;
@@ -212,31 +220,32 @@ const contentTypes = {
212
220
  release: release$1,
213
221
  "release-action": releaseAction$1
214
222
  };
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
- });
223
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
+ return strapi2.plugin("content-releases").service(name);
225
+ };
224
226
  const getGroupName = (queryValue) => {
225
227
  switch (queryValue) {
226
228
  case "contentType":
227
- return "entry.contentType.displayName";
229
+ return "contentType.displayName";
228
230
  case "action":
229
231
  return "type";
230
232
  case "locale":
231
- return _.getOr("No locale", "entry.locale.name");
233
+ return _.getOr("No locale", "locale.name");
232
234
  default:
233
- return "entry.contentType.displayName";
235
+ return "contentType.displayName";
234
236
  }
235
237
  };
236
238
  const createReleaseService = ({ strapi: strapi2 }) => ({
237
239
  async create(releaseData, { user }) {
238
240
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
239
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
241
+ const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
+ "release-validation",
243
+ { strapi: strapi2 }
244
+ );
245
+ await Promise.all([
246
+ validatePendingReleasesLimit(),
247
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
248
+ ]);
240
249
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
241
250
  data: releaseWithCreatorFields
242
251
  });
@@ -258,51 +267,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
258
267
  }
259
268
  });
260
269
  },
261
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
262
- hasEntryAttached
263
- } = {
264
- hasEntryAttached: false
265
- }) {
266
- const whereActions = hasEntryAttached ? {
267
- // Find all Releases where the content type entry is present
268
- actions: {
269
- target_type: contentTypeUid,
270
- target_id: entryId
271
- }
272
- } : {
273
- // Find all Releases where the content type entry is not present
274
- $or: [
275
- {
276
- $not: {
277
- actions: {
278
- target_type: contentTypeUid,
279
- target_id: entryId
280
- }
281
- }
270
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
271
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
272
+ where: {
273
+ actions: {
274
+ target_type: contentTypeUid,
275
+ target_id: entryId
282
276
  },
283
- {
284
- actions: null
277
+ releasedAt: {
278
+ $null: true
285
279
  }
286
- ]
287
- };
288
- const populateAttachedAction = hasEntryAttached ? {
289
- // Filter the action to get only the content type entry
290
- actions: {
291
- where: {
280
+ },
281
+ populate: {
282
+ // Filter the action to get only the content type entry
283
+ actions: {
284
+ where: {
285
+ target_type: contentTypeUid,
286
+ target_id: entryId
287
+ }
288
+ }
289
+ }
290
+ });
291
+ return releases.map((release2) => {
292
+ if (release2.actions?.length) {
293
+ const [actionForEntry] = release2.actions;
294
+ delete release2.actions;
295
+ return {
296
+ ...release2,
297
+ action: actionForEntry
298
+ };
299
+ }
300
+ return release2;
301
+ });
302
+ },
303
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
304
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
+ where: {
306
+ releasedAt: {
307
+ $null: true
308
+ },
309
+ actions: {
292
310
  target_type: contentTypeUid,
293
311
  target_id: entryId
294
312
  }
295
313
  }
296
- } : {};
314
+ });
297
315
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
298
316
  where: {
299
- ...whereActions,
317
+ $or: [
318
+ {
319
+ id: {
320
+ $notIn: releasesRelated.map((release2) => release2.id)
321
+ }
322
+ },
323
+ {
324
+ actions: null
325
+ }
326
+ ],
300
327
  releasedAt: {
301
328
  $null: true
302
329
  }
303
- },
304
- populate: {
305
- ...populateAttachedAction
306
330
  }
307
331
  });
308
332
  return releases.map((release2) => {
@@ -377,7 +401,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
377
401
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
378
402
  ...query,
379
403
  populate: {
380
- entry: true
404
+ entry: {
405
+ populate: "*"
406
+ }
381
407
  },
382
408
  filters: {
383
409
  release: releaseId
@@ -402,14 +428,11 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
402
428
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
403
429
  return {
404
430
  ...action,
405
- entry: {
406
- id: action.entry.id,
407
- contentType: {
408
- displayName,
409
- mainFieldValue: action.entry[mainField]
410
- },
411
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
412
- status: action.entry.publishedAt ? "published" : "draft"
431
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
432
+ contentType: {
433
+ displayName,
434
+ mainFieldValue: action.entry[mainField],
435
+ uid: action.contentType
413
436
  }
414
437
  };
415
438
  });
@@ -440,6 +463,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
440
463
  }
441
464
  return contentTypesData;
442
465
  },
466
+ getContentTypeModelsFromActions(actions) {
467
+ const contentTypeUids = actions.reduce((acc, action) => {
468
+ if (!acc.includes(action.contentType)) {
469
+ acc.push(action.contentType);
470
+ }
471
+ return acc;
472
+ }, []);
473
+ const contentTypeModelsMap = contentTypeUids.reduce(
474
+ (acc, contentTypeUid) => {
475
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
476
+ return acc;
477
+ },
478
+ {}
479
+ );
480
+ return contentTypeModelsMap;
481
+ },
482
+ async getAllComponents() {
483
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
484
+ const components = await contentManagerComponentsService.findAllComponents();
485
+ const componentsMap = components.reduce(
486
+ (acc, component) => {
487
+ acc[component.uid] = component;
488
+ return acc;
489
+ },
490
+ {}
491
+ );
492
+ return componentsMap;
493
+ },
443
494
  async delete(releaseId) {
444
495
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
445
496
  populate: {
@@ -474,7 +525,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
474
525
  populate: {
475
526
  actions: {
476
527
  populate: {
477
- entry: true
528
+ entry: {
529
+ fields: ["id"]
530
+ }
478
531
  }
479
532
  }
480
533
  }
@@ -494,25 +547,49 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
494
547
  const contentTypeUid = action.contentType;
495
548
  if (!actions[contentTypeUid]) {
496
549
  actions[contentTypeUid] = {
497
- publish: [],
498
- unpublish: []
550
+ entriestoPublishIds: [],
551
+ entriesToUnpublishIds: []
499
552
  };
500
553
  }
501
554
  if (action.type === "publish") {
502
- actions[contentTypeUid].publish.push(action.entry);
555
+ actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
503
556
  } else {
504
- actions[contentTypeUid].unpublish.push(action.entry);
557
+ actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
505
558
  }
506
559
  }
507
560
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
508
562
  await strapi2.db.transaction(async () => {
509
563
  for (const contentTypeUid of Object.keys(actions)) {
510
- const { publish, unpublish } = actions[contentTypeUid];
511
- if (publish.length > 0) {
512
- await entityManagerService.publishMany(publish, contentTypeUid);
564
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
+ const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
566
+ const entriesToPublish = await strapi2.entityService.findMany(
567
+ contentTypeUid,
568
+ {
569
+ filters: {
570
+ id: {
571
+ $in: entriestoPublishIds
572
+ }
573
+ },
574
+ populate
575
+ }
576
+ );
577
+ const entriesToUnpublish = await strapi2.entityService.findMany(
578
+ contentTypeUid,
579
+ {
580
+ filters: {
581
+ id: {
582
+ $in: entriesToUnpublishIds
583
+ }
584
+ },
585
+ populate
586
+ }
587
+ );
588
+ if (entriesToPublish.length > 0) {
589
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
513
590
  }
514
- if (unpublish.length > 0) {
515
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
591
+ if (entriesToUnpublish.length > 0) {
592
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
516
593
  }
517
594
  }
518
595
  });
@@ -610,31 +687,25 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
610
687
  if (pendingReleasesCount >= maximumPendingReleases) {
611
688
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
612
689
  }
613
- }
614
- });
615
- const createEventManagerService = () => {
616
- const state = {
617
- destroyListenerCallbacks: []
618
- };
619
- return {
620
- addDestroyListenerCallback(destroyListenerCallback) {
621
- state.destroyListenerCallbacks.push(destroyListenerCallback);
622
- },
623
- destroyAllListeners() {
624
- if (!state.destroyListenerCallbacks.length) {
625
- return;
690
+ },
691
+ async validateUniqueNameForPendingRelease(name) {
692
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
+ filters: {
694
+ releasedAt: {
695
+ $null: true
696
+ },
697
+ name
626
698
  }
627
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
628
- destroyListenerCallback();
629
- });
699
+ });
700
+ const isNameUnique = pendingReleases.length === 0;
701
+ if (!isNameUnique) {
702
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
630
703
  }
631
- };
632
- };
704
+ }
705
+ });
633
706
  const services = {
634
707
  release: createReleaseService,
635
- "release-action": createReleaseActionService,
636
- "release-validation": createReleaseValidationService,
637
- "event-manager": createEventManagerService
708
+ "release-validation": createReleaseValidationService
638
709
  };
639
710
  const RELEASE_SCHEMA = yup.object().shape({
640
711
  name: yup.string().trim().required()
@@ -654,9 +725,7 @@ const releaseController = {
654
725
  const contentTypeUid = query.contentTypeUid;
655
726
  const entryId = query.entryId;
656
727
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
657
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
658
- hasEntryAttached
659
- });
728
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
660
729
  ctx.body = { data };
661
730
  } else {
662
731
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -784,11 +853,30 @@ const releaseActionController = {
784
853
  sort: query.groupBy === "action" ? "type" : query.groupBy,
785
854
  ...query
786
855
  });
787
- const groupedData = await releaseService.groupActions(results, query.groupBy);
856
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
857
+ if (acc[action.contentType]) {
858
+ return acc;
859
+ }
860
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
861
+ ability: ctx.state.userAbility,
862
+ model: action.contentType
863
+ });
864
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
865
+ return acc;
866
+ }, {});
867
+ const sanitizedResults = await mapAsync(results, async (action) => ({
868
+ ...action,
869
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
870
+ }));
871
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
872
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
873
+ const components = await releaseService.getAllComponents();
788
874
  ctx.body = {
789
875
  data: groupedData,
790
876
  meta: {
791
- pagination
877
+ pagination,
878
+ contentTypes: contentTypes2,
879
+ components
792
880
  }
793
881
  };
794
882
  },
@@ -994,19 +1082,14 @@ const routes = {
994
1082
  };
995
1083
  const { features } = require("@strapi/strapi/dist/utils/ee");
996
1084
  const getPlugin = () => {
997
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1085
+ if (features.isEnabled("cms-content-releases")) {
998
1086
  return {
999
1087
  register,
1000
1088
  bootstrap,
1001
1089
  contentTypes,
1002
1090
  services,
1003
1091
  controllers,
1004
- routes,
1005
- destroy() {
1006
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1007
- getService("event-manager").destroyAllListeners();
1008
- }
1009
- }
1092
+ routes
1010
1093
  };
1011
1094
  }
1012
1095
  return {