@strapi/content-releases 0.0.0-next.615ae85762cbae9fc80af36685075ef25abd1c88 → 0.0.0-next.68c52dc267ceb9c5b8183117142351a1b09176c5

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 (31) hide show
  1. package/dist/_chunks/{App-1hHIqUoZ.js → App-OP70yd5M.js} +597 -358
  2. package/dist/_chunks/App-OP70yd5M.js.map +1 -0
  3. package/dist/_chunks/{App-U6GbyLIE.mjs → App-x6Tjj3HN.mjs} +608 -370
  4. package/dist/_chunks/App-x6Tjj3HN.mjs.map +1 -0
  5. package/dist/_chunks/{PurchaseContentReleases-Clm0iACO.mjs → PurchaseContentReleases-3tRbmbY3.mjs} +2 -2
  6. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  7. package/dist/_chunks/{PurchaseContentReleases-YhAPgpG9.js → PurchaseContentReleases-bpIYXOfu.js} +2 -2
  8. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  9. package/dist/_chunks/{en-bDhIlw-B.js → en-3SGjiVyR.js} +22 -6
  10. package/dist/_chunks/en-3SGjiVyR.js.map +1 -0
  11. package/dist/_chunks/{en-GqXgfmzl.mjs → en-bpHsnU0n.mjs} +22 -6
  12. package/dist/_chunks/en-bpHsnU0n.mjs.map +1 -0
  13. package/dist/_chunks/{index-gkExFBa0.mjs → index-1ejXLtzt.mjs} +278 -25
  14. package/dist/_chunks/index-1ejXLtzt.mjs.map +1 -0
  15. package/dist/_chunks/{index-l-FvkQlQ.js → index-ydocdaZ0.js} +266 -13
  16. package/dist/_chunks/index-ydocdaZ0.js.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +2 -2
  19. package/dist/server/index.js +879 -447
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +878 -447
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +14 -12
  24. package/dist/_chunks/App-1hHIqUoZ.js.map +0 -1
  25. package/dist/_chunks/App-U6GbyLIE.mjs.map +0 -1
  26. package/dist/_chunks/PurchaseContentReleases-Clm0iACO.mjs.map +0 -1
  27. package/dist/_chunks/PurchaseContentReleases-YhAPgpG9.js.map +0 -1
  28. package/dist/_chunks/en-GqXgfmzl.mjs.map +0 -1
  29. package/dist/_chunks/en-bDhIlw-B.js.map +0 -1
  30. package/dist/_chunks/index-gkExFBa0.mjs.map +0 -1
  31. package/dist/_chunks/index-l-FvkQlQ.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { contentTypes as contentTypes$1, mapAsync, 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
5
  import EE from "@strapi/strapi/dist/utils/ee";
@@ -50,6 +51,32 @@ const ACTIONS = [
50
51
  pluginName: "content-releases"
51
52
  }
52
53
  ];
