@strapi/content-releases 0.0.0-next.6d59515520a3850456f256fb0e4c54b75054ddf4 → 0.0.0-next.898f8ae81b2cb3f89bd012e9db20a2d9b78a48d2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/_chunks/{App-_20W9dYa.js → App-OK4Xac-O.js} +518 -240
  2. package/dist/_chunks/App-OK4Xac-O.js.map +1 -0
  3. package/dist/_chunks/{App-L1jSxCiL.mjs → App-xAkiD42p.mjs} +522 -245
  4. package/dist/_chunks/App-xAkiD42p.mjs.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-gYDqKYFd.js → en-r0otWaln.js} +18 -4
  10. package/dist/_chunks/en-r0otWaln.js.map +1 -0
  11. package/dist/_chunks/{en-MyLPoISH.mjs → en-veqvqeEr.mjs} +18 -4
  12. package/dist/_chunks/en-veqvqeEr.mjs.map +1 -0
  13. package/dist/_chunks/{index-KJa1Rb5F.js → index-JvA2_26n.js} +134 -27
  14. package/dist/_chunks/index-JvA2_26n.js.map +1 -0
  15. package/dist/_chunks/{index-c4zRX_sg.mjs → index-exoiSU3V.mjs} +139 -32
  16. package/dist/_chunks/index-exoiSU3V.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 +597 -382
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +597 -382
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +12 -9
  24. package/dist/_chunks/App-L1jSxCiL.mjs.map +0 -1
  25. package/dist/_chunks/App-_20W9dYa.js.map +0 -1
  26. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  27. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  28. package/dist/_chunks/index-KJa1Rb5F.js.map +0 -1
  29. package/dist/_chunks/index-c4zRX_sg.mjs.map +0 -1
@@ -3,6 +3,7 @@ const utils = require("@strapi/utils");
3
3
  const lodash = require("lodash");
4
4
  const _ = require("lodash/fp");
5
5
  const EE = require("@strapi/strapi/dist/utils/ee");
6
+ const nodeSchedule = require("node-schedule");
6
7
  const yup = require("yup");
7
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
8
9
  function _interopNamespace(e) {
@@ -72,6 +73,9 @@ const ACTIONS = [
72
73
  pluginName: "content-releases"
73
74
  }
74
75
  ];
76
+ const ALLOWED_WEBHOOK_EVENTS = {
77
+ RELEASES_PUBLISH: "releases.publish"
78
+ };
75
79
  async function deleteActionsOnDisableDraftAndPublish({
76
80
  oldContentTypes,
77
81
  contentTypes: contentTypes2
@@ -106,6 +110,9 @@ const register = async ({ strapi: strapi2 }) => {
106
110
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
107
111
  }
108
112
  };
113
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
114
+ return strapi2.plugin("content-releases").service(name);
115
+ };
109
116
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
110
117
  const bootstrap = async ({ strapi: strapi2 }) => {
111
118
  if (features$1.isEnabled("cms-content-releases")) {
@@ -153,6 +160,27 @@ const bootstrap = async ({ strapi: strapi2 }) => {
153
160
  }
154
161
  }
155
162
  });
163
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
164
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
165
+ strapi2.log.error(
166
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
167
+ );
168
+ throw err;
169
+ });
170
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
171
+ strapi2.webhookStore.addAllowedEvent(key, value);
172
+ });
173
+ }
174
+ }
175
+ };
176
+ const destroy = async ({ strapi: strapi2 }) => {
177
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
178
+ const scheduledJobs = getService("scheduling", {
179
+ strapi: strapi2
180
+ }).getAll();
181
+ for (const [, job] of scheduledJobs) {
182
+ job.cancel();
183
+ }
156
184
  }
157
185
  };
