@strapi/content-releases 0.0.0-next.90a86f595c31de9a89f4255318bfb0cccb30ceed → 0.0.0-next.9bff6e6b8e2a2c7445d3650f1b3459f1b0366db8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/_chunks/{App-pspKUC-W.js → App-c5uGEz9O.js} +596 -355
  2. package/dist/_chunks/App-c5uGEz9O.js.map +1 -0
  3. package/dist/_chunks/{App-8FCxPK8-.mjs → App-xQ5ljY7-.mjs} +606 -366
  4. package/dist/_chunks/App-xQ5ljY7-.mjs.map +1 -0
  5. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs +51 -0
  6. package/dist/_chunks/PurchaseContentReleases-3tRbmbY3.mjs.map +1 -0
  7. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js +51 -0
  8. package/dist/_chunks/PurchaseContentReleases-bpIYXOfu.js.map +1 -0
  9. package/dist/_chunks/{en-r9YocBH0.js → en-3SGjiVyR.js} +23 -5
  10. package/dist/_chunks/en-3SGjiVyR.js.map +1 -0
  11. package/dist/_chunks/{en-m9eTk4UF.mjs → en-bpHsnU0n.mjs} +23 -5
  12. package/dist/_chunks/en-bpHsnU0n.mjs.map +1 -0
  13. package/dist/_chunks/{index-nGaPcY9m.js → index-4U0Q_Fgd.js} +279 -12
  14. package/dist/_chunks/index-4U0Q_Fgd.js.map +1 -0
  15. package/dist/_chunks/{index-8aK7GzI5.mjs → index-ifoPtgmH.mjs} +291 -24
  16. package/dist/_chunks/index-ifoPtgmH.mjs.map +1 -0
  17. package/dist/admin/index.js +1 -1
  18. package/dist/admin/index.mjs +2 -2
  19. package/dist/server/index.js +885 -437
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/index.mjs +884 -437
  22. package/dist/server/index.mjs.map +1 -1
  23. package/package.json +13 -11
  24. package/dist/_chunks/App-8FCxPK8-.mjs.map +0 -1
  25. package/dist/_chunks/App-pspKUC-W.js.map +0 -1
  26. package/dist/_chunks/en-m9eTk4UF.mjs.map +0 -1
  27. package/dist/_chunks/en-r9YocBH0.js.map +0 -1
  28. package/dist/_chunks/index-8aK7GzI5.mjs.map +0 -1
  29. package/dist/_chunks/index-nGaPcY9m.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 = {
@@ -183,6 +431,14 @@ const schema$1 = {
183
431
  scheduledAt: {
184
432
  type: "datetime"
185
433
  },
434
+ timezone: {
435
+ type: "string"
436
+ },
437
+ status: {
438
+ type: "enumeration",
439
+ enum: ["ready", "blocked", "failed", "done", "empty"],
440
+ required: true
441
+ },
186
442
  actions: {
187
443
  type: "relation",
188
444
  relation: "oneToMany",
@@ -235,6 +491,9 @@ const schema = {
235
491
  relation: "manyToOne",
236
492
  target: RELEASE_MODEL_UID,
237
493
  inversedBy: "actions"
494
+ },
495
+ isEntryValid: {
496
+ type: "boolean"
238
497
  }
239
498
  }
240
499
  };
@@ -257,349 +516,89 @@ const getGroupName = (queryValue) => {
257
516
  return "contentType.displayName";
258
517
  }
259
518
  };
