@strapi/content-releases 0.0.0-next.615ae85762cbae9fc80af36685075ef25abd1c88 → 0.0.0-next.6785f47fadddb80404c824be0a0732c652850b45

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-1hHIqUoZ.js → App-dLXY5ei3.js} +636 -382
  3. package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
  4. package/dist/_chunks/{App-U6GbyLIE.mjs → App-jrh58sXY.mjs} +647 -394
  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-bDhIlw-B.js → en-HrREghh3.js} +23 -6
  11. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  12. package/dist/_chunks/{en-GqXgfmzl.mjs → en-ltT1TlKQ.mjs} +23 -6
  13. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  14. package/dist/_chunks/{index-l-FvkQlQ.js → index-CVO0Rqdm.js} +385 -20
  15. package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
  16. package/dist/_chunks/{index-gkExFBa0.mjs → index-PiOGBETy.mjs} +401 -36
  17. package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.mjs +2 -2
  20. package/dist/server/index.js +948 -454
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +947 -454
  23. package/dist/server/index.mjs.map +1 -1
  24. package/package.json +15 -13
  25. package/dist/_chunks/App-1hHIqUoZ.js.map +0 -1
  26. package/dist/_chunks/App-U6GbyLIE.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-GqXgfmzl.mjs.map +0 -1
  30. package/dist/_chunks/en-bDhIlw-B.js.map +0 -1
  31. package/dist/_chunks/index-gkExFBa0.mjs.map +0 -1
  32. package/dist/_chunks/index-l-FvkQlQ.js.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);
@@ -73,6 +75,32 @@ const ACTIONS = [
73
75
  pluginName: "content-releases"
74
76
  }
75
77
  ];
78
+ const ALLOWED_WEBHOOK_EVENTS = {
79
+ RELEASES_PUBLISH: "releases.publish"
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
+ };
76
104
  async function deleteActionsOnDisableDraftAndPublish({
77
105
  oldContentTypes,
78
106
  contentTypes: contentTypes2
@@ -99,31 +127,202 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
99
127
  });
100
128
  }
101
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
+ }
102
280
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
103
281
  const register = async ({ strapi: strapi2 }) => {
104
282
  if (features$2.isEnabled("cms-content-releases")) {
105
283
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
106
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
107
- 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();
108
291
  }
109
- };
110
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
111
- return strapi2.plugin("content-releases").service(name);
112
292
  };
113
293
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
114
294
  const bootstrap = async ({ strapi: strapi2 }) => {
115
295
  if (features$1.isEnabled("cms-content-releases")) {
296
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
297
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
298
+ );
116
299
  strapi2.db.lifecycles.subscribe({
117
- afterDelete(event) {
118
- const { model, result } = event;
119
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
120
- const { id } = result;
121
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
122
- where: {
123
- target_type: model.uid,
124
- 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);
125
322
  }
126
- });
323
+ }
324
+ } catch (error) {
325
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
127
326
  }
128
327
  },
129
328
  /**
@@ -143,38 +342,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
143
342
  * We make this only after deleteMany is succesfully executed to avoid errors
144
343
  */
145
344
  async afterDeleteMany(event) {
146
- const { model, state } = event;
147
- const entriesToDelete = state.entriesToDelete;
148
- if (entriesToDelete) {
149
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
150
- where: {
151
- target_type: model.uid,
152
- target_id: {
153
- $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
+ }
154
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
+ }
367
+ }
368
+ });
369
+ for (const release2 of releases) {
370
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
155
371
  }
372
+ }
373
+ } catch (error) {
374
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
375
+ error
156
376
  });
157
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
+ }
158
414
  }
159
415
  });
160
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
161
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
162
- strapi2.log.error(
163
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
164
- );
165
- throw err;
166
- });
167
- }
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
+ });
168
425
  }
169
426
  };
170
427
  const destroy = async ({ strapi: strapi2 }) => {
171
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
172
- const scheduledJobs = getService("scheduling", {
173
- strapi: strapi2
174
- }).getAll();
175
- for (const [, job] of scheduledJobs) {
176
- job.cancel();
177
- }
428
+ const scheduledJobs = getService("scheduling", {
429
+ strapi: strapi2
430
+ }).getAll();
431
+ for (const [, job] of scheduledJobs) {
432
+ job.cancel();
178
433
  }
179
434
  };