158
186
  const schema$1 = {
@@ -181,6 +209,12 @@ const schema$1 = {
181
209
  releasedAt: {
182
210
  type: "datetime"
183
211
  },
212
+ scheduledAt: {
213
+ type: "datetime"
214
+ },
215
+ timezone: {
216
+ type: "string"
217
+ },
184
218
  actions: {
185
219
  type: "relation",
186
220
  relation: "oneToMany",
@@ -243,9 +277,6 @@ const contentTypes = {
243
277
  release: release$1,
244
278
  "release-action": releaseAction$1
245
279
  };
246
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
247
- return strapi2.plugin("content-releases").service(name);
248
- };
249
280
  const getGroupName = (queryValue) => {
250
281
  switch (queryValue) {
251
282
  case "contentType":
@@ -258,415 +289,504 @@ const getGroupName = (queryValue) => {
258
289
  return "contentType.displayName";
259
290
  }
260
291
  };
261
- const createReleaseService = ({ strapi: strapi2 }) => ({
262
- async create(releaseData, { user }) {
263
- const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
264
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
265
- "release-validation",
266
- { strapi: strapi2 }
267
- );
268
- await Promise.all([
269
- validatePendingReleasesLimit(),
270
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
271
- ]);
272
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
273
- data: releaseWithCreatorFields
292
+ const createReleaseService = ({ strapi: strapi2 }) => {
293
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
294
+ strapi2.eventHub.emit(event, {
295
+ isPublished,
296
+ error,
297
+ release: release2
274
298
  });
275
- },
276
- async findOne(id, query = {}) {
277
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
278
- ...query
279
- });
280
- return release2;
281
- },
282
- findPage(query) {
283
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
284
- ...query,
285
- populate: {
286
- actions: {
287
- // @ts-expect-error Ignore missing properties
288
- count: true
289
- }
299
+ };
300
+ return {
301
+ async create(releaseData, { user }) {
302
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
303
+ const {
304
+ validatePendingReleasesLimit,
305
+ validateUniqueNameForPendingRelease,
306
+ validateScheduledAtIsLaterThanNow
307
+ } = getService("release-validation", { strapi: strapi2 });
308
+ await Promise.all([
309
+ validatePendingReleasesLimit(),
310
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
311
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
312
+ ]);
313
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
314
+ data: releaseWithCreatorFields
315
+ });
316
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
317
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
318
+ await schedulingService.set(release2.id, release2.scheduledAt);
290
319
  }
291
- });
292
- },
293
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
294
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
295
- where: {
296
- actions: {
297
- target_type: contentTypeUid,
298
- target_id: entryId
299
- },
300
- releasedAt: {
301
- $null: true
320
+ strapi2.telemetry.send("didCreateContentRelease");
321
+ return release2;
322
+ },
323
+ async findOne(id, query = {}) {
324
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
325
+ ...query
326
+ });
327
+ return release2;
328
+ },
329
+ findPage(query) {
330
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
331
+ ...query,
332
+ populate: {
333
+ actions: {
334
+ // @ts-expect-error Ignore missing properties
335
+ count: true
336
+ }
302
337
  }
303
- },
304
- populate: {
305
- // Filter the action to get only the content type entry
306
- actions: {
307
- where: {
338
+ });
339
+ },
340
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
341
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
342
+ where: {
343
+ actions: {
308
344
  target_type: contentTypeUid,
309
345
  target_id: entryId
346
+ },
347
+ releasedAt: {
348
+ $null: true
310
349
  }
311
- }
312
- }
313
- });
314
- return releases.map((release2) => {
315
- if (release2.actions?.length) {
316
- const [actionForEntry] = release2.actions;
317
- delete release2.actions;
318
- return {
319
- ...release2,
320
- action: actionForEntry
321
- };
322
- }
323
- return release2;
324
- });
325
- },
326
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
327
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
328
- where: {
329
- releasedAt: {
330
- $null: true
331
350
  },
332
- actions: {
333
- target_type: contentTypeUid,
334
- target_id: entryId
335
- }
336
- }
337
- });
338
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
339
- where: {
340
- $or: [
341
- {
342
- id: {
343
- $notIn: releasesRelated.map((release2) => release2.id)
351
+ populate: {
352
+ // Filter the action to get only the content type entry
353
+ actions: {
354
+ where: {
355
+ target_type: contentTypeUid,
356
+ target_id: entryId
344
357
  }
358
+ }
359
+ }
360
+ });
361
+ return releases.map((release2) => {
362
+ if (release2.actions?.length) {
363
+ const [actionForEntry] = release2.actions;
364
+ delete release2.actions;
365
+ return {
366
+ ...release2,
367
+ action: actionForEntry
368
+ };
369
+ }
370
+ return release2;
371
+ });
372
+ },
373
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
374
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
375
+ where: {
376
+ releasedAt: {
377
+ $null: true
345
378
  },
346
- {
347
- actions: null
379
+ actions: {
380
+ target_type: contentTypeUid,
381
+ target_id: entryId
348
382
  }
349
- ],
350
- releasedAt: {
351
- $null: true
352
383
  }
384
+ });
385
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
386
+ where: {
387
+ $or: [
388
+ {
389
+ id: {
390
+ $notIn: releasesRelated.map((release2) => release2.id)
391
+ }
392
+ },
393
+ {
394
+ actions: null
395
+ }
396
+ ],
397
+ releasedAt: {
398
+ $null: true
399
+ }
400
+ }
401
+ });
402
+ return releases.map((release2) => {
403
+ if (release2.actions?.length) {
404
+ const [actionForEntry] = release2.actions;
405
+ delete release2.actions;
406
+ return {
407
+ ...release2,
408
+ action: actionForEntry
409
+ };
410
+ }
411
+ return release2;
412
+ });
413
+ },
414
+ async update(id, releaseData, { user }) {
415
+ const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(
416
+ releaseData
417
+ );
418
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
419
+ "release-validation",
420
+ { strapi: strapi2 }
421
+ );
422
+ await Promise.all([
423
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
424
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
425
+ ]);
426
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
427
+ if (!release2) {
428
+ throw new utils.errors.NotFoundError(`No release found for id ${id}`);
353
429
  }
