@strapi/content-releases 0.0.0-next.f8af92b375dc730ba47ed2117f25df893aae696c → 0.0.0-next.fc231041206e6f3999b094160cfa05db2892ad54

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,41 +312,94 @@ 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
+ }
134
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);
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
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
141
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
142
- strapi2.log.error(
143
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
144
- );
145
- throw err;
146
- });
147
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
148
- strapi2.webhookStore.addAllowedEvent(key, value);
149
- });
150
- }
386
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
387
+ strapi2.log.error(
388
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
389
+ );
390
+ throw err;
391
+ });
392
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
393
+ strapi2.webhookStore.addAllowedEvent(key, value);
394
+ });
151
395
  }
152
396
  };
153
397
  const destroy = async ({ strapi: strapi2 }) => {
154
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
155
- const scheduledJobs = getService("scheduling", {
156
- strapi: strapi2
157
- }).getAll();
158
- for (const [, job] of scheduledJobs) {
159
- job.cancel();
160
- }
398
+ const scheduledJobs = getService("scheduling", {
399
+ strapi: strapi2
400
+ }).getAll();
401
+ for (const [, job] of scheduledJobs) {
402
+ job.cancel();
161
403
  }
162
404
  };
163
405
  const schema$1 = {
@@ -192,6 +434,11 @@ const schema$1 = {
192
434
  timezone: {
193
435
  type: "string"
194
436
  },
437
+ status: {
438
+ type: "enumeration",
439
+ enum: ["ready", "blocked", "failed", "done", "empty"],
440
+ required: true
441
+ },
195
442
  actions: {
196
443
  type: "relation",
197
444
  relation: "oneToMany",
@@ -244,6 +491,9 @@ const schema = {
244
491
  relation: "manyToOne",
245
492
  target: RELEASE_MODEL_UID,
246
493
  inversedBy: "actions"
494
+ },
495
+ isEntryValid: {
496
+ type: "boolean"
247
497
  }
248
498
  }
249
499
  };
@@ -274,6 +524,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
274
524
  release: release2
275
525
  });
276
526
  };
527
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
528
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
529
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
530
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
531
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
532
+ try {
533
+ if (actionType === "publish") {
534
+ await entityManagerService.publish(entry, uid);
535
+ } else {
536
+ await entityManagerService.unpublish(entry, uid);
537
+ }
538
+ } catch (error) {
539
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
540
+ ;
541
+ else {
542
+ throw error;
543
+ }
544
+ }
545
+ };
546
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
547
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
548
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
549
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
550
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
551
+ filters: {
552
+ id: {
553
+ $in: entriesToPublishIds
554
+ }
555
+ },
556
+ populate
557
+ });
558
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
559
+ filters: {
560
+ id: {
561
+ $in: entriestoUnpublishIds
562
+ }
563
+ },
564
+ populate
565
+ });
566
+ if (entriesToPublish.length > 0) {
567
+ await entityManagerService.publishMany(entriesToPublish, uid);
568
+ }
569
+ if (entriesToUnpublish.length > 0) {
570
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
571
+ }
572
+ };
573
+ const getFormattedActions = async (releaseId) => {
574
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
575
+ where: {
576
+ release: {
577
+ id: releaseId
578
+ }
579
+ },
580
+ populate: {
581
+ entry: {
582
+ fields: ["id"]
583
+ }
584
+ }
585
+ });
586
+ if (actions.length === 0) {
587
+ throw new errors.ValidationError("No entries to publish");
588
+ }
589
+ const collectionTypeActions = {};
590
+ const singleTypeActions = [];
591
+ for (const action of actions) {
592
+ const contentTypeUid = action.contentType;
593
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
594
+ if (!collectionTypeActions[contentTypeUid]) {
595
+ collectionTypeActions[contentTypeUid] = {
596
+ entriesToPublishIds: [],
597
+ entriesToUnpublishIds: []
598
+ };
599
+ }
600
+ if (action.type === "publish") {
601
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
602
+ } else {
603
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
604
+ }
605
+ } else {
606
+ singleTypeActions.push({
607
+ uid: contentTypeUid,
608
+ action: action.type,
609
+ id: action.entry.id
610
+ });
611
+ }
612
+ }
613
+ return { collectionTypeActions, singleTypeActions };
614
+ };
277
615
  return {
278
616
  async create(releaseData, { user }) {
279
617
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@@ -288,9 +626,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
288
626
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
627
  ]);