180
435
  const schema$1 = {
@@ -209,6 +464,11 @@ const schema$1 = {
209
464
  timezone: {
210
465
  type: "string"
211
466
  },
467
+ status: {
468
+ type: "enumeration",
469
+ enum: ["ready", "blocked", "failed", "done", "empty"],
470
+ required: true
471
+ },
212
472
  actions: {
213
473
  type: "relation",
214
474
  relation: "oneToMany",
@@ -261,6 +521,9 @@ const schema = {
261
521
  relation: "manyToOne",
262
522
  target: RELEASE_MODEL_UID,
263
523
  inversedBy: "actions"
524
+ },
525
+ isEntryValid: {
526
+ type: "boolean"
264
527
  }
265
528
  }
266
529
  };
@@ -283,472 +546,594 @@ const getGroupName = (queryValue) => {
283
546
  return "contentType.displayName";
284
547
  }
285
548
  };
286
- const createReleaseService = ({ strapi: strapi2 }) => ({
287
- async create(releaseData, { user }) {
288
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
289
- const {
290
- validatePendingReleasesLimit,
291
- validateUniqueNameForPendingRelease,
292
- validateScheduledAtIsLaterThanNow
293
- } = getService("release-validation", { strapi: strapi2 });
294
- await Promise.all([
295
- validatePendingReleasesLimit(),
296
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
297
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
298
- ]);
299
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
300
- data: releaseWithCreatorFields
549
+ const createReleaseService = ({ strapi: strapi2 }) => {
550
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
551
+ strapi2.eventHub.emit(event, {
552
+ isPublished,
553
+ error,
554
+ release: release2
301
555
  });
302
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
303
- const schedulingService = getService("scheduling", { strapi: strapi2 });
304
- await schedulingService.set(release2.id, release2.scheduledAt);
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
+ }
305
574
  }
306
- strapi2.telemetry.send("didCreateContentRelease");
307
- return release2;
308
- },
309
- async findOne(id, query = {}) {
310
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
311
- ...query
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
312
587
  });
313
- return release2;
314
- },
315
- findPage(query) {
316
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
317
- ...query,
318
- populate: {
319
- actions: {
320
- // @ts-expect-error Ignore missing properties
321
- count: true
588
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
589
+ filters: {
590
+ id: {
591
+ $in: entriestoUnpublishIds
322
592
  }
323
- }
593
+ },
594
+ populate
324
595
  });
325
- },
326
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
327
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
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({
328
605
  where: {
329
- actions: {
330
- target_type: contentTypeUid,
331
- target_id: entryId
332
- },
333
- releasedAt: {
334
- $null: true
606
+ release: {
607
+ id: releaseId
335
608
  }
336
609
  },
337
610
  populate: {
338
- // Filter the action to get only the content type entry
339
- actions: {
340
- where: {
341
- target_type: contentTypeUid,
342
- target_id: entryId
343
- }
611
+ entry: {
612
+ fields: ["id"]
344
613
  }
345
614
  }
346
615
  });
347
- return releases.map((release2) => {
348
- if (release2.actions?.length) {
349
- const [actionForEntry] = release2.actions;
350
- delete release2.actions;
351
- return {
352
- ...release2,
353
- action: actionForEntry
354
- };
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
+ });
355
641
  }
642
+ }
643
+ return { collectionTypeActions, singleTypeActions };
644
+ };
645
+ return {
646
+ async create(releaseData, { user }) {
647
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
648
+ const {
649
+ validatePendingReleasesLimit,
650
+ validateUniqueNameForPendingRelease,
651
+ validateScheduledAtIsLaterThanNow
652
+ } = getService("release-validation", { strapi: strapi2 });
653
+ await Promise.all([
654
+ validatePendingReleasesLimit(),
655
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
656
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
657
+ ]);
658
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
659
+ data: {
660
+ ...releaseWithCreatorFields,
661
+ status: "empty"
662
+ }
663
+ });
664
+ if (releaseWithCreatorFields.scheduledAt) {
665
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
666
+ await schedulingService.set(release2.id, release2.scheduledAt);
667
+ }
668
+ strapi2.telemetry.send("didCreateContentRelease");
356
669
  return release2;
357
- });
358
- },
359
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
360
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
361
- where: {
362
- releasedAt: {
363
- $null: true
364
- },
365
- actions: {
366
- target_type: contentTypeUid,
367
- target_id: entryId
670
+ },
671
+ async findOne(id, query = {}) {
672
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
673
+ ...query
674
+ });
675
+ return release2;
676
+ },
677
+ findPage(query) {
678
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
679
+ ...query,
680
+ populate: {
681
+ actions: {
682
+ // @ts-expect-error Ignore missing properties
683
+ count: true
684
+ }
368
685
  }
686
+ });
687
+ },
688
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
+ let entries = entriesIds;
690
+ if (!Array.isArray(entriesIds)) {
691
+ entries = [entriesIds];
369
692
  }
