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

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 -450
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +947 -450
  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,468 +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
- 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
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
380
743
  }
381
- ],
382
- releasedAt: {
383
- $null: true
384
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" }
459
- },
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
481
- }
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);
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}`);
491
823
  }
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
507
- }
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
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"] } }
529
844
  });
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);
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}`);
541
854
  }
542
- return acc;
543
- }, []);
544
- const contentTypeModelsMap = contentTypeUids.reduce(
545
- (acc, contentTypeUid) => {
546
- 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
+ }
547
875
  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;
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 };
559
903
  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
- }
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
+ };
571
917
  }
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
- }
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);
585
924
  }
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
- {
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, {
600
950
  populate: {
601
951
  actions: {
602
- populate: {
603
- entry: {
604
- fields: ["id"]
605
- }
606
- }
952
+ fields: ["id"]
607
953
  }
608
954
  }
955
+ });
956
+ if (!release2) {
957
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
609
958
  }
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
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
+ }
641
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);
642
975
  }
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 });
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
+ }
650
994
  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
- }
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
+ };
662
1039
  }
1040
+ });
1041
+ if (error) {
1042
+ throw error;
663
1043
  }
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
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
+ }
676
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`
677
1062
  );
678
- const entriesToUnpublish = await strapi2.entityService.findMany(
679
- contentTypeUid,
680
- {
681
- filters: {
682
- id: {
683
- $in: entriesToUnpublishIds
684
- }
685
- },
686
- 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
+ }
687
1075
  }
688
- );
689
- if (entriesToPublish.length > 0) {
690
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
691
- }
692
- if (entriesToUnpublish.length > 0) {
693
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
694
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
+ );
695
1082
  }
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()
704
- }
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
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
716
1091
  }
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
1092
+ }),
1093
+ this.countActions({
1094
+ filters: {
1095
+ release: releaseId,
1096
+ isEntryValid: false
736
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
+ });
737
1110
  }
1111
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1112
+ where: {
1113
+ id: releaseId
1114
+ },
1115
+ data: {
1116
+ status: "ready"
1117
+ }
1118
+ });
738
1119
  }
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
- );
1120
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1121
+ where: {
1122
+ id: releaseId
1123
+ },
1124
+ data: {
1125
+ status: "empty"
1126
+ }
1127
+ });
744
1128
  }
745
- return deletedAction;
1129
+ };
1130
+ };
1131
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1132
+ constructor(message) {
1133
+ super(message);
1134
+ this.name = "AlreadyOnReleaseError";
746
1135
  }
747
- });
1136
+ }
748
1137
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
749
1138
  async validateUniqueEntry(releaseId, releaseActionArgs) {
750
1139
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -757,7 +1146,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
757
1146
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
758
1147
  );
759
1148
  if (isEntryInRelease) {
760
- throw new utils.errors.ValidationError(
1149
+ throw new AlreadyOnReleaseError(
761
1150
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
762
1151
  );
763
1152
  }
@@ -865,15 +1254,25 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
865
1254
  const services = {
866
1255
  release: createReleaseService,
867
1256
  "release-validation": createReleaseValidationService,
868
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1257
+ scheduling: createSchedulingService
869
1258
  };
870
1259
  const RELEASE_SCHEMA = yup__namespace.object().shape({
871
1260
  name: yup__namespace.string().trim().required(),
872
- // scheduledAt is a date, but we always receive strings from the client
873
1261
  scheduledAt: yup__namespace.string().nullable(),
874
- timezone: yup__namespace.string().when("scheduledAt", {
875
- is: (scheduledAt) => !!scheduledAt,
876
- 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(),
877
1276
  otherwise: yup__namespace.string().nullable()
878
1277
  })
879
1278
  }).required().noUnknown();
@@ -908,7 +1307,12 @@ const releaseController = {
908
1307
  }
909
1308
  };
910
1309
  });
911
- 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 } };
912
1316
  }
913
1317
  },
914
1318
  async findOne(ctx) {
@@ -937,6 +1341,33 @@ const releaseController = {
937
1341
  };
938
1342
  ctx.body = { data };
939
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
+ },
940
1371
  async create(ctx) {
941
1372
  const user = ctx.state.user;
942
1373
  const releaseArgs = ctx.request.body;
@@ -1026,6 +1457,38 @@ const releaseActionController = {
1026
1457
  data: releaseAction2
1027
1458
  };
1028
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
+ },
1029
1492
  async findMany(ctx) {
1030
1493
  const releaseId = ctx.params.releaseId;
1031
1494
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1094,6 +1557,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
1094
1557
  const release = {
1095
1558
  type: "admin",
1096
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
+ },
1097
1576
  {
1098
1577
  method: "POST",
1099
1578
  path: "/",
@@ -1211,6 +1690,22 @@ const releaseAction = {
1211
1690
  ]
1212
1691
  }
1213
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
+ },
1214
1709
  {
1215
1710
  method: "GET",
1216
1711
  path: "/:releaseId/actions",
@@ -1279,6 +1774,9 @@ const getPlugin = () => {
1279
1774
  };
1280
1775
  }
1281
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
1282
1780
  contentTypes
1283
1781
  };
1284
1782
  };