@strapi/content-releases 0.0.0-next.1168c576ca50587b1a4377082ee09d2375410204 → 0.0.0-next.16afe21665d27a27f5c86939ba362066eabf1982

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 (31) hide show
  1. package/LICENSE +17 -1
  2. package/dist/_chunks/{App-_20W9dYa.js → App-dLXY5ei3.js} +784 -468
  3. package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
  4. package/dist/_chunks/App-jrh58sXY.mjs +1330 -0
  5. package/dist/_chunks/App-jrh58sXY.mjs.map +1 -0
  6. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs +51 -0
  7. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  8. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js +51 -0
  9. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  10. package/dist/_chunks/{en-gYDqKYFd.js → en-HrREghh3.js} +30 -7
  11. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  12. package/dist/_chunks/{en-MyLPoISH.mjs → en-ltT1TlKQ.mjs} +30 -7
  13. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  14. package/dist/_chunks/{index-KJa1Rb5F.js → index-CVO0Rqdm.js} +469 -41
  15. package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
  16. package/dist/_chunks/{index-c4zRX_sg.mjs → index-PiOGBETy.mjs} +486 -58
  17. package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.mjs +2 -2
  20. package/dist/server/index.js +1066 -398
  21. package/dist/server/index.js.map +1 -1
  22. package/dist/server/index.mjs +1065 -398
  23. package/dist/server/index.mjs.map +1 -1
  24. package/package.json +16 -13
  25. package/dist/_chunks/App-L1jSxCiL.mjs +0 -1015
  26. package/dist/_chunks/App-L1jSxCiL.mjs.map +0 -1
  27. package/dist/_chunks/App-_20W9dYa.js.map +0 -1
  28. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  29. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  30. package/dist/_chunks/index-KJa1Rb5F.js.map +0 -1
  31. package/dist/_chunks/index-c4zRX_sg.mjs.map +0 -1
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const isEqual = require("lodash/isEqual");
3
4
  const lodash = require("lodash");
4
5
  const _ = require("lodash/fp");
5
6
  const EE = require("@strapi/strapi/dist/utils/ee");
7
+ const nodeSchedule = require("node-schedule");
6
8
  const yup = require("yup");
7
9
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
8
10
  function _interopNamespace(e) {
@@ -23,6 +25,7 @@ function _interopNamespace(e) {
23
25
  n.default = e;
24
26
  return Object.freeze(n);
25
27
  }
28
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
26
29
  const ___default = /* @__PURE__ */ _interopDefault(_);
27
30
  const EE__default = /* @__PURE__ */ _interopDefault(EE);
28
31
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
@@ -72,6 +75,32 @@ const ACTIONS = [
72
75
  pluginName: "content-releases"
73
76
  }
74
77
  ];
78
+ const ALLOWED_WEBHOOK_EVENTS = {
79
+ RELEASES_PUBLISH: "releases.publish"
80
+ };
81
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
82
+ return strapi2.plugin("content-releases").service(name);
83
+ };
84
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
85
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
86
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
87
+ const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
88
+ return entry;
89
+ };
90
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
+ try {
92
+ await strapi2.entityValidator.validateEntityCreation(
93
+ strapi2.getModel(contentTypeUid),
94
+ entry,
95
+ void 0,
96
+ // @ts-expect-error - FIXME: entity here is unnecessary
97
+ entry
98
+ );
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ };
75
104
  async function deleteActionsOnDisableDraftAndPublish({
76
105
  oldContentTypes,
77
106
  contentTypes: contentTypes2
@@ -98,28 +127,202 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
98
127
  });
99
128
  }
100
129
  }
130
+ async function migrateIsValidAndStatusReleases() {
131
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
132
+ where: {
133
+ status: null,
134
+ releasedAt: null
135
+ },
136
+ populate: {
137
+ actions: {
138
+ populate: {
139
+ entry: true
140
+ }
141
+ }
142
+ }
143
+ });
144
+ utils.mapAsync(releasesWithoutStatus, async (release2) => {
145
+ const actions = release2.actions;
146
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
147
+ for (const action of notValidatedActions) {
148
+ if (action.entry) {
149
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
150
+ strapi
151
+ });
152
+ if (populatedEntry) {
153
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
154
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
155
+ where: {
156
+ id: action.id
157
+ },
158
+ data: {
159
+ isEntryValid
160
+ }
161
+ });
162
+ }
163
+ }
164
+ }
165
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
166
+ });
167
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
168
+ where: {
169
+ status: null,
170
+ releasedAt: {
171
+ $notNull: true
172
+ }
173
+ }
174
+ });
175
+ utils.mapAsync(publishedReleases, async (release2) => {
176
+ return strapi.db.query(RELEASE_MODEL_UID).update({
177
+ where: {
178
+ id: release2.id
179
+ },
180
+ data: {
181
+ status: "done"
182
+ }
183
+ });
184
+ });
185
+ }
186
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
187
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
188
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
189
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
190
+ );
191
+ const releasesAffected = /* @__PURE__ */ new Set();
192
+ utils.mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
193
+ const oldContentType = oldContentTypes[contentTypeUID];
194
+ const contentType = contentTypes2[contentTypeUID];
195
+ if (!isEqual__default.default(oldContentType?.attributes, contentType?.attributes)) {
196
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
197
+ where: {
198
+ contentType: contentTypeUID
199
+ },
200
+ populate: {
201
+ entry: true,
202
+ release: true
203
+ }
204
+ });
205
+ await utils.mapAsync(actions, async (action) => {
206
+ if (action.entry && action.release) {
207
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
208
+ strapi
209
+ });
210
+ if (populatedEntry) {
211
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
212
+ strapi
213
+ });
214
+ releasesAffected.add(action.release.id);
215
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
216
+ where: {
217
+ id: action.id
218
+ },
219
+ data: {
220
+ isEntryValid
221
+ }
222
+ });
223
+ }
224
+ }
225
+ });
226
+ }
227
+ }).then(() => {
228
+ utils.mapAsync(releasesAffected, async (releaseId) => {
229
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
230
+ });
231
+ });
232
+ }
233
+ }
234
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
235
+ if (!oldContentTypes) {
236
+ return;
237
+ }
238
+ const i18nPlugin = strapi.plugin("i18n");
239
+ if (!i18nPlugin) {
240
+ return;
241
+ }
242
+ for (const uid in contentTypes2) {
243
+ if (!oldContentTypes[uid]) {
244
+ continue;
245
+ }
246
+ const oldContentType = oldContentTypes[uid];
247
+ const contentType = contentTypes2[uid];
248
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
249
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
250
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
251
+ locale: null
252
+ }).where({ contentType: uid }).execute();
253
+ }
254
+ }
255
+ }
256
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
257
+ if (!oldContentTypes) {
258
+ return;
259
+ }
260
+ const i18nPlugin = strapi.plugin("i18n");
261
+ if (!i18nPlugin) {
262
+ return;
263
+ }
264
+ for (const uid in contentTypes2) {
265
+ if (!oldContentTypes[uid]) {
266
+ continue;
267
+ }
268
+ const oldContentType = oldContentTypes[uid];
269
+ const contentType = contentTypes2[uid];
270
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
271
+ const { getDefaultLocale } = i18nPlugin.service("locales");
272
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
273
+ const defaultLocale = await getDefaultLocale();
274
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
275
+ locale: defaultLocale
276
+ }).where({ contentType: uid }).execute();
277
+ }
278
+ }
279
+ }
101
280
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
102
281
  const register = async ({ strapi: strapi2 }) => {
103
282
  if (features$2.isEnabled("cms-content-releases")) {
104
283
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
105
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
106
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
284
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
285
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
286
+ }
287
+ if (strapi2.plugin("graphql")) {
288
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
289
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
290
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
107
291
  }
108
292
  };