370
- });
371
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
372
- where: {
373
- $or: [
374
- {
375
- id: {
376
- $notIn: releasesRelated.map((release2) => release2.id)
693
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
694
+ where: {
695
+ actions: {
696
+ target_type: contentTypeUid,
697
+ target_id: {
698
+ $in: entries
377
699
  }
378
700
  },
379
- {
380
- actions: null
701
+ releasedAt: {
702
+ $null: true
703
+ }
704
+ },
705
+ populate: {
706
+ // Filter the action to get only the content type entry
707
+ actions: {
708
+ where: {
709
+ target_type: contentTypeUid,
710
+ target_id: {
711
+ $in: entries
712
+ }
713
+ },
714
+ populate: {
715
+ entry: {
716
+ select: ["id"]
717
+ }
718
+ }
719
+ }
720
+ }
721
+ });
722
+ return releases.map((release2) => {
723
+ if (release2.actions?.length) {
724
+ const actionsForEntry = release2.actions;
725
+ delete release2.actions;
726
+ return {
727
+ ...release2,
728
+ actions: actionsForEntry
729
+ };
730
+ }
731
+ return release2;
732
+ });
733
+ },
734
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
735
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
736
+ where: {
737
+ releasedAt: {
738
+ $null: true
739
+ },
740
+ actions: {
741
+ target_type: contentTypeUid,
742
+ target_id: entryId
381
743
  }
382
- ],
383
- releasedAt: {
384
- $null: true
385
744
  }
745
+ });
746
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
747
+ where: {
748
+ $or: [
749
+ {
750
+ id: {
751
+ $notIn: releasesRelated.map((release2) => release2.id)
752
+ }
753
+ },
754
+ {
755
+ actions: null
756
+ }
757
+ ],
758
+ releasedAt: {
759
+ $null: true
760
+ }
761
+ }
762
+ });
763
+ return releases.map((release2) => {
764
+ if (release2.actions?.length) {
765
+ const [actionForEntry] = release2.actions;
766
+ delete release2.actions;
767
+ return {
768
+ ...release2,
769
+ action: actionForEntry
770
+ };
771
+ }
772
+ return release2;
773
+ });
774
+ },
775
+ async update(id, releaseData, { user }) {
776
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
777
+ releaseData
778
+ );
779
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
780
+ "release-validation",
781
+ { strapi: strapi2 }
782
+ );
783
+ await Promise.all([
784
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
785
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
786
+ ]);
787
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
788
+ if (!release2) {
789
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
386
790
  }
387
- });
388
- return releases.map((release2) => {
389
- if (release2.actions?.length) {
390
- const [actionForEntry] = release2.actions;
391
- delete release2.actions;
392
- return {
393
- ...release2,
394
- action: actionForEntry
395
- };
791
+ if (release2.releasedAt) {
792
+ throw new utils.errors.ValidationError("Release already published");
396
793
  }
397
- return release2;
398
- });
399
- },
400
- async update(id, releaseData, { user }) {
401
- const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
402
- const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
403
- "release-validation",
404
- { strapi: strapi2 }
405
- );
406
- await Promise.all([
407
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
408
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
409
- ]);
410
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
411
- if (!release2) {
412
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
413
- }
414
- if (release2.releasedAt) {
415
- throw new utils.errors.ValidationError("Release already published");
416
- }
417
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
418
- /*
419
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
420
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
421
- */
422
- // @ts-expect-error see above
423
- data: releaseWithCreatorFields
424
- });
425
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
794
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
795
+ /*
796
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
797
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
798
+ */
799
+ // @ts-expect-error see above
800
+ data: releaseWithCreatorFields
801
+ });
426
802
  const schedulingService = getService("scheduling", { strapi: strapi2 });
427
803
  if (releaseData.scheduledAt) {
428
804
  await schedulingService.set(id, releaseData.scheduledAt);
429
805
  } else if (release2.scheduledAt) {
430
806
  schedulingService.cancel(id);
431
807
  }
