@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,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,41 +336,94 @@ 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
+ }
157
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
+ }
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
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
164
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
165
- strapi2.log.error(
166
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
167
- );
168
- throw err;
169
- });
170
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
171
- strapi2.webhookStore.addAllowedEvent(key, value);
172
- });
173
- }
410
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
411
+ strapi2.log.error(
412
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
413
+ );
414
+ throw err;
415
+ });
416
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
417
+ strapi2.webhookStore.addAllowedEvent(key, value);
418
+ });
174
419
  }
175
420
  };
176
421
  const destroy = async ({ strapi: strapi2 }) => {
177
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
178
- const scheduledJobs = getService("scheduling", {
179
- strapi: strapi2
180
- }).getAll();
181
- for (const [, job] of scheduledJobs) {
182
- job.cancel();
183
- }
422
+ const scheduledJobs = getService("scheduling", {
423
+ strapi: strapi2
424
+ }).getAll();
425
+ for (const [, job] of scheduledJobs) {
426
+ job.cancel();
184
427
  }
185
428
  };
186
429
  const schema$1 = {
@@ -215,6 +458,11 @@ const schema$1 = {
215
458
  timezone: {
216
459
  type: "string"
217
460
  },
461
+ status: {
462
+ type: "enumeration",
463
+ enum: ["ready", "blocked", "failed", "done", "empty"],
464
+ required: true
465
+ },
218
466
  actions: {
219
467
  type: "relation",
220
468
  relation: "oneToMany",
@@ -267,6 +515,9 @@ const schema = {
267
515
  relation: "manyToOne",
268
516
  target: RELEASE_MODEL_UID,
269
517
  inversedBy: "actions"
518
+ },
519
+ isEntryValid: {
520
+ type: "boolean"
270
521
  }
271
522
  }
272
523
  };
@@ -297,6 +548,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
297
548
  release: release2
298
549
  });
299
550
  };
551
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
552
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
553
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
554
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
555
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
556
+ try {
557
+ if (actionType === "publish") {
558
+ await entityManagerService.publish(entry, uid);
559
+ } else {
560
+ await entityManagerService.unpublish(entry, uid);
561
+ }
562
+ } catch (error) {
563
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
564
+ ;
565
+ else {
566
+ throw error;
567
+ }
568
+ }
569
+ };
570
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
571
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
572
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
573
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
574
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
575
+ filters: {
576
+ id: {
577
+ $in: entriesToPublishIds
578
+ }
579
+ },
580
+ populate
581
+ });
582
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
583
+ filters: {
584
+ id: {
585
+ $in: entriestoUnpublishIds
586
+ }
587
+ },
588
+ populate
589
+ });
590
+ if (entriesToPublish.length > 0) {
591
+ await entityManagerService.publishMany(entriesToPublish, uid);
592
+ }
593
+ if (entriesToUnpublish.length > 0) {
594
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
595
+ }
596
+ };
597
+ const getFormattedActions = async (releaseId) => {
598
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
599
+ where: {
600
+ release: {
601
+ id: releaseId
602
+ }
603
+ },
604
+ populate: {
605
+ entry: {
606
+ fields: ["id"]
607
+ }
608
+ }
609
+ });
610
+ if (actions.length === 0) {
611
+ throw new utils.errors.ValidationError("No entries to publish");
612
+ }
613
+ const collectionTypeActions = {};
614
+ const singleTypeActions = [];
615
+ for (const action of actions) {
616
+ const contentTypeUid = action.contentType;
617
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
618
+ if (!collectionTypeActions[contentTypeUid]) {
619
+ collectionTypeActions[contentTypeUid] = {
620
+ entriesToPublishIds: [],
621
+ entriesToUnpublishIds: []
622
+ };
623
+ }
624
+ if (action.type === "publish") {
625
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
626
+ } else {
627
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
628
+ }
629
+ } else {
630
+ singleTypeActions.push({
631
+ uid: contentTypeUid,
632
+ action: action.type,
633
+ id: action.entry.id
634
+ });
635
+ }
636
+ }
637
+ return { collectionTypeActions, singleTypeActions };
638
+ };
300
639
  return {
301
640
  async create(releaseData, { user }) {
302
641
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -311,9 +650,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
311
650
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
651
  ]);