109
293
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
110
294
  const bootstrap = async ({ strapi: strapi2 }) => {
111
295
  if (features$1.isEnabled("cms-content-releases")) {
296
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
297
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
298
+ );
112
299
  strapi2.db.lifecycles.subscribe({
113
- afterDelete(event) {
114
- const { model, result } = event;
115
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
116
- const { id } = result;
117
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
118
- where: {
119
- target_type: model.uid,
120
- target_id: id
300
+ models: contentTypesWithDraftAndPublish,
301
+ async afterDelete(event) {
302
+ try {
303
+ const { model, result } = event;
304
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
305
+ const { id } = result;
306
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
307
+ where: {
308
+ actions: {
309
+ target_type: model.uid,
310
+ target_id: id
311
+ }
312
+ }
313
+ });
314
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
315
+ where: {
316
+ target_type: model.uid,
317
+ target_id: id
318
+ }
319
+ });
320
+ for (const release2 of releases) {
321
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
121
322
  }
122
- });
323
+ }
324
+ } catch (error) {
325
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
123
326
  }
124
327
  },
125
328
  /**
@@ -139,20 +342,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
139
342
  * We make this only after deleteMany is succesfully executed to avoid errors
140
343
  */
141
344
  async afterDeleteMany(event) {
142
- const { model, state } = event;
143
- const entriesToDelete = state.entriesToDelete;
144
- if (entriesToDelete) {
145
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
146
- where: {
147
- target_type: model.uid,
148
- target_id: {
149
- $in: entriesToDelete.map((entry) => entry.id)
345
+ try {
346
+ const { model, state } = event;
347
+ const entriesToDelete = state.entriesToDelete;
348
+ if (entriesToDelete) {
349
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
350
+ where: {
351
+ actions: {
352
+ target_type: model.uid,
353
+ target_id: {
354
+ $in: entriesToDelete.map(
355
+ (entry) => entry.id
356
+ )
357
+ }
358
+ }
359
+ }
360
+ });
361
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
362
+ where: {
363
+ target_type: model.uid,
364
+ target_id: {
365
+ $in: entriesToDelete.map((entry) => entry.id)
366
+ }
150
367
  }
368
+ });
369
+ for (const release2 of releases) {
370
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
151
371
  }
372
+ }
373
+ } catch (error) {
374
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
375
+ error
152
376
  });
153
377
  }
378
+ },
379
+ async afterUpdate(event) {
380
+ try {
381
+ const { model, result } = event;
382
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
383
+ const isEntryValid = await getEntryValidStatus(
384
+ model.uid,
385
+ result,
386
+ {
387
+ strapi: strapi2
388
+ }
389
+ );
390
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
391
+ where: {
392
+ target_type: model.uid,
393
+ target_id: result.id
394
+ },
395
+ data: {
396
+ isEntryValid
397
+ }
398
+ });
399
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
400
+ where: {
401
+ actions: {
402
+ target_type: model.uid,
403
+ target_id: result.id
404
+ }
405
+ }
406
+ });
407
+ for (const release2 of releases) {
408
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
409
+ }
410
+ }
411
+ } catch (error) {
412
+ strapi2.log.error("Error while updating release actions after entry update", { error });
413
+ }
154
414
  }
155
415
  });
416
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
417
+ strapi2.log.error(
418
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
419
+ );
420
+ throw err;
421
+ });
422
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
423
+ strapi2.webhookStore.addAllowedEvent(key, value);
424
+ });
425
+ }
426
+ };
427
+ const destroy = async ({ strapi: strapi2 }) => {
428
+ const scheduledJobs = getService("scheduling", {
429
+ strapi: strapi2
430
+ }).getAll();
431
+ for (const [, job] of scheduledJobs) {
432
+ job.cancel();
156
433
  }
157
434
  };
