@strapi/content-releases 0.0.0-next.6d59515520a3850456f256fb0e4c54b75054ddf4 → 0.0.0-next.78ea7925e0dad75936ae2e937a041a0666e3d65a

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 (29) hide show
  1. package/dist/_chunks/{App-L1jSxCiL.mjs → App-bpzO2Ljh.mjs} +762 -454
  2. package/dist/_chunks/App-bpzO2Ljh.mjs.map +1 -0
  3. package/dist/_chunks/{App-_20W9dYa.js → App-p8aKBitd.js} +754 -445
  4. package/dist/_chunks/App-p8aKBitd.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-MyLPoISH.mjs → en-WuuhP6Bn.mjs} +21 -6
  10. package/dist/_chunks/en-WuuhP6Bn.mjs.map +1 -0
  11. package/dist/_chunks/{en-gYDqKYFd.js → en-gcJJ5htG.js} +21 -6
  12. package/dist/_chunks/en-gcJJ5htG.js.map +1 -0
  13. package/dist/_chunks/{index-c4zRX_sg.mjs → index-AECgcaDa.mjs} +171 -45
  14. package/dist/_chunks/index-AECgcaDa.mjs.map +1 -0
  15. package/dist/_chunks/{index-KJa1Rb5F.js → index-fP3qoWZ4.js} +159 -33
  16. package/dist/_chunks/index-fP3qoWZ4.js.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +2 -2
  19. package/dist/server/index.js +952 -397
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +951 -397
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +14 -11
  24. package/dist/_chunks/App-L1jSxCiL.mjs.map +0 -1
  25. package/dist/_chunks/App-_20W9dYa.js.map +0 -1
  26. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  27. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  28. package/dist/_chunks/index-KJa1Rb5F.js.map +0 -1
  29. package/dist/_chunks/index-c4zRX_sg.mjs.map +0 -1
@@ -1,7 +1,9 @@
1
1
  import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import isEqual from "lodash/isEqual";
2
3
  import { difference, keys } from "lodash";
3
4
  import _ from "lodash/fp";
4
5
  import EE from "@strapi/strapi/dist/utils/ee";
6
+ import { scheduleJob } from "node-schedule";
5
7
  import * as yup from "yup";
6
8
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
7
9
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -49,6 +51,32 @@ const ACTIONS = [
49
51
  pluginName: "content-releases"
50
52
  }
51
53
  ];
54
+ const ALLOWED_WEBHOOK_EVENTS = {
55
+ RELEASES_PUBLISH: "releases.publish"
56
+ };
57
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
58
+ return strapi2.plugin("content-releases").service(name);
59
+ };
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
+ };
52
80
  async function deleteActionsOnDisableDraftAndPublish({
53
81
  oldContentTypes,
54
82
  contentTypes: contentTypes2
@@ -75,28 +103,196 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
75
103
  });
76
104
  }
77
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
116
+ }
117
+ }
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
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
166
+ );
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
+ }
78
250
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
79
251
  const register = async ({ strapi: strapi2 }) => {
80
252
  if (features$2.isEnabled("cms-content-releases")) {
81
253
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
82
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
83
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
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();
84
261
  }
85
262
  };
86
263
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
87
264
  const bootstrap = async ({ strapi: strapi2 }) => {
88
265
  if (features$1.isEnabled("cms-content-releases")) {
266
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
+ );
89
269
  strapi2.db.lifecycles.subscribe({
90
- afterDelete(event) {
91
- const { model, result } = event;
92
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
93
- const { id } = result;
94
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
95
- where: {
96
- target_type: model.uid,
97
- 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);
98
292
  }
99
- });
293
+ }
294
+ } catch (error) {
295
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
100
296
  }
101
297
  },
102
298
  /**
@@ -116,20 +312,98 @@ const bootstrap = async ({ strapi: strapi2 }) => {
116
312
  * We make this only after deleteMany is succesfully executed to avoid errors
117
313
  */
118
314
  async afterDeleteMany(event) {
119
- const { model, state } = event;
120
- const entriesToDelete = state.entriesToDelete;
121
- if (entriesToDelete) {
122
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
123
- where: {
124
- target_type: model.uid,
125
- target_id: {
126
- $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
+ }
127
337
  }
338
+ });
339
+ for (const release2 of releases) {
340
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
128
341
  }
342
+ }
343
+ } catch (error) {
344
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
+ error
129
346
  });
130
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
+ }
131
384
  }
132
385
  });