54
+ const ALLOWED_WEBHOOK_EVENTS = {
55
+ RELEASES_PUBLISH: "releases.publish"
56
+ };
57
+ const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
58
+ return strapi2.plugin("content-releases").service(name);
59
+ };
60
+ const getPopulatedEntry = async (contentTypeUid, entryId, { strapi: strapi2 } = { strapi: global.strapi }) => {
61
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
62
+ const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
63
+ const entry = await strapi2.entityService.findOne(contentTypeUid, entryId, { populate });
64
+ return entry;
65
+ };
66
+ const getEntryValidStatus = async (contentTypeUid, entry, { strapi: strapi2 } = { strapi: global.strapi }) => {
67
+ try {
68
+ await strapi2.entityValidator.validateEntityCreation(
69
+ strapi2.getModel(contentTypeUid),
70
+ entry,
71
+ void 0,
72
+ // @ts-expect-error - FIXME: entity here is unnecessary
73
+ entry
74
+ );
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ };
53
80
  async function deleteActionsOnDisableDraftAndPublish({
54
81
  oldContentTypes,
55
82
  contentTypes: contentTypes2
@@ -76,31 +103,196 @@ async function deleteActionsOnDeleteContentType({ oldContentTypes, contentTypes:
76
103
  });
77
104
  }
78
105
  }
106
+ async function migrateIsValidAndStatusReleases() {
107
+ const releasesWithoutStatus = await strapi.db.query(RELEASE_MODEL_UID).findMany({
108
+ where: {
109
+ status: null,
110
+ releasedAt: null
111
+ },
112
+ populate: {
113
+ actions: {
114
+ populate: {
115
+ entry: true
116
+ }
117
+ }
118
+ }
119
+ });
120
+ mapAsync(releasesWithoutStatus, async (release2) => {
121
+ const actions = release2.actions;
122
+ const notValidatedActions = actions.filter((action) => action.isEntryValid === null);
123
+ for (const action of notValidatedActions) {
124
+ if (action.entry) {
125
+ const populatedEntry = await getPopulatedEntry(action.contentType, action.entry.id, {
126
+ strapi
127
+ });
128
+ if (populatedEntry) {
129
+ const isEntryValid = getEntryValidStatus(action.contentType, populatedEntry, { strapi });
130
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
131
+ where: {
132
+ id: action.id
133
+ },
134
+ data: {
135
+ isEntryValid
136
+ }
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return getService("release", { strapi }).updateReleaseStatus(release2.id);
142
+ });
143
+ const publishedReleases = await strapi.db.query(RELEASE_MODEL_UID).findMany({
144
+ where: {
145
+ status: null,
146
+ releasedAt: {
147
+ $notNull: true
148
+ }
149
+ }
150
+ });
151
+ mapAsync(publishedReleases, async (release2) => {
152
+ return strapi.db.query(RELEASE_MODEL_UID).update({
153
+ where: {
154
+ id: release2.id
155
+ },
156
+ data: {
157
+ status: "done"
158
+ }
159
+ });
160
+ });
161
+ }
162
+ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: contentTypes2 }) {
163
+ if (oldContentTypes !== void 0 && contentTypes2 !== void 0) {
164
+ const contentTypesWithDraftAndPublish = Object.keys(oldContentTypes).filter(
165
+ (uid) => oldContentTypes[uid]?.options?.draftAndPublish
166
+ );
167
+ const releasesAffected = /* @__PURE__ */ new Set();
168
+ mapAsync(contentTypesWithDraftAndPublish, async (contentTypeUID) => {
169
+ const oldContentType = oldContentTypes[contentTypeUID];
170
+ const contentType = contentTypes2[contentTypeUID];
171
+ if (!isEqual(oldContentType?.attributes, contentType?.attributes)) {
172
+ const actions = await strapi.db.query(RELEASE_ACTION_MODEL_UID).findMany({
173
+ where: {
174
+ contentType: contentTypeUID
175
+ },
176
+ populate: {
177
+ entry: true,
178
+ release: true
179
+ }
180
+ });
181
+ await mapAsync(actions, async (action) => {
182
+ if (action.entry && action.release) {
183
+ const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
184
+ strapi
185
+ });
186
+ if (populatedEntry) {
187
+ const isEntryValid = await getEntryValidStatus(contentTypeUID, populatedEntry, {
188
+ strapi
189
+ });
190
+ releasesAffected.add(action.release.id);
191
+ await strapi.db.query(RELEASE_ACTION_MODEL_UID).update({
192
+ where: {
193
+ id: action.id
194
+ },
195
+ data: {
196
+ isEntryValid
197
+ }
198
+ });
199
+ }
200
+ }
201
+ });
202
+ }
203
+ }).then(() => {
204
+ mapAsync(releasesAffected, async (releaseId) => {
205
+ return getService("release", { strapi }).updateReleaseStatus(releaseId);
206
+ });
207
+ });
208
+ }
209
+ }
210
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
211
+ if (!oldContentTypes) {
212
+ return;
213
+ }
214
+ for (const uid in contentTypes2) {
215
+ if (!oldContentTypes[uid]) {
216
+ continue;
217
+ }
218
+ const oldContentType = oldContentTypes[uid];
219
+ const contentType = contentTypes2[uid];
220
+ const i18nPlugin = strapi.plugin("i18n");
221
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
222
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
223
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
224
+ locale: null
225
+ }).where({ contentType: uid }).execute();
226
+ }
227
+ }
228
+ }
229
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
230
+ if (!oldContentTypes) {
231
+ return;
232
+ }
233
+ for (const uid in contentTypes2) {
234
+ if (!oldContentTypes[uid]) {
235
+ continue;
236
+ }
237
+ const oldContentType = oldContentTypes[uid];
238
+ const contentType = contentTypes2[uid];
239
+ const i18nPlugin = strapi.plugin("i18n");
240
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
241
+ const { getDefaultLocale } = i18nPlugin.service("locales");
242
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
243
+ const defaultLocale = await getDefaultLocale();
244
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
245
+ locale: defaultLocale
246
+ }).where({ contentType: uid }).execute();
247
+ }
248
+ }
249
+ }
79
250
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
80
251
  const register = async ({ strapi: strapi2 }) => {
81
252
  if (features$2.isEnabled("cms-content-releases")) {
82
253
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
83
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
84
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType);
254
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
255
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
256
+ }
257
+ if (strapi2.plugin("graphql")) {
258
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
259
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
260
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
85
261
  }
86
- };
87
- const getService = (name, { strapi: strapi2 } = { strapi: global.strapi }) => {
88
- return strapi2.plugin("content-releases").service(name);
89
262
  };
90
263
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
91
264
  const bootstrap = async ({ strapi: strapi2 }) => {
92
265
  if (features$1.isEnabled("cms-content-releases")) {
266
+ const contentTypesWithDraftAndPublish = Object.keys(strapi2.contentTypes).filter(
267
+ (uid) => strapi2.contentTypes[uid]?.options?.draftAndPublish
268
+ );
93
269
  strapi2.db.lifecycles.subscribe({
94
- afterDelete(event) {
95
- const { model, result } = event;
96
- if (model.kind === "collectionType" && model.options?.draftAndPublish) {
97
- const { id } = result;
98
- strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
99
- where: {
100
- target_type: model.uid,
101
- 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);
102
292
  }
103
- });
293
+ }
294
+ } catch (error) {
295
+ strapi2.log.error("Error while deleting release actions after entry delete", { error });
104
296
  }
105
297
  },
