@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
@@ -2,6 +2,7 @@ import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, val
2
2
  import { difference, keys } from "lodash";
3
3
  import _ from "lodash/fp";
4
4
  import EE from "@strapi/strapi/dist/utils/ee";
5
+ import { scheduleJob } from "node-schedule";
5
6
  import * as yup from "yup";
6
7
  const RELEASE_MODEL_UID = "plugin::content-releases.release";
7
8
  const RELEASE_ACTION_MODEL_UID = "plugin::content-releases.release-action";
@@ -49,6 +50,9 @@ const ACTIONS = [
49
50
  pluginName: "content-releases"
50
51
  }
51
52
  ];
53
+ const ALLOWED_WEBHOOK_EVENTS = {
54
+ RELEASES_PUBLISH: "releases.publish"
55
+ };
52
56
  async function deleteActionsOnDisableDraftAndPublish({
53
57
  oldContentTypes,
54
58
  contentTypes: contentTypes2
@@ -83,6 +87,9 @@ const register = async ({ strapi: strapi2 }) => {
83
87
  strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
84
88
  }
85
89
  };
90
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
91
+ return strapi2.plugin("content-releases").service(name);
92
+ };
86
93
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
87
94
  const bootstrap = async ({ strapi: strapi2 }) => {
88
95
  if (features$1.isEnabled("cms-content-releases")) {
@@ -130,6 +137,27 @@ const bootstrap = async ({ strapi: strapi2 }) => {
130
137
  }
131
138
  }
132
139
  });
140
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
141
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
142
+ strapi2.log.error(
143
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
144
+ );
145
+ throw err;
146
+ });
147
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
148
+ strapi2.webhookStore.addAllowedEvent(key, value);
149
+ });
150
+ }
151
+ }
152
+ };
153
+ const destroy = async ({ strapi: strapi2 }) => {
154
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
155
+ const scheduledJobs = getService("scheduling", {
156
+ strapi: strapi2
157
+ }).getAll();
158
+ for (const [, job] of scheduledJobs) {
159
+ job.cancel();
160
+ }
133
161
  }
134
162
  };