386
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
387
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
388
+ strapi2.log.error(
389
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
390
+ );
391
+ throw err;
392
+ });
393
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
394
+ strapi2.webhookStore.addAllowedEvent(key, value);
395
+ });
396
+ }
397
+ }
398
+ };
399
+ const destroy = async ({ strapi: strapi2 }) => {
400
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
401
+ const scheduledJobs = getService("scheduling", {
402
+ strapi: strapi2
403
+ }).getAll();
404
+ for (const [, job] of scheduledJobs) {
405
+ job.cancel();
406
+ }
133
407
  }
134
408
  };
135
409
  const schema$1 = {
@@ -158,6 +432,17 @@ const schema$1 = {
158
432
  releasedAt: {
159
433
  type: "datetime"
160
434
  },
435
+ scheduledAt: {
436
+ type: "datetime"
437
+ },
438
+ timezone: {
439
+ type: "string"
440
+ },
441
+ status: {
442
+ type: "enumeration",
443
+ enum: ["ready", "blocked", "failed", "done", "empty"],
444
+ required: true
445
+ },
161
446
  actions: {
162
447
  type: "relation",
163
448
  relation: "oneToMany",
@@ -210,6 +495,9 @@ const schema = {
210
495
  relation: "manyToOne",
211
496
  target: RELEASE_MODEL_UID,
212
497
  inversedBy: "actions"
498
+ },
499
+ isEntryValid: {
500
+ type: "boolean"
213
501
  }
214
502
  }
215
503
  };
@@ -220,9 +508,6 @@ const contentTypes = {
220
508
  release: release$1,
221
509
  "release-action": releaseAction$1
222
510
  };
223
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
- return strapi2.plugin("content-releases").service(name);
225
- };
226
511
  const getGroupName = (queryValue) => {
227
512
  switch (queryValue) {
228
513
  case "contentType":
@@ -235,415 +520,581 @@ const getGroupName = (queryValue) => {
235
520
  return "contentType.displayName";
236
521
  }
237
522
  };
238
- const createReleaseService = ({ strapi: strapi2 }) => ({
239
- async create(releaseData, { user }) {
240
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
241
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
- "release-validation",
243
- { strapi: strapi2 }
244
- );
245
- await Promise.all([
246
- validatePendingReleasesLimit(),
247
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
248
- ]);
249
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
250
- data: releaseWithCreatorFields
523
+ const createReleaseService = ({ strapi: strapi2 }) => {
524
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
525
+ strapi2.eventHub.emit(event, {
526
+ isPublished,
527
+ error,
528
+ release: release2
251
529
  });
252
- },
253
- async findOne(id, query = {}) {
254
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
255
- ...query
530
+ };
531
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
532
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
533
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
534
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
535
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
536
+ try {
537
+ if (actionType === "publish") {
538
+ await entityManagerService.publish(entry, uid);
539
+ } else {
540
+ await entityManagerService.unpublish(entry, uid);
541
+ }
542
+ } catch (error) {
543
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
544
+ ;
545
+ else {
546
+ throw error;
547
+ }
548
+ }
549
+ };
550
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
551
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
552
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
553
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
554
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
555
+ filters: {
556
+ id: {
557
+ $in: entriesToPublishIds
558
+ }
559
+ },
560
+ populate
256
561
  });
257
- return release2;
258
- },
259
- findPage(query) {
260
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
261
- ...query,
262
- populate: {
263
- actions: {
264
- // @ts-expect-error Ignore missing properties
265
- count: true
562
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
563
+ filters: {
564
+ id: {
565
+ $in: entriestoUnpublishIds
266
566
  }
267
- }
567
+ },
568
+ populate
268
569
  });
