@strapi/content-releases 0.0.0-next.95a939e004e74915357523e3adb118a31fef57ed → 0.0.0-next.a9d79bec775daaf0da4e506b2aebafdb4ca95b06

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";
@@ -50,6 +51,32 @@ const ACTIONS = [
50
51
  pluginName: "content-releases"
51
52
  }
52
53
  ];
54
+ const ALLOWED_WEBHOOK_EVENTS = {
55
+ RELEASES_PUBLISH: "releases.publish"
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
+ };
53
80
  async function deleteActionsOnDisableDraftAndPublish({
54
81
  oldContentTypes,
55
82
  contentTypes: contentTypes2
@@ -76,31 +103,196 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
76
103
  });
77
104
  }
78
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
+ }
79
250
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
80
251
  const register = async ({ strapi: strapi2 }) => {
81
252
  if (features$2.isEnabled("cms-content-releases")) {
82
253
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
83
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
84
- 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();
85
261
  }
86
- };
87
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
88
- return strapi2.plugin("content-releases").service(name);
89
262
  };
90
263
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
91
264
  const bootstrap = async ({ strapi: strapi2 }) => {
92
265
  if (features$1.isEnabled("cms-content-releases")) {
266
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
+ );
93
269
  strapi2.db.lifecycles.subscribe({
94
- afterDelete(event) {
95
- const { model, result } = event;
96
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
97
- const { id } = result;
98
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
99
- where: {
100
- target_type: model.uid,
101
- 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);
102
292
  }
103
- });
293
+ }
294
+ } catch (error) {
295
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
104
296
  }
105
297
  },
106
298
  /**
@@ -120,18 +312,75 @@ const bootstrap = async ({ strapi: strapi2 }) => {
120
312
  * We make this only after deleteMany is succesfully executed to avoid errors
121
313
  */
122
314
  async afterDeleteMany(event) {
123
- const { model, state } = event;
124
- const entriesToDelete = state.entriesToDelete;
125
- if (entriesToDelete) {
126
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
127
- where: {
128
- target_type: model.uid,
129
- target_id: {
130
- $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
+ }
131
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
+ }
337
+ }
338
+ });
339
+ for (const release2 of releases) {
340
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
132
341
  }
342
+ }
343
+ } catch (error) {
344
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
+ error
133
346
  });
134
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
+ }
135
384
  }
136
385
  });
137
386
  if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
@@ -141,6 +390,9 @@ const bootstrap = async ({ strapi: strapi2 }) => {
141
390
  );
142
391
  throw err;
143
392
  });
393
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
394
+ strapi2.webhookStore.addAllowedEvent(key, value);
395
+ });
144
396
  }
145
397
  }
146
398
  };
@@ -186,6 +438,11 @@ const schema$1 = {
186
438
  timezone: {
187
439
  type: "string"
188
440
  },
441
+ status: {
442
+ type: "enumeration",
443
+ enum: ["ready", "blocked", "failed", "done", "empty"],
444
+ required: true
445
+ },
189
446
  actions: {
190
447
  type: "relation",
191
448
  relation: "oneToMany",
@@ -238,6 +495,9 @@ const schema = {
238
495
  relation: "manyToOne",
239
496
  target: RELEASE_MODEL_UID,
240
497
  inversedBy: "actions"
498
+ },
499
+ isEntryValid: {
500
+ type: "boolean"
241
501
  }
242
502
  }
243
503
  };
@@ -260,464 +520,563 @@ const getGroupName = (queryValue) => {
260
520
  return "contentType.displayName";
261
521
  }
262
522
  };
