@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
@@ -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,415 +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
436
- }
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
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
458
  });
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);
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}`);
470
466
  }
471
- return acc;
472
- }, []);
473
- const contentTypeModelsMap = contentTypeUids.reduce(
474
- (acc, contentTypeUid) => {
475
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
467
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
468
+ ...query,
469
+ populate: {
470
+ entry: {
471
+ populate: "*"
472
+ }
473
+ },
474
+ filters: {
475
+ release: releaseId
476
+ }
477
+ });
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
+ }
476
487
  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;
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 {};
511
+ }
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 };
488
515
  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"]
499
- }
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
+ };
500
529
  }
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)
513
- }
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);
514
536
  }
515
- });
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
- {
537
+ return acc;
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, {
525
562
  populate: {
526
563
  actions: {
527
- populate: {
528
- entry: {
529
- fields: ["id"]
530
- }
531
- }
564
+ fields: ["id"]
532
565
  }
533
566
  }
567
+ });
568
+ if (!release2) {
569
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
534
570
  }
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 actions = {};
546
- for (const action of releaseWithPopulatedActionEntries.actions) {
547
- const contentTypeUid = action.contentType;
548
- if (!actions[contentTypeUid]) {
549
- actions[contentTypeUid] = {
550
- entriestoPublishIds: [],
551
- entriesToUnpublishIds: []
552
- };
571
+ if (release2.releasedAt) {
572
+ throw new errors.ValidationError("Release already published");
553
573
  }
554
- if (action.type === "publish") {
555
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
556
- } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
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
+ }
580
+ }
581
+ });
582
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
583
+ });
584
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
585
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
586
+ await schedulingService.cancel(release2.id);
558
587
  }
559
- }
560
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
561
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
562
- await strapi2.db.transaction(async () => {
563
- for (const contentTypeUid of Object.keys(actions)) {
564
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
565
- const { entriestoPublishIds, entriesToUnpublishIds } = actions[contentTypeUid];
566
- const entriesToPublish = await strapi2.entityService.findMany(
567
- contentTypeUid,
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,
568
596
  {
569
- filters: {
570
- id: {
571
- $in: entriestoPublishIds
597
+ populate: {
598
+ actions: {
599
+ populate: {
600
+ entry: {
601
+ fields: ["id"]
602
+ }
603
+ }
572
604
  }
573
- },
574
- populate
605
+ }
575
606
  }
576
607
  );
577
- const entriesToUnpublish = await strapi2.entityService.findMany(
578
- contentTypeUid,
579
- {
580
- filters: {
581
- id: {
582
- $in: entriesToUnpublishIds
608
+ if (!releaseWithPopulatedActionEntries) {
609
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
610
+ }
611
+ if (releaseWithPopulatedActionEntries.releasedAt) {
612
+ throw new errors.ValidationError("Release already published");
613
+ }
614
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
615
+ throw new errors.ValidationError("No entries to publish");
616
+ }
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
+ }
633
+ } else {
634
+ singleTypeActions.push({
635
+ uid: contentTypeUid,
636
+ action: action.type,
637
+ id: action.entry.id
638
+ });
639
+ }
640
+ }
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);
583
652
  }
584
- },
585
- 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
+ }
586
659
  }
587
- );
588
- if (entriesToPublish.length > 0) {
589
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
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
672
+ }
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
+ }
691
+ }
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
+ });
590
713
  }
591
- if (entriesToUnpublish.length > 0) {
592
- 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
+ });
593
722
  }
723
+ throw error;
594
724
  }
595
- });
596
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
597
- data: {
598
- /*
599
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
600
- */
601
- // @ts-expect-error see above
602
- releasedAt: /* @__PURE__ */ new Date()
603
- }
604
- });
605
- return release2;
606
- },
607
- async updateAction(actionId, releaseId, update) {
608
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
609
- where: {
610
- id: actionId,
611
- release: {
612
- id: releaseId,
613
- releasedAt: {
614
- $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
+ }
615
735
  }
616
- }
617
- },
618
- data: update
619
- });
620
- if (!updatedAction) {
621
- throw new errors.NotFoundError(
622
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
623
- );
624
- }
625
- return updatedAction;
626
- },
627
- async deleteAction(actionId, releaseId) {
628
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
629
- where: {
630
- id: actionId,
631
- release: {
632
- id: releaseId,
633
- releasedAt: {
634
- $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
+ }
635
755
  }
636
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
+ );
637
762
  }
638
- });
639
- if (!deletedAction) {
640
- throw new errors.NotFoundError(
641
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
642
- );
763
+ return deletedAction;
643
764
  }
644
- return deletedAction;
645
- }
646
- });
765
+ };
766
+ };
647
767
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
648
768
  async validateUniqueEntry(releaseId, releaseActionArgs) {
649
769
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -688,27 +808,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
808
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
809
  }
690
810
  },
691
- async validateUniqueNameForPendingRelease(name) {
811
+ async validateUniqueNameForPendingRelease(name, id) {
692
812
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
813
  filters: {
694
814
  releasedAt: {
695
815
  $null: true
696
816
  },
697
- name
817
+ name,
818
+ ...id && { id: { $ne: id } }
698
819
  }
699
820
  });
700
821
  const isNameUnique = pendingReleases.length === 0;
701
822
  if (!isNameUnique) {
702
823
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
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
+ }
704
830
  }
705
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
+ };
706
884
  const services = {
707
885
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
886
+ "release-validation": createReleaseValidationService,
887
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
709
888
  };
710
889
  const RELEASE_SCHEMA = yup.object().shape({
711
- 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
+ })
712
908
  }).required().noUnknown();
713
909
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
910
  const releaseController = {
@@ -748,19 +944,18 @@ const releaseController = {
748
944
  const id = ctx.params.id;
749
945
  const releaseService = getService("release", { strapi });
750
946
  const release2 = await releaseService.findOne(id, { populate: ["createdBy"] });
751
- const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
752
- ability: ctx.state.userAbility,
753
- model: RELEASE_MODEL_UID
754
- });
755
- const sanitizedRelease = await permissionsManager.sanitizeOutput(release2);
947
+ if (!release2) {
948
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
949
+ }
756
950
  const count = await releaseService.countActions({
757
951
  filters: {
758
952
  release: id
759
953
  }
760
954
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
955
+ const sanitizedRelease = {
956
+ ...release2,
957
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
958
+ };
764
959
  const data = {
765
960
  ...sanitizedRelease,
766
961
  actions: {
@@ -813,8 +1008,27 @@ const releaseController = {
813
1008
  const id = ctx.params.id;
814
1009
  const releaseService = getService("release", { strapi });
815
1010
  const release2 = await releaseService.publish(id, { user });
1011
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1012
+ releaseService.countActions({
1013
+ filters: {
1014
+ release: id,
1015
+ type: "publish"
1016
+ }
1017
+ }),
1018
+ releaseService.countActions({
1019
+ filters: {
1020
+ release: id,
1021
+ type: "unpublish"
1022
+ }
1023
+ })
1024
+ ]);
816
1025
  ctx.body = {
817
- data: release2
1026
+ data: release2,
1027
+ meta: {
1028
+ totalEntries: countPublishActions + countUnpublishActions,
1029
+ totalPublishedEntries: countPublishActions,
1030
+ totalUnpublishedEntries: countUnpublishActions
1031
+ }
818
1032
  };
819
1033
  }
820
1034
  };
@@ -1086,6 +1300,7 @@ const getPlugin = () => {
1086
1300
  return {
1087
1301
  register,
1088
1302
  bootstrap,
1303
+ destroy,
1089
1304
  contentTypes,
1090
1305
  services,
1091
1306
  controllers,