269
- },
270
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
271
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
570
+ if (entriesToPublish.length > 0) {
571
+ await entityManagerService.publishMany(entriesToPublish, uid);
572
+ }
573
+ if (entriesToUnpublish.length > 0) {
574
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
575
+ }
576
+ };
577
+ const getFormattedActions = async (releaseId) => {
578
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
272
579
  where: {
273
- actions: {
274
- target_type: contentTypeUid,
275
- target_id: entryId
276
- },
277
- releasedAt: {
278
- $null: true
580
+ release: {
581
+ id: releaseId
279
582
  }
280
583
  },
281
584
  populate: {
282
- // Filter the action to get only the content type entry
283
- actions: {
284
- where: {
285
- target_type: contentTypeUid,
286
- target_id: entryId
287
- }
585
+ entry: {
586
+ fields: ["id"]
288
587
  }
289
588
  }
290
589
  });
291
- return releases.map((release2) => {
292
- if (release2.actions?.length) {
293
- const [actionForEntry] = release2.actions;
294
- delete release2.actions;
295
- return {
296
- ...release2,
297
- action: actionForEntry
298
- };
590
+ if (actions.length === 0) {
591
+ throw new errors.ValidationError("No entries to publish");
592
+ }
593
+ const collectionTypeActions = {};
594
+ const singleTypeActions = [];
595
+ for (const action of actions) {
596
+ const contentTypeUid = action.contentType;
597
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
598
+ if (!collectionTypeActions[contentTypeUid]) {
599
+ collectionTypeActions[contentTypeUid] = {
600
+ entriesToPublishIds: [],
601
+ entriesToUnpublishIds: []
602
+ };
603
+ }
604
+ if (action.type === "publish") {
605
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
606
+ } else {
607
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
608
+ }
609
+ } else {
610
+ singleTypeActions.push({
611
+ uid: contentTypeUid,
612
+ action: action.type,
613
+ id: action.entry.id
614
+ });
299
615
  }
300
- return release2;
301
- });
302
- },
303
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
304
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
- where: {
306
- releasedAt: {
307
- $null: true
308
- },
309
- actions: {
310
- target_type: contentTypeUid,
311
- target_id: entryId
616
+ }
617
+ return { collectionTypeActions, singleTypeActions };
618
+ };
619
+ return {
620
+ async create(releaseData, { user }) {
621
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
622
+ const {
623
+ validatePendingReleasesLimit,
624
+ validateUniqueNameForPendingRelease,
625
+ validateScheduledAtIsLaterThanNow
626
+ } = getService("release-validation", { strapi: strapi2 });
627
+ await Promise.all([
628
+ validatePendingReleasesLimit(),
629
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
630
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
631
+ ]);
632
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
633
+ data: {
634
+ ...releaseWithCreatorFields,
635
+ status: "empty"
312
636
  }
637
+ });
638
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
639
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
640
+ await schedulingService.set(release2.id, release2.scheduledAt);
313
641
  }
314
- });
315
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
316
- where: {
317
- $or: [
318
- {
319
- id: {
320
- $notIn: releasesRelated.map((release2) => release2.id)
642
+ strapi2.telemetry.send("didCreateContentRelease");
643
+ return release2;
644
+ },
645
+ async findOne(id, query = {}) {
646
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
647
+ ...query
648
+ });
649
+ return release2;
650
+ },
651
+ findPage(query) {
652
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
653
+ ...query,
654
+ populate: {
655
+ actions: {
656
+ // @ts-expect-error Ignore missing properties
657
+ count: true
658
+ }
659
+ }
660
+ });
661
+ },
662
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
663
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
664
+ where: {
665
+ actions: {
666
+ target_type: contentTypeUid,
667
+ target_id: entryId
668
+ },
669
+ releasedAt: {
670
+ $null: true
671
+ }
672
+ },
673
+ populate: {
674
+ // Filter the action to get only the content type entry
675
+ actions: {
676
+ where: {
677
+ target_type: contentTypeUid,
678
+ target_id: entryId
321
679
  }
680
+ }
681
+ }
682
+ });
683
+ return releases.map((release2) => {
684
+ if (release2.actions?.length) {
685
+ const [actionForEntry] = release2.actions;
686
+ delete release2.actions;
687
+ return {
688
+ ...release2,
689
+ action: actionForEntry
690
+ };
691
+ }
692
+ return release2;
693
+ });
694
+ },
695
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
696
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
697
+ where: {
698
+ releasedAt: {
699
+ $null: true
322
700
  },
323
- {
324
- actions: null
701
+ actions: {
702
+ target_type: contentTypeUid,
703
+ target_id: entryId
325
704
  }
326
- ],
327
- releasedAt: {
328
- $null: true
329
705
  }
706
+ });
707
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
708
+ where: {
709
+ $or: [
710
+ {
711
+ id: {
712
+ $notIn: releasesRelated.map((release2) => release2.id)
713
+ }
714
+ },
715
+ {
716
+ actions: null
717
+ }
718
+ ],
719
+ releasedAt: {
720
+ $null: true
721
+ }
722
+ }
723
+ });
724
+ return releases.map((release2) => {
725
+ if (release2.actions?.length) {
726
+ const [actionForEntry] = release2.actions;
727
+ delete release2.actions;
728
+ return {
729
+ ...release2,
730
+ action: actionForEntry
731
+ };
732
+ }
733
+ return release2;
734
+ });
735
+ },
736
+ async update(id, releaseData, { user }) {
737
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
738
+ releaseData
739
+ );
740
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
741
+ "release-validation",
742
+ { strapi: strapi2 }
743
+ );
744
+ await Promise.all([
745
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
746
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
747
+ ]);
748
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
749
+ if (!release2) {
750
+ throw new errors.NotFoundError(`No release found for id ${id}`);
330
751
  }