135
163
  const schema$1 = {
@@ -158,6 +186,12 @@ const schema$1 = {
158
186
  releasedAt: {
159
187
  type: "datetime"
160
188
  },
189
+ scheduledAt: {
190
+ type: "datetime"
191
+ },
192
+ timezone: {
193
+ type: "string"
194
+ },
161
195
  actions: {
162
196
  type: "relation",
163
197
  relation: "oneToMany",
@@ -220,9 +254,6 @@ const contentTypes = {
220
254
  release: release$1,
221
255
  "release-action": releaseAction$1
222
256
  };
223
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
- return strapi2.plugin("content-releases").service(name);
225
- };
226
257
  const getGroupName = (queryValue) => {
227
258
  switch (queryValue) {
228
259
  case "contentType":
@@ -235,441 +266,504 @@ const getGroupName = (queryValue) => {
235
266
  return "contentType.displayName";
236
267
  }
237
268
  };
238
- const createReleaseService = ({ strapi: strapi2 }) => ({
239
- async create(releaseData, { user }) {
240
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
241
- const { validatePendingReleasesLimit, validateUniqueNameForPendingRelease } = getService(
242
- "release-validation",
243
- { strapi: strapi2 }
244
- );
245
- await Promise.all([
246
- validatePendingReleasesLimit(),
247
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name)
248
- ]);
249
- return strapi2.entityService.create(RELEASE_MODEL_UID, {
250
- data: releaseWithCreatorFields
269
+ const createReleaseService = ({ strapi: strapi2 }) => {
270
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
271
+ strapi2.eventHub.emit(event, {
272
+ isPublished,
273
+ error,
274
+ release: release2
251
275
  });
252
- },
253
- async findOne(id, query = {}) {
254
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
255
- ...query
256
- });
257
- return release2;
258
- },
259
- findPage(query) {
260
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
261
- ...query,
262
- populate: {
263
- actions: {
264
- // @ts-expect-error Ignore missing properties
265
- count: true
266
- }
276
+ };
277
+ return {
278
+ async create(releaseData, { user }) {
279
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
280
+ const {
281
+ validatePendingReleasesLimit,
282
+ validateUniqueNameForPendingRelease,
283
+ validateScheduledAtIsLaterThanNow
284
+ } = getService("release-validation", { strapi: strapi2 });
285
+ await Promise.all([
286
+ validatePendingReleasesLimit(),
287
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
288
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
289
+ ]);
290
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
291
+ data: releaseWithCreatorFields
292
+ });
293
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
294
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
295
+ await schedulingService.set(release2.id, release2.scheduledAt);
267
296
  }
268
- });
269
- },
270
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
271
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
272
- where: {
273
- actions: {
274
- target_type: contentTypeUid,
275
- target_id: entryId
276
- },
277
- releasedAt: {
278
- $null: true
297
+ strapi2.telemetry.send("didCreateContentRelease");
298
+ return release2;
299
+ },
300
+ async findOne(id, query = {}) {
301
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
302
+ ...query
303
+ });
304
+ return release2;
305
+ },
306
+ findPage(query) {
307
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
308
+ ...query,
309
+ populate: {
310
+ actions: {
311
+ // @ts-expect-error Ignore missing properties
312
+ count: true
313
+ }
279
314
  }
280
- },
281
- populate: {
282
- // Filter the action to get only the content type entry
283
- actions: {
284
- where: {
315
+ });
316
+ },
317
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
318
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
319
+ where: {
320
+ actions: {
285
321
  target_type: contentTypeUid,
286
322
  target_id: entryId
323
+ },
324
+ releasedAt: {
325
+ $null: true
287
326
  }
288
- }
289
- }
290
- });
291
- return releases.map((release2) => {
292
- if (release2.actions?.length) {
293
- const [actionForEntry] = release2.actions;
294
- delete release2.actions;
295
- return {
296
- ...release2,
297
- action: actionForEntry
298
- };
299
- }
300
- return release2;
301
- });
302
- },
303
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
304
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
- where: {
306
- releasedAt: {
307
- $null: true
308
327
  },
309
- actions: {
310
- target_type: contentTypeUid,
311
- target_id: entryId
312
- }
313
- }
314
- });
315
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
316
- where: {
317
- $or: [
318
- {
319
- id: {
320
- $notIn: releasesRelated.map((release2) => release2.id)
328
+ populate: {
329
+ // Filter the action to get only the content type entry
330
+ actions: {
331
+ where: {
332
+ target_type: contentTypeUid,
333
+ target_id: entryId
321
334
  }
335
+ }
336
+ }
337
+ });
338
+ return releases.map((release2) => {
339
+ if (release2.actions?.length) {
340
+ const [actionForEntry] = release2.actions;
341
+ delete release2.actions;
342
+ return {
343
+ ...release2,
344
+ action: actionForEntry
345
+ };
346
+ }
347
+ return release2;
348
+ });
349
+ },
350
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
351
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
352
+ where: {
353
+ releasedAt: {
354
+ $null: true
322
355
  },
323
- {
324
- actions: null
356
+ actions: {
357
+ target_type: contentTypeUid,
358
+ target_id: entryId
359
+ }
360
+ }
361
+ });
362
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
363
+ where: {
364
+ $or: [
365
+ {
366
+ id: {
367
+ $notIn: releasesRelated.map((release2) => release2.id)
368
+ }
369
+ },
370
+ {
371
+ actions: null
372
+ }
373
+ ],
374
+ releasedAt: {
375
+ $null: true
325
376
  }
326
- ],
327
- releasedAt: {
328
- $null: true
329
377
  }
