@strapi/content-releases 0.0.0-next.d470b4f75cf00f24f440b80300f1c833c322b871 → 0.0.0-next.d773bfffcd285e7a1e7bf331b6f1224b969dd46b

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 +953 -448
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +952 -448
  23. package/dist/server/index.mjs.map +1 -1
  24. package/package.json +16 -14
  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,468 +546,596 @@ 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
- return release2;
307
- },
308
- async findOne(id, query = {}) {
309
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
310
- ...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
311
587
  });
312
- return release2;
313
- },
314
- findPage(query) {
315
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
316
- ...query,
317
- populate: {
318
- actions: {
319
- // @ts-expect-error Ignore missing properties
320
- count: true
588
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
589
+ filters: {
590
+ id: {
591
+ $in: entriestoUnpublishIds
321
592
  }
322
- }
593
+ },
594
+ populate
323
595
  });
324
- },
325
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
326
- 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({
327
605
  where: {
328
- actions: {
329
- target_type: contentTypeUid,
330
- target_id: entryId
331
- },
332
- releasedAt: {
333
- $null: true
606
+ release: {
607
+ id: releaseId
334
608
  }
335
609
  },
336
610
  populate: {
337
- // Filter the action to get only the content type entry
338
- actions: {
339
- where: {
340
- target_type: contentTypeUid,
341
- target_id: entryId
342
- }
611
+ entry: {
612
+ fields: ["id"]
343
613
  }
344
614
  }
345
615
  });
346
- return releases.map((release2) => {
347
- if (release2.actions?.length) {
348
- const [actionForEntry] = release2.actions;
349
- delete release2.actions;
350
- return {
351
- ...release2,
352
- action: actionForEntry
353
- };
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
+ });
354
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");
355
669
  return release2;
356
- });
357
- },
358
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
359
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
360
- where: {
361
- releasedAt: {
362
- $null: true
363
- },
364
- actions: {
365
- target_type: contentTypeUid,
366
- 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
+ }
367
685
  }
686
+ });
687
+ },
688
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
+ let entries = entriesIds;
690
+ if (!Array.isArray(entriesIds)) {
691
+ entries = [entriesIds];
368
692
  }
369
- });
370
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
371
- where: {
372
- $or: [
373
- {
374
- id: {
375
- $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
376
699
  }
377
700
  },
378
- {
379
- actions: null
701
+ releasedAt: {
702
+ $null: true
380
703
  }
381
- ],
382
- releasedAt: {
383
- $null: true
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
+ };
384
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
743
+ }
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}`);
385
790
  }
386
- });
387
- return releases.map((release2) => {
388
- if (release2.actions?.length) {
389
- const [actionForEntry] = release2.actions;
390
- delete release2.actions;
391
- return {
392
- ...release2,
393
- action: actionForEntry
394
- };
791
+ if (release2.releasedAt) {
792
+ throw new utils.errors.ValidationError("Release already published");
395
793
  }
396
- return release2;
397
- });
398
- },
399
- async update(id, releaseData, { user }) {
400
- const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
401
- const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
402
- "release-validation",
403
- { strapi: strapi2 }
404
- );
405
- await Promise.all([
406
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
407
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
408
- ]);
409
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
410
- if (!release2) {
411
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
412
- }
413
- if (release2.releasedAt) {
414
- throw new utils.errors.ValidationError("Release already published");
415
- }
416
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
417
- /*
418
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
419
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
420
- */
421
- // @ts-expect-error see above
422
- data: releaseWithCreatorFields
423
- });
424
- 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
+ });
425
802
  const schedulingService = getService("scheduling", { strapi: strapi2 });
426
803
  if (releaseData.scheduledAt) {
427
804
  await schedulingService.set(id, releaseData.scheduledAt);
428
805
  } else if (release2.scheduledAt) {
429
806
  schedulingService.cancel(id);
430
807
  }
431
- }
432
- return updatedRelease;
433
- },
434
- async createAction(releaseId, action) {
435
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
436
- strapi: strapi2
437
- });
438
- await Promise.all([
439
- validateEntryContentType(action.entry.contentType),
440
- validateUniqueEntry(releaseId, action)
441
- ]);
442
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
443
- if (!release2) {
444
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
445
- }
446
- if (release2.releasedAt) {
447
- throw new utils.errors.ValidationError("Release already published");
448
- }
449
- const { entry, type } = action;
450
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
451
- data: {
452
- type,
453
- contentType: entry.contentType,
454
- locale: entry.locale,
455
- entry: {
456
- id: entry.id,
457
- __type: entry.contentType,
458
- __pivot: { field: "entry" }
808
+ this.updateReleaseStatus(id);
809
+ strapi2.telemetry.send("didUpdateContentRelease");
810
+ return updatedRelease;
811
+ },
812
+ async createAction(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
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}`);
823
+ }
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
459
842
  },
