@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,5 +1,6 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const isEqual = require("lodash/isEqual");
3
4
  const lodash = require("lodash");
4
5
  const _ = require("lodash/fp");
5
6
  const EE = require("@strapi/strapi/dist/utils/ee");
@@ -24,6 +25,7 @@ function _interopNamespace(e) {
24
25
  n.default = e;
25
26
  return Object.freeze(n);
26
27
  }
28
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
27
29
  const ___default = /* @__PURE__ */ _interopDefault(_);
28
30
  const EE__default = /* @__PURE__ */ _interopDefault(EE);
29
31
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
@@ -76,6 +78,29 @@ const ACTIONS = [
76
78
  const ALLOWED_WEBHOOK_EVENTS = {
77
79
  RELEASES_PUBLISH: "releases.publish"
78
80
  };
81
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
82
+ return strapi2.plugin("content-releases").service(name);
83
+ };
84
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
85
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
86
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
87
+ const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
88
+ return entry;
89
+ };
90
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
+ try {
92
+ await strapi2.entityValidator.validateEntityCreation(
93
+ strapi2.getModel(contentTypeUid),
94
+ entry,
95
+ void 0,
96
+ // @ts-expect-error - FIXME: entity here is unnecessary
97
+ entry
98
+ );
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ };
79
104
  async function deleteActionsOnDisableDraftAndPublish({
80
105
  oldContentTypes,
81
106
  contentTypes: contentTypes2
@@ -102,31 +127,196 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
102
127
  });
103
128
  }
104
129
  }
130
+ async function migrateIsValidAndStatusReleases() {
131
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
132
+ where: {
133
+ status: null,
134
+ releasedAt: null
135
+ },
136
+ populate: {
137
+ actions: {
138
+ populate: {
139
+ entry: true
140
+ }
141
+ }
142
+ }
143
+ });
144
+ utils.mapAsync(releasesWithoutStatus, async (release2) => {
145
+ const actions = release2.actions;
146
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
147
+ for (const action of notValidatedActions) {
148
+ if (action.entry) {
149
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
150
+ strapi
151
+ });
152
+ if (populatedEntry) {
153
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
154
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
155
+ where: {
156
+ id: action.id
157
+ },
158
+ data: {
159
+ isEntryValid
160
+ }
161
+ });
162
+ }
163
+ }
164
+ }
165
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
166
+ });
167
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
168
+ where: {
169
+ status: null,
170
+ releasedAt: {
171
+ $notNull: true
172
+ }
173
+ }
174
+ });
175
+ utils.mapAsync(publishedReleases, async (release2) => {
176
+ return strapi.db.query(RELEASE_MODEL_UID).update({
177
+ where: {
178
+ id: release2.id
179
+ },
180
+ data: {
181
+ status: "done"
182
+ }
183
+ });
184
+ });
185
+ }
186
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
187
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
188
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
189
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
190
+ );
191
+ const releasesAffected = /* @__PURE__ */ new Set();
192
+ utils.mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
193
+ const oldContentType = oldContentTypes[contentTypeUID];
194
+ const contentType = contentTypes2[contentTypeUID];
195
+ if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
196
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
197
+ where: {
198
+ contentType: contentTypeUID
199
+ },
200
+ populate: {
201
+ entry: true,
202
+ release: true
203
+ }
204
+ });
205
+ await utils.mapAsync(actions, async (action) => {
206
+ if (action.entry && action.release) {
207
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
208
+ strapi
209
+ });
210
+ if (populatedEntry) {
211
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
212
+ strapi
213
+ });
214
+ releasesAffected.add(action.release.id);
215
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
216
+ where: {
217
+ id: action.id
218
+ },
219
+ data: {
220
+ isEntryValid
221
+ }
222
+ });
223
+ }
224
+ }
225
+ });
226
+ }
227
+ }).then(() => {
228
+ utils.mapAsync(releasesAffected, async (releaseId) => {
229
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
230
+ });
231
+ });
232
+ }
233
+ }
234
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
235
+ if (!oldContentTypes) {
236
+ return;
237
+ }
238
+ for (const uid in contentTypes2) {
239
+ if (!oldContentTypes[uid]) {
240
+ continue;
241
+ }
242
+ const oldContentType = oldContentTypes[uid];
243
+ const contentType = contentTypes2[uid];
244
+ const i18nPlugin = strapi.plugin("i18n");
245
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
246
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
247
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
248
+ locale: null
249
+ }).where({ contentType: uid }).execute();
250
+ }
251
+ }
252
+ }
253
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
254
+ if (!oldContentTypes) {
255
+ return;
256
+ }
257
+ for (const uid in contentTypes2) {
258
+ if (!oldContentTypes[uid]) {
259
+ continue;
260
+ }
261
+ const oldContentType = oldContentTypes[uid];
262
+ const contentType = contentTypes2[uid];
263
+ const i18nPlugin = strapi.plugin("i18n");
264
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
265
+ const { getDefaultLocale } = i18nPlugin.service("locales");
266
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
267
+ const defaultLocale = await getDefaultLocale();
268
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
269
+ locale: defaultLocale
270
+ }).where({ contentType: uid }).execute();
271
+ }
272
+ }
273
+ }
105
274
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
106
275
  const register = async ({ strapi: strapi2 }) => {
107
276
  if (features$2.isEnabled("cms-content-releases")) {
108
277
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
109
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
110
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
278
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
279
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
280
+ }
281
+ if (strapi2.plugin("graphql")) {
282
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
283
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
284
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
111
285
  }
112
- };
113
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
114
- return strapi2.plugin("content-releases").service(name);
115
286
  };