378
+ });
379
+ return releases.map((release2) => {
380
+ if (release2.actions?.length) {
381
+ const [actionForEntry] = release2.actions;
382
+ delete release2.actions;
383
+ return {
384
+ ...release2,
385
+ action: actionForEntry
386
+ };
387
+ }
388
+ return release2;
389
+ });
390
+ },
391
+ async update(id, releaseData, { user }) {
392
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
393
+ releaseData
394
+ );
395
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
396
+ "release-validation",
397
+ { strapi: strapi2 }
398
+ );
399
+ await Promise.all([
400
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
401
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
402
+ ]);
403
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
404
+ if (!release2) {
405
+ throw new errors.NotFoundError(`No release found for id ${id}`);
330
406
  }
331
- });
332
- return releases.map((release2) => {
333
- if (release2.actions?.length) {
334
- const [actionForEntry] = release2.actions;
335
- delete release2.actions;
336
- return {
337
- ...release2,
338
- action: actionForEntry
339
- };
407
+ if (release2.releasedAt) {
408
+ throw new errors.ValidationError("Release already published");
340
409
  }
341
- return release2;
342
- });
343
- },
344
- async update(id, releaseData, { user }) {
345
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
346
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
347
- if (!release2) {
348
- throw new errors.NotFoundError(`No release found for id ${id}`);
349
- }
350
- if (release2.releasedAt) {
351
- throw new errors.ValidationError("Release already published");
352
- }
353
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
354
- /*
355
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
356
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
357
- */
358
- // @ts-expect-error see above
359
- data: releaseWithCreatorFields
360
- });
361
- return updatedRelease;
362
- },
363
- async createAction(releaseId, action) {
364
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
365
- strapi: strapi2
366
- });
367
- await Promise.all([
368
- validateEntryContentType(action.entry.contentType),
369
- validateUniqueEntry(releaseId, action)
370
- ]);
371
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
372
- if (!release2) {
373
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
374
- }
375
- if (release2.releasedAt) {
376
- throw new errors.ValidationError("Release already published");
377
- }
378
- const { entry, type } = action;
379
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
380
- data: {
381
- type,
382
- contentType: entry.contentType,
383
- locale: entry.locale,
384
- entry: {
385
- id: entry.id,
386
- __type: entry.contentType,
387
- __pivot: { field: "entry" }
388
- },
389
- release: releaseId
390
- },
391
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
392
- });
393
- },
394
- async findActions(releaseId, query) {
395
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
396
- fields: ["id"]
397
- });
398
- if (!release2) {
399
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
400
- }
401
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
402
- ...query,
403
- populate: {
404
- entry: {
405
- populate: "*"
410
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
411
+ /*
412
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
413
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
414
+ */
415
+ // @ts-expect-error see above
416
+ data: releaseWithCreatorFields
417
+ });
418
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
419
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
420
+ if (releaseData.scheduledAt) {
421
+ await schedulingService.set(id, releaseData.scheduledAt);
422
+ } else if (release2.scheduledAt) {
423
+ schedulingService.cancel(id);
406
424
  }
407
- },
408
- filters: {
409
- release: releaseId
410
425
  }