331
- });
332
- return releases.map((release2) => {
333
- if (release2.actions?.length) {
334
- const [actionForEntry] = release2.actions;
335
- delete release2.actions;
336
- return {
337
- ...release2,
338
- action: actionForEntry
339
- };
752
+ if (release2.releasedAt) {
753
+ throw new errors.ValidationError("Release already published");
340
754
  }
341
- return release2;
342
- });
343
- },
344
- async update(id, releaseData, { user }) {
345
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
346
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
347
- if (!release2) {
348
- throw new errors.NotFoundError(`No release found for id ${id}`);
349
- }
350
- if (release2.releasedAt) {
351
- throw new errors.ValidationError("Release already published");
352
- }
353
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
354
- /*
355
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
356
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
357
- */
358
- // @ts-expect-error see above
359
- data: releaseWithCreatorFields
360
- });
361
- return updatedRelease;
362
- },
363
- async createAction(releaseId, action) {
364
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
365
- strapi: strapi2
366
- });
367
- await Promise.all([
368
- validateEntryContentType(action.entry.contentType),
369
- validateUniqueEntry(releaseId, action)
370
- ]);
371
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
372
- if (!release2) {
373
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
374
- }
375
- if (release2.releasedAt) {
376
- throw new errors.ValidationError("Release already published");
377
- }
378
- const { entry, type } = action;
379
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
380
- data: {
381
- type,
382
- contentType: entry.contentType,
383
- locale: entry.locale,
384
- entry: {
385
- id: entry.id,
386
- __type: entry.contentType,
387
- __pivot: { field: "entry" }
388
- },
389
- release: releaseId
390
- },
391
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
392
- });
393
- },
394
- async findActions(releaseId, query) {
395
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
396
- fields: ["id"]
397
- });
398
- if (!release2) {
399
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
400
- }
401
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
402
- ...query,
403
- populate: {
404
- entry: {
405
- populate: "*"
755
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
756
+ /*
757
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
758
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
759
+ */
760
+ // @ts-expect-error see above
761
+ data: releaseWithCreatorFields
762
+ });
763
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
764
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
765
+ if (releaseData.scheduledAt) {
766
+ await schedulingService.set(id, releaseData.scheduledAt);
767
+ } else if (release2.scheduledAt) {
768
+ schedulingService.cancel(id);
406
769
  }
407
- },
408
- filters: {
409
- release: releaseId
410
770
  }
411
- });
412
- },
413
- async countActions(query) {
414
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
415
- },
416
- async groupActions(actions, groupBy) {
417
- const contentTypeUids = actions.reduce((acc, action) => {
418
- if (!acc.includes(action.contentType)) {
419
- acc.push(action.contentType);
771
+ this.updateReleaseStatus(id);
772
+ strapi2.telemetry.send("didUpdateContentRelease");
773
+ return updatedRelease;
774
+ },
775
+ async createAction(releaseId, action) {
776
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
777
+ strapi: strapi2
778
+ });
779
+ await Promise.all([
780
+ validateEntryContentType(action.entry.contentType),
781
+ validateUniqueEntry(releaseId, action)
782
+ ]);
783
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
784
+ if (!release2) {
785
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
420
786
  }
421
- return acc;
422
- }, []);
423
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
424
- contentTypeUids
425
- );
426
- const allLocalesDictionary = await this.getLocalesDataForActions();
427
- const formattedData = actions.map((action) => {
428
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
429
- return {
430
- ...action,
431
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
432
- contentType: {
433
- displayName,
434
- mainFieldValue: action.entry[mainField],
435
- uid: action.contentType
436
- }
437
- };
438
- });
439
- const groupName = getGroupName(groupBy);
440
- return _.groupBy(groupName)(formattedData);
441
- },
442
- async getLocalesDataForActions() {
443
- if (!strapi2.plugin("i18n")) {
444
- return {};
445
- }
446
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
447
- return allLocales.reduce((acc, locale) => {
448
- acc[locale.code] = { name: locale.name, code: locale.code };
449
- return acc;
450
- }, {});
451
- },
452
- async getContentTypesDataForActions(contentTypesUids) {
453
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
454
- const contentTypesData = {};
455
- for (const contentTypeUid of contentTypesUids) {
456
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
457
- uid: contentTypeUid
787
+ if (release2.releasedAt) {
788
+ throw new errors.ValidationError("Release already published");
789
+ }
790
+ const { entry, type } = action;
791
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
792
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
793
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
794
+ data: {
795
+ type,
796
+ contentType: entry.contentType,
797
+ locale: entry.locale,
798
+ isEntryValid,
799
+ entry: {
800
+ id: entry.id,
801
+ __type: entry.contentType,
802
+ __pivot: { field: "entry" }
803
+ },
804
+ release: releaseId
805
+ },
806
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
807
  });
