diff --git a/meteor/server/api/rest/v1/studios.ts b/meteor/server/api/rest/v1/studios.ts index 75e3f2a10a..0b98c4b4a1 100644 --- a/meteor/server/api/rest/v1/studios.ts +++ b/meteor/server/api/rest/v1/studios.ts @@ -45,6 +45,11 @@ class StudiosServerAPI implements StudiosRestAPI { _event: string, apiStudio: APIStudio ): Promise> { + const studioCount = await Studios.countDocuments() + if (studioCount > 0) { + return ClientAPI.responseError(UserError.create(UserErrorMessage.SystemSingleStudio, {}, 400)) + } + const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio) checkValidation(`addStudio`, blueprintConfigValidation) @@ -156,6 +161,14 @@ class StudiosServerAPI implements StudiosRestAPI { event: string, studioId: StudioId ): Promise> { + const studioCount = await Studios.countDocuments() + if (studioCount === 1) { + throw new Meteor.Error( + 400, + `The last studio in the system cannot be deleted (there must be at least one studio)` + ) + } + const existingStudio = await Studios.findOneAsync(studioId) if (existingStudio) { const playlists = (await RundownPlaylists.findFetchAsync( diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index f5f75b7fe7..127db12777 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -38,6 +38,14 @@ async function insertStudio(context: MethodContext, newId?: StudioId): Promise { + const studioCount = await Studios.countDocuments() + if (studioCount > 0) { + throw new Meteor.Error( + 400, + `Only one studio is supported per installation (there are currently ${studioCount})` + ) + } + return Studios.insertAsync( literal({ _id: newId || getRandomId(), @@ -79,6 +87,14 @@ async function removeStudio(context: MethodContext, studioId: StudioId): Promise assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_STUDIOS) + const studioCount = await Studios.countDocuments() + if (studioCount === 1) { + throw new Meteor.Error( + 400, + `The last studio in the system cannot be deleted (there must be at least one studio)` + ) + } + const studio = await Studios.findOneAsync(studioId) if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 9a1382d9d6..7c7cef98e2 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,6 +1,7 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' import { MongoInternals } from 'meteor/mongo' +import { Studios } from '../collections' /* * ************************************************************************************** @@ -44,4 +45,18 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + + { + id: 'Ensure a single studio', + canBeRunAutomatically: true, + validate: async () => { + const studioCount = await Studios.countDocuments() + if (studioCount === 0) return `No studios found` + if (studioCount > 1) return `There are ${studioCount} studios, but only one is supported` + return false + }, + migrate: async () => { + // Do nothing, the user will have to resolve this manually + }, + }, ]) diff --git a/packages/corelib/src/error.ts b/packages/corelib/src/error.ts index 65d3e4f57b..3cbf71c558 100644 --- a/packages/corelib/src/error.ts +++ b/packages/corelib/src/error.ts @@ -63,6 +63,7 @@ export enum UserErrorMessage { IdempotencyKeyMissing = 47, IdempotencyKeyAlreadyUsed = 48, RateLimitExceeded = 49, + SystemSingleStudio = 50, } const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { @@ -124,6 +125,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = { [UserErrorMessage.IdempotencyKeyMissing]: t(`Idempotency-Key is missing`), [UserErrorMessage.IdempotencyKeyAlreadyUsed]: t(`Idempotency-Key is already used`), [UserErrorMessage.RateLimitExceeded]: t(`Rate limit exceeded`), + [UserErrorMessage.SystemSingleStudio]: t(`System must have exactly one studio`), } export interface SerializedUserError { diff --git a/packages/webui/src/client/ui/Settings/SettingsMenu.tsx b/packages/webui/src/client/ui/Settings/SettingsMenu.tsx index 490070a0fa..788fd8005e 100644 --- a/packages/webui/src/client/ui/Settings/SettingsMenu.tsx +++ b/packages/webui/src/client/ui/Settings/SettingsMenu.tsx @@ -86,12 +86,16 @@ function SettingsMenuStudios() { MeteorCall.studio.insertStudio().catch(catchError('studio.insertStudio')) }, []) + // An installation should have only one studio https://github.com/Sofie-Automation/sofie-core/issues/1450 + const canAddStudio = studios.length === 0 + const canDeleteStudio = studios.length > 1 + return ( <> - + {studios.map((studio) => ( - + ))} ) @@ -241,8 +245,9 @@ function SettingsCollapsibleGroup({ interface SettingsMenuStudioProps { studio: DBStudio + canDelete: boolean } -function SettingsMenuStudio({ studio }: Readonly) { +function SettingsMenuStudio({ studio, canDelete }: Readonly) { const { t } = useTranslation() const onDeleteStudio = React.useCallback( @@ -291,9 +296,11 @@ function SettingsMenuStudio({ studio }: Readonly) { ) : null} - + {canDelete && ( + + )} ) }