@strapi/content-releases 0.0.0-next.37dd1e3ff22e1635b69683abadd444912ae0dbff → 0.0.0-next.3cc05002fb92029975799c3113971bb5b5198d7c

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/dist/_chunks/App-R-kksTW7.mjs +1308 -0
  2. package/dist/_chunks/App-R-kksTW7.mjs.map +1 -0
  3. package/dist/_chunks/{App-5PRKHpa2.js → App-WZHc_d3m.js} +779 -420
  4. package/dist/_chunks/App-WZHc_d3m.js.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-ngTk74JV.mjs → en-RdapH-9X.mjs} +22 -7
  10. package/dist/_chunks/en-RdapH-9X.mjs.map +1 -0
  11. package/dist/_chunks/{en-haKSQIo8.js → en-faJDuv3q.js} +22 -7
  12. package/dist/_chunks/en-faJDuv3q.js.map +1 -0
  13. package/dist/_chunks/{index-PEkKIRyJ.js → index-k6fw6RYi.js} +240 -56
  14. package/dist/_chunks/index-k6fw6RYi.js.map +1 -0
  15. package/dist/_chunks/{index-_Zsj8MUA.mjs → index-vpSczx8v.mjs} +255 -71
  16. package/dist/_chunks/index-vpSczx8v.mjs.map +1 -0
  17. package/dist/admin/index.js +2 -1
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +3 -2
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/server/index.js +1078 -371
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/index.mjs +1077 -372
  24. package/dist/server/index.mjs.map +1 -1
  25. package/package.json +15 -11
  26. package/dist/_chunks/App-5PRKHpa2.js.map +0 -1
  27. package/dist/_chunks/App-J4jrthEu.mjs +0 -950
  28. package/dist/_chunks/App-J4jrthEu.mjs.map +0 -1
  29. package/dist/_chunks/en-haKSQIo8.js.map +0 -1
  30. package/dist/_chunks/en-ngTk74JV.mjs.map +0 -1
  31. package/dist/_chunks/index-PEkKIRyJ.js.map +0 -1
  32. package/dist/_chunks/index-_Zsj8MUA.mjs.map +0 -1
@@ -1,5 +1,9 @@
1
- import { setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import isEqual from "lodash/isEqual";
3
+ import { difference, keys } from "lodash";
2
4
  import _ from "lodash/fp";
5
+ import EE from "@strapi/strapi/dist/utils/ee";
6
+ import { scheduleJob } from "node-schedule";
3
7
  import * as yup from "yup";
4
8
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
5
9
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -47,47 +51,248 @@ const ACTIONS = [
47
51
  pluginName: "content-releases"
48
52
  }
49
53
  ];
54
+ const ALLOWED_WEBHOOK_EVENTS = {
55
+ RELEASES_PUBLISH: "releases.publish"
56
+ };
50
57
  const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
51
58
  return strapi2.plugin("content-releases").service(name);
52
59
  };
53
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
54
- const register = async ({ strapi: strapi2 }) => {
55
- if (features$2.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
56
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
57
- const releaseActionService = getService("release-action", { strapi: strapi2 });
58
- const eventManager = getService("event-manager", { strapi: strapi2 });
59
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
60
- "content-type.update",
61
- async ({ contentType }) => {
62
- if (contentType.schema.options.draftAndPublish === false) {
63
- await releaseActionService.deleteManyForContentType(contentType.uid);
60
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
61
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
62
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
63
+ const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
64
+ return entry;
65
+ };
66
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
67
+ try {
68
+ await strapi2.entityValidator.validateEntityCreation(
69
+ strapi2.getModel(contentTypeUid),
70
+ entry,
71
+ void 0,
72
+ // @ts-expect-error - FIXME: entity here is unnecessary
73
+ entry
74
+ );
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
80
+ async function deleteActionsOnDisableDraftAndPublish({
81
+ oldContentTypes,
82
+ contentTypes: contentTypes2
83
+ }) {
84
+ if (!oldContentTypes) {
85
+ return;
86
+ }
87
+ for (const uid in contentTypes2) {
88
+ if (!oldContentTypes[uid]) {
89
+ continue;
90
+ }
91
+ const oldContentType = oldContentTypes[uid];
92
+ const contentType = contentTypes2[uid];
93
+ if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
94
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
95
+ }
96
+ }
97
+ }
98
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
99
+ const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
100
+ if (deletedContentTypes.length) {
101
+ await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
102
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
103
+ });
104
+ }
105
+ }
106
+ async function migrateIsValidAndStatusReleases() {
107
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
108
+ where: {
109
+ status: null,
110
+ releasedAt: null
111
+ },
112
+ populate: {
113
+ actions: {
114
+ populate: {
115
+ entry: true
64
116
  }
65
117
  }
66
- );
67
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
68
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
69
- "content-type.delete",
70
- async ({ contentType }) => {
71
- await releaseActionService.deleteManyForContentType(contentType.uid);
118
+ }
119
+ });
120
+ mapAsync(releasesWithoutStatus, async (release2) => {
121
+ const actions = release2.actions;
122
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
123
+ for (const action of notValidatedActions) {
124
+ if (action.entry) {
125
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
126
+ strapi
127
+ });
128
+ if (populatedEntry) {
129
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
130
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
131
+ where: {
132
+ id: action.id
133
+ },
134
+ data: {
135
+ isEntryValid
136
+ }
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
142
+ });
143
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
144
+ where: {
145
+ status: null,
146
+ releasedAt: {
147
+ $notNull: true
72
148
  }
149
+ }
150
+ });
151
+ mapAsync(publishedReleases, async (release2) => {
152
+ return strapi.db.query(RELEASE_MODEL_UID).update({
153
+ where: {
154
+ id: release2.id
155
+ },
156
+ data: {
157
+ status: "done"
158
+ }
159
+ });
160
+ });
161
+ }
162
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
163
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
164
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
165
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
73
166
  );
