@strapi/content-releases 0.0.0-next.44f19b3d2f81d983c343a219aa2781ee0deecb5f → 0.0.0-next.4af8963f6880c5fb9fae32ecd580f5cd33eaddda

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-3ycH2d3s.mjs → App-ise7GunC.mjs} +365 -154
  2. package/dist/_chunks/App-ise7GunC.mjs.map +1 -0
  3. package/dist/_chunks/{App-5PsAyVt2.js → App-w2Zq-wj5.js} +363 -151
  4. package/dist/_chunks/App-w2Zq-wj5.js.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +1 -0
  9. package/dist/_chunks/{en-2DuPv5k0.js → en-7P4i1cWH.js} +11 -3
  10. package/dist/_chunks/en-7P4i1cWH.js.map +1 -0
  11. package/dist/_chunks/{en-SOqjCdyh.mjs → en-pb1wUzhy.mjs} +11 -3
  12. package/dist/_chunks/en-pb1wUzhy.mjs.map +1 -0
  13. package/dist/_chunks/{index-4gUWuCQV.mjs → index-D-Yjf60c.mjs} +57 -16
  14. package/dist/_chunks/index-D-Yjf60c.mjs.map +1 -0
  15. package/dist/_chunks/{index-D57Rztnc.js → index-Q8Pv7enO.js} +57 -16
  16. package/dist/_chunks/index-Q8Pv7enO.js.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +1 -1
  19. package/dist/server/index.js +573 -403
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +573 -403
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +12 -9
  24. package/dist/_chunks/App-3ycH2d3s.mjs.map +0 -1
  25. package/dist/_chunks/App-5PsAyVt2.js.map +0 -1
  26. package/dist/_chunks/en-2DuPv5k0.js.map +0 -1
  27. package/dist/_chunks/en-SOqjCdyh.mjs.map +0 -1
  28. package/dist/_chunks/index-4gUWuCQV.mjs.map +0 -1
  29. package/dist/_chunks/index-D57Rztnc.js.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,441 +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
382
+ }
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
348
399
  }