106
298
  /**
@@ -120,38 +312,94 @@ const bootstrap = async ({ strapi: strapi2 }) => {
120
312
  * We make this only after deleteMany is succesfully executed to avoid errors
121
313
  */
122
314
  async afterDeleteMany(event) {
123
- const { model, state } = event;
124
- const entriesToDelete = state.entriesToDelete;
125
- if (entriesToDelete) {
126
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
127
- where: {
128
- target_type: model.uid,
129
- target_id: {
130
- $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(
325
+ (entry) => entry.id
326
+ )
327
+ }
328
+ }
131
329
  }
330
+ });
331
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
332
+ where: {
333
+ target_type: model.uid,
334
+ target_id: {
335
+ $in: entriesToDelete.map((entry) => entry.id)
336
+ }
337
+ }
338
+ });
339
+ for (const release2 of releases) {
340
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
132
341
  }
342
+ }
343
+ } catch (error) {
344
+ strapi2.log.error("Error while deleting release actions after entry deleteMany", {
345
+ error
133
346
  });
134
347
  }
348
+ },
349
+ async afterUpdate(event) {
350
+ try {
351
+ const { model, result } = event;
352
+ if (model.kind === "collectionType" && model.options?.draftAndPublish) {
353
+ const isEntryValid = await getEntryValidStatus(
354
+ model.uid,
355
+ result,
356
+ {
357
+ strapi: strapi2
358
+ }
359
+ );
360
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
361
+ where: {
362
+ target_type: model.uid,
363
+ target_id: result.id
364
+ },
365
+ data: {
366
+ isEntryValid
367
+ }
368
+ });
369
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
370
+ where: {
371
+ actions: {
372
+ target_type: model.uid,
373
+ target_id: result.id
374
+ }
375
+ }
376
+ });
377
+ for (const release2 of releases) {
378
+ getService("release", { strapi: strapi2 }).updateReleaseStatus(release2.id);
379
+ }
380
+ }
381
+ } catch (error) {
382
+ strapi2.log.error("Error while updating release actions after entry update", { error });
383
+ }
135
384
  }
136
385
  });
137
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
138
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
139
- strapi2.log.error(
140
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
141
- );
142
- throw err;
143
- });
144
- }
386
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
387
+ strapi2.log.error(
388
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
389
+ );
390
+ throw err;
391
+ });
392
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
393
+ strapi2.webhookStore.addAllowedEvent(key, value);
394
+ });
145
395
  }
146
396
  };
147
397
  const destroy = async ({ strapi: strapi2 }) => {
148
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
149
- const scheduledJobs = getService("scheduling", {
150
- strapi: strapi2
151
- }).getAll();
152
- for (const [, job] of scheduledJobs) {
153
- job.cancel();
154
- }
398
+ const scheduledJobs = getService("scheduling", {
399
+ strapi: strapi2
400
+ }).getAll();
401
+ for (const [, job] of scheduledJobs) {
402
+ job.cancel();
155
403
  }
156
404
  };
157
405
  const schema$1 = {
@@ -186,6 +434,11 @@ const schema$1 = {
186
434
  timezone: {
187
435
  type: "string"
188
436
  },
437
+ status: {
438
+ type: "enumeration",
439
+ enum: ["ready", "blocked", "failed", "done", "empty"],
440
+ required: true
441
+ },
189
442
  actions: {
190
443
  type: "relation",
191
444
  relation: "oneToMany",
@@ -238,6 +491,9 @@ const schema = {
238
491
  relation: "manyToOne",
239
492
  target: RELEASE_MODEL_UID,
240
493
  inversedBy: "actions"
494
+ },
495
+ isEntryValid: {
496
+ type: "boolean"
241
497
  }
242
498
  }
243
499
  };
@@ -260,356 +516,89 @@ const getGroupName = (queryValue) => {
260
516
  return "contentType.displayName";
261
517
  }
262
518
  };