74
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
167
+ const releasesAffected = /* @__PURE__ */ new Set();
168
+ mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
169
+ const oldContentType = oldContentTypes[contentTypeUID];
170
+ const contentType = contentTypes2[contentTypeUID];
171
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
172
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
173
+ where: {
174
+ contentType: contentTypeUID
175
+ },
176
+ populate: {
177
+ entry: true,
178
+ release: true
179
+ }
180
+ });
181
+ await mapAsync(actions, async (action) => {
182
+ if (action.entry && action.release) {
183
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
+ strapi
185
+ });
186
+ if (populatedEntry) {
187
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
188
+ strapi
189
+ });
190
+ releasesAffected.add(action.release.id);
191
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
192
+ where: {
193
+ id: action.id
194
+ },
195
+ data: {
196
+ isEntryValid
197
+ }
198
+ });
199
+ }
200
+ }
201
+ });
202
+ }
203
+ }).then(() => {
204
+ mapAsync(releasesAffected, async (releaseId) => {
205
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
206
+ });
207
+ });
208
+ }
209
+ }
210
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
211
+ if (!oldContentTypes) {
212
+ return;
213
+ }
214
+ for (const uid in contentTypes2) {
215
+ if (!oldContentTypes[uid]) {
216
+ continue;
217
+ }
218
+ const oldContentType = oldContentTypes[uid];
219
+ const contentType = contentTypes2[uid];
220
+ const i18nPlugin = strapi.plugin("i18n");
221
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
222
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
223
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
224
+ locale: null
225
+ }).where({ contentType: uid }).execute();
226
+ }
227
+ }
228
+ }
229
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
230
+ if (!oldContentTypes) {
231
+ return;
232
+ }
233
+ for (const uid in contentTypes2) {
234
+ if (!oldContentTypes[uid]) {
235
+ continue;
236
+ }
237
+ const oldContentType = oldContentTypes[uid];
238
+ const contentType = contentTypes2[uid];
239
+ const i18nPlugin = strapi.plugin("i18n");
240
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
241
+ const { getDefaultLocale } = i18nPlugin.service("locales");
242
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
243
+ const defaultLocale = await getDefaultLocale();
244
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
245
+ locale: defaultLocale
246
+ }).where({ contentType: uid }).execute();
247
+ }
248
+ }
249
+ }
250
+ const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
251
+ const register = async ({ strapi: strapi2 }) => {
252
+ if (features$2.isEnabled("cms-content-releases")) {
253
+ await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
254
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
255
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
256
+ }
257
+ if (strapi2.plugin("graphql")) {
258
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
259
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
260
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
75
261
  }
76
262
  };
77
263
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
78
264
  const bootstrap = async ({ strapi: strapi2 }) => {
79
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
265
+ if (features$1.isEnabled("cms-content-releases")) {
266
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
+ );
80
269
  strapi2.db.lifecycles.subscribe({
81
- afterDelete(event) {
82
- const { model, result } = event;
83
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
84
- const { id } = result;
85
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
86
- where: {
87
- target_type: model.uid,
88
- target_id: id
270
+ models: contentTypesWithDraftAndPublish,
271
+ async afterDelete(event) {
272
+ try {
273
+ const { model, result } = event;
274
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
275
+ const { id } = result;
276
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
277
+ where: {
278
+ actions: {
279
+ target_type: model.uid,
280
+ target_id: id
281
+ }
282
+ }
283
+ });
284
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
285
+ where: {
286
+ target_type: model.uid,
287
+ target_id: id
288
+ }
289
+ });
290
+ for (const release2 of releases) {
291
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
89
292
  }
90
- });
293
+ }
294
+ } catch (error) {
295
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
91
296
  }
92
297
  },
93
298
  /**
@@ -96,7 +301,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
96
301
  */
97
302
  async beforeDeleteMany(event) {
98
303
  const { model, params } = event;
99
- if (model.kind === "collectionType" && model.options.draftAndPublish) {
304
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
100
305
  const { where } = params;
101
306
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
102
307
  event.state.entriesToDelete = entriesToDelete;
@@ -107,20 +312,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
107
312
  * We make this only after deleteMany is succesfully executed to avoid errors
108
313
  */
109
314
  async afterDeleteMany(event) {
110
- const { model, state } = event;
111
- const entriesToDelete = state.entriesToDelete;
112
- if (entriesToDelete) {
113
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
114
- where: {
115
- target_type: model.uid,
116
- target_id: {
117
- $in: entriesToDelete.map((entry) => entry.id)
315
+ try {
316
+ const { model, state } = event;
317
+ const entriesToDelete = state.entriesToDelete;
318
+ if (entriesToDelete) {
319
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
320
+ where: {
321
+ actions: {
322
+ target_type: model.uid,
323
+ target_id: {
324
+ $in: entriesToDelete.map(
325
+ (entry) => entry.id
326
+ )
327
+ }
328
+ }
329
+ }
330
+ });
331
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
332
+ where: {
333
+ target_type: model.uid,
334
+ target_id: {
335
+ $in: entriesToDelete.map((entry) => entry.id)
336
+ }
118
337
  }
338
+ });
339
+ for (const release2 of releases) {
340
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
119
341
  }
342
+ }
343
+ } catch (error) {
344
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
+ error
120
346
  });
121
347
  }
348
+ },
349
+ async afterUpdate(event) {
350
+ try {
351
+ const { model, result } = event;
352
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
353
+ const isEntryValid = await getEntryValidStatus(
354
+ model.uid,
355
+ result,
356
+ {
357
+ strapi: strapi2
358
+ }
359
+ );
360
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
361
+ where: {
362
+ target_type: model.uid,
363
+ target_id: result.id
364
+ },
365
+ data: {
366
+ isEntryValid
367
+ }
368
+ });
369
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
370
+ where: {
371
+ actions: {
372
+ target_type: model.uid,
373
+ target_id: result.id
374
+ }
375
+ }
376
+ });
377
+ for (const release2 of releases) {
378
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
379
+ }
380
+ }
381
+ } catch (error) {
382
+ strapi2.log.error("Error while updating release actions after entry update", { error });
383
+ }
122
384
  }
123
385
  });