260
- const createReleaseService = ({ strapi: strapi2 }) => ({
261
- async create(releaseData, { user }) {
262
- const releaseWithCreatorFields = await setCreatorFields({ user })(releaseData);
263
- const {
264
- validatePendingReleasesLimit,
265
- validateUniqueNameForPendingRelease,
266
- validateScheduledAtIsLaterThanNow
267
- } = getService("release-validation", { strapi: strapi2 });
268
- await Promise.all([
269
- validatePendingReleasesLimit(),
270
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name),
271
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
272
- ]);
273
- const release2 = await strapi2.entityService.create(RELEASE_MODEL_UID, {
274
- data: releaseWithCreatorFields
275
- });
276
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
277
- const schedulingService = getService("scheduling", { strapi: strapi2 });
278
- await schedulingService.set(release2.id, release2.scheduledAt);
279
- }
280
- return release2;
281
- },
282
- async findOne(id, query = {}) {
283
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id, {
284
- ...query
285
- });
286
- return release2;
287
- },
288
- findPage(query) {
289
- return strapi2.entityService.findPage(RELEASE_MODEL_UID, {
290
- ...query,
291
- populate: {
292
- actions: {
293
- // @ts-expect-error Ignore missing properties
294
- count: true
295
- }
296
- }
297
- });
298
- },
299
- async findManyWithContentTypeEntryAttached(contentTypeUid, entryId) {
300
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
301
- where: {
302
- actions: {
303
- target_type: contentTypeUid,
304
- target_id: entryId
305
- },
306
- releasedAt: {
307
- $null: true
308
- }
309
- },
310
- populate: {
311
- // Filter the action to get only the content type entry
312
- actions: {
313
- where: {
314
- target_type: contentTypeUid,
315
- target_id: entryId
316
- }
317
- }
318
- }
319
- });
320
- return releases.map((release2) => {
321
- if (release2.actions?.length) {
322
- const [actionForEntry] = release2.actions;
323
- delete release2.actions;
324
- return {
325
- ...release2,
326
- action: actionForEntry
327
- };
328
- }
329
- return release2;
330
- });
331
- },
332
- async findManyWithoutContentTypeEntryAttached(contentTypeUid, entryId) {
333
- const releasesRelated = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
334
- where: {
335
- releasedAt: {
336
- $null: true
337
- },
338
- actions: {
339
- target_type: contentTypeUid,
340
- target_id: entryId
341
- }
342
- }
343
- });
344
- const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
345
- where: {
346
- $or: [
347
- {
348
- id: {
349
- $notIn: releasesRelated.map((release2) => release2.id)
350
- }
351
- },
352
- {
353
- actions: null
354
- }
355
- ],
356
- releasedAt: {
357
- $null: true
358
- }
359
- }
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
360
525
  });
361
- return releases.map((release2) => {
362
- if (release2.actions?.length) {
363
- const [actionForEntry] = release2.actions;
364
- delete release2.actions;
365
- return {
366
- ...release2,
367
- action: actionForEntry
368
- };
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);
369
537
  }
370
- return release2;
371
- });
372
- },
373
- async update(id, releaseData, { user }) {
374
- const releaseWithCreatorFields = await setCreatorFields({ user, isEdition: true })(releaseData);
375
- const { validateUniqueNameForPendingRelease, validateScheduledAtIsLaterThanNow } = getService(
376
- "release-validation",
377
- { strapi: strapi2 }
378
- );
379
- await Promise.all([
380
- validateUniqueNameForPendingRelease(releaseWithCreatorFields.name, id),
381
- validateScheduledAtIsLaterThanNow(releaseWithCreatorFields.scheduledAt)
382
- ]);
383
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, id);
384
- if (!release2) {
385
- throw new errors.NotFoundError(`No release found for id ${id}`);
386
- }
387
- if (release2.releasedAt) {
388
- throw new errors.ValidationError("Release already published");
389
- }
390
- const updatedRelease = await strapi2.entityService.update(RELEASE_MODEL_UID, id, {
391
- /*
392
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">>
393
- * is not compatible with the type we are passing here: UpdateRelease.Request['body']
394
- */
395
- // @ts-expect-error see above
396
- data: releaseWithCreatorFields
397
- });
398
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
399
- const schedulingService = getService("scheduling", { strapi: strapi2 });
400
- if (releaseData.scheduledAt) {
401
- await schedulingService.set(id, releaseData.scheduledAt);
402
- } else if (release2.scheduledAt) {
403
- 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;
404
543
  }
405
544
  }