432
- }
433
- strapi2.telemetry.send("didUpdateContentRelease");
434
- return updatedRelease;
435
- },
436
- async createAction(releaseId, action) {
437
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
438
- strapi: strapi2
439
- });
440
- await Promise.all([
441
- validateEntryContentType(action.entry.contentType),
442
- validateUniqueEntry(releaseId, action)
443
- ]);
444
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
445
- if (!release2) {
446
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
447
- }
448
- if (release2.releasedAt) {
449
- throw new utils.errors.ValidationError("Release already published");
450
- }
451
- const { entry, type } = action;
452
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
453
- data: {
454
- type,
455
- contentType: entry.contentType,
456
- locale: entry.locale,
457
- entry: {
458
- id: entry.id,
459
- __type: entry.contentType,
460
- __pivot: { field: "entry" }
461
- },
462
- release: releaseId
463
- },
464
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
465
- });
466
- },
467
- async findActions(releaseId, query) {
468
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
469
- fields: ["id"]
470
- });
471
- if (!release2) {
472
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
473
- }
474
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
475
- ...query,
476
- populate: {
477
- entry: {
478
- populate: "*"
479
- }
480
- },
481
- filters: {
482
- release: releaseId
483
- }
484
- });
485
- },
486
- async countActions(query) {
487
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
488
- },
489
- async groupActions(actions, groupBy) {
490
- const contentTypeUids = actions.reduce((acc, action) => {
491
- if (!acc.includes(action.contentType)) {
492
- acc.push(action.contentType);
808
+ this.updateReleaseStatus(id);
809
+ strapi2.telemetry.send("didUpdateContentRelease");
810
+ return updatedRelease;
811
+ },
812
+ async createAction(releaseId, action) {
813
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
814
+ strapi: strapi2
815
+ });
816
+ await Promise.all([
817
+ validateEntryContentType(action.entry.contentType),
818
+ validateUniqueEntry(releaseId, action)
819
+ ]);
820
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
821
+ if (!release2) {
822
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
493
823
  }
494
- return acc;
495
- }, []);
496
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
497
- contentTypeUids
498
- );
499
- const allLocalesDictionary = await this.getLocalesDataForActions();
500
- const formattedData = actions.map((action) => {
501
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
502
- return {
503
- ...action,
504
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
505
- contentType: {
506
- displayName,
507
- mainFieldValue: action.entry[mainField],
508
- uid: action.contentType
509
- }
510
- };
511
- });
512
- const groupName = getGroupName(groupBy);
513
- return ___default.default.groupBy(groupName)(formattedData);
514
- },
515
- async getLocalesDataForActions() {
516
- if (!strapi2.plugin("i18n")) {
517
- return {};
518
- }
519
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
520
- return allLocales.reduce((acc, locale) => {
521
- acc[locale.code] = { name: locale.name, code: locale.code };
522
- return acc;
523
- }, {});
524
- },
525
- async getContentTypesDataForActions(contentTypesUids) {
526
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
527
- const contentTypesData = {};
528
- for (const contentTypeUid of contentTypesUids) {
529
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
530
- uid: contentTypeUid
824
+ if (release2.releasedAt) {
825
+ throw new utils.errors.ValidationError("Release already published");
826
+ }
827
+ const { entry, type } = action;
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, {
831
+ data: {
832
+ type,
833
+ contentType: entry.contentType,
834
+ locale: entry.locale,
835
+ isEntryValid,
836
+ entry: {
837
+ id: entry.id,
838
+ __type: entry.contentType,
839
+ __pivot: { field: "entry" }
840
+ },
841
+ release: releaseId
842
+ },
843
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
531
844
  });
532
- contentTypesData[contentTypeUid] = {
533
- mainField: contentTypeConfig.settings.mainField,
534
- displayName: strapi2.getModel(contentTypeUid).info.displayName
535
- };
536
- }
537
- return contentTypesData;
538
- },
539
- getContentTypeModelsFromActions(actions) {
540
- const contentTypeUids = actions.reduce((acc, action) => {
541
- if (!acc.includes(action.contentType)) {
542
- acc.push(action.contentType);
845
+ this.updateReleaseStatus(releaseId);
846
+ return releaseAction2;
847
+ },
848
+ async findActions(releaseId, query) {
849
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
850
+ fields: ["id"]
851
+ });
852
+ if (!release2) {
853
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
543
854
  }
544
- return acc;
545
- }, []);
546
- const contentTypeModelsMap = contentTypeUids.reduce(
547
- (acc, contentTypeUid) => {
548
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
855
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
856
+ ...query,
857
+ populate: {
858
+ entry: {
859
+ populate: "*"
860
+ }
861
+ },
862
+ filters: {
863
+ release: releaseId
864
+ }
865
+ });
866
+ },
867
+ async countActions(query) {
868
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
869
+ },
870
+ async groupActions(actions, groupBy) {
871
+ const contentTypeUids = actions.reduce((acc, action) => {
872
+ if (!acc.includes(action.contentType)) {
873
+ acc.push(action.contentType);
874
+ }
549
875
  return acc;
550
- },
551
- {}
552
- );
553
- return contentTypeModelsMap;
554
- },
555
- async getAllComponents() {
556
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
557
- const components = await contentManagerComponentsService.findAllComponents();
558
- const componentsMap = components.reduce(
559
- (acc, component) => {
560
- acc[component.uid] = component;
876
+ }, []);
877
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
878
+ contentTypeUids
879
+ );
880
+ const allLocalesDictionary = await this.getLocalesDataForActions();
881
+ const formattedData = actions.map((action) => {
882
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
883
+ return {
884
+ ...action,
885
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
886
+ contentType: {
887
+ displayName,
888
+ mainFieldValue: action.entry[mainField],
889
+ uid: action.contentType
890
+ }
891
+ };
892
+ });
893
+ const groupName = getGroupName(groupBy);
894
+ return ___default.default.groupBy(groupName)(formattedData);
895
+ },
896
+ async getLocalesDataForActions() {
897
+ if (!strapi2.plugin("i18n")) {
898
+ return {};
899
+ }
900
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
901
+ return allLocales.reduce((acc, locale) => {
902
+ acc[locale.code] = { name: locale.name, code: locale.code };
561
903
  return acc;
562
- },
563
- {}
564
- );
565
- return componentsMap;
566
- },
567
- async delete(releaseId) {
568
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
569
- populate: {
570
- actions: {
571
- fields: ["id"]
572
- }
904
+ }, {});
905
+ },
906
+ async getContentTypesDataForActions(contentTypesUids) {
907
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
908
+ const contentTypesData = {};
909
+ for (const contentTypeUid of contentTypesUids) {
910
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
911
+ uid: contentTypeUid
912
+ });
913
+ contentTypesData[contentTypeUid] = {
914
+ mainField: contentTypeConfig.settings.mainField,
915
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
916
+ };
573
917
  }
