@strapi/content-releases 0.0.0-experimental.cae3a5a17d131a6f59673b62d01cfac869ea9cc2 → 0.0.0-experimental.d362bf200f5f9359a4bbd4a549603de5ee1f04ca

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 (108) hide show
  1. package/dist/_chunks/App-1LckaIGY.js +1352 -0
  2. package/dist/_chunks/App-1LckaIGY.js.map +1 -0
  3. package/dist/_chunks/App-X01LBg5V.mjs +1329 -0
  4. package/dist/_chunks/App-X01LBg5V.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-SOqjCdyh.mjs → en-RdapH-9X.mjs} +18 -7
  10. package/dist/_chunks/en-RdapH-9X.mjs.map +1 -0
  11. package/dist/_chunks/{en-2DuPv5k0.js → en-faJDuv3q.js} +18 -7
  12. package/dist/_chunks/en-faJDuv3q.js.map +1 -0
  13. package/dist/_chunks/{index-bsuc8ZwZ.mjs → index-OD9AlD-6.mjs} +233 -107
  14. package/dist/_chunks/index-OD9AlD-6.mjs.map +1 -0
  15. package/dist/_chunks/{index-_lT-gI3M.js → index-cYWov2wa.js} +226 -100
  16. package/dist/_chunks/index-cYWov2wa.js.map +1 -0
  17. package/dist/admin/index.js +1 -15
  18. package/dist/admin/index.js.map +1 -1
  19. package/dist/admin/index.mjs +1 -15
  20. package/dist/admin/index.mjs.map +1 -1
  21. package/dist/admin/src/components/CMReleasesContainer.d.ts +1 -0
  22. package/dist/admin/src/components/RelativeTime.d.ts +28 -0
  23. package/dist/admin/src/components/ReleaseActionMenu.d.ts +26 -0
  24. package/dist/admin/src/components/ReleaseActionOptions.d.ts +9 -0
  25. package/dist/admin/src/components/ReleaseModal.d.ts +16 -0
  26. package/dist/admin/src/constants.d.ts +58 -0
  27. package/dist/admin/src/index.d.ts +3 -0
  28. package/dist/admin/src/pages/App.d.ts +1 -0
  29. package/dist/admin/src/pages/PurchaseContentReleases.d.ts +2 -0
  30. package/dist/admin/src/pages/ReleaseDetailsPage.d.ts +2 -0
  31. package/dist/admin/src/pages/ReleasesPage.d.ts +8 -0
  32. package/dist/admin/src/pages/tests/mockReleaseDetailsPageData.d.ts +181 -0
  33. package/dist/admin/src/pages/tests/mockReleasesPageData.d.ts +39 -0
  34. package/dist/admin/src/pluginId.d.ts +1 -0
  35. package/dist/admin/src/services/axios.d.ts +29 -0
  36. package/dist/admin/src/services/release.d.ts +369 -0
  37. package/dist/admin/src/store/hooks.d.ts +7 -0
  38. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  39. package/dist/admin/src/utils/time.d.ts +1 -0
  40. package/dist/server/index.js +1014 -417
  41. package/dist/server/index.js.map +1 -1
  42. package/dist/server/index.mjs +1014 -417
  43. package/dist/server/index.mjs.map +1 -1
  44. package/dist/server/src/bootstrap.d.ts +5 -0
  45. package/dist/server/src/bootstrap.d.ts.map +1 -0
  46. package/dist/server/src/constants.d.ts +12 -0
  47. package/dist/server/src/constants.d.ts.map +1 -0
  48. package/dist/server/src/content-types/index.d.ts +99 -0
  49. package/dist/server/src/content-types/index.d.ts.map +1 -0
  50. package/dist/server/src/content-types/release/index.d.ts +48 -0
  51. package/dist/server/src/content-types/release/index.d.ts.map +1 -0
  52. package/dist/server/src/content-types/release/schema.d.ts +47 -0
  53. package/dist/server/src/content-types/release/schema.d.ts.map +1 -0
  54. package/dist/server/src/content-types/release-action/index.d.ts +50 -0
  55. package/dist/server/src/content-types/release-action/index.d.ts.map +1 -0
  56. package/dist/server/src/content-types/release-action/schema.d.ts +49 -0
  57. package/dist/server/src/content-types/release-action/schema.d.ts.map +1 -0
  58. package/dist/server/src/controllers/index.d.ts +19 -0
  59. package/dist/server/src/controllers/index.d.ts.map +1 -0
  60. package/dist/server/src/controllers/release-action.d.ts +10 -0
  61. package/dist/server/src/controllers/release-action.d.ts.map +1 -0
  62. package/dist/server/src/controllers/release.d.ts +11 -0
  63. package/dist/server/src/controllers/release.d.ts.map +1 -0
  64. package/dist/server/src/controllers/validation/release-action.d.ts +8 -0
  65. package/dist/server/src/controllers/validation/release-action.d.ts.map +1 -0
  66. package/dist/server/src/controllers/validation/release.d.ts +2 -0
  67. package/dist/server/src/controllers/validation/release.d.ts.map +1 -0
  68. package/dist/server/src/destroy.d.ts +5 -0
  69. package/dist/server/src/destroy.d.ts.map +1 -0
  70. package/dist/server/src/index.d.ts +2095 -0
  71. package/dist/server/src/index.d.ts.map +1 -0
  72. package/dist/server/src/migrations/index.d.ts +13 -0
  73. package/dist/server/src/migrations/index.d.ts.map +1 -0
  74. package/dist/server/src/register.d.ts +5 -0
  75. package/dist/server/src/register.d.ts.map +1 -0
  76. package/dist/server/src/routes/index.d.ts +35 -0
  77. package/dist/server/src/routes/index.d.ts.map +1 -0
  78. package/dist/server/src/routes/release-action.d.ts +18 -0
  79. package/dist/server/src/routes/release-action.d.ts.map +1 -0
  80. package/dist/server/src/routes/release.d.ts +18 -0
  81. package/dist/server/src/routes/release.d.ts.map +1 -0
  82. package/dist/server/src/services/index.d.ts +1826 -0
  83. package/dist/server/src/services/index.d.ts.map +1 -0
  84. package/dist/server/src/services/release.d.ts +66 -0
  85. package/dist/server/src/services/release.d.ts.map +1 -0
  86. package/dist/server/src/services/scheduling.d.ts +18 -0
  87. package/dist/server/src/services/scheduling.d.ts.map +1 -0
  88. package/dist/server/src/services/validation.d.ts +18 -0
  89. package/dist/server/src/services/validation.d.ts.map +1 -0
  90. package/dist/server/src/utils/index.d.ts +14 -0
  91. package/dist/server/src/utils/index.d.ts.map +1 -0
  92. package/dist/shared/contracts/release-actions.d.ts +131 -0
  93. package/dist/shared/contracts/release-actions.d.ts.map +1 -0
  94. package/dist/shared/contracts/releases.d.ts +166 -0
  95. package/dist/shared/contracts/releases.d.ts.map +1 -0
  96. package/dist/shared/types.d.ts +24 -0
  97. package/dist/shared/types.d.ts.map +1 -0
  98. package/dist/shared/validation-schemas.d.ts +2 -0
  99. package/dist/shared/validation-schemas.d.ts.map +1 -0
  100. package/package.json +25 -29
  101. package/dist/_chunks/App-_Jj3tWts.mjs +0 -1053
  102. package/dist/_chunks/App-_Jj3tWts.mjs.map +0 -1
  103. package/dist/_chunks/App-iqqoPnBO.js +0 -1075
  104. package/dist/_chunks/App-iqqoPnBO.js.map +0 -1
  105. package/dist/_chunks/en-2DuPv5k0.js.map +0 -1
  106. package/dist/_chunks/en-SOqjCdyh.mjs.map +0 -1
  107. package/dist/_chunks/index-_lT-gI3M.js.map +0 -1
  108. package/dist/_chunks/index-bsuc8ZwZ.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 { contentTypes as contentTypes$1, 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,6 +50,35 @@ const ACTIONS = [
49
50
  pluginName: "content-releases"
50
51
  }
51
52
  ];
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.db.query(contentTypeUid).findOne({
63
+ where: { id: entryId },
64
+ populate
65
+ });
66
+ return entry;
67
+ };
68
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
69
+ try {
70
+ await strapi2.entityValidator.validateEntityCreation(
71
+ strapi2.getModel(contentTypeUid),
72
+ entry,
73
+ void 0,
74
+ // @ts-expect-error - FIXME: entity here is unnecessary
75
+ entry
76
+ );
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
52
82
  async function deleteActionsOnDisableDraftAndPublish({
53
83
  oldContentTypes,
54
84
  contentTypes: contentTypes2
@@ -70,33 +100,199 @@ async function deleteActionsOnDisableDraftAndPublish({
70
100
  async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes: contentTypes2 }) {
71
101
  const deletedContentTypes = difference(keys(oldContentTypes), keys(contentTypes2)) ?? [];
72
102
  if (deletedContentTypes.length) {
73
- await mapAsync(deletedContentTypes, async (deletedContentTypeUID) => {
103
+ await async.map(deletedContentTypes, async (deletedContentTypeUID) => {
74
104
  return strapi.db?.queryBuilder(RELEASE_ACTION_MODEL_UID).delete().where({ contentType: deletedContentTypeUID }).execute();
75
105
  });
76
106
  }
77
107
  }
78
- const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
108
+ async function migrateIsValidAndStatusReleases() {
109
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
110
+ where: {
111
+ status: null,
112
+ releasedAt: null
113
+ },
114
+ populate: {
115
+ actions: {
116
+ populate: {
117
+ entry: true
118
+ }
119
+ }
120
+ }
121
+ });
122
+ async.map(releasesWithoutStatus, async (release2) => {
123
+ const actions = release2.actions;
124
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
125
+ for (const action of notValidatedActions) {
126
+ if (action.entry) {
127
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
128
+ strapi
129
+ });
130
+ if (populatedEntry) {
131
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
132
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
133
+ where: {
134
+ id: action.id
135
+ },
136
+ data: {
137
+ isEntryValid
138
+ }
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
144
+ });
145
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
146
+ where: {
147
+ status: null,
148
+ releasedAt: {
149
+ $notNull: true
150
+ }
151
+ }
152
+ });
153
+ async.map(publishedReleases, async (release2) => {
154
+ return strapi.db.query(RELEASE_MODEL_UID).update({
155
+ where: {
156
+ id: release2.id
157
+ },
158
+ data: {
159
+ status: "done"
160
+ }
161
+ });
162
+ });
163
+ }
164
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
165
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
166
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
167
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
168
+ );
169
+ const releasesAffected = /* @__PURE__ */ new Set();
170
+ async.map(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
171
+ const oldContentType = oldContentTypes[contentTypeUID];
172
+ const contentType = contentTypes2[contentTypeUID];
173
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
174
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
175
+ where: {
176
+ contentType: contentTypeUID
177
+ },
178
+ populate: {
179
+ entry: true,
180
+ release: true
181
+ }
182
+ });
183
+ await async.map(actions, async (action) => {
184
+ if (action.entry && action.release) {
185
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
186
+ strapi
187
+ });
188
+ if (populatedEntry) {
189
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
190
+ strapi
191
+ });
192
+ releasesAffected.add(action.release.id);
193
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
194
+ where: {
195
+ id: action.id
196
+ },
197
+ data: {
198
+ isEntryValid
199
+ }
200
+ });
201
+ }
202
+ }
203
+ });
204
+ }
205
+ }).then(() => {
206
+ async.map(releasesAffected, async (releaseId) => {
207
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
208
+ });
209
+ });
210
+ }
211
+ }
212
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
213
+ if (!oldContentTypes) {
214
+ return;
215
+ }
216
+ for (const uid in contentTypes2) {
217
+ if (!oldContentTypes[uid]) {
218
+ continue;
219
+ }
220
+ const oldContentType = oldContentTypes[uid];
221
+ const contentType = contentTypes2[uid];
222
+ const i18nPlugin = strapi.plugin("i18n");
223
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
224
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
225
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
226
+ locale: null
227
+ }).where({ contentType: uid }).execute();
228
+ }
229
+ }
230
+ }
231
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
232
+ if (!oldContentTypes) {
233
+ return;
234
+ }
235
+ for (const uid in contentTypes2) {
236
+ if (!oldContentTypes[uid]) {
237
+ continue;
238
+ }
239
+ const oldContentType = oldContentTypes[uid];
240
+ const contentType = contentTypes2[uid];
241
+ const i18nPlugin = strapi.plugin("i18n");
242
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
243
+ const { getDefaultLocale } = i18nPlugin.service("locales");
244
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
245
+ const defaultLocale = await getDefaultLocale();
246
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
247
+ locale: defaultLocale
248
+ }).where({ contentType: uid }).execute();
249
+ }
250
+ }
251
+ }
79
252
  const register = async ({ strapi: strapi2 }) => {
80
- if (features$2.isEnabled("cms-content-releases")) {
253
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
81
254
  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);
255
+ strapi2.hook("strapi::content-types.beforeSync").register(disableContentTypeLocalized).register(deleteActionsOnDisableDraftAndPublish);
256
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
257
+ }
258
+ if (strapi2.plugin("graphql")) {
259
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
260
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
261
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
84
262
  }
