@strapi/content-releases 0.0.0-next.e1ede8c55a0e1e22ce20137bf238fc374bd5dd51 → 0.0.0-next.f4ff842a3cb7b83db540bee67554b704e042b042

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 (33) hide show
  1. package/dist/_chunks/App-dLXY5ei3.js +1353 -0
  2. package/dist/_chunks/App-dLXY5ei3.js.map +1 -0
  3. package/dist/_chunks/App-jrh58sXY.mjs +1330 -0
  4. package/dist/_chunks/App-jrh58sXY.mjs.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  9. package/dist/_chunks/{en-haKSQIo8.js → en-HrREghh3.js} +31 -7
  10. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  11. package/dist/_chunks/{en-ngTk74JV.mjs → en-ltT1TlKQ.mjs} +31 -7
  12. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  13. package/dist/_chunks/{index-EdBmRHRU.js → index-CVO0Rqdm.js} +551 -64
  14. package/dist/_chunks/index-CVO0Rqdm.js.map +1 -0
  15. package/dist/_chunks/{index-XAQOX_IB.mjs → index-PiOGBETy.mjs} +570 -83
  16. package/dist/_chunks/index-PiOGBETy.mjs.map +1 -0
  17. package/dist/admin/index.js +2 -1
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +3 -2
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/server/index.js +1192 -299
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/index.mjs +1191 -300
  24. package/dist/server/index.mjs.map +1 -1
  25. package/package.json +16 -12
  26. package/dist/_chunks/App-g2P5kbSm.mjs +0 -945
  27. package/dist/_chunks/App-g2P5kbSm.mjs.map +0 -1
  28. package/dist/_chunks/App-o5_WfqR-.js +0 -967
  29. package/dist/_chunks/App-o5_WfqR-.js.map +0 -1
  30. package/dist/_chunks/en-haKSQIo8.js.map +0 -1
  31. package/dist/_chunks/en-ngTk74JV.mjs.map +0 -1
  32. package/dist/_chunks/index-EdBmRHRU.js.map +0 -1
  33. package/dist/_chunks/index-XAQOX_IB.mjs.map +0 -1
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  const utils = require("@strapi/utils");
3
+ const isEqual = require("lodash/isEqual");
4
+ const lodash = require("lodash");
3
5
  const _ = require("lodash/fp");
6
+ const EE = require("@strapi/strapi/dist/utils/ee");
7
+ const nodeSchedule = require("node-schedule");
4
8
  const yup = require("yup");
5
9
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
6
10
  function _interopNamespace(e) {
@@ -21,7 +25,9 @@ function _interopNamespace(e) {
21
25
  n.default = e;
22
26
  return Object.freeze(n);
23
27
  }
28
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
24
29
  const ___default = /* @__PURE__ */ _interopDefault(_);
30
+ const EE__default = /* @__PURE__ */ _interopDefault(EE);
25
31
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
26
32
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
27
33
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -69,10 +75,361 @@ const ACTIONS = [
69
75
  pluginName: "content-releases"
70
76
  }
71
77
  ];
72
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
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
+ };
104
+ async function deleteActionsOnDisableDraftAndPublish({
105
+ oldContentTypes,
106
+ contentTypes: contentTypes2
107
+ }) {
108
+ if (!oldContentTypes) {
109
+ return;
110
+ }
111
+ for (const uid in contentTypes2) {
112
+ if (!oldContentTypes[uid]) {
113
+ continue;
114
+ }
115
+ const oldContentType = oldContentTypes[uid];
116
+ const contentType = contentTypes2[uid];
117
+ if (utils.contentTypes.hasDraftAndPublish(oldContentType) && !utils.contentTypes.hasDraftAndPublish(contentType)) {
118
+ await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
119
+ }
120
+ }
121
+ }
122
+ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
123
+ const deletedContentTypes = lodash.difference(lodash.keys(oldContentTypes), lodash.keys(contentTypes2)) ?? [];
124
+ if (deletedContentTypes.length) {
125
+ await utils.mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
126
+ return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
127
+ });
128
+ }
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
+ }
280
+ const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
73
281
  const register = async ({ strapi: strapi2 }) => {
74
- if (features$1.isEnabled("cms-content-releases") && strapi2.features.future.isEnabled("contentReleases")) {
282
+ if (features$2.isEnabled("cms-content-releases")) {
75
283
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
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();
291
+ }
292
+ };
293
+ const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
294
+ const bootstrap = async ({ strapi: strapi2 }) => {
295
+ if (features$1.isEnabled("cms-content-releases")) {
296
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
297
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
298
+ );
299
+ strapi2.db.lifecycles.subscribe({
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);
322
+ }
323
+ }
324
+ } catch (error) {
325
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
326
+ }
327
+ },
328
+ /**
329
+ * deleteMany hook doesn't return the deleted entries ids
330
+ * so we need to fetch them before deleting the entries to save the ids on our state
331
+ */
332
+ async beforeDeleteMany(event) {
333
+ const { model, params } = event;
334
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
335
+ const { where } = params;
336
+ const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
337
+ event.state.entriesToDelete = entriesToDelete;
338
+ }
339
+ },
340
+ /**
341
+ * We delete the release actions related to deleted entries
342
+ * We make this only after deleteMany is succesfully executed to avoid errors
343
+ */
344
+ async afterDeleteMany(event) {
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
+ }
367
+ }
368
+ });
369
+ for (const release2 of releases) {
370
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
371
+ }
372
+ }
373
+ } catch (error) {
374
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
375
+ error
376
+ });
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
+ }
414
+ }
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();
76
433
  }
