@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,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,202 @@ 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
+ const i18nPlugin = strapi.plugin("i18n");
239
+ if (!i18nPlugin) {
240
+ return;
241
+ }
242
+ for (const uid in contentTypes2) {
243
+ if (!oldContentTypes[uid]) {
244
+ continue;
245
+ }
246
+ const oldContentType = oldContentTypes[uid];
247
+ const contentType = contentTypes2[uid];
248
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
249
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
250
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
251
+ locale: null
252
+ }).where({ contentType: uid }).execute();
253
+ }
254
+ }
255
+ }
256
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
257
+ if (!oldContentTypes) {
258
+ return;
259
+ }
260
+ const i18nPlugin = strapi.plugin("i18n");
261
+ if (!i18nPlugin) {
262
+ return;
263
+ }
264
+ for (const uid in contentTypes2) {
265
+ if (!oldContentTypes[uid]) {
266
+ continue;
267
+ }
268
+ const oldContentType = oldContentTypes[uid];
269
+ const contentType = contentTypes2[uid];
270
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
271
+ const { getDefaultLocale } = i18nPlugin.service("locales");
272
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
273
+ const defaultLocale = await getDefaultLocale();
274
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
275
+ locale: defaultLocale
276
+ }).where({ contentType: uid }).execute();
277
+ }
278
+ }
279
+ }
105
280
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
106
281
  const register = async ({ strapi: strapi2 }) => {
107
282
  if (features$2.isEnabled("cms-content-releases")) {
108
283
  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);
284
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
285
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
286
+ }
287
+ if (strapi2.plugin("graphql")) {
288
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
289
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
290
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
111
291
  }
112
- };
113
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
114
- return strapi2.plugin("content-releases").service(name);
115
292
  };
116
293
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
117
294
  const bootstrap = async ({ strapi: strapi2 }) => {
118
295
  if (features$1.isEnabled("cms-content-releases")) {
296
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
297
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
298
+ );
119
299
  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
300
+ models: contentTypesWithDraftAndPublish,
301
+ async afterDelete(event) {
302
+ try {
303
+ const { model, result } = event;
304
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
305
+ const { id } = result;
306
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
307
+ where: {
308
+ actions: {
309
+ target_type: model.uid,
310
+ target_id: id
311
+ }
312
+ }
313
+ });
314
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
315
+ where: {
316
+ target_type: model.uid,
317
+ target_id: id
318
+ }
319
+ });
320
+ for (const release2 of releases) {
321
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
128
322
  }
129
- });
323
+ }
324
+ } catch (error) {
325
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
130
326
  }
131
327
  },
132
328
  /**
@@ -146,41 +342,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
146
342
  * We make this only after deleteMany is succesfully executed to avoid errors
147
343
  */
148
344
  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)
345
+ try {
346
+ const { model, state } = event;
347
+ const entriesToDelete = state.entriesToDelete;
348
+ if (entriesToDelete) {
349
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
350
+ where: {
351
+ actions: {
352
+ target_type: model.uid,
353
+ target_id: {
354
+ $in: entriesToDelete.map(
355
+ (entry) => entry.id
356
+ )
357
+ }
358
+ }
359
+ }
360
+ });
361
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
362
+ where: {
363
+ target_type: model.uid,
364
+ target_id: {
365
+ $in: entriesToDelete.map((entry) => entry.id)
366
+ }
157
367
  }
368
+ });
369
+ for (const release2 of releases) {
370
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
158
371
  }
372
+ }
373
+ } catch (error) {
374
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
375
+ error
159
376
  });
160
377
  }
378
+ },
379
+ async afterUpdate(event) {
380
+ try {
381
+ const { model, result } = event;
382
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
383
+ const isEntryValid = await getEntryValidStatus(
384
+ model.uid,
385
+ result,
386
+ {
387
+ strapi: strapi2
388
+ }
389
+ );
390
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
391
+ where: {
392
+ target_type: model.uid,
393
+ target_id: result.id
394
+ },
395
+ data: {
396
+ isEntryValid
397
+ }
398
+ });
399
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
400
+ where: {
401
+ actions: {
402
+ target_type: model.uid,
403
+ target_id: result.id
404
+ }
405
+ }
406
+ });
407
+ for (const release2 of releases) {
408
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
409
+ }
410
+ }
411
+ } catch (error) {
412
+ strapi2.log.error("Error while updating release actions after entry update", { error });
413
+ }
161
414
  }