85
263
  };
86
- const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
87
264
  const bootstrap = async ({ strapi: strapi2 }) => {
88
- if (features$1.isEnabled("cms-content-releases")) {
265
+ if (strapi2.ee.features.isEnabled("cms-content-releases")) {
266
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
+ );
89
269
  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
270
+ models: contentTypesWithDraftAndPublish,
271
+ async afterDelete(event) {
272
+ try {
273
+ const { model, result } = event;
274
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
275
+ const { id } = result;
276
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
277
+ where: {
278
+ actions: {
279
+ target_type: model.uid,
280
+ target_id: id
281
+ }
282
+ }
283
+ });
284
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
285
+ where: {
286
+ target_type: model.uid,
287
+ target_id: id
288
+ }
289
+ });
290
+ for (const release2 of releases) {
291
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
98
292
  }
99
- });
293
+ }
294
+ } catch (error) {
295
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
100
296
  }
101
297
  },
102
298
  /**
@@ -116,20 +312,88 @@ const bootstrap = async ({ strapi: strapi2 }) => {
116
312
  * We make this only after deleteMany is succesfully executed to avoid errors
117
313
  */
118
314
  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)
315
+ try {
316
+ const { model, state } = event;
317
+ const entriesToDelete = state.entriesToDelete;
318
+ if (entriesToDelete) {
319
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
320
+ where: {
321
+ actions: {
322
+ target_type: model.uid,
323
+ target_id: {
324
+ $in: entriesToDelete.map((entry) => entry.id)
325
+ }
326
+ }
327
+ }
328
+ });
329
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
330
+ where: {
331
+ target_type: model.uid,
332
+ target_id: {
333
+ $in: entriesToDelete.map((entry) => entry.id)
334
+ }
127
335
  }
