@strapi/content-releases 0.0.0-experimental.fb8e9fec2e10d6b55e3aee59c4eb76bb7a11432a → 0.0.0-experimental.fc1ac2acd58c8a5a858679956b6d102ac5ee4011

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 (105) hide show
  1. package/dist/_chunks/App-0Er6xxcq.mjs +1358 -0
  2. package/dist/_chunks/App-0Er6xxcq.mjs.map +1 -0
  3. package/dist/_chunks/App-C768ulk4.js +1381 -0
  4. package/dist/_chunks/App-C768ulk4.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-MyLPoISH.mjs → en-WuuhP6Bn.mjs} +21 -6
  10. package/dist/_chunks/en-WuuhP6Bn.mjs.map +1 -0
  11. package/dist/_chunks/{en-gYDqKYFd.js → en-gcJJ5htG.js} +21 -6
  12. package/dist/_chunks/en-gcJJ5htG.js.map +1 -0
  13. package/dist/_chunks/{index-KJa1Rb5F.js → index-BLSMpbpZ.js} +164 -44
  14. package/dist/_chunks/index-BLSMpbpZ.js.map +1 -0
  15. package/dist/_chunks/{index-c4zRX_sg.mjs → index-fJx1up7m.mjs} +178 -58
  16. package/dist/_chunks/index-fJx1up7m.mjs.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +2 -2
  19. package/dist/admin/src/components/CMReleasesContainer.d.ts +1 -0
  20. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  21. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  22. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  23. package/dist/admin/src/components/ReleaseModal.d.ts +16 -0
  24. package/dist/admin/src/constants.d.ts +58 -0
  25. package/dist/admin/src/index.d.ts +3 -0
  26. package/dist/admin/src/pages/App.d.ts +1 -0
  27. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  28. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  29. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  30. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  31. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  32. package/dist/admin/src/pluginId.d.ts +1 -0
  33. package/dist/admin/src/services/axios.d.ts +29 -0
  34. package/dist/admin/src/services/release.d.ts +369 -0
  35. package/dist/admin/src/store/hooks.d.ts +7 -0
  36. package/dist/admin/src/utils/time.d.ts +1 -0
  37. package/dist/server/index.js +889 -436
  38. package/dist/server/index.js.map +1 -1
  39. package/dist/server/index.mjs +889 -436
  40. package/dist/server/index.mjs.map +1 -1
  41. package/dist/server/src/bootstrap.d.ts +5 -0
  42. package/dist/server/src/bootstrap.d.ts.map +1 -0
  43. package/dist/server/src/constants.d.ts +12 -0
  44. package/dist/server/src/constants.d.ts.map +1 -0
  45. package/dist/server/src/content-types/index.d.ts +99 -0
  46. package/dist/server/src/content-types/index.d.ts.map +1 -0
  47. package/dist/server/src/content-types/release/index.d.ts +48 -0
  48. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  49. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  50. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  51. package/dist/server/src/content-types/release-action/index.d.ts +50 -0
  52. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  53. package/dist/server/src/content-types/release-action/schema.d.ts +49 -0
  54. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  55. package/dist/server/src/controllers/index.d.ts +18 -0
  56. package/dist/server/src/controllers/index.d.ts.map +1 -0
  57. package/dist/server/src/controllers/release-action.d.ts +9 -0
  58. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  59. package/dist/server/src/controllers/release.d.ts +11 -0
  60. package/dist/server/src/controllers/release.d.ts.map +1 -0
  61. package/dist/server/src/controllers/validation/release-action.d.ts +3 -0
  62. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  63. package/dist/server/src/controllers/validation/release.d.ts +2 -0
  64. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  65. package/dist/server/src/destroy.d.ts +5 -0
  66. package/dist/server/src/destroy.d.ts.map +1 -0
  67. package/dist/server/src/index.d.ts +3838 -0
  68. package/dist/server/src/index.d.ts.map +1 -0
  69. package/dist/server/src/migrations/index.d.ts +10 -0
  70. package/dist/server/src/migrations/index.d.ts.map +1 -0
  71. package/dist/server/src/register.d.ts +5 -0
  72. package/dist/server/src/register.d.ts.map +1 -0
  73. package/dist/server/src/routes/index.d.ts +35 -0
  74. package/dist/server/src/routes/index.d.ts.map +1 -0
  75. package/dist/server/src/routes/release-action.d.ts +18 -0
  76. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  77. package/dist/server/src/routes/release.d.ts +18 -0
  78. package/dist/server/src/routes/release.d.ts.map +1 -0
  79. package/dist/server/src/services/index.d.ts +3572 -0
  80. package/dist/server/src/services/index.d.ts.map +1 -0
  81. package/dist/server/src/services/release.d.ts +1812 -0
  82. package/dist/server/src/services/release.d.ts.map +1 -0
  83. package/dist/server/src/services/scheduling.d.ts +18 -0
  84. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  85. package/dist/server/src/services/validation.d.ts +14 -0
  86. package/dist/server/src/services/validation.d.ts.map +1 -0
  87. package/dist/server/src/utils/index.d.ts +18 -0
  88. package/dist/server/src/utils/index.d.ts.map +1 -0
  89. package/dist/shared/contracts/release-actions.d.ts +105 -0
  90. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  91. package/dist/shared/contracts/releases.d.ts +166 -0
  92. package/dist/shared/contracts/releases.d.ts.map +1 -0
  93. package/dist/shared/types.d.ts +24 -0
  94. package/dist/shared/types.d.ts.map +1 -0
  95. package/dist/shared/validation-schemas.d.ts +2 -0
  96. package/dist/shared/validation-schemas.d.ts.map +1 -0
  97. package/package.json +19 -23
  98. package/dist/_chunks/App-L1jSxCiL.mjs +0 -1015
  99. package/dist/_chunks/App-L1jSxCiL.mjs.map +0 -1
  100. package/dist/_chunks/App-_20W9dYa.js +0 -1037
  101. package/dist/_chunks/App-_20W9dYa.js.map +0 -1
  102. package/dist/_chunks/en-MyLPoISH.mjs.map +0 -1
  103. package/dist/_chunks/en-gYDqKYFd.js.map +0 -1
  104. package/dist/_chunks/index-KJa1Rb5F.js.map +0 -1
  105. package/dist/_chunks/index-c4zRX_sg.mjs.map +0 -1