354
- });
355
- return releases.map((release2) => {
356
- if (release2.actions?.length) {
357
- const [actionForEntry] = release2.actions;
358
- delete release2.actions;
359
- return {
360
- ...release2,
361
- action: actionForEntry
362
- };
430
+ if (release2.releasedAt) {
431
+ throw new utils.errors.ValidationError("Release already published");
363
432
  }
364
- return release2;
365
- });
366
- },
367
- async update(id, releaseData, { user }) {
368
- const releaseWithCreatorFields = await utils.setCreatorFields({ user, isEdition: true })(releaseData);
369
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
370
- if (!release2) {
371
- throw new utils.errors.NotFoundError(`No release found for id ${id}`);
372
- }
373
- if (release2.releasedAt) {
374
- throw new utils.errors.ValidationError("Release already published");
375
- }
376
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
377
- /*
378
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
379
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
380
- */
381
- // @ts-expect-error see above
382
- data: releaseWithCreatorFields
383
- });
384
- return updatedRelease;
385
- },
386
- async createAction(releaseId, action) {
387
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
388
- strapi: strapi2
389
- });
390
- await Promise.all([
391
- validateEntryContentType(action.entry.contentType),
392
- validateUniqueEntry(releaseId, action)
393
- ]);
394
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
395
- if (!release2) {
396
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
397
- }
398
- if (release2.releasedAt) {
399
- throw new utils.errors.ValidationError("Release already published");
400
- }
401
- const { entry, type } = action;
402
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
403
- data: {
404
- type,
405
- contentType: entry.contentType,
406
- locale: entry.locale,
407
- entry: {
408
- id: entry.id,
409
- __type: entry.contentType,
410
- __pivot: { field: "entry" }
411
- },
412
- release: releaseId
413
- },
414
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
415
- });
416
- },
417
- async findActions(releaseId, query) {
418
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
419
- fields: ["id"]
420
- });
421
- if (!release2) {
422
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
423
- }
424
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
425
- ...query,
426
- populate: {
427
- entry: {
428
- populate: "*"
433
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
434
+ /*
435
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
436
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
437
+ */
438
+ // @ts-expect-error see above
439
+ data: releaseWithCreatorFields
440
+ });
441
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
442
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
443
+ if (releaseData.scheduledAt) {
444
+ await schedulingService.set(id, releaseData.scheduledAt);
445
+ } else if (release2.scheduledAt) {
446
+ schedulingService.cancel(id);
429
447
  }
430
- },
431
- filters: {
432
- release: releaseId
433
448
  }
