@strapi/content-releases 4.20.3 → 4.20.4

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
1
  import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import isEqual from "lodash/isEqual";
2
3
  import { difference, keys } from "lodash";
3
4
  import _ from "lodash/fp";
4
5
  import EE from "@strapi/strapi/dist/utils/ee";
@@ -53,6 +54,29 @@ const ACTIONS = [
53
54
  const ALLOWED_WEBHOOK_EVENTS = {
54
55
  RELEASES_PUBLISH: "releases.publish"
55
56
  };
57
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
58
+ return strapi2.plugin("content-releases").service(name);
59
+ };
60
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
61
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
62
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
63
+ const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
64
+ return entry;
65
+ };
66
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
67
+ try {
68
+ await strapi2.entityValidator.validateEntityCreation(
69
+ strapi2.getModel(contentTypeUid),
70
+ entry,
71
+ void 0,
72
+ // @ts-expect-error - FIXME: entity here is unnecessary
73
+ entry
74
+ );
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
56
80
  async function deleteActionsOnDisableDraftAndPublish({
57
81
  oldContentTypes,
58
82
  contentTypes: contentTypes2
@@ -79,31 +103,151 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
79
103
  });
80
104
  }
81
105
  }
106
+ async function migrateIsValidAndStatusReleases() {
107
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
108
+ where: {
109
+ status: null,
110
+ releasedAt: null
111
+ },
112
+ populate: {
113
+ actions: {
114
+ populate: {
115
+ entry: true
116
+ }
117
+ }
118
+ }
119
+ });
120
+ mapAsync(releasesWithoutStatus, async (release2) => {
121
+ const actions = release2.actions;
122
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
123
+ for (const action of notValidatedActions) {
124
+ if (action.entry) {
125
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
126
+ strapi
127
+ });
128
+ if (populatedEntry) {
129
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
130
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
131
+ where: {
132
+ id: action.id
133
+ },
134
+ data: {
135
+ isEntryValid
136
+ }
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
142
+ });
143
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
144
+ where: {
145
+ status: null,
146
+ releasedAt: {
147
+ $notNull: true
148
+ }
149
+ }
150
+ });
151
+ mapAsync(publishedReleases, async (release2) => {
152
+ return strapi.db.query(RELEASE_MODEL_UID).update({
153
+ where: {
154
+ id: release2.id
155
+ },
156
+ data: {
157
+ status: "done"
158
+ }
159
+ });
160
+ });
161
+ }
162
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
163
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
164
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
165
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
166
+ );
167
+ const releasesAffected = /* @__PURE__ */ new Set();
168
+ mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
169
+ const oldContentType = oldContentTypes[contentTypeUID];
170
+ const contentType = contentTypes2[contentTypeUID];
171
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
172
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
173
+ where: {
174
+ contentType: contentTypeUID
175
+ },
176
+ populate: {
177
+ entry: true,
178
+ release: true
179
+ }
180
+ });
181
+ await mapAsync(actions, async (action) => {
182
+ if (action.entry) {
183
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
+ strapi
185
+ });
186
+ if (populatedEntry) {
187
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
188
+ strapi
189
+ });
190
+ releasesAffected.add(action.release.id);
191
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
192
+ where: {
193
+ id: action.id
194
+ },
195
+ data: {
196
+ isEntryValid
197
+ }
198
+ });
199
+ }
200
+ }
201
+ });
202
+ }
203
+ }).then(() => {
204
+ mapAsync(releasesAffected, async (releaseId) => {
205
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
206
+ });
207
+ });
208
+ }
209
+ }
82
210
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
83
211
  const register = async ({ strapi: strapi2 }) => {
84
212
  if (features$2.isEnabled("cms-content-releases")) {
85
213
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
86
214
  strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
87
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
215
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
88
216
  }
89
217
  };
90
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
- return strapi2.plugin("content-releases").service(name);
92
- };
93
218
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
94
219
  const bootstrap = async ({ strapi: strapi2 }) => {
95
220
  if (features$1.isEnabled("cms-content-releases")) {
221
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
222
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
223
+ );
96
224
  strapi2.db.lifecycles.subscribe({
97
- afterDelete(event) {
98
- const { model, result } = event;
99
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
100
- const { id } = result;
101
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
102
- where: {
103
- target_type: model.uid,
104
- target_id: id
225
+ models: contentTypesWithDraftAndPublish,
226
+ async afterDelete(event) {
227
+ try {
228
+ const { model, result } = event;
229
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
230
+ const { id } = result;
231
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
232
+ where: {
233
+ actions: {
234
+ target_type: model.uid,
235
+ target_id: id
236
+ }
237
+ }
238
+ });
239
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
240
+ where: {
241
+ target_type: model.uid,
242
+ target_id: id
243
+ }
244
+ });
245
+ for (const release2 of releases) {
246
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
105
247
  }
106
- });
248
+ }
249
+ } catch (error) {
250
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
107
251
  }
108
252
  },
109
253
  /**
@@ -123,18 +267,75 @@ const bootstrap = async ({ strapi: strapi2 }) => {
123
267
  * We make this only after deleteMany is succesfully executed to avoid errors
124
268
  */