162
415
  });
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
- }
416
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
417
+ strapi2.log.error(
418
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
419
+ );
420
+ throw err;
421
+ });
422
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
423
+ strapi2.webhookStore.addAllowedEvent(key, value);
424
+ });
174
425
  }
175
426
  };
176
427
  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
- }
428
+ const scheduledJobs = getService("scheduling", {
429
+ strapi: strapi2
430
+ }).getAll();
431
+ for (const [, job] of scheduledJobs) {
432
+ job.cancel();
184
433
  }
185
434
  };
186
435
  const schema$1 = {
@@ -215,6 +464,11 @@ const schema$1 = {
215
464
  timezone: {
216
465
  type: "string"
217
466
  },
467
+ status: {
468
+ type: "enumeration",
469
+ enum: ["ready", "blocked", "failed", "done", "empty"],
470
+ required: true
471
+ },
218
472
  actions: {
219
473
  type: "relation",
220
474
  relation: "oneToMany",
@@ -267,6 +521,9 @@ const schema = {
267
521
  relation: "manyToOne",
268
522
  target: RELEASE_MODEL_UID,
269
523
  inversedBy: "actions"
524
+ },
525
+ isEntryValid: {
526
+ type: "boolean"
270
527
  }
271
528
  }
272
529
  };
@@ -297,6 +554,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
297
554
  release: release2
298
555
  });
299
556
  };
557
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
558
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
559
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
560
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
561
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
562
+ try {
563
+ if (actionType === "publish") {
564
+ await entityManagerService.publish(entry, uid);
565
+ } else {
566
+ await entityManagerService.unpublish(entry, uid);
567
+ }
568
+ } catch (error) {
569
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
570
+ ;
571
+ else {
572
+ throw error;
573
+ }
574
+ }
575
+ };
576
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
577
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
578
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
579
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
580
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
581
+ filters: {
582
+ id: {
583
+ $in: entriesToPublishIds
584
+ }
585
+ },
586
+ populate
587
+ });
588
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
589
+ filters: {
590
+ id: {
591
+ $in: entriestoUnpublishIds
592
+ }
593
+ },
594
+ populate
595
+ });
596
+ if (entriesToPublish.length > 0) {
597
+ await entityManagerService.publishMany(entriesToPublish, uid);
598
+ }
599
+ if (entriesToUnpublish.length > 0) {
600
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
601
+ }
602
+ };
603
+ const getFormattedActions = async (releaseId) => {
604
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
605
+ where: {
606
+ release: {
607
+ id: releaseId
608
+ }
609
+ },
610
+ populate: {
611
+ entry: {
612
+ fields: ["id"]
613
+ }
614
+ }
615
+ });
616
+ if (actions.length === 0) {
617
+ throw new utils.errors.ValidationError("No entries to publish");
618
+ }
619
+ const collectionTypeActions = {};
620
+ const singleTypeActions = [];
621
+ for (const action of actions) {
622
+ const contentTypeUid = action.contentType;
623
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
624
+ if (!collectionTypeActions[contentTypeUid]) {
625
+ collectionTypeActions[contentTypeUid] = {
626
+ entriesToPublishIds: [],
627
+ entriesToUnpublishIds: []
628
+ };
629
+ }
630
+ if (action.type === "publish") {
631
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
632
+ } else {
633
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
634
+ }
635
+ } else {
636
+ singleTypeActions.push({
637
+ uid: contentTypeUid,
638
+ action: action.type,
639
+ id: action.entry.id
640
+ });
641
+ }
642
+ }
643
+ return { collectionTypeActions, singleTypeActions };
644
+ };
300
645
  return {
301
646
  async create(releaseData, { user }) {
302
647
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -311,9 +656,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
311
656
  validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
657
  ]);
313
658
  const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
- data: releaseWithCreatorFields
659
+ data: {
660
+ ...releaseWithCreatorFields,
661
+ status: "empty"
662
+ }
315
663
  });
316
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
664
+ if (releaseWithCreatorFields.scheduledAt) {
317
665
  const schedulingService = getService("scheduling", { strapi: strapi2 });
318
666
  await schedulingService.set(release2.id, release2.scheduledAt);
319
667
  }
@@ -337,12 +685,18 @@ const createReleaseService = ({ strapi: strapi2 }) => {
337
685
  }
338
686
  });