313
652
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
- data: releaseWithCreatorFields
653
+ data: {
654
+ ...releaseWithCreatorFields,
655
+ status: "empty"
656
+ }
315
657
  });
316
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
658
+ if (releaseWithCreatorFields.scheduledAt) {
317
659
  const schedulingService = getService("scheduling", { strapi: strapi2 });
318
660
  await schedulingService.set(release2.id, release2.scheduledAt);
319
661
  }
@@ -438,14 +780,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
438
780
  // @ts-expect-error see above
439
781
  data: releaseWithCreatorFields
440
782
  });
441
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
442
- const schedulingService = getService("scheduling", { strapi: strapi2 });
443
- if (releaseData.scheduledAt) {
444
- await schedulingService.set(id, releaseData.scheduledAt);
445
- } else if (release2.scheduledAt) {
446
- schedulingService.cancel(id);
447
- }
783
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
784
+ if (releaseData.scheduledAt) {
785
+ await schedulingService.set(id, releaseData.scheduledAt);
786
+ } else if (release2.scheduledAt) {
787
+ schedulingService.cancel(id);
448
788
  }
789
+ this.updateReleaseStatus(id);
449
790
  strapi2.telemetry.send("didUpdateContentRelease");
450
791
  return updatedRelease;
451
792
  },
@@ -465,11 +806,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
465
806
  throw new utils.errors.ValidationError("Release already published");
466
807
  }
467
808
  const { entry, type } = action;
468
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
809
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
810
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
811
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
469
812
  data: {
470
813
  type,
471
814
  contentType: entry.contentType,
472
815
  locale: entry.locale,
816
+ isEntryValid,
473
817
  entry: {
474
818
  id: entry.id,
475
819
  __type: entry.contentType,
@@ -479,6 +823,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
479
823
  },
480
824
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
481
825
  });
826
+ this.updateReleaseStatus(releaseId);
827
+ return releaseAction2;
482
828
  },
483
829
  async findActions(releaseId, query) {
484
830
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -604,7 +950,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
604
950
  });
605
951
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
606
952
  });
607
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
953
+ if (release2.scheduledAt) {
608
954
  const schedulingService = getService("scheduling", { strapi: strapi2 });
609
955
  await schedulingService.cancel(release2.id);
610
956
  }
@@ -612,139 +958,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
612
958
  return release2;
613
959
  },