158
435
  const schema$1 = {
@@ -181,6 +458,17 @@ const schema$1 = {
181
458
  releasedAt: {
182
459
  type: "datetime"
183
460
  },
461
+ scheduledAt: {
462
+ type: "datetime"
463
+ },
464
+ timezone: {
465
+ type: "string"
466
+ },
467
+ status: {
468
+ type: "enumeration",
469
+ enum: ["ready", "blocked", "failed", "done", "empty"],
470
+ required: true
471
+ },
184
472
  actions: {
185
473
  type: "relation",
186
474
  relation: "oneToMany",
@@ -233,6 +521,9 @@ const schema = {
233
521
  relation: "manyToOne",
234
522
  target: RELEASE_MODEL_UID,
235
523
  inversedBy: "actions"
524
+ },
525
+ isEntryValid: {
526
+ type: "boolean"
236
527
  }
237
528
  }
238
529
  };
@@ -243,9 +534,6 @@ const contentTypes = {
243
534
  release: release$1,
244
535
  "release-action": releaseAction$1
245
536
  };
246
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
- return strapi2.plugin("content-releases").service(name);
248
- };
249
537
  const getGroupName = (queryValue) => {
250
538
  switch (queryValue) {
251
539
  case "contentType":
@@ -258,415 +546,596 @@ const getGroupName = (queryValue) => {
258
546
  return "contentType.displayName";
259
547
  }
260
548
  };
261
- const createReleaseService = ({ strapi: strapi2 }) => ({
262
- async create(releaseData, { user }) {
263
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
264
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
- "release-validation",
266
- { strapi: strapi2 }
267
- );
268
- await Promise.all([
269
- validatePendingReleasesLimit(),
270
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
271
- ]);
272
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
273
- data: releaseWithCreatorFields
549
+ const createReleaseService = ({ strapi: strapi2 }) => {
550
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
551
+ strapi2.eventHub.emit(event, {
552
+ isPublished,
553
+ error,
554
+ release: release2
274
555
  });
275
- },
276
- async findOne(id, query = {}) {
277
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
278
- ...query
556
+ };
557
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
558
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
559
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
560
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
561
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
562
+ try {
563
+ if (actionType === "publish") {
564
+ await entityManagerService.publish(entry, uid);
565
+ } else {
566
+ await entityManagerService.unpublish(entry, uid);
567
+ }
568
+ } catch (error) {
569
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
570
+ ;
571
+ else {
572
+ throw error;
573
+ }
574
+ }
575
+ };
576
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
577
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
578
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
579
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
580
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
581
+ filters: {
582
+ id: {
583
+ $in: entriesToPublishIds
584
+ }
585
+ },
586
+ populate
279
587
  });
280
- return release2;
281
- },
282
- findPage(query) {
283
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
284
- ...query,
285
- populate: {
286
- actions: {
287
- // @ts-expect-error Ignore missing properties
288
- count: true
588
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
589
+ filters: {
590
+ id: {
591
+ $in: entriestoUnpublishIds
289
592
  }
290
- }
593
+ },
594
+ populate
291
595
  });
292
- },
293
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
294
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
596
+ if (entriesToPublish.length > 0) {
597
+ await entityManagerService.publishMany(entriesToPublish, uid);
598
+ }
599
+ if (entriesToUnpublish.length > 0) {
600
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
601
+ }
602
+ };
603
+ const getFormattedActions = async (releaseId) => {
604
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
295
605
  where: {
296
- actions: {
297
- target_type: contentTypeUid,
298
- target_id: entryId
299
- },
300
- releasedAt: {
301
- $null: true
606
+ release: {
607
+ id: releaseId
302
608
  }
303
609
  },
304
610
  populate: {
305
- // Filter the action to get only the content type entry
306
- actions: {
307
- where: {
308
- target_type: contentTypeUid,
309
- target_id: entryId
310
- }
611
+ entry: {
612
+ fields: ["id"]
311
613
  }
312
614
  }
313
615
  });
314
- return releases.map((release2) => {
315
- if (release2.actions?.length) {
316
- const [actionForEntry] = release2.actions;
317
- delete release2.actions;
318
- return {
319
- ...release2,
320
- action: actionForEntry
321
- };
616
+ if (actions.length === 0) {
617
+ throw new utils.errors.ValidationError("No entries to publish");
618
+ }
619
+ const collectionTypeActions = {};
620
+ const singleTypeActions = [];
621
+ for (const action of actions) {
622
+ const contentTypeUid = action.contentType;
623
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
624
+ if (!collectionTypeActions[contentTypeUid]) {
625
+ collectionTypeActions[contentTypeUid] = {
626
+ entriesToPublishIds: [],
627
+ entriesToUnpublishIds: []
628
+ };
629
+ }
630
+ if (action.type === "publish") {
631
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
632
+ } else {
633
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
634
+ }
635
+ } else {
636
+ singleTypeActions.push({
637
+ uid: contentTypeUid,
638
+ action: action.type,
639
+ id: action.entry.id
640
+ });
641
+ }
642
+ }
643
+ return { collectionTypeActions, singleTypeActions };
644
+ };
645
+ return {
646
+ async create(releaseData, { user }) {
647
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
648
+ const {
649
+ validatePendingReleasesLimit,
650
+ validateUniqueNameForPendingRelease,
651
+ validateScheduledAtIsLaterThanNow
652
+ } = getService("release-validation", { strapi: strapi2 });
653
+ await Promise.all([
654
+ validatePendingReleasesLimit(),
655
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
656
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
657
+ ]);
658
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
659
+ data: {
660
+ ...releaseWithCreatorFields,
661
+ status: "empty"
662
+ }
663
+ });
664
+ if (releaseWithCreatorFields.scheduledAt) {
665
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
666
+ await schedulingService.set(release2.id, release2.scheduledAt);
322
667
  }
668
+ strapi2.telemetry.send("didCreateContentRelease");
323
669
  return release2;
324
- });
325
- },
326
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
327
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
328
- where: {
329
- releasedAt: {
330
- $null: true
331
- },
332
- actions: {
333
- target_type: contentTypeUid,
334
- target_id: entryId
670
+ },
671
+ async findOne(id, query = {}) {
672
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
673
+ ...query
674
+ });
675
+ return release2;
676
+ },
677
+ findPage(query) {
678
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
679
+ ...query,
680
+ populate: {
681
+ actions: {
682
+ // @ts-expect-error Ignore missing properties
683
+ count: true
684
+ }
335
685
  }
686
+ });
687
+ },
688
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
+ let entries = entriesIds;
690
+ if (!Array.isArray(entriesIds)) {
691
+ entries = [entriesIds];
336
692
  }
