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

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 +629 -176
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +628 -176
  23. package/dist/server/index.mjs.map +1 -1
  24. package/package.json +15 -14
  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
+ }
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
+ }
134
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,18 +775,17 @@ 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
  },
429
- async createAction(releaseId, action) {
788
+ async createAction(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
430
789
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
431
790
  strapi: strapi2
432
791
  });
@@ -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,10 @@ const createReleaseService = ({ strapi: strapi2 }) => {
456
818
  },
457
819
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
820
  });
821
+ if (!disableUpdateReleaseStatus) {
822
+ this.updateReleaseStatus(releaseId);
823
+ }
824
+ return releaseAction2;
459
825
  },
460
826
  async findActions(releaseId, query) {
461
827
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -581,7 +947,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
581
947
  });
582
948
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
583
949
  });
584
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
950
+ if (release2.scheduledAt) {
585
951
  const schedulingService = getService("scheduling", { strapi: strapi2 });
586
952
  await schedulingService.cancel(release2.id);
587
953
  }
@@ -589,139 +955,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
589
955
  return release2;
590
956
  },
591
957
  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) {
958
+ const {
959
+ release: release2,
960
+ error
961
+ } = await strapi2.db.transaction(async ({ trx }) => {
962
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
963
+ if (!lockedRelease) {
609
964
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
610
965
  }
611
- if (releaseWithPopulatedActionEntries.releasedAt) {
966
+ if (lockedRelease.releasedAt) {
612
967
  throw new errors.ValidationError("Release already published");
613
968
  }
614
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
- throw new errors.ValidationError("No entries to publish");
969
+ if (lockedRelease.status === "failed") {
970
+ throw new errors.ValidationError("Release failed to publish");
616
971
  }
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);
972
+ try {
973
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
974
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
975
+ releaseId
976
+ );
977
+ await strapi2.db.transaction(async () => {
978
+ for (const { uid, action, id } of singleTypeActions) {
979
+ await publishSingleTypeAction(uid, action, id);
687
980
  }
688
- if (entriesToUnpublish.length > 0) {
689
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
981
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
982
+ const uid = contentTypeUid;
983
+ await publishCollectionTypeAction(
984
+ uid,
985
+ collectionTypeActions[uid].entriesToPublishIds,
986
+ collectionTypeActions[uid].entriesToUnpublishIds
987
+ );
690
988
  }
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
989
+ });
990
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
991
+ where: {
992
+ id: releaseId
993
+ },
994
+ data: {
995
+ status: "done",
996
+ releasedAt: /* @__PURE__ */ new Date()
705
997
  }
706
- }
707
- });
708
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
998
+ });
709
999
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
710
1000
  isPublished: true,
711
- release: release2
1001
+ release: release22
712
1002
  });
713
- }
714
- strapi2.telemetry.send("didPublishContentRelease");
715
- return release2;
716
- } catch (error) {
717
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1003
+ strapi2.telemetry.send("didPublishContentRelease");
1004
+ return { release: release22, error: null };
1005
+ } catch (error2) {
718
1006
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
719
1007
  isPublished: false,
720
- error
1008
+ error: error2
721
1009
  });
1010
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1011
+ status: "failed"
1012
+ }).transacting(trx).execute();
1013
+ return {
1014
+ release: null,
1015
+ error: error2
1016
+ };
722
1017
  }
1018
+ });
1019
+ if (error) {
723
1020
  throw error;
724
1021
  }
1022
+ return release2;
725
1023
  },
726
1024
  async updateAction(actionId, releaseId, update) {
727
1025
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -760,10 +1058,60 @@ const createReleaseService = ({ strapi: strapi2 }) => {
760
1058
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
1059
  );
762
1060
  }
1061
+ this.updateReleaseStatus(releaseId);
763
1062
  return deletedAction;
1063
+ },
1064
+ async updateReleaseStatus(releaseId) {
1065
+ const [totalActions, invalidActions] = await Promise.all([
1066
+ this.countActions({
1067
+ filters: {
1068
+ release: releaseId
1069
+ }
1070
+ }),
1071
+ this.countActions({
1072
+ filters: {
1073
+ release: releaseId,
1074
+ isEntryValid: false
1075
+ }
1076
+ })
1077
+ ]);
1078
+ if (totalActions > 0) {
1079
+ if (invalidActions > 0) {
1080
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1081
+ where: {
1082
+ id: releaseId
1083
+ },
1084
+ data: {
1085
+ status: "blocked"
1086
+ }
1087
+ });
1088
+ }
1089
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1090
+ where: {
1091
+ id: releaseId
1092
+ },
1093
+ data: {
1094
+ status: "ready"
1095
+ }
1096
+ });
1097
+ }
1098
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1099
+ where: {
1100
+ id: releaseId
1101
+ },
1102
+ data: {
1103
+ status: "empty"
1104
+ }
1105
+ });
764
1106
  }
765
1107
  };
766
1108
  };
1109
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1110
+ constructor(message) {
1111
+ super(message);
1112
+ this.name = "AlreadyOnReleaseError";
1113
+ }
1114
+ }
767
1115
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
768
1116
  async validateUniqueEntry(releaseId, releaseActionArgs) {
769
1117
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -776,7 +1124,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
776
1124
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
777
1125
  );