339
687
  },
340
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
688
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
+ let entries = entriesIds;
690
+ if (!Array.isArray(entriesIds)) {
691
+ entries = [entriesIds];
692
+ }
341
693
  const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
342
694
  where: {
343
695
  actions: {
344
696
  target_type: contentTypeUid,
345
- target_id: entryId
697
+ target_id: {
698
+ $in: entries
699
+ }
346
700
  },
347
701
  releasedAt: {
348
702
  $null: true
@@ -353,18 +707,25 @@ const createReleaseService = ({ strapi: strapi2 }) => {
353
707
  actions: {
354
708
  where: {
355
709
  target_type: contentTypeUid,
356
- target_id: entryId
710
+ target_id: {
711
+ $in: entries
712
+ }
713
+ },
714
+ populate: {
715
+ entry: {
716
+ select: ["id"]
717
+ }
357
718
  }
358
719
  }
359
720
  }
360
721
  });
361
722
  return releases.map((release2) => {
362
723
  if (release2.actions?.length) {
363
- const [actionForEntry] = release2.actions;
724
+ const actionsForEntry = release2.actions;
364
725
  delete release2.actions;
365
726
  return {
366
727
  ...release2,
367
- action: actionForEntry
728
+ actions: actionsForEntry
368
729
  };
369
730
  }
370
731
  return release2;
@@ -438,18 +799,17 @@ const createReleaseService = ({ strapi: strapi2 }) => {
438
799
  // @ts-expect-error see above
439
800
  data: releaseWithCreatorFields
440
801
  });
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
- }
802
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
803
+ if (releaseData.scheduledAt) {
804
+ await schedulingService.set(id, releaseData.scheduledAt);
805
+ } else if (release2.scheduledAt) {
806
+ schedulingService.cancel(id);
448
807
  }
808
+ this.updateReleaseStatus(id);
449
809
  strapi2.telemetry.send("didUpdateContentRelease");
450
810
  return updatedRelease;
451
811
  },
452
- async createAction(releaseId, action) {
812
+ async createAction(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
453
813
  const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
454
814
  strapi: strapi2
455
815
  });
@@ -465,11 +825,14 @@ const createReleaseService = ({ strapi: strapi2 }) => {
465
825
  throw new utils.errors.ValidationError("Release already published");
466
826
  }
467
827
  const { entry, type } = action;
468
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
828
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
829
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
830
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
469
831
  data: {
470
832
  type,
471
833
  contentType: entry.contentType,
472
834
  locale: entry.locale,
835
+ isEntryValid,
473
836
  entry: {
474
837
  id: entry.id,
475
838
  __type: entry.contentType,
@@ -479,6 +842,10 @@ const createReleaseService = ({ strapi: strapi2 }) => {
479
842
  },
480
843
  populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
481
844
  });
845
+ if (!disableUpdateReleaseStatus) {
846
+ this.updateReleaseStatus(releaseId);
847
+ }
848
+ return releaseAction2;
482
849
  },
483
850
  async findActions(releaseId, query) {
484
851
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -604,7 +971,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
604
971
  });
605
972
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
606
973
  });
607
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
974
+ if (release2.scheduledAt) {
608
975
  const schedulingService = getService("scheduling", { strapi: strapi2 });
609
976
  await schedulingService.cancel(release2.id);
610
977
  }
@@ -612,139 +979,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
612
979
  return release2;
613
980
  },
614
981
  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) {
982
+ const {
983
+ release: release2,
984
+ error
985
+ } = await strapi2.db.transaction(async ({ trx }) => {
986
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
987
+ if (!lockedRelease) {
632
988
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
633
989
  }
634
- if (releaseWithPopulatedActionEntries.releasedAt) {
990
+ if (lockedRelease.releasedAt) {
635
991
  throw new utils.errors.ValidationError("Release already published");
636
992
  }
637
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
- throw new utils.errors.ValidationError("No entries to publish");
993
+ if (lockedRelease.status === "failed") {
994
+ throw new utils.errors.ValidationError("Release failed to publish");
639
995
  }
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
- }
663
- }
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);
996
+ try {
997
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
998
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
999
+ releaseId
1000
+ );
1001
+ await strapi2.db.transaction(async () => {
1002
+ for (const { uid, action, id } of singleTypeActions) {
1003
+ await publishSingleTypeAction(uid, action, id);
710
1004
  }
711
- if (entriesToUnpublish.length > 0) {
712
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
1005
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
1006
+ const uid = contentTypeUid;
1007
+ await publishCollectionTypeAction(
1008
+ uid,
1009
+ collectionTypeActions[uid].entriesToPublishIds,
1010
+ collectionTypeActions[uid].entriesToUnpublishIds
1011
+ );
713
1012
  }
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
1013
+ });
1014
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
1015
+ where: {
1016
+ id: releaseId
1017
+ },
1018
+ data: {
1019
+ status: "done",
1020
+ releasedAt: /* @__PURE__ */ new Date()
728
1021
  }
729
- }
730
- });
731
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1022
+ });
732
1023
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
733
1024
  isPublished: true,