434
- });
435
- },
436
- async countActions(query) {
437
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
438
- },
439
- async groupActions(actions, groupBy) {
440
- const contentTypeUids = actions.reduce((acc, action) => {
441
- if (!acc.includes(action.contentType)) {
442
- acc.push(action.contentType);
449
+ strapi2.telemetry.send("didUpdateContentRelease");
450
+ return updatedRelease;
451
+ },
452
+ async createAction(releaseId, action) {
453
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
454
+ strapi: strapi2
455
+ });
456
+ await Promise.all([
457
+ validateEntryContentType(action.entry.contentType),
458
+ validateUniqueEntry(releaseId, action)
459
+ ]);
460
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
461
+ if (!release2) {
462
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
443
463
  }
444
- return acc;
445
- }, []);
446
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
447
- contentTypeUids
448
- );
449
- const allLocalesDictionary = await this.getLocalesDataForActions();
450
- const formattedData = actions.map((action) => {
451
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
452
- return {
453
- ...action,
454
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
455
- contentType: {
456
- displayName,
457
- mainFieldValue: action.entry[mainField],
458
- uid: action.contentType
459
- }
460
- };
461
- });
462
- const groupName = getGroupName(groupBy);
463
- return ___default.default.groupBy(groupName)(formattedData);
464
- },
465
- async getLocalesDataForActions() {
466
- if (!strapi2.plugin("i18n")) {
467
- return {};
468
- }
469
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
470
- return allLocales.reduce((acc, locale) => {
471
- acc[locale.code] = { name: locale.name, code: locale.code };
472
- return acc;
473
- }, {});
474
- },
475
- async getContentTypesDataForActions(contentTypesUids) {
476
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
477
- const contentTypesData = {};
478
- for (const contentTypeUid of contentTypesUids) {
479
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
480
- uid: contentTypeUid
464
+ if (release2.releasedAt) {
465
+ throw new utils.errors.ValidationError("Release already published");
466
+ }
467
+ const { entry, type } = action;
468
+ return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
469
+ data: {
470
+ type,
471
+ contentType: entry.contentType,
472
+ locale: entry.locale,
473
+ entry: {
474
+ id: entry.id,
475
+ __type: entry.contentType,
476
+ __pivot: { field: "entry" }
477
+ },
478
+ release: releaseId
479
+ },
480
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
481
481
  });
482
- contentTypesData[contentTypeUid] = {
483
- mainField: contentTypeConfig.settings.mainField,
484
- displayName: strapi2.getModel(contentTypeUid).info.displayName
485
- };
486
- }
487
- return contentTypesData;
488
- },
489
- getContentTypeModelsFromActions(actions) {
490
- const contentTypeUids = actions.reduce((acc, action) => {
491
- if (!acc.includes(action.contentType)) {
492
- acc.push(action.contentType);
482
+ },
483
+ async findActions(releaseId, query) {
484
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
485
+ fields: ["id"]
486
+ });
487
+ if (!release2) {
488
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
493
489
  }
494
- return acc;
495
- }, []);
496
- const contentTypeModelsMap = contentTypeUids.reduce(
497
- (acc, contentTypeUid) => {
498
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
490
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
491
+ ...query,
492
+ populate: {
493
+ entry: {
494
+ populate: "*"
495
+ }
496
+ },
497
+ filters: {
498
+ release: releaseId
499
+ }
500
+ });
501
+ },
502
+ async countActions(query) {
503
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
504
+ },
505
+ async groupActions(actions, groupBy) {
506
+ const contentTypeUids = actions.reduce((acc, action) => {
507
+ if (!acc.includes(action.contentType)) {
508
+ acc.push(action.contentType);
509
+ }
499
510
  return acc;
500
- },
501
- {}
502
- );
503
- return contentTypeModelsMap;
504
- },
505
- async getAllComponents() {
506
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
507
- const components = await contentManagerComponentsService.findAllComponents();
508
- const componentsMap = components.reduce(
509
- (acc, component) => {
510
- acc[component.uid] = component;
511
+ }, []);
512
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
513
+ contentTypeUids
514
+ );
515
+ const allLocalesDictionary = await this.getLocalesDataForActions();
516
+ const formattedData = actions.map((action) => {
517
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
518
+ return {
519
+ ...action,
520
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
521
+ contentType: {
522
+ displayName,
523
+ mainFieldValue: action.entry[mainField],
524
+ uid: action.contentType
525
+ }
526
+ };
527
+ });
528
+ const groupName = getGroupName(groupBy);
529
+ return ___default.default.groupBy(groupName)(formattedData);
530
+ },
531
+ async getLocalesDataForActions() {
532
+ if (!strapi2.plugin("i18n")) {
533
+ return {};
534
+ }
535
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
536
+ return allLocales.reduce((acc, locale) => {
537
+ acc[locale.code] = { name: locale.name, code: locale.code };
511
538
  return acc;
512
- },
513
- {}
514
- );
515
- return componentsMap;
516
- },
517
- async delete(releaseId) {
518
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
519
- populate: {
520
- actions: {
521
- fields: ["id"]
522
- }
539
+ }, {});
540
+ },
541
+ async getContentTypesDataForActions(contentTypesUids) {
542
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
543
+ const contentTypesData = {};
544
+ for (const contentTypeUid of contentTypesUids) {
545
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
546
+ uid: contentTypeUid
547
+ });
548
+ contentTypesData[contentTypeUid] = {
549
+ mainField: contentTypeConfig.settings.mainField,
550
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
551
+ };
523
552
  }