263
- const createReleaseService = ({ strapi: strapi2 }) => ({
264
- async create(releaseData, { user }) {
265
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
266
- const {
267
- validatePendingReleasesLimit,
268
- validateUniqueNameForPendingRelease,
269
- validateScheduledAtIsLaterThanNow
270
- } = getService("release-validation", { strapi: strapi2 });
271
- await Promise.all([
272
- validatePendingReleasesLimit(),
273
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
274
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
275
- ]);
276
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
277
- data: releaseWithCreatorFields
519
+ const createReleaseService = ({ strapi: strapi2 }) => {
520
+ const dispatchWebhook = (event, { isPublished, release: release2, error }) => {
521
+ strapi2.eventHub.emit(event, {
522
+ isPublished,
523
+ error,
524
+ release: release2
278
525
  });
279
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
280
- const schedulingService = getService("scheduling", { strapi: strapi2 });
281
- await schedulingService.set(release2.id, release2.scheduledAt);
282
- }
283
- strapi2.telemetry.send("didCreateContentRelease");
284
- return release2;
285
- },
286
- async findOne(id, query = {}) {
287
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
288
- ...query
289
- });
290
- return release2;
291
- },
292
- findPage(query) {
293
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
294
- ...query,
295
- populate: {
296
- actions: {
297
- // @ts-expect-error Ignore missing properties
298
- count: true
299
- }
300
- }
301
- });
302
- },
303
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
304
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
305
- where: {
306
- actions: {
307
- target_type: contentTypeUid,
308
- target_id: entryId
309
- },
310
- releasedAt: {
311
- $null: true
312
- }
313
- },
314
- populate: {
315
- // Filter the action to get only the content type entry
316
- actions: {
317
- where: {
318
- target_type: contentTypeUid,
319
- target_id: entryId
320
- }
321
- }
322
- }
323
- });
324
- return releases.map((release2) => {
325
- if (release2.actions?.length) {
326
- const [actionForEntry] = release2.actions;
327
- delete release2.actions;
328
- return {
329
- ...release2,
330
- action: actionForEntry
331
- };
332
- }
333
- return release2;
334
- });
335
- },
336
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
337
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
338
- where: {
339
- releasedAt: {
340
- $null: true
341
- },
342
- actions: {
343
- target_type: contentTypeUid,
344
- target_id: entryId
345
- }
346
- }
347
- });
348
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
349
- where: {
350
- $or: [
351
- {
352
- id: {
353
- $notIn: releasesRelated.map((release2) => release2.id)
354
- }
355
- },
356
- {
357
- actions: null
358
- }
359
- ],
360
- releasedAt: {
361
- $null: true
362
- }
363
- }
364
- });
365
- return releases.map((release2) => {
366
- if (release2.actions?.length) {
367
- const [actionForEntry] = release2.actions;
368
- delete release2.actions;
369
- return {
370
- ...release2,
371
- action: actionForEntry
372
- };
526
+ };
527
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
528
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
529
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
530
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
531
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
532
+ try {
533
+ if (actionType === "publish") {
534
+ await entityManagerService.publish(entry, uid);
535
+ } else {
536
+ await entityManagerService.unpublish(entry, uid);
373
537
  }
374
- return release2;
375
- });
376
- },
377
- async update(id, releaseData, { user }) {
378
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
379
- const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
380
- "release-validation",
381
- { strapi: strapi2 }
382
- );
383
- await Promise.all([
384
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
385
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
386
- ]);
387
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
388
- if (!release2) {
389
- throw new errors.NotFoundError(`No release found for id ${id}`);
390
- }
391
- if (release2.releasedAt) {
392
- throw new errors.ValidationError("Release already published");
393
- }
394
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
395
- /*
396
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
397
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
398
- */
399
- // @ts-expect-error see above
400
- data: releaseWithCreatorFields
401
- });
402
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
403
- const schedulingService = getService("scheduling", { strapi: strapi2 });
404
- if (releaseData.scheduledAt) {
405
- await schedulingService.set(id, releaseData.scheduledAt);
406
- } else if (release2.scheduledAt) {
407
- schedulingService.cancel(id);
538
+ } catch (error) {
539
+ if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
540
+ ;
541
+ else {
542
+ throw error;
408
543
  }
409
544
  }
410
- strapi2.telemetry.send("didUpdateContentRelease");
411
- return updatedRelease;
412
- },
413
- async createAction(releaseId, action) {
414
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
415
- strapi: strapi2
416
- });
417
- await Promise.all([
418
- validateEntryContentType(action.entry.contentType),
419
- validateUniqueEntry(releaseId, action)
420
- ]);
421
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
422
- if (!release2) {
423
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
424
- }
425
- if (release2.releasedAt) {
426
- throw new errors.ValidationError("Release already published");
427
- }
428
- const { entry, type } = action;
429
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
430
- data: {
431
- type,
432
- contentType: entry.contentType,
433
- locale: entry.locale,
434
- entry: {
435
- id: entry.id,
436
- __type: entry.contentType,
437
- __pivot: { field: "entry" }
438
- },
439
- release: releaseId
440
- },
441
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
442
- });
443
- },
444
- async findActions(releaseId, query) {
445
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
446
- fields: ["id"]
447
- });
448
- if (!release2) {
449
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
450
- }
451
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
452
- ...query,
453
- populate: {
454
- entry: {
455
- populate: "*"
545
+ };
546
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
547
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
548
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
549
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
550
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
551
+ filters: {
552
+ id: {
553
+ $in: entriesToPublishIds
456
554
  }
457
555
  },
458
- filters: {
459
- release: releaseId
460
- }
556
+ populate
461
557
  });
