@strapi/content-releases 4.20.4 → 4.21.0

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.
@@ -203,7 +203,7 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
203
203
  }
204
204
  });
205
205
  await utils.mapAsync(actions, async (action) => {
206
- if (action.entry) {
206
+ if (action.entry && action.release) {
207
207
  const populatedEntry = await getPopulatedEntry(contentTypeUID, action.entry.id, {
208
208
  strapi
209
209
  });
@@ -231,12 +231,57 @@ async function revalidateChangedContentTypes({ oldContentTypes, contentTypes: co
231
231
  });
232
232
  }
233
233
  }
234
+ async function disableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
235
+ if (!oldContentTypes) {
236
+ return;
237
+ }
238
+ for (const uid in contentTypes2) {
239
+ if (!oldContentTypes[uid]) {
240
+ continue;
241
+ }
242
+ const oldContentType = oldContentTypes[uid];
243
+ const contentType = contentTypes2[uid];
244
+ const i18nPlugin = strapi.plugin("i18n");
245
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
246
+ if (isLocalizedContentType(oldContentType) && !isLocalizedContentType(contentType)) {
247
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
248
+ locale: null
249
+ }).where({ contentType: uid }).execute();
250
+ }
251
+ }
252
+ }
253
+ async function enableContentTypeLocalized({ oldContentTypes, contentTypes: contentTypes2 }) {
254
+ if (!oldContentTypes) {
255
+ return;
256
+ }
257
+ for (const uid in contentTypes2) {
258
+ if (!oldContentTypes[uid]) {
259
+ continue;
260
+ }
261
+ const oldContentType = oldContentTypes[uid];
262
+ const contentType = contentTypes2[uid];
263
+ const i18nPlugin = strapi.plugin("i18n");
264
+ const { isLocalizedContentType } = i18nPlugin.service("content-types");
265
+ const { getDefaultLocale } = i18nPlugin.service("locales");
266
+ if (!isLocalizedContentType(oldContentType) && isLocalizedContentType(contentType)) {
267
+ const defaultLocale = await getDefaultLocale();
268
+ await strapi.db.queryBuilder(RELEASE_ACTION_MODEL_UID).update({
269
+ locale: defaultLocale
270
+ }).where({ contentType: uid }).execute();
271
+ }
272
+ }
273
+ }
234
274
  const { features: features$2 } = require("@strapi/strapi/dist/utils/ee");
235
275
  const register = async ({ strapi: strapi2 }) => {
236
276
  if (features$2.isEnabled("cms-content-releases")) {
237
277
  await strapi2.admin.services.permission.actionProvider.registerMany(ACTIONS);
238
- strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish);
239
- strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
278
+ strapi2.hook("strapi::content-types.beforeSync").register(deleteActionsOnDisableDraftAndPublish).register(disableContentTypeLocalized);
279
+ strapi2.hook("strapi::content-types.afterSync").register(deleteActionsOnDeleteContentType).register(enableContentTypeLocalized).register(revalidateChangedContentTypes).register(migrateIsValidAndStatusReleases);
280
+ }
281
+ if (strapi2.plugin("graphql")) {
282
+ const graphqlExtensionService = strapi2.plugin("graphql").service("extension");
283
+ graphqlExtensionService.shadowCRUD(RELEASE_MODEL_UID).disable();
284
+ graphqlExtensionService.shadowCRUD(RELEASE_ACTION_MODEL_UID).disable();
240
285
  }
241
286
  };
242
287
  const { features: features$1 } = require("@strapi/strapi/dist/utils/ee");
@@ -362,27 +407,23 @@ const bootstrap = async ({ strapi: strapi2 }) => {
362
407
  }
363
408
  }
364
409
  });
365
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
366
- getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
367
- strapi2.log.error(
368
- "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
369
- );
370
- throw err;
371
- });
372
- Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
373
- strapi2.webhookStore.addAllowedEvent(key, value);
374
- });
375
- }
410
+ getService("scheduling", { strapi: strapi2 }).syncFromDatabase().catch((err) => {
411
+ strapi2.log.error(
412
+ "Error while syncing scheduled jobs from the database in the content-releases plugin. This could lead to errors in the releases scheduling."
413
+ );
414
+ throw err;
415
+ });
416
+ Object.entries(ALLOWED_WEBHOOK_EVENTS).forEach(([key, value]) => {
417
+ strapi2.webhookStore.addAllowedEvent(key, value);
418
+ });
376
419
  }
377
420
  };
