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

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.
Files changed (32) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/{App-OK4Xac-O.js → App-dLXY5ei3.js} +677 -639
  3. package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
  4. package/dist/_chunks/{App-xAkiD42p.mjs → App-jrh58sXY.mjs} +690 -652
  5. package/dist/_chunks/App-jrh58sXY.mjs.map +1 -0
  6. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-3tRbmbY3.mjs} +2 -2
  7. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  8. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-bpIYXOfu.js} +2 -2
  9. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  10. package/dist/_chunks/{en-r0otWaln.js → en-HrREghh3.js} +14 -5
  11. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  12. package/dist/_chunks/{en-veqvqeEr.mjs → en-ltT1TlKQ.mjs} +14 -5
  13. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  14. package/dist/_chunks/{index-JvA2_26n.js → index-CVO0Rqdm.js} +343 -22
  15. package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
  16. package/dist/_chunks/{index-exoiSU3V.mjs → index-PiOGBETy.mjs} +358 -37
  17. package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.mjs +1 -1
  20. package/dist/server/index.js +621 -175
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +620 -175
  23. package/dist/server/index.mjs.map +1 -1
  24. package/package.json +13 -13
  25. package/dist/_chunks/App-OK4Xac-O.js.map +0 -1
  26. package/dist/_chunks/App-xAkiD42p.mjs.map +0 -1
  27. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  28. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  29. package/dist/_chunks/en-r0otWaln.js.map +0 -1
  30. package/dist/_chunks/en-veqvqeEr.mjs.map +0 -1
  31. package/dist/_chunks/index-JvA2_26n.js.map +0 -1
  32. package/dist/_chunks/index-exoiSU3V.mjs.map +0 -1
@@ -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,202 @@ 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
+ const i18nPlugin = strapi.plugin("i18n");
215
+ if (!i18nPlugin) {
216
+ return;
217
+ }
218
+ for (const uid in contentTypes2) {
219
+ if (!oldContentTypes[uid]) {
220
+ continue;
221
+ }
222
+ const oldContentType = oldContentTypes[uid];
223
+ const contentType = contentTypes2[uid];
224
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
225
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
226
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
227
+ locale: null
228
+ }).where({ contentType: uid }).execute();
229
+ }
230
+ }
231
+ }
232
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
233
+ if (!oldContentTypes) {
234
+ return;
235
+ }
236
+ const i18nPlugin = strapi.plugin("i18n");
237
+ if (!i18nPlugin) {
238
+ return;
239
+ }
240
+ for (const uid in contentTypes2) {
241
+ if (!oldContentTypes[uid]) {
242
+ continue;
243
+ }
244
+ const oldContentType = oldContentTypes[uid];
245
+ const contentType = contentTypes2[uid];
246
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
247
+ const { getDefaultLocale } = i18nPlugin.service("locales");
248
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
249
+ const defaultLocale = await getDefaultLocale();
250
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
251
+ locale: defaultLocale
252
+ }).where({ contentType: uid }).execute();
253
+ }
254
+ }
255
+ }
82
256
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
83
257
  const register = async ({ strapi: strapi2 }) => {
84
258
  if (features$2.isEnabled("cms-content-releases")) {
85
259
  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);
260
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
261
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
262
+ }
263
+ if (strapi2.plugin("graphql")) {
264
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
265
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
266
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
88
267
  }
89
- };
90
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
- return strapi2.plugin("content-releases").service(name);
92
268
  };
93
269
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
94
270
  const bootstrap = async ({ strapi: strapi2 }) => {
95
271
  if (features$1.isEnabled("cms-content-releases")) {
272
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
273
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
274
+ );
96
275
  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
276
+ models: contentTypesWithDraftAndPublish,
277
+ async afterDelete(event) {
278
+ try {
279
+ const { model, result } = event;
280
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
281
+ const { id } = result;
282
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
283
+ where: {
284
+ actions: {
285
+ target_type: model.uid,
286
+ target_id: id
287
+ }
288
+ }
289
+ });
290
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
291
+ where: {
292
+ target_type: model.uid,
293
+ target_id: id
294
+ }
295
+ });
296
+ for (const release2 of releases) {
297
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
105
298
  }
106
- });
299
+ }
300
+ } catch (error) {
301
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
107
302
  }
108
303
  },
109
304
  /**
@@ -123,41 +318,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
123
318
  * We make this only after deleteMany is succesfully executed to avoid errors
124
319
  */
125
320
  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)