386
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
387
+ strapi2.log.error(
388
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
389
+ );
390
+ throw err;
391
+ });
392
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
393
+ strapi2.webhookStore.addAllowedEvent(key, value);
394
+ });
395
+ }
396
+ };
397
+ const destroy = async ({ strapi: strapi2 }) => {
398
+ const scheduledJobs = getService("scheduling", {
399
+ strapi: strapi2
400
+ }).getAll();
401
+ for (const [, job] of scheduledJobs) {
402
+ job.cancel();
124
403
  }
125
404
  };
126
405
  const schema$1 = {
@@ -149,6 +428,17 @@ const schema$1 = {
149
428
  releasedAt: {
150
429
  type: "datetime"
151
430
  },
431
+ scheduledAt: {
432
+ type: "datetime"
433
+ },
434
+ timezone: {
435
+ type: "string"
436
+ },
437
+ status: {
438
+ type: "enumeration",
439
+ enum: ["ready", "blocked", "failed", "done", "empty"],
440
+ required: true
441
+ },
152
442
  actions: {
153
443
  type: "relation",
154
444
  relation: "oneToMany",
@@ -201,6 +491,9 @@ const schema = {
201
491
  relation: "manyToOne",
202
492
  target: RELEASE_MODEL_UID,
203
493
  inversedBy: "actions"
494
+ },
495
+ isEntryValid: {
496
+ type: "boolean"
204
497
  }
205
498
  }
206
499
  };
@@ -211,354 +504,593 @@ const contentTypes = {
211
504
  release: release$1,
212
505
  "release-action": releaseAction$1
213
506
  };
214
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
215
- async deleteManyForContentType(contentTypeUid) {
216
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
217
- where: {
218
- target_type: contentTypeUid
219
- }
220
- });
221
- }
222
- });
223
507
  const getGroupName = (queryValue) => {
224
508
  switch (queryValue) {
225
509
  case "contentType":
226
- return "entry.contentType.displayName";
510
+ return "contentType.displayName";
227
511
  case "action":
228
512
  return "type";
229
513
  case "locale":
230
- return _.getOr("No locale", "entry.locale.name");
514
+ return _.getOr("No locale", "locale.name");
231
515
  default:
232
- return "entry.contentType.displayName";
516
+ return "contentType.displayName";
233
517
  }
234
518
  };
235
- const createReleaseService = ({ strapi: strapi2 }) => ({
236
- async create(releaseData, { user }) {
237
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
238
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
239
- data: releaseWithCreatorFields
519
+ const createReleaseService = ({ strapi: strapi2 }) => {
520
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
521
+ strapi2.eventHub.emit(event, {
522
+ isPublished,
523
+ error,
524
+ release: release2
240
525
  });
241
- },
242
- async findOne(id, query = {}) {
243
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
244
- ...query
526
+ };
527
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
528
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
529
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
530
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
531
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
532
+ try {
533
+ if (actionType === "publish") {
534
+ await entityManagerService.publish(entry, uid);
535
+ } else {
536
+ await entityManagerService.unpublish(entry, uid);
537
+ }
538
+ } catch (error) {
539
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
540
+ ;
541
+ else {
542
+ throw error;
543
+ }
544
+ }
545
+ };
546
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
547
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
548
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
549
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
550
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
551
+ filters: {
552
+ id: {
553
+ $in: entriesToPublishIds
554
+ }
555
+ },
556
+ populate
245
557
  });
246
- return release2;
247
- },
248
- findPage(query) {
249
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
250
- ...query,
558
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
559
+ filters: {
560
+ id: {
561
+ $in: entriestoUnpublishIds
562
+ }
563
+ },
564
+ populate
565
+ });
566
+ if (entriesToPublish.length > 0) {
567
+ await entityManagerService.publishMany(entriesToPublish, uid);
568
+ }
569
+ if (entriesToUnpublish.length > 0) {
570
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
571
+ }
572
+ };
573
+ const getFormattedActions = async (releaseId) => {
574
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
575
+ where: {
576
+ release: {
577
+ id: releaseId
578
+ }
579
+ },
251
580
  populate: {
252
- actions: {
253
- // @ts-expect-error Ignore missing properties
254
- count: true
581
+ entry: {
582
+ fields: ["id"]
255
583
  }
256
584
  }
257
585
  });
258
- },
259
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
260
- hasEntryAttached
261
- } = {
262
- hasEntryAttached: false
263
- }) {
264
- const whereActions = hasEntryAttached ? {
265
- // Find all Releases where the content type entry is present
266
- actions: {
267
- target_type: contentTypeUid,
268
- target_id: entryId
269
- }
270
- } : {
271
- // Find all Releases where the content type entry is not present
272
- $or: [
273
- {
274
- $not: {
275
- actions: {
586
+ if (actions.length === 0) {
587
+ throw new errors.ValidationError("No entries to publish");
588
+ }
589
+ const collectionTypeActions = {};
590
+ const singleTypeActions = [];
591
+ for (const action of actions) {
592
+ const contentTypeUid = action.contentType;
593
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
594
+ if (!collectionTypeActions[contentTypeUid]) {
595
+ collectionTypeActions[contentTypeUid] = {
596
+ entriesToPublishIds: [],
597
+ entriesToUnpublishIds: []
598
+ };
599
+ }
600
+ if (action.type === "publish") {
601
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
602
+ } else {
603
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
604
+ }
605
+ } else {
606
+ singleTypeActions.push({
607
+ uid: contentTypeUid,
608
+ action: action.type,
609
+ id: action.entry.id
610
+ });
611
+ }
612
+ }
613
+ return { collectionTypeActions, singleTypeActions };
614
+ };
615
+ return {
616
+ async create(releaseData, { user }) {
617
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
618
+ const {
619
+ validatePendingReleasesLimit,
620
+ validateUniqueNameForPendingRelease,
621
+ validateScheduledAtIsLaterThanNow
622
+ } = getService("release-validation", { strapi: strapi2 });
623
+ await Promise.all([
624
+ validatePendingReleasesLimit(),
625
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
626
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
627
+ ]);
628
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
629
+ data: {
630
+ ...releaseWithCreatorFields,
631
+ status: "empty"
632
+ }
633
+ });
634
+ if (releaseWithCreatorFields.scheduledAt) {
635
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
636
+ await schedulingService.set(release2.id, release2.scheduledAt);
637
+ }
638
+ strapi2.telemetry.send("didCreateContentRelease");
639
+ return release2;
640
+ },
641
+ async findOne(id, query = {}) {
642
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
643
+ ...query
644
+ });
645
+ return release2;
646
+ },
647
+ findPage(query) {
648
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
649
+ ...query,
650
+ populate: {
651
+ actions: {
652
+ // @ts-expect-error Ignore missing properties
653
+ count: true
654
+ }
655
+ }
656
+ });
657
+ },
658
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
659
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
660
+ where: {
661
+ actions: {
662
+ target_type: contentTypeUid,
663
+ target_id: entryId
664
+ },
665
+ releasedAt: {
666
+ $null: true
667
+ }
668
+ },
669
+ populate: {
670
+ // Filter the action to get only the content type entry
671
+ actions: {
672
+ where: {
276
673
  target_type: contentTypeUid,
277
674
  target_id: entryId
278
675
  }
279
676
  }
280
- },
281
- {
282
- actions: null
283
677
  }