460
- release: releaseId
461
- },
462
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
463
- });
464
- },
465
- async findActions(releaseId, query) {
466
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
467
- fields: ["id"]
468
- });
469
- if (!release2) {
470
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
471
- }
472
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
473
- ...query,
474
- populate: {
475
- entry: {
476
- populate: "*"
477
- }
478
- },
479
- filters: {
480
- release: releaseId
843
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
844
+ });
845
+ if (!disableUpdateReleaseStatus) {
846
+ this.updateReleaseStatus(releaseId);
481
847
  }
482
- });
483
- },
484
- async countActions(query) {
485
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
486
- },
487
- async groupActions(actions, groupBy) {
488
- const contentTypeUids = actions.reduce((acc, action) => {
489
- if (!acc.includes(action.contentType)) {
490
- acc.push(action.contentType);
848
+ return releaseAction2;
849
+ },
850
+ async findActions(releaseId, query) {
851
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
852
+ fields: ["id"]
853
+ });
854
+ if (!release2) {
855
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
491
856
  }
492
- return acc;
493
- }, []);
494
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
495
- contentTypeUids
496
- );
497
- const allLocalesDictionary = await this.getLocalesDataForActions();
498
- const formattedData = actions.map((action) => {
499
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
500
- return {
501
- ...action,
502
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
503
- contentType: {
504
- displayName,
505
- mainFieldValue: action.entry[mainField],
506
- uid: action.contentType
857
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
858
+ ...query,
859
+ populate: {
860
+ entry: {
861
+ populate: "*"
862
+ }
863
+ },
864
+ filters: {
865
+ release: releaseId
507
866
  }
508
- };
509
- });
510
- const groupName = getGroupName(groupBy);
511
- return ___default.default.groupBy(groupName)(formattedData);
512
- },
513
- async getLocalesDataForActions() {
514
- if (!strapi2.plugin("i18n")) {
515
- return {};
516
- }
517
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
518
- return allLocales.reduce((acc, locale) => {
519
- acc[locale.code] = { name: locale.name, code: locale.code };
520
- return acc;
521
- }, {});
522
- },
523
- async getContentTypesDataForActions(contentTypesUids) {
524
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
525
- const contentTypesData = {};
526
- for (const contentTypeUid of contentTypesUids) {
527
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
528
- uid: contentTypeUid
529
867
  });
530
- contentTypesData[contentTypeUid] = {
531
- mainField: contentTypeConfig.settings.mainField,
532
- displayName: strapi2.getModel(contentTypeUid).info.displayName
533
- };
534
- }
535
- return contentTypesData;
536
- },
537
- getContentTypeModelsFromActions(actions) {
538
- const contentTypeUids = actions.reduce((acc, action) => {
539
- if (!acc.includes(action.contentType)) {
540
- acc.push(action.contentType);
541
- }
542
- return acc;
543
- }, []);
544
- const contentTypeModelsMap = contentTypeUids.reduce(
545
- (acc, contentTypeUid) => {
546
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
868
+ },
869
+ async countActions(query) {
870
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
871
+ },
872
+ async groupActions(actions, groupBy) {
873
+ const contentTypeUids = actions.reduce((acc, action) => {
874
+ if (!acc.includes(action.contentType)) {
875
+ acc.push(action.contentType);
876
+ }
547
877
  return acc;
548
- },
549
- {}
550
- );
551
- return contentTypeModelsMap;
552
- },
553
- async getAllComponents() {
554
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
555
- const components = await contentManagerComponentsService.findAllComponents();
556
- const componentsMap = components.reduce(
557
- (acc, component) => {
558
- acc[component.uid] = component;
878
+ }, []);
879
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
880
+ contentTypeUids
881
+ );
882
+ const allLocalesDictionary = await this.getLocalesDataForActions();
883
+ const formattedData = actions.map((action) => {
884
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
885
+ return {
886
+ ...action,
887
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
888
+ contentType: {
889
+ displayName,
890
+ mainFieldValue: action.entry[mainField],
891
+ uid: action.contentType
892
+ }
893
+ };
894
+ });
895
+ const groupName = getGroupName(groupBy);
896
+ return ___default.default.groupBy(groupName)(formattedData);
897
+ },
898
+ async getLocalesDataForActions() {
899
+ if (!strapi2.plugin("i18n")) {
900
+ return {};
901
+ }
902
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
903
+ return allLocales.reduce((acc, locale) => {
904
+ acc[locale.code] = { name: locale.name, code: locale.code };
559
905
  return acc;
560
- },
561
- {}
562
- );
563
- return componentsMap;
564
- },
565
- async delete(releaseId) {
566
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
567
- populate: {
568
- actions: {
569
- fields: ["id"]
570
- }
906
+ }, {});
907
+ },
908
+ async getContentTypesDataForActions(contentTypesUids) {
909
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
910
+ const contentTypesData = {};
911
+ for (const contentTypeUid of contentTypesUids) {
912
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
913
+ uid: contentTypeUid
914
+ });
915
+ contentTypesData[contentTypeUid] = {
916
+ mainField: contentTypeConfig.settings.mainField,
917
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
918
+ };
571
919
  }