411
- });
412
- },
413
- async countActions(query) {
414
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
415
- },
416
- async groupActions(actions, groupBy) {
417
- const contentTypeUids = actions.reduce((acc, action) => {
418
- if (!acc.includes(action.contentType)) {
419
- acc.push(action.contentType);
426
+ strapi2.telemetry.send("didUpdateContentRelease");
427
+ return updatedRelease;
428
+ },
429
+ async createAction(releaseId, action) {
430
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
431
+ strapi: strapi2
432
+ });
433
+ await Promise.all([
434
+ validateEntryContentType(action.entry.contentType),
435
+ validateUniqueEntry(releaseId, action)
436
+ ]);
437
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
438
+ if (!release2) {
439
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
420
440
  }
421
- return acc;
422
- }, []);
423
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
424
- contentTypeUids
425
- );
426
- const allLocalesDictionary = await this.getLocalesDataForActions();
427
- const formattedData = actions.map((action) => {
428
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
429
- return {
430
- ...action,
431
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
432
- contentType: {
433
- displayName,
434
- mainFieldValue: action.entry[mainField],
435
- uid: action.contentType
441
+ if (release2.releasedAt) {
442
+ throw new errors.ValidationError("Release already published");
443
+ }
444
+ const { entry, type } = action;
445
+ return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
446
+ data: {
447
+ type,
448
+ contentType: entry.contentType,
449
+ locale: entry.locale,
450
+ entry: {
451
+ id: entry.id,
452
+ __type: entry.contentType,
453
+ __pivot: { field: "entry" }
454
+ },
455
+ release: releaseId
456
+ },
457
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
+ });
459
+ },
460
+ async findActions(releaseId, query) {
461
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
462
+ fields: ["id"]
463
+ });
464
+ if (!release2) {
465
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
466
+ }
467
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
468
+ ...query,
469
+ populate: {
470
+ entry: {
471
+ populate: "*"
472
+ }
473
+ },
474
+ filters: {
475
+ release: releaseId
436
476
  }
437
- };
438
- });
439
- const groupName = getGroupName(groupBy);
440
- return _.groupBy(groupName)(formattedData);
441
- },
442
- async getLocalesDataForActions() {
443
- if (!strapi2.plugin("i18n")) {
444
- return {};
445
- }
446
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
447
- return allLocales.reduce((acc, locale) => {
448
- acc[locale.code] = { name: locale.name, code: locale.code };
449
- return acc;
450
- }, {});
451
- },
452
- async getContentTypesDataForActions(contentTypesUids) {
453
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
454
- const contentTypesData = {};
455
- for (const contentTypeUid of contentTypesUids) {
456
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
457
- uid: contentTypeUid
458
477
  });