321
+ try {
322
+ const { model, state } = event;
323
+ const entriesToDelete = state.entriesToDelete;
324
+ if (entriesToDelete) {
325
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
326
+ where: {
327
+ actions: {
328
+ target_type: model.uid,
329
+ target_id: {
330
+ $in: entriesToDelete.map(
331
+ (entry) => entry.id
332
+ )
333
+ }
334
+ }
134
335
  }
336
+ });
337
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
338
+ where: {
339
+ target_type: model.uid,
340
+ target_id: {
341
+ $in: entriesToDelete.map((entry) => entry.id)
342
+ }
343
+ }
344
+ });
345
+ for (const release2 of releases) {
346
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
135
347
  }
348
+ }
349
+ } catch (error) {
350
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
351
+ error
136
352
  });
137
353
  }
354
+ },
355
+ async afterUpdate(event) {
356
+ try {
357
+ const { model, result } = event;
358
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
359
+ const isEntryValid = await getEntryValidStatus(
360
+ model.uid,
361
+ result,
362
+ {
363
+ strapi: strapi2
364
+ }
365
+ );
366
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
367
+ where: {
368
+ target_type: model.uid,
369
+ target_id: result.id
370
+ },
371
+ data: {
372
+ isEntryValid
373
+ }
374
+ });
375
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
376
+ where: {
377
+ actions: {
378
+ target_type: model.uid,
379
+ target_id: result.id
380
+ }
381
+ }
382
+ });
383
+ for (const release2 of releases) {
384
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
385
+ }
386
+ }
387
+ } catch (error) {
388
+ strapi2.log.error("Error while updating release actions after entry update", { error });
389
+ }
138
390
  }
139
391
  });
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
- }
392
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
393
+ strapi2.log.error(
394
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
395
+ );
396
+ throw err;
397
+ });
398
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
399
+ strapi2.webhookStore.addAllowedEvent(key, value);
400
+ });
151
401
  }
152
402
  };
153
403
  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
- }
404
+ const scheduledJobs = getService("scheduling", {
405
+ strapi: strapi2
406
+ }).getAll();
407
+ for (const [, job] of scheduledJobs) {
408
+ job.cancel();
161
409
  }
162
410
  };
163
411
  const schema$1 = {
@@ -192,6 +440,11 @@ const schema$1 = {
192
440
  timezone: {
193
441
  type: "string"
194
442
  },
443
+ status: {
444
+ type: "enumeration",
445
+ enum: ["ready", "blocked", "failed", "done", "empty"],
446
+ required: true
447
+ },
195
448
  actions: {
196
449
  type: "relation",
197
450
  relation: "oneToMany",
@@ -244,6 +497,9 @@ const schema = {
244
497
  relation: "manyToOne",
245
498
  target: RELEASE_MODEL_UID,
246
499
  inversedBy: "actions"
500
+ },
501
+ isEntryValid: {
502
+ type: "boolean"
247
503
  }
248
504
  }
249
505
  };
@@ -274,6 +530,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
274
530
  release: release2
275
531
  });
276
532
  };
533
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
534
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
535
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
536
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
537
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
538
+ try {
539
+ if (actionType === "publish") {
540
+ await entityManagerService.publish(entry, uid);
541
+ } else {
542
+ await entityManagerService.unpublish(entry, uid);
543
+ }
544
+ } catch (error) {
545
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
546
+ ;
547
+ else {
548
+ throw error;
549
+ }
550
+ }
551
+ };
552
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
553
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
554
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
555
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
556
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
557
+ filters: {
558
+ id: {
559
+ $in: entriesToPublishIds
560
+ }
561
+ },
562
+ populate
563
+ });
564
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
565
+ filters: {
566
+ id: {
567
+ $in: entriestoUnpublishIds
568
+ }
569
+ },
570
+ populate
571
+ });
572
+ if (entriesToPublish.length > 0) {
573
+ await entityManagerService.publishMany(entriesToPublish, uid);
574
+ }
575
+ if (entriesToUnpublish.length > 0) {
576
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
577
+ }
578
+ };
579
+ const getFormattedActions = async (releaseId) => {
580
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
581
+ where: {
582
+ release: {
583
+ id: releaseId
584
+ }
585
+ },
586
+ populate: {
587
+ entry: {
588
+ fields: ["id"]
589
+ }
590
+ }
591
+ });
592
+ if (actions.length === 0) {
593
+ throw new errors.ValidationError("No entries to publish");
594
+ }
595
+ const collectionTypeActions = {};
596
+ const singleTypeActions = [];
597
+ for (const action of actions) {
598
+ const contentTypeUid = action.contentType;
599
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
600
+ if (!collectionTypeActions[contentTypeUid]) {
601
+ collectionTypeActions[contentTypeUid] = {
602
+ entriesToPublishIds: [],
603
+ entriesToUnpublishIds: []
604
+ };
605
+ }
606
+ if (action.type === "publish") {
607
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
608
+ } else {
609
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
610
+ }
611
+ } else {
612
+ singleTypeActions.push({
613
+ uid: contentTypeUid,
614
+ action: action.type,
615
+ id: action.entry.id
616
+ });
617
+ }
618
+ }
619
+ return { collectionTypeActions, singleTypeActions };
620
+ };
277
621
  return {
278
622
  async create(releaseData, { user }) {
279
623
  const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
@@ -288,9 +632,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
288
632
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
633
  ]);