378
421
  const destroy = async ({ strapi: strapi2 }) => {
379
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
380
- const scheduledJobs = getService("scheduling", {
381
- strapi: strapi2
382
- }).getAll();
383
- for (const [, job] of scheduledJobs) {
384
- job.cancel();
385
- }
422
+ const scheduledJobs = getService("scheduling", {
423
+ strapi: strapi2
424
+ }).getAll();
425
+ for (const [, job] of scheduledJobs) {
426
+ job.cancel();
386
427
  }
387
428
  };
388
429
  const schema$1 = {
@@ -507,6 +548,94 @@ const createReleaseService = ({ strapi: strapi2 }) => {
507
548
  release: release2
508
549
  });
509
550
  };
551
+ const publishSingleTypeAction = async (uid, actionType, entryId) => {
552
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
553
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
554
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
555
+ const entry = await strapi2.entityService.findOne(uid, entryId, { populate });
556
+ try {
557
+ if (actionType === "publish") {
558
+ await entityManagerService.publish(entry, uid);
559
+ } else {
560
+ await entityManagerService.unpublish(entry, uid);
561
+ }
562
+ } catch (error) {
563
+ if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft"))
564
+ ;
565
+ else {
566
+ throw error;
567
+ }
568
+ }
569
+ };
570
+ const publishCollectionTypeAction = async (uid, entriesToPublishIds, entriestoUnpublishIds) => {
571
+ const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
572
+ const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
573
+ const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
574
+ const entriesToPublish = await strapi2.entityService.findMany(uid, {
575
+ filters: {
576
+ id: {
577
+ $in: entriesToPublishIds
578
+ }
579
+ },
580
+ populate
581
+ });
582
+ const entriesToUnpublish = await strapi2.entityService.findMany(uid, {
583
+ filters: {
584
+ id: {
585
+ $in: entriestoUnpublishIds
586
+ }
587
+ },
588
+ populate
589
+ });
590
+ if (entriesToPublish.length > 0) {
591
+ await entityManagerService.publishMany(entriesToPublish, uid);
592
+ }
593
+ if (entriesToUnpublish.length > 0) {
594
+ await entityManagerService.unpublishMany(entriesToUnpublish, uid);
595
+ }
596
+ };
597
+ const getFormattedActions = async (releaseId) => {
598
+ const actions = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).findMany({
599
+ where: {
600
+ release: {
601
+ id: releaseId
602
+ }
603
+ },
604
+ populate: {
605
+ entry: {
606
+ fields: ["id"]
607
+ }
608
+ }
609
+ });
610
+ if (actions.length === 0) {
611
+ throw new utils.errors.ValidationError("No entries to publish");
612
+ }
613
+ const collectionTypeActions = {};
614
+ const singleTypeActions = [];
615
+ for (const action of actions) {
616
+ const contentTypeUid = action.contentType;
617
+ if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
618
+ if (!collectionTypeActions[contentTypeUid]) {
619
+ collectionTypeActions[contentTypeUid] = {
620
+ entriesToPublishIds: [],
621
+ entriesToUnpublishIds: []
622
+ };
623
+ }
624
+ if (action.type === "publish") {
625
+ collectionTypeActions[contentTypeUid].entriesToPublishIds.push(action.entry.id);
626
+ } else {
627
+ collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
628
+ }
629
+ } else {
630
+ singleTypeActions.push({
631
+ uid: contentTypeUid,
632
+ action: action.type,
633
+ id: action.entry.id
634
+ });
635
+ }
636
+ }
637
+ return { collectionTypeActions, singleTypeActions };
638
+ };
510
639
  return {
511
640
  async create(releaseData, { user }) {
512
641
  const releaseWithCreatorFields = await utils.setCreatorFields({ user })(releaseData);
@@ -526,7 +655,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
526
655
  status: "empty"
527
656
  }
528
657
  });
529
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && releaseWithCreatorFields.scheduledAt) {
658
+ if (releaseWithCreatorFields.scheduledAt) {
530
659
  const schedulingService = getService("scheduling", { strapi: strapi2 });
531
660
  await schedulingService.set(release2.id, release2.scheduledAt);
532
661
  }
@@ -651,13 +780,11 @@ const createReleaseService = ({ strapi: strapi2 }) => {
651
780
  // @ts-expect-error see above
652
781
  data: releaseWithCreatorFields
653
782
  });