459
- contentTypesData[contentTypeUid] = {
460
- mainField: contentTypeConfig.settings.mainField,
461
- displayName: strapi2.getModel(contentTypeUid).info.displayName
462
- };
463
- }
464
- return contentTypesData;
465
- },
466
- getContentTypeModelsFromActions(actions) {
467
- const contentTypeUids = actions.reduce((acc, action) => {
468
- if (!acc.includes(action.contentType)) {
469
- acc.push(action.contentType);
478
+ },
479
+ async countActions(query) {
480
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
481
+ },
482
+ async groupActions(actions, groupBy) {
483
+ const contentTypeUids = actions.reduce((acc, action) => {
484
+ if (!acc.includes(action.contentType)) {
485
+ acc.push(action.contentType);
486
+ }
487
+ return acc;
488
+ }, []);
489
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
490
+ contentTypeUids
491
+ );
492
+ const allLocalesDictionary = await this.getLocalesDataForActions();
493
+ const formattedData = actions.map((action) => {
494
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
495
+ return {
496
+ ...action,
497
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
498
+ contentType: {
499
+ displayName,
500
+ mainFieldValue: action.entry[mainField],
501
+ uid: action.contentType
502
+ }
503
+ };
504
+ });
505
+ const groupName = getGroupName(groupBy);
506
+ return _.groupBy(groupName)(formattedData);
507
+ },
508
+ async getLocalesDataForActions() {
509
+ if (!strapi2.plugin("i18n")) {
510
+ return {};
470
511
  }
471
- return acc;
472
- }, []);
473
- const contentTypeModelsMap = contentTypeUids.reduce(
474
- (acc, contentTypeUid) => {
475
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
512
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
513
+ return allLocales.reduce((acc, locale) => {
514
+ acc[locale.code] = { name: locale.name, code: locale.code };
476
515
  return acc;
477
- },
478
- {}
479
- );
480
- return contentTypeModelsMap;
481
- },
482
- async getAllComponents() {
483
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
484
- const components = await contentManagerComponentsService.findAllComponents();
485
- const componentsMap = components.reduce(
486
- (acc, component) => {
487
- acc[component.uid] = component;
516
+ }, {});
517
+ },
518
+ async getContentTypesDataForActions(contentTypesUids) {
519
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
520
+ const contentTypesData = {};
521
+ for (const contentTypeUid of contentTypesUids) {
522
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
523
+ uid: contentTypeUid
524
+ });
525
+ contentTypesData[contentTypeUid] = {
526
+ mainField: contentTypeConfig.settings.mainField,
527
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
528
+ };
529
+ }
530
+ return contentTypesData;
531
+ },
532
+ getContentTypeModelsFromActions(actions) {
533
+ const contentTypeUids = actions.reduce((acc, action) => {
534
+ if (!acc.includes(action.contentType)) {
535
+ acc.push(action.contentType);
536
+ }
488
537
  return acc;
489
- },
490
- {}
491
- );
492
- return componentsMap;
493
- },
494
- async delete(releaseId) {
495
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
496
- populate: {
497
- actions: {
498
- fields: ["id"]
538
+ }, []);
539
+ const contentTypeModelsMap = contentTypeUids.reduce(
540
+ (acc, contentTypeUid) => {
541
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
542
+ return acc;
543
+ },
544
+ {}
545
+ );
546
+ return contentTypeModelsMap;
547
+ },
548
+ async getAllComponents() {
549
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
550
+ const components = await contentManagerComponentsService.findAllComponents();
551
+ const componentsMap = components.reduce(
552
+ (acc, component) => {
553
+ acc[component.uid] = component;
554
+ return acc;
555
+ },
556
+ {}
557
+ );
558
+ return componentsMap;
559
+ },
560
+ async delete(releaseId) {
561
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
562
+ populate: {
563
+ actions: {
564
+ fields: ["id"]
565
+ }
499
566
  }
567
+ });
568
+ if (!release2) {
569
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
500
570
  }