462
- },
463
- async countActions(query) {
464
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
465
- },
466
- async groupActions(actions, groupBy) {
467
- const contentTypeUids = actions.reduce((acc, action) => {
468
- if (!acc.includes(action.contentType)) {
469
- acc.push(action.contentType);
470
- }
471
- return acc;
472
- }, []);
473
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
474
- contentTypeUids
475
- );
476
- const allLocalesDictionary = await this.getLocalesDataForActions();
477
- const formattedData = actions.map((action) => {
478
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
479
- return {
480
- ...action,
481
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
482
- contentType: {
483
- displayName,
484
- mainFieldValue: action.entry[mainField],
485
- uid: action.contentType
558
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
559
+ filters: {
560
+ id: {
561
+ $in: entriestoUnpublishIds
486
562
  }
487
- };
563
+ },
564
+ populate
488
565
  });
489
- const groupName = getGroupName(groupBy);
490
- return _.groupBy(groupName)(formattedData);
491
- },
492
- async getLocalesDataForActions() {
493
- if (!strapi2.plugin("i18n")) {
494
- return {};
566
+ if (entriesToPublish.length > 0) {
567
+ await entityManagerService.publishMany(entriesToPublish, uid);
495
568
  }
496
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
497
- return allLocales.reduce((acc, locale) => {
498
- acc[locale.code] = { name: locale.name, code: locale.code };
499
- return acc;
500
- }, {});
501
- },
502
- async getContentTypesDataForActions(contentTypesUids) {
503
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
504
- const contentTypesData = {};
505
- for (const contentTypeUid of contentTypesUids) {
506
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
507
- uid: contentTypeUid
508
- });
509
- contentTypesData[contentTypeUid] = {
510
- mainField: contentTypeConfig.settings.mainField,
511
- displayName: strapi2.getModel(contentTypeUid).info.displayName
512
- };
569
+ if (entriesToUnpublish.length > 0) {
570
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
513
571
  }
514
- return contentTypesData;
515
- },
516
- getContentTypeModelsFromActions(actions) {
517
- const contentTypeUids = actions.reduce((acc, action) => {
518
- if (!acc.includes(action.contentType)) {
519
- acc.push(action.contentType);
520
- }
521
- return acc;
522
- }, []);
523
- const contentTypeModelsMap = contentTypeUids.reduce(
524
- (acc, contentTypeUid) => {
525
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
526
- return acc;
527
- },
528
- {}
529
- );
530
- return contentTypeModelsMap;
531
- },
532
- async getAllComponents() {
533
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
534
- const components = await contentManagerComponentsService.findAllComponents();
535
- const componentsMap = components.reduce(
536
- (acc, component) => {
537
- acc[component.uid] = component;
538
- return acc;
572
+ };
573
+ const getFormattedActions = async (releaseId) => {
574
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
575
+ where: {
576
+ release: {
577
+ id: releaseId
578
+ }
539
579
  },
540
- {}
541
- );
542
- return componentsMap;
543
- },
544
- async delete(releaseId) {
545
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
546
580
  populate: {
547
- actions: {
581
+ entry: {
548
582
  fields: ["id"]
549
583
  }
550
584
  }
551
585
  });
552
- if (!release2) {
553
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
554
- }
555
- if (release2.releasedAt) {
556
- throw new errors.ValidationError("Release already published");
557
- }
558
- await strapi2.db.transaction(async () => {
559
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
560
- where: {
561
- id: {
562
- $in: release2.actions.map((action) => action.id)
563
- }
564
- }
565
- });
566
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
567
- });
568
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
569
- const schedulingService = getService("scheduling", { strapi: strapi2 });
570
- await schedulingService.cancel(release2.id);
571
- }
572
- strapi2.telemetry.send("didDeleteContentRelease");
573
- return release2;
574
- },
575
- async publish(releaseId) {
576
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
577
- RELEASE_MODEL_UID,
578
- releaseId,
579
- {
580
- populate: {
581
- actions: {
582
- populate: {
583
- entry: {
584
- fields: ["id"]
585
- }
586
- }
587
- }
588
- }
589
- }
590
- );
591
- if (!releaseWithPopulatedActionEntries) {
592
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
593
- }
594
- if (releaseWithPopulatedActionEntries.releasedAt) {
595
- throw new errors.ValidationError("Release already published");
596
- }
597
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
586
+ if (actions.length === 0) {
598
587
  throw new errors.ValidationError("No entries to publish");
599
588
  }
600
589
  const collectionTypeActions = {};
601
590
  const singleTypeActions = [];
602
- for (const action of releaseWithPopulatedActionEntries.actions) {
591
+ for (const action of actions) {
603
592
  const contentTypeUid = action.contentType;
604
593
  if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
605
594
  if (!collectionTypeActions[contentTypeUid]) {
606
595
  collectionTypeActions[contentTypeUid] = {
607
- entriestoPublishIds: [],
596
+ entriesToPublishIds: [],
608
597
  entriesToUnpublishIds: []
609
598
  };
610
599
  }
611
600
  if (action.type === "publish") {
612
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
601
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
613
602
  } else {
614
603
  collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
615
604
  }
@@ -621,111 +610,487 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
621
610
  });