337
- });
338
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
339
- where: {
340
- $or: [
341
- {
342
- id: {
343
- $notIn: releasesRelated.map((release2) => release2.id)
693
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
694
+ where: {
695
+ actions: {
696
+ target_type: contentTypeUid,
697
+ target_id: {
698
+ $in: entries
344
699
  }
345
700
  },
346
- {
347
- actions: null
701
+ releasedAt: {
702
+ $null: true
703
+ }
704
+ },
705
+ populate: {
706
+ // Filter the action to get only the content type entry
707
+ actions: {
708
+ where: {
709
+ target_type: contentTypeUid,
710
+ target_id: {
711
+ $in: entries
712
+ }
713
+ },
714
+ populate: {
715
+ entry: {
716
+ select: ["id"]
717
+ }
718
+ }
719
+ }
720
+ }
721
+ });
722
+ return releases.map((release2) => {
723
+ if (release2.actions?.length) {
724
+ const actionsForEntry = release2.actions;
725
+ delete release2.actions;
726
+ return {
727
+ ...release2,
728
+ actions: actionsForEntry
729
+ };
730
+ }
731
+ return release2;
732
+ });
733
+ },
734
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
735
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
736
+ where: {
737
+ releasedAt: {
738
+ $null: true
739
+ },
740
+ actions: {
741
+ target_type: contentTypeUid,
742
+ target_id: entryId
348
743
  }
349
- ],
350
- releasedAt: {
351
- $null: true
352
744
  }
745
+ });
746
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
747
+ where: {
748
+ $or: [
749
+ {
750
+ id: {
751
+ $notIn: releasesRelated.map((release2) => release2.id)
752
+ }
753
+ },
754
+ {
755
+ actions: null
756
+ }
757
+ ],
758
+ releasedAt: {
759
+ $null: true
760
+ }
761
+ }
762
+ });
763
+ return releases.map((release2) => {
764
+ if (release2.actions?.length) {
765
+ const [actionForEntry] = release2.actions;
766
+ delete release2.actions;
767
+ return {
768
+ ...release2,
769
+ action: actionForEntry
770
+ };
771
+ }
772
+ return release2;
773
+ });
774
+ },
775
+ async update(id, releaseData, { user }) {
776
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
777
+ releaseData
778
+ );
779
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
780
+ "release-validation",
781
+ { strapi: strapi2 }
782
+ );
783
+ await Promise.all([
784
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
785
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
786
+ ]);
787
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
788
+ if (!release2) {
789
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
353
790
  }
354
- });
355
- return releases.map((release2) => {
356
- if (release2.actions?.length) {
357
- const [actionForEntry] = release2.actions;
358
- delete release2.actions;
359
- return {
360
- ...release2,
361
- action: actionForEntry
362
- };
791
+ if (release2.releasedAt) {
792
+ throw new utils.errors.ValidationError("Release already published");
363
793
  }
364
- return release2;
365
- });
366
- },
367
- async update(id, releaseData, { user }) {
368
- const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
369
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
370
- if (!release2) {
371
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
372
- }
373
- if (release2.releasedAt) {
374
- throw new utils.errors.ValidationError("Release already published");
375
- }
376
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
377
- /*
378
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
379
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
380
- */
381
- // @ts-expect-error see above
382
- data: releaseWithCreatorFields
383
- });
384
- return updatedRelease;
385
- },
386
- async createAction(releaseId, action) {
387
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
388
- strapi: strapi2
389
- });
390
- await Promise.all([
391
- validateEntryContentType(action.entry.contentType),
392
- validateUniqueEntry(releaseId, action)
393
- ]);
394
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
395
- if (!release2) {
396
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
397
- }
398
- if (release2.releasedAt) {
399
- throw new utils.errors.ValidationError("Release already published");
400
- }
401
- const { entry, type } = action;
402
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
403
- data: {
404
- type,
405
- contentType: entry.contentType,
406
- locale: entry.locale,
407
- entry: {
408
- id: entry.id,
409
- __type: entry.contentType,
410
- __pivot: { field: "entry" }
411
- },
412
- release: releaseId
413
- },
414
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
415
- });
416
- },
417
- async findActions(releaseId, query) {
418
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
419
- fields: ["id"]
420
- });
421
- if (!release2) {
422
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
423
- }
424
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
425
- ...query,
426
- populate: {
427
- entry: {
428
- populate: "*"
429
- }
430
- },
431
- filters: {
432
- release: releaseId
794
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
795
+ /*
796
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
797
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
798
+ */
799
+ // @ts-expect-error see above
800
+ data: releaseWithCreatorFields
801
+ });
802
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
803
+ if (releaseData.scheduledAt) {
804
+ await schedulingService.set(id, releaseData.scheduledAt);
805
+ } else if (release2.scheduledAt) {
806
+ schedulingService.cancel(id);
433
807
  }
434
- });
435
- },
436
- async countActions(query) {
437
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
438
- },
439
- async groupActions(actions, groupBy) {
440
- const contentTypeUids = actions.reduce((acc, action) => {
441
- if (!acc.includes(action.contentType)) {
442
- acc.push(action.contentType);
808
+ this.updateReleaseStatus(id);
809
+ strapi2.telemetry.send("didUpdateContentRelease");
810
+ return updatedRelease;
811
+ },
812
+ async createAction(releaseId, action, { disableUpdateReleaseStatus = false } = {}) {
813
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
814
+ strapi: strapi2
815
+ });
816
+ await Promise.all([
817
+ validateEntryContentType(action.entry.contentType),
818
+ validateUniqueEntry(releaseId, action)
819
+ ]);
820
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
821
+ if (!release2) {
822
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
823
+ }
824
+ if (release2.releasedAt) {
825
+ throw new utils.errors.ValidationError("Release already published");
826
+ }
827
+ const { entry, type } = action;
828
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
829
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
830
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
831
+ data: {
832
+ type,
833
+ contentType: entry.contentType,
834
+ locale: entry.locale,
835
+ isEntryValid,
836
+ entry: {
837
+ id: entry.id,
838
+ __type: entry.contentType,
839
+ __pivot: { field: "entry" }
840
+ },
841
+ release: releaseId
842
+ },
843
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
844
+ });
845
+ if (!disableUpdateReleaseStatus) {
846
+ this.updateReleaseStatus(releaseId);
443
847
  }