524
- });
525
- if (!release2) {
526
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
527
- }
528
- if (release2.releasedAt) {
529
- throw new utils.errors.ValidationError("Release already published");
530
- }
531
- await strapi2.db.transaction(async () => {
532
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
533
- where: {
534
- id: {
535
- $in: release2.actions.map((action) => action.id)
536
- }
553
+ return contentTypesData;
554
+ },
555
+ getContentTypeModelsFromActions(actions) {
556
+ const contentTypeUids = actions.reduce((acc, action) => {
557
+ if (!acc.includes(action.contentType)) {
558
+ acc.push(action.contentType);
537
559
  }
538
- });
539
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
540
- });
541
- return release2;
542
- },
543
- async publish(releaseId) {
544
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
545
- RELEASE_MODEL_UID,
546
- releaseId,
547
- {
560
+ return acc;
561
+ }, []);
562
+ const contentTypeModelsMap = contentTypeUids.reduce(
563
+ (acc, contentTypeUid) => {
564
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
565
+ return acc;
566
+ },
567
+ {}
568
+ );
569
+ return contentTypeModelsMap;
570
+ },
571
+ async getAllComponents() {
572
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
573
+ const components = await contentManagerComponentsService.findAllComponents();
574
+ const componentsMap = components.reduce(
575
+ (acc, component) => {
576
+ acc[component.uid] = component;
577
+ return acc;
578
+ },
579
+ {}
580
+ );
581
+ return componentsMap;
582
+ },
583
+ async delete(releaseId) {
584
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
548
585
  populate: {
549
586
  actions: {
550
- populate: {
551
- entry: {
552
- fields: ["id"]
553
- }
554
- }
587
+ fields: ["id"]
555
588
  }
556
589
  }
590
+ });
591
+ if (!release2) {
592
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
557
593
  }
558
- );
559
- if (!releaseWithPopulatedActionEntries) {
560
- throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
561
- }
562
- if (releaseWithPopulatedActionEntries.releasedAt) {
563
- throw new utils.errors.ValidationError("Release already published");
564
- }
565
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
566
- throw new utils.errors.ValidationError("No entries to publish");
567
- }
568
- const actions = {};
569
- for (const action of releaseWithPopulatedActionEntries.actions) {
570
- const contentTypeUid = action.contentType;
571
- if (!actions[contentTypeUid]) {
572
- actions[contentTypeUid] = {
573
- entriestoPublishIds: [],
574
- entriesToUnpublishIds: []
575
- };
594
+ if (release2.releasedAt) {
595
+ throw new utils.errors.ValidationError("Release already published");
576
596
  }
577
- if (action.type === "publish") {
578
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
579
- } else {
580
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
597
+ await strapi2.db.transaction(async () => {
598
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
599
+ where: {
600
+ id: {
601
+ $in: release2.actions.map((action) => action.id)
602
+ }
603
+ }
604
+ });
605
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
606
+ });
607
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
608
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
609
+ await schedulingService.cancel(release2.id);
581
610
  }