459
- contentTypesData[contentTypeUid] = {
460
- mainField: contentTypeConfig.settings.mainField,
461
- displayName: strapi2.getModel(contentTypeUid).info.displayName
462
- };
463
- }
464
- return contentTypesData;
465
- },
466
- getContentTypeModelsFromActions(actions) {
467
- const contentTypeUids = actions.reduce((acc, action) => {
468
- if (!acc.includes(action.contentType)) {
469
- acc.push(action.contentType);
808
+ this.updateReleaseStatus(releaseId);
809
+ return releaseAction2;
810
+ },
811
+ async findActions(releaseId, query) {
812
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
813
+ fields: ["id"]
814
+ });
815
+ if (!release2) {
816
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
470
817
  }
471
- return acc;
472
- }, []);
473
- const contentTypeModelsMap = contentTypeUids.reduce(
474
- (acc, contentTypeUid) => {
475
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
818
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
819
+ ...query,
820
+ populate: {
821
+ entry: {
822
+ populate: "*"
823
+ }
824
+ },
825
+ filters: {
826
+ release: releaseId
827
+ }
828
+ });
829
+ },
830
+ async countActions(query) {
831
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
832
+ },
833
+ async groupActions(actions, groupBy) {
834
+ const contentTypeUids = actions.reduce((acc, action) => {
835
+ if (!acc.includes(action.contentType)) {
836
+ acc.push(action.contentType);
837
+ }
476
838
  return acc;
477
- },
478
- {}
479
- );
480
- return contentTypeModelsMap;
481
- },
482
- async getAllComponents() {
483
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
484
- const components = await contentManagerComponentsService.findAllComponents();
485
- const componentsMap = components.reduce(
486
- (acc, component) => {
487
- acc[component.uid] = component;
839
+ }, []);
840
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
841
+ contentTypeUids
842
+ );
843
+ const allLocalesDictionary = await this.getLocalesDataForActions();
844
+ const formattedData = actions.map((action) => {
845
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
846
+ return {
847
+ ...action,
848
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
849
+ contentType: {
850
+ displayName,
851
+ mainFieldValue: action.entry[mainField],
852
+ uid: action.contentType
853
+ }
854
+ };
855
+ });
856
+ const groupName = getGroupName(groupBy);
857
+ return _.groupBy(groupName)(formattedData);
858
+ },
859
+ async getLocalesDataForActions() {
860
+ if (!strapi2.plugin("i18n")) {
861
+ return {};
862
+ }
863
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
864
+ return allLocales.reduce((acc, locale) => {
865
+ acc[locale.code] = { name: locale.name, code: locale.code };
488
866
  return acc;
489
- },
490
- {}
491
- );
492
- return componentsMap;
493
- },
494
- async delete(releaseId) {
495
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
496
- populate: {
497
- actions: {
498
- fields: ["id"]
499
- }
867
+ }, {});
868
+ },
869
+ async getContentTypesDataForActions(contentTypesUids) {
870
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
871
+ const contentTypesData = {};
872
+ for (const contentTypeUid of contentTypesUids) {
873
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
874
+ uid: contentTypeUid
875
+ });
876
+ contentTypesData[contentTypeUid] = {
877
+ mainField: contentTypeConfig.settings.mainField,
878
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
879
+ };
500
880
  }
501
- });
502
- if (!release2) {
503
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
504
- }
505
- if (release2.releasedAt) {
506
- throw new errors.ValidationError("Release already published");
507
- }
508
- await strapi2.db.transaction(async () => {
509
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
510
- where: {
511
- id: {
512
- $in: release2.actions.map((action) => action.id)
513
- }
881
+ return contentTypesData;
882
+ },
883
+ getContentTypeModelsFromActions(actions) {
884
+ const contentTypeUids = actions.reduce((acc, action) => {
885
+ if (!acc.includes(action.contentType)) {
886
+ acc.push(action.contentType);
514
887
  }
515
- });
516
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
517
- });
518
- return release2;
519
- },
520
- async publish(releaseId) {
521
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
522
- RELEASE_MODEL_UID,
523
- releaseId,
524
- {
888
+ return acc;
889
+ }, []);
890
+ const contentTypeModelsMap = contentTypeUids.reduce(
891
+ (acc, contentTypeUid) => {
892
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
893
+ return acc;
894
+ },
895
+ {}
896
+ );
897
+ return contentTypeModelsMap;
898
+ },
899
+ async getAllComponents() {
900
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
901
+ const components = await contentManagerComponentsService.findAllComponents();
902
+ const componentsMap = components.reduce(
903
+ (acc, component) => {
904
+ acc[component.uid] = component;
905
+ return acc;
906
+ },
907
+ {}
908
+ );
909
+ return componentsMap;
910
+ },
911
+ async delete(releaseId) {
912
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
525
913
  populate: {
526
914
  actions: {
527
- populate: {
528
- entry: {
529
- fields: ["id"]
530
- }
531
- }
915
+ fields: ["id"]
532
916
  }
533
917
  }
918
+ });
919
+ if (!release2) {
920
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
534
921
  }
