@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,5 +1,6 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const lodash = require("lodash");
3
4
  const _ = require("lodash/fp");
4
5
  const EE = require("@strapi/strapi/dist/utils/ee");
5
6
  const yup = require("yup");
@@ -71,40 +72,47 @@ const ACTIONS = [
71
72
  pluginName: "content-releases"
72
73
  }
73
74
  ];
74
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
75
- return strapi2.plugin("content-releases").service(name);
76
- };
75
+ async function deleteActionsOnDisableDraftAndPublish({
76
+ oldContentTypes,
77
+ contentTypes: contentTypes2
78
+ }) {
79
+ if (!oldContentTypes) {
80
+ return;
81
+ }
82
+ for (const uid in contentTypes2) {
83
+ if (!oldContentTypes[uid]) {
84
+ continue;
85
+ }
86
+ const oldContentType = oldContentTypes[uid];
87
+ const contentType = contentTypes2[uid];
88
+ if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
89
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
90
+ }
91
+ }
92
+ }
93
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
94
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
95
+ if (deletedContentTypes.length) {
96
+ await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
97
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
98
+ });
99
+ }
100
+ }
77
101
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
78
102
  const register = async ({ strapi: strapi2 }) => {
79
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
103
+ if (features$2.isEnabled("cms-content-releases")) {
80
104
  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);
105
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
106
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
99
107
  }
100
108
  };
101
109
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
102
110
  const bootstrap = async ({ strapi: strapi2 }) => {
103
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
111
+ if (features$1.isEnabled("cms-content-releases")) {
104
112
  strapi2.db.lifecycles.subscribe({
105
113
  afterDelete(event) {
106
114
  const { model, result } = event;
107
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
115
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
108
116
  const { id } = result;
109
117
  strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
110
118
  where: {
@@ -120,7 +128,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
120
128
  */
121
129
  async beforeDeleteMany(event) {
122
130
  const { model, params } = event;
123
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
131
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
124
132
  const { where } = params;
125
133
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
126
134
  event.state.entriesToDelete = entriesToDelete;
@@ -235,31 +243,32 @@ const contentTypes = {
235
243
  release: release$1,
236
244
  "release-action": releaseAction$1
237
245
  };
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
- });
246
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
+ return strapi2.plugin("content-releases").service(name);
248
+ };
247
249
  const getGroupName = (queryValue) => {
248
250
  switch (queryValue) {
249
251
  case "contentType":
250
- return "entry.contentType.displayName";
252
+ return "contentType.displayName";
251
253
  case "action":
252
254
  return "type";
253
255
  case "locale":
254
- return ___default.default.getOr("No locale", "entry.locale.name");
256
+ return ___default.default.getOr("No locale", "locale.name");
255
257
  default:
256
- return "entry.contentType.displayName";
258
+ return "contentType.displayName";
257
259
  }
258
260
  };
259
261
  const createReleaseService = ({ strapi: strapi2 }) => ({
260
262
  async create(releaseData, { user }) {
261
263
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
262
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
264
+ const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
+ "release-validation",
266
+ { strapi: strapi2 }
267
+ );
268
+ await Promise.all([
269
+ validatePendingReleasesLimit(),
270
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
271
+ ]);
263
272
  return strapi2.entityService.create(RELEASE_MODEL_UID, {
264
273
  data: releaseWithCreatorFields
265
274
  });
@@ -281,51 +290,66 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
281
290
  }
282
291
  });
283
292
  },
284
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
285
- hasEntryAttached
286
- } = {
287
- hasEntryAttached: false
288
- }) {
289
- const whereActions = hasEntryAttached ? {
290
- // Find all Releases where the content type entry is present
291
- actions: {
292
- target_type: contentTypeUid,
293
- target_id: entryId
294
- }
295
- } : {
296
- // Find all Releases where the content type entry is not present
297
- $or: [
298
- {
299
- $not: {
300
- actions: {
301
- target_type: contentTypeUid,
302
- target_id: entryId
303
- }
304
- }
293
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
294
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
295
+ where: {
296
+ actions: {
297
+ target_type: contentTypeUid,
298
+ target_id: entryId
305
299
  },
306
- {
307
- actions: null
300
+ releasedAt: {
301
+ $null: true
308
302
  }
309
- ]
310
- };
311
- const populateAttachedAction = hasEntryAttached ? {
312
- // Filter the action to get only the content type entry
313
- actions: {
314
- where: {
303
+ },
304
+ populate: {
305
+ // Filter the action to get only the content type entry
306
+ actions: {
307
+ where: {
308
+ target_type: contentTypeUid,
309
+ target_id: entryId
310
+ }
311
+ }
312
+ }
313
+ });
314
+ return releases.map((release2) => {
315
+ if (release2.actions?.length) {
316
+ const [actionForEntry] = release2.actions;
317
+ delete release2.actions;
318
+ return {
319
+ ...release2,
320
+ action: actionForEntry
321
+ };
322
+ }
323
+ return release2;
324
+ });
325
+ },
326
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
327
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
328
+ where: {
329
+ releasedAt: {
330
+ $null: true
331
+ },
332
+ actions: {
315
333
  target_type: contentTypeUid,
316
334
  target_id: entryId
317
335
  }
318
336
  }
319
- } : {};
337
+ });
320
338
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
321
339
  where: {
322
- ...whereActions,
340
+ $or: [
341
+ {
342
+ id: {
343
+ $notIn: releasesRelated.map((release2) => release2.id)
344
+ }
345
+ },
346
+ {
347
+ actions: null
348
+ }
349
+ ],
323
350
  releasedAt: {
324
351
  $null: true
325
352
  }
326
- },
327
- populate: {
328
- ...populateAttachedAction
329
353
  }