582
- }
583
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
584
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
585
- await strapi2.db.transaction(async () => {
586
- for (const contentTypeUid of Object.keys(actions)) {
587
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
588
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
589
- const entriesToPublish = await strapi2.entityService.findMany(
590
- contentTypeUid,
611
+ strapi2.telemetry.send("didDeleteContentRelease");
612
+ return release2;
613
+ },
614
+ async publish(releaseId) {
615
+ try {
616
+ const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
617
+ RELEASE_MODEL_UID,
618
+ releaseId,
591
619
  {
592
- filters: {
593
- id: {
594
- $in: entriestoPublishIds
620
+ populate: {
621
+ actions: {
622
+ populate: {
623
+ entry: {
624
+ fields: ["id"]
625
+ }
626
+ }
595
627
  }
596
- },
597
- populate
628
+ }
598
629
  }
599
630
  );
600
- const entriesToUnpublish = await strapi2.entityService.findMany(
601
- contentTypeUid,
602
- {
603
- filters: {
604
- id: {
605
- $in: entriesToUnpublishIds
631
+ if (!releaseWithPopulatedActionEntries) {
632
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
633
+ }
634
+ if (releaseWithPopulatedActionEntries.releasedAt) {
635
+ throw new utils.errors.ValidationError("Release already published");
636
+ }
637
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
+ throw new utils.errors.ValidationError("No entries to publish");
639
+ }
640
+ const collectionTypeActions = {};
641
+ const singleTypeActions = [];
642
+ for (const action of releaseWithPopulatedActionEntries.actions) {
643
+ const contentTypeUid = action.contentType;
644
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
645
+ if (!collectionTypeActions[contentTypeUid]) {
646
+ collectionTypeActions[contentTypeUid] = {
647
+ entriestoPublishIds: [],
648
+ entriesToUnpublishIds: []
649
+ };
650
+ }
651
+ if (action.type === "publish") {
652
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
653
+ } else {
654
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
655
+ }
656
+ } else {
657
+ singleTypeActions.push({
658
+ uid: contentTypeUid,
659
+ action: action.type,
660
+ id: action.entry.id
661
+ });
662
+ }
663
+ }
664
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
665
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
666
+ await strapi2.db.transaction(async () => {
667
+ for (const { uid, action, id } of singleTypeActions) {
668
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
669
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
670
+ try {
671
+ if (action === "publish") {
672
+ await entityManagerService.publish(entry, uid);
673
+ } else {
674
+ await entityManagerService.unpublish(entry, uid);
606
675
  }
607
- },
608
- populate
676
+ } catch (error) {
677
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
678
+ } else {
679
+ throw error;
680
+ }
681
+ }
609
682
  }
610
- );
611
- if (entriesToPublish.length > 0) {
612
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
683
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
684
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
685
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
686
+ const entriesToPublish = await strapi2.entityService.findMany(
687
+ contentTypeUid,
688
+ {
689
+ filters: {
690
+ id: {
691
+ $in: entriestoPublishIds
692
+ }
693
+ },
694
+ populate
695
+ }
696
+ );
697
+ const entriesToUnpublish = await strapi2.entityService.findMany(
698
+ contentTypeUid,
699
+ {
700
+ filters: {
701
+ id: {
702
+ $in: entriesToUnpublishIds
703
+ }
704
+ },
705
+ populate
706
+ }
707
+ );
708
+ if (entriesToPublish.length > 0) {
709
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
710
+ }
711
+ if (entriesToUnpublish.length > 0) {
712
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
713
+ }
714
+ }
715
+ });
716
+ const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
717
+ data: {
718
+ /*
719
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
720
+ */
721
+ // @ts-expect-error see above
722
+ releasedAt: /* @__PURE__ */ new Date()
723
+ },
724
+ populate: {
725
+ actions: {
726
+ // @ts-expect-error is not expecting count but it is working
727
+ count: true
728
+ }
729
+ }
730
+ });
731
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
732
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
733
+ isPublished: true,
734
+ release: release2
735
+ });
613
736
  }