501
- });
502
- if (!release2) {
503
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
504
- }
505
- if (release2.releasedAt) {
506
- throw new errors.ValidationError("Release already published");
507
- }
508
- await strapi2.db.transaction(async () => {
509
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
510
- where: {
511
- id: {
512
- $in: release2.actions.map((action) => action.id)
571
+ if (release2.releasedAt) {
572
+ throw new errors.ValidationError("Release already published");
573
+ }
574
+ await strapi2.db.transaction(async () => {
575
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
576
+ where: {
577
+ id: {
578
+ $in: release2.actions.map((action) => action.id)
579
+ }
513
580
  }
514
- }
581
+ });
582
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
515
583
  });
516
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
517
- });
518
- return release2;
519
- },
520
- async publish(releaseId) {
521
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
522
- RELEASE_MODEL_UID,
523
- releaseId,
524
- {
525
- populate: {
526
- actions: {
584
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
585
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
586
+ await schedulingService.cancel(release2.id);
587
+ }
588
+ strapi2.telemetry.send("didDeleteContentRelease");
589
+ return release2;
590
+ },
591
+ async publish(releaseId) {
592
+ try {
593
+ const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
594
+ RELEASE_MODEL_UID,
595
+ releaseId,
596
+ {
527
597
  populate: {
528
- entry: {
529
- fields: ["id"]
598
+ actions: {
599
+ populate: {
600
+ entry: {
601
+ fields: ["id"]
602
+ }
603
+ }
530
604
  }
531
605
  }
532
606
  }
607
+ );
608
+ if (!releaseWithPopulatedActionEntries) {
609
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
533
610
  }
534
- }
535
- );
536
- if (!releaseWithPopulatedActionEntries) {
537
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
538
- }
539
- if (releaseWithPopulatedActionEntries.releasedAt) {
540
- throw new errors.ValidationError("Release already published");
541
- }
542
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
543
- throw new errors.ValidationError("No entries to publish");
544
- }
545
- const collectionTypeActions = {};
546
- const singleTypeActions = [];
547
- for (const action of releaseWithPopulatedActionEntries.actions) {
548
- const contentTypeUid = action.contentType;
549
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
550
- if (!collectionTypeActions[contentTypeUid]) {
551
- collectionTypeActions[contentTypeUid] = {
552
- entriestoPublishIds: [],
553
- entriesToUnpublishIds: []
554
- };
611
+ if (releaseWithPopulatedActionEntries.releasedAt) {
612
+ throw new errors.ValidationError("Release already published");
555
613
  }
556
- if (action.type === "publish") {
557
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
558
- } else {
559
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
614
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
+ throw new errors.ValidationError("No entries to publish");
560
616
  }
561
- } else {
562
- singleTypeActions.push({
563
- uid: contentTypeUid,
564
- action: action.type,
565
- id: action.entry.id
566
- });
567
- }
568
- }
569
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
570
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
571
- await strapi2.db.transaction(async () => {
572
- for (const { uid, action, id } of singleTypeActions) {
573
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
574
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
575
- try {
576
- if (action === "publish") {
577
- await entityManagerService.publish(entry, uid);
617
+ const collectionTypeActions = {};
618
+ const singleTypeActions = [];
619
+ for (const action of releaseWithPopulatedActionEntries.actions) {
620
+ const contentTypeUid = action.contentType;
621
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
622
+ if (!collectionTypeActions[contentTypeUid]) {
623
+ collectionTypeActions[contentTypeUid] = {
624
+ entriestoPublishIds: [],
625
+ entriesToUnpublishIds: []
626
+ };
627
+ }
628
+ if (action.type === "publish") {
629
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
630
+ } else {
631
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
632
+ }
578
633
  } else {
579
- await entityManagerService.unpublish(entry, uid);
580
- }
581
- } catch (error) {
582
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
583
- ;
584
- else {
585
- throw error;
634
+ singleTypeActions.push({
635
+ uid: contentTypeUid,
636
+ action: action.type,
637
+ id: action.entry.id
638
+ });
586
639
  }
587
640
  }
588
- }
589
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
590
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
591
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
592
- const entriesToPublish = await strapi2.entityService.findMany(
593
- contentTypeUid,
594
- {
595
- filters: {
596
- id: {
597
- $in: entriestoPublishIds
641
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
642
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
643
+ await strapi2.db.transaction(async () => {
644
+ for (const { uid, action, id } of singleTypeActions) {
645
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
646
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
647
+ try {
648
+ if (action === "publish") {
649
+ await entityManagerService.publish(entry, uid);
650
+ } else {
651
+ await entityManagerService.unpublish(entry, uid);
598
652
  }
599
- },
600
- populate
653
+ } catch (error) {
654
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
655
+ } else {
656
+ throw error;
657
+ }
658
+ }
601
659
  }
602
- );
603
- const entriesToUnpublish = await strapi2.entityService.findMany(
604
- contentTypeUid,
605
- {
606
- filters: {
607
- id: {
608
- $in: entriesToUnpublishIds
660
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
661
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
662
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
663
+ const entriesToPublish = await strapi2.entityService.findMany(
664
+ contentTypeUid,
665
+ {
666
+ filters: {
667
+ id: {
668
+ $in: entriestoPublishIds
669
+ }
670
+ },
671
+ populate
609
672
  }
610
- },
611
- populate
673
+ );
674
+ const entriesToUnpublish = await strapi2.entityService.findMany(
675
+ contentTypeUid,
676
+ {
677
+ filters: {
678
+ id: {
679
+ $in: entriesToUnpublishIds
680
+ }
681
+ },
682
+ populate
683
+ }
684
+ );
685
+ if (entriesToPublish.length > 0) {
686
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
687
+ }
688
+ if (entriesToUnpublish.length > 0) {
689
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
690
+ }
612
691
  }
613
- );
614
- if (entriesToPublish.length > 0) {
615
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
692
+ });
693
+ const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
694
+ data: {
695
+ /*
696
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
697
+ */
698
+ // @ts-expect-error see above
699
+ releasedAt: /* @__PURE__ */ new Date()
700
+ },
701
+ populate: {
702
+ actions: {
703
+ // @ts-expect-error is not expecting count but it is working
704
+ count: true
705
+ }
706
+ }
707
+ });
708
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
709
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
710
+ isPublished: true,
711
+ release: release2
712
+ });
616
713
  }
617
- if (entriesToUnpublish.length > 0) {
618
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
714
+ strapi2.telemetry.send("didPublishContentRelease");
715
+ return release2;
716
+ } catch (error) {
717
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
718
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
719
+ isPublished: false,
720
+ error
721
+ });
619
722
  }
