@strapi/content-releases 0.0.0-experimental.ee4d311a5e6a131fad03cf07e4696f49fdd9c2e6 → 0.0.0-experimental.f77206734629a2b88793a7a8abca40388843c656

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 (30) hide show
  1. package/dist/_chunks/{App-0yPbcoGt.js → App-HjWtUYmc.js} +784 -468
  2. package/dist/_chunks/App-HjWtUYmc.js.map +1 -0
  3. package/dist/_chunks/App-gu1aiP6i.mjs +1330 -0
  4. package/dist/_chunks/App-gu1aiP6i.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-gYDqKYFd.js → en-HrREghh3.js} +30 -7
  10. package/dist/_chunks/en-HrREghh3.js.map +1 -0
  11. package/dist/_chunks/{en-MyLPoISH.mjs → en-ltT1TlKQ.mjs} +30 -7
  12. package/dist/_chunks/en-ltT1TlKQ.mjs.map +1 -0
  13. package/dist/_chunks/{index-l5iuP0Hb.js → index-ZNwxYN8H.js} +468 -41
  14. package/dist/_chunks/index-ZNwxYN8H.js.map +1 -0
  15. package/dist/_chunks/{index-EIe8S-cw.mjs → index-mvj9PSKd.mjs} +485 -58
  16. package/dist/_chunks/index-mvj9PSKd.mjs.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +2 -2
  19. package/dist/server/index.js +1093 -395
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +1093 -396
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +15 -12
  24. package/dist/_chunks/App-0yPbcoGt.js.map +0 -1
  25. package/dist/_chunks/App-BWaM2ihP.mjs +0 -1015
  26. package/dist/_chunks/App-BWaM2ihP.mjs.map +0 -1
  27. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  28. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  29. package/dist/_chunks/index-EIe8S-cw.mjs.map +0 -1
  30. package/dist/_chunks/index-l5iuP0Hb.js.map +0 -1
@@ -1,7 +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");
4
6
  const EE = require("@strapi/strapi/dist/utils/ee");
7
+ const nodeSchedule = require("node-schedule");
5
8
  const yup = require("yup");
6
9
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
7
10
  function _interopNamespace(e) {
@@ -22,6 +25,7 @@ function _interopNamespace(e) {
22
25
  n.default = e;
23
26
  return Object.freeze(n);
24
27
  }
28
+ const isEqual__default = /* @__PURE__ */ _interopDefault(isEqual);
25
29
  const ___default = /* @__PURE__ */ _interopDefault(_);
26
30
  const EE__default = /* @__PURE__ */ _interopDefault(EE);
27
31
  const yup__namespace = /* @__PURE__ */ _interopNamespace(yup);
@@ -71,47 +75,254 @@ const ACTIONS = [
71
75
  pluginName: "content-releases"
72
76
  }
73
77
  ];
78
+ const ALLOWED_WEBHOOK_EVENTS = {
79
+ RELEASES_PUBLISH: "releases.publish"
80
+ };
74
81
  const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
75
82
  return strapi2.plugin("content-releases").service(name);
76
83
  };