290
628
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
- data: releaseWithCreatorFields
629
+ data: {
630
+ ...releaseWithCreatorFields,
631
+ status: "empty"
632
+ }
292
633
  });
293
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
634
+ if (releaseWithCreatorFields.scheduledAt) {
294
635
  const schedulingService = getService("scheduling", { strapi: strapi2 });
295
636
  await schedulingService.set(release2.id, release2.scheduledAt);
296
637
  }
@@ -415,14 +756,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
415
756
  // @ts-expect-error see above
416
757
  data: releaseWithCreatorFields
417
758
  });
418
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
419
- const schedulingService = getService("scheduling", { strapi: strapi2 });
420
- if (releaseData.scheduledAt) {
421
- await schedulingService.set(id, releaseData.scheduledAt);
422
- } else if (release2.scheduledAt) {
423
- schedulingService.cancel(id);
424
- }
759
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
760
+ if (releaseData.scheduledAt) {
761
+ await schedulingService.set(id, releaseData.scheduledAt);
762
+ } else if (release2.scheduledAt) {
763
+ schedulingService.cancel(id);
425
764
  }
765
+ this.updateReleaseStatus(id);
426
766
  strapi2.telemetry.send("didUpdateContentRelease");
427
767
  return updatedRelease;
428
768
  },
@@ -442,11 +782,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
442
782
  throw new errors.ValidationError("Release already published");
443
783
  }
444
784
  const { entry, type } = action;
445
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
785
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
786
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
787
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
446
788
  data: {
447
789
  type,
448
790
  contentType: entry.contentType,
449
791
  locale: entry.locale,
792
+ isEntryValid,
450
793
  entry: {
451
794
  id: entry.id,
452
795
  __type: entry.contentType,
@@ -456,6 +799,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
456
799
  },
457
800
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
801
  });
802
+ this.updateReleaseStatus(releaseId);
803
+ return releaseAction2;
459
804
  },
460
805
  async findActions(releaseId, query) {
461
806
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -581,7 +926,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
581
926
  });
582
927
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
583
928
  });
584
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
929
+ if (release2.scheduledAt) {
585
930
  const schedulingService = getService("scheduling", { strapi: strapi2 });
586
931
  await schedulingService.cancel(release2.id);
587
932
  }
@@ -589,139 +934,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
589
934
  return release2;
590
935
  },