290
634
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
- data: releaseWithCreatorFields
635
+ data: {
636
+ ...releaseWithCreatorFields,
637
+ status: "empty"
638
+ }
292
639
  });
293
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
640
+ if (releaseWithCreatorFields.scheduledAt) {
294
641
  const schedulingService = getService("scheduling", { strapi: strapi2 });
295
642
  await schedulingService.set(release2.id, release2.scheduledAt);
296
643
  }
@@ -314,12 +661,18 @@ const createReleaseService = ({ strapi: strapi2 }) => {
314
661
  }
315
662
  });
316
663
  },
317
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
664
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
665
+ let entries = entriesIds;
666
+ if (!Array.isArray(entriesIds)) {
667
+ entries = [entriesIds];
668
+ }
318
669
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
319
670
  where: {
320
671
  actions: {
321
672
  target_type: contentTypeUid,
322
- target_id: entryId
673
+ target_id: {
674
+ $in: entries
675
+ }
323
676
  },
324
677
  releasedAt: {
325
678
  $null: true
@@ -330,18 +683,25 @@ const createReleaseService = ({ strapi: strapi2 }) => {
330
683
  actions: {
331
684
  where: {
332
685
  target_type: contentTypeUid,
333
- target_id: entryId
686
+ target_id: {
687
+ $in: entries
688
+ }
689
+ },
690
+ populate: {
691
+ entry: {
692
+ select: ["id"]
693
+ }
334
694
  }
335
695
  }
336
696
  }
337
697
  });
338
698
  return releases.map((release2) => {
339
699
  if (release2.actions?.length) {
340
- const [actionForEntry] = release2.actions;
700
+ const actionsForEntry = release2.actions;
341
701
  delete release2.actions;
342
702
  return {
343
703
  ...release2,
344
- action: actionForEntry
704
+ actions: actionsForEntry
345
705
  };
346
706
  }
347
707
  return release2;
@@ -415,14 +775,13 @@ const createReleaseService = ({ strapi: strapi2 }) => {
415
775
  // @ts-expect-error see above
416
776
  data: releaseWithCreatorFields
417
777
  });
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
- }
778
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
779
+ if (releaseData.scheduledAt) {
780
+ await schedulingService.set(id, releaseData.scheduledAt);
781
+ } else if (release2.scheduledAt) {
782
+ schedulingService.cancel(id);
425
783
  }
784
+ this.updateReleaseStatus(id);
426
785
  strapi2.telemetry.send("didUpdateContentRelease");
427
786
  return updatedRelease;
428
787
  },
@@ -442,11 +801,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
442
801
  throw new errors.ValidationError("Release already published");
443
802
  }
444
803
  const { entry, type } = action;
445
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
804
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
805
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
806
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
446
807
  data: {
447
808
  type,
448
809
  contentType: entry.contentType,
449
810
  locale: entry.locale,
811
+ isEntryValid,
450
812
  entry: {
451
813
  id: entry.id,
452
814
  __type: entry.contentType,
@@ -456,6 +818,8 @@ const createReleaseService = ({ strapi: strapi2 }) => {
456
818
  },
457
819
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
820
  });
821
+ this.updateReleaseStatus(releaseId);
822
+ return releaseAction2;
459
823
  },
460
824
  async findActions(releaseId, query) {
461
825
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -581,7 +945,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
581
945
  });
582
946
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
583
947
  });
584
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
948
+ if (release2.scheduledAt) {
585
949
  const schedulingService = getService("scheduling", { strapi: strapi2 });
586
950
  await schedulingService.cancel(release2.id);
587
951
  }
@@ -589,139 +953,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
589
953
  return release2;
590
954
  },