77
434
  };
78
435
  const schema$1 = {
@@ -101,6 +458,17 @@ const schema$1 = {
101
458
  releasedAt: {
102
459
  type: "datetime"
103
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
+ },
104
472
  actions: {
105
473
  type: "relation",
106
474
  relation: "oneToMany",
@@ -153,6 +521,9 @@ const schema = {
153
521
  relation: "manyToOne",
154
522
  target: RELEASE_MODEL_UID,
155
523
  inversedBy: "actions"
524
+ },
525
+ isEntryValid: {
526
+ type: "boolean"
156
527
  }
157
528
  }
158
529
  };
@@ -163,327 +534,606 @@ const contentTypes = {
163
534
  release: release$1,
164
535
  "release-action": releaseAction$1
165
536
  };
166
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
167
- return strapi2.plugin("content-releases").service(name);
168
- };
169
537
  const getGroupName = (queryValue) => {
170
538
  switch (queryValue) {
171
539
  case "contentType":
172
- return "entry.contentType.displayName";
540
+ return "contentType.displayName";
173
541
  case "action":
174
542
  return "type";
175
543
  case "locale":
176
- return ___default.default.getOr("No locale", "entry.locale.name");
544
+ return ___default.default.getOr("No locale", "locale.name");
177
545
  default:
178
- return "entry.contentType.displayName";
546
+ return "contentType.displayName";
179
547
  }
180
548
  };
181
- const createReleaseService = ({ strapi: strapi2 }) => ({
182
- async create(releaseData, { user }) {
183
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
184
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
185
- 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
186
555
  });
187
- },
188
- async findOne(id, query = {}) {
189
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
190
- ...query
191
- });
192
- return release2;
193
- },
194
- findPage(query) {
195
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
196
- ...query,
197
- populate: {
198
- actions: {
199
- // @ts-expect-error Ignore missing properties
200
- count: true
201
- }
202
- }
203
- });
204
- },
205
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
206
- hasEntryAttached
207
- } = {
208
- hasEntryAttached: false
209
- }) {
210
- const whereActions = hasEntryAttached ? {
211
- // Find all Releases where the content type entry is present
212
- actions: {
213
- target_type: contentTypeUid,
214
- target_id: entryId
215
- }
216
- } : {
217
- // Find all Releases where the content type entry is not present
218
- $or: [
219
- {
220
- $not: {
221
- actions: {
222
- target_type: contentTypeUid,
223
- target_id: entryId
224
- }
225
- }
226
- },
227
- {
228
- actions: null
229
- }
230
- ]
231
- };
232
- const populateAttachedAction = hasEntryAttached ? {
233
- // Filter the action to get only the content type entry
234
- actions: {
235
- where: {
236
- target_type: contentTypeUid,
237
- target_id: entryId
238
- }
239
- }
240
- } : {};
241
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
242
- where: {
243
- ...whereActions,
244
- releasedAt: {
245
- $null: true
246
- }
247
- },
248
- populate: {
249
- ...populateAttachedAction
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);
250
567
  }
251
- });
252
- return releases.map((release2) => {
253
- if (release2.actions?.length) {
254
- const [actionForEntry] = release2.actions;
255
- delete release2.actions;
256
- return {
257
- ...release2,
258
- action: actionForEntry
259
- };
568
+ } catch (error) {
569
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
570
+ ;
571
+ else {
572
+ throw error;
260
573
  }
261
- return release2;
262
- });
263
- },
264
- async update(id, releaseData, { user }) {
265
- const updatedRelease = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
266
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
267
- /*
268
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
269
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
270
- */
271
- // @ts-expect-error see above
272
- data: updatedRelease
273
- });
274
- if (!release2) {
275
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
276
574
  }
277
- return release2;
278
- },
279
- async createAction(releaseId, action) {
280
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
281
- strapi: strapi2
282
- });
283
- await Promise.all([
284
- validateEntryContentType(action.entry.contentType),
285
- validateUniqueEntry(releaseId, action)
286
- ]);
287
- const { entry, type } = action;
288
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
289
- data: {
290
- type,
291
- contentType: entry.contentType,
292
- locale: entry.locale,
293
- entry: {
294
- id: entry.id,
295
- __type: entry.contentType,
296
- __pivot: { field: "entry" }
297
- },
298
- release: releaseId
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
+ }
299
585
  },
300
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
586
+ populate
301
587
  });