263
- const createReleaseService = ({ strapi: strapi2 }) => ({
264
- async create(releaseData, { user }) {
265
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
266
- const {
267
- validatePendingReleasesLimit,
268
- validateUniqueNameForPendingRelease,
269
- validateScheduledAtIsLaterThanNow
270
- } = getService("release-validation", { strapi: strapi2 });
271
- await Promise.all([
272
- validatePendingReleasesLimit(),
273
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
274
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
275
- ]);
276
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
277
- data: releaseWithCreatorFields
523
+ const createReleaseService = ({ strapi: strapi2 }) => {
524
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
525
+ strapi2.eventHub.emit(event, {
526
+ isPublished,
527
+ error,
528
+ release: release2
278
529
  });
279
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
280
- const schedulingService = getService("scheduling", { strapi: strapi2 });
281
- await schedulingService.set(release2.id, release2.scheduledAt);
282
- }
283
- return release2;
284
- },
285
- async findOne(id, query = {}) {
286
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
287
- ...query
288
- });
289
- return release2;
290
- },
291
- findPage(query) {
292
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
293
- ...query,
294
- populate: {
295
- actions: {
296
- // @ts-expect-error Ignore missing properties
297
- count: true
530
+ };
531
+ return {
532
+ async create(releaseData, { user }) {
533
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
534
+ const {
535
+ validatePendingReleasesLimit,
536
+ validateUniqueNameForPendingRelease,
537
+ validateScheduledAtIsLaterThanNow
538
+ } = getService("release-validation", { strapi: strapi2 });
539
+ await Promise.all([
540
+ validatePendingReleasesLimit(),
541
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
542
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
543
+ ]);
544
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
545
+ data: {
546
+ ...releaseWithCreatorFields,
547
+ status: "empty"
298
548
  }
549
+ });
550
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
551
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
552
+ await schedulingService.set(release2.id, release2.scheduledAt);
299
553
  }
300
- });
301
- },
302
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
303
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
304
- where: {
305
- actions: {
306
- target_type: contentTypeUid,
307
- target_id: entryId
554
+ strapi2.telemetry.send("didCreateContentRelease");
555
+ return release2;
556
+ },
557
+ async findOne(id, query = {}) {
558
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
559
+ ...query
560
+ });
561
+ return release2;
562
+ },
563
+ findPage(query) {
564
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
565
+ ...query,
566
+ populate: {
567
+ actions: {
568
+ // @ts-expect-error Ignore missing properties
569
+ count: true
570
+ }
571
+ }
572
+ });
573
+ },
574
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
575
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
576
+ where: {
577
+ actions: {
578
+ target_type: contentTypeUid,
579
+ target_id: entryId
580
+ },
581
+ releasedAt: {
582
+ $null: true
583
+ }
308
584
  },
309
- releasedAt: {
310
- $null: true
585
+ populate: {
586
+ // Filter the action to get only the content type entry
587
+ actions: {
588
+ where: {
589
+ target_type: contentTypeUid,
590
+ target_id: entryId
591
+ }
592
+ }
311
593
  }
312
- },
313
- populate: {
314
- // Filter the action to get only the content type entry
315
- actions: {
316
- where: {
594
+ });
595
+ return releases.map((release2) => {
596
+ if (release2.actions?.length) {
597
+ const [actionForEntry] = release2.actions;
598
+ delete release2.actions;
599
+ return {
600
+ ...release2,
601
+ action: actionForEntry
602
+ };
603
+ }
604
+ return release2;
605
+ });
606
+ },
607
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
608
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
609
+ where: {
610
+ releasedAt: {
611
+ $null: true
612
+ },
613
+ actions: {
317
614
  target_type: contentTypeUid,
318
615
  target_id: entryId
319
616
  }
320
617
  }
618
+ });
619
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
620
+ where: {
621
+ $or: [
622
+ {
623
+ id: {
624
+ $notIn: releasesRelated.map((release2) => release2.id)
625
+ }
626
+ },
627
+ {
628
+ actions: null
629
+ }
630
+ ],
631
+ releasedAt: {
632
+ $null: true
633
+ }
634
+ }
635
+ });
636
+ return releases.map((release2) => {
637
+ if (release2.actions?.length) {
638
+ const [actionForEntry] = release2.actions;
639
+ delete release2.actions;
640
+ return {
641
+ ...release2,
642
+ action: actionForEntry
643
+ };
644
+ }
645
+ return release2;
646
+ });
647
+ },
648
+ async update(id, releaseData, { user }) {
649
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
650
+ releaseData
651
+ );
652
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
653
+ "release-validation",
654
+ { strapi: strapi2 }
655
+ );
656
+ await Promise.all([
657
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
658
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
659
+ ]);
660
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
661
+ if (!release2) {
662
+ throw new errors.NotFoundError(`No release found for id ${id}`);
321
663
  }