284
- ]
285
- };
286
- const populateAttachedAction = hasEntryAttached ? {
287
- // Filter the action to get only the content type entry
288
- actions: {
678
+ });
679
+ return releases.map((release2) => {
680
+ if (release2.actions?.length) {
681
+ const [actionForEntry] = release2.actions;
682
+ delete release2.actions;
683
+ return {
684
+ ...release2,
685
+ action: actionForEntry
686
+ };
687
+ }
688
+ return release2;
689
+ });
690
+ },
691
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
692
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
289
693
  where: {
290
- target_type: contentTypeUid,
291
- target_id: entryId
694
+ releasedAt: {
695
+ $null: true
696
+ },
697
+ actions: {
698
+ target_type: contentTypeUid,
699
+ target_id: entryId
700
+ }
292
701
  }
293
- }
294
- } : {};
295
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
296
- where: {
297
- ...whereActions,
298
- releasedAt: {
299
- $null: true
702
+ });
703
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
704
+ where: {
705
+ $or: [
706
+ {
707
+ id: {
708
+ $notIn: releasesRelated.map((release2) => release2.id)
709
+ }
710
+ },
711
+ {
712
+ actions: null
713
+ }
714
+ ],
715
+ releasedAt: {
716
+ $null: true
717
+ }
300
718
  }
301
- },
302
- populate: {
303
- ...populateAttachedAction
719
+ });
720
+ return releases.map((release2) => {
721
+ if (release2.actions?.length) {
722
+ const [actionForEntry] = release2.actions;
723
+ delete release2.actions;
724
+ return {
725
+ ...release2,
726
+ action: actionForEntry
727
+ };
728
+ }
729
+ return release2;
730
+ });
731
+ },
732
+ async update(id, releaseData, { user }) {
733
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
734
+ releaseData
735
+ );
736
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
737
+ "release-validation",
738
+ { strapi: strapi2 }
739
+ );
740
+ await Promise.all([
741
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
742
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
743
+ ]);
744
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
745
+ if (!release2) {
746
+ throw new errors.NotFoundError(`No release found for id ${id}`);
304
747
  }
305
- });
306
- return releases.map((release2) => {
307
- if (release2.actions?.length) {
308
- const [actionForEntry] = release2.actions;
309
- delete release2.actions;
310
- return {
311
- ...release2,
312
- action: actionForEntry
313
- };
748
+ if (release2.releasedAt) {
749
+ throw new errors.ValidationError("Release already published");
314
750
  }
315
- return release2;
316
- });
317
- },
318
- async update(id, releaseData, { user }) {
319
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
320
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
321
- if (!release2) {
322
- throw new errors.NotFoundError(`No release found for id ${id}`);
323
- }
324
- if (release2.releasedAt) {
325
- throw new errors.ValidationError("Release already published");
326
- }
327
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
328
- /*
329
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
330
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
331
- */
332
- // @ts-expect-error see above
333
- data: releaseWithCreatorFields
334
- });
335
- return updatedRelease;
336
- },
337
- async createAction(releaseId, action) {
338
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
339
- strapi: strapi2
340
- });
341
- await Promise.all([
342
- validateEntryContentType(action.entry.contentType),
343
- validateUniqueEntry(releaseId, action)
344
- ]);
345
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
346
- if (!release2) {
347
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
348
- }
349
- if (release2.releasedAt) {
350
- throw new errors.ValidationError("Release already published");
351
- }
352
- const { entry, type } = action;
353
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
354
- data: {
355
- type,
356
- contentType: entry.contentType,
357
- locale: entry.locale,
358
- entry: {
359
- id: entry.id,
360
- __type: entry.contentType,
361
- __pivot: { field: "entry" }
362
- },
363
- release: releaseId
364
- },
365
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
366
- });
367
- },
368
- async findActions(releaseId, query) {
369
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
370
- fields: ["id"]
371
- });
372
- if (!release2) {
373
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
374
- }
375
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
376
- ...query,
377
- populate: {
378
- entry: true
379
- },
380
- filters: {
381
- release: releaseId
751
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
752
+ /*
753
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
754
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
755
+ */
756
+ // @ts-expect-error see above
757
+ data: releaseWithCreatorFields
758
+ });
759
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
760
+ if (releaseData.scheduledAt) {
761
+ await schedulingService.set(id, releaseData.scheduledAt);
762
+ } else if (release2.scheduledAt) {
763
+ schedulingService.cancel(id);
382
764
  }