535
- );
536
- if (!releaseWithPopulatedActionEntries) {
537
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
538
- }
539
- if (releaseWithPopulatedActionEntries.releasedAt) {
540
- throw new errors.ValidationError("Release already published");
541
- }
542
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
543
- throw new errors.ValidationError("No entries to publish");
544
- }
545
- const actions = {};
546
- for (const action of releaseWithPopulatedActionEntries.actions) {
547
- const contentTypeUid = action.contentType;
548
- if (!actions[contentTypeUid]) {
549
- actions[contentTypeUid] = {
550
- entriestoPublishIds: [],
551
- entriesToUnpublishIds: []
552
- };
922
+ if (release2.releasedAt) {
923
+ throw new errors.ValidationError("Release already published");
553
924
  }
554
- if (action.type === "publish") {
555
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
556
- } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
925
+ await strapi2.db.transaction(async () => {
926
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
927
+ where: {
928
+ id: {
929
+ $in: release2.actions.map((action) => action.id)
930
+ }
931
+ }
932
+ });
933
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
934
+ });
935
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
936
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
937
+ await schedulingService.cancel(release2.id);
558
938
  }
559
- }
560
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
562
- await strapi2.db.transaction(async () => {
563
- for (const contentTypeUid of Object.keys(actions)) {
564
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
566
- const entriesToPublish = await strapi2.entityService.findMany(
567
- contentTypeUid,
568
- {
569
- filters: {
570
- id: {
571
- $in: entriestoPublishIds
572
- }
939
+ strapi2.telemetry.send("didDeleteContentRelease");
940
+ return release2;
941
+ },
942
+ async publish(releaseId) {
943
+ const {
944
+ release: release2,
945
+ error
946
+ } = await strapi2.db.transaction(async ({ trx }) => {
947
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
948
+ if (!lockedRelease) {
949
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
950
+ }
951
+ if (lockedRelease.releasedAt) {
952
+ throw new errors.ValidationError("Release already published");
953
+ }
954
+ if (lockedRelease.status === "failed") {
955
+ throw new errors.ValidationError("Release failed to publish");
956
+ }
957
+ try {
958
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
959
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
960
+ releaseId
961
+ );
962
+ await strapi2.db.transaction(async () => {
963
+ for (const { uid, action, id } of singleTypeActions) {
964
+ await publishSingleTypeAction(uid, action, id);
965
+ }
966
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
967
+ const uid = contentTypeUid;
968
+ await publishCollectionTypeAction(
969
+ uid,
970
+ collectionTypeActions[uid].entriesToPublishIds,
971
+ collectionTypeActions[uid].entriesToUnpublishIds
972
+ );
973
+ }
974
+ });
975
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
976
+ where: {
977
+ id: releaseId
573
978
  },
574
- populate
979
+ data: {
980
+ status: "done",
981
+ releasedAt: /* @__PURE__ */ new Date()
982
+ }
983
+ });
984
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
985
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
986
+ isPublished: true,
987
+ release: release22
988
+ });
575
989
  }
576
- );
577
- const entriesToUnpublish = await strapi2.entityService.findMany(
578
- contentTypeUid,
579
- {
580
- filters: {
581
- id: {
582
- $in: entriesToUnpublishIds
583
- }
584
- },
585
- populate
990
+ strapi2.telemetry.send("didPublishContentRelease");
991
+ return { release: release22, error: null };
992
+ } catch (error2) {
993
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
994
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
995
+ isPublished: false,
996
+ error: error2
997
+ });
586
998
  }
587
- );
588
- if (entriesToPublish.length > 0) {
589
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
590
- }
591
- if (entriesToUnpublish.length > 0) {
592
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
999
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1000
+ status: "failed"
1001
+ }).transacting(trx).execute();
1002
+ return {
1003
+ release: null,
1004
+ error: error2
1005
+ };
593
1006
  }
1007
+ });
1008
+ if (error) {
1009
+ throw error;
594
1010
  }