778
1126
  if (isEntryInRelease) {
779
- throw new errors.ValidationError(
1127
+ throw new AlreadyOnReleaseError(
780
1128
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
781
1129
  );
782
1130
  }
@@ -884,7 +1232,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
884
1232
  const services = {
885
1233
  release: createReleaseService,
886
1234
  "release-validation": createReleaseValidationService,
887
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1235
+ scheduling: createSchedulingService
888
1236
  };
889
1237
  const RELEASE_SCHEMA = yup.object().shape({
890
1238
  name: yup.string().trim().required(),
@@ -937,7 +1285,12 @@ const releaseController = {
937
1285
  }
938
1286
  };
939
1287
  });
940
- ctx.body = { data, meta: { pagination } };
1288
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1289
+ where: {
1290
+ releasedAt: null
1291
+ }
1292
+ });
1293
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
941
1294
  }
942
1295
  },
943
1296
  async findOne(ctx) {
@@ -966,6 +1319,33 @@ const releaseController = {
966
1319
  };
967
1320
  ctx.body = { data };
968
1321
  },
1322
+ async mapEntriesToReleases(ctx) {
1323
+ const { contentTypeUid, entriesIds } = ctx.query;
1324
+ if (!contentTypeUid || !entriesIds) {
1325
+ throw new errors.ValidationError("Missing required query parameters");
1326
+ }
1327
+ const releaseService = getService("release", { strapi });
1328
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1329
+ contentTypeUid,
1330
+ entriesIds
1331
+ );
1332
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1333
+ (acc, release2) => {
1334
+ release2.actions.forEach((action) => {
1335
+ if (!acc[action.entry.id]) {
1336
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1337
+ } else {
1338
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1339
+ }
1340
+ });
1341
+ return acc;
1342
+ },
1343
+ {}
1344
+ );
1345
+ ctx.body = {
1346
+ data: mappedEntriesInReleases
1347
+ };
1348
+ },
969
1349
  async create(ctx) {
970
1350
  const user = ctx.state.user;
971
1351
  const releaseArgs = ctx.request.body;
@@ -1055,6 +1435,43 @@ const releaseActionController = {
1055
1435
  data: releaseAction2
1056
1436
  };
1057
1437
  },
1438
+ async createMany(ctx) {
1439
+ const releaseId = ctx.params.releaseId;
1440
+ const releaseActionsArgs = ctx.request.body;
1441
+ await Promise.all(
1442
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1443
+ );
1444
+ const releaseService = getService("release", { strapi });
1445
+ const releaseActions = await strapi.db.transaction(async () => {
1446
+ const releaseActions2 = await Promise.all(
1447
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1448
+ try {
1449
+ const action = await releaseService.createAction(releaseId, releaseActionArgs, {
1450
+ disableUpdateReleaseStatus: true
1451
+ });
1452
+ return action;
1453
+ } catch (error) {
1454
+ if (error instanceof AlreadyOnReleaseError) {
1455
+ return null;
1456
+ }
1457
+ throw error;
1458
+ }
1459
+ })
1460
+ );
1461
+ return releaseActions2;
1462
+ });
1463
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1464
+ if (newReleaseActions.length > 0) {
1465
+ releaseService.updateReleaseStatus(releaseId);
1466
+ }
1467
+ ctx.body = {
1468
+ data: newReleaseActions,
1469
+ meta: {
1470
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1471
+ totalEntries: releaseActions.length
1472
+ }
1473
+ };
1474
+ },
1058
1475
  async findMany(ctx) {
1059
1476
  const releaseId = ctx.params.releaseId;
1060
1477
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1123,6 +1540,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1123
1540
  const release = {
1124
1541
  type: "admin",
1125
1542
  routes: [
1543
+ {
1544
+ method: "GET",
1545
+ path: "/mapEntriesToReleases",
1546
+ handler: "release.mapEntriesToReleases",
1547
+ config: {
1548
+ policies: [
1549
+ "admin::isAuthenticatedAdmin",
1550
+ {
1551
+ name: "admin::hasPermissions",
1552
+ config: {
1553
+ actions: ["plugin::content-releases.read"]
1554
+ }
1555
+ }
1556
+ ]
1557
+ }
1558
+ },
1126
1559
  {
1127
1560
  method: "POST",
1128
1561
  path: "/",
@@ -1240,6 +1673,22 @@ const releaseAction = {
1240
1673
  ]
1241
1674
  }
1242
1675
  },
1676
+ {
1677
+ method: "POST",
1678
+ path: "/:releaseId/actions/bulk",
1679
+ handler: "release-action.createMany",
1680
+ config: {
1681
+ policies: [
1682
+ "admin::isAuthenticatedAdmin",
1683
+ {
1684
+ name: "admin::hasPermissions",
1685
+ config: {
1686
+ actions: ["plugin::content-releases.create-action"]
1687
+ }
1688
+ }
1689
+ ]
1690
+ }
1691
+ },
1243
1692
  {
1244
1693
  method: "GET",
1245
1694
  path: "/:releaseId/actions",
@@ -1308,6 +1757,9 @@ const getPlugin = () => {
1308
1757
  };
1309
1758
  }
1310
1759
  return {
1760
+ // Always return register, it handles its own feature check
1761
+ register,
1762
+ // Always return contentTypes to avoid losing data when the feature is disabled
1311
1763
  contentTypes
1312
1764
  };
1313
1765
  };