614
960
  async publish(releaseId) {
615
- try {
616
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
617
- RELEASE_MODEL_UID,
618
- releaseId,
619
- {
620
- populate: {
621
- actions: {
622
- populate: {
623
- entry: {
624
- fields: ["id"]
625
- }
626
- }
627
- }
628
- }
629
- }
630
- );
631
- if (!releaseWithPopulatedActionEntries) {
961
+ const {
962
+ release: release2,
963
+ error
964
+ } = await strapi2.db.transaction(async ({ trx }) => {
965
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
966
+ if (!lockedRelease) {
632
967
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
633
968
  }
634
- if (releaseWithPopulatedActionEntries.releasedAt) {
969
+ if (lockedRelease.releasedAt) {
635
970
  throw new utils.errors.ValidationError("Release already published");
636
971
  }
637
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
- throw new utils.errors.ValidationError("No entries to publish");
639
- }
640
- const collectionTypeActions = {};
641
- const singleTypeActions = [];
642
- for (const action of releaseWithPopulatedActionEntries.actions) {
643
- const contentTypeUid = action.contentType;
644
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
645
- if (!collectionTypeActions[contentTypeUid]) {
646
- collectionTypeActions[contentTypeUid] = {
647
- entriestoPublishIds: [],
648
- entriesToUnpublishIds: []
649
- };
650
- }
651
- if (action.type === "publish") {
652
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
653
- } else {
654
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
655
- }
656
- } else {
657
- singleTypeActions.push({
658
- uid: contentTypeUid,
659
- action: action.type,
660
- id: action.entry.id
661
- });
662
- }
972
+ if (lockedRelease.status === "failed") {
973
+ throw new utils.errors.ValidationError("Release failed to publish");
663
974
  }
664
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
665
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
666
- await strapi2.db.transaction(async () => {
667
- for (const { uid, action, id } of singleTypeActions) {
668
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
669
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
670
- try {
671
- if (action === "publish") {
672
- await entityManagerService.publish(entry, uid);
673
- } else {
674
- await entityManagerService.unpublish(entry, uid);
675
- }
676
- } catch (error) {
677
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
678
- } else {
679
- throw error;
680
- }
681
- }
682
- }
683
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
684
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
685
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
686
- const entriesToPublish = await strapi2.entityService.findMany(
687
- contentTypeUid,
688
- {
689
- filters: {
690
- id: {
691
- $in: entriestoPublishIds
692
- }
693
- },
694
- populate
695
- }
696
- );
697
- const entriesToUnpublish = await strapi2.entityService.findMany(
698
- contentTypeUid,
699
- {
700
- filters: {
701
- id: {
702
- $in: entriesToUnpublishIds
703
- }
704
- },
705
- populate
706
- }
707
- );
708
- if (entriesToPublish.length > 0) {
709
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
975
+ try {
976
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
977
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
978
+ releaseId
979
+ );
980
+ await strapi2.db.transaction(async () => {
981
+ for (const { uid, action, id } of singleTypeActions) {
982
+ await publishSingleTypeAction(uid, action, id);
710
983
  }
711
- if (entriesToUnpublish.length > 0) {
712
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
984
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
985
+ const uid = contentTypeUid;
986
+ await publishCollectionTypeAction(
987
+ uid,
988
+ collectionTypeActions[uid].entriesToPublishIds,
989
+ collectionTypeActions[uid].entriesToUnpublishIds
990
+ );
713
991
  }
714
- }
715
- });
716
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
717
- data: {
718
- /*
719
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
720
- */
721
- // @ts-expect-error see above
722
- releasedAt: /* @__PURE__ */ new Date()
723
- },
724
- populate: {
725
- actions: {
726
- // @ts-expect-error is not expecting count but it is working
727
- count: true
992
+ });
993
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
994
+ where: {
995
+ id: releaseId
996
+ },
997
+ data: {
998
+ status: "done",
999
+ releasedAt: /* @__PURE__ */ new Date()
728
1000
  }
729
- }
730
- });
731
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1001
+ });
732
1002
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
733
1003
  isPublished: true,
734
- release: release2
1004
+ release: release22
735
1005
  });
736
- }
737
- strapi2.telemetry.send("didPublishContentRelease");
738
- return release2;
739
- } catch (error) {
740
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1006
+ strapi2.telemetry.send("didPublishContentRelease");
1007
+ return { release: release22, error: null };
1008
+ } catch (error2) {
741
1009
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
742
1010
  isPublished: false,
743
- error
1011
+ error: error2
744
1012
  });
1013
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1014
+ status: "failed"
1015
+ }).transacting(trx).execute();
1016
+ return {
1017
+ release: null,
1018
+ error: error2
1019
+ };
745
1020
  }
1021
+ });
1022
+ if (error) {
746
1023
  throw error;
747
1024
  }
1025
+ return release2;
748
1026
  },
749
1027
  async updateAction(actionId, releaseId, update) {
750
1028
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -783,10 +1061,60 @@ const createReleaseService = ({ strapi: strapi2 }) => {
783
1061
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
1062
  );
785
1063
  }
1064
+ this.updateReleaseStatus(releaseId);
786
1065
  return deletedAction;
1066
+ },
1067
+ async updateReleaseStatus(releaseId) {
1068
+ const [totalActions, invalidActions] = await Promise.all([
1069
+ this.countActions({
1070
+ filters: {
1071
+ release: releaseId
1072
+ }
1073
+ }),
1074
+ this.countActions({
1075
+ filters: {
1076
+ release: releaseId,
1077
+ isEntryValid: false
1078
+ }
1079
+ })
1080
+ ]);
1081
+ if (totalActions > 0) {
1082
+ if (invalidActions > 0) {
1083
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1084
+ where: {
1085
+ id: releaseId
1086
+ },
1087
+ data: {
1088
+ status: "blocked"
1089
+ }
1090
+ });
1091
+ }
1092
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1093
+ where: {
1094
+ id: releaseId
1095
+ },
1096
+ data: {
1097
+ status: "ready"
1098
+ }
1099
+ });
1100
+ }
1101
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1102
+ where: {
1103
+ id: releaseId
1104
+ },
1105
+ data: {
1106
+ status: "empty"
1107
+ }
1108
+ });
787
1109
  }