@@ -1,7 +1,8 @@
1
- import { contentTypes as contentTypes$1, mapAsync, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
1
+ import { async, setCreatorFields, errors, validateYupSchema, yup as yup$1 } from "@strapi/utils";
2
+ import isEqual from "lodash/isEqual";
2
3
  import { difference, keys } from "lodash";
3
4
  import _ from "lodash/fp";
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,54 +50,178 @@ const ACTIONS = [
49
50
  pluginName: "content-releases"
50
51
  }
51
52
  ];
52
- async function deleteActionsOnDisableDraftAndPublish({
53
- oldContentTypes,
54
- contentTypes: contentTypes2
55
- }) {
56
- if (!oldContentTypes) {
57
- return;
58
- }
59
- for (const uid in contentTypes2) {
60
- if (!oldContentTypes[uid]) {
61
- continue;
62
- }
63
- const oldContentType = oldContentTypes[uid];
64
- const contentType = contentTypes2[uid];
65
- if (contentTypes$1.hasDraftAndPublish(oldContentType) && !contentTypes$1.hasDraftAndPublish(contentType)) {
66
- await strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: uid }).execute();
67
- }
53
+ const ALLOWED_WEBHOOK_EVENTS = {
54
+ RELEASES_PUBLISH: "releases.publish"
55
+ };
56
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
57
+ return strapi2.plugin("content-releases").service(name);
58
+ };
59
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
60
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
61
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
62
+ const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
63
+ return entry;
64
+ };
65
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
66
+ try {
67
+ await strapi2.entityValidator.validateEntityCreation(
68
+ strapi2.getModel(contentTypeUid),
69
+ entry,
70
+ void 0,
71
+ // @ts-expect-error - FIXME: entity here is unnecessary
72
+ entry
73
+ );
74
+ return true;
75
+ } catch {
76
+ return false;
68
77
  }
69
- }
78
+ };
70
79
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
71
80
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
72
81
  if (deletedContentTypes.length) {
73
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
82
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
74
83
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
75
84
  });
76
85
  }
77
86
  }
78
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
87
+ async function migrateIsValidAndStatusReleases() {
88
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
89
+ where: {
90
+ status: null,
91
+ releasedAt: null
92
+ },
93
+ populate: {
94
+ actions: {
95
+ populate: {
96
+ entry: true
97
+ }
98
+ }
99
+ }
100
+ });
101
+ async.map(releasesWithoutStatus, async (release2) => {
102
+ const actions = release2.actions;
103
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
104
+ for (const action of notValidatedActions) {
105
+ if (action.entry) {
106
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
107
+ strapi
108
+ });
109
+ if (populatedEntry) {
110
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
111
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
112
+ where: {
113
+ id: action.id
114
+ },
115
+ data: {
116
+ isEntryValid
117
+ }
118
+ });
119
+ }
120
+ }
121
+ }
122
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
123
+ });
124
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
125
+ where: {
126
+ status: null,
127
+ releasedAt: {
128
+ $notNull: true
129
+ }
130
+ }
131
+ });
132
+ async.map(publishedReleases, async (release2) => {
133
+ return strapi.db.query(RELEASE_MODEL_UID).update({
134
+ where: {
135
+ id: release2.id
136
+ },
137
+ data: {
138
+ status: "done"
139
+ }
140
+ });
141
+ });
142
+ }
143
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
144
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
145
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes);
146
+ const releasesAffected = /* @__PURE__ */ new Set();
147
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
148
+ const oldContentType = oldContentTypes[contentTypeUID];
149
+ const contentType = contentTypes2[contentTypeUID];
150
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
151
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
152
+ where: {
153
+ contentType: contentTypeUID
154
+ },
155
+ populate: {
156
+ entry: true,
157
+ release: true
158
+ }
159
+ });
160
+ await async.map(actions, async (action) => {
161
+ if (action.entry) {
162
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
163
+ strapi
164
+ });
165
+ if (populatedEntry) {
166
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
167
+ strapi
168
+ });
169
+ releasesAffected.add(action.release.id);
170
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
171
+ where: {
172
+ id: action.id
173
+ },
174
+ data: {
175
+ isEntryValid
176
+ }
177
+ });
178
+ }
179
+ }
180
+ });
181
+ }
182
+ }).then(() => {
183
+ async.map(releasesAffected, async (releaseId) => {
184
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
185
+ });
186
+ });
187
+ }
188
+ }
79
189
  const register = async ({ strapi: strapi2 }) => {
80
- if (features$2.isEnabled("cms-content-releases")) {
190
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
81
191
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
82
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
83
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
192
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
84
193
  }
85
194
  };