77
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
78
- const register = async ({ strapi: strapi2 }) => {
79
- if (features$2.isEnabled("cms-content-releases")) {
80
- await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
81
- const releaseActionService = getService("release-action", { strapi: strapi2 });
82
- const eventManager = getService("event-manager", { strapi: strapi2 });
83
- const destroyContentTypeUpdateListener = strapi2.eventHub.on(
84
- "content-type.update",
85
- async ({ contentType }) => {
86
- if (contentType.schema?.options?.draftAndPublish === false) {
87
- await releaseActionService.deleteManyForContentType(contentType.uid);
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
88
140
  }
89
141
  }
90
- );
91
- eventManager.addDestroyListenerCallback(destroyContentTypeUpdateListener);
92
- const destroyContentTypeDeleteListener = strapi2.eventHub.on(
93
- "content-type.delete",
94
- async ({ contentType }) => {
95
- await releaseActionService.deleteManyForContentType(contentType.uid);
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"
96
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
97
190
  );
98
- eventManager.addDestroyListenerCallback(destroyContentTypeDeleteListener);
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");
281
+ const register = async ({ strapi: strapi2 }) => {
282
+ if (features$2.isEnabled("cms-content-releases")) {
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();
99
291
  }
100
292
  };
101
293
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
102
294
  const bootstrap = async ({ strapi: strapi2 }) => {
103
295
  if (features$1.isEnabled("cms-content-releases")) {
296
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
297
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
298
+ );
104
299
  strapi2.db.lifecycles.subscribe({
105
- afterDelete(event) {
106
- const { model, result } = event;
107
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
108
- const { id } = result;
109
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
110
- where: {
111
- target_type: model.uid,
112
- 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);
113
322
  }
114
- });
323
+ }
324
+ } catch (error) {
325
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
115
326
  }
116
327
  },
117
328
  /**
@@ -131,20 +342,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
131
342
  * We make this only after deleteMany is succesfully executed to avoid errors
132
343
  */
133
344
  async afterDeleteMany(event) {
134
- const { model, state } = event;
135
- const entriesToDelete = state.entriesToDelete;
136
- if (entriesToDelete) {
137
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
138
- where: {
139
- target_type: model.uid,
140
- target_id: {
141
- $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
+ }
142
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);
143
371
  }
372
+ }
373
+ } catch (error) {
374
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
375
+ error
144
376
  });
145
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
+ }
146
414
  }
147
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();
148
433
  }
149
434
  };
150
435
  const schema$1 = {
@@ -173,6 +458,17 @@ const schema$1 = {
173
458
  releasedAt: {
174
459
  type: "datetime"
175
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
+ },
176
472
  actions: {
177
473
  type: "relation",
178
474
  relation: "oneToMany",
@@ -225,6 +521,9 @@ const schema = {
225
521
  relation: "manyToOne",
226
522
  target: RELEASE_MODEL_UID,
227
523
  inversedBy: "actions"
524
+ },
525
+ isEntryValid: {
526
+ type: "boolean"
228
527
  }
229
528
  }
230
529
  };
@@ -235,15 +534,6 @@ const contentTypes = {
235
534
  release: release$1,
236
535
  "release-action": releaseAction$1
237
536
  };
238
- const createReleaseActionService = ({ strapi: strapi2 }) => ({
239
- async deleteManyForContentType(contentTypeUid) {
240
- return strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
241
- where: {
242
- target_type: contentTypeUid
243
- }
244
- });
245
- }
246
- });
247
537
  const getGroupName = (queryValue) => {
248
538
  switch (queryValue) {
249
539
  case "contentType":
@@ -256,367 +546,594 @@ const getGroupName = (queryValue) => {
256
546
  return "contentType.displayName";
257
547
  }
258
548
  };
259
- const createReleaseService = ({ strapi: strapi2 }) => ({
260
- async create(releaseData, { user }) {
261
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
262
- await getService("release-validation", { strapi: strapi2 }).validatePendingReleasesLimit();
263
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
264
- data: releaseWithCreatorFields
265
- });
266
- },
267
- async findOne(id, query = {}) {
268
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
269
- ...query
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
270
555
  });
271
- return release2;
272
- },
273
- findPage(query) {
274
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
275
- ...query,
276
- populate: {
277
- actions: {
278
- // @ts-expect-error Ignore missing properties
279
- count: true
280
- }
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);
281
567
  }