622
611
  }
623
612
  }
624
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
625
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
626
- await strapi2.db.transaction(async () => {
627
- for (const { uid, action, id } of singleTypeActions) {
628
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
629
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
630
- try {
631
- if (action === "publish") {
632
- await entityManagerService.publish(entry, uid);
633
- } else {
634
- await entityManagerService.unpublish(entry, uid);
613
+ return { collectionTypeActions, singleTypeActions };
614
+ };
615
+ return {
616
+ async create(releaseData, { user }) {
617
+ const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
618
+ const {
619
+ validatePendingReleasesLimit,
620
+ validateUniqueNameForPendingRelease,
621
+ validateScheduledAtIsLaterThanNow
622
+ } = getService("release-validation", { strapi: strapi2 });
623
+ await Promise.all([
624
+ validatePendingReleasesLimit(),
625
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
626
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
627
+ ]);
628
+ const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
629
+ data: {
630
+ ...releaseWithCreatorFields,
631
+ status: "empty"
632
+ }
633
+ });
634
+ if (releaseWithCreatorFields.scheduledAt) {
635
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
636
+ await schedulingService.set(release2.id, release2.scheduledAt);
637
+ }
638
+ strapi2.telemetry.send("didCreateContentRelease");
639
+ return release2;
640
+ },
641
+ async findOne(id, query = {}) {
642
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
643
+ ...query
644
+ });
645
+ return release2;
646
+ },
647
+ findPage(query) {
648
+ return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
649
+ ...query,
650
+ populate: {
651
+ actions: {
652
+ // @ts-expect-error Ignore missing properties
653
+ count: true
635
654
  }
636
- } catch (error) {
637
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
638
- ;
639
- else {
640
- throw error;
655
+ }
656
+ });
657
+ },
658
+ async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
659
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
660
+ where: {
661
+ actions: {
662
+ target_type: contentTypeUid,
663
+ target_id: entryId
664
+ },
665
+ releasedAt: {
666
+ $null: true
667
+ }
668
+ },
669
+ populate: {
670
+ // Filter the action to get only the content type entry
671
+ actions: {
672
+ where: {
673
+ target_type: contentTypeUid,
674
+ target_id: entryId
675
+ }
641
676
  }
642
677
  }
643
- }
644
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
645
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
646
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
647
- const entriesToPublish = await strapi2.entityService.findMany(
648
- contentTypeUid,
649
- {
650
- filters: {
651
- id: {
652
- $in: entriestoPublishIds
653
- }
654
- },
655
- populate
678
+ });
679
+ return releases.map((release2) => {
680
+ if (release2.actions?.length) {
681
+ const [actionForEntry] = release2.actions;
682
+ delete release2.actions;
683
+ return {
684
+ ...release2,
685
+ action: actionForEntry
686
+ };
687
+ }
688
+ return release2;
689
+ });
690
+ },
691
+ async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
692
+ const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
693
+ where: {
694
+ releasedAt: {
695
+ $null: true
696
+ },
697
+ actions: {
698
+ target_type: contentTypeUid,
699
+ target_id: entryId
656
700
  }
657
- );
658
- const entriesToUnpublish = await strapi2.entityService.findMany(
659
- contentTypeUid,
660
- {
661
- filters: {
701
+ }
702
+ });
703
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
704
+ where: {
705
+ $or: [
706
+ {
662
707
  id: {
663
- $in: entriesToUnpublishIds
708
+ $notIn: releasesRelated.map((release2) => release2.id)
664
709
  }
665
710
  },
666
- populate
711
+ {
712
+ actions: null
713
+ }
714
+ ],
715
+ releasedAt: {
716
+ $null: true
667
717
  }
668
- );
669
- if (entriesToPublish.length > 0) {
670
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
671
718
  }
672
- if (entriesToUnpublish.length > 0) {
673
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
719
+ });
720
+ return releases.map((release2) => {
721
+ if (release2.actions?.length) {
722
+ const [actionForEntry] = release2.actions;
723
+ delete release2.actions;
724
+ return {
725
+ ...release2,
726
+ action: actionForEntry
727
+ };
674
728
  }
729
+ return release2;
730
+ });
731
+ },
732
+ async update(id, releaseData, { user }) {
733
+ const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(
734
+ releaseData
735
+ );
736
+ const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
737
+ "release-validation",
738
+ { strapi: strapi2 }
739
+ );
740
+ await Promise.all([
741
+ validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
742
+ validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
743
+ ]);
744
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
745
+ if (!release2) {
746
+ throw new errors.NotFoundError(`No release found for id ${id}`);
675
747
  }