86
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
87
195
  const bootstrap = async ({ strapi: strapi2 }) => {
88
- if (features$1.isEnabled("cms-content-releases")) {
196
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
197
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes);
89
198
  strapi2.db.lifecycles.subscribe({
90
- afterDelete(event) {
91
- const { model, result } = event;
92
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
93
- const { id } = result;
94
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
95
- where: {
96
- target_type: model.uid,
97
- target_id: id
199
+ models: contentTypesWithDraftAndPublish,
200
+ async afterDelete(event) {
201
+ try {
202
+ const { model, result } = event;
203
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
204
+ const { id } = result;
205
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
206
+ where: {
207
+ actions: {
208
+ target_type: model.uid,
209
+ target_id: id
210
+ }
211
+ }
212
+ });
213
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
214
+ where: {
215
+ target_type: model.uid,
216
+ target_id: id
217
+ }
218
+ });
219
+ for (const release2 of releases) {
220
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
98
221
  }
99
- });
222
+ }
223
+ } catch (error) {
224
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
100
225
  }
101
226
  },
102
227
  /**
@@ -105,7 +230,7 @@ const bootstrap = async ({ strapi: strapi2 }) => {
105
230
  */
106
231
  async beforeDeleteMany(event) {
107
232
  const { model, params } = event;
108
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
233
+ if (model.kind === "collectionType") {
109
234
  const { where } = params;
110
235
  const entriesToDelete = await strapi2.db.query(model.uid).findMany({ select: ["id"], where });
111
236
  event.state.entriesToDelete = entriesToDelete;
@@ -116,20 +241,98 @@ const bootstrap = async ({ strapi: strapi2 }) => {
116
241
  * We make this only after deleteMany is succesfully executed to avoid errors
117
242
  */
118
243
  async afterDeleteMany(event) {
119
- const { model, state } = event;
120
- const entriesToDelete = state.entriesToDelete;
121
- if (entriesToDelete) {
122
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
123
- where: {
124
- target_type: model.uid,
125
- target_id: {
126
- $in: entriesToDelete.map((entry) => entry.id)
244
+ try {
245
+ const { model, state } = event;
246
+ const entriesToDelete = state.entriesToDelete;
247
+ if (entriesToDelete) {
248
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
249
+ where: {
250
+ actions: {
251
+ target_type: model.uid,
252
+ target_id: {
253
+ $in: entriesToDelete.map(
254
+ (entry) => entry.id
255
+ )
256
+ }
257
+ }
258
+ }
259
+ });
260
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
261
+ where: {
262
+ target_type: model.uid,
263
+ target_id: {
264
+ $in: entriesToDelete.map((entry) => entry.id)
265
+ }
127
266
  }
267
+ });
268
+ for (const release2 of releases) {
269
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
128
270
  }
271
+ }
272
+ } catch (error) {
273
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
274
+ error
129
275
  });
130
276
  }
277
+ },
278
+ async afterUpdate(event) {
279
+ try {
280
+ const { model, result } = event;
281
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
282
+ const isEntryValid = await getEntryValidStatus(
283
+ model.uid,
284
+ result,
285
+ {
286
+ strapi: strapi2
287
+ }
288
+ );
289
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
290
+ where: {
291
+ target_type: model.uid,
292
+ target_id: result.id
293
+ },
294
+ data: {
295
+ isEntryValid
296
+ }
297
+ });
298
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
299
+ where: {
300
+ actions: {
301
+ target_type: model.uid,
302
+ target_id: result.id
303
+ }
304
+ }
305
+ });
306
+ for (const release2 of releases) {
307
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
308
+ }
309
+ }
310
+ } catch (error) {
311
+ strapi2.log.error("Error while updating release actions after entry update", { error });
312
+ }
131
313
  }
132
314
  });
315
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
316
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
317
+ strapi2.log.error(
318
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
319
+ );
320
+ throw err;
321
+ });
322
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
323
+ strapi2.webhookStore.addAllowedEvent(key, value);
324
+ });
325
+ }
326
+ }
327
+ };
328
+ const destroy = async ({ strapi: strapi2 }) => {
329
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
330
+ const scheduledJobs = getService("scheduling", {
331
+ strapi: strapi2
332
+ }).getAll();
333
+ for (const [, job] of scheduledJobs) {
334
+ job.cancel();
335
+ }
133
336
  }