302
- },
303
- async findActions(releaseId, query) {
304
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
305
- fields: ["id"]
306
- });
307
- if (!release2) {
308
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
309
- }
310
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
311
- ...query,
312
- populate: {
313
- entry: true
314
- },
588
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
315
589
  filters: {
316
- release: releaseId
317
- }
318
- });
319
- },
320
- async countActions(query) {
321
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
322
- },
323
- async groupActions(actions, groupBy) {
324
- const contentTypeUids = actions.reduce((acc, action) => {
325
- if (!acc.includes(action.contentType)) {
326
- acc.push(action.contentType);
327
- }
328
- return acc;
329
- }, []);
330
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
331
- contentTypeUids
332
- );
333
- const allLocales = await strapi2.plugin("i18n").service("locales").find();
334
- const allLocalesDictionary = allLocales.reduce((acc, locale) => {
335
- acc[locale.code] = { name: locale.name, code: locale.code };
336
- return acc;
337
- }, {});
338
- const formattedData = actions.map((action) => {
339
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
340
- return {
341
- ...action,
342
- entry: {
343
- id: action.entry.id,
344
- contentType: {
345
- displayName,
346
- mainFieldValue: action.entry[mainField]
347
- },
348
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
349
- status: action.entry.publishedAt ? "published" : "draft"
590
+ id: {
591
+ $in: entriestoUnpublishIds
350
592
  }
351
- };
593
+ },
594
+ populate
352
595
  });
353
- const groupName = getGroupName(groupBy);
354
- return ___default.default.groupBy(groupName)(formattedData);
355
- },
356
- async getContentTypesDataForActions(contentTypesUids) {
357
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
358
- const contentTypesData = {};
359
- for (const contentTypeUid of contentTypesUids) {
360
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
361
- uid: contentTypeUid
362
- });
363
- contentTypesData[contentTypeUid] = {
364
- mainField: contentTypeConfig.settings.mainField,
365
- displayName: strapi2.getModel(contentTypeUid).info.displayName
366
- };
596
+ if (entriesToPublish.length > 0) {
597
+ await entityManagerService.publishMany(entriesToPublish, uid);
367
598
  }
368
- return contentTypesData;
369
- },
370
- async delete(releaseId) {
371
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
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({
605
+ where: {
606
+ release: {
607
+ id: releaseId
608
+ }
609
+ },
372
610
  populate: {
373
- actions: {
611
+ entry: {
374
612
  fields: ["id"]
375
613
  }
376
614
  }
377
615
  });
378
- if (!release2) {
379
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
616
+ if (actions.length === 0) {
617
+ throw new utils.errors.ValidationError("No entries to publish");
380
618
  }
381
- if (release2.releasedAt) {
382
- throw new utils.errors.ValidationError("Release already published");
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
+ }
383
642
  }
384
- await strapi2.db.transaction(async () => {
385
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
386
- where: {
387
- id: {
388
- $in: release2.actions.map((action) => action.id)
643
+ return { collectionTypeActions, singleTypeActions };
644
+ };
645
+ return {
646
+ async create(releaseData, { user }) {
647
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
648
+ const {
649
+ validatePendingReleasesLimit,
650
+ validateUniqueNameForPendingRelease,
651
+ validateScheduledAtIsLaterThanNow
652
+ } = getService("release-validation", { strapi: strapi2 });
653
+ await Promise.all([
654
+ validatePendingReleasesLimit(),
655
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
656
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
657
+ ]);
658
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
659
+ data: {
660
+ ...releaseWithCreatorFields,
661
+ status: "empty"
662
+ }
663
+ });
664
+ if (releaseWithCreatorFields.scheduledAt) {
665
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
666
+ await schedulingService.set(release2.id, release2.scheduledAt);
667
+ }
668
+ strapi2.telemetry.send("didCreateContentRelease");
669
+ return release2;
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
389
684
  }
390
685
  }
391
686
  });
392
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
393
- });
394
- return release2;
395
- },
396
- async publish(releaseId) {
397
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
398
- RELEASE_MODEL_UID,
399
- releaseId,
400
- {
687
+ },
688
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
+ let entries = entriesIds;
690
+ if (!Array.isArray(entriesIds)) {
691
+ entries = [entriesIds];
692
+ }
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
699
+ }
700
+ },
701
+ releasedAt: {
702
+ $null: true
703
+ }
704
+ },
401
705
  populate: {
706
+ // Filter the action to get only the content type entry
402
707
  actions: {
708
+ where: {
709
+ target_type: contentTypeUid,
710
+ target_id: {
711
+ $in: entries
712
+ }
713
+ },
403
714
  populate: {
404
- entry: true
715
+ entry: {
716
+ select: ["id"]
717
+ }
405
718
  }
406
719
  }
407
720
  }