282
- });
283
- },
284
- async findManyForContentTypeEntry(contentTypeUid, entryId, {
285
- hasEntryAttached
286
- } = {
287
- hasEntryAttached: false
288
- }) {
289
- const whereActions = hasEntryAttached ? {
290
- // Find all Releases where the content type entry is present
291
- actions: {
292
- target_type: contentTypeUid,
293
- target_id: entryId
568
+ } catch (error) {
569
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
570
+ ;
571
+ else {
572
+ throw error;
294
573
  }
295
- } : {
296
- // Find all Releases where the content type entry is not present
297
- $or: [
298
- {
299
- $not: {
300
- actions: {
301
- target_type: contentTypeUid,
302
- target_id: entryId
303
- }
304
- }
305
- },
306
- {
307
- actions: null
308
- }
309
- ]
310
- };
311
- const populateAttachedAction = hasEntryAttached ? {
312
- // Filter the action to get only the content type entry
313
- actions: {
314
- where: {
315
- target_type: contentTypeUid,
316
- target_id: entryId
317
- }
318
- }
319
- } : {};
320
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
321
- where: {
322
- ...whereActions,
323
- releasedAt: {
324
- $null: true
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
325
584
  }
326
585
  },
327
- populate: {
328
- ...populateAttachedAction
329
- }
330
- });
331
- return releases.map((release2) => {
332
- if (release2.actions?.length) {
333
- const [actionForEntry] = release2.actions;
334
- delete release2.actions;
335
- return {
336
- ...release2,
337
- action: actionForEntry
338
- };
339
- }
340
- return release2;
586
+ populate
341
587
  });
342
- },
343
- async update(id, releaseData, { user }) {
344
- const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
345
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
346
- if (!release2) {
347
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
348
- }
349
- if (release2.releasedAt) {
350
- throw new utils.errors.ValidationError("Release already published");
351
- }
352
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
353
- /*
354
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
355
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
356
- */
357
- // @ts-expect-error see above
358
- data: releaseWithCreatorFields
359
- });
360
- return updatedRelease;
361
- },
362
- async createAction(releaseId, action) {
363
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
364
- strapi: strapi2
588
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
589
+ filters: {
590
+ id: {
591
+ $in: entriestoUnpublishIds
592
+ }
593
+ },
594
+ populate
365
595
  });
366
- await Promise.all([
367
- validateEntryContentType(action.entry.contentType),
368
- validateUniqueEntry(releaseId, action)
369
- ]);
370
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
371
- if (!release2) {
372
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
596
+ if (entriesToPublish.length > 0) {
597
+ await entityManagerService.publishMany(entriesToPublish, uid);
373
598
  }
374
- if (release2.releasedAt) {
375
- throw new utils.errors.ValidationError("Release already published");
599
+ if (entriesToUnpublish.length > 0) {
600
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
376
601
  }
377
- const { entry, type } = action;
378
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
379
- data: {
380
- type,
381
- contentType: entry.contentType,
382
- locale: entry.locale,
383
- entry: {
384
- id: entry.id,
385
- __type: entry.contentType,
386
- __pivot: { field: "entry" }
387
- },
388
- release: releaseId
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
+ }
389
609
  },
390
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
391
- });
392
- },
393
- async findActions(releaseId, query) {
394
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
395
- fields: ["id"]
396
- });
397
- if (!release2) {
398
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
399
- }
400
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
401
- ...query,
402
610
  populate: {
403
611
  entry: {
404
- populate: "*"
612
+ fields: ["id"]
405
613
  }
406
- },
407
- filters: {
408
- release: releaseId
409
614
  }
410
615
  });
411
- },
412
- async countActions(query) {
413
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
414
- },
415
- async groupActions(actions, groupBy) {
416
- const contentTypeUids = actions.reduce((acc, action) => {
417
- if (!acc.includes(action.contentType)) {
418
- acc.push(action.contentType);
419
- }
420
- return acc;
421
- }, []);
422
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
423
- contentTypeUids
424
- );
425
- const allLocalesDictionary = await this.getLocalesDataForActions();
426
- const formattedData = actions.map((action) => {
427
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
428
- return {
429
- ...action,
430
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
431
- contentType: {
432
- displayName,
433
- mainFieldValue: action.entry[mainField],
434
- uid: action.contentType
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
+ };
435
629
  }
436
- };
437
- });
438
- const groupName = getGroupName(groupBy);
439
- return ___default.default.groupBy(groupName)(formattedData);
440
- },
441
- async getLocalesDataForActions() {
442
- if (!strapi2.plugin("i18n")) {
443
- return {};
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
+ }
444
642
  }