134
337
  };
135
338
  const schema$1 = {
@@ -158,6 +361,17 @@ const schema$1 = {
158
361
  releasedAt: {
159
362
  type: "datetime"
160
363
  },
364
+ scheduledAt: {
365
+ type: "datetime"
366
+ },
367
+ timezone: {
368
+ type: "string"
369
+ },
370
+ status: {
371
+ type: "enumeration",
372
+ enum: ["ready", "blocked", "failed", "done", "empty"],
373
+ required: true
374
+ },
161
375
  actions: {
162
376
  type: "relation",
163
377
  relation: "oneToMany",
@@ -210,6 +424,9 @@ const schema = {
210
424
  relation: "manyToOne",
211
425
  target: RELEASE_MODEL_UID,
212
426
  inversedBy: "actions"
427
+ },
428
+ isEntryValid: {
429
+ type: "boolean"
213
430
  }
214
431
  }
215
432
  };
@@ -220,9 +437,6 @@ const contentTypes = {
220
437
  release: release$1,
221
438
  "release-action": releaseAction$1
222
439
  };
223
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
- return strapi2.plugin("content-releases").service(name);
225
- };
226
440
  const getGroupName = (queryValue) => {
227
441
  switch (queryValue) {
228
442
  case "contentType":
@@ -235,415 +449,563 @@ const getGroupName = (queryValue) => {
235
449
  return "contentType.displayName";
236
450
  }
237
451
  };
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
452
+ const createReleaseService = ({ strapi: strapi2 }) => {
453
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
454
+ strapi2.eventHub.emit(event, {
455
+ isPublished,
456
+ error,
457
+ release: release2
251
458
  });
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
459
+ };
460
+ return {
461
+ async create(releaseData, { user }) {
462
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
463
+ const {
464
+ validatePendingReleasesLimit,
465
+ validateUniqueNameForPendingRelease,
466
+ validateScheduledAtIsLaterThanNow
467
+ } = getService("release-validation", { strapi: strapi2 });
468
+ await Promise.all([
469
+ validatePendingReleasesLimit(),
470
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
471
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
472
+ ]);
473
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
474
+ data: {
475
+ ...releaseWithCreatorFields,
476
+ status: "empty"
266
477
  }
478
+ });
479
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
480
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
481
+ await schedulingService.set(release2.id, release2.scheduledAt);
267
482
  }
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
483
+ strapi2.telemetry.send("didCreateContentRelease");
484
+ return release2;
485
+ },
486
+ async findOne(id, query = {}) {
487
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
488
+ ...query
489
+ });
490
+ return release2;
491
+ },
492
+ findPage(query) {
493
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
494
+ ...query,
495
+ populate: {
496
+ actions: {
497
+ // @ts-expect-error Ignore missing properties
498
+ count: true
499
+ }
279
500
  }
280
- },
281
- populate: {
282
- // Filter the action to get only the content type entry
283
- actions: {
284
- where: {
501
+ });
502
+ },
503
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
504
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
505
+ where: {
506
+ actions: {
285
507
  target_type: contentTypeUid,
286
508
  target_id: entryId
509
+ },
510
+ releasedAt: {
511
+ $null: true
287
512
  }
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
513
  },
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)
514
+ populate: {
515
+ // Filter the action to get only the content type entry
516
+ actions: {
517
+ where: {
518
+ target_type: contentTypeUid,
519
+ target_id: entryId
321
520
  }
521
+ }
522
+ }
523
+ });
524
+ return releases.map((release2) => {
525
+ if (release2.actions?.length) {
526
+ const [actionForEntry] = release2.actions;
527
+ delete release2.actions;
528
+ return {
529
+ ...release2,
530
+ action: actionForEntry
531
+ };
532
+ }
533
+ return release2;
534
+ });
535
+ },
536
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
537
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
538
+ where: {
539
+ releasedAt: {
540
+ $null: true
322
541
  },
323
- {
324
- actions: null
542
+ actions: {
543
+ target_type: contentTypeUid,
544
+ target_id: entryId
545
+ }
546
+ }
547
+ });
548
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
549
+ where: {
550
+ $or: [
551
+ {
552
+ id: {
553
+ $notIn: releasesRelated.map((release2) => release2.id)
554
+ }
555
+ },
556
+ {
557
+ actions: null
558
+ }
559
+ ],
560
+ releasedAt: {
561
+ $null: true
325
562
  }
326
- ],
327
- releasedAt: {
328
- $null: true
329
563
  }