336
+ });
337
+ for (const release2 of releases) {
338
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
128
339
  }
340
+ }
341
+ } catch (error) {
342
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
343
+ error
129
344
  });
130
345
  }
346
+ },
347
+ async afterUpdate(event) {
348
+ try {
349
+ const { model, result } = event;
350
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
351
+ const isEntryValid = await getEntryValidStatus(model.uid, result, {
352
+ strapi: strapi2
353
+ });
354
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
355
+ where: {
356
+ target_type: model.uid,
357
+ target_id: result.id
358
+ },
359
+ data: {
360
+ isEntryValid
361
+ }
362
+ });
363
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
364
+ where: {
365
+ actions: {
366
+ target_type: model.uid,
367
+ target_id: result.id
368
+ }
369
+ }
370
+ });
371
+ for (const release2 of releases) {
372
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
373
+ }
374
+ }
375
+ } catch (error) {
376
+ strapi2.log.error("Error while updating release actions after entry update", { error });
377
+ }
131
378
  }
132
379
  });
380
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
381
+ strapi2.log.error(
382
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
383
+ );
384
+ throw err;
385
+ });
386
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
387
+ strapi2.webhookStore.addAllowedEvent(key, value);
388
+ });
389
+ }
390
+ };
391
+ const destroy = async ({ strapi: strapi2 }) => {
392
+ const scheduledJobs = getService("scheduling", {
393
+ strapi: strapi2
394
+ }).getAll();
395
+ for (const [, job] of scheduledJobs) {
396
+ job.cancel();
133
397
  }
134
398
  };