322
- });
323
- return releases.map((release2) => {
324
- if (release2.actions?.length) {
325
- const [actionForEntry] = release2.actions;
326
- delete release2.actions;
327
- return {
328
- ...release2,
329
- action: actionForEntry
330
- };
664
+ if (release2.releasedAt) {
665
+ throw new errors.ValidationError("Release already published");
331
666
  }
332
- return release2;
333
- });
334
- },
335
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
336
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
337
- where: {
338
- releasedAt: {
339
- $null: true
340
- },
341
- actions: {
342
- target_type: contentTypeUid,
343
- target_id: entryId
667
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
668
+ /*
669
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
670
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
671
+ */
672
+ // @ts-expect-error see above
673
+ data: releaseWithCreatorFields
674
+ });
675
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
676
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
677
+ if (releaseData.scheduledAt) {
678
+ await schedulingService.set(id, releaseData.scheduledAt);
679
+ } else if (release2.scheduledAt) {
680
+ schedulingService.cancel(id);
344
681
  }
345
682
  }
346
- });
347
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
348
- where: {
349
- $or: [
350
- {
351
- id: {
352
- $notIn: releasesRelated.map((release2) => release2.id)
353
- }
354
- },
355
- {
356
- actions: null
357
- }
358
- ],
359
- releasedAt: {
360
- $null: true
361
- }
683
+ this.updateReleaseStatus(id);
684
+ strapi2.telemetry.send("didUpdateContentRelease");
685
+ return updatedRelease;
686
+ },
687
+ async createAction(releaseId, action) {
688
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
689
+ strapi: strapi2
690
+ });
691
+ await Promise.all([
692
+ validateEntryContentType(action.entry.contentType),
693
+ validateUniqueEntry(releaseId, action)
694
+ ]);
695
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
696
+ if (!release2) {
697
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
362
698
  }
363
- });
364
- return releases.map((release2) => {
365
- if (release2.actions?.length) {
366
- const [actionForEntry] = release2.actions;
367
- delete release2.actions;
368
- return {
369
- ...release2,
370
- action: actionForEntry
371
- };
699
+ if (release2.releasedAt) {
700
+ throw new errors.ValidationError("Release already published");
372
701
  }
373
- return release2;
374
- });
375
- },
376
- async update(id, releaseData, { user }) {
377
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
378
- const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
379
- "release-validation",
380
- { strapi: strapi2 }
381
- );
382
- await Promise.all([
383
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
384
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
385
- ]);
386
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
387
- if (!release2) {
388
- throw new errors.NotFoundError(`No release found for id ${id}`);
389
- }
390
- if (release2.releasedAt) {
391
- throw new errors.ValidationError("Release already published");
392
- }
393
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
394
- /*
395
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
396
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
397
- */
398
- // @ts-expect-error see above
399
- data: releaseWithCreatorFields
400
- });
401
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
402
- const schedulingService = getService("scheduling", { strapi: strapi2 });
403
- if (releaseData.scheduledAt) {
404
- await schedulingService.set(id, releaseData.scheduledAt);
405
- } else if (release2.scheduledAt) {
406
- schedulingService.cancel(id);
702
+ const { entry, type } = action;
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, {
706
+ data: {
707
+ type,
708
+ contentType: entry.contentType,
709
+ locale: entry.locale,
710
+ isEntryValid,
711
+ entry: {
712
+ id: entry.id,
713
+ __type: entry.contentType,
714
+ __pivot: { field: "entry" }
715
+ },
716
+ release: releaseId
717
+ },
718
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
719
+ });
720
+ this.updateReleaseStatus(releaseId);
721
+ return releaseAction2;
722
+ },
723
+ async findActions(releaseId, query) {
724
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
725
+ fields: ["id"]
726
+ });
727
+ if (!release2) {
728
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
407
729
  }