591
936
  async publish(releaseId) {
592
- try {
593
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
594
- RELEASE_MODEL_UID,
595
- releaseId,
596
- {
597
- populate: {
598
- actions: {
599
- populate: {
600
- entry: {
601
- fields: ["id"]
602
- }
603
- }
604
- }
605
- }
606
- }
607
- );
608
- if (!releaseWithPopulatedActionEntries) {
937
+ const {
938
+ release: release2,
939
+ error
940
+ } = await strapi2.db.transaction(async ({ trx }) => {
941
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
942
+ if (!lockedRelease) {
609
943
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
610
944
  }
611
- if (releaseWithPopulatedActionEntries.releasedAt) {
945
+ if (lockedRelease.releasedAt) {
612
946
  throw new errors.ValidationError("Release already published");
613
947
  }
614
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
- throw new errors.ValidationError("No entries to publish");
616
- }
617
- const collectionTypeActions = {};
618
- const singleTypeActions = [];
619
- for (const action of releaseWithPopulatedActionEntries.actions) {
620
- const contentTypeUid = action.contentType;
621
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
622
- if (!collectionTypeActions[contentTypeUid]) {
623
- collectionTypeActions[contentTypeUid] = {
624
- entriestoPublishIds: [],
625
- entriesToUnpublishIds: []
626
- };
627
- }
628
- if (action.type === "publish") {
629
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
630
- } else {
631
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
632
- }
633
- } else {
634
- singleTypeActions.push({
635
- uid: contentTypeUid,
636
- action: action.type,
637
- id: action.entry.id
638
- });
639
- }
948
+ if (lockedRelease.status === "failed") {
949
+ throw new errors.ValidationError("Release failed to publish");
640
950
  }
641
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
642
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
643
- await strapi2.db.transaction(async () => {
644
- for (const { uid, action, id } of singleTypeActions) {
645
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
646
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
647
- try {
648
- if (action === "publish") {
649
- await entityManagerService.publish(entry, uid);
650
- } else {
651
- await entityManagerService.unpublish(entry, uid);
652
- }
653
- } catch (error) {
654
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
655
- } else {
656
- throw error;
657
- }
658
- }
659
- }
660
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
661
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
662
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
663
- const entriesToPublish = await strapi2.entityService.findMany(
664
- contentTypeUid,
665
- {
666
- filters: {
667
- id: {
668
- $in: entriestoPublishIds
669
- }
670
- },
671
- populate
672
- }
673
- );
674
- const entriesToUnpublish = await strapi2.entityService.findMany(
675
- contentTypeUid,
676
- {
677
- filters: {
678
- id: {
679
- $in: entriesToUnpublishIds
680
- }
681
- },
682
- populate
683
- }
684
- );
685
- if (entriesToPublish.length > 0) {
686
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
951
+ try {
952
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
953
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
954
+ releaseId
955
+ );
956
+ await strapi2.db.transaction(async () => {
957
+ for (const { uid, action, id } of singleTypeActions) {
958
+ await publishSingleTypeAction(uid, action, id);
687
959
  }
688
- if (entriesToUnpublish.length > 0) {
689
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
960
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
961
+ const uid = contentTypeUid;
962
+ await publishCollectionTypeAction(
963
+ uid,
964
+ collectionTypeActions[uid].entriesToPublishIds,
965
+ collectionTypeActions[uid].entriesToUnpublishIds
966
+ );
690
967
  }
691
- }
692
- });
693
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
694
- data: {
695
- /*
696
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
697
- */
698
- // @ts-expect-error see above
699
- releasedAt: /* @__PURE__ */ new Date()
700
- },
701
- populate: {
702
- actions: {
703
- // @ts-expect-error is not expecting count but it is working
704
- count: true
968
+ });
969
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
970
+ where: {
971
+ id: releaseId
972
+ },
973
+ data: {
974
+ status: "done",
975
+ releasedAt: /* @__PURE__ */ new Date()
705
976
  }
706
- }
707
- });
708
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
977
+ });
709
978
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
710
979
  isPublished: true,
711
- release: release2
980
+ release: release22
712
981
  });
713
- }
714
- strapi2.telemetry.send("didPublishContentRelease");
715
- return release2;
716
- } catch (error) {
717
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
982
+ strapi2.telemetry.send("didPublishContentRelease");
983
+ return { release: release22, error: null };
984
+ } catch (error2) {
718
985
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
719
986
  isPublished: false,
720
- error
987
+ error: error2
721
988
  });
989
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
990
+ status: "failed"
991
+ }).transacting(trx).execute();
992
+ return {
993
+ release: null,
994
+ error: error2
995
+ };
722
996
  }
997
+ });
998
+ if (error) {
723
999
  throw error;
724
1000
  }
1001
+ return release2;
725
1002
  },
726
1003
  async updateAction(actionId, releaseId, update) {
727
1004
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -760,10 +1037,60 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1037
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
1038
  );
762
1039
  }
1040
+ this.updateReleaseStatus(releaseId);
763
1041
  return deletedAction;
1042
+ },
1043
+ async updateReleaseStatus(releaseId) {
1044
+ const [totalActions, invalidActions] = await Promise.all([
1045
+ this.countActions({
1046
+ filters: {
1047
+ release: releaseId
1048
+ }
1049
+ }),
1050
+ this.countActions({
1051
+ filters: {
1052
+ release: releaseId,
1053
+ isEntryValid: false
1054
+ }
1055
+ })
1056
+ ]);
1057
+ if (totalActions > 0) {
1058
+ if (invalidActions > 0) {
1059
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1060
+ where: {
1061
+ id: releaseId
1062
+ },
1063
+ data: {
1064
+ status: "blocked"
1065
+ }
1066
+ });
1067
+ }
1068
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1069
+ where: {
1070
+ id: releaseId
1071
+ },
1072
+ data: {
1073
+ status: "ready"
1074
+ }
1075
+ });
1076
+ }
1077
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1078
+ where: {
1079
+ id: releaseId
1080
+ },
1081
+ data: {
1082
+ status: "empty"
1083
+ }
1084
+ });
764
1085
  }