383
- });
384
- },
385
- async countActions(query) {
386
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
387
- },
388
- async groupActions(actions, groupBy) {
389
- const contentTypeUids = actions.reduce((acc, action) => {
390
- if (!acc.includes(action.contentType)) {
391
- acc.push(action.contentType);
765
+ this.updateReleaseStatus(id);
766
+ strapi2.telemetry.send("didUpdateContentRelease");
767
+ return updatedRelease;
768
+ },
769
+ async createAction(releaseId, action) {
770
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
771
+ strapi: strapi2
772
+ });
773
+ await Promise.all([
774
+ validateEntryContentType(action.entry.contentType),
775
+ validateUniqueEntry(releaseId, action)
776
+ ]);
777
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
778
+ if (!release2) {
779
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
392
780
  }
393
- return acc;
394
- }, []);
395
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
396
- contentTypeUids
397
- );
398
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
399
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
400
- acc[locale.code] = { name: locale.name, code: locale.code };
401
- return acc;
402
- }, {});
403
- const formattedData = actions.map((action) => {
404
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
405
- return {
406
- ...action,
407
- entry: {
408
- id: action.entry.id,
409
- contentType: {
410
- displayName,
411
- mainFieldValue: action.entry[mainField]
781
+ if (release2.releasedAt) {
782
+ throw new errors.ValidationError("Release already published");
783
+ }
784
+ const { entry, type } = action;
785
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
786
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
787
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
788
+ data: {
789
+ type,
790
+ contentType: entry.contentType,
791
+ locale: entry.locale,
792
+ isEntryValid,
793
+ entry: {
794
+ id: entry.id,
795
+ __type: entry.contentType,
796
+ __pivot: { field: "entry" }
412
797
  },
413
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
414
- status: action.entry.publishedAt ? "published" : "draft"
415
- }
416
- };
417
- });
418
- const groupName = getGroupName(groupBy);
419
- return _.groupBy(groupName)(formattedData);
420
- },
421
- async getContentTypesDataForActions(contentTypesUids) {
422
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
423
- const contentTypesData = {};
424
- for (const contentTypeUid of contentTypesUids) {
425
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
426
- uid: contentTypeUid
798
+ release: releaseId
799
+ },
800
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
427
801
  });
428
- contentTypesData[contentTypeUid] = {
429
- mainField: contentTypeConfig.settings.mainField,
430
- displayName: strapi2.getModel(contentTypeUid).info.displayName
431
- };
432
- }
433
- return contentTypesData;
434
- },
435
- async delete(releaseId) {
436
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
437
- populate: {
438
- actions: {
439
- fields: ["id"]
440
- }
802
+ this.updateReleaseStatus(releaseId);
803
+ return releaseAction2;
804
+ },
805
+ async findActions(releaseId, query) {
806
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
807
+ fields: ["id"]
808
+ });
809
+ if (!release2) {
810
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
441
811
  }
442
- });
443
- if (!release2) {
444
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
445
- }
446
- if (release2.releasedAt) {
447
- throw new errors.ValidationError("Release already published");
448
- }
449
- await strapi2.db.transaction(async () => {
450
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
451
- where: {
452
- id: {
453
- $in: release2.actions.map((action) => action.id)
812
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
813
+ ...query,
814
+ populate: {
815
+ entry: {
816
+ populate: "*"
454
817
  }
818
+ },
819
+ filters: {
820
+ release: releaseId
455
821
  }
456
822
  });
457
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
458
- });
459
- return release2;
460
- },
461
- async publish(releaseId) {
462
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
463
- RELEASE_MODEL_UID,
464
- releaseId,
465
- {
823
+ },
824
+ async countActions(query) {
825
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
826
+ },
827
+ async groupActions(actions, groupBy) {
828
+ const contentTypeUids = actions.reduce((acc, action) => {
829
+ if (!acc.includes(action.contentType)) {
830
+ acc.push(action.contentType);
831
+ }
832
+ return acc;
833
+ }, []);
834
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
835
+ contentTypeUids
836
+ );
837
+ const allLocalesDictionary = await this.getLocalesDataForActions();
838
+ const formattedData = actions.map((action) => {
839
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
840
+ return {
841
+ ...action,
842
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
843
+ contentType: {
844
+ displayName,
845
+ mainFieldValue: action.entry[mainField],
846
+ uid: action.contentType
847
+ }
848
+ };
849
+ });
850
+ const groupName = getGroupName(groupBy);
851
+ return _.groupBy(groupName)(formattedData);
852
+ },
853
+ async getLocalesDataForActions() {
854
+ if (!strapi2.plugin("i18n")) {
855
+ return {};
856
+ }
857
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
858
+ return allLocales.reduce((acc, locale) => {
859
+ acc[locale.code] = { name: locale.name, code: locale.code };
860
+ return acc;
861
+ }, {});
862
+ },
863
+ async getContentTypesDataForActions(contentTypesUids) {
864
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
865
+ const contentTypesData = {};
866
+ for (const contentTypeUid of contentTypesUids) {
867
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
868
+ uid: contentTypeUid
869
+ });
870
+ contentTypesData[contentTypeUid] = {
871
+ mainField: contentTypeConfig.settings.mainField,
872
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
873
+ };
874
+ }
875
+ return contentTypesData;
876
+ },
877
+ getContentTypeModelsFromActions(actions) {
878
+ const contentTypeUids = actions.reduce((acc, action) => {
879
+ if (!acc.includes(action.contentType)) {
880
+ acc.push(action.contentType);
881
+ }
882
+ return acc;
883
+ }, []);
884
+ const contentTypeModelsMap = contentTypeUids.reduce(
885
+ (acc, contentTypeUid) => {
886
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
887
+ return acc;
888
+ },
889
+ {}
890
+ );
891
+ return contentTypeModelsMap;
892
+ },
893
+ async getAllComponents() {
894
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
895
+ const components = await contentManagerComponentsService.findAllComponents();
896
+ const componentsMap = components.reduce(
897
+ (acc, component) => {
898
+ acc[component.uid] = component;
899
+ return acc;
900
+ },
901
+ {}
902
+ );
903
+ return componentsMap;
904
+ },
905
+ async delete(releaseId) {
906
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
466
907
  populate: {
467
908
  actions: {
468
- populate: {
469
- entry: true
470
- }
909
+ fields: ["id"]
471
910
  }
472
911
  }
912
+ });
913
+ if (!release2) {
914
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
473
915
  }