408
- }
409
- return updatedRelease;
410
- },
411
- async createAction(releaseId, action) {
412
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
413
- strapi: strapi2
414
- });
415
- await Promise.all([
416
- validateEntryContentType(action.entry.contentType),
417
- validateUniqueEntry(releaseId, action)
418
- ]);
419
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
420
- if (!release2) {
421
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
422
- }
423
- if (release2.releasedAt) {
424
- throw new errors.ValidationError("Release already published");
425
- }
426
- const { entry, type } = action;
427
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
428
- data: {
429
- type,
430
- contentType: entry.contentType,
431
- locale: entry.locale,
432
- entry: {
433
- id: entry.id,
434
- __type: entry.contentType,
435
- __pivot: { field: "entry" }
730
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
731
+ ...query,
732
+ populate: {
733
+ entry: {
734
+ populate: "*"
735
+ }
436
736
  },
437
- release: releaseId
438
- },
439
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
440
- });
441
- },
442
- async findActions(releaseId, query) {
443
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
444
- fields: ["id"]
445
- });
446
- if (!release2) {
447
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
448
- }
449
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
450
- ...query,
451
- populate: {
452
- entry: {
453
- populate: "*"
737
+ filters: {
738
+ release: releaseId
454
739
  }
455
- },
456
- filters: {
457
- release: releaseId
458
- }
459
- });
460
- },
461
- async countActions(query) {
462
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
463
- },
464
- async groupActions(actions, groupBy) {
465
- const contentTypeUids = actions.reduce((acc, action) => {
466
- if (!acc.includes(action.contentType)) {
467
- acc.push(action.contentType);
468
- }
469
- return acc;
470
- }, []);
471
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
472
- contentTypeUids
473
- );
474
- const allLocalesDictionary = await this.getLocalesDataForActions();
475
- const formattedData = actions.map((action) => {
476
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
477
- return {
478
- ...action,
479
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
480
- contentType: {
481
- displayName,
482
- mainFieldValue: action.entry[mainField],
483
- uid: action.contentType
740
+ });
741
+ },
742
+ async countActions(query) {
743
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
744
+ },
745
+ async groupActions(actions, groupBy) {
746
+ const contentTypeUids = actions.reduce((acc, action) => {
747
+ if (!acc.includes(action.contentType)) {
748
+ acc.push(action.contentType);
484
749
  }
485
- };
486
- });
487
- const groupName = getGroupName(groupBy);
488
- return _.groupBy(groupName)(formattedData);
489
- },
490
- async getLocalesDataForActions() {
491
- if (!strapi2.plugin("i18n")) {
492
- return {};
493
- }
494
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
495
- return allLocales.reduce((acc, locale) => {
496
- acc[locale.code] = { name: locale.name, code: locale.code };
497
- return acc;
498
- }, {});
499
- },
500
- async getContentTypesDataForActions(contentTypesUids) {
501
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
502
- const contentTypesData = {};
503
- for (const contentTypeUid of contentTypesUids) {
504
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
505
- uid: contentTypeUid
750
+ return acc;
751
+ }, []);
752
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
753
+ contentTypeUids
754
+ );
755
+ const allLocalesDictionary = await this.getLocalesDataForActions();
756
+ const formattedData = actions.map((action) => {
757
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
758
+ return {
759
+ ...action,
760
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
761
+ contentType: {
762
+ displayName,
763
+ mainFieldValue: action.entry[mainField],
764
+ uid: action.contentType
765
+ }
766
+ };
506
767
  });