595
- });
596
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
597
- data: {
598
- /*
599
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
600
- */
601
- // @ts-expect-error see above
602
- releasedAt: /* @__PURE__ */ new Date()
1011
+ return release2;
1012
+ },
1013
+ async updateAction(actionId, releaseId, update) {
1014
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1015
+ where: {
1016
+ id: actionId,
1017
+ release: {
1018
+ id: releaseId,
1019
+ releasedAt: {
1020
+ $null: true
1021
+ }
1022
+ }
1023
+ },
1024
+ data: update
1025
+ });
1026
+ if (!updatedAction) {
1027
+ throw new errors.NotFoundError(
1028
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1029
+ );
603
1030
  }
604
- });
605
- return release2;
606
- },
607
- async updateAction(actionId, releaseId, update) {
608
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
609
- where: {
610
- id: actionId,
611
- release: {
612
- id: releaseId,
613
- releasedAt: {
614
- $null: true
1031
+ return updatedAction;
1032
+ },
1033
+ async deleteAction(actionId, releaseId) {
1034
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1035
+ where: {
1036
+ id: actionId,
1037
+ release: {
1038
+ id: releaseId,
1039
+ releasedAt: {
1040
+ $null: true
1041
+ }
615
1042
  }
616
1043
  }
617
- },
618
- data: update
619
- });
620
- if (!updatedAction) {
621
- throw new errors.NotFoundError(
622
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
623
- );
624
- }
625
- return updatedAction;
626
- },
627
- async deleteAction(actionId, releaseId) {
628
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
629
- where: {
630
- id: actionId,
631
- release: {
632
- id: releaseId,
633
- releasedAt: {
634
- $null: true
1044
+ });
1045
+ if (!deletedAction) {
1046
+ throw new errors.NotFoundError(
1047
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1048
+ );
1049
+ }
1050
+ this.updateReleaseStatus(releaseId);
1051
+ return deletedAction;
1052
+ },
1053
+ async updateReleaseStatus(releaseId) {
1054
+ const [totalActions, invalidActions] = await Promise.all([
1055
+ this.countActions({
1056
+ filters: {
1057
+ release: releaseId
1058
+ }
1059
+ }),
1060
+ this.countActions({
1061
+ filters: {
1062
+ release: releaseId,
1063
+ isEntryValid: false
635
1064
  }
1065
+ })
1066
+ ]);
1067
+ if (totalActions > 0) {
1068
+ if (invalidActions > 0) {
1069
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1070
+ where: {
1071
+ id: releaseId
1072
+ },
1073
+ data: {
1074
+ status: "blocked"
1075
+ }
1076
+ });
636
1077
  }
1078
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1079
+ where: {
1080
+ id: releaseId
1081
+ },
1082
+ data: {
1083
+ status: "ready"
1084
+ }
1085
+ });
637
1086
  }
638
- });
639
- if (!deletedAction) {
640
- throw new errors.NotFoundError(
641
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
642
- );
1087
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1088
+ where: {
1089
+ id: releaseId
1090
+ },
1091
+ data: {
1092
+ status: "empty"
1093
+ }
1094
+ });
643
1095
  }
644
- return deletedAction;
645
- }
646
- });
1096
+ };
1097
+ };
647
1098
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
648
1099
  async validateUniqueEntry(releaseId, releaseActionArgs) {
649
1100
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -688,27 +1139,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
1139
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
1140
  }
690
1141
  },
691
- async validateUniqueNameForPendingRelease(name) {
1142
+ async validateUniqueNameForPendingRelease(name, id) {
692
1143
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
1144
  filters: {
694
1145
  releasedAt: {
695
1146
  $null: true
696
1147
  },
697
- name
1148
+ name,
1149
+ ...id && { id: { $ne: id } }
698
1150
  }
699
1151
  });
700
1152
  const isNameUnique = pendingReleases.length === 0;
701
1153
  if (!isNameUnique) {
702
1154
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
1155
  }
1156
+ },
1157
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1158
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1159
+ throw new errors.ValidationError("Scheduled at must be later than now");
1160
+ }
704
1161
  }
705
1162
  });