474
- );
475
- if (!releaseWithPopulatedActionEntries) {
476
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
477
- }
478
- if (releaseWithPopulatedActionEntries.releasedAt) {
479
- throw new errors.ValidationError("Release already published");
480
- }
481
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
482
- throw new errors.ValidationError("No entries to publish");
483
- }
484
- const actions = {};
485
- for (const action of releaseWithPopulatedActionEntries.actions) {
486
- const contentTypeUid = action.contentType;
487
- if (!actions[contentTypeUid]) {
488
- actions[contentTypeUid] = {
489
- publish: [],
490
- unpublish: []
491
- };
916
+ if (release2.releasedAt) {
917
+ throw new errors.ValidationError("Release already published");
492
918
  }
493
- if (action.type === "publish") {
494
- actions[contentTypeUid].publish.push(action.entry);
495
- } else {
496
- actions[contentTypeUid].unpublish.push(action.entry);
919
+ await strapi2.db.transaction(async () => {
920
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
921
+ where: {
922
+ id: {
923
+ $in: release2.actions.map((action) => action.id)
924
+ }
925
+ }
926
+ });
927
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
928
+ });
929
+ if (release2.scheduledAt) {
930
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
931
+ await schedulingService.cancel(release2.id);
497
932
  }
498
- }
499
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
500
- await strapi2.db.transaction(async () => {
501
- for (const contentTypeUid of Object.keys(actions)) {
502
- const { publish, unpublish } = actions[contentTypeUid];
503
- if (publish.length > 0) {
504
- await entityManagerService.publishMany(publish, contentTypeUid);
933
+ strapi2.telemetry.send("didDeleteContentRelease");
934
+ return release2;
935
+ },
936
+ async publish(releaseId) {
937
+ const {
938
+ release: release2,
939
+ error
940
+ } = await strapi2.db.transaction(async ({ trx }) => {
941
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
942
+ if (!lockedRelease) {
943
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
944
+ }
945
+ if (lockedRelease.releasedAt) {
946
+ throw new errors.ValidationError("Release already published");
505
947
  }
506
- if (unpublish.length > 0) {
507
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
948
+ if (lockedRelease.status === "failed") {
949
+ throw new errors.ValidationError("Release failed to publish");
950
+ }
951
+ try {
952
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
953
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
954
+ releaseId
955
+ );
956
+ await strapi2.db.transaction(async () => {
957
+ for (const { uid, action, id } of singleTypeActions) {
958
+ await publishSingleTypeAction(uid, action, id);
959
+ }
960
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
961
+ const uid = contentTypeUid;
962
+ await publishCollectionTypeAction(
963
+ uid,
964
+ collectionTypeActions[uid].entriesToPublishIds,
965
+ collectionTypeActions[uid].entriesToUnpublishIds
966
+ );
967
+ }
968
+ });
969
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
970
+ where: {
971
+ id: releaseId
972
+ },
973
+ data: {
974
+ status: "done",
975
+ releasedAt: /* @__PURE__ */ new Date()
976
+ }
977
+ });
978
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
979
+ isPublished: true,
980
+ release: release22
981
+ });
982
+ strapi2.telemetry.send("didPublishContentRelease");
983
+ return { release: release22, error: null };
984
+ } catch (error2) {
985
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
986
+ isPublished: false,
987
+ error: error2
988
+ });
989
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
990
+ status: "failed"
991
+ }).transacting(trx).execute();
992
+ return {
993
+ release: null,
994
+ error: error2
995
+ };
508
996
  }
997
+ });
998
+ if (error) {
999
+ throw error;
509
1000
  }
510
- });
511
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
512
- data: {
513
- /*
514
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
515
- */
516
- // @ts-expect-error see above
517
- releasedAt: /* @__PURE__ */ new Date()
1001
+ return release2;
1002
+ },
1003
+ async updateAction(actionId, releaseId, update) {
1004
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1005
+ where: {
1006
+ id: actionId,
1007
+ release: {
1008
+ id: releaseId,
1009
+ releasedAt: {
1010
+ $null: true
1011
+ }
1012
+ }
1013
+ },
1014
+ data: update
1015
+ });
1016
+ if (!updatedAction) {
1017
+ throw new errors.NotFoundError(
1018
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1019
+ );
518
1020
  }
519
- });
520
- return release2;
521
- },
522
- async updateAction(actionId, releaseId, update) {
523
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
524
- where: {
525
- id: actionId,
526
- release: {
527
- id: releaseId,
528
- releasedAt: {
529
- $null: true
1021
+ return updatedAction;
1022
+ },
1023
+ async deleteAction(actionId, releaseId) {
1024
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1025
+ where: {
1026
+ id: actionId,
1027
+ release: {
1028
+ id: releaseId,
1029
+ releasedAt: {
1030
+ $null: true
1031
+ }
530
1032
  }
531
1033
  }
532
- },
533
- data: update
534
- });
535
- if (!updatedAction) {
536
- throw new errors.NotFoundError(
537
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
538
- );
539
- }
540
- return updatedAction;
541
- },
542
- async deleteAction(actionId, releaseId) {
543
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
544
- where: {
545
- id: actionId,
546
- release: {
547
- id: releaseId,
548
- releasedAt: {
549
- $null: true
1034
+ });
1035
+ if (!deletedAction) {
1036
+ throw new errors.NotFoundError(
1037
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1038
+ );
1039
+ }
1040
+ this.updateReleaseStatus(releaseId);
1041
+ return deletedAction;
1042
+ },
1043
+ async updateReleaseStatus(releaseId) {
1044
+ const [totalActions, invalidActions] = await Promise.all([
1045
+ this.countActions({
1046
+ filters: {
1047
+ release: releaseId
1048
+ }
1049
+ }),
1050
+ this.countActions({
1051
+ filters: {
1052
+ release: releaseId,
1053
+ isEntryValid: false
550
1054
  }
1055
+ })
1056
+ ]);
1057
+ if (totalActions > 0) {
1058
+ if (invalidActions > 0) {
1059
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1060
+ where: {
1061
+ id: releaseId
1062
+ },
1063
+ data: {
1064
+ status: "blocked"
1065
+ }
1066
+ });
551
1067
  }
1068
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1069
+ where: {
1070
+ id: releaseId
1071
+ },
1072
+ data: {
1073
+ status: "ready"
1074
+ }
1075
+ });
552
1076
  }