574
- });
575
- if (!release2) {
576
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
577
- }
578
- if (release2.releasedAt) {
579
- throw new utils.errors.ValidationError("Release already published");
580
- }
581
- await strapi2.db.transaction(async () => {
582
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
583
- where: {
584
- id: {
585
- $in: release2.actions.map((action) => action.id)
586
- }
918
+ return contentTypesData;
919
+ },
920
+ getContentTypeModelsFromActions(actions) {
921
+ const contentTypeUids = actions.reduce((acc, action) => {
922
+ if (!acc.includes(action.contentType)) {
923
+ acc.push(action.contentType);
587
924
  }
588
- });
589
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
590
- });
591
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
592
- const schedulingService = getService("scheduling", { strapi: strapi2 });
593
- await schedulingService.cancel(release2.id);
594
- }
595
- strapi2.telemetry.send("didDeleteContentRelease");
596
- return release2;
597
- },
598
- async publish(releaseId) {
599
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
600
- RELEASE_MODEL_UID,
601
- releaseId,
602
- {
925
+ return acc;
926
+ }, []);
927
+ const contentTypeModelsMap = contentTypeUids.reduce(
928
+ (acc, contentTypeUid) => {
929
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
930
+ return acc;
931
+ },
932
+ {}
933
+ );
934
+ return contentTypeModelsMap;
935
+ },
936
+ async getAllComponents() {
937
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
938
+ const components = await contentManagerComponentsService.findAllComponents();
939
+ const componentsMap = components.reduce(
940
+ (acc, component) => {
941
+ acc[component.uid] = component;
942
+ return acc;
943
+ },
944
+ {}
945
+ );
946
+ return componentsMap;
947
+ },
948
+ async delete(releaseId) {
949
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
603
950
  populate: {
604
951
  actions: {
605
- populate: {
606
- entry: {
607
- fields: ["id"]
608
- }
609
- }
952
+ fields: ["id"]
610
953
  }
611
954
  }
955
+ });
956
+ if (!release2) {
957
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
612
958
  }
