@strapi/content-releases 4.20.3 → 4.20.5

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,196 @@ 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 && action.release) {
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
+ }
210
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
211
+ if (!oldContentTypes) {
212
+ return;
213
+ }
214
+ for (const uid in contentTypes2) {
215
+ if (!oldContentTypes[uid]) {
216
+ continue;
217
+ }
218
+ const oldContentType = oldContentTypes[uid];
219
+ const contentType = contentTypes2[uid];
220
+ const i18nPlugin = strapi.plugin("i18n");
221
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
222
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
223
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
224
+ locale: null
225
+ }).where({ contentType: uid }).execute();
226
+ }
227
+ }
228
+ }
229
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
230
+ if (!oldContentTypes) {
231
+ return;
232
+ }
233
+ for (const uid in contentTypes2) {
234
+ if (!oldContentTypes[uid]) {
235
+ continue;
236
+ }
237
+ const oldContentType = oldContentTypes[uid];
238
+ const contentType = contentTypes2[uid];
239
+ const i18nPlugin = strapi.plugin("i18n");
240
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
241
+ const { getDefaultLocale } = i18nPlugin.service("locales");
242
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
243
+ const defaultLocale = await getDefaultLocale();
244
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
245
+ locale: defaultLocale
246
+ }).where({ contentType: uid }).execute();
247
+ }
248
+ }
249
+ }
82
250
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
83
251
  const register = async ({ strapi: strapi2 }) => {
84
252
  if (features$2.isEnabled("cms-content-releases")) {
85
253
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
86
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
87
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
254
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
255
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
256
+ }
257
+ if (strapi2.plugin("graphql")) {
258
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
259
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
260
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
88
261
  }
89
- };
90
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
- return strapi2.plugin("content-releases").service(name);
92
262
  };
93
263
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
94
264
  const bootstrap = async ({ strapi: strapi2 }) => {
95
265
  if (features$1.isEnabled("cms-content-releases")) {
266
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
+ );
96
269
  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
270
+ models: contentTypesWithDraftAndPublish,
271
+ async afterDelete(event) {
272
+ try {
273
+ const { model, result } = event;
274
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
275
+ const { id } = result;
276
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
277
+ where: {
278
+ actions: {
279
+ target_type: model.uid,
280
+ target_id: id
281
+ }
282
+ }
283
+ });
284
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
285
+ where: {
286
+ target_type: model.uid,
287
+ target_id: id
288
+ }
289
+ });
290
+ for (const release2 of releases) {
291
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
105
292
  }
106
- });
293
+ }
294
+ } catch (error) {
295
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
107
296
  }
108
297
  },
109
298
  /**
@@ -123,18 +312,75 @@ const bootstrap = async ({ strapi: strapi2 }) => {
123
312
  * We make this only after deleteMany is succesfully executed to avoid errors
124
313
  */
125
314
  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)
315
+ try {
316
+ const { model, state } = event;
317
+ const entriesToDelete = state.entriesToDelete;
318
+ if (entriesToDelete) {
319
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
320
+ where: {
321
+ actions: {
322
+ target_type: model.uid,
323
+ target_id: {
324
+ $in: entriesToDelete.map(
325
+ (entry) => entry.id
326
+ )
327
+ }
328
+ }
329
+ }
330
+ });
331
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
332
+ where: {
333
+ target_type: model.uid,
334
+ target_id: {
335
+ $in: entriesToDelete.map((entry) => entry.id)
336
+ }
134
337
  }
338
+ });
339
+ for (const release2 of releases) {
340
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
135
341
  }
342
+ }
343
+ } catch (error) {
344
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
+ error
136
346
  });
137
347
  }
348
+ },
349
+ async afterUpdate(event) {
350
+ try {
351
+ const { model, result } = event;
352
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
353
+ const isEntryValid = await getEntryValidStatus(
354
+ model.uid,
355
+ result,
356
+ {
357
+ strapi: strapi2
358
+ }
359
+ );
360
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
361
+ where: {
362
+ target_type: model.uid,
363
+ target_id: result.id
364
+ },
365
+ data: {
366
+ isEntryValid
367
+ }
368
+ });
369
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
370
+ where: {
371
+ actions: {
372
+ target_type: model.uid,
373
+ target_id: result.id
374
+ }
375
+ }
376
+ });
377
+ for (const release2 of releases) {
378
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
379
+ }
380
+ }
381
+ } catch (error) {
382
+ strapi2.log.error("Error while updating release actions after entry update", { error });
383
+ }
138
384
  }