406
- return updatedRelease;
407
- },
408
- async createAction(releaseId, action) {
409
- const { validateEntryContentType, validateUniqueEntry } = getService("release-validation", {
410
- strapi: strapi2
411
- });
412
- await Promise.all([
413
- validateEntryContentType(action.entry.contentType),
414
- validateUniqueEntry(releaseId, action)
415
- ]);
416
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId);
417
- if (!release2) {
418
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
419
- }
420
- if (release2.releasedAt) {
421
- throw new errors.ValidationError("Release already published");
422
- }
423
- const { entry, type } = action;
424
- return strapi2.entityService.create(RELEASE_ACTION_MODEL_UID, {
425
- data: {
426
- type,
427
- contentType: entry.contentType,
428
- locale: entry.locale,
429
- entry: {
430
- id: entry.id,
431
- __type: entry.contentType,
432
- __pivot: { field: "entry" }
433
- },
434
- release: releaseId
435
- },
436
- populate: { release: { fields: ["id"] }, entry: { fields: ["id"] } }
437
- });
438
- },
439
- async findActions(releaseId, query) {
440
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
441
- fields: ["id"]
442
- });
443
- if (!release2) {
444
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
445
- }
446
- return strapi2.entityService.findPage(RELEASE_ACTION_MODEL_UID, {
447
- ...query,
448
- populate: {
449
- entry: {
450
- 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
451
554
  }
452
555
  },
453
- filters: {
454
- release: releaseId
455
- }
556
+ populate
456
557
  });