613
- );
614
- if (!releaseWithPopulatedActionEntries) {
615
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
616
- }
617
- if (releaseWithPopulatedActionEntries.releasedAt) {
618
- throw new utils.errors.ValidationError("Release already published");
619
- }
620
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
621
- throw new utils.errors.ValidationError("No entries to publish");
622
- }
623
- const collectionTypeActions = {};
624
- const singleTypeActions = [];
625
- for (const action of releaseWithPopulatedActionEntries.actions) {
626
- const contentTypeUid = action.contentType;
627
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
628
- if (!collectionTypeActions[contentTypeUid]) {
629
- collectionTypeActions[contentTypeUid] = {
630
- entriestoPublishIds: [],
631
- entriesToUnpublishIds: []
632
- };
633
- }
634
- if (action.type === "publish") {
635
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
636
- } else {
637
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
638
- }
639
- } else {
640
- singleTypeActions.push({
641
- uid: contentTypeUid,
642
- action: action.type,
643
- id: action.entry.id
959
+ if (release2.releasedAt) {
960
+ throw new utils.errors.ValidationError("Release already published");
961
+ }
962
+ await strapi2.db.transaction(async () => {
963
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
964
+ where: {
965
+ id: {
966
+ $in: release2.actions.map((action) => action.id)
967
+ }
968
+ }
644
969
  });
970
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
971
+ });
972
+ if (release2.scheduledAt) {
973
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
974
+ await schedulingService.cancel(release2.id);
645
975
  }
646
- }
647
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
648
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
649
- await strapi2.db.transaction(async () => {
650
- for (const { uid, action, id } of singleTypeActions) {
651
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
652
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
976
+ strapi2.telemetry.send("didDeleteContentRelease");
977
+ return release2;
978
+ },
979
+ async publish(releaseId) {
980
+ const {
981
+ release: release2,
982
+ error
983
+ } = await strapi2.db.transaction(async ({ trx }) => {
984
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
985
+ if (!lockedRelease) {
986
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
987
+ }
988
+ if (lockedRelease.releasedAt) {
989
+ throw new utils.errors.ValidationError("Release already published");
990
+ }
991
+ if (lockedRelease.status === "failed") {
992
+ throw new utils.errors.ValidationError("Release failed to publish");
993
+ }
653
994
  try {
654
- if (action === "publish") {
655
- await entityManagerService.publish(entry, uid);
656
- } else {
657
- await entityManagerService.unpublish(entry, uid);
658
- }
659
- } catch (error) {
660
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
661
- ;
662
- else {
663
- throw error;
664
- }
995
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
996
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
997
+ releaseId
998
+ );
999
+ await strapi2.db.transaction(async () => {
1000
+ for (const { uid, action, id } of singleTypeActions) {
1001
+ await publishSingleTypeAction(uid, action, id);
1002
+ }
1003
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
1004
+ const uid = contentTypeUid;
1005
+ await publishCollectionTypeAction(
1006
+ uid,
1007
+ collectionTypeActions[uid].entriesToPublishIds,
1008
+ collectionTypeActions[uid].entriesToUnpublishIds
1009
+ );
1010
+ }
1011
+ });
1012
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
1013
+ where: {
1014
+ id: releaseId
1015
+ },
1016
+ data: {
1017
+ status: "done",
1018
+ releasedAt: /* @__PURE__ */ new Date()
1019
+ }
1020
+ });
1021
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1022
+ isPublished: true,
1023
+ release: release22
1024
+ });
1025
+ strapi2.telemetry.send("didPublishContentRelease");
1026
+ return { release: release22, error: null };
1027
+ } catch (error2) {
1028
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1029
+ isPublished: false,
1030
+ error: error2
1031
+ });
1032
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1033
+ status: "failed"
1034
+ }).transacting(trx).execute();
1035
+ return {
1036
+ release: null,
1037
+ error: error2
1038
+ };
665
1039
  }
1040
+ });
1041
+ if (error) {
1042
+ throw error;
666
1043
  }
667
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
668
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
669
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
670
- const entriesToPublish = await strapi2.entityService.findMany(
671
- contentTypeUid,
672
- {
673
- filters: {
674
- id: {
675
- $in: entriestoPublishIds
676
- }
677
- },
678
- populate
1044
+ return release2;
1045
+ },
1046
+ async updateAction(actionId, releaseId, update) {
1047
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1048
+ where: {
1049
+ id: actionId,
1050
+ release: {
1051
+ id: releaseId,
1052
+ releasedAt: {
1053
+ $null: true
1054
+ }
679
1055
  }
1056
+ },
1057
+ data: update
1058
+ });
1059
+ if (!updatedAction) {
1060
+ throw new utils.errors.NotFoundError(
1061
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
680
1062
  );
681
- const entriesToUnpublish = await strapi2.entityService.findMany(
682
- contentTypeUid,
683
- {
684
- filters: {
685
- id: {
686
- $in: entriesToUnpublishIds
687
- }
688
- },
689
- populate
1063
+ }
1064
+ return updatedAction;
1065
+ },
1066
+ async deleteAction(actionId, releaseId) {
1067
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1068
+ where: {
1069
+ id: actionId,
1070
+ release: {
1071
+ id: releaseId,
1072
+ releasedAt: {
1073
+ $null: true
1074
+ }
690
1075
  }
691
- );
692
- if (entriesToPublish.length > 0) {
693
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
694
- }
695
- if (entriesToUnpublish.length > 0) {
696
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
697
1076
  }
1077
+ });
1078
+ if (!deletedAction) {
1079
+ throw new utils.errors.NotFoundError(
1080
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1081
+ );
698
1082
  }