408
- }
409
- );
410
- if (!releaseWithPopulatedActionEntries) {
411
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
412
- }
413
- if (releaseWithPopulatedActionEntries.releasedAt) {
414
- throw new utils.errors.ValidationError("Release already published");
415
- }
416
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
417
- throw new utils.errors.ValidationError("No entries to publish");
418
- }
419
- const actions = {};
420
- for (const action of releaseWithPopulatedActionEntries.actions) {
421
- const contentTypeUid = action.contentType;
422
- if (!actions[contentTypeUid]) {
423
- actions[contentTypeUid] = {
424
- publish: [],
425
- unpublish: []
426
- };
427
- }
428
- if (action.type === "publish") {
429
- actions[contentTypeUid].publish.push(action.entry);
430
- } else {
431
- actions[contentTypeUid].unpublish.push(action.entry);
432
- }
433
- }
434
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
435
- await strapi2.db.transaction(async () => {
436
- for (const contentTypeUid of Object.keys(actions)) {
437
- const { publish, unpublish } = actions[contentTypeUid];
438
- if (publish.length > 0) {
439
- await entityManagerService.publishMany(publish, contentTypeUid);
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
743
+ }
744
+ }
745
+ });
746
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
747
+ where: {
748
+ $or: [
749
+ {
750
+ id: {
751
+ $notIn: releasesRelated.map((release2) => release2.id)
752
+ }
753
+ },
754
+ {
755
+ actions: null
756
+ }
757
+ ],
758
+ releasedAt: {
759
+ $null: true
760
+ }
440
761
  }