139
385
  });
140
386
  if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
@@ -192,6 +438,11 @@ const schema$1 = {
192
438
  timezone: {
193
439
  type: "string"
194
440
  },
441
+ status: {
442
+ type: "enumeration",
443
+ enum: ["ready", "blocked", "failed", "done", "empty"],
444
+ required: true
445
+ },
195
446
  actions: {
196
447
  type: "relation",
197
448
  relation: "oneToMany",
@@ -244,6 +495,9 @@ const schema = {
244
495
  relation: "manyToOne",
245
496
  target: RELEASE_MODEL_UID,
246
497
  inversedBy: "actions"
498
+ },
499
+ isEntryValid: {
500
+ type: "boolean"
247
501
  }
248
502
  }
249
503
  };
@@ -288,7 +542,10 @@ const createReleaseService = ({ strapi: strapi2 }) => {
288
542
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
543
  ]);
290
544
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
- data: releaseWithCreatorFields
545
+ data: {
546
+ ...releaseWithCreatorFields,
547
+ status: "empty"
548
+ }
292
549
  });
293
550
  if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
294
551
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -423,6 +680,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
423
680
  schedulingService.cancel(id);
424
681
  }
425
682
  }
683
+ this.updateReleaseStatus(id);
426
684
  strapi2.telemetry.send("didUpdateContentRelease");
427
685
  return updatedRelease;
428
686
  },
@@ -442,11 +700,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
442
700
  throw new errors.ValidationError("Release already published");
443
701
  }
444
702
  const { entry, type } = action;
445
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
703
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
704
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
705
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
446
706
  data: {
447
707
  type,
448
708
  contentType: entry.contentType,
449
709
  locale: entry.locale,
710
+ isEntryValid,
450
711
  entry: {
451
712
  id: entry.id,
452
713
  __type: entry.contentType,
@@ -456,6 +717,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
456
717
  },
457
718
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
719
  });
720
+ this.updateReleaseStatus(releaseId);
721
+ return releaseAction2;
459
722
  },
460
723
  async findActions(releaseId, query) {
461
724
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -720,6 +983,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
720
983
  error
721
984
  });
722
985
  }
986
+ strapi2.db.query(RELEASE_MODEL_UID).update({
987
+ where: { id: releaseId },
988
+ data: {
989
+ status: "failed"
990
+ }
991
+ });
723
992
  throw error;
724
993
  }
725
994
  },
@@ -760,7 +1029,51 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1029
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
1030
  );
762
1031
  }
1032
+ this.updateReleaseStatus(releaseId);
763
1033
  return deletedAction;
1034
+ },
1035
+ async updateReleaseStatus(releaseId) {
1036
+ const [totalActions, invalidActions] = await Promise.all([
1037
+ this.countActions({
1038
+ filters: {
1039
+ release: releaseId
1040
+ }
1041
+ }),
1042
+ this.countActions({
1043
+ filters: {
1044
+ release: releaseId,
1045
+ isEntryValid: false
1046
+ }
1047
+ })
1048
+ ]);
1049
+ if (totalActions > 0) {
1050
+ if (invalidActions > 0) {
1051
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1052
+ where: {
1053
+ id: releaseId
1054
+ },
1055
+ data: {
1056
+ status: "blocked"
1057
+ }
1058
+ });
1059
+ }
1060
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1061
+ where: {
1062
+ id: releaseId
1063
+ },
1064
+ data: {
1065
+ status: "ready"
1066
+ }
1067
+ });
1068
+ }
1069
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1070
+ where: {
1071
+ id: releaseId
1072
+ },
1073
+ data: {
1074
+ status: "empty"
1075
+ }
1076
+ });
764
1077
  }
765
1078
  };
766
1079
  };
@@ -937,7 +1250,12 @@ const releaseController = {
937
1250
  }
938
1251
  };
939
1252
  });
940
- ctx.body = { data, meta: { pagination } };
1253
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1254
+ where: {
1255
+ releasedAt: null
1256
+ }
1257
+ });
1258
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
941
1259
  }
942
1260
  },
943
1261
  async findOne(ctx) {
@@ -1308,6 +1626,9 @@ const getPlugin = () => {
1308
1626
  };
1309
1627
  }
1310
1628
  return {
1629
+ // Always return register, it handles its own feature check
1630
+ register,
1631
+ // Always return contentTypes to avoid losing data when the feature is disabled
1311
1632
  contentTypes
1312
1633
  };
1313
1634
  };