564
+ });
565
+ return releases.map((release2) => {
566
+ if (release2.actions?.length) {
567
+ const [actionForEntry] = release2.actions;
568
+ delete release2.actions;
569
+ return {
570
+ ...release2,
571
+ action: actionForEntry
572
+ };
573
+ }
574
+ return release2;
575
+ });
576
+ },
577
+ async update(id, releaseData, { user }) {
578
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
579
+ releaseData
580
+ );
581
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
582
+ "release-validation",
583
+ { strapi: strapi2 }
584
+ );
585
+ await Promise.all([
586
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
587
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
588
+ ]);
589
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
590
+ if (!release2) {
591
+ throw new errors.NotFoundError(`No release found for id ${id}`);
330
592
  }
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
- };
593
+ if (release2.releasedAt) {
594
+ throw new errors.ValidationError("Release already published");
340
595
  }
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: "*"
596
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
597
+ /*
598
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
599
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
600
+ */
601
+ // @ts-expect-error see above
602
+ data: releaseWithCreatorFields
603
+ });
604
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
605
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
606
+ if (releaseData.scheduledAt) {
607
+ await schedulingService.set(id, releaseData.scheduledAt);
608
+ } else if (release2.scheduledAt) {
609
+ schedulingService.cancel(id);
406
610
  }
407
- },
408
- filters: {
409
- release: releaseId
410
611
  }
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);
612
+ this.updateReleaseStatus(id);
613
+ strapi2.telemetry.send("didUpdateContentRelease");
614
+ return updatedRelease;
615
+ },
616
+ async createAction(releaseId, action) {
617
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
618
+ strapi: strapi2
619
+ });
620
+ await Promise.all([
621
+ validateEntryContentType(action.entry.contentType),
622
+ validateUniqueEntry(releaseId, action)
623
+ ]);
624
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
625
+ if (!release2) {
626
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
420
627
  }
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
628
+ if (release2.releasedAt) {
629
+ throw new errors.ValidationError("Release already published");
630
+ }
631
+ const { entry, type } = action;
632
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
633
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
634
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
635
+ data: {
636
+ type,
637
+ contentType: entry.contentType,
638
+ locale: entry.locale,
639
+ isEntryValid,
640
+ entry: {
641
+ id: entry.id,
642
+ __type: entry.contentType,
643
+ __pivot: { field: "entry" }
644
+ },
645
+ release: releaseId
646
+ },
647
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
458
648
  });
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);
649
+ this.updateReleaseStatus(releaseId);
650
+ return releaseAction2;
651
+ },
652
+ async findActions(releaseId, query) {
653
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
654
+ fields: ["id"]
655
+ });
656
+ if (!release2) {
657
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
470
658
  }
471
- return acc;
472
- }, []);
473
- const contentTypeModelsMap = contentTypeUids.reduce(
474
- (acc, contentTypeUid) => {
475
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
659
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
660
+ ...query,
661
+ populate: {
662
+ entry: {
663
+ populate: "*"
664
+ }
665
+ },
666
+ filters: {
667
+ release: releaseId
668
+ }
669
+ });
670
+ },
671
+ async countActions(query) {
672
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
673
+ },
674
+ async groupActions(actions, groupBy) {
675
+ const contentTypeUids = actions.reduce((acc, action) => {
676
+ if (!acc.includes(action.contentType)) {
677
+ acc.push(action.contentType);
678
+ }
476
679
  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;
680
+ }, []);
681
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
682
+ contentTypeUids
683
+ );
684
+ const allLocalesDictionary = await this.getLocalesDataForActions();
685
+ const formattedData = actions.map((action) => {
686
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
687
+ return {
688
+ ...action,
689
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
690
+ contentType: {
691
+ displayName,
692
+ mainFieldValue: action.entry[mainField],
693
+ uid: action.contentType
694
+ }
695
+ };
696
+ });
697
+ const groupName = getGroupName(groupBy);
698
+ return _.groupBy(groupName)(formattedData);
699
+ },
700
+ async getLocalesDataForActions() {
701
+ if (!strapi2.plugin("i18n")) {
702
+ return {};
703
+ }
704
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
705
+ return allLocales.reduce((acc, locale) => {
706
+ acc[locale.code] = { name: locale.name, code: locale.code };
488
707
  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
- }
708
+ }, {});
709
+ },
710
+ async getContentTypesDataForActions(contentTypesUids) {
711
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
712
+ const contentTypesData = {};
713
+ for (const contentTypeUid of contentTypesUids) {
714
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
715
+ uid: contentTypeUid
716
+ });
717
+ contentTypesData[contentTypeUid] = {
718
+ mainField: contentTypeConfig.settings.mainField,
719
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
720
+ };
500
721
  }
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
- }
722
+ return contentTypesData;
723
+ },
724
+ getContentTypeModelsFromActions(actions) {
725
+ const contentTypeUids = actions.reduce((acc, action) => {
726
+ if (!acc.includes(action.contentType)) {
727
+ acc.push(action.contentType);
514
728
  }
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
- {
729
+ return acc;
730
+ }, []);
731
+ const contentTypeModelsMap = contentTypeUids.reduce(
732
+ (acc, contentTypeUid) => {
733
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
734
+ return acc;
735
+ },
736
+ {}
737
+ );
738
+ return contentTypeModelsMap;
739
+ },
740
+ async getAllComponents() {
741
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
742
+ const components = await contentManagerComponentsService.findAllComponents();
743
+ const componentsMap = components.reduce(
744
+ (acc, component) => {
745
+ acc[component.uid] = component;
746
+ return acc;
747
+ },
748
+ {}
749
+ );
750
+ return componentsMap;
751
+ },
752
+ async delete(releaseId) {
753
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
525
754
  populate: {
526
755
  actions: {
527
- populate: {
528
- entry: {
529
- fields: ["id"]
530
- }
531
- }
756
+ fields: ["id"]
532
757
  }
533
758
  }
759
+ });
760
+ if (!release2) {
761
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
534
762
  }
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
- };
763
+ if (release2.releasedAt) {
764
+ throw new errors.ValidationError("Release already published");
553
765
  }