444
- return acc;
445
- }, []);
446
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
447
- contentTypeUids
448
- );
449
- const allLocalesDictionary = await this.getLocalesDataForActions();
450
- const formattedData = actions.map((action) => {
451
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
452
- return {
453
- ...action,
454
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
455
- contentType: {
456
- displayName,
457
- mainFieldValue: action.entry[mainField],
458
- uid: action.contentType
459
- }
460
- };
461
- });
462
- const groupName = getGroupName(groupBy);
463
- return ___default.default.groupBy(groupName)(formattedData);
464
- },
465
- async getLocalesDataForActions() {
466
- if (!strapi2.plugin("i18n")) {
467
- return {};
468
- }
469
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
470
- return allLocales.reduce((acc, locale) => {
471
- acc[locale.code] = { name: locale.name, code: locale.code };
472
- return acc;
473
- }, {});
474
- },
475
- async getContentTypesDataForActions(contentTypesUids) {
476
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
477
- const contentTypesData = {};
478
- for (const contentTypeUid of contentTypesUids) {
479
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
480
- uid: contentTypeUid
848
+ return releaseAction2;
849
+ },
850
+ async findActions(releaseId, query) {
851
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
852
+ fields: ["id"]
481
853
  });
482
- contentTypesData[contentTypeUid] = {
483
- mainField: contentTypeConfig.settings.mainField,
484
- displayName: strapi2.getModel(contentTypeUid).info.displayName
485
- };
486
- }
487
- return contentTypesData;
488
- },
489
- getContentTypeModelsFromActions(actions) {
490
- const contentTypeUids = actions.reduce((acc, action) => {
491
- if (!acc.includes(action.contentType)) {
492
- acc.push(action.contentType);
854
+ if (!release2) {
855
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
493
856
  }
494
- return acc;
495
- }, []);
496
- const contentTypeModelsMap = contentTypeUids.reduce(
497
- (acc, contentTypeUid) => {
498
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
857
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
858
+ ...query,
859
+ populate: {
860
+ entry: {
861
+ populate: "*"
862
+ }
863
+ },
864
+ filters: {
865
+ release: releaseId
866
+ }
867
+ });
868
+ },
869
+ async countActions(query) {
870
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
871
+ },
872
+ async groupActions(actions, groupBy) {
873
+ const contentTypeUids = actions.reduce((acc, action) => {
874
+ if (!acc.includes(action.contentType)) {
875
+ acc.push(action.contentType);
876
+ }
499
877
  return acc;
500
- },
501
- {}
502
- );
503
- return contentTypeModelsMap;
504
- },
505
- async getAllComponents() {
506
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
507
- const components = await contentManagerComponentsService.findAllComponents();
508
- const componentsMap = components.reduce(
509
- (acc, component) => {
510
- acc[component.uid] = component;
878
+ }, []);
879
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
880
+ contentTypeUids
881
+ );
882
+ const allLocalesDictionary = await this.getLocalesDataForActions();
883
+ const formattedData = actions.map((action) => {
884
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
885
+ return {
886
+ ...action,
887
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
888
+ contentType: {
889
+ displayName,
890
+ mainFieldValue: action.entry[mainField],
891
+ uid: action.contentType
892
+ }
893
+ };
894
+ });
895
+ const groupName = getGroupName(groupBy);
896
+ return ___default.default.groupBy(groupName)(formattedData);
897
+ },
898
+ async getLocalesDataForActions() {
899
+ if (!strapi2.plugin("i18n")) {
900
+ return {};
901
+ }
902
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
903
+ return allLocales.reduce((acc, locale) => {
904
+ acc[locale.code] = { name: locale.name, code: locale.code };
511
905
  return acc;
512
- },
513
- {}
514
- );
515
- return componentsMap;
516
- },
517
- async delete(releaseId) {
518
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
519
- populate: {
520
- actions: {
521
- fields: ["id"]
522
- }
906
+ }, {});
907
+ },
908
+ async getContentTypesDataForActions(contentTypesUids) {
909
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
910
+ const contentTypesData = {};
911
+ for (const contentTypeUid of contentTypesUids) {
912
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
913
+ uid: contentTypeUid
914
+ });
915
+ contentTypesData[contentTypeUid] = {
916
+ mainField: contentTypeConfig.settings.mainField,
917
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
918
+ };
523
919
  }
524
- });
525
- if (!release2) {
526
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
527
- }
528
- if (release2.releasedAt) {
529
- throw new utils.errors.ValidationError("Release already published");
530
- }
531
- await strapi2.db.transaction(async () => {
532
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
533
- where: {
534
- id: {
535
- $in: release2.actions.map((action) => action.id)
536
- }
920
+ return contentTypesData;
921
+ },
922
+ getContentTypeModelsFromActions(actions) {
923
+ const contentTypeUids = actions.reduce((acc, action) => {
924
+ if (!acc.includes(action.contentType)) {
925
+ acc.push(action.contentType);
537
926
  }
538
- });
539
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
540
- });
541
- return release2;
542
- },
543
- async publish(releaseId) {
544
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
545
- RELEASE_MODEL_UID,
546
- releaseId,
547
- {
927
+ return acc;
928
+ }, []);
929
+ const contentTypeModelsMap = contentTypeUids.reduce(
930
+ (acc, contentTypeUid) => {
931
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
932
+ return acc;
933
+ },
934
+ {}
935
+ );
936
+ return contentTypeModelsMap;
937
+ },
938
+ async getAllComponents() {
939
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
940
+ const components = await contentManagerComponentsService.findAllComponents();
941
+ const componentsMap = components.reduce(
942
+ (acc, component) => {
943
+ acc[component.uid] = component;
944
+ return acc;
945
+ },
946
+ {}
947
+ );
948
+ return componentsMap;
949
+ },
950
+ async delete(releaseId) {
951
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
548
952
  populate: {
549
953
  actions: {
550
- populate: {
551
- entry: {
552
- fields: ["id"]
553
- }
554
- }
954
+ fields: ["id"]
555
955
  }
556
956
  }
957
+ });
958
+ if (!release2) {
959
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
557
960
  }