441
- if (unpublish.length > 0) {
442
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
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
+ };
443
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}`);
444
790
  }
445
- });
446
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
447
- data: {
791
+ if (release2.releasedAt) {
792
+ throw new utils.errors.ValidationError("Release already published");
793
+ }
794
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
448
795
  /*
449
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
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']
450
798
  */
451
799
  // @ts-expect-error see above
452
- releasedAt: /* @__PURE__ */ new Date()
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);
453
807
  }
454
- });
455
- return release2;
456
- },
457
- async updateAction(actionId, releaseId, update) {
458
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
459
- where: {
460
- id: actionId,
461
- release: releaseId
462
- },
463
- data: update
464
- });
465
- if (!updatedAction) {
466
- throw new utils.errors.NotFoundError(
467
- `Action with id ${actionId} not found in release with id ${releaseId}`
808
+ this.updateReleaseStatus(id);
809
+ strapi2.telemetry.send("didUpdateContentRelease");
810
+ return updatedRelease;
811
+ },
812
+ async createAction(releaseId, action) {
813
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
814
+ strapi: strapi2
815
+ });
816
+ await Promise.all([
817
+ validateEntryContentType(action.entry.contentType),
818
+ validateUniqueEntry(releaseId, action)
819
+ ]);
820
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
821
+ if (!release2) {
822
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
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
+ this.updateReleaseStatus(releaseId);
846
+ return releaseAction2;
847
+ },
848
+ async findActions(releaseId, query) {
849
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
850
+ fields: ["id"]
851
+ });
852
+ if (!release2) {
853
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
854
+ }
855
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
856
+ ...query,
857
+ populate: {
858
+ entry: {
859
+ populate: "*"
860
+ }
861
+ },
862
+ filters: {
863
+ release: releaseId
864
+ }
865
+ });
866
+ },
867
+ async countActions(query) {
868
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
869
+ },
870
+ async groupActions(actions, groupBy) {
871
+ const contentTypeUids = actions.reduce((acc, action) => {
872
+ if (!acc.includes(action.contentType)) {
873
+ acc.push(action.contentType);
874
+ }
875
+ return acc;
876
+ }, []);
877
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
878
+ contentTypeUids
468
879
  );
469
- }
470
- return updatedAction;
471
- },
472
- async deleteAction(actionId, releaseId) {
473
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
474
- where: {
475
- id: actionId,
476
- release: releaseId
880
+ const allLocalesDictionary = await this.getLocalesDataForActions();
881
+ const formattedData = actions.map((action) => {
882
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
883
+ return {
884
+ ...action,
885
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
886
+ contentType: {
887
+ displayName,
888
+ mainFieldValue: action.entry[mainField],
889
+ uid: action.contentType
890
+ }
891
+ };
892
+ });
893
+ const groupName = getGroupName(groupBy);
894
+ return ___default.default.groupBy(groupName)(formattedData);
895
+ },
896
+ async getLocalesDataForActions() {
897
+ if (!strapi2.plugin("i18n")) {
898
+ return {};
477
899
  }
478
- });
479
- if (!deletedAction) {
480
- throw new utils.errors.NotFoundError(
481
- `Action with id ${actionId} not found in release with id ${releaseId}`
900
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
901
+ return allLocales.reduce((acc, locale) => {
902
+ acc[locale.code] = { name: locale.name, code: locale.code };
903
+ return acc;
904
+ }, {});
905
+ },
906
+ async getContentTypesDataForActions(contentTypesUids) {
907
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
908
+ const contentTypesData = {};
909
+ for (const contentTypeUid of contentTypesUids) {
910
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
911
+ uid: contentTypeUid
912
+ });
913
+ contentTypesData[contentTypeUid] = {
914
+ mainField: contentTypeConfig.settings.mainField,
915
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
916
+ };
917
+ }
918
+ return contentTypesData;
919
+ },
920
+ getContentTypeModelsFromActions(actions) {
921
+ const contentTypeUids = actions.reduce((acc, action) => {
922
+ if (!acc.includes(action.contentType)) {
923
+ acc.push(action.contentType);
924
+ }
925
+ return acc;
926
+ }, []);
927
+ const contentTypeModelsMap = contentTypeUids.reduce(
928
+ (acc, contentTypeUid) => {
929
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
930
+ return acc;
931
+ },
932
+ {}
482
933
  );
934
+ return contentTypeModelsMap;
935
+ },
936
+ async getAllComponents() {
937
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
938
+ const components = await contentManagerComponentsService.findAllComponents();
939
+ const componentsMap = components.reduce(
940
+ (acc, component) => {
941
+ acc[component.uid] = component;
942
+ return acc;
943
+ },
944
+ {}
945
+ );
946
+ return componentsMap;
947
+ },
948
+ async delete(releaseId) {
949
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
950
+ populate: {
951
+ actions: {
952
+ fields: ["id"]
953
+ }
954
+ }
955
+ });
956
+ if (!release2) {
957
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
958
+ }
959
+ if (release2.releasedAt) {
960
+ throw new utils.errors.ValidationError("Release already published");
961
+ }
962
+ await strapi2.db.transaction(async () => {
963
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
964
+ where: {
965
+ id: {
966
+ $in: release2.actions.map((action) => action.id)
967
+ }
968
+ }
969
+ });
970
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
971
+ });
972
+ if (release2.scheduledAt) {
973
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
974
+ await schedulingService.cancel(release2.id);
975
+ }
976
+ strapi2.telemetry.send("didDeleteContentRelease");
977
+ return release2;
978
+ },
979
+ async publish(releaseId) {
980
+ const {
981
+ release: release2,
982
+ error
983
+ } = await strapi2.db.transaction(async ({ trx }) => {
984
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
985
+ if (!lockedRelease) {
986
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
987
+ }
988
+ if (lockedRelease.releasedAt) {
989
+ throw new utils.errors.ValidationError("Release already published");
990
+ }
991
+ if (lockedRelease.status === "failed") {
992
+ throw new utils.errors.ValidationError("Release failed to publish");
993
+ }
994
+ try {
995
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
996
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
997
+ releaseId
998
+ );
999
+ await strapi2.db.transaction(async () => {
1000
+ for (const { uid, action, id } of singleTypeActions) {
1001
+ await publishSingleTypeAction(uid, action, id);
1002
+ }
1003
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
1004
+ const uid = contentTypeUid;
1005
+ await publishCollectionTypeAction(
1006
+ uid,
1007
+ collectionTypeActions[uid].entriesToPublishIds,
1008
+ collectionTypeActions[uid].entriesToUnpublishIds
1009
+ );
1010
+ }
1011
+ });
1012
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
1013
+ where: {
1014
+ id: releaseId
1015
+ },
1016
+ data: {
1017
+ status: "done",
1018
+ releasedAt: /* @__PURE__ */ new Date()
1019
+ }
1020
+ });
1021
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1022
+ isPublished: true,
1023
+ release: release22
1024
+ });
1025
+ strapi2.telemetry.send("didPublishContentRelease");
1026
+ return { release: release22, error: null };
1027
+ } catch (error2) {
1028
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
1029
+ isPublished: false,
1030
+ error: error2
1031
+ });
1032
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
1033
+ status: "failed"
1034
+ }).transacting(trx).execute();
1035
+ return {
1036
+ release: null,
1037
+ error: error2
1038
+ };
1039
+ }
1040
+ });
1041
+ if (error) {
1042
+ throw error;
1043
+ }
1044
+ return release2;
1045
+ },
1046
+ async updateAction(actionId, releaseId, update) {
1047
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1048
+ where: {
1049
+ id: actionId,
1050
+ release: {
1051
+ id: releaseId,
1052
+ releasedAt: {
1053
+ $null: true
1054
+ }
1055
+ }
1056
+ },
1057
+ data: update
1058
+ });
1059
+ if (!updatedAction) {
1060
+ throw new utils.errors.NotFoundError(
1061
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1062
+ );
1063
+ }
1064
+ return updatedAction;
1065
+ },
1066
+ async deleteAction(actionId, releaseId) {
1067
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1068
+ where: {
1069
+ id: actionId,
1070
+ release: {
1071
+ id: releaseId,
1072
+ releasedAt: {
1073
+ $null: true
1074
+ }
1075
+ }
1076
+ }
1077
+ });
1078
+ if (!deletedAction) {
1079
+ throw new utils.errors.NotFoundError(
1080
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1081
+ );
1082
+ }
1083
+ this.updateReleaseStatus(releaseId);
1084
+ return deletedAction;
1085
+ },
1086
+ async updateReleaseStatus(releaseId) {
1087
+ const [totalActions, invalidActions] = await Promise.all([
1088
+ this.countActions({
1089
+ filters: {
1090
+ release: releaseId
1091
+ }
1092
+ }),
1093
+ this.countActions({
1094
+ filters: {
1095
+ release: releaseId,
1096
+ isEntryValid: false
1097
+ }
1098
+ })
1099
+ ]);
1100
+ if (totalActions > 0) {
1101
+ if (invalidActions > 0) {
1102
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1103
+ where: {
1104
+ id: releaseId
1105
+ },
1106
+ data: {
1107
+ status: "blocked"
1108
+ }
1109
+ });
1110
+ }
1111
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1112
+ where: {
1113
+ id: releaseId
1114
+ },
1115
+ data: {
1116
+ status: "ready"
1117
+ }
1118
+ });
1119
+ }
1120
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1121
+ where: {
1122
+ id: releaseId
1123
+ },
1124
+ data: {
1125
+ status: "empty"
1126
+ }
1127
+ });
483
1128
  }
484
- return deletedAction;
1129
+ };
1130
+ };
1131
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1132
+ constructor(message) {
1133
+ super(message);
1134
+ this.name = "AlreadyOnReleaseError";
485
1135
  }
486
- });
1136
+ }
487
1137
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
488
1138
  async validateUniqueEntry(releaseId, releaseActionArgs) {
489
1139
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -496,7 +1146,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
496
1146
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
497
1147
  );
498
1148
  if (isEntryInRelease) {
499
- throw new utils.errors.ValidationError(
1149
+ throw new AlreadyOnReleaseError(
500
1150
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
501
1151
  );
502
1152
  }
@@ -511,11 +1161,120 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
511
1161
  `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