507
- contentTypesData[contentTypeUid] = {
508
- mainField: contentTypeConfig.settings.mainField,
509
- displayName: strapi2.getModel(contentTypeUid).info.displayName
510
- };
511
- }
512
- return contentTypesData;
513
- },
514
- getContentTypeModelsFromActions(actions) {
515
- const contentTypeUids = actions.reduce((acc, action) => {
516
- if (!acc.includes(action.contentType)) {
517
- acc.push(action.contentType);
768
+ const groupName = getGroupName(groupBy);
769
+ return _.groupBy(groupName)(formattedData);
770
+ },
771
+ async getLocalesDataForActions() {
772
+ if (!strapi2.plugin("i18n")) {
773
+ return {};
518
774
  }
519
- return acc;
520
- }, []);
521
- const contentTypeModelsMap = contentTypeUids.reduce(
522
- (acc, contentTypeUid) => {
523
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
775
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
776
+ return allLocales.reduce((acc, locale) => {
777
+ acc[locale.code] = { name: locale.name, code: locale.code };
524
778
  return acc;
525
- },
526
- {}
527
- );
528
- return contentTypeModelsMap;
529
- },
530
- async getAllComponents() {
531
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
532
- const components = await contentManagerComponentsService.findAllComponents();
533
- const componentsMap = components.reduce(
534
- (acc, component) => {
535
- acc[component.uid] = component;
779
+ }, {});
780
+ },
781
+ async getContentTypesDataForActions(contentTypesUids) {
782
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
783
+ const contentTypesData = {};
784
+ for (const contentTypeUid of contentTypesUids) {
785
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
786
+ uid: contentTypeUid
787
+ });
788
+ contentTypesData[contentTypeUid] = {
789
+ mainField: contentTypeConfig.settings.mainField,
790
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
791
+ };
792
+ }
793
+ return contentTypesData;
794
+ },
795
+ getContentTypeModelsFromActions(actions) {
796
+ const contentTypeUids = actions.reduce((acc, action) => {
797
+ if (!acc.includes(action.contentType)) {
798
+ acc.push(action.contentType);
799
+ }
536
800
  return acc;
537
- },
538
- {}
539
- );
540
- return componentsMap;
541
- },
542
- async delete(releaseId) {
543
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
544
- populate: {
545
- actions: {
546
- fields: ["id"]
801
+ }, []);
802
+ const contentTypeModelsMap = contentTypeUids.reduce(
803
+ (acc, contentTypeUid) => {
804
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
805
+ return acc;
806
+ },
807
+ {}
808
+ );
809
+ return contentTypeModelsMap;
810
+ },
811
+ async getAllComponents() {
812
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
813
+ const components = await contentManagerComponentsService.findAllComponents();
814
+ const componentsMap = components.reduce(
815
+ (acc, component) => {
816
+ acc[component.uid] = component;
817
+ return acc;
818
+ },
819
+ {}
820
+ );
821
+ return componentsMap;
822
+ },
823
+ async delete(releaseId) {
824
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
825
+ populate: {
826
+ actions: {
827
+ fields: ["id"]
828
+ }
547
829
  }
830
+ });
831
+ if (!release2) {
832
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
548
833
  }