554
- if (action.type === "publish") {
555
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
556
- } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
766
+ await strapi2.db.transaction(async () => {
767
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
768
+ where: {
769
+ id: {
770
+ $in: release2.actions.map((action) => action.id)
771
+ }
772
+ }
773
+ });
774
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
775
+ });
776
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
777
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
778
+ await schedulingService.cancel(release2.id);
558
779
  }
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,
780
+ strapi2.telemetry.send("didDeleteContentRelease");
781
+ return release2;
782
+ },
783
+ async publish(releaseId) {
784
+ try {
785
+ const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
786
+ RELEASE_MODEL_UID,
787
+ releaseId,
568
788
  {
569
- filters: {
570
- id: {
571
- $in: entriestoPublishIds
789
+ populate: {
790
+ actions: {
791
+ populate: {
792
+ entry: {
793
+ fields: ["id"]
794
+ }
795
+ }
572
796
  }
573
- },
574
- populate
797
+ }
575
798
  }
576
799
  );
577
- const entriesToUnpublish = await strapi2.entityService.findMany(
578
- contentTypeUid,
579
- {
580
- filters: {
581
- id: {
582
- $in: entriesToUnpublishIds
800
+ if (!releaseWithPopulatedActionEntries) {
801
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
802
+ }
803
+ if (releaseWithPopulatedActionEntries.releasedAt) {
804
+ throw new errors.ValidationError("Release already published");
805
+ }
806
+ if (releaseWithPopulatedActionEntries.actions.length === 0) {
807
+ throw new errors.ValidationError("No entries to publish");
808
+ }
809
+ const collectionTypeActions = {};
810
+ const singleTypeActions = [];
811
+ for (const action of releaseWithPopulatedActionEntries.actions) {
812
+ const contentTypeUid = action.contentType;
813
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
814
+ if (!collectionTypeActions[contentTypeUid]) {
815
+ collectionTypeActions[contentTypeUid] = {
816
+ entriestoPublishIds: [],
817
+ entriesToUnpublishIds: []
818
+ };
819
+ }
820
+ if (action.type === "publish") {
821
+ collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
822
+ } else {
823
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
824
+ }
825
+ } else {
826
+ singleTypeActions.push({
827
+ uid: contentTypeUid,
828
+ action: action.type,
829
+ id: action.entry.id
830
+ });
831
+ }
832
+ }
833
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
834
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
835
+ await strapi2.db.transaction(async () => {
836
+ for (const { uid, action, id } of singleTypeActions) {
837
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
838
+ const entry = await strapi2.entityService.findOne(uid, id, { populate });
839
+ try {
840
+ if (action === "publish") {
841
+ await entityManagerService.publish(entry, uid);
842
+ } else {
843
+ await entityManagerService.unpublish(entry, uid);
583
844
  }
584
- },
585
- populate
845
+ } catch (error) {
846
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
847
+ } else {
848
+ throw error;
849
+ }
850
+ }
586
851
  }
587
- );
588
- if (entriesToPublish.length > 0) {
589
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
852
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
853
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
854
+ const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
855
+ const entriesToPublish = await strapi2.entityService.findMany(
856
+ contentTypeUid,
857
+ {
858
+ filters: {
859
+ id: {
860
+ $in: entriestoPublishIds
861
+ }
862
+ },
863
+ populate
864
+ }
865
+ );
866
+ const entriesToUnpublish = await strapi2.entityService.findMany(
867
+ contentTypeUid,
868
+ {
869
+ filters: {
870
+ id: {
871
+ $in: entriesToUnpublishIds
872
+ }
873
+ },
874
+ populate
875
+ }
876
+ );
877
+ if (entriesToPublish.length > 0) {
878
+ await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
879
+ }
880
+ if (entriesToUnpublish.length > 0) {
881
+ await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
882
+ }
883
+ }
884
+ });
885
+ const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
886
+ data: {
887
+ /*
888
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
889
+ */
890
+ // @ts-expect-error see above
891
+ releasedAt: /* @__PURE__ */ new Date()
892
+ },
893
+ populate: {
894
+ actions: {
895
+ // @ts-expect-error is not expecting count but it is working
896
+ count: true
897
+ }
898
+ }
899
+ });
900
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
901
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
902
+ isPublished: true,
903
+ release: release2
904
+ });
590
905
  }