699
- });
700
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
701
- data: {
702
- /*
703
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
704
- */
705
- // @ts-expect-error see above
706
- releasedAt: /* @__PURE__ */ new Date()
707
- }
708
- });
709
- strapi2.telemetry.send("didPublishContentRelease");
710
- return release2;
711
- },
712
- async updateAction(actionId, releaseId, update) {
713
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
714
- where: {
715
- id: actionId,
716
- release: {
717
- id: releaseId,
718
- releasedAt: {
719
- $null: true
1083
+ this.updateReleaseStatus(releaseId);
1084
+ return deletedAction;
1085
+ },
1086
+ async updateReleaseStatus(releaseId) {
1087
+ const [totalActions, invalidActions] = await Promise.all([
1088
+ this.countActions({
1089
+ filters: {
1090
+ release: releaseId
720
1091
  }
721
- }
722
- },
723
- data: update
724
- });
725
- if (!updatedAction) {
726
- throw new utils.errors.NotFoundError(
727
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
728
- );
729
- }
730
- return updatedAction;
731
- },
732
- async deleteAction(actionId, releaseId) {
733
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
734
- where: {
735
- id: actionId,
736
- release: {
737
- id: releaseId,
738
- releasedAt: {
739
- $null: true
1092
+ }),
1093
+ this.countActions({
1094
+ filters: {
1095
+ release: releaseId,
1096
+ isEntryValid: false
740
1097
  }
1098
+ })
1099
+ ]);
1100
+ if (totalActions > 0) {
1101
+ if (invalidActions > 0) {
1102
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1103
+ where: {
1104
+ id: releaseId
1105
+ },
1106
+ data: {
1107
+ status: "blocked"
1108
+ }
1109
+ });
741
1110
  }
1111
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1112
+ where: {
1113
+ id: releaseId
1114
+ },
1115
+ data: {
1116
+ status: "ready"
1117
+ }
1118
+ });
742
1119
  }
743
- });
744
- if (!deletedAction) {
745
- throw new utils.errors.NotFoundError(
746
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
747
- );
1120
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1121
+ where: {
1122
+ id: releaseId
1123
+ },
1124
+ data: {
1125
+ status: "empty"
1126
+ }
1127
+ });
748
1128
  }
749
- return deletedAction;
1129
+ };
1130
+ };
1131
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1132
+ constructor(message) {
1133
+ super(message);
1134
+ this.name = "AlreadyOnReleaseError";
750
1135
  }
751
- });
1136
+ }
752
1137
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
753
1138
  async validateUniqueEntry(releaseId, releaseActionArgs) {
754
1139
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -761,7 +1146,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
761
1146
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
762
1147
  );
763
1148
  if (isEntryInRelease) {
764
- throw new utils.errors.ValidationError(
1149
+ throw new AlreadyOnReleaseError(
765
1150
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
766
1151
  );
767
1152
  }
@@ -869,15 +1254,25 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
869
1254
  const services = {
870
1255
  release: createReleaseService,
871
1256
  "release-validation": createReleaseValidationService,
872
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1257
+ scheduling: createSchedulingService
873
1258
  };
874
1259
  const RELEASE_SCHEMA = yup__namespace.object().shape({
875
1260
  name: yup__namespace.string().trim().required(),
876
- // scheduledAt is a date, but we always receive strings from the client
877
1261
  scheduledAt: yup__namespace.string().nullable(),
878
- timezone: yup__namespace.string().when("scheduledAt", {
879
- is: (scheduledAt) => !!scheduledAt,
880
- then: yup__namespace.string().required(),
1262
+ isScheduled: yup__namespace.boolean().optional(),
1263
+ time: yup__namespace.string().when("isScheduled", {
1264
+ is: true,
1265
+ then: yup__namespace.string().trim().required(),
1266
+ otherwise: yup__namespace.string().nullable()
1267
+ }),
1268
+ timezone: yup__namespace.string().when("isScheduled", {
1269
+ is: true,
1270
+ then: yup__namespace.string().required().nullable(),
1271
+ otherwise: yup__namespace.string().nullable()
1272
+ }),
1273
+ date: yup__namespace.string().when("isScheduled", {
1274
+ is: true,
1275
+ then: yup__namespace.string().required().nullable(),
881
1276
  otherwise: yup__namespace.string().nullable()
882
1277
  })
883
1278
  }).required().noUnknown();
@@ -912,7 +1307,12 @@ const releaseController = {
912
1307
  }
913
1308
  };
914
1309
  });