549
- });
550
- if (!release2) {
551
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
552
- }
553
- if (release2.releasedAt) {
554
- throw new errors.ValidationError("Release already published");
555
- }
556
- await strapi2.db.transaction(async () => {
557
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
558
- where: {
559
- id: {
560
- $in: release2.actions.map((action) => action.id)
834
+ if (release2.releasedAt) {
835
+ throw new errors.ValidationError("Release already published");
836
+ }
837
+ await strapi2.db.transaction(async () => {
838
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
839
+ where: {
840
+ id: {
841
+ $in: release2.actions.map((action) => action.id)
842
+ }
561
843
  }
562
- }
844
+ });
845
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
563
846
  });
564
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
565
- });
566
- return release2;
567
- },
568
- async publish(releaseId) {
569
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
570
- RELEASE_MODEL_UID,
571
- releaseId,
572
- {
573
- populate: {
574
- actions: {
847
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
848
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
849
+ await schedulingService.cancel(release2.id);
850
+ }
851
+ strapi2.telemetry.send("didDeleteContentRelease");
852
+ return release2;
853
+ },
854
+ async publish(releaseId) {
855
+ try {
856
+ const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
857
+ RELEASE_MODEL_UID,
858
+ releaseId,
859
+ {
575
860
  populate: {
576
- entry: {
577
- fields: ["id"]
861
+ actions: {
862
+ populate: {
863
+ entry: {
864
+ fields: ["id"]
865
+ }
866
+ }
578
867
  }
579
868
  }
580
869
  }
870
+ );
871
+ if (!releaseWithPopulatedActionEntries) {
872
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
581
873
  }
582
- }
583
- );
584
- if (!releaseWithPopulatedActionEntries) {
585
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
586
- }
587
- if (releaseWithPopulatedActionEntries.releasedAt) {
588
- throw new errors.ValidationError("Release already published");
589
- }
590
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
591
- throw new errors.ValidationError("No entries to publish");
592
- }
593
- const collectionTypeActions = {};
594
- const singleTypeActions = [];
595
- for (const action of releaseWithPopulatedActionEntries.actions) {
596
- const contentTypeUid = action.contentType;
597
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
598
- if (!collectionTypeActions[contentTypeUid]) {
599
- collectionTypeActions[contentTypeUid] = {
600
- entriestoPublishIds: [],
601
- entriesToUnpublishIds: []
602
- };
874
+ if (releaseWithPopulatedActionEntries.releasedAt) {
875
+ throw new errors.ValidationError("Release already published");
603
876
  }
604
- if (action.type === "publish") {
605
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
606
- } else {
607
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
877
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
878
+ throw new errors.ValidationError("No entries to publish");
608
879
  }
609
- } else {
610
- singleTypeActions.push({
611
- uid: contentTypeUid,
612
- action: action.type,
613
- id: action.entry.id
614
- });
615
- }
616
- }
617
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
618
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
619
- await strapi2.db.transaction(async () => {
620
- for (const { uid, action, id } of singleTypeActions) {
621
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
622
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
623
- try {
624
- if (action === "publish") {
625
- await entityManagerService.publish(entry, uid);
880
+ const collectionTypeActions = {};
881
+ const singleTypeActions = [];
882
+ for (const action of releaseWithPopulatedActionEntries.actions) {
883
+ const contentTypeUid = action.contentType;
884
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
885
+ if (!collectionTypeActions[contentTypeUid]) {
886
+ collectionTypeActions[contentTypeUid] = {
887
+ entriestoPublishIds: [],
888
+ entriesToUnpublishIds: []
889
+ };
890
+ }
891
+ if (action.type === "publish") {
892
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
893
+ } else {
894
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
895
+ }
626
896
  } else {
627
- await entityManagerService.unpublish(entry, uid);
628
- }
629
- } catch (error) {
630
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
631
- ;
632
- else {
633
- throw error;
897
+ singleTypeActions.push({
898
+ uid: contentTypeUid,
899
+ action: action.type,
900
+ id: action.entry.id
901
+ });
634
902
  }
635
903
  }
636
- }
637
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
638
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
639
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
640
- const entriesToPublish = await strapi2.entityService.findMany(
641
- contentTypeUid,
642
- {
643
- filters: {
644
- id: {
645
- $in: entriestoPublishIds
904
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
905
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
906
+ await strapi2.db.transaction(async () => {
907
+ for (const { uid, action, id } of singleTypeActions) {
908
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
909
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
910
+ try {
911
+ if (action === "publish") {
912
+ await entityManagerService.publish(entry, uid);
913
+ } else {
914
+ await entityManagerService.unpublish(entry, uid);
646
915
  }
647
- },
648
- populate
916
+ } catch (error) {
917
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
918
+ } else {
919
+ throw error;
920
+ }
921
+ }
649
922
  }
650
- );
651
- const entriesToUnpublish = await strapi2.entityService.findMany(
652
- contentTypeUid,
653
- {
654
- filters: {
655
- id: {
656
- $in: entriesToUnpublishIds
923
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
924
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
925
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
926
+ const entriesToPublish = await strapi2.entityService.findMany(
927
+ contentTypeUid,
928
+ {
929
+ filters: {
930
+ id: {
931
+ $in: entriestoPublishIds
932
+ }
933
+ },
934
+ populate
657
935
  }
658
- },
659
- populate
936
+ );
937
+ const entriesToUnpublish = await strapi2.entityService.findMany(
938
+ contentTypeUid,
939
+ {
940
+ filters: {
941
+ id: {
942
+ $in: entriesToUnpublishIds
943
+ }
944
+ },
945
+ populate
946
+ }
947
+ );
948
+ if (entriesToPublish.length > 0) {
949
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
950
+ }
951
+ if (entriesToUnpublish.length > 0) {
952
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
953
+ }
660
954
  }
661
- );
662
- if (entriesToPublish.length > 0) {
663
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
955
+ });
956
+ const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
957
+ data: {
958
+ /*
959
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
960
+ */
961
+ // @ts-expect-error see above
962
+ releasedAt: /* @__PURE__ */ new Date()
963
+ },
964
+ populate: {
965
+ actions: {
966
+ // @ts-expect-error is not expecting count but it is working
967
+ count: true
968
+ }
969
+ }
970
+ });
971
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
972
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
973
+ isPublished: true,
974
+ release: release2
975
+ });
664
976
  }
665
- if (entriesToUnpublish.length > 0) {
666
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
977
+ strapi2.telemetry.send("didPublishContentRelease");
978
+ return release2;
979
+ } catch (error) {
980
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
981
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
982
+ isPublished: false,
983
+ error
984
+ });
667
985
  }
986
+ strapi2.db.query(RELEASE_MODEL_UID).update({
987
+ where: { id: releaseId },
988
+ data: {
989
+ status: "failed"
990
+ }
991
+ });
992
+ throw error;
668
993
  }