135
399
  const schema$1 = {
@@ -158,6 +422,17 @@ const schema$1 = {
158
422
  releasedAt: {
159
423
  type: "datetime"
160
424
  },
425
+ scheduledAt: {
426
+ type: "datetime"
427
+ },
428
+ timezone: {
429
+ type: "string"
430
+ },
431
+ status: {
432
+ type: "enumeration",
433
+ enum: ["ready", "blocked", "failed", "done", "empty"],
434
+ required: true
435
+ },
161
436
  actions: {
162
437
  type: "relation",
163
438
  relation: "oneToMany",
@@ -210,6 +485,9 @@ const schema = {
210
485
  relation: "manyToOne",
211
486
  target: RELEASE_MODEL_UID,
212
487
  inversedBy: "actions"
488
+ },
489
+ isEntryValid: {
490
+ type: "boolean"
213
491
  }
214
492
  }
215
493
  };
@@ -220,9 +498,6 @@ const contentTypes = {
220
498
  release: release$1,
221
499
  "release-action": releaseAction$1
222
500
  };
223
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
224
- return strapi2.plugin("content-releases").service(name);
225
- };
226
501
  const getGroupName = (queryValue) => {
227
502
  switch (queryValue) {
228
503
  case "contentType":
@@ -235,419 +510,594 @@ const getGroupName = (queryValue) => {
235
510
  return "contentType.displayName";
236
511
  }
237
512
  };
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
513
+ const createReleaseService = ({ strapi: strapi2 }) => {
514
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
515
+ strapi2.eventHub.emit(event, {
516
+ isPublished,
517
+ error,
518
+ release: release2
251
519
  });
252
- },
253
- async findOne(id, query = {}) {
254
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
255
- ...query
520
+ };
521
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
522
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
523
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
524
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
525
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
526
+ try {
527
+ if (actionType === "publish") {
528
+ await entityManagerService.publish(entry, uid);
529
+ } else {
530
+ await entityManagerService.unpublish(entry, uid);
531
+ }
532
+ } catch (error) {
533
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
534
+ ;
535
+ else {
536
+ throw error;
537
+ }
538
+ }
539
+ };
540
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
541
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
542
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
543
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
544
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
545
+ filters: {
546
+ id: {
547
+ $in: entriesToPublishIds
548
+ }
549
+ },
550
+ populate
256
551
  });
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
552
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
553
+ filters: {
554
+ id: {
555
+ $in: entriestoUnpublishIds
266
556
  }
267
- }
557
+ },
558
+ populate
268
559
  });
269
- },
270
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
271
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
560
+ if (entriesToPublish.length > 0) {
561
+ await entityManagerService.publishMany(entriesToPublish, uid);
562
+ }
563
+ if (entriesToUnpublish.length > 0) {
564
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
565
+ }
566
+ };
567
+ const getFormattedActions = async (releaseId) => {
568
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
272
569
  where: {
273
- actions: {
274
- target_type: contentTypeUid,
275
- target_id: entryId
276
- },
277
- releasedAt: {
278
- $null: true
570
+ release: {
571
+ id: releaseId
279
572
  }
280
573
  },
281
574
  populate: {
282
- // Filter the action to get only the content type entry
283
- actions: {
284
- where: {
285
- target_type: contentTypeUid,
286
- target_id: entryId
287
- }
575
+ entry: {
576
+ fields: ["id"]
288
577
  }
289
578
  }
290
579
  });
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
- };
580
+ if (actions.length === 0) {
581
+ throw new errors.ValidationError("No entries to publish");
582
+ }
583
+ const collectionTypeActions = {};
584
+ const singleTypeActions = [];
585
+ for (const action of actions) {
586
+ const contentTypeUid = action.contentType;
587
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
588
+ if (!collectionTypeActions[contentTypeUid]) {
589
+ collectionTypeActions[contentTypeUid] = {
590
+ entriesToPublishIds: [],
591
+ entriesToUnpublishIds: []
592
+ };
593
+ }
594
+ if (action.type === "publish") {
595
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
596
+ } else {
597
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
598
+ }
599
+ } else {
600
+ singleTypeActions.push({
601
+ uid: contentTypeUid,
602
+ action: action.type,
603
+ id: action.entry.id
604
+ });
299
605
  }
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
- },
309
- actions: {
310
- target_type: contentTypeUid,
311
- target_id: entryId
606
+ }
607
+ return { collectionTypeActions, singleTypeActions };
608
+ };
609
+ return {
610
+ async create(releaseData, { user }) {
611
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
612
+ const {
613
+ validatePendingReleasesLimit,
614
+ validateUniqueNameForPendingRelease,
615
+ validateScheduledAtIsLaterThanNow
616
+ } = getService("release-validation", { strapi: strapi2 });
617
+ await Promise.all([
618
+ validatePendingReleasesLimit(),
619
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
620
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
621
+ ]);
622
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).create({
623
+ data: {
624
+ ...releaseWithCreatorFields,
625
+ status: "empty"
312
626
  }
627
+ });
628
+ if (releaseWithCreatorFields.scheduledAt) {
629
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
630
+ await schedulingService.set(release2.id, release2.scheduledAt);
313
631
  }
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)
632
+ strapi2.telemetry.send("didCreateContentRelease");
633
+ return release2;
634
+ },
635
+ async findOne(id, query = {}) {
636
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query);
637
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
638
+ ...dbQuery,
639
+ where: { id }
640
+ });
641
+ return release2;
642
+ },
643
+ findPage(query) {
644
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_MODEL_UID, query ?? {});
645
+ return strapi2.db.query(RELEASE_MODEL_UID).findPage({
646
+ ...dbQuery,
647
+ populate: {
648
+ actions: {
649
+ count: true
650
+ }
651
+ }
652
+ });
653
+ },
654
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
655
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
656
+ where: {
657
+ actions: {
658
+ target_type: contentTypeUid,
659
+ target_id: entryId
660
+ },
661
+ releasedAt: {
662
+ $null: true
663
+ }
664
+ },
665
+ populate: {
666
+ // Filter the action to get only the content type entry
667
+ actions: {
668
+ where: {
669
+ target_type: contentTypeUid,
670
+ target_id: entryId
321
671
  }
672
+ }
673
+ }
674
+ });
675
+ return releases.map((release2) => {
676
+ if (release2.actions?.length) {
677
+ const [actionForEntry] = release2.actions;
678
+ delete release2.actions;
679
+ return {
680
+ ...release2,
681
+ action: actionForEntry
682
+ };
683
+ }
684
+ return release2;
685
+ });
686
+ },
687
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
688
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
689
+ where: {
690
+ releasedAt: {
691
+ $null: true
322
692
  },
323
- {
324
- actions: null
693
+ actions: {
694
+ target_type: contentTypeUid,
695
+ target_id: entryId
325
696
  }
326
- ],
327
- releasedAt: {
328
- $null: true
329
697
  }
698
+ });
699
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
700
+ where: {
701
+ $or: [
702
+ {
703
+ id: {
704
+ $notIn: releasesRelated.map((release2) => release2.id)
705
+ }
706
+ },
707
+ {
708
+ actions: null
709
+ }
710
+ ],
711
+ releasedAt: {
712
+ $null: true
713
+ }
714
+ }
715
+ });
716
+ return releases.map((release2) => {
717
+ if (release2.actions?.length) {
718
+ const [actionForEntry] = release2.actions;
719
+ delete release2.actions;
720
+ return {
721
+ ...release2,
722
+ action: actionForEntry
723
+ };
724
+ }
725
+ return release2;
726
+ });
727
+ },
728
+ async update(id, releaseData, { user }) {
729
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
730
+ releaseData
731
+ );
732
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
733
+ "release-validation",
734
+ { strapi: strapi2 }
735
+ );
736
+ await Promise.all([
737
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
738
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
739
+ ]);
740
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id } });
741
+ if (!release2) {
742
+ throw new errors.NotFoundError(`No release found for id ${id}`);
330
743
  }
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
- };
744
+ if (release2.releasedAt) {
745
+ throw new errors.ValidationError("Release already published");
340
746
  }
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: "*"
406
- }
407
- },
408
- filters: {
409
- release: releaseId
747
+ const updatedRelease = await strapi2.db.query(RELEASE_MODEL_UID).update({
748
+ where: { id },
749
+ data: releaseWithCreatorFields
750
+ });
751
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
752
+ if (releaseData.scheduledAt) {
753
+ await schedulingService.set(id, releaseData.scheduledAt);
754
+ } else if (release2.scheduledAt) {
755
+ schedulingService.cancel(id);
410
756
  }
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);
757
+ this.updateReleaseStatus(id);
758
+ strapi2.telemetry.send("didUpdateContentRelease");
759
+ return updatedRelease;
760
+ },
761
+ async createAction(releaseId, action) {
762
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
763
+ strapi: strapi2
764
+ });
765
+ await Promise.all([
766
+ validateEntryContentType(action.entry.contentType),
767
+ validateUniqueEntry(releaseId, action)
768
+ ]);
769
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId } });
770
+ if (!release2) {
771
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
420
772
  }
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
773
+ if (release2.releasedAt) {
774
+ throw new errors.ValidationError("Release already published");
775
+ }
776
+ const { entry, type } = action;
777
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
778
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
779
+ const releaseAction2 = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).create({
780
+ data: {
781
+ type,
782
+ contentType: entry.contentType,
783
+ locale: entry.locale,
784
+ isEntryValid,
785
+ entry: {
786
+ id: entry.id,
787
+ __type: entry.contentType,
788
+ __pivot: { field: "entry" }
789
+ },
790
+ release: releaseId
791
+ },
792
+ populate: { release: { select: ["id"] }, entry: { select: ["id"] } }
458
793
  });
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);
794
+ this.updateReleaseStatus(releaseId);
795
+ return releaseAction2;
796
+ },
797
+ async findActions(releaseId, query) {
798
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
799
+ where: { id: releaseId },
800
+ select: ["id"]
801
+ });
802
+ if (!release2) {
803
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
470
804
  }