116
287
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
117
288
  const bootstrap = async ({ strapi: strapi2 }) => {
118
289
  if (features$1.isEnabled("cms-content-releases")) {
290
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
291
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
292
+ );
119
293
  strapi2.db.lifecycles.subscribe({
120
- afterDelete(event) {
121
- const { model, result } = event;
122
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
123
- const { id } = result;
124
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
125
- where: {
126
- target_type: model.uid,
127
- target_id: id
294
+ models: contentTypesWithDraftAndPublish,
295
+ async afterDelete(event) {
296
+ try {
297
+ const { model, result } = event;
298
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
299
+ const { id } = result;
300
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
301
+ where: {
302
+ actions: {
303
+ target_type: model.uid,
304
+ target_id: id
305
+ }
306
+ }
307
+ });
308
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
309
+ where: {
310
+ target_type: model.uid,
311
+ target_id: id
312
+ }
313
+ });
314
+ for (const release2 of releases) {
315
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
128
316
  }
129
- });
317
+ }
318
+ } catch (error) {
319
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
130
320
  }
131
321
  },
132
322
  /**
@@ -146,18 +336,75 @@ const bootstrap = async ({ strapi: strapi2 }) => {
146
336
  * We make this only after deleteMany is succesfully executed to avoid errors
147
337
  */
148
338
  async afterDeleteMany(event) {
149
- const { model, state } = event;
150
- const entriesToDelete = state.entriesToDelete;
151
- if (entriesToDelete) {
152
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
153
- where: {
154
- target_type: model.uid,
155
- target_id: {
156
- $in: entriesToDelete.map((entry) => entry.id)
339
+ try {
340
+ const { model, state } = event;
341
+ const entriesToDelete = state.entriesToDelete;
342
+ if (entriesToDelete) {
343
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
344
+ where: {
345
+ actions: {
346
+ target_type: model.uid,
347
+ target_id: {
348
+ $in: entriesToDelete.map(
349
+ (entry) => entry.id
350
+ )
351
+ }
352
+ }
353
+ }
354
+ });
355
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
356
+ where: {
357
+ target_type: model.uid,
358
+ target_id: {
359
+ $in: entriesToDelete.map((entry) => entry.id)
360
+ }
157
361
  }
362
+ });
363
+ for (const release2 of releases) {
364
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
158
365
  }
366
+ }
367
+ } catch (error) {
368
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
369
+ error
159
370
  });
160
371
  }
372
+ },
373
+ async afterUpdate(event) {
374
+ try {
375
+ const { model, result } = event;
376
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
377
+ const isEntryValid = await getEntryValidStatus(
378
+ model.uid,
379
+ result,
380
+ {
381
+ strapi: strapi2
382
+ }
383
+ );
384
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
385
+ where: {
386
+ target_type: model.uid,
387
+ target_id: result.id
388
+ },
389
+ data: {
390
+ isEntryValid
391
+ }
392
+ });
393
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
394
+ where: {
395
+ actions: {
396
+ target_type: model.uid,
397
+ target_id: result.id
398
+ }
399
+ }
400
+ });
401
+ for (const release2 of releases) {
402
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
403
+ }
404
+ }
405
+ } catch (error) {
406
+ strapi2.log.error("Error while updating release actions after entry update", { error });
407
+ }
161
408
  }
162
409
  });
163
410
  if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