591
- if (entriesToUnpublish.length > 0) {
592
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
906
+ strapi2.telemetry.send("didPublishContentRelease");
907
+ return release2;
908
+ } catch (error) {
909
+ if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
910
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
911
+ isPublished: false,
912
+ error
913
+ });
593
914
  }
915
+ strapi2.db.query(RELEASE_MODEL_UID).update({
916
+ where: { id: releaseId },
917
+ data: {
918
+ status: "failed"
919
+ }
920
+ });
921
+ throw error;
594
922
  }
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()
923
+ },
924
+ async updateAction(actionId, releaseId, update) {
925
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
926
+ where: {
927
+ id: actionId,
928
+ release: {
929
+ id: releaseId,
930
+ releasedAt: {
931
+ $null: true
932
+ }
933
+ }
934
+ },
935
+ data: update
936
+ });
937
+ if (!updatedAction) {
938
+ throw new errors.NotFoundError(
939
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
940
+ );
603
941
  }
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
942
+ return updatedAction;
943
+ },
944
+ async deleteAction(actionId, releaseId) {
945
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
946
+ where: {
947
+ id: actionId,
948
+ release: {
949
+ id: releaseId,
950
+ releasedAt: {
951
+ $null: true
952
+ }
615
953
  }
616
954
  }
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
955
+ });
956
+ if (!deletedAction) {
957
+ throw new errors.NotFoundError(
958
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
959
+ );
960
+ }
961
+ this.updateReleaseStatus(releaseId);
962
+ return deletedAction;
963
+ },
964
+ async updateReleaseStatus(releaseId) {
965
+ const [totalActions, invalidActions] = await Promise.all([
966
+ this.countActions({
967
+ filters: {
968
+ release: releaseId
969
+ }
970
+ }),
971
+ this.countActions({
972
+ filters: {
973
+ release: releaseId,
974
+ isEntryValid: false
635
975
  }
976
+ })
977
+ ]);
978
+ if (totalActions > 0) {
979
+ if (invalidActions > 0) {
980
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
981
+ where: {
982
+ id: releaseId
983
+ },
984
+ data: {
985
+ status: "blocked"
986
+ }
987
+ });
636
988
  }
989
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
990
+ where: {
991
+ id: releaseId
992
+ },
993
+ data: {
994
+ status: "ready"
995
+ }
996
+ });
637
997
  }
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
- );
998
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
999
+ where: {
1000
+ id: releaseId
1001
+ },
1002
+ data: {
1003
+ status: "empty"
1004
+ }
1005
+ });
643
1006
  }
644
- return deletedAction;
645
- }
646
- });
1007
+ };
1008
+ };
647
1009
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
648
1010
  async validateUniqueEntry(releaseId, releaseActionArgs) {
649
1011
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -666,17 +1028,9 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
666
1028
  if (!contentType) {
667
1029
  throw new errors.NotFoundError(`No content type found for uid ${contentTypeUid}`);
668
1030
  }
669
- if (!contentType.options?.draftAndPublish) {
670
- throw new errors.ValidationError(
671
- `Content type with uid ${contentTypeUid} does not have draftAndPublish enabled`
672
- );
673
- }
674
1031
  },
675
1032
  async validatePendingReleasesLimit() {
676
- const maximumPendingReleases = (
677
- // @ts-expect-error - options is not typed into features
678
- EE.features.get("cms-content-releases")?.options?.maximumReleases || 3
679
- );
1033
+ const maximumPendingReleases = strapi2.ee.features.get("cms-content-releases")?.options?.maximumReleases || 3;
680
1034
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
681
1035
  filters: {
682
1036
  releasedAt: {
@@ -688,27 +1042,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
1042
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
1043
  }
690
1044
  },
691
- async validateUniqueNameForPendingRelease(name) {
1045
+ async validateUniqueNameForPendingRelease(name, id) {
692
1046
  const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
1047
  filters: {
694
1048
  releasedAt: {
695
1049
  $null: true
696
1050
  },
697
- name
1051
+ name,
1052
+ ...id && { id: { $ne: id } }
698
1053
  }
699
1054
  });
700
1055
  const isNameUnique = pendingReleases.length === 0;
701
1056
  if (!isNameUnique) {
702
1057
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
1058
  }
1059
+ },
1060
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1061
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1062
+ throw new errors.ValidationError("Scheduled at must be later than now");
1063
+ }
704
1064
  }
705
1065
  });