591
955
  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) {
956
+ const {
957
+ release: release2,
958
+ error
959
+ } = await strapi2.db.transaction(async ({ trx }) => {
960
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
961
+ if (!lockedRelease) {
609
962
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
610
963
  }
611
- if (releaseWithPopulatedActionEntries.releasedAt) {
964
+ if (lockedRelease.releasedAt) {
612
965
  throw new errors.ValidationError("Release already published");
613
966
  }
614
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
- throw new errors.ValidationError("No entries to publish");
967
+ if (lockedRelease.status === "failed") {
968
+ throw new errors.ValidationError("Release failed to publish");
616
969
  }
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
- }
640
- }
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);
970
+ try {
971
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
972
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
973
+ releaseId
974
+ );
975
+ await strapi2.db.transaction(async () => {
976
+ for (const { uid, action, id } of singleTypeActions) {
977
+ await publishSingleTypeAction(uid, action, id);
687
978
  }
688
- if (entriesToUnpublish.length > 0) {
689
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
979
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
980
+ const uid = contentTypeUid;
981
+ await publishCollectionTypeAction(
982
+ uid,
983
+ collectionTypeActions[uid].entriesToPublishIds,
984
+ collectionTypeActions[uid].entriesToUnpublishIds
985
+ );
690
986
  }
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
987
+ });
988
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
989
+ where: {
990
+ id: releaseId
991
+ },
992
+ data: {
993
+ status: "done",
994
+ releasedAt: /* @__PURE__ */ new Date()
705
995
  }
706
- }
707
- });
708
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
996
+ });
709
997
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
710
998
  isPublished: true,
711
- release: release2
999
+ release: release22
712
1000
  });
713
- }
714
- strapi2.telemetry.send("didPublishContentRelease");
715
- return release2;
716
- } catch (error) {
717
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1001
+ strapi2.telemetry.send("didPublishContentRelease");
1002
+ return { release: release22, error: null };
1003
+ } catch (error2) {
718
1004
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
719
1005
  isPublished: false,
720
- error
1006
+ error: error2
721
1007
  });
1008
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1009
+ status: "failed"
1010
+ }).transacting(trx).execute();
1011
+ return {
1012
+ release: null,
1013
+ error: error2
1014
+ };
722
1015
  }
1016
+ });
1017
+ if (error) {
723
1018
  throw error;
724
1019
  }
1020
+ return release2;
725
1021
  },
726
1022
  async updateAction(actionId, releaseId, update) {
727
1023
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -760,10 +1056,60 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1056
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
1057
  );
762
1058
  }
1059
+ this.updateReleaseStatus(releaseId);
763
1060
  return deletedAction;
1061
+ },
1062
+ async updateReleaseStatus(releaseId) {
1063
+ const [totalActions, invalidActions] = await Promise.all([
1064
+ this.countActions({
1065
+ filters: {
1066
+ release: releaseId
1067
+ }
1068
+ }),
1069
+ this.countActions({
1070
+ filters: {
1071
+ release: releaseId,
1072
+ isEntryValid: false
1073
+ }
1074
+ })
1075
+ ]);
1076
+ if (totalActions > 0) {
1077
+ if (invalidActions > 0) {
1078
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1079
+ where: {
1080
+ id: releaseId
1081
+ },
1082
+ data: {
1083
+ status: "blocked"
1084
+ }
1085
+ });
1086
+ }
1087
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1088
+ where: {
1089
+ id: releaseId
1090
+ },
1091
+ data: {
1092
+ status: "ready"
1093
+ }
1094
+ });
1095
+ }
1096
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1097
+ where: {
1098
+ id: releaseId
1099
+ },
1100
+ data: {
1101
+ status: "empty"
1102
+ }
1103
+ });
764
1104
  }
765
1105
  };
766
1106
  };
1107
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1108
+ constructor(message) {
1109
+ super(message);
1110
+ this.name = "AlreadyOnReleaseError";
1111
+ }
1112
+ }
767
1113
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
768
1114
  async validateUniqueEntry(releaseId, releaseActionArgs) {
769
1115
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -776,7 +1122,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
776
1122
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
777
1123
  );
778
1124
  if (isEntryInRelease) {
779
- throw new errors.ValidationError(
1125
+ throw new AlreadyOnReleaseError(
780
1126
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
781
1127
  );
782
1128
  }
@@ -884,7 +1230,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
884
1230
  const services = {
885
1231
  release: createReleaseService,
886
1232
  "release-validation": createReleaseValidationService,
887
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1233
+ scheduling: createSchedulingService
888
1234
  };
889
1235
  const RELEASE_SCHEMA = yup.object().shape({
890
1236
  name: yup.string().trim().required(),
@@ -937,7 +1283,12 @@ const releaseController = {
937
1283
  }
938
1284
  };
939
1285
  });