654
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
655
- const schedulingService = getService("scheduling", { strapi: strapi2 });
656
- if (releaseData.scheduledAt) {
657
- await schedulingService.set(id, releaseData.scheduledAt);
658
- } else if (release2.scheduledAt) {
659
- schedulingService.cancel(id);
660
- }
783
+ const schedulingService = getService("scheduling", { strapi: strapi2 });
784
+ if (releaseData.scheduledAt) {
785
+ await schedulingService.set(id, releaseData.scheduledAt);
786
+ } else if (release2.scheduledAt) {
787
+ schedulingService.cancel(id);
661
788
  }
662
789
  this.updateReleaseStatus(id);
663
790
  strapi2.telemetry.send("didUpdateContentRelease");
@@ -823,7 +950,7 @@ const createReleaseService = ({ strapi: strapi2 }) => {
823
950
  });
824
951
  await strapi2.entityService.delete(RELEASE_MODEL_UID, releaseId);
825
952
  });
826
- if (strapi2.features.future.isEnabled("contentReleasesScheduling") && release2.scheduledAt) {
953
+ if (release2.scheduledAt) {
827
954
  const schedulingService = getService("scheduling", { strapi: strapi2 });
828
955
  await schedulingService.cancel(release2.id);
829
956
  }
@@ -831,145 +958,71 @@ const createReleaseService = ({ strapi: strapi2 }) => {
831
958
  return release2;
832
959
  },
833
960
  async publish(releaseId) {
834
- try {
835
- const releaseWithPopulatedActionEntries = await strapi2.entityService.findOne(
836
- RELEASE_MODEL_UID,
837
- releaseId,
838
- {
839
- populate: {
840
- actions: {
841
- populate: {
842
- entry: {
843
- fields: ["id"]
844
- }
845
- }
846
- }
847
- }
848
- }
849
- );
850
- if (!releaseWithPopulatedActionEntries) {
961
+ const {
962
+ release: release2,
963
+ error
964
+ } = await strapi2.db.transaction(async ({ trx }) => {
965
+ const lockedRelease = await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).select(["id", "name", "releasedAt", "status"]).first().transacting(trx).forUpdate().execute();
966
+ if (!lockedRelease) {
851
967
  throw new utils.errors.NotFoundError(`No release found for id ${releaseId}`);
852
968
  }
853
- if (releaseWithPopulatedActionEntries.releasedAt) {
969
+ if (lockedRelease.releasedAt) {
854
970
  throw new utils.errors.ValidationError("Release already published");
855
971
  }
856
- if (releaseWithPopulatedActionEntries.actions.length === 0) {
857
- throw new utils.errors.ValidationError("No entries to publish");
972
+ if (lockedRelease.status === "failed") {
973
+ throw new utils.errors.ValidationError("Release failed to publish");
858
974
  }
859
- const collectionTypeActions = {};
860
- const singleTypeActions = [];
861
- for (const action of releaseWithPopulatedActionEntries.actions) {
862
- const contentTypeUid = action.contentType;
863
- if (strapi2.contentTypes[contentTypeUid].kind === "collectionType") {
864
- if (!collectionTypeActions[contentTypeUid]) {
865
- collectionTypeActions[contentTypeUid] = {
866
- entriestoPublishIds: [],
867
- entriesToUnpublishIds: []
868
- };
869
- }
870
- if (action.type === "publish") {
871
- collectionTypeActions[contentTypeUid].entriestoPublishIds.push(action.entry.id);
872
- } else {
873
- collectionTypeActions[contentTypeUid].entriesToUnpublishIds.push(action.entry.id);
874
- }
875
- } else {
876
- singleTypeActions.push({
877
- uid: contentTypeUid,
878
- action: action.type,
879
- id: action.entry.id
880
- });
881
- }
882
- }
883
- const entityManagerService = strapi2.plugin("content-manager").service("entity-manager");
884
- const populateBuilderService = strapi2.plugin("content-manager").service("populate-builder");
885
- await strapi2.db.transaction(async () => {
886
- for (const { uid, action, id } of singleTypeActions) {
887
- const populate = await populateBuilderService(uid).populateDeep(Infinity).build();
888
- const entry = await strapi2.entityService.findOne(uid, id, { populate });
889
- try {
890
- if (action === "publish") {
891
- await entityManagerService.publish(entry, uid);
892
- } else {
893
- await entityManagerService.unpublish(entry, uid);
894
- }
895
- } catch (error) {
896
- if (error instanceof utils.errors.ApplicationError && (error.message === "already.published" || error.message === "already.draft")) {
897
- } else {
898
- throw error;
899
- }
900
- }
901
- }
902
- for (const contentTypeUid of Object.keys(collectionTypeActions)) {
903
- const populate = await populateBuilderService(contentTypeUid).populateDeep(Infinity).build();
904
- const { entriestoPublishIds, entriesToUnpublishIds } = collectionTypeActions[contentTypeUid];
905
- const entriesToPublish = await strapi2.entityService.findMany(
906
- contentTypeUid,
907
- {
908
- filters: {
909
- id: {
910
- $in: entriestoPublishIds
911
- }
912
- },
913
- populate
914
- }
915
- );
916
- const entriesToUnpublish = await strapi2.entityService.findMany(
917
- contentTypeUid,
918
- {
919
- filters: {
920
- id: {
921
- $in: entriesToUnpublishIds
922
- }
923
- },
924
- populate
925
- }
926
- );
927
- if (entriesToPublish.length > 0) {
928
- await entityManagerService.publishMany(entriesToPublish, contentTypeUid);
975
+ try {
976
+ strapi2.log.info(`[Content Releases] Starting to publish release ${lockedRelease.name}`);
977
+ const { collectionTypeActions, singleTypeActions } = await getFormattedActions(
978
+ releaseId
979
+ );
980
+ await strapi2.db.transaction(async () => {
981
+ for (const { uid, action, id } of singleTypeActions) {
982
+ await publishSingleTypeAction(uid, action, id);
929
983
  }
930
- if (entriesToUnpublish.length > 0) {
931
- await entityManagerService.unpublishMany(entriesToUnpublish, contentTypeUid);
984
+ for (const contentTypeUid of Object.keys(collectionTypeActions)) {
985
+ const uid = contentTypeUid;
986
+ await publishCollectionTypeAction(
987
+ uid,
988
+ collectionTypeActions[uid].entriesToPublishIds,
989
+ collectionTypeActions[uid].entriesToUnpublishIds
990
+ );
932
991
  }
933
- }
934
- });
935
- const release2 = await strapi2.entityService.update(RELEASE_MODEL_UID, releaseId, {
936
- data: {
937
- /*
938
- * The type returned from the entity service: Partial<Input<"plugin::content-releases.release">> looks like it's wrong
939
- */
940
- // @ts-expect-error see above
941
- releasedAt: /* @__PURE__ */ new Date()
942
- },
943
- populate: {
944
- actions: {
945
- // @ts-expect-error is not expecting count but it is working
946
- count: true
992
+ });
993
+ const release22 = await strapi2.db.query(RELEASE_MODEL_UID).update({
994
+ where: {
995
+ id: releaseId
996
+ },
997
+ data: {
998
+ status: "done",
999
+ releasedAt: /* @__PURE__ */ new Date()
947
1000
  }
948
- }
949
- });
950
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1001
+ });
951
1002
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
952
1003
  isPublished: true,
953
- release: release2
1004
+ release: release22
954
1005
  });
955
- }
956
- strapi2.telemetry.send("didPublishContentRelease");
957
- return release2;
958
- } catch (error) {
959
- if (strapi2.features.future.isEnabled("contentReleasesScheduling")) {
1006
+ strapi2.telemetry.send("didPublishContentRelease");
1007
+ return { release: release22, error: null };
1008
+ } catch (error2) {
960
1009
  dispatchWebhook(ALLOWED_WEBHOOK_EVENTS.RELEASES_PUBLISH, {
961
1010
  isPublished: false,
962
- error
1011
+ error: error2
963
1012
  });
964
- }
965
- strapi2.db.query(RELEASE_MODEL_UID).update({
966
- where: { id: releaseId },
967
- data: {
1013
+ await strapi2.db?.queryBuilder(RELEASE_MODEL_UID).where({ id: releaseId }).update({
968
1014
  status: "failed"
969
- }
970
- });
1015
+ }).transacting(trx).execute();
1016
+ return {
1017
+ release: null,
1018
+ error: error2
1019
+ };
1020
+ }
1021
+ });
1022
+ if (error) {
971
1023
  throw error;
972
1024
  }
1025
+ return release2;
973
1026
  },
974
1027
  async updateAction(actionId, releaseId, update) {
975
1028
  const updatedAction = await strapi2.db.query(RELEASE_ACTION_MODEL_UID).update({
@@ -1056,6 +1109,12 @@ const createReleaseService = ({ strapi: strapi2 }) => {
1056
1109
  }
1057
1110
  };
1058
1111
  };
1112
+ class AlreadyOnReleaseError extends utils.errors.ApplicationError {
1113
+ constructor(message) {
1114
+ super(message);
1115
+ this.name = "AlreadyOnReleaseError";
1116
+ }
1117
+ }
1059
1118
  const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1060
1119
  async validateUniqueEntry(releaseId, releaseActionArgs) {
1061
1120
  const release2 = await strapi2.entityService.findOne(RELEASE_MODEL_UID, releaseId, {
@@ -1068,7 +1127,7 @@ const createReleaseValidationService = ({ strapi: strapi2 }) => ({
1068
1127
  (action) => Number(action.entry.id) === Number(releaseActionArgs.entry.id) && action.contentType === releaseActionArgs.entry.contentType
1069
1128
  );
1070
1129
  if (isEntryInRelease) {
1071
- throw new utils.errors.ValidationError(
1130
+ throw new AlreadyOnReleaseError(
1072
1131
  `Entry with id ${releaseActionArgs.entry.id} and contentType ${releaseActionArgs.entry.contentType} already exists in release with id ${releaseId}`
1073
1132
  );
1074
1133
  }
@@ -1176,7 +1235,7 @@ const createSchedulingService = ({ strapi: strapi2 }) => {
1176
1235
  const services = {
1177
1236
  release: createReleaseService,
1178
1237
  "release-validation": createReleaseValidationService,
1179
- ...strapi.features.future.isEnabled("contentReleasesScheduling") ? { scheduling: createSchedulingService } : {}
1238
+ scheduling: createSchedulingService
1180
1239
  };
1181
1240
  const RELEASE_SCHEMA = yup__namespace.object().shape({
1182
1241
  name: yup__namespace.string().trim().required(),
@@ -1352,6 +1411,38 @@ const releaseActionController = {
1352
1411
  data: releaseAction2
1353
1412
  };
1354
1413
  },
1414
+ async createMany(ctx) {
1415
+ const releaseId = ctx.params.releaseId;
1416
+ const releaseActionsArgs = ctx.request.body;
1417
+ await Promise.all(
1418
+ releaseActionsArgs.map((releaseActionArgs) => validateReleaseAction(releaseActionArgs))
1419
+ );
1420
+ const releaseService = getService("release", { strapi });
1421
+ const releaseActions = await strapi.db.transaction(async () => {
1422
+ const releaseActions2 = await Promise.all(
1423
+ releaseActionsArgs.map(async (releaseActionArgs) => {
1424
+ try {
1425
+ const action = await releaseService.createAction(releaseId, releaseActionArgs);
1426
+ return action;
1427
+ } catch (error) {
1428
+ if (error instanceof AlreadyOnReleaseError) {
1429
+ return null;
1430
+ }
1431
+ throw error;
1432
+ }
1433
+ })
1434
+ );
1435
+ return releaseActions2;
1436
+ });
1437
+ const newReleaseActions = releaseActions.filter((action) => action !== null);
1438
+ ctx.body = {
1439
+ data: newReleaseActions,
1440
+ meta: {
1441
+ entriesAlreadyInRelease: releaseActions.length - newReleaseActions.length,
1442
+ totalEntries: releaseActions.length
1443
+ }
1444
+ };
1445
+ },
1355
1446
  async findMany(ctx) {
1356
1447
  const releaseId = ctx.params.releaseId;
1357
1448
  const permissionsManager = strapi.admin.services.permission.createPermissionsManager({
@@ -1537,6 +1628,22 @@ const releaseAction = {
1537
1628
  ]
1538
1629
  }
1539
1630
  },
1631
+ {
1632
+ method: "POST",
1633
+ path: "/:releaseId/actions/bulk",
1634
+ handler: "release-action.createMany",
1635
+ config: {
1636
+ policies: [
1637
+ "admin::isAuthenticatedAdmin",
1638
+ {
1639
+ name: "admin::hasPermissions",
1640
+ config: {
1641
+ actions: ["plugin::content-releases.create-action"]
1642
+ }
1643
+ }
1644
+ ]
1645
+ }
1646
+ },
1540
1647
  {
1541
1648
  method: "GET",
1542
1649
  path: "/:releaseId/actions",
@@ -1605,6 +1712,9 @@ const getPlugin = () => {
1605
1712
  };
1606
1713
  }
1607
1714
  return {
1715
+ // Always return register, it handles its own feature check
1716
+ register,
1717
+ // Always return contentTypes to avoid losing data when the feature is disabled
1608
1718
  contentTypes
1609
1719
  };
1610
1720
  };