676
- });
677
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
678
- data: {
748
+ if (release2.releasedAt) {
749
+ throw new errors.ValidationError("Release already published");
750
+ }
751
+ const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
679
752
  /*
680
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
753
+ * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
754
+ * is not compatible with the type we are passing here: UpdateRelease.Request['body']
681
755
  */
682
756
  // @ts-expect-error see above
683
- releasedAt: /* @__PURE__ */ new Date()
757
+ data: releaseWithCreatorFields
758
+ });
759
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
760
+ if (releaseData.scheduledAt) {
761
+ await schedulingService.set(id, releaseData.scheduledAt);
762
+ } else if (release2.scheduledAt) {
763
+ schedulingService.cancel(id);
684
764
  }
685
- });
686
- strapi2.telemetry.send("didPublishContentRelease");
687
- return release2;
688
- },
689
- async updateAction(actionId, releaseId, update) {
690
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
691
- where: {
692
- id: actionId,
693
- release: {
694
- id: releaseId,
695
- releasedAt: {
696
- $null: true
765
+ this.updateReleaseStatus(id);
766
+ strapi2.telemetry.send("didUpdateContentRelease");
767
+ return updatedRelease;
768
+ },
769
+ async createAction(releaseId, action) {
770
+ const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
771
+ strapi: strapi2
772
+ });
773
+ await Promise.all([
774
+ validateEntryContentType(action.entry.contentType),
775
+ validateUniqueEntry(releaseId, action)
776
+ ]);
777
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
778
+ if (!release2) {
779
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
780
+ }
781
+ if (release2.releasedAt) {
782
+ throw new errors.ValidationError("Release already published");
783
+ }
784
+ const { entry, type } = action;
785
+ const populatedEntry = await getPopulatedEntry(entry.contentType, entry.id, { strapi: strapi2 });
786
+ const isEntryValid = await getEntryValidStatus(entry.contentType, populatedEntry, { strapi: strapi2 });
787
+ const releaseAction2 = await strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
788
+ data: {
789
+ type,
790
+ contentType: entry.contentType,
791
+ locale: entry.locale,
792
+ isEntryValid,
793
+ entry: {
794
+ id: entry.id,
795
+ __type: entry.contentType,
796
+ __pivot: { field: "entry" }
797
+ },
798
+ release: releaseId
799
+ },
800
+ populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
801
+ });
802
+ this.updateReleaseStatus(releaseId);
803
+ return releaseAction2;
804
+ },
805
+ async findActions(releaseId, query) {
806
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
807
+ fields: ["id"]
808
+ });
809
+ if (!release2) {
810
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
811
+ }
812
+ return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
813
+ ...query,
814
+ populate: {
815
+ entry: {
816
+ populate: "*"
697
817
  }
818
+ },
819
+ filters: {
820
+ release: releaseId
698
821
  }
699
- },
700
- data: update
701
- });
702
- if (!updatedAction) {
703
- throw new errors.NotFoundError(
704
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
822
+ });
823
+ },
824
+ async countActions(query) {
825
+ return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
826
+ },
827
+ async groupActions(actions, groupBy) {
828
+ const contentTypeUids = actions.reduce((acc, action) => {
829
+ if (!acc.includes(action.contentType)) {
830
+ acc.push(action.contentType);
831
+ }
832
+ return acc;
833
+ }, []);
834
+ const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
835
+ contentTypeUids
705
836
  );