765
1086
  };
766
1087
  };
1088
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1089
+ constructor(message) {
1090
+ super(message);
1091
+ this.name = "AlreadyOnReleaseError";
1092
+ }
1093
+ }
767
1094
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
768
1095
  async validateUniqueEntry(releaseId, releaseActionArgs) {
769
1096
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -776,7 +1103,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
776
1103
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
777
1104
  );
778
1105
  if (isEntryInRelease) {
779
- throw new errors.ValidationError(
1106
+ throw new AlreadyOnReleaseError(
780
1107
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
781
1108
  );
782
1109
  }
@@ -884,7 +1211,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
884
1211
  const services = {
885
1212
  release: createReleaseService,
886
1213
  "release-validation": createReleaseValidationService,
887
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1214
+ scheduling: createSchedulingService
888
1215
  };
889
1216
  const RELEASE_SCHEMA = yup.object().shape({
890
1217
  name: yup.string().trim().required(),
@@ -937,7 +1264,12 @@ const releaseController = {
937
1264
  }
938
1265
  };
939
1266
  });
940
- ctx.body = { data, meta: { pagination } };
1267
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1268
+ where: {
1269
+ releasedAt: null
1270
+ }
1271
+ });
1272
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
941
1273
  }
942
1274
  },
943
1275
  async findOne(ctx) {
@@ -1055,6 +1387,38 @@ const releaseActionController = {
1055
1387
  data: releaseAction2
1056
1388
  };
1057
1389
  },
1390
+ async createMany(ctx) {
1391
+ const releaseId = ctx.params.releaseId;
1392
+ const releaseActionsArgs = ctx.request.body;
1393
+ await Promise.all(
1394
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1395
+ );
1396
+ const releaseService = getService("release", { strapi });
1397
+ const releaseActions = await strapi.db.transaction(async () => {
1398
+ const releaseActions2 = await Promise.all(
1399
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1400
+ try {
1401
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1402
+ return action;
1403
+ } catch (error) {
1404
+ if (error instanceof AlreadyOnReleaseError) {
1405
+ return null;
1406
+ }
1407
+ throw error;
1408
+ }
1409
+ })
1410
+ );
1411
+ return releaseActions2;
1412
+ });
1413
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1414
+ ctx.body = {
1415
+ data: newReleaseActions,
1416
+ meta: {
1417
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1418
+ totalEntries: releaseActions.length
1419
+ }
1420
+ };
1421
+ },
1058
1422
  async findMany(ctx) {
1059
1423
  const releaseId = ctx.params.releaseId;
1060
1424
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1240,6 +1604,22 @@ const releaseAction = {
1240
1604
  ]
1241
1605
  }
1242
1606
  },
1607
+ {
1608
+ method: "POST",
1609
+ path: "/:releaseId/actions/bulk",
1610
+ handler: "release-action.createMany",
1611
+ config: {
1612
+ policies: [
1613
+ "admin::isAuthenticatedAdmin",
1614
+ {
1615
+ name: "admin::hasPermissions",
1616
+ config: {
1617
+ actions: ["plugin::content-releases.create-action"]
1618
+ }
1619
+ }
1620
+ ]
1621
+ }
1622
+ },
1243
1623
  {
1244
1624
  method: "GET",
1245
1625
  path: "/:releaseId/actions",
@@ -1308,6 +1688,9 @@ const getPlugin = () => {
1308
1688
  };
1309
1689
  }
1310
1690
  return {
1691
+ // Always return register, it handles its own feature check
1692
+ register,
1693
+ // Always return contentTypes to avoid losing data when the feature is disabled
1311
1694
  contentTypes
1312
1695
  };
1313
1696
  };