558
- );
559
- if (!releaseWithPopulatedActionEntries) {
560
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
561
- }
562
- if (releaseWithPopulatedActionEntries.releasedAt) {
563
- throw new utils.errors.ValidationError("Release already published");
564
- }
565
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
566
- throw new utils.errors.ValidationError("No entries to publish");
567
- }
568
- const actions = {};
569
- for (const action of releaseWithPopulatedActionEntries.actions) {
570
- const contentTypeUid = action.contentType;
571
- if (!actions[contentTypeUid]) {
572
- actions[contentTypeUid] = {
573
- entriestoPublishIds: [],
574
- entriesToUnpublishIds: []
575
- };
961
+ if (release2.releasedAt) {
962
+ throw new utils.errors.ValidationError("Release already published");
576
963
  }
577
- if (action.type === "publish") {
578
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
579
- } else {
580
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
964
+ await strapi2.db.transaction(async () => {
965
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
966
+ where: {
967
+ id: {
968
+ $in: release2.actions.map((action) => action.id)
969
+ }
970
+ }
971
+ });
972
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
973
+ });
974
+ if (release2.scheduledAt) {
975
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
976
+ await schedulingService.cancel(release2.id);
581
977
  }
582
- }
583
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
585
- await strapi2.db.transaction(async () => {
586
- for (const contentTypeUid of Object.keys(actions)) {
587
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
589
- const entriesToPublish = await strapi2.entityService.findMany(
590
- contentTypeUid,
591
- {
592
- filters: {
593
- id: {
594
- $in: entriestoPublishIds
595
- }
978
+ strapi2.telemetry.send("didDeleteContentRelease");
979
+ return release2;
980
+ },
981
+ async publish(releaseId) {
982
+ const {
983
+ release: release2,
984
+ error
985
+ } = await strapi2.db.transaction(async ({ trx }) => {
986
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
987
+ if (!lockedRelease) {
988
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
989
+ }
990
+ if (lockedRelease.releasedAt) {
991
+ throw new utils.errors.ValidationError("Release already published");
992
+ }
993
+ if (lockedRelease.status === "failed") {
994
+ throw new utils.errors.ValidationError("Release failed to publish");
995
+ }
996
+ try {
997
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
998
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
999
+ releaseId
1000
+ );
1001
+ await strapi2.db.transaction(async () => {
1002
+ for (const { uid, action, id } of singleTypeActions) {
1003
+ await publishSingleTypeAction(uid, action, id);
1004
+ }
1005
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
1006
+ const uid = contentTypeUid;
1007
+ await publishCollectionTypeAction(
1008
+ uid,
1009
+ collectionTypeActions[uid].entriesToPublishIds,
1010
+ collectionTypeActions[uid].entriesToUnpublishIds
1011
+ );
1012
+ }
1013
+ });
1014
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
1015
+ where: {
1016
+ id: releaseId
596
1017
  },
597
- populate
1018
+ data: {
1019
+ status: "done",
1020
+ releasedAt: /* @__PURE__ */ new Date()
1021
+ }
1022
+ });
1023
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1024
+ isPublished: true,
1025
+ release: release22
1026
+ });
1027
+ strapi2.telemetry.send("didPublishContentRelease");
1028
+ return { release: release22, error: null };
1029
+ } catch (error2) {
1030
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1031
+ isPublished: false,
1032
+ error: error2
1033
+ });
1034
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1035
+ status: "failed"
1036
+ }).transacting(trx).execute();
1037
+ return {
1038
+ release: null,
1039
+ error: error2
1040
+ };
1041
+ }
1042
+ });
1043
+ if (error) {
1044
+ throw error;
1045
+ }
1046
+ return release2;
1047
+ },
1048
+ async updateAction(actionId, releaseId, update) {
1049
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1050
+ where: {
1051
+ id: actionId,
1052
+ release: {
1053
+ id: releaseId,
1054
+ releasedAt: {
1055
+ $null: true
1056
+ }
598
1057
  }
1058
+ },
1059
+ data: update
1060
+ });
1061
+ if (!updatedAction) {
1062
+ throw new utils.errors.NotFoundError(
1063
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
599
1064
  );
600
- const entriesToUnpublish = await strapi2.entityService.findMany(
601
- contentTypeUid,
602
- {
603
- filters: {
604
- id: {
605
- $in: entriesToUnpublishIds
606
- }
607
- },
608
- populate
1065
+ }
1066
+ return updatedAction;
1067
+ },
1068
+ async deleteAction(actionId, releaseId) {
1069
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1070
+ where: {
1071
+ id: actionId,
1072
+ release: {
1073
+ id: releaseId,
1074
+ releasedAt: {
1075
+ $null: true
1076
+ }
609
1077
  }
610
- );
611
- if (entriesToPublish.length > 0) {
612
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
613
- }
614
- if (entriesToUnpublish.length > 0) {
615
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
616
1078
  }
1079
+ });
1080
+ if (!deletedAction) {
1081
+ throw new utils.errors.NotFoundError(
1082
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1083
+ );
617
1084
  }
618
- });
619
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
620
- data: {
621
- /*
622
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
623
- */
624
- // @ts-expect-error see above
625
- releasedAt: /* @__PURE__ */ new Date()
626
- }
627
- });
628
- return release2;
629
- },
630
- async updateAction(actionId, releaseId, update) {
631
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
632
- where: {
633
- id: actionId,
634
- release: {
635
- id: releaseId,
636
- releasedAt: {
637
- $null: true
1085
+ this.updateReleaseStatus(releaseId);
1086
+ return deletedAction;
1087
+ },
1088
+ async updateReleaseStatus(releaseId) {
1089
+ const [totalActions, invalidActions] = await Promise.all([
1090
+ this.countActions({
1091
+ filters: {
1092
+ release: releaseId
638
1093
  }
639
- }
640
- },
641
- data: update
642
- });
643
- if (!updatedAction) {
644
- throw new utils.errors.NotFoundError(
645
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
646
- );
647
- }
648
- return updatedAction;
649
- },
650
- async deleteAction(actionId, releaseId) {
651
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
652
- where: {
653
- id: actionId,
654
- release: {
655
- id: releaseId,
656
- releasedAt: {
657
- $null: true
1094
+ }),
1095
+ this.countActions({
1096
+ filters: {
1097
+ release: releaseId,
1098
+ isEntryValid: false
658
1099
  }
1100
+ })
1101
+ ]);
1102
+ if (totalActions > 0) {
1103
+ if (invalidActions > 0) {
1104
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1105
+ where: {
1106
+ id: releaseId
1107
+ },
1108
+ data: {
1109
+ status: "blocked"
1110
+ }
1111
+ });
659
1112
  }
1113
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1114
+ where: {
1115
+ id: releaseId
1116
+ },
1117
+ data: {
1118
+ status: "ready"
1119
+ }
1120
+ });
660
1121
  }