125
269
  async afterDeleteMany(event) {
126
- const { model, state } = event;
127
- const entriesToDelete = state.entriesToDelete;
128
- if (entriesToDelete) {
129
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
130
- where: {
131
- target_type: model.uid,
132
- target_id: {
133
- $in: entriesToDelete.map((entry) => entry.id)
270
+ try {
271
+ const { model, state } = event;
272
+ const entriesToDelete = state.entriesToDelete;
273
+ if (entriesToDelete) {
274
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
275
+ where: {
276
+ actions: {
277
+ target_type: model.uid,
278
+ target_id: {
279
+ $in: entriesToDelete.map(
280
+ (entry) => entry.id
281
+ )
282
+ }
283
+ }
134
284
  }
285
+ });
286
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
287
+ where: {
288
+ target_type: model.uid,
289
+ target_id: {
290
+ $in: entriesToDelete.map((entry) => entry.id)
291
+ }
292
+ }
293
+ });
294
+ for (const release2 of releases) {
295
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
135
296
  }
297
+ }
298
+ } catch (error) {
299
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
300
+ error
136
301
  });
137
302
  }
303
+ },
304
+ async afterUpdate(event) {
305
+ try {
306
+ const { model, result } = event;
307
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
308
+ const isEntryValid = await getEntryValidStatus(
309
+ model.uid,
310
+ result,
311
+ {
312
+ strapi: strapi2
313
+ }
314
+ );
315
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
316
+ where: {
317
+ target_type: model.uid,
318
+ target_id: result.id
319
+ },
320
+ data: {
321
+ isEntryValid
322
+ }
323
+ });
324
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
325
+ where: {
326
+ actions: {
327
+ target_type: model.uid,
328
+ target_id: result.id
329
+ }
330
+ }
331
+ });
332
+ for (const release2 of releases) {
333
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
334
+ }
335
+ }
336
+ } catch (error) {
337
+ strapi2.log.error("Error while updating release actions after entry update", { error });
338
+ }
138
339
  }
139
340
  });
140
341
  if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
@@ -192,6 +393,11 @@ const schema$1 = {
192
393
  timezone: {
193
394
  type: "string"
194
395
  },
396
+ status: {
397
+ type: "enumeration",
398
+ enum: ["ready", "blocked", "failed", "done", "empty"],
399
+ required: true
400
+ },
195
401
  actions: {
196
402
  type: "relation",
197
403
  relation: "oneToMany",
@@ -244,6 +450,9 @@ const schema = {
244
450
  relation: "manyToOne",
245
451
  target: RELEASE_MODEL_UID,
246
452
  inversedBy: "actions"
453
+ },
454
+ isEntryValid: {
455
+ type: "boolean"
247
456
  }
248
457
  }
249
458
  };
@@ -288,7 +497,10 @@ const createReleaseService = ({ strapi: strapi2 }) => {
288
497
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
498
  ]);
290
499
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
- data: releaseWithCreatorFields
500
+ data: {
501
+ ...releaseWithCreatorFields,
502
+ status: "empty"
503
+ }
292
504
  });
293
505
  if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
294
506
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -423,6 +635,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
423
635
  schedulingService.cancel(id);
424
636
  }
425
637
  }
638
+ this.updateReleaseStatus(id);
426
639
  strapi2.telemetry.send("didUpdateContentRelease");
427
640
  return updatedRelease;
428
641
  },
@@ -442,11 +655,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
442
655
  throw new errors.ValidationError("Release already published");
443
656
  }
444
657
  const { entry, type } = action;
445
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
658
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
659
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
660
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
446
661
  data: {
447
662
  type,
448
663
  contentType: entry.contentType,
449
664
  locale: entry.locale,
665
+ isEntryValid,
450
666
  entry: {
451
667
  id: entry.id,
452
668
  __type: entry.contentType,
@@ -456,6 +672,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
456
672
  },
457
673
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
674
  });
675
+ this.updateReleaseStatus(releaseId);
676
+ return releaseAction2;
459
677
  },
460
678
  async findActions(releaseId, query) {
461
679
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -720,6 +938,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
720
938
  error
721
939
  });
722
940
  }
941
+ strapi2.db.query(RELEASE_MODEL_UID).update({
942
+ where: { id: releaseId },
943
+ data: {
944
+ status: "failed"
945
+ }
946
+ });
723
947
  throw error;
724
948
  }
725
949
  },
@@ -760,7 +984,51 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
984
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
985
  );
762
986
  }
987
+ this.updateReleaseStatus(releaseId);
763
988
  return deletedAction;
989
+ },
990
+ async updateReleaseStatus(releaseId) {
991
+ const [totalActions, invalidActions] = await Promise.all([
992
+ this.countActions({
993
+ filters: {
994
+ release: releaseId
995
+ }
996
+ }),
997
+ this.countActions({
998
+ filters: {
999
+ release: releaseId,
1000
+ isEntryValid: false
1001
+ }
1002
+ })
1003
+ ]);
1004
+ if (totalActions > 0) {
1005
+ if (invalidActions > 0) {
1006
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1007
+ where: {
1008
+ id: releaseId
1009
+ },
1010
+ data: {
1011
+ status: "blocked"
1012
+ }
1013
+ });
1014
+ }
1015
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1016
+ where: {
1017
+ id: releaseId
1018
+ },
1019
+ data: {
1020
+ status: "ready"
1021
+ }
1022
+ });
1023
+ }
1024
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1025
+ where: {
1026
+ id: releaseId
1027
+ },
1028
+ data: {
1029
+ status: "empty"
1030
+ }
1031
+ });
764
1032
  }
765
1033
  };
766
1034
  };
@@ -937,7 +1205,12 @@ const releaseController = {
937
1205
  }
938
1206
  };
939
1207
  });
940
- ctx.body = { data, meta: { pagination } };
1208
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1209
+ where: {
1210
+ releasedAt: null
1211
+ }
1212
+ });
1213
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
941
1214
  }
942
1215
  },
943
1216
  async findOne(ctx) {