706
- }
707
- return updatedAction;
708
- },
709
- async deleteAction(actionId, releaseId) {
710
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
711
- where: {
712
- id: actionId,
713
- release: {
714
- id: releaseId,
715
- releasedAt: {
716
- $null: true
837
+ const allLocalesDictionary = await this.getLocalesDataForActions();
838
+ const formattedData = actions.map((action) => {
839
+ const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
840
+ return {
841
+ ...action,
842
+ locale: action.locale ? allLocalesDictionary[action.locale] : null,
843
+ contentType: {
844
+ displayName,
845
+ mainFieldValue: action.entry[mainField],
846
+ uid: action.contentType
717
847
  }
718
- }
848
+ };
849
+ });
850
+ const groupName = getGroupName(groupBy);
851
+ return _.groupBy(groupName)(formattedData);
852
+ },
853
+ async getLocalesDataForActions() {
854
+ if (!strapi2.plugin("i18n")) {
855
+ return {};
719
856
  }
720
- });
721
- if (!deletedAction) {
722
- throw new errors.NotFoundError(
723
- `Action with id ${actionId} not found in release with id ${releaseId} or it is already published`
857
+ const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
858
+ return allLocales.reduce((acc, locale) => {
859
+ acc[locale.code] = { name: locale.name, code: locale.code };
860
+ return acc;
861
+ }, {});
862
+ },
863
+ async getContentTypesDataForActions(contentTypesUids) {
864
+ const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
865
+ const contentTypesData = {};
866
+ for (const contentTypeUid of contentTypesUids) {
867
+ const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
868
+ uid: contentTypeUid
869
+ });
870
+ contentTypesData[contentTypeUid] = {
871
+ mainField: contentTypeConfig.settings.mainField,
872
+ displayName: strapi2.getModel(contentTypeUid).info.displayName
873
+ };
874
+ }
875
+ return contentTypesData;
876
+ },
877
+ getContentTypeModelsFromActions(actions) {
878
+ const contentTypeUids = actions.reduce((acc, action) => {
879
+ if (!acc.includes(action.contentType)) {
880
+ acc.push(action.contentType);
881
+ }
882
+ return acc;
883
+ }, []);
884
+ const contentTypeModelsMap = contentTypeUids.reduce(
885
+ (acc, contentTypeUid) => {
886
+ acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
887
+ return acc;
888
+ },
889
+ {}
724
890
  );
891
+ return contentTypeModelsMap;
892
+ },
893
+ async getAllComponents() {
894
+ const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
895
+ const components = await contentManagerComponentsService.findAllComponents();
896
+ const componentsMap = components.reduce(
897
+ (acc, component) => {
898
+ acc[component.uid] = component;
899
+ return acc;
900
+ },
901
+ {}
902
+ );
903
+ return componentsMap;
904
+ },
905
+ async delete(releaseId) {
906
+ const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
907
+ populate: {
908
+ actions: {
909
+ fields: ["id"]
910
+ }
911
+ }
912
+ });
913
+ if (!release2) {
914
+ throw new errors.NotFoundError(`No release found for id ${releaseId}`);
915
+ }
916
+ if (release2.releasedAt) {
917
+ throw new errors.ValidationError("Release already published");
918
+ }
919
+ await strapi2.db.transaction(async () => {
920
+ await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
921
+ where: {
922
+ id: {
923
+ $in: release2.actions.map((action) => action.id)
924
+ }
925
+ }
926
+ });
927
+ await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
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");
947
+ }
948
+ if (lockedRelease.status === "failed") {
949
+ throw new errors.ValidationError("Release failed to publish");
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) {
999
+ throw error;
1000
+ }
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
+ );
1020
+ }
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
+ }
1032
+ }
1033
+ }
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
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
+ });
1067
+ }
1068
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1069
+ where: {
1070
+ id: releaseId
1071
+ },
1072
+ data: {
1073
+ status: "ready"
1074
+ }
1075
+ });
1076
+ }
1077
+ return strapi2.db.query(RELEASE_MODEL_UID).update({
1078
+ where: {
1079
+ id: releaseId
1080
+ },
1081
+ data: {
1082
+ status: "empty"
1083
+ }
1084
+ });
725
1085
  }
726
- return deletedAction;
1086
+ };
1087
+ };
1088
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1089
+ constructor(message) {
1090
+ super(message);
1091
+ this.name = "AlreadyOnReleaseError";
727
1092
  }
728
- });
1093
+ }
729
1094
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
730
1095
  async validateUniqueEntry(releaseId, releaseActionArgs) {
731
1096
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -738,7 +1103,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
738
1103
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
739
1104
  );
740
1105
  if (isEntryInRelease) {
741
- throw new errors.ValidationError(
1106
+ throw new AlreadyOnReleaseError(
742
1107
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
743
1108
  );
744
1109
  }
@@ -846,15 +1211,25 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
846
1211
  const services = {
847
1212
  release: createReleaseService,
848
1213
  "release-validation": createReleaseValidationService,
849
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1214
+ scheduling: createSchedulingService
850
1215
  };
851
1216
  const RELEASE_SCHEMA = yup.object().shape({
852
1217
  name: yup.string().trim().required(),
853
- // scheduledAt is a date, but we always receive strings from the client
854
1218
  scheduledAt: yup.string().nullable(),
855
- timezone: yup.string().when("scheduledAt", {
856
- is: (scheduledAt) => !!scheduledAt,
857
- then: yup.string().required(),
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(),
858
1233
  otherwise: yup.string().nullable()
859
1234
  })
860
1235
  }).required().noUnknown();
@@ -889,7 +1264,12 @@ const releaseController = {
889
1264
  }
890
1265
  };
891
1266
  });
892
- ctx.body = { data, meta: { pagination } };
1267
+ const pendingReleasesCount = await strapi.query(RELEASE_MODEL_UID).count({
1268
+ where: {
1269
+ releasedAt: null
1270
+ }
1271
+ });
1272
+ ctx.body = { data, meta: { pagination, pendingReleasesCount } };
893
1273
  }
894
1274
  },
895
1275
  async findOne(ctx) {
@@ -1007,6 +1387,38 @@ const releaseActionController = {
1007
1387
  data: releaseAction2
1008
1388
  };
1009
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
+ },
1010
1422
  async findMany(ctx) {
1011
1423
  const releaseId = ctx.params.releaseId;
1012
1424
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1192,6 +1604,22 @@ const releaseAction = {
1192
1604
  ]
1193
1605
  }
1194
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
+ },
1195
1623
  {
1196
1624
  method: "GET",
1197
1625
  path: "/:releaseId/actions",
@@ -1260,6 +1688,9 @@ const getPlugin = () => {
1260
1688
  };
1261
1689
  }
1262
1690
  return {
1691
+ // Always return register, it handles its own feature check
1692
+ register,
1693
+ // Always return contentTypes to avoid losing data when the feature is disabled
1263
1694
  contentTypes
1264
1695
  };
1265
1696
  };