457
- },
458
- async countActions(query) {
459
- return strapi2.entityService.count(RELEASE_ACTION_MODEL_UID, query);
460
- },
461
- async groupActions(actions, groupBy) {
462
- const contentTypeUids = actions.reduce((acc, action) => {
463
- if (!acc.includes(action.contentType)) {
464
- acc.push(action.contentType);
465
- }
466
- return acc;
467
- }, []);
468
- const allReleaseContentTypesDictionary = await this.getContentTypesDataForActions(
469
- contentTypeUids
470
- );
471
- const allLocalesDictionary = await this.getLocalesDataForActions();
472
- const formattedData = actions.map((action) => {
473
- const { mainField, displayName } = allReleaseContentTypesDictionary[action.contentType];
474
- return {
475
- ...action,
476
- locale: action.locale ? allLocalesDictionary[action.locale] : null,
477
- contentType: {
478
- displayName,
479
- mainFieldValue: action.entry[mainField],
480
- uid: action.contentType
558
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
559
+ filters: {
560
+ id: {
561
+ $in: entriestoUnpublishIds
481
562
  }
482
- };
563
+ },
564
+ populate
483
565
  });
484
- const groupName = getGroupName(groupBy);
485
- return _.groupBy(groupName)(formattedData);
486
- },
487
- async getLocalesDataForActions() {
488
- if (!strapi2.plugin("i18n")) {
489
- return {};
566
+ if (entriesToPublish.length > 0) {
567
+ await entityManagerService.publishMany(entriesToPublish, uid);
490
568
  }
491
- const allLocales = await strapi2.plugin("i18n").service("locales").find() || [];
492
- return allLocales.reduce((acc, locale) => {
493
- acc[locale.code] = { name: locale.name, code: locale.code };
494
- return acc;
495
- }, {});
496
- },
497
- async getContentTypesDataForActions(contentTypesUids) {
498
- const contentManagerContentTypeService = strapi2.plugin("content-manager").service("content-types");
499
- const contentTypesData = {};
500
- for (const contentTypeUid of contentTypesUids) {
501
- const contentTypeConfig = await contentManagerContentTypeService.findConfiguration({
502
- uid: contentTypeUid
503
- });
504
- contentTypesData[contentTypeUid] = {
505
- mainField: contentTypeConfig.settings.mainField,
506
- displayName: strapi2.getModel(contentTypeUid).info.displayName
507
- };
569
+ if (entriesToUnpublish.length > 0) {
570
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
508
571
  }
509
- return contentTypesData;
510
- },
511
- getContentTypeModelsFromActions(actions) {
512
- const contentTypeUids = actions.reduce((acc, action) => {
513
- if (!acc.includes(action.contentType)) {
514
- acc.push(action.contentType);
515
- }
516
- return acc;
517
- }, []);
518
- const contentTypeModelsMap = contentTypeUids.reduce(
519
- (acc, contentTypeUid) => {
520
- acc[contentTypeUid] = strapi2.getModel(contentTypeUid);
521
- return acc;
522
- },
523
- {}
524
- );
525
- return contentTypeModelsMap;
526
- },
527
- async getAllComponents() {
528
- const contentManagerComponentsService = strapi2.plugin("content-manager").service("components");
529
- const components = await contentManagerComponentsService.findAllComponents();
530
- const componentsMap = components.reduce(
531
- (acc, component) => {
532
- acc[component.uid] = component;
533
- 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
+ }
534
579
  },
535
- {}
536
- );
537
- return componentsMap;
538
- },
539
- async delete(releaseId) {
540
- const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
541
580
  populate: {
542
- actions: {
581
+ entry: {
543
582
  fields: ["id"]
544
583
  }
545
584
  }
546
585
  });
547
- if (!release2) {
548
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
549
- }
550
- if (release2.releasedAt) {
551
- throw new errors.ValidationError("Release already published");
552
- }
553
- await strapi2.db.transaction(async () => {
554
- await strapi2.db.query(RELEASE_ACTION_MODEL_UID).deleteMany({
555
- where: {
556
- id: {
557
- $in: release2.actions.map((action) => action.id)
558
- }
559
- }
560
- });
561
- await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
562
- });
563
- return release2;
564
- },
565
- async publish(releaseId) {
566
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
567
- RELEASE_MODEL_UID,
568
- releaseId,
569
- {
570
- populate: {
571
- actions: {
572
- populate: {
573
- entry: {
574
- fields: ["id"]
575
- }
576
- }
577
- }
578
- }
579
- }
580
- );
581
- if (!releaseWithPopulatedActionEntries) {
582
- throw new errors.NotFoundError(`No release found for id ${releaseId}`);
583
- }
584
- if (releaseWithPopulatedActionEntries.releasedAt) {
585
- throw new errors.ValidationError("Release already published");
586
- }
587
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
586
+ if (actions.length === 0) {
588
587
  throw new errors.ValidationError("No entries to publish");
589
588
  }
590
589
  const collectionTypeActions = {};
591
590
  const singleTypeActions = [];
592
- for (const action of releaseWithPopulatedActionEntries.actions) {
591
+ for (const action of actions) {
593
592
  const contentTypeUid = action.contentType;
594
593
  if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
595
594
  if (!collectionTypeActions[contentTypeUid]) {
596
595
  collectionTypeActions[contentTypeUid] = {
597
- entriestoPublishIds: [],
596
+ entriesToPublishIds: [],
598
597
  entriesToUnpublishIds: []
599
598
  };
600
599
  }
601
600
  if (action.type === "publish") {
602
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
601
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
603
602
  } else {
604
603
  collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
605
604
  }
@@ -611,110 +610,487 @@ const createReleaseService = ({ strapi: strapi2 }) => ({
611
610
  });
612
611
  }
613
612
  }
614
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
615
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
616
- await strapi2.db.transaction(async () => {
617
- for (const { uid, action, id } of singleTypeActions) {
618
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
619
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
620
- try {
621
- if (action === "publish") {
622
- await entityManagerService.publish(entry, uid);
623
- } else {
624
- 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
625
654
  }
626
- } catch (error) {
627
- if (error instanceof errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
628
- ;
629
- else {
630
- 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
+ }
631
676
  }
632
677
  }
633
- }
634
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
635
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
636
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
637
- const entriesToPublish = await strapi2.entityService.findMany(
638
- contentTypeUid,
639
- {
640
- filters: {
641
- id: {
642
- $in: entriestoPublishIds
643
- }
644
- },
645
- 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
646
700
  }
647
- );
648
- const entriesToUnpublish = await strapi2.entityService.findMany(
649
- contentTypeUid,
650
- {
651
- filters: {
701
+ }
702
+ });
703
+ const releases = await strapi2.db.query(RELEASE_MODEL_UID).findMany({
704
+ where: {
705
+ $or: [
706
+ {
652
707
  id: {
653
- $in: entriesToUnpublishIds
708
+ $notIn: releasesRelated.map((release2) => release2.id)
654
709
  }
655
710
  },
656
- populate
711
+ {
712
+ actions: null
713
+ }
714
+ ],
715
+ releasedAt: {
716
+ $null: true
657
717
  }
658
- );
659
- if (entriesToPublish.length > 0) {
660
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
661
718
  }
662
- if (entriesToUnpublish.length > 0) {
663
- 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
+ };
664
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}`);
665
747
  }
666
- });
667
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
668
- 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, {
669
752
  /*
670
- * 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']
671
755
  */
672
756
  // @ts-expect-error see above
673
- 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);
674
764
  }
675
- });
676
- return release2;
677
- },
678
- async updateAction(actionId, releaseId, update) {
679
- const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
680
- where: {
681
- id: actionId,
682
- release: {
683
- id: releaseId,
684
- releasedAt: {
685
- $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: "*"
686
817
  }
818
+ },
819
+ filters: {
820
+ release: releaseId
687
821
  }
688
- },
689
- data: update
690
- });
691
- if (!updatedAction) {
692
- throw new errors.NotFoundError(
693
- `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
694
836
  );
695
- }
696
- return updatedAction;
697
- },
698
- async deleteAction(actionId, releaseId) {
699
- const deletedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).delete({
700
- where: {
701
- id: actionId,
702
- release: {
703
- id: releaseId,
704
- releasedAt: {
705
- $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
706
847
  }
707
- }
848
+ };
849
+ });
850
+ const groupName = getGroupName(groupBy);
851
+ return _.groupBy(groupName)(formattedData);
852
+ },
853
+ async getLocalesDataForActions() {
854
+ if (!strapi2.plugin("i18n")) {
855
+ return {};
708
856
  }
709
- });
710
- if (!deletedAction) {
711
- throw new errors.NotFoundError(
712
- `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
+ {}
713
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
+ });
714
1085
  }
715
- return deletedAction;
1086
+ };
1087
+ };
1088
+ class AlreadyOnReleaseError extends errors.ApplicationError {
1089
+ constructor(message) {
1090
+ super(message);
1091
+ this.name = "AlreadyOnReleaseError";
716
1092
  }
717
- });
1093
+ }
718
1094
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
719
1095
  async validateUniqueEntry(releaseId, releaseActionArgs) {
720
1096
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -727,7 +1103,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
727
1103
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
728
1104
  );
729
1105
  if (isEntryInRelease) {
730
- throw new errors.ValidationError(
1106
+ throw new AlreadyOnReleaseError(
731
1107
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
732
1108
  );
733
1109
  }
@@ -835,12 +1211,27 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
835
1211
  const services = {
836
1212
  release: createReleaseService,
837
1213
  "release-validation": createReleaseValidationService,
838
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1214
+ scheduling: createSchedulingService
839
1215
  };
840
1216
  const RELEASE_SCHEMA = yup.object().shape({
841
1217
  name: yup.string().trim().required(),
842
- // scheduledAt is a date, but we always receive strings from the client
843
- scheduledAt: yup.string().nullable()
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
+ })
844
1235
  }).required().noUnknown();
845
1236
  const validateRelease = validateYupSchema(RELEASE_SCHEMA);
846
1237
  const releaseController = {
@@ -873,7 +1264,12 @@ const releaseController = {
873
1264
  }
874
1265
  };
875
1266
  });
876
- 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 } };
877
1273
  }
878
1274
  },
879
1275
  async findOne(ctx) {
@@ -991,6 +1387,38 @@ const releaseActionController = {
991
1387
  data: releaseAction2
992
1388
  };
993
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
+ },
994
1422
  async findMany(ctx) {
995
1423
  const releaseId = ctx.params.releaseId;
996
1424
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1176,6 +1604,22 @@ const releaseAction = {
1176
1604
  ]
1177
1605
  }
1178
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
+ },
1179
1623
  {
1180
1624
  method: "GET",
1181
1625
  path: "/:releaseId/actions",
@@ -1244,6 +1688,9 @@ const getPlugin = () => {
1244
1688
  };
1245
1689
  }
1246
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
1247
1694
  contentTypes
1248
1695
  };
1249
1696
  };