614
- if (entriesToUnpublish.length > 0) {
615
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
737
+ strapi2.telemetry.send("didPublishContentRelease");
738
+ return release2;
739
+ } catch (error) {
740
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
741
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
742
+ isPublished: false,
743
+ error
744
+ });
616
745
  }
746
+ throw error;
617
747
  }
618
- });
619
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
620
- data: {
621
- /*
622
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
623
- */
624
- // @ts-expect-error see above
625
- releasedAt: /* @__PURE__ */ new Date()
626
- }
627
- });
628
- return release2;
629
- },
630
- async updateAction(actionId, releaseId, update) {
631
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
632
- where: {
633
- id: actionId,
634
- release: {
635
- id: releaseId,
636
- releasedAt: {
637
- $null: true
748
+ },
749
+ async updateAction(actionId, releaseId, update) {
750
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
751
+ where: {
752
+ id: actionId,
753
+ release: {
754
+ id: releaseId,
755
+ releasedAt: {
756
+ $null: true
757
+ }
638
758
  }
639
- }
640
- },
641
- data: update
642
- });
643
- if (!updatedAction) {
644
- throw new utils.errors.NotFoundError(
645
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
646
- );
647
- }
648
- return updatedAction;
649
- },
650
- async deleteAction(actionId, releaseId) {
651
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
652
- where: {
653
- id: actionId,
654
- release: {
655
- id: releaseId,
656
- releasedAt: {
657
- $null: true
759
+ },
760
+ data: update
761
+ });
762
+ if (!updatedAction) {
763
+ throw new utils.errors.NotFoundError(
764
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
765
+ );
766
+ }
767
+ return updatedAction;
768
+ },
769
+ async deleteAction(actionId, releaseId) {
770
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
771
+ where: {
772
+ id: actionId,
773
+ release: {
774
+ id: releaseId,
775
+ releasedAt: {
776
+ $null: true
777
+ }
658
778
  }
659
779
  }
780
+ });
781
+ if (!deletedAction) {
782
+ throw new utils.errors.NotFoundError(
783
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
784
+ );
660
785
  }
661
- });
662
- if (!deletedAction) {
663
- throw new utils.errors.NotFoundError(
664
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
665
- );
786
+ return deletedAction;
666
787
  }
667
- return deletedAction;
668
- }
669
- });
788
+ };
789
+ };
670
790
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
671
791
  async validateUniqueEntry(releaseId, releaseActionArgs) {
672
792
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -711,27 +831,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
711
831
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
712
832
  }
713
833
  },
714
- async validateUniqueNameForPendingRelease(name) {
834
+ async validateUniqueNameForPendingRelease(name, id) {
715
835
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
716
836
  filters: {
717
837
  releasedAt: {
718
838
  $null: true
719
839
  },
720
- name
840
+ name,
841
+ ...id && { id: { $ne: id } }
721
842
  }
722
843
  });
723
844
  const isNameUnique = pendingReleases.length === 0;
724
845
  if (!isNameUnique) {
725
846
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
726
847
  }
848
+ },
849
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
850
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
851
+ throw new utils.errors.ValidationError("Scheduled at must be later than now");
852
+ }
727
853
  }
728
854
  });