471
- return acc;
472
- }, []);
473
- const contentTypeModelsMap = contentTypeUids.reduce(
474
- (acc, contentTypeUid) => {
475
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
805
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
806
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).findPage({
807
+ ...dbQuery,
808
+ populate: {
809
+ entry: {
810
+ populate: "*"
811
+ }
812
+ },
813
+ where: {
814
+ release: releaseId
815
+ }
816
+ });
817
+ },
818
+ async countActions(query) {
819
+ const dbQuery = strapi2.get("query-params").transform(RELEASE_ACTION_MODEL_UID, query ?? {});
820
+ return strapi2.db.query(RELEASE_ACTION_MODEL_UID).count(dbQuery);
821
+ },
822
+ async groupActions(actions, groupBy) {
823
+ const contentTypeUids = actions.reduce((acc, action) => {
824
+ if (!acc.includes(action.contentType)) {
825
+ acc.push(action.contentType);
826
+ }
476
827
  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;
828
+ }, []);
829
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
830
+ contentTypeUids
831
+ );
832
+ const allLocalesDictionary = await this.getLocalesDataForActions();
833
+ const formattedData = actions.map((action) => {
834
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
835
+ return {
836
+ ...action,
837
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
838
+ contentType: {
839
+ displayName,
840
+ mainFieldValue: action.entry[mainField],
841
+ uid: action.contentType
842
+ }
843
+ };
844
+ });
845
+ const groupName = getGroupName(groupBy);
846
+ return _.groupBy(groupName)(formattedData);
847
+ },
848
+ async getLocalesDataForActions() {
849
+ if (!strapi2.plugin("i18n")) {
850
+ return {};
851
+ }
852
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
853
+ return allLocales.reduce((acc, locale) => {
854
+ acc[locale.code] = { name: locale.name, code: locale.code };
488
855
  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
- }
856
+ }, {});
857
+ },
858
+ async getContentTypesDataForActions(contentTypesUids) {
859
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
860
+ const contentTypesData = {};
861
+ for (const contentTypeUid of contentTypesUids) {
862
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
863
+ uid: contentTypeUid
864
+ });
865
+ contentTypesData[contentTypeUid] = {
866
+ mainField: contentTypeConfig.settings.mainField,
867
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
868
+ };
500
869
  }
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
- }
870
+ return contentTypesData;
871
+ },
872
+ getContentTypeModelsFromActions(actions) {
873
+ const contentTypeUids = actions.reduce((acc, action) => {
874
+ if (!acc.includes(action.contentType)) {
875
+ acc.push(action.contentType);
514
876
  }
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
- {
877
+ return acc;
878
+ }, []);
879
+ const contentTypeModelsMap = contentTypeUids.reduce(
880
+ (acc, contentTypeUid) => {
881
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
882
+ return acc;
883
+ },
884
+ {}
885
+ );
886
+ return contentTypeModelsMap;
887
+ },
888
+ async getAllComponents() {
889
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
890
+ const components = await contentManagerComponentsService.findAllComponents();
891
+ const componentsMap = components.reduce(
892
+ (acc, component) => {
893
+ acc[component.uid] = component;
894
+ return acc;
895
+ },
896
+ {}
897
+ );
898
+ return componentsMap;
899
+ },
900
+ async delete(releaseId) {
901
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
902
+ where: { id: releaseId },
525
903
  populate: {
526
904
  actions: {
527
- populate: {
528
- entry: {
529
- fields: ["id"]
530
- }
531
- }
905
+ select: ["id"]
532
906
  }
533
907
  }
908
+ });
909
+ if (!release2) {
910
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
534
911
  }
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
- };
553
- }
554
- if (action.type === "publish") {
555
- actions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
556
- } else {
557
- actions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
912
+ if (release2.releasedAt) {
913
+ throw new errors.ValidationError("Release already published");
558
914
  }
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,
568
- {
569
- filters: {
570
- id: {
571
- $in: entriestoPublishIds
572
- }
573
- },
574
- populate
915
+ await strapi2.db.transaction(async () => {
916
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
917
+ where: {
918
+ id: {
919
+ $in: release2.actions.map((action) => action.id)
920
+ }
575
921
  }
576
- );
577
- const entriesToUnpublish = await strapi2.entityService.findMany(
578
- contentTypeUid,
579
- {
580
- filters: {
581
- id: {
582
- $in: entriesToUnpublishIds
583
- }
584
- },
585
- populate
922
+ });
923
+ await strapi2.db.query(RELEASE_MODEL_UID).delete({
924
+ where: {
925
+ id: releaseId
586
926
  }
587
- );
588
- if (entriesToPublish.length > 0) {
589
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
927
+ });
928
+ });
929
+ if (release2.scheduledAt) {
930
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
931
+ await schedulingService.cancel(release2.id);
932
+ }
933
+ strapi2.telemetry.send("didDeleteContentRelease");
934
+ return release2;
935
+ },
936
+ async publish(releaseId) {
937
+ const {
938
+ release: release2,
939
+ error
940
+ } = await strapi2.db.transaction(async ({ trx }) => {
941
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
942
+ if (!lockedRelease) {
943
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
944
+ }
945
+ if (lockedRelease.releasedAt) {
946
+ throw new errors.ValidationError("Release already published");
590
947
  }
591
- if (entriesToUnpublish.length > 0) {
592
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
948
+ if (lockedRelease.status === "failed") {
949
+ throw new errors.ValidationError("Release failed to publish");
593
950
  }
951
+ try {
952
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
953
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
954
+ releaseId
955
+ );
956
+ await strapi2.db.transaction(async () => {
957
+ for (const { uid, action, id } of singleTypeActions) {
958
+ await publishSingleTypeAction(uid, action, id);
959
+ }
960
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
961
+ const uid = contentTypeUid;
962
+ await publishCollectionTypeAction(
963
+ uid,
964
+ collectionTypeActions[uid].entriesToPublishIds,
965
+ collectionTypeActions[uid].entriesToUnpublishIds
966
+ );
967
+ }
968
+ });
969
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
970
+ where: {
971
+ id: releaseId
972
+ },
973
+ data: {
974
+ status: "done",
975
+ releasedAt: /* @__PURE__ */ new Date()
976
+ }
977
+ });
978
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
979
+ isPublished: true,
980
+ release: release22
981
+ });
982
+ strapi2.telemetry.send("didPublishContentRelease");
983
+ return { release: release22, error: null };
984
+ } catch (error2) {
985
+ dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
986
+ isPublished: false,
987
+ error: error2
988
+ });
989
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
990
+ status: "failed"
991
+ }).transacting(trx).execute();
992
+ return {
993
+ release: null,
994
+ error: error2
995
+ };
996
+ }
997
+ });
998
+ if (error instanceof Error) {
999
+ throw error;
594
1000
  }
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()
1001
+ return release2;
1002
+ },
1003
+ async updateAction(actionId, releaseId, update) {
1004
+ const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
1005
+ where: {
1006
+ id: actionId,
1007
+ release: {
1008
+ id: releaseId,
1009
+ releasedAt: {
1010
+ $null: true
1011
+ }
1012
+ }
1013
+ },
1014
+ data: update
1015
+ });
1016
+ if (!updatedAction) {
1017
+ throw new errors.NotFoundError(
1018
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1019
+ );
603
1020
  }
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
1021
+ return updatedAction;
1022
+ },
1023
+ async deleteAction(actionId, releaseId) {
1024
+ const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
1025
+ where: {
1026
+ id: actionId,
1027
+ release: {
1028
+ id: releaseId,
1029
+ releasedAt: {
1030
+ $null: true
1031
+ }
615
1032
  }
616
1033
  }
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
1034
+ });
1035
+ if (!deletedAction) {
1036
+ throw new errors.NotFoundError(
1037
+ `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
1038
+ );
1039
+ }
1040
+ this.updateReleaseStatus(releaseId);
1041
+ return deletedAction;
1042
+ },
1043
+ async updateReleaseStatus(releaseId) {
1044
+ const [totalActions, invalidActions] = await Promise.all([
1045
+ this.countActions({
1046
+ filters: {
1047
+ release: releaseId
635
1048
  }
1049
+ }),
1050
+ this.countActions({
1051
+ filters: {
1052
+ release: releaseId,
1053
+ isEntryValid: false
1054
+ }
1055
+ })
1056
+ ]);
1057
+ if (totalActions > 0) {
1058
+ if (invalidActions > 0) {
1059
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1060
+ where: {
1061
+ id: releaseId
1062
+ },
1063
+ data: {
1064
+ status: "blocked"
1065
+ }
1066
+ });
636
1067
  }
1068
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1069
+ where: {
1070
+ id: releaseId
1071
+ },
1072
+ data: {
1073
+ status: "ready"
1074
+ }
1075
+ });
637
1076
  }
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
- );
1077
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1078
+ where: {
1079
+ id: releaseId
1080
+ },
1081
+ data: {
1082
+ status: "empty"
1083
+ }
1084
+ });
643
1085
  }
644
- return deletedAction;
1086
+ };
1087
+ };
1088
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1089
+ constructor(message) {
1090
+ super(message);
1091
+ this.name = "AlreadyOnReleaseError";
645
1092
  }
646
- });
1093
+ }
647
1094
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
648
1095
  async validateUniqueEntry(releaseId, releaseActionArgs) {
649
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
650
- populate: { actions: { populate: { entry: { fields: ["id"] } } } }
1096
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({
1097
+ where: {
1098
+ id: releaseId
1099
+ },
1100
+ populate: { actions: { populate: { entry: { select: ["id"] } } } }
651
1101
  });
652
1102
  if (!release2) {
653
1103
  throw new errors.NotFoundError(`No release found for id ${releaseId}`);
@@ -656,7 +1106,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
656
1106
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
657
1107
  );
658
1108
  if (isEntryInRelease) {
659
- throw new errors.ValidationError(
1109
+ throw new AlreadyOnReleaseError(
660
1110
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
661
1111
  );
662
1112
  }
@@ -673,10 +1123,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
673
1123
  }
674
1124
  },
675
1125
  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
- );
1126
+ const maximumPendingReleases = strapi2.ee.features.get("cms-content-releases")?.options?.maximumReleases || 3;
680
1127
  const [, pendingReleasesCount] = await strapi2.db.query(RELEASE_MODEL_UID).findWithCount({
681
1128
  filters: {
682
1129
  releasedAt: {
@@ -688,27 +1135,103 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
688
1135
  throw new errors.ValidationError("You have reached the maximum number of pending releases");
689
1136
  }
690
1137
  },
691
- async validateUniqueNameForPendingRelease(name) {
692
- const pendingReleases = await strapi2.entityService.findMany(RELEASE_MODEL_UID, {
693
- filters: {
1138
+ async validateUniqueNameForPendingRelease(name, id) {
1139
+ const pendingReleases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1140
+ where: {
694
1141
  releasedAt: {
695
1142
  $null: true
696
1143
  },
697
- name
1144
+ name,
1145
+ ...id && { id: { $ne: id } }
698
1146
  }
699
1147
  });
700
1148
  const isNameUnique = pendingReleases.length === 0;
701
1149
  if (!isNameUnique) {
702
1150
  throw new errors.ValidationError(`Release with name ${name} already exists`);
703
1151
  }
1152
+ },
1153
+ async validateScheduledAtIsLaterThanNow(scheduledAt) {
1154
+ if (scheduledAt && new Date(scheduledAt) <= /* @__PURE__ */ new Date()) {
1155
+ throw new errors.ValidationError("Scheduled at must be later than now");
1156
+ }
704
1157
  }
705
1158
  });
1159
+ const createSchedulingService = ({ strapi: strapi2 }) => {
1160
+ const scheduledJobs = /* @__PURE__ */ new Map();
1161
+ return {
1162
+ async set(releaseId, scheduleDate) {
1163
+ const release2 = await strapi2.db.query(RELEASE_MODEL_UID).findOne({ where: { id: releaseId, releasedAt: null } });
1164
+ if (!release2) {
1165
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
1166
+ }
1167
+ const job = scheduleJob(scheduleDate, async () => {
1168
+ try {
1169
+ await getService("release").publish(releaseId);
1170
+ } catch (error) {
1171
+ }
1172
+ this.cancel(releaseId);
1173
+ });
1174
+ if (scheduledJobs.has(releaseId)) {
1175
+ this.cancel(releaseId);
1176
+ }
1177
+ scheduledJobs.set(releaseId, job);
1178
+ return scheduledJobs;
1179
+ },
1180
+ cancel(releaseId) {
1181
+ if (scheduledJobs.has(releaseId)) {
1182
+ scheduledJobs.get(releaseId).cancel();
1183
+ scheduledJobs.delete(releaseId);
1184
+ }
1185
+ return scheduledJobs;
1186
+ },
1187
+ getAll() {
1188
+ return scheduledJobs;
1189
+ },
1190
+ /**
1191
+ * On bootstrap, we can use this function to make sure to sync the scheduled jobs from the database that are not yet released
1192
+ * This is useful in case the server was restarted and the scheduled jobs were lost
1193
+ * This also could be used to sync different Strapi instances in case of a cluster
1194
+ */
1195
+ async syncFromDatabase() {
1196
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
1197
+ where: {
1198
+ scheduledAt: {
1199
+ $gte: /* @__PURE__ */ new Date()
1200
+ },
1201
+ releasedAt: null
1202
+ }
1203
+ });
1204
+ for (const release2 of releases) {
1205
+ this.set(release2.id, release2.scheduledAt);
1206
+ }
1207
+ return scheduledJobs;
1208
+ }
1209
+ };
1210
+ };
706
1211
  const services = {
707
1212
  release: createReleaseService,
708
- "release-validation": createReleaseValidationService
1213
+ "release-validation": createReleaseValidationService,
1214
+ scheduling: createSchedulingService
709
1215
  };
710
1216
  const RELEASE_SCHEMA = yup.object().shape({
711
- name: yup.string().trim().required()
1217
+ name: yup.string().trim().required(),
1218
+ scheduledAt: yup.string().nullable(),
1219
+ isScheduled: yup.boolean().optional(),
1220
+ time: yup.string().when("isScheduled", {
1221
+ is: true,
1222
+ then: yup.string().trim().required(),
1223
+ otherwise: yup.string().nullable()
1224
+ }),
1225
+ timezone: yup.string().when("isScheduled", {
1226
+ is: true,
1227
+ then: yup.string().required().nullable(),
1228
+ otherwise: yup.string().nullable()
1229
+ }),
1230
+ date: yup.string().when("isScheduled", {
1231
+ is: true,
1232
+ then: yup.string().required().nullable(),
1233
+ otherwise: yup.string().nullable()
1234
+ })
712
1235
  }).required().noUnknown();
713
1236
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
714
1237
  const releaseController = {
@@ -741,26 +1264,30 @@ const releaseController = {
741
1264
  }
742
1265
  };
743
1266
  });
744
- ctx.body = { data, meta: { pagination } };
1267
+ const pendingReleasesCount = await strapi.db.query(RELEASE_MODEL_UID).count({
1268
+ where: {
1269
+ releasedAt: null
1270
+ }
1271
+ });
1272
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
745
1273
  }
746
1274
  },
747
1275
  async findOne(ctx) {
748
1276
  const id = ctx.params.id;
749
1277
  const releaseService = getService("release", { strapi });
750
1278
  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);
1279
+ if (!release2) {
1280
+ throw new errors.NotFoundError(`Release not found for id: ${id}`);
1281
+ }
756
1282
  const count = await releaseService.countActions({
757
1283
  filters: {
758
1284
  release: id
759
1285
  }
760
1286
  });
761
- if (!release2) {
762
- throw new errors.NotFoundError(`Release not found for id: ${id}`);
763
- }
1287
+ const sanitizedRelease = {
1288
+ ...release2,
1289
+ createdBy: release2.createdBy ? strapi.admin.services.user.sanitizeUser(release2.createdBy) : null
1290
+ };
764
1291
  const data = {
765
1292
  ...sanitizedRelease,
766
1293
  actions: {
@@ -813,8 +1340,27 @@ const releaseController = {
813
1340
  const id = ctx.params.id;
814
1341
  const releaseService = getService("release", { strapi });
815
1342
  const release2 = await releaseService.publish(id, { user });
1343
+ const [countPublishActions, countUnpublishActions] = await Promise.all([
1344
+ releaseService.countActions({
1345
+ filters: {
1346
+ release: id,
1347
+ type: "publish"
1348
+ }
1349
+ }),
1350
+ releaseService.countActions({
1351
+ filters: {
1352
+ release: id,
1353
+ type: "unpublish"
1354
+ }
1355
+ })
1356
+ ]);
816
1357
  ctx.body = {
817
- data: release2
1358
+ data: release2,
1359
+ meta: {
1360
+ totalEntries: countPublishActions + countUnpublishActions,
1361
+ totalPublishedEntries: countPublishActions,
1362
+ totalUnpublishedEntries: countUnpublishActions
1363
+ }
818
1364
  };
819
1365
  }
820
1366
  };
@@ -841,6 +1387,38 @@ const releaseActionController = {
841
1387
  data: releaseAction2
842
1388
  };
843
1389
  },
1390
+ async createMany(ctx) {
1391
+ const releaseId = ctx.params.releaseId;
1392
+ const releaseActionsArgs = ctx.request.body;
1393
+ await Promise.all(
1394
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1395
+ );
1396
+ const releaseService = getService("release", { strapi });
1397
+ const releaseActions = await strapi.db.transaction(async () => {
1398
+ const releaseActions2 = await Promise.all(
1399
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1400
+ try {
1401
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1402
+ return action;
1403
+ } catch (error) {
1404
+ if (error instanceof AlreadyOnReleaseError) {
1405
+ return null;
1406
+ }
1407
+ throw error;
1408
+ }
1409
+ })
1410
+ );
1411
+ return releaseActions2;
1412
+ });
1413
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1414
+ ctx.body = {
1415
+ data: newReleaseActions,
1416
+ meta: {
1417
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1418
+ totalEntries: releaseActions.length
1419
+ }
1420
+ };
1421
+ },
844
1422
  async findMany(ctx) {
845
1423
  const releaseId = ctx.params.releaseId;
846
1424
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -864,7 +1442,7 @@ const releaseActionController = {
864
1442
  acc[action.contentType] = contentTypePermissionsManager.sanitizeOutput;
865
1443
  return acc;
866
1444
  }, {});
867
- const sanitizedResults = await mapAsync(results, async (action) => ({
1445
+ const sanitizedResults = await async.map(results, async (action) => ({
868
1446
  ...action,
869
1447
  entry: await contentTypeOutputSanitizers[action.contentType](action.entry)
870
1448
  }));
@@ -1026,6 +1604,22 @@ const releaseAction = {
1026
1604
  ]
1027
1605
  }
1028
1606
  },
1607
+ {
1608
+ method: "POST",
1609
+ path: "/:releaseId/actions/bulk",
1610
+ handler: "release-action.createMany",
1611
+ config: {
1612
+ policies: [
1613
+ "admin::isAuthenticatedAdmin",
1614
+ {
1615
+ name: "admin::hasPermissions",
1616
+ config: {
1617
+ actions: ["plugin::content-releases.create-action"]
1618
+ }
1619
+ }
1620
+ ]
1621
+ }
1622
+ },
1029
1623
  {
1030
1624
  method: "GET",
1031
1625
  path: "/:releaseId/actions",
@@ -1080,12 +1674,12 @@ const routes = {
1080
1674
  release,
1081
1675
  "release-action": releaseAction
1082
1676
  };
1083
- const { features } = require("@strapi/strapi/dist/utils/ee");
1084
1677
  const getPlugin = () => {
1085
- if (features.isEnabled("cms-content-releases")) {
1678
+ if (strapi.ee.features.isEnabled("cms-content-releases")) {
1086
1679
  return {
1087
1680
  register,
1088
1681
  bootstrap,
1682
+ destroy,
1089
1683
  contentTypes,
1090
1684
  services,
1091
1685
  controllers,
@@ -1093,6 +1687,9 @@ const getPlugin = () => {
1093
1687
  };
1094
1688
  }
1095
1689
  return {
1690
+ // Always return register, it handles its own feature check
1691
+ register,
1692
+ // Always return contentTypes to avoid losing data when the feature is disabled
1096
1693
  contentTypes
1097
1694
  };
1098
1695
  };