330
354
  });
331
355
  return releases.map((release2) => {
@@ -400,7 +424,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
400
424
  return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
401
425
  ...query,
402
426
  populate: {
403
- entry: true
427
+ entry: {
428
+ populate: "*"
429
+ }
404
430
  },
405
431
  filters: {
406
432
  release: releaseId
@@ -425,14 +451,11 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
425
451
  const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
426
452
  return {
427
453
  ...action,
428
- entry: {
429
- id: action.entry.id,
430
- contentType: {
431
- displayName,
432
- mainFieldValue: action.entry[mainField]
433
- },
434
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
435
- status: action.entry.publishedAt ? "published" : "draft"
454
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
455
+ contentType: {
456
+ displayName,
457
+ mainFieldValue: action.entry[mainField],
458
+ uid: action.contentType
436
459
  }
437
460
  };
438
461
  });
@@ -463,6 +486,34 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
463
486
  }
464
487
  return contentTypesData;
465
488
  },
489
+ getContentTypeModelsFromActions(actions) {
490
+ const contentTypeUids = actions.reduce((acc, action) => {
491
+ if (!acc.includes(action.contentType)) {
492
+ acc.push(action.contentType);
493
+ }
494
+ return acc;
495
+ }, []);
496
+ const contentTypeModelsMap = contentTypeUids.reduce(
497
+ (acc, contentTypeUid) => {
498
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
499
+ return acc;
500
+ },
501
+ {}
502
+ );
503
+ return contentTypeModelsMap;
504
+ },
505
+ async getAllComponents() {
506
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
507
+ const components = await contentManagerComponentsService.findAllComponents();
508
+ const componentsMap = components.reduce(
509
+ (acc, component) => {
510
+ acc[component.uid] = component;
511
+ return acc;
512
+ },
513
+ {}
514
+ );
515
+ return componentsMap;
516
+ },
466
517
  async delete(releaseId) {
467
518
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
468
519
  populate: {
@@ -497,7 +548,9 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
497
548
  populate: {
498
549
  actions: {
499
550
  populate: {
500
- entry: true
551
+ entry: {
552
+ fields: ["id"]
553
+ }
501
554
  }
502
555
  }
503
556
  }
@@ -517,25 +570,49 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
517
570
  const contentTypeUid = action.contentType;
518
571
  if (!actions[contentTypeUid]) {
519
572
  actions[contentTypeUid] = {
520
- publish: [],
521
- unpublish: []
573
+ entriestoPublishIds: [],
574
+ entriesToUnpublishIds: []
522
575
  };
523
576
  }
524
577
  if (action.type === "publish") {
525
- actions[contentTypeUid].publish.push(action.entry);
578
+ actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
526
579
  } else {
527
- actions[contentTypeUid].unpublish.push(action.entry);
580
+ actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
528
581
  }
529
582
  }
530
583
  const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
531
585
  await strapi2.db.transaction(async () => {
532
586
  for (const contentTypeUid of Object.keys(actions)) {
533
- const { publish, unpublish } = actions[contentTypeUid];
534
- if (publish.length > 0) {
535
- await entityManagerService.publishMany(publish, contentTypeUid);
587
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
+ const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
589
+ const entriesToPublish = await strapi2.entityService.findMany(
590
+ contentTypeUid,
591
+ {
592
+ filters: {
593
+ id: {
594
+ $in: entriestoPublishIds
595
+ }
596
+ },
597
+ populate
598
+ }
599
+ );
600
+ const entriesToUnpublish = await strapi2.entityService.findMany(
601
+ contentTypeUid,
602
+ {
603
+ filters: {
604
+ id: {
605
+ $in: entriesToUnpublishIds
606
+ }
607
+ },
608
+ populate
609
+ }
610
+ );
611
+ if (entriesToPublish.length > 0) {
612
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
536
613
  }
537
- if (unpublish.length > 0) {
538
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
614
+ if (entriesToUnpublish.length > 0) {
615
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
539
616
  }
540
617
  }
541
618
  });
@@ -633,31 +710,25 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
633
710
  if (pendingReleasesCount >= maximumPendingReleases) {
634
711
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
635
712
  }
636
- }
637
- });
638
- const createEventManagerService = () => {
639
- const state = {
640
- destroyListenerCallbacks: []
641
- };
642
- return {
643
- addDestroyListenerCallback(destroyListenerCallback) {
644
- state.destroyListenerCallbacks.push(destroyListenerCallback);
645
- },
646
- destroyAllListeners() {
647
- if (!state.destroyListenerCallbacks.length) {
648
- return;
713
+ },
714
+ async validateUniqueNameForPendingRelease(name) {
715
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
+ filters: {
717
+ releasedAt: {
718
+ $null: true
719
+ },
720
+ name
649
721
  }
650
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
651
- destroyListenerCallback();
652
- });
722
+ });
723
+ const isNameUnique = pendingReleases.length === 0;
724
+ if (!isNameUnique) {
725
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
653
726
  }