512
1162
  );
513
1163
  }
1164
+ },
1165
+ async validatePendingReleasesLimit() {
1166
+ const maximumPendingReleases = (
1167
+ // @ts-expect-error - options is not typed into features
1168
+ EE__default.default.features.get("cms-content-releases")?.options?.maximumReleases || 3
1169
+ );
1170
+ const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
1171
+ filters: {
1172
+ releasedAt: {
1173
+ $null: true
1174
+ }
1175
+ }
1176
+ });
1177
+ if (pendingReleasesCount >= maximumPendingReleases) {
1178
+ throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
1179
+ }
1180
+ },
1181
+ async validateUniqueNameForPendingRelease(name, id) {
1182
+ const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
1183
+ filters: {
1184
+ releasedAt: {
1185
+ $null: true
1186
+ },
1187
+ name,
1188
+ ...id && { id: { $ne: id } }
1189
+ }
1190
+ });
1191
+ const isNameUnique = pendingReleases.length === 0;
1192
+ if (!isNameUnique) {
1193
+ throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
1194
+ }
1195
+ },
1196
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1197
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1198
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
1199
+ }
514
1200
  }
515
1201
  });
516
- const services = { release: createReleaseService, "release-validation": createReleaseValidationService };
1202
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1203
+ const scheduledJobs = /* @__PURE__ */ new Map();
1204
+ return {
1205
+ async set(releaseId, scheduleDate) {
1206
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1207
+ if (!release2) {
1208
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
1209
+ }
1210
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
1211
+ try {
1212
+ await getService("release").publish(releaseId);
1213
+ } catch (error) {
1214
+ }
1215
+ this.cancel(releaseId);
1216
+ });
1217
+ if (scheduledJobs.has(releaseId)) {
1218
+ this.cancel(releaseId);
1219
+ }
1220
+ scheduledJobs.set(releaseId, job);
1221
+ return scheduledJobs;
1222
+ },
1223
+ cancel(releaseId) {
1224
+ if (scheduledJobs.has(releaseId)) {
1225
+ scheduledJobs.get(releaseId).cancel();
1226
+ scheduledJobs.delete(releaseId);
1227
+ }
1228
+ return scheduledJobs;
1229
+ },
1230
+ getAll() {
1231
+ return scheduledJobs;
1232
+ },
1233
+ /**
1234
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1235
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1236
+ * This also could be used to sync different Strapi instances in case of a cluster
1237
+ */
1238
+ async syncFromDatabase() {
1239
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1240
+ where: {
1241
+ scheduledAt: {
1242
+ $gte: /* @__PURE__ */ new Date()
1243
+ },
1244
+ releasedAt: null
1245
+ }
1246
+ });
1247
+ for (const release2 of releases) {
1248
+ this.set(release2.id, release2.scheduledAt);
1249
+ }
1250
+ return scheduledJobs;
1251
+ }
1252
+ };
1253
+ };
1254
+ const services = {
1255
+ release: createReleaseService,
1256
+ "release-validation": createReleaseValidationService,
1257
+ scheduling: createSchedulingService
1258
+ };
517
1259
  const RELEASE_SCHEMA = yup__namespace.object().shape({
518
- name: yup__namespace.string().trim().required()
1260
+ name: yup__namespace.string().trim().required(),
1261
+ scheduledAt: yup__namespace.string().nullable(),
1262
+ isScheduled: yup__namespace.boolean().optional(),
1263
+ time: yup__namespace.string().when("isScheduled", {
1264
+ is: true,
1265
+ then: yup__namespace.string().trim().required(),
1266
+ otherwise: yup__namespace.string().nullable()
1267
+ }),
1268
+ timezone: yup__namespace.string().when("isScheduled", {
1269
+ is: true,
1270
+ then: yup__namespace.string().required().nullable(),
1271
+ otherwise: yup__namespace.string().nullable()
1272
+ }),
1273
+ date: yup__namespace.string().when("isScheduled", {
1274
+ is: true,
1275
+ then: yup__namespace.string().required().nullable(),
1276
+ otherwise: yup__namespace.string().nullable()
1277
+ })
519
1278
  }).required().noUnknown();