661
- });
662
- if (!deletedAction) {
663
- throw new utils.errors.NotFoundError(
664
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
665
- );
1122
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1123
+ where: {
1124
+ id: releaseId
1125
+ },
1126
+ data: {
1127
+ status: "empty"
1128
+ }
1129
+ });
666
1130
  }
667
- return deletedAction;
1131
+ };
1132
+ };
1133
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1134
+ constructor(message) {
1135
+ super(message);
1136
+ this.name = "AlreadyOnReleaseError";
668
1137
  }
669
- });
1138
+ }
670
1139
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
671
1140
  async validateUniqueEntry(releaseId, releaseActionArgs) {
672
1141
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -679,7 +1148,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
679
1148
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
680
1149
  );
681
1150
  if (isEntryInRelease) {
682
- throw new utils.errors.ValidationError(
1151
+ throw new AlreadyOnReleaseError(
683
1152
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
684
1153
  );
685
1154
  }
@@ -711,27 +1180,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
711
1180
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
712
1181
  }
713
1182
  },
714
- async validateUniqueNameForPendingRelease(name) {
1183
+ async validateUniqueNameForPendingRelease(name, id) {
715
1184
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
1185
  filters: {
717
1186
  releasedAt: {
718
1187
  $null: true
719
1188
  },
720
- name
1189
+ name,
1190
+ ...id && { id: { $ne: id } }
721
1191
  }
722
1192
  });
723
1193
  const isNameUnique = pendingReleases.length === 0;
724
1194
  if (!isNameUnique) {
725
1195
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
1196
  }
1197
+ },
1198
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1199
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1200
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
1201
+ }
727
1202
  }
728
1203
  });
1204
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1205
+ const scheduledJobs = /* @__PURE__ */ new Map();
1206
+ return {
1207
+ async set(releaseId, scheduleDate) {
1208
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1209
+ if (!release2) {
1210
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1211
+ }
1212
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1213
+ try {
1214
+ await getService("release").publish(releaseId);
1215
+ } catch (error) {
1216
+ }
1217
+ this.cancel(releaseId);
1218
+ });
1219
+ if (scheduledJobs.has(releaseId)) {
1220
+ this.cancel(releaseId);
1221
+ }
1222
+ scheduledJobs.set(releaseId, job);
1223
+ return scheduledJobs;
1224
+ },
1225
+ cancel(releaseId) {
1226
+ if (scheduledJobs.has(releaseId)) {
1227
+ scheduledJobs.get(releaseId).cancel();
1228
+ scheduledJobs.delete(releaseId);
1229
+ }
1230
+ return scheduledJobs;
1231
+ },
1232
+ getAll() {
1233
+ return scheduledJobs;
1234
+ },
1235
+ /**
1236
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1237
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1238
+ * This also could be used to sync different Strapi instances in case of a cluster
1239
+ */
1240
+ async syncFromDatabase() {
1241
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1242
+ where: {
1243
+ scheduledAt: {
1244
+ $gte: /* @__PURE__ */ new Date()
1245
+ },
1246
+ releasedAt: null
1247
+ }
1248
+ });
1249
+ for (const release2 of releases) {
1250
+ this.set(release2.id, release2.scheduledAt);
1251
+ }
1252
+ return scheduledJobs;
1253
+ }
1254
+ };
1255
+ };
729
1256
  const services = {
730
1257
  release: createReleaseService,
731
- "release-validation": createReleaseValidationService
1258
+ "release-validation": createReleaseValidationService,
1259
+ scheduling: createSchedulingService
732
1260
  };
733
1261
  const RELEASE_SCHEMA = yup__namespace.object().shape({
734
- name: yup__namespace.string().trim().required()
1262
+ name: yup__namespace.string().trim().required(),
1263
+ scheduledAt: yup__namespace.string().nullable(),
1264
+ isScheduled: yup__namespace.boolean().optional(),
1265
+ time: yup__namespace.string().when("isScheduled", {
1266
+ is: true,
1267
+ then: yup__namespace.string().trim().required(),
1268
+ otherwise: yup__namespace.string().nullable()
1269
+ }),
1270
+ timezone: yup__namespace.string().when("isScheduled", {
1271
+ is: true,
1272
+ then: yup__namespace.string().required().nullable(),
1273
+ otherwise: yup__namespace.string().nullable()
1274
+ }),
1275
+ date: yup__namespace.string().when("isScheduled", {
1276
+ is: true,
1277
+ then: yup__namespace.string().required().nullable(),
1278
+ otherwise: yup__namespace.string().nullable()
1279
+ })
735
1280
  }).required().noUnknown();
736
1281
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
737
1282
  const releaseController = {
@@ -764,26 +1309,30 @@ const releaseController = {
764
1309
  }
765
1310
  };
766
1311
  });
767
- ctx.body = { data, meta: { pagination } };
1312
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1313
+ where: {
1314
+ releasedAt: null
1315
+ }
1316
+ });
1317
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
768
1318
  }
769
1319
  },
770
1320
  async findOne(ctx) {
771
1321
  const id = ctx.params.id;
772
1322
  const releaseService = getService("release", { strapi });
773
1323
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
774
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
775
- ability: ctx.state.userAbility,
776
- model: RELEASE_MODEL_UID
777
- });
778
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1324
+ if (!release2) {
1325
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1326
+ }
779
1327
  const count = await releaseService.countActions({
780
1328
  filters: {
781
1329
  release: id
782
1330
  }
783
1331
  });