654
- };
655
- };
727
+ }
728
+ });
656
729
  const services = {
657
730
  release: createReleaseService,
658
- "release-action": createReleaseActionService,
659
- "release-validation": createReleaseValidationService,
660
- "event-manager": createEventManagerService
731
+ "release-validation": createReleaseValidationService
661
732
  };
662
733
  const RELEASE_SCHEMA = yup__namespace.object().shape({
663
734
  name: yup__namespace.string().trim().required()
@@ -677,9 +748,7 @@ const releaseController = {
677
748
  const contentTypeUid = query.contentTypeUid;
678
749
  const entryId = query.entryId;
679
750
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
680
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
681
- hasEntryAttached
682
- });
751
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
683
752
  ctx.body = { data };
684
753
  } else {
685
754
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -807,11 +876,30 @@ const releaseActionController = {
807
876
  sort: query.groupBy === "action" ? "type" : query.groupBy,
808
877
  ...query
809
878
  });
810
- const groupedData = await releaseService.groupActions(results, query.groupBy);
879
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
880
+ if (acc[action.contentType]) {
881
+ return acc;
882
+ }
883
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
884
+ ability: ctx.state.userAbility,
885
+ model: action.contentType
886
+ });
887
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
888
+ return acc;
889
+ }, {});
890
+ const sanitizedResults = await utils.mapAsync(results, async (action) => ({
891
+ ...action,
892
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
893
+ }));
894
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
895
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
896
+ const components = await releaseService.getAllComponents();
811
897
  ctx.body = {
812
898
  data: groupedData,
813
899
  meta: {
814
- pagination
900
+ pagination,
901
+ contentTypes: contentTypes2,
902
+ components
815
903
  }
816
904
  };
817
905
  },
@@ -1017,19 +1105,14 @@ const routes = {
1017
1105
  };
1018
1106
  const { features } = require("@strapi/strapi/dist/utils/ee");
1019
1107
  const getPlugin = () => {
1020
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1108
+ if (features.isEnabled("cms-content-releases")) {
1021
1109
  return {
1022
1110
  register,
1023
1111
  bootstrap,
1024
1112
  contentTypes,
1025
1113
  services,
1026
1114
  controllers,
1027
- routes,
1028
- destroy() {
1029
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1030
- getService("event-manager").destroyAllListeners();
1031
- }
1032
- }
1115
+ routes
1033
1116
  };
1034
1117
  }
1035
1118
  return {