Skip to content

Commit 38439f9

Browse files
authored
feat: limit the system to have a single studio #1450 (#1534)
This is not strict about it, if a system has more than one then it will simply get a stuck migration. Once the system has a single studio it will not allow adding or removing any.
1 parent 3814a75 commit 38439f9

File tree

5 files changed

+59
-6
lines changed

5 files changed

+59
-6
lines changed

meteor/server/api/rest/v1/studios.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class StudiosServerAPI implements StudiosRestAPI {
4545
_event: string,
4646
apiStudio: APIStudio
4747
): Promise<ClientAPI.ClientResponse<string>> {
48+
const studioCount = await Studios.countDocuments()
49+
if (studioCount > 0) {
50+
return ClientAPI.responseError(UserError.create(UserErrorMessage.SystemSingleStudio, {}, 400))
51+
}
52+
4853
const blueprintConfigValidation = await validateAPIBlueprintConfigForStudio(apiStudio)
4954
checkValidation(`addStudio`, blueprintConfigValidation)
5055

@@ -156,6 +161,14 @@ class StudiosServerAPI implements StudiosRestAPI {
156161
event: string,
157162
studioId: StudioId
158163
): Promise<ClientAPI.ClientResponse<void>> {
164+
const studioCount = await Studios.countDocuments()
165+
if (studioCount === 1) {
166+
throw new Meteor.Error(
167+
400,
168+
`The last studio in the system cannot be deleted (there must be at least one studio)`
169+
)
170+
}
171+
159172
const existingStudio = await Studios.findOneAsync(studioId)
160173
if (existingStudio) {
161174
const playlists = (await RundownPlaylists.findFetchAsync(

meteor/server/api/studio/api.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ async function insertStudio(context: MethodContext, newId?: StudioId): Promise<S
3838
return insertStudioInner(null, newId)
3939
}
4040
export async function insertStudioInner(organizationId: OrganizationId | null, newId?: StudioId): Promise<StudioId> {
41+
const studioCount = await Studios.countDocuments()
42+
if (studioCount > 0) {
43+
throw new Meteor.Error(
44+
400,
45+
`Only one studio is supported per installation (there are currently ${studioCount})`
46+
)
47+
}
48+
4149
return Studios.insertAsync(
4250
literal<DBStudio>({
4351
_id: newId || getRandomId(),
@@ -79,6 +87,14 @@ async function removeStudio(context: MethodContext, studioId: StudioId): Promise
7987

8088
assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_STUDIOS)
8189

90+
const studioCount = await Studios.countDocuments()
91+
if (studioCount === 1) {
92+
throw new Meteor.Error(
93+
400,
94+
`The last studio in the system cannot be deleted (there must be at least one studio)`
95+
)
96+
}
97+
8298
const studio = await Studios.findOneAsync(studioId)
8399
if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`)
84100

meteor/server/migration/X_X_X.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { addMigrationSteps } from './databaseMigration'
22
import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion'
33
import { MongoInternals } from 'meteor/mongo'
4+
import { Studios } from '../collections'
45

56
/*
67
* **************************************************************************************
@@ -44,4 +45,18 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
4445
}
4546
},
4647
},
48+
49+
{
50+
id: 'Ensure a single studio',
51+
canBeRunAutomatically: true,
52+
validate: async () => {
53+
const studioCount = await Studios.countDocuments()
54+
if (studioCount === 0) return `No studios found`
55+
if (studioCount > 1) return `There are ${studioCount} studios, but only one is supported`
56+
return false
57+
},
58+
migrate: async () => {
59+
// Do nothing, the user will have to resolve this manually
60+
},
61+
},
4762
])

packages/corelib/src/error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export enum UserErrorMessage {
6363
IdempotencyKeyMissing = 47,
6464
IdempotencyKeyAlreadyUsed = 48,
6565
RateLimitExceeded = 49,
66+
SystemSingleStudio = 50,
6667
}
6768

6869
const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
@@ -124,6 +125,7 @@ const UserErrorMessagesTranslations: { [key in UserErrorMessage]: string } = {
124125
[UserErrorMessage.IdempotencyKeyMissing]: t(`Idempotency-Key is missing`),
125126
[UserErrorMessage.IdempotencyKeyAlreadyUsed]: t(`Idempotency-Key is already used`),
126127
[UserErrorMessage.RateLimitExceeded]: t(`Rate limit exceeded`),
128+
[UserErrorMessage.SystemSingleStudio]: t(`System must have exactly one studio`),
127129
}
128130

129131
export interface SerializedUserError {

packages/webui/src/client/ui/Settings/SettingsMenu.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,16 @@ function SettingsMenuStudios() {
8686
MeteorCall.studio.insertStudio().catch(catchError('studio.insertStudio'))
8787
}, [])
8888

89+
// An installation should have only one studio https://github.com/Sofie-Automation/sofie-core/issues/1450
90+
const canAddStudio = studios.length === 0
91+
const canDeleteStudio = studios.length > 1
92+
8993
return (
9094
<>
91-
<SectionHeading title={t('Studios')} addClick={onAddStudio} />
95+
<SectionHeading title={t('Studios')} addClick={canAddStudio ? onAddStudio : undefined} />
9296

9397
{studios.map((studio) => (
94-
<SettingsMenuStudio key={unprotectString(studio._id)} studio={studio} />
98+
<SettingsMenuStudio key={unprotectString(studio._id)} studio={studio} canDelete={canDeleteStudio} />
9599
))}
96100
</>
97101
)
@@ -241,8 +245,9 @@ function SettingsCollapsibleGroup({
241245

242246
interface SettingsMenuStudioProps {
243247
studio: DBStudio
248+
canDelete: boolean
244249
}
245-
function SettingsMenuStudio({ studio }: Readonly<SettingsMenuStudioProps>) {
250+
function SettingsMenuStudio({ studio, canDelete }: Readonly<SettingsMenuStudioProps>) {
246251
const { t } = useTranslation()
247252

248253
const onDeleteStudio = React.useCallback(
@@ -291,9 +296,11 @@ function SettingsMenuStudio({ studio }: Readonly<SettingsMenuStudioProps>) {
291296
<FontAwesomeIcon icon={faExclamationTriangle} />
292297
</button>
293298
) : null}
294-
<button className="action-btn" onClick={onDeleteStudio}>
295-
<FontAwesomeIcon icon={faTrash} />
296-
</button>
299+
{canDelete && (
300+
<button className="action-btn" onClick={onDeleteStudio}>
301+
<FontAwesomeIcon icon={faTrash} />
302+
</button>
303+
)}
297304
</SettingsCollapsibleGroup>
298305
)
299306
}

0 commit comments

Comments
 (0)