@@ -215,6 +462,11 @@ const schema$1 = {
215
462
  timezone: {
216
463
  type: "string"
217
464
  },
465
+ status: {
466
+ type: "enumeration",
467
+ enum: ["ready", "blocked", "failed", "done", "empty"],
468
+ required: true
469
+ },
218
470
  actions: {
219
471
  type: "relation",
220
472
  relation: "oneToMany",
@@ -267,6 +519,9 @@ const schema = {
267
519
  relation: "manyToOne",
268
520
  target: RELEASE_MODEL_UID,
269
521
  inversedBy: "actions"
522
+ },
523
+ isEntryValid: {
524
+ type: "boolean"
270
525
  }
271
526
  }
272
527
  };
@@ -311,7 +566,10 @@ const createReleaseService = ({ strapi: strapi2 }) => {
311
566
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
567
  ]);
313
568
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
- data: releaseWithCreatorFields
569
+ data: {
570
+ ...releaseWithCreatorFields,
571
+ status: "empty"
572
+ }
315
573
  });
316
574
  if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
317
575
  const schedulingService = getService("scheduling", { strapi: strapi2 });
@@ -446,6 +704,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
446
704
  schedulingService.cancel(id);
447
705
  }
448
706
  }
707
+ this.updateReleaseStatus(id);
449
708
  strapi2.telemetry.send("didUpdateContentRelease");
450
709
  return updatedRelease;
451
710
  },
@@ -465,11 +724,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
465
724
  throw new utils.errors.ValidationError("Release already published");
466
725
  }
467
726
  const { entry, type } = action;
468
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
727
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
728
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
729
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
469
730
  data: {
470
731
  type,
471
732
  contentType: entry.contentType,
472
733
  locale: entry.locale,
734
+ isEntryValid,
473
735
  entry: {
474
736
  id: entry.id,
475
737
  __type: entry.contentType,
@@ -479,6 +741,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
479
741
  },
480
742
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
481
743
  });
744
+ this.updateReleaseStatus(releaseId);
745
+ return releaseAction2;
482
746
  },
483
747
  async findActions(releaseId, query) {
484
748
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -743,6 +1007,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
743
1007
  error
744
1008
  });
745
1009
  }
1010
+ strapi2.db.query(RELEASE_MODEL_UID).update({
1011
+ where: { id: releaseId },
1012
+ data: {
1013
+ status: "failed"
1014
+ }
1015
+ });
746
1016
  throw error;
747
1017
  }
748
1018
  },
@@ -783,7 +1053,51 @@ const createReleaseService = ({ strapi: strapi2 }) => {
783
1053
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
1054
  );
785
1055
  }
1056
+ this.updateReleaseStatus(releaseId);
786
1057
  return deletedAction;
1058
+ },
1059
+ async updateReleaseStatus(releaseId) {
1060
+ const [totalActions, invalidActions] = await Promise.all([
1061
+ this.countActions({
1062
+ filters: {
1063
+ release: releaseId
1064
+ }
1065
+ }),
1066
+ this.countActions({
1067
+ filters: {
1068
+ release: releaseId,
1069
+ isEntryValid: false
1070
+ }
1071
+ })
1072
+ ]);
1073
+ if (totalActions > 0) {
1074
+ if (invalidActions > 0) {
1075
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1076
+ where: {
1077
+ id: releaseId
1078
+ },
1079
+ data: {
1080
+ status: "blocked"
1081
+ }
1082
+ });
1083
+ }
1084
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1085
+ where: {
1086
+ id: releaseId
1087
+ },
1088
+ data: {
1089
+ status: "ready"
1090
+ }
1091
+ });
1092
+ }
1093
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1094
+ where: {
1095
+ id: releaseId
1096
+ },
1097
+ data: {
1098
+ status: "empty"
1099
+ }
1100
+ });
787
1101
  }
788
1102
  };
789
1103
  };
@@ -960,7 +1274,12 @@ const releaseController = {
960
1274
  }
961
1275
  };
962
1276
  });
963
- ctx.body = { data, meta: { pagination } };
1277
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1278
+ where: {
1279
+ releasedAt: null
1280
+ }
1281
+ });
1282
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
964
1283
  }
965
1284
  },
966
1285
  async findOne(ctx) {
@@ -1331,6 +1650,9 @@ const getPlugin = () => {
1331
1650
  };
1332
1651
  }
1333
1652
  return {
1653
+ // Always return register, it handles its own feature check
1654
+ register,
1655
+ // Always return contentTypes to avoid losing data when the feature is disabled
1334
1656
  contentTypes
1335
1657
  };
1336
1658
  };