788
1110
  };
789
1111
  };
1112
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1113
+ constructor(message) {
1114
+ super(message);
1115
+ this.name = "AlreadyOnReleaseError";
1116
+ }
1117
+ }
790
1118
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
791
1119
  async validateUniqueEntry(releaseId, releaseActionArgs) {
792
1120
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -799,7 +1127,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
799
1127
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
800
1128
  );
801
1129
  if (isEntryInRelease) {
802
- throw new utils.errors.ValidationError(
1130
+ throw new AlreadyOnReleaseError(
803
1131
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
804
1132
  );
805
1133
  }
@@ -907,7 +1235,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
907
1235
  const services = {
908
1236
  release: createReleaseService,
909
1237
  "release-validation": createReleaseValidationService,
910
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1238
+ scheduling: createSchedulingService
911
1239
  };
912
1240
  const RELEASE_SCHEMA = yup__namespace.object().shape({
913
1241
  name: yup__namespace.string().trim().required(),
@@ -960,7 +1288,12 @@ const releaseController = {
960
1288
  }
961
1289
  };
962
1290
  });
963
- ctx.body = { data, meta: { pagination } };
1291
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1292
+ where: {
1293
+ releasedAt: null
1294
+ }
1295
+ });
1296
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
964
1297
  }
965
1298
  },
966
1299
  async findOne(ctx) {
@@ -1078,6 +1411,38 @@ const releaseActionController = {
1078
1411
  data: releaseAction2
1079
1412
  };
1080
1413
  },
1414
+ async createMany(ctx) {
1415
+ const releaseId = ctx.params.releaseId;
1416
+ const releaseActionsArgs = ctx.request.body;
1417
+ await Promise.all(
1418
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1419
+ );
1420
+ const releaseService = getService("release", { strapi });
1421
+ const releaseActions = await strapi.db.transaction(async () => {
1422
+ const releaseActions2 = await Promise.all(
1423
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1424
+ try {
1425
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1426
+ return action;
1427
+ } catch (error) {
1428
+ if (error instanceof AlreadyOnReleaseError) {
1429
+ return null;
1430
+ }
1431
+ throw error;
1432
+ }
1433
+ })
1434
+ );
1435
+ return releaseActions2;
1436
+ });
1437
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1438
+ ctx.body = {
1439
+ data: newReleaseActions,
1440
+ meta: {
1441
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1442
+ totalEntries: releaseActions.length
1443
+ }
1444
+ };
1445
+ },
1081
1446
  async findMany(ctx) {
1082
1447
  const releaseId = ctx.params.releaseId;
1083
1448
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1263,6 +1628,22 @@ const releaseAction = {
1263
1628
  ]
1264
1629
  }
1265
1630
  },
1631
+ {
1632
+ method: "POST",
1633
+ path: "/:releaseId/actions/bulk",
1634
+ handler: "release-action.createMany",
1635
+ config: {
1636
+ policies: [
1637
+ "admin::isAuthenticatedAdmin",
1638
+ {
1639
+ name: "admin::hasPermissions",
1640
+ config: {
1641
+ actions: ["plugin::content-releases.create-action"]
1642
+ }
1643
+ }
1644
+ ]
1645
+ }
1646
+ },
1266
1647
  {
1267
1648
  method: "GET",
1268
1649
  path: "/:releaseId/actions",
@@ -1331,6 +1712,9 @@ const getPlugin = () => {
1331
1712
  };
1332
1713
  }
1333
1714
  return {
1715
+ // Always return register, it handles its own feature check
1716
+ register,
1717
+ // Always return contentTypes to avoid losing data when the feature is disabled
1334
1718
  contentTypes
1335
1719
  };
1336
1720
  };