940
- ctx.body = { data, meta: { pagination } };
1286
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1287
+ where: {
1288
+ releasedAt: null
1289
+ }
1290
+ });
1291
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
941
1292
  }
942
1293
  },
943
1294
  async findOne(ctx) {
@@ -966,6 +1317,33 @@ const releaseController = {
966
1317
  };
967
1318
  ctx.body = { data };
968
1319
  },
1320
+ async mapEntriesToReleases(ctx) {
1321
+ const { contentTypeUid, entriesIds } = ctx.query;
1322
+ if (!contentTypeUid || !entriesIds) {
1323
+ throw new errors.ValidationError("Missing required query parameters");
1324
+ }
1325
+ const releaseService = getService("release", { strapi });
1326
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1327
+ contentTypeUid,
1328
+ entriesIds
1329
+ );
1330
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1331
+ (acc, release2) => {
1332
+ release2.actions.forEach((action) => {
1333
+ if (!acc[action.entry.id]) {
1334
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1335
+ } else {
1336
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1337
+ }
1338
+ });
1339
+ return acc;
1340
+ },
1341
+ {}
1342
+ );
1343
+ ctx.body = {
1344
+ data: mappedEntriesInReleases
1345
+ };
1346
+ },
969
1347
  async create(ctx) {
970
1348
  const user = ctx.state.user;
971
1349
  const releaseArgs = ctx.request.body;
@@ -1055,6 +1433,38 @@ const releaseActionController = {
1055
1433
  data: releaseAction2
1056
1434
  };
1057
1435
  },
1436
+ async createMany(ctx) {
1437
+ const releaseId = ctx.params.releaseId;
1438
+ const releaseActionsArgs = ctx.request.body;
1439
+ await Promise.all(
1440
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1441
+ );
1442
+ const releaseService = getService("release", { strapi });
1443
+ const releaseActions = await strapi.db.transaction(async () => {
1444
+ const releaseActions2 = await Promise.all(
1445
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1446
+ try {
1447
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1448
+ return action;
1449
+ } catch (error) {
1450
+ if (error instanceof AlreadyOnReleaseError) {
1451
+ return null;
1452
+ }
1453
+ throw error;
1454
+ }
1455
+ })
1456
+ );
1457
+ return releaseActions2;
1458
+ });
1459
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1460
+ ctx.body = {
1461
+ data: newReleaseActions,
1462
+ meta: {
1463
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1464
+ totalEntries: releaseActions.length
1465
+ }
1466
+ };
1467
+ },
1058
1468
  async findMany(ctx) {
1059
1469
  const releaseId = ctx.params.releaseId;
1060
1470
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1123,6 +1533,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1123
1533
  const release = {
1124
1534
  type: "admin",
1125
1535
  routes: [
1536
+ {
1537
+ method: "GET",
1538
+ path: "/mapEntriesToReleases",
1539
+ handler: "release.mapEntriesToReleases",
1540
+ config: {
1541
+ policies: [
1542
+ "admin::isAuthenticatedAdmin",
1543
+ {
1544
+ name: "admin::hasPermissions",
1545
+ config: {
1546
+ actions: ["plugin::content-releases.read"]
1547
+ }
1548
+ }
1549
+ ]
1550
+ }
1551
+ },
1126
1552
  {
1127
1553
  method: "POST",
1128
1554
  path: "/",
@@ -1240,6 +1666,22 @@ const releaseAction = {
1240
1666
  ]
1241
1667
  }
1242
1668
  },
1669
+ {
1670
+ method: "POST",
1671
+ path: "/:releaseId/actions/bulk",
1672
+ handler: "release-action.createMany",
1673
+ config: {
1674
+ policies: [
1675
+ "admin::isAuthenticatedAdmin",
1676
+ {
1677
+ name: "admin::hasPermissions",
1678
+ config: {
1679
+ actions: ["plugin::content-releases.create-action"]
1680
+ }
1681
+ }
1682
+ ]
1683
+ }
1684
+ },
1243
1685
  {
1244
1686
  method: "GET",
1245
1687
  path: "/:releaseId/actions",
@@ -1308,6 +1750,9 @@ const getPlugin = () => {
1308
1750
  };
1309
1751
  }
1310
1752
  return {
1753
+ // Always return register, it handles its own feature check
1754
+ register,
1755
+ // Always return contentTypes to avoid losing data when the feature is disabled
1311
1756
  contentTypes
1312
1757
  };
1313
1758
  };