784
- if (!release2) {
785
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
786
- }
1332
+ const sanitizedRelease = {
1333
+ ...release2,
1334
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1335
+ };
787
1336
  const data = {
788
1337
  ...sanitizedRelease,
789
1338
  actions: {
@@ -794,6 +1343,33 @@ const releaseController = {
794
1343
  };
795
1344
  ctx.body = { data };
796
1345
  },
1346
+ async mapEntriesToReleases(ctx) {
1347
+ const { contentTypeUid, entriesIds } = ctx.query;
1348
+ if (!contentTypeUid || !entriesIds) {
1349
+ throw new utils.errors.ValidationError("Missing required query parameters");
1350
+ }
1351
+ const releaseService = getService("release", { strapi });
1352
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1353
+ contentTypeUid,
1354
+ entriesIds
1355
+ );
1356
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1357
+ (acc, release2) => {
1358
+ release2.actions.forEach((action) => {
1359
+ if (!acc[action.entry.id]) {
1360
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1361
+ } else {
1362
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1363
+ }
1364
+ });
1365
+ return acc;
1366
+ },
1367
+ {}
1368
+ );
1369
+ ctx.body = {
1370
+ data: mappedEntriesInReleases
1371
+ };
1372
+ },
797
1373
  async create(ctx) {
798
1374
  const user = ctx.state.user;
799
1375
  const releaseArgs = ctx.request.body;
@@ -836,8 +1412,27 @@ const releaseController = {
836
1412
  const id = ctx.params.id;
837
1413
  const releaseService = getService("release", { strapi });
838
1414
  const release2 = await releaseService.publish(id, { user });
1415
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1416
+ releaseService.countActions({
1417
+ filters: {
1418
+ release: id,
1419
+ type: "publish"
1420
+ }
1421
+ }),
1422
+ releaseService.countActions({
1423
+ filters: {
1424
+ release: id,
1425
+ type: "unpublish"
1426
+ }
1427
+ })
1428
+ ]);
839
1429
  ctx.body = {
840
- data: release2
1430
+ data: release2,
1431
+ meta: {
1432
+ totalEntries: countPublishActions + countUnpublishActions,
1433
+ totalPublishedEntries: countPublishActions,
1434
+ totalUnpublishedEntries: countUnpublishActions
1435
+ }
841
1436
  };
842
1437
  }
843
1438
  };
@@ -864,6 +1459,43 @@ const releaseActionController = {
864
1459
  data: releaseAction2
865
1460
  };
866
1461
  },
1462
+ async createMany(ctx) {
1463
+ const releaseId = ctx.params.releaseId;
1464
+ const releaseActionsArgs = ctx.request.body;
1465
+ await Promise.all(
1466
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1467
+ );
1468
+ const releaseService = getService("release", { strapi });
1469
+ const releaseActions = await strapi.db.transaction(async () => {
1470
+ const releaseActions2 = await Promise.all(
1471
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1472
+ try {
1473
+ const action = await releaseService.createAction(releaseId, releaseActionArgs, {
1474
+ disableUpdateReleaseStatus: true
1475
+ });
1476
+ return action;
1477
+ } catch (error) {
1478
+ if (error instanceof AlreadyOnReleaseError) {
1479
+ return null;
1480
+ }
1481
+ throw error;
1482
+ }
1483
+ })
1484
+ );
1485
+ return releaseActions2;
1486
+ });
1487
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1488
+ if (newReleaseActions.length > 0) {
1489
+ releaseService.updateReleaseStatus(releaseId);
1490
+ }
1491
+ ctx.body = {
1492
+ data: newReleaseActions,
1493
+ meta: {
1494
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1495
+ totalEntries: releaseActions.length
1496
+ }
1497
+ };
1498
+ },
867
1499
  async findMany(ctx) {
868
1500
  const releaseId = ctx.params.releaseId;
869
1501
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -932,6 +1564,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
932
1564
  const release = {
933
1565
  type: "admin",
934
1566
  routes: [
1567
+ {
1568
+ method: "GET",
1569
+ path: "/mapEntriesToReleases",
1570
+ handler: "release.mapEntriesToReleases",
1571
+ config: {
1572
+ policies: [
1573
+ "admin::isAuthenticatedAdmin",
1574
+ {
1575
+ name: "admin::hasPermissions",
1576
+ config: {
1577
+ actions: ["plugin::content-releases.read"]
1578
+ }
1579
+ }
1580
+ ]
1581
+ }
1582
+ },
935
1583
  {
936
1584
  method: "POST",
937
1585
  path: "/",
@@ -1049,6 +1697,22 @@ const releaseAction = {
1049
1697
  ]
1050
1698
  }
1051
1699
  },
1700
+ {
1701
+ method: "POST",
1702
+ path: "/:releaseId/actions/bulk",
1703
+ handler: "release-action.createMany",
1704
+ config: {
1705
+ policies: [
1706
+ "admin::isAuthenticatedAdmin",
1707
+ {
1708
+ name: "admin::hasPermissions",
1709
+ config: {
1710
+ actions: ["plugin::content-releases.create-action"]
1711
+ }
1712
+ }
1713
+ ]
1714
+ }
1715
+ },
1052
1716
  {
1053
1717
  method: "GET",
1054
1718
  path: "/:releaseId/actions",
@@ -1109,6 +1773,7 @@ const getPlugin = () => {
1109
1773
  return {
1110
1774
  register,
1111
1775
  bootstrap,
1776
+ destroy,
1112
1777
  contentTypes,
1113
1778
  services,
1114
1779
  controllers,
@@ -1116,6 +1781,9 @@ const getPlugin = () => {
1116
1781
  };
1117
1782
  }
1118
1783
  return {
1784
+ // Always return register, it handles its own feature check
1785
+ register,
1786
+ // Always return contentTypes to avoid losing data when the feature is disabled
1119
1787
  contentTypes
1120
1788
  };
1121
1789
  };