915
- ctx.body = { data, meta: { pagination } };
1310
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1311
+ where: {
1312
+ releasedAt: null
1313
+ }
1314
+ });
1315
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
916
1316
  }
917
1317
  },
918
1318
  async findOne(ctx) {
@@ -941,6 +1341,33 @@ const releaseController = {
941
1341
  };
942
1342
  ctx.body = { data };
943
1343
  },
1344
+ async mapEntriesToReleases(ctx) {
1345
+ const { contentTypeUid, entriesIds } = ctx.query;
1346
+ if (!contentTypeUid || !entriesIds) {
1347
+ throw new utils.errors.ValidationError("Missing required query parameters");
1348
+ }
1349
+ const releaseService = getService("release", { strapi });
1350
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1351
+ contentTypeUid,
1352
+ entriesIds
1353
+ );
1354
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1355
+ (acc, release2) => {
1356
+ release2.actions.forEach((action) => {
1357
+ if (!acc[action.entry.id]) {
1358
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1359
+ } else {
1360
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1361
+ }
1362
+ });
1363
+ return acc;
1364
+ },
1365
+ {}
1366
+ );
1367
+ ctx.body = {
1368
+ data: mappedEntriesInReleases
1369
+ };
1370
+ },
944
1371
  async create(ctx) {
945
1372
  const user = ctx.state.user;
946
1373
  const releaseArgs = ctx.request.body;
@@ -1030,6 +1457,38 @@ const releaseActionController = {
1030
1457
  data: releaseAction2
1031
1458
  };
1032
1459
  },
1460
+ async createMany(ctx) {
1461
+ const releaseId = ctx.params.releaseId;
1462
+ const releaseActionsArgs = ctx.request.body;
1463
+ await Promise.all(
1464
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1465
+ );
1466
+ const releaseService = getService("release", { strapi });
1467
+ const releaseActions = await strapi.db.transaction(async () => {
1468
+ const releaseActions2 = await Promise.all(
1469
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1470
+ try {
1471
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1472
+ return action;
1473
+ } catch (error) {
1474
+ if (error instanceof AlreadyOnReleaseError) {
1475
+ return null;
1476
+ }
1477
+ throw error;
1478
+ }
1479
+ })
1480
+ );
1481
+ return releaseActions2;
1482
+ });
1483
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1484
+ ctx.body = {
1485
+ data: newReleaseActions,
1486
+ meta: {
1487
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1488
+ totalEntries: releaseActions.length
1489
+ }
1490
+ };
1491
+ },
1033
1492
  async findMany(ctx) {
1034
1493
  const releaseId = ctx.params.releaseId;
1035
1494
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1098,6 +1557,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1098
1557
  const release = {
1099
1558
  type: "admin",
1100
1559
  routes: [
1560
+ {
1561
+ method: "GET",
1562
+ path: "/mapEntriesToReleases",
1563
+ handler: "release.mapEntriesToReleases",
1564
+ config: {
1565
+ policies: [
1566
+ "admin::isAuthenticatedAdmin",
1567
+ {
1568
+ name: "admin::hasPermissions",
1569
+ config: {
1570
+ actions: ["plugin::content-releases.read"]
1571
+ }
1572
+ }
1573
+ ]
1574
+ }
1575
+ },
1101
1576
  {
1102
1577
  method: "POST",
1103
1578
  path: "/",
@@ -1215,6 +1690,22 @@ const releaseAction = {
1215
1690
  ]
1216
1691
  }
1217
1692
  },
1693
+ {
1694
+ method: "POST",
1695
+ path: "/:releaseId/actions/bulk",
1696
+ handler: "release-action.createMany",
1697
+ config: {
1698
+ policies: [
1699
+ "admin::isAuthenticatedAdmin",
1700
+ {
1701
+ name: "admin::hasPermissions",
1702
+ config: {
1703
+ actions: ["plugin::content-releases.create-action"]
1704
+ }
1705
+ }
1706
+ ]
1707
+ }
1708
+ },
1218
1709
  {
1219
1710
  method: "GET",
1220
1711
  path: "/:releaseId/actions",
@@ -1283,6 +1774,9 @@ const getPlugin = () => {
1283
1774
  };
1284
1775
  }
1285
1776
  return {
1777
+ // Always return register, it handles its own feature check
1778
+ register,
1779
+ // Always return contentTypes to avoid losing data when the feature is disabled
1286
1780
  contentTypes
1287
1781
  };
1288
1782
  };