520
1279
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
521
1280
  const releaseController = {
@@ -532,9 +1291,7 @@ const releaseController = {
532
1291
  const contentTypeUid = query.contentTypeUid;
533
1292
  const entryId = query.entryId;
534
1293
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
535
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
536
- hasEntryAttached
537
- });
1294
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
538
1295
  ctx.body = { data };
539
1296
  } else {
540
1297
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -550,26 +1307,30 @@ const releaseController = {
550
1307
  }
551
1308
  };
552
1309
  });
553
- ctx.body = { data, meta: { pagination } };
1310
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1311
+ where: {
1312
+ releasedAt: null
1313
+ }
1314
+ });
1315
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
554
1316
  }
555
1317
  },
556
1318
  async findOne(ctx) {
557
1319
  const id = ctx.params.id;
558
1320
  const releaseService = getService("release", { strapi });
559
1321
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
560
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
561
- ability: ctx.state.userAbility,
562
- model: RELEASE_MODEL_UID
563
- });
564
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1322
+ if (!release2) {
1323
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1324
+ }
565
1325
  const count = await releaseService.countActions({
566
1326
  filters: {
567
1327
  release: id
568
1328
  }
569
1329
  });
570
- if (!release2) {
571
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
572
- }
1330
+ const sanitizedRelease = {
1331
+ ...release2,
1332
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1333
+ };
573
1334
  const data = {
574
1335
  ...sanitizedRelease,
575
1336
  actions: {
@@ -580,6 +1341,33 @@ const releaseController = {
580
1341
  };
581
1342
  ctx.body = { data };
582
1343
  },
1344
+ async mapEntriesToReleases(ctx) {
1345
+ const { contentTypeUid, entriesIds } = ctx.query;
1346
+ if (!contentTypeUid || !entriesIds) {
1347
+ throw new utils.errors.ValidationError("Missing required query parameters");
1348
+ }
1349
+ const releaseService = getService("release", { strapi });
1350
+ const releasesWithActions = await releaseService.findManyWithContentTypeEntryAttached(
1351
+ contentTypeUid,
1352
+ entriesIds
1353
+ );
1354
+ const mappedEntriesInReleases = releasesWithActions.reduce(
1355
+ (acc, release2) => {
1356
+ release2.actions.forEach((action) => {
1357
+ if (!acc[action.entry.id]) {
1358
+ acc[action.entry.id] = [{ id: release2.id, name: release2.name }];
1359
+ } else {
1360
+ acc[action.entry.id].push({ id: release2.id, name: release2.name });
1361
+ }
1362
+ });
1363
+ return acc;
1364
+ },
1365
+ {}
1366
+ );
1367
+ ctx.body = {
1368
+ data: mappedEntriesInReleases
1369
+ };
1370
+ },
583
1371
  async create(ctx) {
584
1372
  const user = ctx.state.user;
585
1373
  const releaseArgs = ctx.request.body;
@@ -622,8 +1410,27 @@ const releaseController = {
622
1410
  const id = ctx.params.id;
623
1411
  const releaseService = getService("release", { strapi });
624
1412
  const release2 = await releaseService.publish(id, { user });
1413
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1414
+ releaseService.countActions({
1415
+ filters: {
1416
+ release: id,
1417
+ type: "publish"
1418
+ }
1419
+ }),
1420
+ releaseService.countActions({
1421
+ filters: {
1422
+ release: id,
1423
+ type: "unpublish"
1424
+ }
1425
+ })
1426
+ ]);
625
1427
  ctx.body = {
626
- data: release2
1428
+ data: release2,
1429
+ meta: {
1430
+ totalEntries: countPublishActions + countUnpublishActions,
1431
+ totalPublishedEntries: countPublishActions,
1432
+ totalUnpublishedEntries: countUnpublishActions
1433
+ }
627
1434
  };
628
1435
  }
629
1436
  };
@@ -650,6 +1457,38 @@ const releaseActionController = {
650
1457
  data: releaseAction2
651
1458
  };
652
1459
  },