734
- release: release2
1025
+ release: release22
735
1026
  });
736
- }
737
- strapi2.telemetry.send("didPublishContentRelease");
738
- return release2;
739
- } catch (error) {
740
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1027
+ strapi2.telemetry.send("didPublishContentRelease");
1028
+ return { release: release22, error: null };
1029
+ } catch (error2) {
741
1030
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
742
1031
  isPublished: false,
743
- error
1032
+ error: error2
744
1033
  });
1034
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1035
+ status: "failed"
1036
+ }).transacting(trx).execute();
1037
+ return {
1038
+ release: null,
1039
+ error: error2
1040
+ };
745
1041
  }
1042
+ });
1043
+ if (error) {
746
1044
  throw error;
747
1045
  }
1046
+ return release2;
748
1047
  },
749
1048
  async updateAction(actionId, releaseId, update) {
750
1049
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -783,10 +1082,60 @@ const createReleaseService = ({ strapi: strapi2 }) => {
783
1082
  `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
1083
  );
785
1084
  }
1085
+ this.updateReleaseStatus(releaseId);
786
1086
  return deletedAction;
1087
+ },
1088
+ async updateReleaseStatus(releaseId) {
1089
+ const [totalActions, invalidActions] = await Promise.all([
1090
+ this.countActions({
1091
+ filters: {
1092
+ release: releaseId
1093
+ }
1094
+ }),
1095
+ this.countActions({
1096
+ filters: {
1097
+ release: releaseId,
1098
+ isEntryValid: false
1099
+ }
1100
+ })
1101
+ ]);
1102
+ if (totalActions > 0) {
1103
+ if (invalidActions > 0) {
1104
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1105
+ where: {
1106
+ id: releaseId
1107
+ },
1108
+ data: {
1109
+ status: "blocked"
1110
+ }
1111
+ });
1112
+ }
1113
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1114
+ where: {
1115
+ id: releaseId
1116
+ },
1117
+ data: {
1118
+ status: "ready"
1119
+ }
1120
+ });
1121
+ }
1122
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1123
+ where: {
1124
+ id: releaseId
1125
+ },
1126
+ data: {
1127
+ status: "empty"
1128
+ }
1129
+ });
787
1130
  }
788
1131
  };
789
1132
  };
1133
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1134
+ constructor(message) {
1135
+ super(message);
1136
+ this.name = "AlreadyOnReleaseError";
1137
+ }
1138
+ }
790
1139
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
791
1140
  async validateUniqueEntry(releaseId, releaseActionArgs) {
792
1141
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -799,7 +1148,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
799
1148
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
800
1149
  );
801
1150
  if (isEntryInRelease) {
802
- throw new utils.errors.ValidationError(
1151
+ throw new AlreadyOnReleaseError(
803
1152
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
804
1153
  );
805
1154
  }
@@ -907,7 +1256,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
907
1256
  const services = {
908
1257
  release: createReleaseService,
909
1258
  "release-validation": createReleaseValidationService,
910
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1259
+ scheduling: createSchedulingService
911
1260
  };
912
1261
  const RELEASE_SCHEMA = yup__namespace.object().shape({
913
1262
  name: yup__namespace.string().trim().required(),
@@ -960,7 +1309,12 @@ const releaseController = {
960
1309
  }
961
1310
  };
962
1311
  });
963
- ctx.body = { data, meta: { pagination } };
1312
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1313
+ where: {
1314
+ releasedAt: null
1315
+ }
1316
+ });
1317
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
964
1318
  }
965
1319
  },
966
1320
  async findOne(ctx) {
@@ -989,6 +1343,33 @@ const releaseController = {
989
1343
  };
990
1344
  ctx.body = { data };
991
1345
  },
1346
+ async mapEntriesToReleases(ctx) {
1347
+ const { contentTypeUid, entriesIds } = ctx.query;
1348
+ if (!contentTypeUid || !entriesIds) {
1349
+ throw new utils.errors.ValidationError("Missing required query parameters");
1350
+ }
1351
+ const releaseService = getService("release", { strapi });
1352
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1353
+ contentTypeUid,
1354
+ entriesIds
1355
+ );
1356
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1357
+ (acc, release2) => {
1358
+ release2.actions.forEach((action) => {
1359
+ if (!acc[action.entry.id]) {
1360
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1361
+ } else {
1362
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1363
+ }
1364
+ });
1365
+ return acc;
1366
+ },
1367
+ {}
1368
+ );
1369
+ ctx.body = {
1370
+ data: mappedEntriesInReleases
1371
+ };
1372
+ },
992
1373
  async create(ctx) {
993
1374
  const user = ctx.state.user;
994
1375
  const releaseArgs = ctx.request.body;
@@ -1078,6 +1459,43 @@ const releaseActionController = {
1078
1459
  data: releaseAction2
1079
1460
  };
1080
1461
  },
1462
+ async createMany(ctx) {
1463
+ const releaseId = ctx.params.releaseId;
1464
+ const releaseActionsArgs = ctx.request.body;
1465
+ await Promise.all(
1466
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1467
+ );
1468
+ const releaseService = getService("release", { strapi });
1469
+ const releaseActions = await strapi.db.transaction(async () => {
1470
+ const releaseActions2 = await Promise.all(
1471
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1472
+ try {
1473
+ const action = await releaseService.createAction(releaseId, releaseActionArgs, {
1474
+ disableUpdateReleaseStatus: true
1475
+ });
1476
+ return action;
1477
+ } catch (error) {
1478
+ if (error instanceof AlreadyOnReleaseError) {
1479
+ return null;
1480
+ }
1481
+ throw error;
1482
+ }
1483
+ })
1484
+ );
1485
+ return releaseActions2;
1486
+ });
1487
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1488
+ if (newReleaseActions.length > 0) {
1489
+ releaseService.updateReleaseStatus(releaseId);
1490
+ }
1491
+ ctx.body = {
1492
+ data: newReleaseActions,
1493
+ meta: {
1494
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1495
+ totalEntries: releaseActions.length
1496
+ }
1497
+ };
1498
+ },
1081
1499
  async findMany(ctx) {
1082
1500
  const releaseId = ctx.params.releaseId;
1083
1501
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1146,6 +1564,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1146
1564
  const release = {
1147
1565
  type: "admin",
1148
1566
  routes: [
1567
+ {
1568
+ method: "GET",
1569
+ path: "/mapEntriesToReleases",
1570
+ handler: "release.mapEntriesToReleases",
1571
+ config: {
1572
+ policies: [
1573
+ "admin::isAuthenticatedAdmin",
1574
+ {
1575
+ name: "admin::hasPermissions",
1576
+ config: {
1577
+ actions: ["plugin::content-releases.read"]
1578
+ }
1579
+ }
1580
+ ]
1581
+ }
1582
+ },
1149
1583
  {
1150
1584
  method: "POST",
1151
1585
  path: "/",
@@ -1263,6 +1697,22 @@ const releaseAction = {
1263
1697
  ]
1264
1698
  }
1265
1699
  },
1700
+ {
1701
+ method: "POST",
1702
+ path: "/:releaseId/actions/bulk",
1703
+ handler: "release-action.createMany",
1704
+ config: {
1705
+ policies: [
1706
+ "admin::isAuthenticatedAdmin",
1707
+ {
1708
+ name: "admin::hasPermissions",
1709
+ config: {
1710
+ actions: ["plugin::content-releases.create-action"]
1711
+ }
1712
+ }
1713
+ ]
1714
+ }
1715
+ },
1266
1716
  {
1267
1717
  method: "GET",
1268
1718
  path: "/:releaseId/actions",
@@ -1331,6 +1781,9 @@ const getPlugin = () => {
1331
1781
  };
1332
1782
  }
1333
1783
  return {
1784
+ // Always return register, it handles its own feature check
1785
+ register,
1786
+ // Always return contentTypes to avoid losing data when the feature is disabled
1334
1787
  contentTypes
1335
1788
  };
1336
1789
  };