553
- });
554
- if (!deletedAction) {
555
- throw new errors.NotFoundError(
556
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
557
- );
1077
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1078
+ where: {
1079
+ id: releaseId
1080
+ },
1081
+ data: {
1082
+ status: "empty"
1083
+ }
1084
+ });
558
1085
  }
559
- return deletedAction;
1086
+ };
1087
+ };
1088
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1089
+ constructor(message) {
1090
+ super(message);
1091
+ this.name = "AlreadyOnReleaseError";
560
1092
  }
561
- });
1093
+ }
562
1094
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
563
1095
  async validateUniqueEntry(releaseId, releaseActionArgs) {
564
1096
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -571,7 +1103,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
571
1103
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
572
1104
  );
573
1105
  if (isEntryInRelease) {
574
- throw new errors.ValidationError(
1106
+ throw new AlreadyOnReleaseError(
575
1107
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
576
1108
  );
577
1109
  }
@@ -586,34 +1118,120 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
586
1118
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
587
1119
  );
588
1120
  }
1121
+ },
1122
+ async validatePendingReleasesLimit() {
1123
+ const maximumPendingReleases = (
1124
+ // @ts-expect-error - options is not typed into features
1125
+ EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
1126
+ );
1127
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1128
+ filters: {
1129
+ releasedAt: {
1130
+ $null: true
1131
+ }
1132
+ }
1133
+ });
1134
+ if (pendingReleasesCount >= maximumPendingReleases) {
1135
+ throw new errors.ValidationError("You have reached the maximum number of pending releases");
1136
+ }
1137
+ },
1138
+ async validateUniqueNameForPendingRelease(name, id) {
1139
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1140
+ filters: {
1141
+ releasedAt: {
1142
+ $null: true
1143
+ },
1144
+ name,
1145
+ ...id && { id: { $ne: id } }
1146
+ }
1147
+ });
1148
+ const isNameUnique = pendingReleases.length === 0;
1149
+ if (!isNameUnique) {
1150
+ throw new errors.ValidationError(`Release with name ${name} already exists`);
1151
+ }
1152
+ },
1153
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1154
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1155
+ throw new errors.ValidationError("Scheduled at must be later than now");
1156
+ }
589
1157
  }
590
1158
  });
591
- const createEventManagerService = () => {
592
- const state = {
593
- destroyListenerCallbacks: []
594
- };
1159
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1160
+ const scheduledJobs = /* @__PURE__ */ new Map();
595
1161
  return {
596
- addDestroyListenerCallback(destroyListenerCallback) {
597
- state.destroyListenerCallbacks.push(destroyListenerCallback);
1162
+ async set(releaseId, scheduleDate) {
1163
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1164
+ if (!release2) {
1165
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1166
+ }
1167
+ const job = scheduleJob(scheduleDate, async () => {
1168
+ try {
1169
+ await getService("release").publish(releaseId);
1170
+ } catch (error) {
1171
+ }
1172
+ this.cancel(releaseId);
1173
+ });
1174
+ if (scheduledJobs.has(releaseId)) {
1175
+ this.cancel(releaseId);
1176
+ }
1177
+ scheduledJobs.set(releaseId, job);
1178
+ return scheduledJobs;
598
1179
  },
599
- destroyAllListeners() {
600
- if (!state.destroyListenerCallbacks.length) {
601
- return;
1180
+ cancel(releaseId) {
1181
+ if (scheduledJobs.has(releaseId)) {
1182
+ scheduledJobs.get(releaseId).cancel();
1183
+ scheduledJobs.delete(releaseId);
602
1184
  }
603
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
604
- destroyListenerCallback();
1185
+ return scheduledJobs;
1186
+ },
1187
+ getAll() {
1188
+ return scheduledJobs;
1189
+ },
1190
+ /**
1191
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1192
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1193
+ * This also could be used to sync different Strapi instances in case of a cluster
1194
+ */
1195
+ async syncFromDatabase() {
1196
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1197
+ where: {
1198
+ scheduledAt: {
1199
+ $gte: /* @__PURE__ */ new Date()
1200
+ },
1201
+ releasedAt: null
1202
+ }
605
1203
  });
1204
+ for (const release2 of releases) {
1205
+ this.set(release2.id, release2.scheduledAt);
1206
+ }
1207
+ return scheduledJobs;
606
1208
  }
607
1209
  };
608
1210
  };
609
1211
  const services = {
610
1212
  release: createReleaseService,
611
- "release-action": createReleaseActionService,
612
1213
  "release-validation": createReleaseValidationService,
613
- "event-manager": createEventManagerService
1214
+ scheduling: createSchedulingService
614
1215
  };
615
1216
  const RELEASE_SCHEMA = yup.object().shape({
616
- name: yup.string().trim().required()
1217
+ name: yup.string().trim().required(),
1218
+ scheduledAt: yup.string().nullable(),
1219
+ isScheduled: yup.boolean().optional(),
1220
+ time: yup.string().when("isScheduled", {
1221
+ is: true,
1222
+ then: yup.string().trim().required(),
1223
+ otherwise: yup.string().nullable()
1224
+ }),
1225
+ timezone: yup.string().when("isScheduled", {
1226
+ is: true,
1227
+ then: yup.string().required().nullable(),
1228
+ otherwise: yup.string().nullable()
1229
+ }),
1230
+ date: yup.string().when("isScheduled", {
1231
+ is: true,
1232
+ then: yup.string().required().nullable(),
1233
+ otherwise: yup.string().nullable()
1234
+ })
617
1235
  }).required().noUnknown();
618
1236
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
619
1237
  const releaseController = {
@@ -630,9 +1248,7 @@ const releaseController = {
630
1248
  const contentTypeUid = query.contentTypeUid;
631
1249
  const entryId = query.entryId;
632
1250
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
633
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
634
- hasEntryAttached
635
- });
1251
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
636
1252
  ctx.body = { data };
637
1253
  } else {
638
1254
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -648,26 +1264,30 @@ const releaseController = {
648
1264
  }
649
1265
  };