349
- ],
350
- releasedAt: {
351
- $null: true
352
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
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
+ });
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}`);
489
+ }
490
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
491
+ ...query,
492
+ populate: {
493
+ entry: {
494
+ populate: "*"
495
+ }
496
+ },
497
+ filters: {
498
+ release: releaseId
459
499
  }
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
481
500
  });
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);
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
+ }
510
+ return acc;
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 {};
493
534
  }
494
- return acc;
495
- }, []);
496
- const contentTypeModelsMap = contentTypeUids.reduce(
497
- (acc, contentTypeUid) => {
498
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
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 };
499
538
  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;
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
+ };
552
+ }
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);
559
+ }
511
560
  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"]
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, {
585
+ populate: {
586
+ actions: {
587
+ fields: ["id"]
588
+ }
522
589
  }
590
+ });
591
+ if (!release2) {
592
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
523
593
  }
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)
594
+ if (release2.releasedAt) {
595
+ throw new utils.errors.ValidationError("Release already published");
596
+ }
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
+ }
536
603
  }
537
- }
604
+ });
605
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
538
606
  });
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
- {
548
- populate: {
549
- actions: {
607
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
608
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
609
+ await schedulingService.cancel(release2.id);
610
+ }
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,
619
+ {
550
620
  populate: {
551
- entry: {
552
- fields: ["id"]
621
+ actions: {
622
+ populate: {
623
+ entry: {
624
+ fields: ["id"]
625
+ }
626
+ }
553
627
  }
554
628
  }
555
629
  }
630
+ );
631
+ if (!releaseWithPopulatedActionEntries) {
632
+ throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
556
633
  }
557
- }
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 collectionTypeActions = {};
569
- const singleTypeActions = [];
570
- for (const action of releaseWithPopulatedActionEntries.actions) {
571
- const contentTypeUid = action.contentType;
572
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
573
- if (!collectionTypeActions[contentTypeUid]) {
574
- collectionTypeActions[contentTypeUid] = {
575
- entriestoPublishIds: [],
576
- entriesToUnpublishIds: []
577
- };
634
+ if (releaseWithPopulatedActionEntries.releasedAt) {
635
+ throw new utils.errors.ValidationError("Release already published");
578
636
  }
579
- if (action.type === "publish") {
580
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
581
- } else {
582
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
637
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
638
+ throw new utils.errors.ValidationError("No entries to publish");
583
639
  }
584
- } else {
585
- singleTypeActions.push({
586
- uid: contentTypeUid,
587
- action: action.type,
588
- id: action.entry.id
589
- });
590
- }
591
- }
592
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
593
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
594
- await strapi2.db.transaction(async () => {
595
- for (const { uid, action, id } of singleTypeActions) {
596
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
597
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
598
- try {
599
- if (action === "publish") {
600
- await entityManagerService.publish(entry, uid);
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
+ }
601
656
  } else {
602
- await entityManagerService.unpublish(entry, uid);
603
- }
604
- } catch (error) {
605
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
606
- ;
607
- else {
608
- throw error;
657
+ singleTypeActions.push({
658
+ uid: contentTypeUid,
659
+ action: action.type,
660
+ id: action.entry.id
661
+ });
609
662
  }
610
663
  }
611
- }
612
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
613
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
614
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
615
- const entriesToPublish = await strapi2.entityService.findMany(
616
- contentTypeUid,
617
- {
618
- filters: {
619
- id: {
620
- $in: entriestoPublishIds
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);
621
675
  }
622
- },
623
- 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
+ }
624
682
  }
625
- );
626
- const entriesToUnpublish = await strapi2.entityService.findMany(
627
- contentTypeUid,
628
- {
629
- filters: {
630
- id: {
631
- $in: entriesToUnpublishIds
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
632
695
  }
633
- },
634
- populate
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
+ }
635
714
  }
636
- );
637
- if (entriesToPublish.length > 0) {
638
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
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
+ });
639
736
  }
640
- if (entriesToUnpublish.length > 0) {
641
- 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
+ });
642
745
  }
746
+ throw error;
643
747
  }
644
- });
645
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
646
- data: {
647
- /*
648
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
649
- */
650
- // @ts-expect-error see above
651
- releasedAt: /* @__PURE__ */ new Date()
652
- }
653
- });
654
- return release2;
655
- },
656
- async updateAction(actionId, releaseId, update) {
657
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
658
- where: {
659
- id: actionId,
660
- release: {
661
- id: releaseId,
662
- releasedAt: {
663
- $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
+ }
664
758
  }
665
- }
666
- },
667
- data: update
668
- });
669
- if (!updatedAction) {
670
- throw new utils.errors.NotFoundError(
671
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
672
- );
673
- }
674
- return updatedAction;
675
- },
676
- async deleteAction(actionId, releaseId) {
677
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
678
- where: {
679
- id: actionId,
680
- release: {
681
- id: releaseId,
682
- releasedAt: {
683
- $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
+ }
684
778
  }
685
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
+ );
686
785
  }
687
- });
688
- if (!deletedAction) {
689
- throw new utils.errors.NotFoundError(
690
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
691
- );
786
+ return deletedAction;
692
787
  }
693
- return deletedAction;
694
- }
695
- });
788
+ };
789
+ };
696
790
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
697
791
  async validateUniqueEntry(releaseId, releaseActionArgs) {
698
792
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -737,27 +831,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
737
831
  throw new utils.errors.ValidationError("You have reached the maximum number of pending releases");
738
832
  }
739
833
  },
740
- async validateUniqueNameForPendingRelease(name) {
834
+ async validateUniqueNameForPendingRelease(name, id) {
741
835
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
742
836
  filters: {
743
837
  releasedAt: {
744
838
  $null: true
745
839
  },
746
- name
840
+ name,
841
+ ...id && { id: { $ne: id } }
747
842
  }
748
843
  });
749
844
  const isNameUnique = pendingReleases.length === 0;
750
845
  if (!isNameUnique) {
751
846
  throw new utils.errors.ValidationError(`Release with name ${name} already exists`);
752
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
+ }
753
853
  }
754
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
+ };
755
907
  const services = {
756
908
  release: createReleaseService,
757
- "release-validation": createReleaseValidationService
909
+ "release-validation": createReleaseValidationService,
910
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
758
911
  };
759
912
  const RELEASE_SCHEMA = yup__namespace.object().shape({
760
- 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
+ })
761
931
  }).required().noUnknown();
762
932
  const validateRelease = utils.validateYupSchema(RELEASE_SCHEMA);
763
933
  const releaseController = {
@@ -797,19 +967,18 @@ const releaseController = {
797
967
  const id = ctx.params.id;
798
968
  const releaseService = getService("release", { strapi });
799
969
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
800
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
801
- ability: ctx.state.userAbility,
802
- model: RELEASE_MODEL_UID
803
- });
804
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
970
+ if (!release2) {
971
+ throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
972
+ }
805
973
  const count = await releaseService.countActions({
806
974
  filters: {
807
975
  release: id
808
976
  }
809
977
  });
810
- if (!release2) {
811
- throw new utils.errors.NotFoundError(`Release not found for id: ${id}`);
812
- }
978
+ const sanitizedRelease = {
979
+ ...release2,
980
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
981
+ };
813
982
  const data = {
814
983
  ...sanitizedRelease,
815
984
  actions: {
@@ -1154,6 +1323,7 @@ const getPlugin = () => {
1154
1323
  return {
1155
1324
  register,
1156
1325
  bootstrap,
1326
+ destroy,
1157
1327
  contentTypes,
1158
1328
  services,
1159
1329
  controllers,