1460
+ async createMany(ctx) {
1461
+ const releaseId = ctx.params.releaseId;
1462
+ const releaseActionsArgs = ctx.request.body;
1463
+ await Promise.all(
1464
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1465
+ );
1466
+ const releaseService = getService("release", { strapi });
1467
+ const releaseActions = await strapi.db.transaction(async () => {
1468
+ const releaseActions2 = await Promise.all(
1469
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1470
+ try {
1471
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1472
+ return action;
1473
+ } catch (error) {
1474
+ if (error instanceof AlreadyOnReleaseError) {
1475
+ return null;
1476
+ }
1477
+ throw error;
1478
+ }
1479
+ })
1480
+ );
1481
+ return releaseActions2;
1482
+ });
1483
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1484
+ ctx.body = {
1485
+ data: newReleaseActions,
1486
+ meta: {
1487
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1488
+ totalEntries: releaseActions.length
1489
+ }
1490
+ };
1491
+ },
653
1492
  async findMany(ctx) {
654
1493
  const releaseId = ctx.params.releaseId;
655
1494
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -662,11 +1501,30 @@ const releaseActionController = {
662
1501
  sort: query.groupBy === "action" ? "type" : query.groupBy,
663
1502
  ...query
664
1503
  });
665
- const groupedData = await releaseService.groupActions(results, query.groupBy);
1504
+ const contentTypeOutputSanitizers = results.reduce((acc, action) => {
1505
+ if (acc[action.contentType]) {
1506
+ return acc;
1507
+ }
1508
+ const contentTypePermissionsManager = strapi.admin.services.permission.createPermissionsManager({
1509
+ ability: ctx.state.userAbility,
1510
+ model: action.contentType
1511
+ });
1512
+ acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
1513
+ return acc;
1514
+ }, {});
1515
+ const sanitizedResults = await utils.mapAsync(results, async (action) => ({
1516
+ ...action,
1517
+ entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
1518
+ }));
1519
+ const groupedData = await releaseService.groupActions(sanitizedResults, query.groupBy);
1520
+ const contentTypes2 = releaseService.getContentTypeModelsFromActions(results);
1521
+ const components = await releaseService.getAllComponents();
666
1522
  ctx.body = {
667
1523
  data: groupedData,
668
1524
  meta: {
669
- pagination
1525
+ pagination,
1526
+ contentTypes: contentTypes2,
1527
+ components
670
1528
  }
671
1529
  };
672
1530
  },
@@ -688,10 +1546,8 @@ const releaseActionController = {
688
1546
  async delete(ctx) {
689
1547
  const actionId = ctx.params.actionId;
690
1548
  const releaseId = ctx.params.releaseId;
691
- const deletedReleaseAction = await getService("release", { strapi }).deleteAction(
692
- actionId,
693
- releaseId
694
- );
1549
+ const releaseService = getService("release", { strapi });
1550
+ const deletedReleaseAction = await releaseService.deleteAction(actionId, releaseId);
695
1551
  ctx.body = {
696
1552
  data: deletedReleaseAction
697
1553
  };
@@ -701,6 +1557,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
701
1557
  const release = {
702
1558
  type: "admin",
703
1559
  routes: [
1560
+ {
1561
+ method: "GET",
1562
+ path: "/mapEntriesToReleases",
1563
+ handler: "release.mapEntriesToReleases",
1564
+ config: {
1565
+ policies: [
1566
+ "admin::isAuthenticatedAdmin",
1567
+ {
1568
+ name: "admin::hasPermissions",
1569
+ config: {
1570
+ actions: ["plugin::content-releases.read"]
1571
+ }
1572
+ }
1573
+ ]
1574
+ }
1575
+ },
704
1576
  {
705
1577
  method: "POST",
706
1578
  path: "/",
@@ -818,6 +1690,22 @@ const releaseAction = {
818
1690
  ]
819
1691
  }
820
1692
  },
1693
+ {
1694
+ method: "POST",
1695
+ path: "/:releaseId/actions/bulk",
1696
+ handler: "release-action.createMany",
1697
+ config: {
1698
+ policies: [
1699
+ "admin::isAuthenticatedAdmin",
1700
+ {
1701
+ name: "admin::hasPermissions",
1702
+ config: {
1703
+ actions: ["plugin::content-releases.create-action"]
1704
+ }
1705
+ }
1706
+ ]
1707
+ }
1708
+ },
821
1709
  {
822
1710
  method: "GET",
823
1711
  path: "/:releaseId/actions",
@@ -874,9 +1762,11 @@ const routes = {
874
1762
  };
875
1763
  const { features } = require("@strapi/strapi/dist/utils/ee");
876
1764
  const getPlugin = () => {
877
- if (features.isEnabled("cms-content-releases") && strapi.features.future.isEnabled("contentReleases")) {
1765
+ if (features.isEnabled("cms-content-releases")) {
878
1766
  return {
879
1767
  register,
1768
+ bootstrap,
1769
+ destroy,
880
1770
  contentTypes,
881
1771
  services,
882
1772
  controllers,
@@ -884,6 +1774,9 @@ const getPlugin = () => {
884
1774
  };
885
1775
  }
886
1776
  return {
1777
+ // Always return register, it handles its own feature check
1778
+ register,
1779
+ // Always return contentTypes to avoid losing data when the feature is disabled
887
1780
  contentTypes
888
1781
  };
889
1782
  };