572
- });
573
- if (!release2) {
574
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
575
- }
576
- if (release2.releasedAt) {
577
- throw new utils.errors.ValidationError("Release already published");
578
- }
579
- await strapi2.db.transaction(async () => {
580
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
581
- where: {
582
- id: {
583
- $in: release2.actions.map((action) => action.id)
584
- }
920
+ return contentTypesData;
921
+ },
922
+ getContentTypeModelsFromActions(actions) {
923
+ const contentTypeUids = actions.reduce((acc, action) => {
924
+ if (!acc.includes(action.contentType)) {
925
+ acc.push(action.contentType);
585
926
  }
586
- });
587
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
588
- });
589
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
590
- const schedulingService = getService("scheduling", { strapi: strapi2 });
591
- await schedulingService.cancel(release2.id);
592
- }
593
- return release2;
594
- },
595
- async publish(releaseId) {
596
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
597
- RELEASE_MODEL_UID,
598
- releaseId,
599
- {
927
+ return acc;
928
+ }, []);
929
+ const contentTypeModelsMap = contentTypeUids.reduce(
930
+ (acc, contentTypeUid) => {
931
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
932
+ return acc;
933
+ },
934
+ {}
935
+ );
936
+ return contentTypeModelsMap;
937
+ },
938
+ async getAllComponents() {
939
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
940
+ const components = await contentManagerComponentsService.findAllComponents();
941
+ const componentsMap = components.reduce(
942
+ (acc, component) => {
943
+ acc[component.uid] = component;
944
+ return acc;
945
+ },
946
+ {}
947
+ );
948
+ return componentsMap;
949
+ },
950
+ async delete(releaseId) {
951
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
600
952
  populate: {
601
953
  actions: {
602
- populate: {
603
- entry: {
604
- fields: ["id"]
605
- }
606
- }
954
+ fields: ["id"]
607
955
  }
608
956
  }
957
+ });
958
+ if (!release2) {
959
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
609
960
  }