669
- });
670
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
671
- data: {
672
- /*
673
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
674
- */
675
- // @ts-expect-error see above
676
- releasedAt: /* @__PURE__ */ new Date()
994
+ },
995
+ async updateAction(actionId, releaseId, update) {
996
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
997
+ where: {
998
+ id: actionId,
999
+ release: {
1000
+ id: releaseId,
1001
+ releasedAt: {
1002
+ $null: true
1003
+ }
1004
+ }
1005
+ },
1006
+ data: update
1007
+ });
1008
+ if (!updatedAction) {
1009
+ throw new errors.NotFoundError(
1010
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1011
+ );
677
1012
  }
678
- });
679
- return release2;
680
- },
681
- async updateAction(actionId, releaseId, update) {
682
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
683
- where: {
684
- id: actionId,
685
- release: {
686
- id: releaseId,
687
- releasedAt: {
688
- $null: true
1013
+ return updatedAction;
1014
+ },
1015
+ async deleteAction(actionId, releaseId) {
1016
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1017
+ where: {
1018
+ id: actionId,
1019
+ release: {
1020
+ id: releaseId,
1021
+ releasedAt: {
1022
+ $null: true
1023
+ }
689
1024
  }
690
1025
  }
691
- },
692
- data: update
693
- });
694
- if (!updatedAction) {
695
- throw new errors.NotFoundError(
696
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
697
- );
698
- }
699
- return updatedAction;
700
- },
701
- async deleteAction(actionId, releaseId) {
702
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
703
- where: {
704
- id: actionId,
705
- release: {
706
- id: releaseId,
707
- releasedAt: {
708
- $null: true
1026
+ });
1027
+ if (!deletedAction) {
1028
+ throw new errors.NotFoundError(
1029
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1030
+ );
1031
+ }
1032
+ this.updateReleaseStatus(releaseId);
1033
+ return deletedAction;
1034
+ },
1035
+ async updateReleaseStatus(releaseId) {
1036
+ const [totalActions, invalidActions] = await Promise.all([
1037
+ this.countActions({
1038
+ filters: {
1039
+ release: releaseId
709
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
+ });
710
1059
  }
1060
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1061
+ where: {
1062
+ id: releaseId
1063
+ },
1064
+ data: {
1065
+ status: "ready"
1066
+ }
1067
+ });
711
1068
  }
712
- });
713
- if (!deletedAction) {
714
- throw new errors.NotFoundError(
715
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
716
- );
1069
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1070
+ where: {
1071
+ id: releaseId
1072
+ },
1073
+ data: {
1074
+ status: "empty"
1075
+ }
1076
+ });
717
1077
  }
718
- return deletedAction;
719
- }
720
- });
1078
+ };
1079
+ };
721
1080
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
722
1081
  async validateUniqueEntry(releaseId, releaseActionArgs) {
723
1082
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -842,11 +1201,21 @@ const services = {
842
1201
  };
843
1202
  const RELEASE_SCHEMA = yup.object().shape({
844
1203
  name: yup.string().trim().required(),
845
- // scheduledAt is a date, but we always receive strings from the client
846
1204
  scheduledAt: yup.string().nullable(),
847
- timezone: yup.string().when("scheduledAt", {
848
- is: (scheduledAt) => !!scheduledAt,
849
- then: yup.string().required(),
1205
+ isScheduled: yup.boolean().optional(),
1206
+ time: yup.string().when("isScheduled", {
1207
+ is: true,
1208
+ then: yup.string().trim().required(),
1209
+ otherwise: yup.string().nullable()
1210
+ }),
1211
+ timezone: yup.string().when("isScheduled", {
1212
+ is: true,
1213
+ then: yup.string().required().nullable(),
1214
+ otherwise: yup.string().nullable()
1215
+ }),
1216
+ date: yup.string().when("isScheduled", {
1217
+ is: true,
1218
+ then: yup.string().required().nullable(),
850
1219
  otherwise: yup.string().nullable()
851
1220
  })
852
1221
  }).required().noUnknown();
@@ -881,7 +1250,12 @@ const releaseController = {
881
1250
  }
882
1251
  };
883
1252
  });
884
- 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 } };
885
1259
  }
886
1260
  },
887
1261
  async findOne(ctx) {
@@ -1252,6 +1626,9 @@ const getPlugin = () => {
1252
1626
  };
1253
1627
  }
1254
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
1255
1632
  contentTypes
1256
1633
  };
1257
1634
  };