1066
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1067
+ const scheduledJobs = /* @__PURE__ */ new Map();
1068
+ return {
1069
+ async set(releaseId, scheduleDate) {
1070
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1071
+ if (!release2) {
1072
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1073
+ }
1074
+ const job = scheduleJob(scheduleDate, async () => {
1075
+ try {
1076
+ await getService("release").publish(releaseId);
1077
+ } catch (error) {
1078
+ }
1079
+ this.cancel(releaseId);
1080
+ });
1081
+ if (scheduledJobs.has(releaseId)) {
1082
+ this.cancel(releaseId);
1083
+ }
1084
+ scheduledJobs.set(releaseId, job);
1085
+ return scheduledJobs;
1086
+ },
1087
+ cancel(releaseId) {
1088
+ if (scheduledJobs.has(releaseId)) {
1089
+ scheduledJobs.get(releaseId).cancel();
1090
+ scheduledJobs.delete(releaseId);
1091
+ }
1092
+ return scheduledJobs;
1093
+ },
1094
+ getAll() {
1095
+ return scheduledJobs;
1096
+ },
1097
+ /**
1098
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1099
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1100
+ * This also could be used to sync different Strapi instances in case of a cluster
1101
+ */
1102
+ async syncFromDatabase() {
1103
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1104
+ where: {
1105
+ scheduledAt: {
1106
+ $gte: /* @__PURE__ */ new Date()
1107
+ },
1108
+ releasedAt: null
1109
+ }
1110
+ });
1111
+ for (const release2 of releases) {
1112
+ this.set(release2.id, release2.scheduledAt);
1113
+ }
1114
+ return scheduledJobs;
1115
+ }
1116
+ };
1117
+ };
706
1118
  const services = {
707
1119
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
1120
+ "release-validation": createReleaseValidationService,
1121
+ ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
709
1122
  };
710
1123
  const RELEASE_SCHEMA = yup.object().shape({
711
- name: yup.string().trim().required()
1124
+ name: yup.string().trim().required(),
1125
+ scheduledAt: yup.string().nullable(),
1126
+ isScheduled: yup.boolean().optional(),
1127
+ time: yup.string().when("isScheduled", {
1128
+ is: true,
1129
+ then: yup.string().trim().required(),
1130
+ otherwise: yup.string().nullable()
1131
+ }),
1132
+ timezone: yup.string().when("isScheduled", {
1133
+ is: true,
1134
+ then: yup.string().required().nullable(),
1135
+ otherwise: yup.string().nullable()
1136
+ }),
1137
+ date: yup.string().when("isScheduled", {
1138
+ is: true,
1139
+ then: yup.string().required().nullable(),
1140
+ otherwise: yup.string().nullable()
1141
+ })
712
1142
  }).required().noUnknown();
713
1143
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
1144
  const releaseController = {
@@ -741,26 +1171,30 @@ const releaseController = {
741
1171
  }
742
1172
  };
743
1173
  });
744
- ctx.body = { data, meta: { pagination } };
1174
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1175
+ where: {
1176
+ releasedAt: null
1177
+ }
1178
+ });
1179
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
745
1180
  }
746
1181
  },
747
1182
  async findOne(ctx) {
748
1183
  const id = ctx.params.id;
749
1184
  const releaseService = getService("release", { strapi });
750
1185
  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);
1186
+ if (!release2) {
1187
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
1188
+ }
756
1189
  const count = await releaseService.countActions({
757
1190
  filters: {
758
1191
  release: id
759
1192
  }
760
1193
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
1194
+ const sanitizedRelease = {
1195
+ ...release2,
1196
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1197
+ };
764
1198
  const data = {
765
1199
  ...sanitizedRelease,
766
1200
  actions: {
@@ -813,8 +1247,27 @@ const releaseController = {
813
1247
  const id = ctx.params.id;
814
1248
  const releaseService = getService("release", { strapi });
815
1249
  const release2 = await releaseService.publish(id, { user });
1250
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1251
+ releaseService.countActions({
1252
+ filters: {
1253
+ release: id,
1254
+ type: "publish"
1255
+ }
1256
+ }),
1257
+ releaseService.countActions({
1258
+ filters: {
1259
+ release: id,
1260
+ type: "unpublish"
1261
+ }
1262
+ })
1263
+ ]);
816
1264
  ctx.body = {
817
- data: release2
1265
+ data: release2,
1266
+ meta: {
1267
+ totalEntries: countPublishActions + countUnpublishActions,
1268
+ totalPublishedEntries: countPublishActions,
1269
+ totalUnpublishedEntries: countUnpublishActions
1270
+ }
818
1271
  };
819
1272
  }
820
1273
  };
@@ -864,7 +1317,7 @@ const releaseActionController = {
864
1317
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
865
1318
  return acc;
866
1319
  }, {});
867
- const sanitizedResults = await mapAsync(results, async (action) => ({
1320
+ const sanitizedResults = await async.map(results, async (action) => ({
868
1321
  ...action,
869
1322
  entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
870
1323
  }));
@@ -1080,12 +1533,12 @@ const routes = {
1080
1533
  release,
1081
1534
  "release-action": releaseAction
1082
1535
  };
1083
- const { features } = require("@strapi/strapi/dist/utils/ee");
1084
1536
  const getPlugin = () => {
1085
- if (features.isEnabled("cms-content-releases")) {
1537
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1086
1538
  return {
1087
1539
  register,
1088
1540
  bootstrap,
1541
+ destroy,
1089
1542
  contentTypes,
1090
1543
  services,
1091
1544
  controllers,