723
+ throw error;
620
724
  }
621
- });
622
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
623
- data: {
624
- /*
625
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
626
- */
627
- // @ts-expect-error see above
628
- releasedAt: /* @__PURE__ */ new Date()
629
- }
630
- });
631
- return release2;
632
- },
633
- async updateAction(actionId, releaseId, update) {
634
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
635
- where: {
636
- id: actionId,
637
- release: {
638
- id: releaseId,
639
- releasedAt: {
640
- $null: true
725
+ },
726
+ async updateAction(actionId, releaseId, update) {
727
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
728
+ where: {
729
+ id: actionId,
730
+ release: {
731
+ id: releaseId,
732
+ releasedAt: {
733
+ $null: true
734
+ }
641
735
  }
642
- }
643
- },
644
- data: update
645
- });
646
- if (!updatedAction) {
647
- throw new errors.NotFoundError(
648
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
649
- );
650
- }
651
- return updatedAction;
652
- },
653
- async deleteAction(actionId, releaseId) {
654
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
655
- where: {
656
- id: actionId,
657
- release: {
658
- id: releaseId,
659
- releasedAt: {
660
- $null: true
736
+ },
737
+ data: update
738
+ });
739
+ if (!updatedAction) {
740
+ throw new errors.NotFoundError(
741
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
742
+ );
743
+ }
744
+ return updatedAction;
745
+ },
746
+ async deleteAction(actionId, releaseId) {
747
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
748
+ where: {
749
+ id: actionId,
750
+ release: {
751
+ id: releaseId,
752
+ releasedAt: {
753
+ $null: true
754
+ }
661
755
  }
662
756
  }
757
+ });
758
+ if (!deletedAction) {
759
+ throw new errors.NotFoundError(
760
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
761
+ );
663
762
  }
664
- });
665
- if (!deletedAction) {
666
- throw new errors.NotFoundError(
667
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
668
- );
763
+ return deletedAction;
669
764
  }
670
- return deletedAction;
671
- }
672
- });
765
+ };
766
+ };
673
767
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
674
768
  async validateUniqueEntry(releaseId, releaseActionArgs) {
675
769
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -714,27 +808,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
714
808
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
715
809
  }
716
810
  },
717
- async validateUniqueNameForPendingRelease(name) {
811
+ async validateUniqueNameForPendingRelease(name, id) {
718
812
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
719
813
  filters: {
720
814
  releasedAt: {
721
815
  $null: true
722
816
  },
723
- name
817
+ name,
818
+ ...id && { id: { $ne: id } }
724
819
  }
725
820
  });
726
821
  const isNameUnique = pendingReleases.length === 0;
727
822
  if (!isNameUnique) {
728
823
  throw new errors.ValidationError(`Release with name ${name} already exists`);
729
824
  }
825
+ },
826
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
827
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
828
+ throw new errors.ValidationError("Scheduled at must be later than now");
829
+ }
730
830
  }