610
- );
611
- if (!releaseWithPopulatedActionEntries) {
612
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
613
- }
614
- if (releaseWithPopulatedActionEntries.releasedAt) {
615
- throw new utils.errors.ValidationError("Release already published");
616
- }
617
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
618
- throw new utils.errors.ValidationError("No entries to publish");
619
- }
620
- const collectionTypeActions = {};
621
- const singleTypeActions = [];
622
- for (const action of releaseWithPopulatedActionEntries.actions) {
623
- const contentTypeUid = action.contentType;
624
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
625
- if (!collectionTypeActions[contentTypeUid]) {
626
- collectionTypeActions[contentTypeUid] = {
627
- entriestoPublishIds: [],
628
- entriesToUnpublishIds: []
629
- };
630
- }
631
- if (action.type === "publish") {
632
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
633
- } else {
634
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
635
- }
636
- } else {
637
- singleTypeActions.push({
638
- uid: contentTypeUid,
639
- action: action.type,
640
- id: action.entry.id
961
+ if (release2.releasedAt) {
962
+ throw new utils.errors.ValidationError("Release already published");
963
+ }
964
+ await strapi2.db.transaction(async () => {
965
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
966
+ where: {
967
+ id: {
968
+ $in: release2.actions.map((action) => action.id)
969
+ }
970
+ }
641
971
  });
972
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
973
+ });
974
+ if (release2.scheduledAt) {
975
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
976
+ await schedulingService.cancel(release2.id);
642
977
  }
643
- }
644
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
645
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
646
- await strapi2.db.transaction(async () => {
647
- for (const { uid, action, id } of singleTypeActions) {
648
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
649
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
978
+ strapi2.telemetry.send("didDeleteContentRelease");
979
+ return release2;
980
+ },
981
+ async publish(releaseId) {
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) {
988
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
989
+ }
990
+ if (lockedRelease.releasedAt) {
991
+ throw new utils.errors.ValidationError("Release already published");
992
+ }
993
+ if (lockedRelease.status === "failed") {
994
+ throw new utils.errors.ValidationError("Release failed to publish");
995
+ }
650
996
  try {
651
- if (action === "publish") {
652
- await entityManagerService.publish(entry, uid);
653
- } else {
654
- await entityManagerService.unpublish(entry, uid);
655
- }
656
- } catch (error) {
657
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
658
- ;
659
- else {
660
- throw error;
661
- }
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);
1004
+ }
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
+ );
1012
+ }
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()
1021
+ }
1022
+ });
1023
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1024
+ isPublished: true,
1025
+ release: release22
1026
+ });
1027
+ strapi2.telemetry.send("didPublishContentRelease");
1028
+ return { release: release22, error: null };
1029
+ } catch (error2) {
1030
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1031
+ isPublished: false,
1032
+ error: error2
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
+ };
662
1041
  }
1042
+ });
1043
+ if (error) {
1044
+ throw error;
663
1045
  }
664
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
665
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
666
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
667
- const entriesToPublish = await strapi2.entityService.findMany(
668
- contentTypeUid,
669
- {
670
- filters: {
671
- id: {
672
- $in: entriestoPublishIds
673
- }
674
- },
675
- populate
1046
+ return release2;
1047
+ },
1048
+ async updateAction(actionId, releaseId, update) {
1049
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1050
+ where: {
1051
+ id: actionId,
1052
+ release: {
1053
+ id: releaseId,
1054
+ releasedAt: {
1055
+ $null: true
1056
+ }
676
1057
  }
1058
+ },
1059
+ data: update
1060
+ });
1061
+ if (!updatedAction) {
1062
+ throw new utils.errors.NotFoundError(
1063
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
677
1064
  );
678
- const entriesToUnpublish = await strapi2.entityService.findMany(
679
- contentTypeUid,
680
- {
681
- filters: {
682
- id: {
683
- $in: entriesToUnpublishIds
684
- }
685
- },
686
- populate
1065
+ }
1066
+ return updatedAction;
1067
+ },
1068
+ async deleteAction(actionId, releaseId) {
1069
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1070
+ where: {
1071
+ id: actionId,
1072
+ release: {
1073
+ id: releaseId,
1074
+ releasedAt: {
1075
+ $null: true
1076
+ }
687
1077
  }
688
- );
689
- if (entriesToPublish.length > 0) {
690
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
691
1078
  }
692
- if (entriesToUnpublish.length > 0) {
693
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
694
- }
695
- }
696
- });
697
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
698
- data: {
699
- /*
700
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
701
- */
702
- // @ts-expect-error see above
703
- releasedAt: /* @__PURE__ */ new Date()
1079
+ });
1080
+ if (!deletedAction) {
1081
+ throw new utils.errors.NotFoundError(
1082
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1083
+ );
704
1084
  }
705
- });
706
- return release2;
707
- },
708
- async updateAction(actionId, releaseId, update) {
709
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
710
- where: {
711
- id: actionId,
712
- release: {
713
- id: releaseId,
714
- releasedAt: {
715
- $null: true
1085
+ this.updateReleaseStatus(releaseId);
1086
+ return deletedAction;
1087
+ },
1088
+ async updateReleaseStatus(releaseId) {
1089
+ const [totalActions, invalidActions] = await Promise.all([
1090
+ this.countActions({
1091
+ filters: {
1092
+ release: releaseId
716
1093
  }
717
- }
718
- },
719
- data: update
720
- });
721
- if (!updatedAction) {
722
- throw new utils.errors.NotFoundError(
723
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
724
- );
725
- }
726
- return updatedAction;
727
- },
728
- async deleteAction(actionId, releaseId) {
729
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
730
- where: {
731
- id: actionId,
732
- release: {
733
- id: releaseId,
734
- releasedAt: {
735
- $null: true
1094
+ }),
1095
+ this.countActions({
1096
+ filters: {
1097
+ release: releaseId,
1098
+ isEntryValid: false
736
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
+ });
737
1112
  }
1113
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1114
+ where: {
1115
+ id: releaseId
1116
+ },
1117
+ data: {
1118
+ status: "ready"
1119
+ }
1120
+ });
738
1121
  }
739
- });
740
- if (!deletedAction) {
741
- throw new utils.errors.NotFoundError(
742
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
743
- );
1122
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1123
+ where: {
1124
+ id: releaseId
1125
+ },
1126
+ data: {
1127
+ status: "empty"
1128
+ }
1129
+ });
744
1130
  }