855
+ const createSchedulingService = ({ strapi: strapi2 }) => {
856
+ const scheduledJobs = /* @__PURE__ */ new Map();
857
+ return {
858
+ async set(releaseId, scheduleDate) {
859
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
860
+ if (!release2) {
861
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
862
+ }
863
+ const job = nodeSchedule.scheduleJob(scheduleDate, async () => {
864
+ try {
865
+ await getService("release").publish(releaseId);
866
+ } catch (error) {
867
+ }
868
+ this.cancel(releaseId);
869
+ });
870
+ if (scheduledJobs.has(releaseId)) {
871
+ this.cancel(releaseId);
872
+ }
873
+ scheduledJobs.set(releaseId, job);
874
+ return scheduledJobs;
875
+ },
876
+ cancel(releaseId) {
877
+ if (scheduledJobs.has(releaseId)) {
878
+ scheduledJobs.get(releaseId).cancel();
879
+ scheduledJobs.delete(releaseId);
880
+ }
881
+ return scheduledJobs;
882
+ },
883
+ getAll() {
884
+ return scheduledJobs;
885
+ },
886
+ /**
887
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
888
+ * This is useful in case the server was restarted and the scheduled jobs were lost
889
+ * This also could be used to sync different Strapi instances in case of a cluster
890
+ */
891
+ async syncFromDatabase() {
892
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
893
+ where: {
894
+ scheduledAt: {
895
+ $gte: /* @__PURE__ */ new Date()
896
+ },
897
+ releasedAt: null
898
+ }
899
+ });
900
+ for (const release2 of releases) {
901
+ this.set(release2.id, release2.scheduledAt);
902
+ }
903
+ return scheduledJobs;
904
+ }
905
+ };
906
+ };
729
907
  const services = {
730
908
  release: createReleaseService,
731
- "release-validation": createReleaseValidationService
909
+ "release-validation": createReleaseValidationService,
910
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
732
911
  };
733
912
  const RELEASE_SCHEMA = yup__namespace.object().shape({
734
- name: yup__namespace.string().trim().required()
913
+ name: yup__namespace.string().trim().required(),
914
+ scheduledAt: yup__namespace.string().nullable(),
915
+ isScheduled: yup__namespace.boolean().optional(),
916
+ time: yup__namespace.string().when("isScheduled", {
917
+ is: true,
918
+ then: yup__namespace.string().trim().required(),
919
+ otherwise: yup__namespace.string().nullable()
920
+ }),
921
+ timezone: yup__namespace.string().when("isScheduled", {
922
+ is: true,
923
+ then: yup__namespace.string().required().nullable(),
924
+ otherwise: yup__namespace.string().nullable()
925
+ }),
926
+ date: yup__namespace.string().when("isScheduled", {
927
+ is: true,
928
+ then: yup__namespace.string().required().nullable(),
929
+ otherwise: yup__namespace.string().nullable()
930
+ })
735
931
  }).required().noUnknown();
736
932
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
737
933
  const releaseController = {
@@ -771,19 +967,18 @@ const releaseController = {
771
967
  const id = ctx.params.id;
772
968
  const releaseService = getService("release", { strapi });
773
969
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
774
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
775
- ability: ctx.state.userAbility,
776
- model: RELEASE_MODEL_UID
777
- });
778
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
970
+ if (!release2) {
971
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
972
+ }
779
973
  const count = await releaseService.countActions({
780
974
  filters: {
781
975
  release: id
782
976
  }
783
977
  });
784
- if (!release2) {
785
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
786
- }
978
+ const sanitizedRelease = {
979
+ ...release2,
980
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
981
+ };
787
982
  const data = {
788
983
  ...sanitizedRelease,
789
984
  actions: {
@@ -836,8 +1031,27 @@ const releaseController = {
836
1031
  const id = ctx.params.id;
837
1032
  const releaseService = getService("release", { strapi });
838
1033
  const release2 = await releaseService.publish(id, { user });
1034
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1035
+ releaseService.countActions({
1036
+ filters: {
1037
+ release: id,
1038
+ type: "publish"
1039
+ }
1040
+ }),
1041
+ releaseService.countActions({
1042
+ filters: {
1043
+ release: id,
1044
+ type: "unpublish"
1045
+ }
1046
+ })
1047
+ ]);
839
1048
  ctx.body = {
840
- data: release2
1049
+ data: release2,
1050
+ meta: {
1051
+ totalEntries: countPublishActions + countUnpublishActions,
1052
+ totalPublishedEntries: countPublishActions,
1053
+ totalUnpublishedEntries: countUnpublishActions
1054
+ }
841
1055
  };
842
1056
  }
843
1057
  };
@@ -1109,6 +1323,7 @@ const getPlugin = () => {
1109
1323
  return {
1110
1324
  register,
1111
1325
  bootstrap,
1326
+ destroy,
1112
1327
  contentTypes,
1113
1328
  services,
1114
1329
  controllers,