731
831
  });
832
+ const createSchedulingService = ({ strapi: strapi2 }) => {
833
+ const scheduledJobs = /* @__PURE__ */ new Map();
834
+ return {
835
+ async set(releaseId, scheduleDate) {
836
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
837
+ if (!release2) {
838
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
839
+ }
840
+ const job = scheduleJob(scheduleDate, async () => {
841
+ try {
842
+ await getService("release").publish(releaseId);
843
+ } catch (error) {
844
+ }
845
+ this.cancel(releaseId);
846
+ });
847
+ if (scheduledJobs.has(releaseId)) {
848
+ this.cancel(releaseId);
849
+ }
850
+ scheduledJobs.set(releaseId, job);
851
+ return scheduledJobs;
852
+ },
853
+ cancel(releaseId) {
854
+ if (scheduledJobs.has(releaseId)) {
855
+ scheduledJobs.get(releaseId).cancel();
856
+ scheduledJobs.delete(releaseId);
857
+ }
858
+ return scheduledJobs;
859
+ },
860
+ getAll() {
861
+ return scheduledJobs;
862
+ },
863
+ /**
864
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
865
+ * This is useful in case the server was restarted and the scheduled jobs were lost
866
+ * This also could be used to sync different Strapi instances in case of a cluster
867
+ */
868
+ async syncFromDatabase() {
869
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
870
+ where: {
871
+ scheduledAt: {
872
+ $gte: /* @__PURE__ */ new Date()
873
+ },
874
+ releasedAt: null
875
+ }
876
+ });
877
+ for (const release2 of releases) {
878
+ this.set(release2.id, release2.scheduledAt);
879
+ }
880
+ return scheduledJobs;
881
+ }
882
+ };
883
+ };
732
884
  const services = {
733
885
  release: createReleaseService,
734
- "release-validation": createReleaseValidationService
886
+ "release-validation": createReleaseValidationService,
887
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
735
888
  };
736
889
  const RELEASE_SCHEMA = yup.object().shape({
737
- name: yup.string().trim().required()
890
+ name: yup.string().trim().required(),
891
+ scheduledAt: yup.string().nullable(),
892
+ isScheduled: yup.boolean().optional(),
893
+ time: yup.string().when("isScheduled", {
894
+ is: true,
895
+ then: yup.string().trim().required(),
896
+ otherwise: yup.string().nullable()
897
+ }),
898
+ timezone: yup.string().when("isScheduled", {
899
+ is: true,
900
+ then: yup.string().required().nullable(),
901
+ otherwise: yup.string().nullable()
902
+ }),
903
+ date: yup.string().when("isScheduled", {
904
+ is: true,
905
+ then: yup.string().required().nullable(),
906
+ otherwise: yup.string().nullable()
907
+ })
738
908
  }).required().noUnknown();
739
909
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
740
910
  const releaseController = {
@@ -774,19 +944,18 @@ const releaseController = {
774
944
  const id = ctx.params.id;
775
945
  const releaseService = getService("release", { strapi });
776
946
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
777
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
778
- ability: ctx.state.userAbility,
779
- model: RELEASE_MODEL_UID
780
- });
781
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
947
+ if (!release2) {
948
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
949
+ }
782
950
  const count = await releaseService.countActions({
783
951
  filters: {
784
952
  release: id
785
953
  }
786
954
  });
787
- if (!release2) {
788
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
789
- }
955
+ const sanitizedRelease = {
956
+ ...release2,
957
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
958
+ };
790
959
  const data = {
791
960
  ...sanitizedRelease,
792
961
  actions: {
@@ -1131,6 +1300,7 @@ const getPlugin = () => {
1131
1300
  return {
1132
1301
  register,
1133
1302
  bootstrap,
1303
+ destroy,
1134
1304
  contentTypes,
1135
1305
  services,
1136
1306
  controllers,