745
- return deletedAction;
1131
+ };
1132
+ };
1133
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1134
+ constructor(message) {
1135
+ super(message);
1136
+ this.name = "AlreadyOnReleaseError";
746
1137
  }
747
- });
1138
+ }
748
1139
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
749
1140
  async validateUniqueEntry(releaseId, releaseActionArgs) {
750
1141
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -757,7 +1148,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
757
1148
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
758
1149
  );
759
1150
  if (isEntryInRelease) {
760
- throw new utils.errors.ValidationError(
1151
+ throw new AlreadyOnReleaseError(
761
1152
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
762
1153
  );
763
1154
  }
@@ -865,15 +1256,25 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
865
1256
  const services = {
866
1257
  release: createReleaseService,
867
1258
  "release-validation": createReleaseValidationService,
868
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1259
+ scheduling: createSchedulingService
869
1260
  };
870
1261
  const RELEASE_SCHEMA = yup__namespace.object().shape({
871
1262
  name: yup__namespace.string().trim().required(),
872
- // scheduledAt is a date, but we always receive strings from the client
873
1263
  scheduledAt: yup__namespace.string().nullable(),
874
- timezone: yup__namespace.string().when("scheduledAt", {
875
- is: (scheduledAt) => !!scheduledAt,
876
- then: yup__namespace.string().required(),
1264
+ isScheduled: yup__namespace.boolean().optional(),
1265
+ time: yup__namespace.string().when("isScheduled", {
1266
+ is: true,
1267
+ then: yup__namespace.string().trim().required(),
1268
+ otherwise: yup__namespace.string().nullable()
1269
+ }),
1270
+ timezone: yup__namespace.string().when("isScheduled", {
1271
+ is: true,
1272
+ then: yup__namespace.string().required().nullable(),
1273
+ otherwise: yup__namespace.string().nullable()
1274
+ }),
1275
+ date: yup__namespace.string().when("isScheduled", {
1276
+ is: true,
1277
+ then: yup__namespace.string().required().nullable(),
877
1278
  otherwise: yup__namespace.string().nullable()
878
1279
  })
879
1280
  }).required().noUnknown();
@@ -908,7 +1309,12 @@ const releaseController = {
908
1309
  }
909
1310
  };
910
1311
  });
911
- 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 } };
912
1318
  }
913
1319
  },
914
1320
  async findOne(ctx) {
@@ -937,6 +1343,33 @@ const releaseController = {
937
1343
  };
938
1344
  ctx.body = { data };
939
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
+ },
940
1373
  async create(ctx) {
941
1374
  const user = ctx.state.user;
942
1375
  const releaseArgs = ctx.request.body;
@@ -1026,6 +1459,43 @@ const releaseActionController = {
1026
1459
  data: releaseAction2
1027
1460
  };
1028
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
+ },
1029
1499
  async findMany(ctx) {
1030
1500
  const releaseId = ctx.params.releaseId;
1031
1501
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1094,6 +1564,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1094
1564
  const release = {
1095
1565
  type: "admin",
1096
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
+ },
1097
1583
  {
1098
1584
  method: "POST",
1099
1585
  path: "/",
@@ -1211,6 +1697,22 @@ const releaseAction = {
1211
1697
  ]
1212
1698
  }
1213
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
+ },
1214
1716
  {
1215
1717
  method: "GET",
1216
1718
  path: "/:releaseId/actions",
@@ -1279,6 +1781,9 @@ const getPlugin = () => {
1279
1781
  };
1280
1782
  }
1281
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
1282
1787
  contentTypes
1283
1788
  };
1284
1789
  };