650
1266
  });
651
- ctx.body = { data, meta: { pagination } };
1267
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1268
+ where: {
1269
+ releasedAt: null
1270
+ }
1271
+ });
1272
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
652
1273
  }
653
1274
  },
654
1275
  async findOne(ctx) {
655
1276
  const id = ctx.params.id;
656
1277
  const releaseService = getService("release", { strapi });
657
1278
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
658
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
659
- ability: ctx.state.userAbility,
660
- model: RELEASE_MODEL_UID
661
- });
662
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1279
+ if (!release2) {
1280
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
1281
+ }
663
1282
  const count = await releaseService.countActions({
664
1283
  filters: {
665
1284
  release: id
666
1285
  }
667
1286
  });
668
- if (!release2) {
669
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
670
- }
1287
+ const sanitizedRelease = {
1288
+ ...release2,
1289
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1290
+ };
671
1291
  const data = {
672
1292
  ...sanitizedRelease,
673
1293
  actions: {
@@ -720,8 +1340,27 @@ const releaseController = {
720
1340
  const id = ctx.params.id;
721
1341
  const releaseService = getService("release", { strapi });
722
1342
  const release2 = await releaseService.publish(id, { user });
1343
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1344
+ releaseService.countActions({
1345
+ filters: {
1346
+ release: id,
1347
+ type: "publish"
1348
+ }
1349
+ }),
1350
+ releaseService.countActions({
1351
+ filters: {
1352
+ release: id,
1353
+ type: "unpublish"
1354
+ }
1355
+ })
1356
+ ]);
723
1357
  ctx.body = {
724
- data: release2
1358
+ data: release2,
1359
+ meta: {
1360
+ totalEntries: countPublishActions + countUnpublishActions,
1361
+ totalPublishedEntries: countPublishActions,
1362
+ totalUnpublishedEntries: countUnpublishActions
1363
+ }
725
1364
  };
726
1365
  }
727
1366
  };
@@ -748,6 +1387,38 @@ const releaseActionController = {
748
1387
  data: releaseAction2
749
1388
  };
750
1389
  },
1390
+ async createMany(ctx) {
1391
+ const releaseId = ctx.params.releaseId;
1392
+ const releaseActionsArgs = ctx.request.body;
1393
+ await Promise.all(
1394
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1395
+ );
1396
+ const releaseService = getService("release", { strapi });
1397
+ const releaseActions = await strapi.db.transaction(async () => {
1398
+ const releaseActions2 = await Promise.all(
1399
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1400
+ try {
1401
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1402
+ return action;
1403
+ } catch (error) {
1404
+ if (error instanceof AlreadyOnReleaseError) {
1405
+ return null;
1406
+ }
1407
+ throw error;
1408
+ }
1409
+ })
1410
+ );
1411
+ return releaseActions2;
1412
+ });
1413
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1414
+ ctx.body = {
1415
+ data: newReleaseActions,
1416
+ meta: {
1417
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1418
+ totalEntries: releaseActions.length
1419
+ }
1420
+ };
1421
+ },
751
1422
  async findMany(ctx) {
752
1423
  const releaseId = ctx.params.releaseId;
753
1424
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -760,11 +1431,30 @@ const releaseActionController = {
760
1431
  sort: query.groupBy === "action" ? "type" : query.groupBy,
761
1432
  ...query
762
1433
  });
763
- const groupedData = await releaseService.groupActions(results, query.groupBy);
1434
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1435
+ if (acc[action.contentType]) {
1436
+ return acc;
1437
+ }
1438
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1439
+ ability: ctx.state.userAbility,
1440
+ model: action.contentType
1441
+ });
1442
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1443
+ return acc;
1444
+ }, {});
1445
+ const sanitizedResults = await mapAsync(results, async (action) => ({
1446
+ ...action,
1447
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1448
+ }));
1449
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1450
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1451
+ const components = await releaseService.getAllComponents();
764
1452
  ctx.body = {
765
1453
  data: groupedData,
766
1454
  meta: {
767
- pagination
1455
+ pagination,
1456
+ contentTypes: contentTypes2,
1457
+ components
768
1458
  }
769
1459
  };
770
1460
  },
@@ -914,6 +1604,22 @@ const releaseAction = {
914
1604
  ]
915
1605
  }
916
1606
  },
1607
+ {
1608
+ method: "POST",
1609
+ path: "/:releaseId/actions/bulk",
1610
+ handler: "release-action.createMany",
1611
+ config: {
1612
+ policies: [
1613
+ "admin::isAuthenticatedAdmin",
1614
+ {
1615
+ name: "admin::hasPermissions",
1616
+ config: {
1617
+ actions: ["plugin::content-releases.create-action"]
1618
+ }
1619
+ }
1620
+ ]
1621
+ }
1622
+ },
917
1623
  {
918
1624
  method: "GET",
919
1625
  path: "/:releaseId/actions",
@@ -970,22 +1676,21 @@ const routes = {
970
1676
  };
971
1677
  const { features } = require("@strapi/strapi/dist/utils/ee");
972
1678
  const getPlugin = () => {
973
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1679
+ if (features.isEnabled("cms-content-releases")) {
974
1680
  return {
975
1681
  register,
976
1682
  bootstrap,
1683
+ destroy,
977
1684
  contentTypes,
978
1685
  services,
979
1686
  controllers,
980
- routes,
981
- destroy() {
982
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
983
- getService("event-manager").destroyAllListeners();
984
- }
985
- }
1687
+ routes
986
1688
  };
987
1689
  }
988
1690
  return {
1691
+ // Always return register, it handles its own feature check
1692
+ register,
1693
+ // Always return contentTypes to avoid losing data when the feature is disabled
989
1694
  contentTypes
990
1695
  };
991
1696
  };