1163
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1164
+ const scheduledJobs = /* @__PURE__ */ new Map();
1165
+ return {
1166
+ async set(releaseId, scheduleDate) {
1167
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1168
+ if (!release2) {
1169
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1170
+ }
1171
+ const job = scheduleJob(scheduleDate, async () => {
1172
+ try {
1173
+ await getService("release").publish(releaseId);
1174
+ } catch (error) {
1175
+ }
1176
+ this.cancel(releaseId);
1177
+ });
1178
+ if (scheduledJobs.has(releaseId)) {
1179
+ this.cancel(releaseId);
1180
+ }
1181
+ scheduledJobs.set(releaseId, job);
1182
+ return scheduledJobs;
1183
+ },
1184
+ cancel(releaseId) {
1185
+ if (scheduledJobs.has(releaseId)) {
1186
+ scheduledJobs.get(releaseId).cancel();
1187
+ scheduledJobs.delete(releaseId);
1188
+ }
1189
+ return scheduledJobs;
1190
+ },
1191
+ getAll() {
1192
+ return scheduledJobs;
1193
+ },
1194
+ /**
1195
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1196
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1197
+ * This also could be used to sync different Strapi instances in case of a cluster
1198
+ */
1199
+ async syncFromDatabase() {
1200
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1201
+ where: {
1202
+ scheduledAt: {
1203
+ $gte: /* @__PURE__ */ new Date()
1204
+ },
1205
+ releasedAt: null
1206
+ }
1207
+ });
1208
+ for (const release2 of releases) {
1209
+ this.set(release2.id, release2.scheduledAt);
1210
+ }
1211
+ return scheduledJobs;
1212
+ }
1213
+ };
1214
+ };
706
1215
  const services = {
707
1216
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
1217
+ "release-validation": createReleaseValidationService,
1218
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
709
1219
  };
710
1220
  const RELEASE_SCHEMA = yup.object().shape({
711
- name: yup.string().trim().required()
1221
+ name: yup.string().trim().required(),
1222
+ scheduledAt: yup.string().nullable(),
1223
+ isScheduled: yup.boolean().optional(),
1224
+ time: yup.string().when("isScheduled", {
1225
+ is: true,
1226
+ then: yup.string().trim().required(),
1227
+ otherwise: yup.string().nullable()
1228
+ }),
1229
+ timezone: yup.string().when("isScheduled", {
1230
+ is: true,
1231
+ then: yup.string().required().nullable(),
1232
+ otherwise: yup.string().nullable()
1233
+ }),
1234
+ date: yup.string().when("isScheduled", {
1235
+ is: true,
1236
+ then: yup.string().required().nullable(),
1237
+ otherwise: yup.string().nullable()
1238
+ })
712
1239
  }).required().noUnknown();
713
1240
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
1241
  const releaseController = {
@@ -741,26 +1268,30 @@ const releaseController = {
741
1268
  }
742
1269
  };
743
1270
  });
744
- ctx.body = { data, meta: { pagination } };
1271
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1272
+ where: {
1273
+ releasedAt: null
1274
+ }
1275
+ });
1276
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
745
1277
  }
746
1278
  },
747
1279
  async findOne(ctx) {
748
1280
  const id = ctx.params.id;
749
1281
  const releaseService = getService("release", { strapi });
750
1282
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
751
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
752
- ability: ctx.state.userAbility,
753
- model: RELEASE_MODEL_UID
754
- });
755
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1283
+ if (!release2) {
1284
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
1285
+ }
756
1286
  const count = await releaseService.countActions({
757
1287
  filters: {
758
1288
  release: id
759
1289
  }
760
1290
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
1291
+ const sanitizedRelease = {
1292
+ ...release2,
1293
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1294
+ };
764
1295
  const data = {
765
1296
  ...sanitizedRelease,
766
1297
  actions: {
@@ -813,8 +1344,27 @@ const releaseController = {
813
1344
  const id = ctx.params.id;
814
1345
  const releaseService = getService("release", { strapi });
815
1346
  const release2 = await releaseService.publish(id, { user });
1347
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1348
+ releaseService.countActions({
1349
+ filters: {
1350
+ release: id,
1351
+ type: "publish"
1352
+ }
1353
+ }),
1354
+ releaseService.countActions({
1355
+ filters: {
1356
+ release: id,
1357
+ type: "unpublish"
1358
+ }
1359
+ })
1360
+ ]);
816
1361
  ctx.body = {
817
- data: release2
1362
+ data: release2,
1363
+ meta: {
1364
+ totalEntries: countPublishActions + countUnpublishActions,
1365
+ totalPublishedEntries: countPublishActions,
1366
+ totalUnpublishedEntries: countUnpublishActions
1367
+ }
818
1368
  };
819
1369
  }
820
1370
  };
@@ -1086,6 +1636,7 @@ const getPlugin = () => {
1086
1636
  return {
1087
1637
  register,
1088
1638
  bootstrap,
1639
+ destroy,
1089
1640
  contentTypes,
1090
1641
  services,
1091
1642
  controllers,
@@ -1093,6 +1644,9 @@ const getPlugin = () => {
1093
1644
  };
1094
1645
  }
1095
1646
  return {
1647
+ // Always return register, it handles its own feature check
1648
+ register,
1649
+ // Always return contentTypes to avoid losing data when the feature is disabled
1096
1650
  contentTypes
1097
1651
  };
1098
1652
  };