445
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
446
- return allLocales.reduce((acc, locale) => {
447
- acc[locale.code] = { name: locale.name, code: locale.code };
448
- return acc;
449
- }, {});
450
- },
451
- async getContentTypesDataForActions(contentTypesUids) {
452
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
453
- const contentTypesData = {};
454
- for (const contentTypeUid of contentTypesUids) {
455
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
456
- uid: contentTypeUid
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
+ }
457
663
  });
458
- contentTypesData[contentTypeUid] = {
459
- mainField: contentTypeConfig.settings.mainField,
460
- displayName: strapi2.getModel(contentTypeUid).info.displayName
461
- };
462
- }
463
- return contentTypesData;
464
- },
465
- getContentTypeModelsFromActions(actions) {
466
- const contentTypeUids = actions.reduce((acc, action) => {
467
- if (!acc.includes(action.contentType)) {
468
- acc.push(action.contentType);
664
+ if (releaseWithCreatorFields.scheduledAt) {
665
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
666
+ await schedulingService.set(release2.id, release2.scheduledAt);
469
667
  }
470
- return acc;
471
- }, []);
472
- const contentTypeModelsMap = contentTypeUids.reduce(
473
- (acc, contentTypeUid) => {
474
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
475
- return acc;
476
- },
477
- {}
478
- );
479
- return contentTypeModelsMap;
480
- },
481
- async getAllComponents() {
482
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
483
- const components = await contentManagerComponentsService.findAllComponents();
484
- const componentsMap = components.reduce(
485
- (acc, component) => {
486
- acc[component.uid] = component;
487
- return acc;
488
- },
489
- {}
490
- );
491
- return componentsMap;
492
- },
493
- async delete(releaseId) {
494
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
495
- populate: {
496
- actions: {
497
- fields: ["id"]
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
684
+ }
498
685
  }
686
+ });
687
+ },
688
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entriesIds) {
689
+ let entries = entriesIds;
690
+ if (!Array.isArray(entriesIds)) {
691
+ entries = [entriesIds];
499
692
  }
500
- });
501
- if (!release2) {
502
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
503
- }
504
- if (release2.releasedAt) {
505
- throw new utils.errors.ValidationError("Release already published");
506
- }
507
- await strapi2.db.transaction(async () => {
508
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
693
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
509
694
  where: {
510
- id: {
511
- $in: release2.actions.map((action) => action.id)
695
+ actions: {
696
+ target_type: contentTypeUid,
697
+ target_id: {
698
+ $in: entries
699
+ }
700
+ },
701
+ releasedAt: {
702
+ $null: true
512
703
  }
513
- }
514
- });
515
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
516
- });
517
- return release2;
518
- },
519
- async publish(releaseId) {
520
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
521
- RELEASE_MODEL_UID,
522
- releaseId,
523
- {
704
+ },
524
705
  populate: {
706
+ // Filter the action to get only the content type entry
525
707
  actions: {
708
+ where: {
709
+ target_type: contentTypeUid,
710
+ target_id: {
711
+ $in: entries
712
+ }
713
+ },
526
714
  populate: {
527
- entry: true
715
+ entry: {
716
+ select: ["id"]
717
+ }
528
718
  }
529
719
  }
530
720
  }
531
- }
532
- );
533
- if (!releaseWithPopulatedActionEntries) {
534
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
535
- }
536
- if (releaseWithPopulatedActionEntries.releasedAt) {
537
- throw new utils.errors.ValidationError("Release already published");
538
- }
539
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
540
- throw new utils.errors.ValidationError("No entries to publish");
541
- }
542
- const actions = {};
543
- for (const action of releaseWithPopulatedActionEntries.actions) {
544
- const contentTypeUid = action.contentType;
545
- if (!actions[contentTypeUid]) {
546
- actions[contentTypeUid] = {
547
- publish: [],
548
- unpublish: []
549
- };
550
- }
551
- if (action.type === "publish") {
552
- actions[contentTypeUid].publish.push(action.entry);
553
- } else {
554
- actions[contentTypeUid].unpublish.push(action.entry);
555
- }
556
- }
557
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
558
- await strapi2.db.transaction(async () => {
559
- for (const contentTypeUid of Object.keys(actions)) {
560
- const { publish, unpublish } = actions[contentTypeUid];
561
- if (publish.length > 0) {
562
- 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
+ }
563
744
  }
564
- if (unpublish.length > 0) {
565
- await entityManagerService.unpublishMany(unpublish, contentTypeUid);
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
+ }
566
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}`);
567
790
  }
568
- });
569
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
570
- 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, {
571
795
  /*
572
- * 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']
573
798
  */
574
799
  // @ts-expect-error see above
575
- 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);
576
807
  }
577
- });
578
- return release2;
579
- },
580
- async updateAction(actionId, releaseId, update) {
581
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
582
- where: {
583
- id: actionId,
584
- release: {
585
- id: releaseId,
586
- releasedAt: {
587
- $null: true
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: "*"
588
860
  }
861
+ },
862
+ filters: {
863
+ release: releaseId
589
864
  }
590
- },
591
- data: update
592
- });
593
- if (!updatedAction) {
594
- throw new utils.errors.NotFoundError(
595
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
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
596
879
  );
597
- }
598
- return updatedAction;
599
- },
600
- async deleteAction(actionId, releaseId) {
601
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
602
- where: {
603
- id: actionId,
604
- release: {
605
- id: releaseId,
606
- releasedAt: {
607
- $null: true
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
608
890
  }
609
- }
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 {};
610
899
  }
611
- });
612
- if (!deletedAction) {
613
- throw new utils.errors.NotFoundError(
614
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
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
+ {}
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
+ {}
615
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
+ });
616
1128
  }
617
- return deletedAction;
1129
+ };
1130
+ };
1131
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1132
+ constructor(message) {
1133
+ super(message);
1134
+ this.name = "AlreadyOnReleaseError";
618
1135
  }
619
- });
1136
+ }
620
1137
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
621
1138
  async validateUniqueEntry(releaseId, releaseActionArgs) {
622
1139
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -629,7 +1146,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
629
1146
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
630
1147
  );
631
1148
  if (isEntryInRelease) {
632
- throw new utils.errors.ValidationError(
1149
+ throw new AlreadyOnReleaseError(
633
1150
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
634
1151
  );
635
1152
  }
@@ -660,34 +1177,104 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
660
1177
  if (pendingReleasesCount >= maximumPendingReleases) {
661
1178
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
662
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
+ }
663
1200
  }
664
1201
  });
665
- const createEventManagerService = () => {
666
- const state = {
667
- destroyListenerCallbacks: []
668
- };
1202
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1203
+ const scheduledJobs = /* @__PURE__ */ new Map();
669
1204
  return {
670
- addDestroyListenerCallback(destroyListenerCallback) {
671
- state.destroyListenerCallbacks.push(destroyListenerCallback);
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;
672
1222
  },
673
- destroyAllListeners() {
674
- if (!state.destroyListenerCallbacks.length) {
675
- return;
1223
+ cancel(releaseId) {
1224
+ if (scheduledJobs.has(releaseId)) {
1225
+ scheduledJobs.get(releaseId).cancel();
1226
+ scheduledJobs.delete(releaseId);
676
1227
  }
677
- state.destroyListenerCallbacks.forEach((destroyListenerCallback) => {
678
- destroyListenerCallback();
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
+ }
679
1246
  });
1247
+ for (const release2 of releases) {
1248
+ this.set(release2.id, release2.scheduledAt);
1249
+ }
1250
+ return scheduledJobs;
680
1251
  }
681
1252
  };
682
1253
  };
683
1254
  const services = {
684
1255
  release: createReleaseService,
685
- "release-action": createReleaseActionService,
686
1256
  "release-validation": createReleaseValidationService,
687
- "event-manager": createEventManagerService
1257
+ scheduling: createSchedulingService
688
1258
  };
689
1259
  const RELEASE_SCHEMA = yup__namespace.object().shape({
690
- 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
+ })
691
1278
  }).required().noUnknown();
692
1279
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
693
1280
  const releaseController = {
@@ -704,9 +1291,7 @@ const releaseController = {
704
1291
  const contentTypeUid = query.contentTypeUid;
705
1292
  const entryId = query.entryId;
706
1293
  const hasEntryAttached = typeof query.hasEntryAttached === "string" ? JSON.parse(query.hasEntryAttached) : false;
707
- const data = await releaseService.findManyForContentTypeEntry(contentTypeUid, entryId, {
708
- hasEntryAttached
709
- });
1294
+ const data = hasEntryAttached ? await releaseService.findManyWithContentTypeEntryAttached(contentTypeUid, entryId) : await releaseService.findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId);
710
1295
  ctx.body = { data };
711
1296
  } else {
712
1297
  const query = await permissionsManager.sanitizeQuery(ctx.query);
@@ -722,26 +1307,30 @@ const releaseController = {
722
1307
  }
723
1308
  };
724
1309
  });
725
- 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 } };
726
1316
  }
727
1317
  },
728
1318
  async findOne(ctx) {
729
1319
  const id = ctx.params.id;
730
1320
  const releaseService = getService("release", { strapi });
731
1321
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
732
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
733
- ability: ctx.state.userAbility,
734
- model: RELEASE_MODEL_UID
735
- });
736
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
1322
+ if (!release2) {
1323
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
1324
+ }
737
1325
  const count = await releaseService.countActions({
738
1326
  filters: {
739
1327
  release: id
740
1328
  }
741
1329
  });
742
- if (!release2) {
743
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
744
- }
1330
+ const sanitizedRelease = {
1331
+ ...release2,
1332
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1333
+ };
745
1334
  const data = {
746
1335
  ...sanitizedRelease,
747
1336
  actions: {
@@ -752,6 +1341,33 @@ const releaseController = {
752
1341
  };
753
1342
  ctx.body = { data };
754
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
+ },
755
1371
  async create(ctx) {
756
1372
  const user = ctx.state.user;
757
1373
  const releaseArgs = ctx.request.body;
@@ -794,8 +1410,27 @@ const releaseController = {
794
1410
  const id = ctx.params.id;
795
1411
  const releaseService = getService("release", { strapi });
796
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
+ ]);
797
1427
  ctx.body = {
798
- data: release2
1428
+ data: release2,
1429
+ meta: {
1430
+ totalEntries: countPublishActions + countUnpublishActions,
1431
+ totalPublishedEntries: countPublishActions,
1432
+ totalUnpublishedEntries: countUnpublishActions
1433
+ }
799
1434
  };
800
1435
  }
801
1436
  };
@@ -822,6 +1457,38 @@ const releaseActionController = {
822
1457
  data: releaseAction2
823
1458
  };
824
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
+ },
825
1492
  async findMany(ctx) {
826
1493
  const releaseId = ctx.params.releaseId;
827
1494
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -890,6 +1557,22 @@ const controllers = { release: releaseController, "release-action": releaseActio
890
1557
  const release = {
891
1558
  type: "admin",
892
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
+ },
893
1576
  {
894
1577
  method: "POST",
895
1578
  path: "/",
@@ -1007,6 +1690,22 @@ const releaseAction = {
1007
1690
  ]
1008
1691
  }
1009
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
+ },
1010
1709
  {
1011
1710
  method: "GET",
1012
1711
  path: "/:releaseId/actions",
@@ -1067,18 +1766,17 @@ const getPlugin = () => {
1067
1766
  return {
1068
1767
  register,
1069
1768
  bootstrap,
1769
+ destroy,
1070
1770
  contentTypes,
1071
1771
  services,
1072
1772
  controllers,
1073
- routes,
1074
- destroy() {
1075
- if (features.isEnabled("cms-content-releases")) {
1076
- getService("event-manager").destroyAllListeners();
1077
- }
1078
- }
1773
+ routes
1079
1774
  };
1080
1775
  }
1081
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
1082
1780
  contentTypes
1083
1781
  };
1084
1782
  };