From 8670727bba5ac9cffe0e5d36b4bd45d6664482be Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Dec 2024 15:08:24 +0000 Subject: [PATCH 01/18] feat: meteor 3.0.4 --- .github/actions/setup-meteor/action.yaml | 4 +- .github/workflows/audit.yaml | 4 +- .github/workflows/node.yaml | 32 +- .github/workflows/prerelease-libs.yml | 2 +- .node-version | 2 +- DEVELOPER.md | 6 +- meteor/.eslintignore | 1 - meteor/.eslintrc.js | 3 +- meteor/.meteor/packages | 20 +- meteor/.meteor/release | 2 +- meteor/.meteor/versions | 124 ++- meteor/Dockerfile | 34 +- meteor/Dockerfile.circle | 24 +- meteor/__mocks__/Fibers.ts | 34 - meteor/__mocks__/_setupMocks.ts | 6 - meteor/__mocks__/helpers/jest.ts | 46 -- meteor/__mocks__/helpers/lib.ts | 2 +- meteor/__mocks__/meteor.ts | 164 +--- meteor/__mocks__/mongo.ts | 181 ++++- .../__mocks__/plugins/meteor-async-await.js | 74 -- meteor/__mocks__/reactive-var.ts | 18 - meteor/__mocks__/suppressLogging.ts | 1 + meteor/eslint-rules/README.md | 3 - meteor/eslint-rules/index.js | 15 - meteor/eslint-rules/noFocusedTestRule.js | 99 --- meteor/eslint-rules/noFocusedTestRule.ts | 112 --- meteor/eslint-rules/package.json | 6 - meteor/eslint-rules/utils.js | 484 ------------ meteor/eslint-rules/utils.ts | 724 ------------------ meteor/jest.config.js | 18 +- meteor/package.json | 17 +- .../server/__tests__/_testEnvironment.test.ts | 79 +- meteor/server/__tests__/coreSystem.test.ts | 3 +- meteor/server/__tests__/cronjobs.test.ts | 64 +- meteor/server/__tests__/logging.test.ts | 5 +- meteor/server/__tests__/systemTime.test.ts | 4 +- meteor/server/api/ExternalMessageQueue.ts | 4 +- meteor/server/api/__tests__/cleanup.test.ts | 11 +- meteor/server/api/__tests__/client.test.ts | 144 ++-- .../__tests__/externalMessageQueue.test.ts | 11 +- meteor/server/api/__tests__/methods.test.ts | 27 - .../api/__tests__/peripheralDevice.test.ts | 60 +- .../api/__tests__/rundownLayouts.test.ts | 5 +- .../__tests__/userActions/buckets.test.ts2 | 13 +- .../api/__tests__/userActions/general.test.ts | 9 +- .../userActions/mediaManager.test.ts | 12 +- .../api/__tests__/userActions/system.test.ts | 11 +- meteor/server/api/blueprintConfigPresets.ts | 8 +- .../api/blueprints/__tests__/api.test.ts | 49 +- .../__tests__/migrationContext.test.ts | 326 ++++---- .../server/api/blueprints/migrationContext.ts | 128 ++-- .../api/deviceTriggers/StudioObserver.ts | 8 +- meteor/server/api/deviceTriggers/observer.ts | 3 +- .../mosDevice/__tests__/actions.test.ts | 7 +- meteor/server/api/ingest/rundownInput.ts | 4 +- meteor/server/api/methods.ts | 22 +- meteor/server/api/profiler/apm.ts | 52 ++ .../api/{profiler.ts => profiler/index.ts} | 6 +- meteor/server/api/rest/koa.ts | 25 + .../server/api/rest/v0/__tests__/rest.test.ts | 5 +- meteor/server/api/rest/v0/index.ts | 6 +- meteor/server/api/snapshot.ts | 1 - meteor/server/api/studio/api.ts | 4 +- meteor/server/api/system.ts | 6 +- meteor/server/api/user.ts | 17 +- meteor/server/collections/collection.ts | 66 +- .../implementations/asyncCollection.ts | 297 +++++-- .../collections/implementations/base.ts | 130 ---- .../collections/implementations/mock.ts | 122 +-- .../implementations/readonlyWrapper.ts | 12 +- meteor/server/collections/index.ts | 3 +- meteor/server/coreSystem/index.ts | 12 +- meteor/server/lib.ts | 5 +- meteor/server/lib/__tests__/lib.test.ts | 41 +- meteor/server/lib/lib.ts | 87 --- meteor/server/methods.ts | 3 +- .../migration/__tests__/migrations.test.ts | 5 +- meteor/server/migration/databaseMigration.ts | 18 +- .../upgrades/__tests__/showStyleBase.test.ts | 23 +- meteor/server/performanceMonitor.ts | 3 +- .../lib/ReactiveCacheCollection.ts | 4 + .../lib/__tests__/rundownsObserver.test.ts | 10 +- meteor/server/publications/lib/lib.ts | 4 +- .../server/publications/lib/observerChain.ts | 6 +- .../__tests__/checkPieceContentStatus.test.ts | 7 +- .../__tests__/publication.test.ts | 15 +- .../security/__tests__/security.test.ts | 12 +- meteor/server/security/lib/security.ts | 2 +- meteor/server/security/lib/securityVerify.ts | 6 +- meteor/server/security/system.ts | 2 +- .../server/systemStatus/__tests__/api.test.ts | 4 +- .../__tests__/systemStatus.test.ts | 15 +- .../typings/meteor-kschingiz-elastic-apm.d.ts | 355 --------- meteor/server/worker/worker.ts | 4 +- meteor/yarn.lock | 371 +++++---- package.json | 14 +- packages/blueprints-integration/package.json | 2 +- .../blueprints-integration/src/migrations.ts | 18 +- packages/corelib/package.json | 2 +- packages/documentation/package.json | 2 +- packages/is_node_14.js | 5 - packages/job-worker/package.json | 4 +- .../__tests__/externalMessageQueue.test.ts | 24 +- .../lookahead/__tests__/lookahead.test.ts | 2 +- .../job-worker/src/playout/timings/events.ts | 2 +- packages/live-status-gateway/Dockerfile | 4 +- .../live-status-gateway/Dockerfile.circle | 2 +- packages/live-status-gateway/package.json | 2 +- packages/meteor-lib/package.json | 2 +- packages/mos-gateway/Dockerfile | 4 +- packages/mos-gateway/Dockerfile.circle | 2 +- packages/mos-gateway/package.json | 2 +- packages/openapi/package.json | 2 +- packages/package.json | 4 +- packages/playout-gateway/Dockerfile | 4 +- packages/playout-gateway/Dockerfile.circle | 2 +- packages/playout-gateway/package.json | 2 +- packages/playout-gateway/src/tsrHandler.ts | 2 +- packages/server-core-integration/package.json | 2 +- .../src/lib/methods.ts | 2 +- .../server-core-integration/src/lib/ping.ts | 2 +- .../src/lib/watchDog.ts | 4 +- packages/shared-lib/package.json | 2 +- packages/webui/.eslintrc.cjs | 1 - packages/webui/package.json | 2 +- packages/webui/src/__mocks__/mongo.ts | 2 +- .../data/mos/__tests__/plugin-support.test.ts | 2 + packages/webui/src/client/lib/viewPort.ts | 2 +- .../src/client/ui/Prompter/PrompterView.tsx | 2 +- .../Parts/SegmentTimelinePart.tsx | 2 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 2 +- .../ui/SegmentTimeline/SourceLayerItem.tsx | 2 +- .../ui/Shelf/TimelineDashboardPanel.tsx | 2 +- packages/webui/src/meteor/meteor.js | 7 - packages/webui/src/meteor/tracker.js | 13 - packages/yarn.lock | 324 ++++---- scripts/fixTestFibers.js | 21 - scripts/run.mjs | 7 +- sonar-project.properties | 2 +- 139 files changed, 1592 insertions(+), 3967 deletions(-) delete mode 100644 meteor/__mocks__/Fibers.ts delete mode 100644 meteor/__mocks__/plugins/meteor-async-await.js delete mode 100644 meteor/__mocks__/reactive-var.ts delete mode 100644 meteor/eslint-rules/README.md delete mode 100644 meteor/eslint-rules/index.js delete mode 100644 meteor/eslint-rules/noFocusedTestRule.js delete mode 100644 meteor/eslint-rules/noFocusedTestRule.ts delete mode 100644 meteor/eslint-rules/package.json delete mode 100644 meteor/eslint-rules/utils.js delete mode 100644 meteor/eslint-rules/utils.ts delete mode 100644 meteor/server/api/__tests__/methods.test.ts create mode 100644 meteor/server/api/profiler/apm.ts rename meteor/server/api/{profiler.ts => profiler/index.ts} (68%) delete mode 100644 meteor/server/collections/implementations/base.ts delete mode 100644 meteor/server/typings/meteor-kschingiz-elastic-apm.d.ts delete mode 100644 packages/is_node_14.js delete mode 100644 scripts/fixTestFibers.js diff --git a/.github/actions/setup-meteor/action.yaml b/.github/actions/setup-meteor/action.yaml index 52d26fcaa1..b96960585d 100644 --- a/.github/actions/setup-meteor/action.yaml +++ b/.github/actions/setup-meteor/action.yaml @@ -3,7 +3,5 @@ description: "Setup Meteor" runs: using: "composite" steps: - - run: curl "https://install.meteor.com/?release=2.13.3" | sh - shell: bash - - run: meteor npm install -g yarn + - run: curl "https://install.meteor.com/?release=3.0.4" | sh shell: bash diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index fc4f66d19e..c236b7dfb1 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -29,7 +29,7 @@ jobs: run: | yarn cd meteor - meteor npm run validate:prod-dependencies + yarn validate:prod-dependencies env: CI: true @@ -57,7 +57,7 @@ jobs: run: | yarn cd meteor - meteor npm run validate:all-dependencies + yarn run validate:all-dependencies env: CI: true diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index aa76ac1464..188b49af71 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -46,7 +46,7 @@ jobs: # setup zodern:types. No linters are setup, so this simply installs the packages meteor lint - meteor npm run ci:lint + yarn ci:lint env: CI: true @@ -85,7 +85,7 @@ jobs: # setup zodern:types. No linters are setup, so this simply installs the packages meteor lint - NODE_OPTIONS="--max-old-space-size=6144" meteor npm run unitci --force-exit + NODE_OPTIONS="--max-old-space-size=6144" yarn unitci --force-exit env: CI: true - name: Send coverage @@ -480,38 +480,30 @@ jobs: - blueprints-integration - server-core-integration - shared-lib - node-version: [14.x, 18.x, 20.x, 22.x] + - openapi + node-version: [20.x, 22.x] include: # include additional configs, to run certain packages only for a certain version of node - - node-version: 14.x + - node-version: 20.x package-name: corelib send-coverage: true - - node-version: 14.x + - node-version: 20.x package-name: job-worker send-coverage: true - # manual openapi to avoid testing for 14.x - - node-version: 18.x - package-name: openapi - - node-version: 20.x - package-name: openapi - - node-version: 22.x - package-name: openapi # No tests for the gateways yet - # - node-version: 18.x + # - node-version: 20.x # package-name: playout-gateway - # - node-version: 18.x + # - node-version: 20.x # package-name: mos-gateway - - node-version: 18.x + - node-version: 20.x package-name: live-status-gateway send-coverage: true - - node-version: 18.x + - node-version: 20.x package-name: webui # manual meteor-lib as it only needs a couple of versions - - node-version: 18.x + - node-version: 20.x package-name: meteor-lib send-coverage: true - - node-version: 14.x - package-name: meteor-lib steps: - uses: actions/checkout@v4 @@ -531,7 +523,7 @@ jobs: run: | cd packages yarn config set cacheFolder /home/runner/test-packages-cache - node is_node_14.js && yarn lerna run --ignore openapi install || yarn install + yarn install yarn lerna run --scope \*\*/${{ matrix.package-name }} --include-dependencies --stream build env: CI: true diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index e9750028c4..7ca1a31f2a 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -53,7 +53,7 @@ jobs: - blueprints-integration - server-core-integration - shared-lib - node-version: [14.x, 18.x, 20.x, 22.x] + node-version: [20.x, 22.x] steps: - uses: actions/checkout@v4 diff --git a/.node-version b/.node-version index b492b08635..10fef252a9 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.16 +20.18 diff --git a/DEVELOPER.md b/DEVELOPER.md index df3d084cb0..140ac0712a 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -18,10 +18,8 @@ Follow these instructions to start up Sofie Core in development mode. (For produ ### Prerequisites -- Install [Node.js](https://nodejs.org) 14 (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) -- Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor@2`) -- Install [Node.js](https://nodejs.org) 18 (using the same method you used above, you can uninstall node 14 if needed) -- Install an older version of corepack (`npm install --global corepack@0.15.3`) +- Install [Node.js](https://nodejs.org) 20 (using [nvm](https://github.com/nvm-sh/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) is the recommended way to install Node.js) +- Install [Meteor](https://www.meteor.com/install) (`npm install --global meteor`) - Enable [corepack](https://nodejs.org/api/corepack.html#corepack) (`corepack enable`) as administrator/root. If `corepack` is not found, you may need to install it first with `npm install --global corepack` - If on Windows, you may need to `npm install --global windows-build-tools` but this is not always necessary diff --git a/meteor/.eslintignore b/meteor/.eslintignore index 8b3e1e2f6b..e2a1ee2fca 100644 --- a/meteor/.eslintignore +++ b/meteor/.eslintignore @@ -1,6 +1,5 @@ .meteor public -eslint-rules scripts server/_force_restart.js /packages/ diff --git a/meteor/.eslintrc.js b/meteor/.eslintrc.js index dedc902676..a6d6491a5a 100644 --- a/meteor/.eslintrc.js +++ b/meteor/.eslintrc.js @@ -20,7 +20,7 @@ const tmpRules = { } const tsBase = { - extends: [...tsExtends, 'plugin:custom-rules/all'], + extends: [...tsExtends], plugins: tsPlugins, ...tsParser, settings: { @@ -50,7 +50,6 @@ const tsBase = { allowModules: ['meteor', 'mongodb'], }, ], - 'jest/no-standalone-expect': 'off', // testInFiber confuses the rule ...tmpRules, }, } diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 4e5355b070..8d1724b1db 100644 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -8,19 +8,17 @@ # but you can also edit it by hand. -meteor-base@1.5.1 # Packages every Meteor app needs to have -mongo@1.16.10 # The database Meteor supports right now -reactive-var@1.0.12 # Reactive variable for tracker +meteor@2.0.1 +webapp@2.0.3 +ddp@1.4.2 -ecmascript@0.16.8 # Enable ECMAScript2015+ syntax in app code -typescript@4.9.5 # Enable TypeScript syntax in .ts and .tsx modules -shell-server@0.5.0 # Server-side component of the `meteor shell` command +mongo@2.0.2 # The database Meteor supports right now -tracker@1.3.3 # Meteor's client-side reactive programming library +ecmascript@0.16.9 # Enable ECMAScript2015+ syntax in app code +typescript@5.4.3 # Enable TypeScript syntax in .ts and .tsx modules -dynamic-import@0.7.3 -ostrio:meteor-root -accounts-password@2.4.0 +tracker@1.3.4 # Meteor's client-side reactive programming library + +accounts-password@3.0.2 -julusian:meteor-elastic-apm@2.5.2 zodern:types diff --git a/meteor/.meteor/release b/meteor/.meteor/release index 5152abe9d5..b1e86a359f 100644 --- a/meteor/.meteor/release +++ b/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.16 +METEOR@3.0.4 diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions index 23b868e06f..6048cd7897 100644 --- a/meteor/.meteor/versions +++ b/meteor/.meteor/versions @@ -1,65 +1,59 @@ -accounts-base@2.2.11 -accounts-password@2.4.0 -allow-deny@1.1.1 -autoupdate@1.8.0 -babel-compiler@7.10.5 -babel-runtime@1.5.1 -base64@1.0.12 -binary-heap@1.0.11 -boilerplate-generator@1.7.2 -callback-hook@1.5.1 -check@1.4.1 -ddp@1.4.1 -ddp-client@2.6.2 -ddp-common@1.4.1 -ddp-rate-limiter@1.2.1 -ddp-server@2.7.1 -diff-sequence@1.1.2 -dynamic-import@0.7.3 -ecmascript@0.16.8 -ecmascript-runtime@0.8.1 -ecmascript-runtime-client@0.12.1 -ecmascript-runtime-server@0.11.0 -ejson@1.1.3 -email@2.2.6 -es5-shim@4.8.0 -fetch@0.1.4 -geojson-utils@1.0.11 -hot-code-push@1.0.4 -id-map@1.1.1 -inter-process-messaging@0.1.1 -julusian:meteor-elastic-apm@2.5.2 -kschingiz:meteor-measured@1.0.3 -localstorage@1.2.0 -logging@1.3.4 -meteor@1.11.5 -meteor-base@1.5.1 -minimongo@1.9.4 -modern-browsers@0.1.10 -modules@0.20.0 -modules-runtime@0.13.1 -mongo@1.16.10 -mongo-decimal@0.1.3 -mongo-dev-server@1.1.0 -mongo-id@1.0.8 -npm-mongo@4.17.2 -ordered-dict@1.1.0 -ostrio:meteor-root@1.1.1 -promise@0.12.2 -random@1.2.1 -rate-limit@1.1.1 -react-fast-refresh@0.2.8 -reactive-var@1.0.12 -reload@1.3.1 -retry@1.1.0 -routepolicy@1.1.1 -sha@1.0.9 -shell-server@0.5.0 -socket-stream-client@0.5.2 -tracker@1.3.3 -typescript@4.9.5 -underscore@1.6.1 -url@1.3.2 -webapp@1.13.8 -webapp-hashing@1.1.1 -zodern:types@1.0.9 +accounts-base@3.0.3 +accounts-password@3.0.2 +allow-deny@2.0.0 +babel-compiler@7.11.1 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +boilerplate-generator@2.0.0 +callback-hook@1.6.0 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.0.2 +ddp-common@1.4.4 +ddp-rate-limiter@1.2.2 +ddp-server@3.0.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.9 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.2 +ecmascript-runtime-server@0.11.1 +ejson@1.1.4 +email@3.1.0 +facts-base@1.0.2 +fetch@0.1.5 +geojson-utils@1.0.12 +id-map@1.2.0 +inter-process-messaging@0.1.2 +localstorage@1.2.1 +logging@1.3.5 +meteor@2.0.1 +minimongo@2.0.1 +modern-browsers@0.1.11 +modules@0.20.2 +modules-runtime@0.13.2 +mongo@2.0.2 +mongo-decimal@0.1.4 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@4.17.4 +ordered-dict@1.2.0 +promise@1.0.0 +random@1.2.2 +rate-limit@1.1.2 +react-fast-refresh@0.2.9 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +sha@1.0.10 +socket-stream-client@0.5.3 +tracker@1.3.4 +typescript@5.4.3 +underscore@1.6.4 +url@1.3.4 +webapp@2.0.3 +webapp-hashing@1.1.2 +zodern:types@1.0.13 diff --git a/meteor/Dockerfile b/meteor/Dockerfile index 10b06912e1..cee205aede 100644 --- a/meteor/Dockerfile +++ b/meteor/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:experimental # BUILD WEBUI -FROM node:18 +FROM node:20 COPY packages /opt/core/packages WORKDIR /opt/core/packages @@ -14,8 +14,8 @@ RUN yarn install && yarn build # RUN yarn workspaces focus --production @sofie-automation/job-worker @sofie-automation/corelib # BUILD IMAGE -FROM meteor/node:14.21.4 -RUN curl "https://install.meteor.com/?release=2.13.3" | sh +FROM node:20 +RUN curl "https://install.meteor.com/?release=3.0.4" | sh # Temporary change the NODE_ENV env variable, so that all libraries are installed: ENV NODE_ENV_TMP $NODE_ENV @@ -37,8 +37,8 @@ RUN rm -R /opt/core/packages/webui # Force meteor to setup the runtime RUN meteor --version --allow-superuser -RUN meteor corepack enable -RUN meteor yarn install +RUN corepack enable +RUN yarn install # Restore the NODE_ENV variable: ENV NODE_ENV $NODE_ENV_TMP @@ -50,29 +50,9 @@ RUN npm install RUN mv /opt/bundle/programs/web.browser/assets /opt/bundle/programs/web.browser/app/assets || true # DEPLOY IMAGE -FROM alpine:3.19 - -ENV NODE_VERSION=14.21.4 -ENV NODE_URL="https://static.meteor.com/dev-bundle-node-os/unofficial-builds/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" -ENV DIR_NODE=/usr/local - -RUN apk add --no-cache \ - libstdc++ \ - && apk add --no-cache --virtual .build-deps-full \ - binutils-gold \ - curl \ - gnupg \ - xz - -RUN echo $NODE_URL \ - && curl -sSL "$NODE_URL" | tar -xz -C /usr/local/ && mv $DIR_NODE/node-v${NODE_VERSION}-linux-x64 $DIR_NODE/v$NODE_VERSION - -# add node and npm to path so the commands are available -ENV NODE_PATH $DIR_NODE/v$NODE_VERSION/lib/node_modules -ENV PATH $DIR_NODE/v$NODE_VERSION/bin:$PATH +FROM node:20-alpine -# confirm installation -RUN node -v && npm -v +RUN apk add --no-cache tzdata COPY --from=1 /opt/bundle /opt/core COPY meteor/docker-entrypoint.sh /opt diff --git a/meteor/Dockerfile.circle b/meteor/Dockerfile.circle index 9456265025..1e39e80f81 100644 --- a/meteor/Dockerfile.circle +++ b/meteor/Dockerfile.circle @@ -1,27 +1,7 @@ # DEPLOY IMAGE -FROM alpine:3.19 +FROM node:20-alpine -ENV NODE_VERSION=14.21.4 -ENV NODE_URL="https://static.meteor.com/dev-bundle-node-os/unofficial-builds/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" -ENV DIR_NODE=/usr/local - -RUN apk add --no-cache \ - libstdc++ \ - && apk add --no-cache --virtual .build-deps-full \ - binutils-gold \ - curl \ - gnupg \ - xz - -RUN echo $NODE_URL \ - && curl -sSL "$NODE_URL" | tar -xz -C /usr/local/ && mv $DIR_NODE/node-v${NODE_VERSION}-linux-x64 $DIR_NODE/v$NODE_VERSION - -# add node and npm to path so the commands are available -ENV NODE_PATH $DIR_NODE/v$NODE_VERSION/lib/node_modules -ENV PATH $DIR_NODE/v$NODE_VERSION/bin:$PATH - -# confirm installation -RUN node -v && npm -v +RUN apk add --no-cache tzdata COPY meteor/bundle /opt/core COPY meteor/docker-entrypoint.sh /opt diff --git a/meteor/__mocks__/Fibers.ts b/meteor/__mocks__/Fibers.ts deleted file mode 100644 index d17290fce6..0000000000 --- a/meteor/__mocks__/Fibers.ts +++ /dev/null @@ -1,34 +0,0 @@ -let Fiber: any -try { - Fiber = require('fibers-npm') -} catch (e: any) { - if (e.toString().match(/Missing binary/)) { - // Temporary workaround: - throw Error(` -Note: When you get the "Missing binary"-error when running in Jest -be sure you have run npm install (so that the postInstall script has run) -and that you ran npm install with the correct Node version - -Original error: -${e.toString()}`) - // Head over to - // meteor/node_modules/fibers/fibers.js - // and add this line to line 13: - // if (process.env.JEST_WORKER_ID !== undefined ) modPath += '.node' - } else throw e -} -/** - * Run function in a Fiber - * Example Jest test: - * test('tempTestAsync', async () => { - * await runInFiber(() => { - * // This code runs in a fiber - * const val = tempTestAsync(1,2,3) - * expect(val).toEqual(1 + 2 + 3) - * }) - * }) - */ -export function isInFiber(): boolean { - return !!Fiber.current -} -export { Fiber } diff --git a/meteor/__mocks__/_setupMocks.ts b/meteor/__mocks__/_setupMocks.ts index c869b5d3e4..b4508a82bb 100644 --- a/meteor/__mocks__/_setupMocks.ts +++ b/meteor/__mocks__/_setupMocks.ts @@ -1,15 +1,10 @@ import { setLogLevel } from '../server/logging' -import { Fiber } from './Fibers' import { resetRandomId } from './random' -import { makeCompatible } from 'meteor-promise' import { LogLevel } from '../server/lib/tempLib' import { SupressLogMessages } from './suppressLogging' // This file is run before all tests start. -// Set up how Meteor handles Promises & Fibers: -makeCompatible(Promise, Fiber) - // 'Mock' the random string generator jest.mock('nanoid', (...args) => require('./random').setup(args), { virtual: true }) @@ -21,7 +16,6 @@ jest.mock('meteor/check', (...args) => require('./check').setup(args), { virtual jest.mock('meteor/tracker', (...args) => require('./tracker').setup(args), { virtual: true }) jest.mock('meteor/accounts-base', (...args) => require('./accounts-base').setup(args), { virtual: true }) jest.mock('meteor/ejson', (...args) => require('./ejson').setup(args), { virtual: true }) -jest.mock('meteor/reactive-var', (...args) => require('./reactive-var').setup(args), { virtual: true }) jest.mock('meteor/mdg:validated-method', (...args) => require('./validated-method').setup(args), { virtual: true }) jest.mock('meteor/julusian:meteor-elastic-apm', (...args) => require('./meteor-elastic-apm').setup(args), { diff --git a/meteor/__mocks__/helpers/jest.ts b/meteor/__mocks__/helpers/jest.ts index 192fd073e9..1ed979896d 100644 --- a/meteor/__mocks__/helpers/jest.ts +++ b/meteor/__mocks__/helpers/jest.ts @@ -1,47 +1,3 @@ -/* eslint-disable jest/no-export, jest/valid-title, jest/expect-expect, jest/no-focused-tests */ -import { runInFiber } from '../meteor' - -export function beforeAllInFiber(fcn: () => void | Promise, timeout?: number): void { - beforeAll(async () => { - await runInFiber(fcn) - }, timeout) -} -export function afterAllInFiber(fcn: () => void | Promise, timeout?: number): void { - afterAll(async () => { - await runInFiber(fcn) - }, timeout) -} -export function beforeEachInFiber(fcn: () => void | Promise, timeout?: number): void { - beforeEach(async () => { - await runInFiber(fcn) - }, timeout) -} -export function afterEachInFiber(fcn: () => void | Promise, timeout?: number): void { - afterEach(async () => { - await runInFiber(fcn) - }, timeout) -} - -export function testInFiber(testName: string, fcn: () => void | Promise, timeout?: number): void { - test( - testName, - async () => { - await runInFiber(fcn) - }, - timeout - ) -} - -export function testInFiberOnly(testName: string, fcn: () => void | Promise, timeout?: number): void { - // eslint-disable-next-line custom-rules/no-focused-test - test.only( - testName, - async () => { - await runInFiber(fcn) - }, - timeout - ) -} const orgSetTimeout = setTimeout const DateOrg = Date export async function runAllTimers(): Promise { @@ -98,5 +54,3 @@ export async function waitUntil(expectFcn: () => void | Promise, maxWaitTi } } } - -// testInFiber.only = testInFiberOnly diff --git a/meteor/__mocks__/helpers/lib.ts b/meteor/__mocks__/helpers/lib.ts index 5424c55a43..06b0adc241 100644 --- a/meteor/__mocks__/helpers/lib.ts +++ b/meteor/__mocks__/helpers/lib.ts @@ -24,7 +24,7 @@ const METHOD_NAMES = [ 'remove', 'update', 'upsert', - '_ensureIndex', + 'createIndex', 'findFetchAsync', 'findOneAsync', 'insertAsync', diff --git a/meteor/__mocks__/meteor.ts b/meteor/__mocks__/meteor.ts index 693aab07e3..1b2ec69418 100644 --- a/meteor/__mocks__/meteor.ts +++ b/meteor/__mocks__/meteor.ts @@ -1,6 +1,3 @@ -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import * as _ from 'underscore' -import { Fiber } from './Fibers' import { MongoMock } from './mongo' let controllableDefer = false @@ -167,7 +164,10 @@ export namespace MeteorMock { export function methods(addMethods: { [name: string]: Function }): void { Object.assign(mockMethods, addMethods) } - export function call(methodName: string, ...args: any[]): any { + export function call(_methodName: string, ..._args: any[]): any { + throw new Error(500, `Meteor.call should not be used, use Meteor.callAsync instead`) + } + export async function callAsync(methodName: string, ...args: any[]): Promise { const fcn: Function = mockMethods[methodName] if (!fcn) { console.log(methodName) @@ -176,26 +176,10 @@ export namespace MeteorMock { throw new Error(404, `Method '${methodName}' not found`) } - const lastArg = args.length > 0 && args[args.length - 1] - if (lastArg && typeof lastArg === 'function') { - const callback = args.pop() + // Defer + await sleepNoFakeTimers(0) - defer(() => { - try { - Promise.resolve(fcn.call(getMethodContext(), ...args)) - .then((result) => { - callback(undefined, result) - }) - .catch((e) => { - callback(e) - }) - } catch (e) { - callback(e) - } - }) - } else { - return waitForPromiseLocal(Promise.resolve(fcn.call(getMethodContext(), ...args))) - } + return fcn.call(getMethodContext(), ...args) } export function apply( methodName: string, @@ -213,12 +197,29 @@ export namespace MeteorMock { // but it'll do for now: call(methodName, ...args, asyncCallback) } + export async function applyAsync( + methodName: string, + args: any[], + _options?: { + wait?: boolean + onResultReceived?: Function + returnStubValue?: boolean + throwStubExceptions?: boolean + } + ): Promise { + // ? + // This is a bad mock, since it doesn't support any of the options.. + // but it'll do for now: + return callAsync(methodName, ...args) + } export function absoluteUrl(path?: string): string { return path + '' // todo } export function setTimeout(fcn: () => void | Promise, time: number): number { return $.setTimeout(() => { - runInFiber(fcn).catch(console.error) + Promise.resolve() + .then(async () => fcn()) + .catch(console.error) }, time) as number } export function clearTimeout(timer: number): void { @@ -226,7 +227,9 @@ export namespace MeteorMock { } export function setInterval(fcn: () => void | Promise, time: number): number { return $.setInterval(() => { - runInFiber(fcn).catch(console.error) + Promise.resolve() + .then(async () => fcn()) + .catch(console.error) }, time) as number } export function clearInterval(timer: number): void { @@ -234,7 +237,9 @@ export namespace MeteorMock { } export function defer(fcn: () => void | Promise): void { return (controllableDefer ? $.setTimeout : $.orgSetTimeout)(() => { - runInFiber(fcn).catch(console.error) + Promise.resolve() + .then(async () => fcn()) + .catch(console.error) }, 0) } @@ -242,43 +247,13 @@ export namespace MeteorMock { mockStartupFunctions.push(fcn) } - export function wrapAsync(fcn: Function, context?: Object): any { - return (...args: any[]) => { - const fiber = Fiber.current - if (!fiber) throw new Error(500, `It appears that wrapAsync isn't running in a fiber`) - - const callback = (err: any, value: any) => { - if (err) { - fiber.throwInto(err) - } else { - fiber.run(value) - } - } - fcn.apply(context, [...args, callback]) - - const returnValue = Fiber.yield() - return returnValue - } - } - export function publish(publicationName: string, handler: Function): any { publications[publicationName] = handler } export function bindEnvironment(fcn: Function): any { - { - // the outer bindEnvironment must be called from a fiber - const fiber = Fiber.current - if (!fiber) throw new Error(500, `It appears that bindEnvironment isn't running in a fiber`) - } - return (...args: any[]) => { - const fiber = Fiber.current - if (fiber) { - return fcn(...args) - } else { - return runInFiber(() => fcn(...args)).catch(console.error) - } + return fcn(...args) } } export let users: MongoMock.Collection | undefined = undefined @@ -287,12 +262,12 @@ export namespace MeteorMock { /** * Run the Meteor.startup() functions */ - export function mockRunMeteorStartup(): void { - _.each(mockStartupFunctions, (fcn) => { - fcn() - }) + export async function mockRunMeteorStartup(): Promise { + for (const fcn of mockStartupFunctions) { + await fcn() + } - waitTimeNoFakeTimers(10) // So that any observers or defers has had time to run. + await waitTimeNoFakeTimers(10) // So that any observers or defers has had time to run. } export function mockLoginUser(newUser: Meteor.User): void { mockUser = newUser @@ -310,25 +285,6 @@ export namespace MeteorMock { return publications } - // locally defined function here, so there are no import to the rest of the code - const waitForPromiseLocal: (p: Promise) => T = wrapAsync(function waitForPromises( - p: Promise, - cb: (err: any | null, result?: any) => T - ) { - if (cb === undefined && typeof p === 'function') { - cb = p as any - p = undefined as any - } - - Promise.resolve(p) - .then((result) => { - cb(null, result) - }) - .catch((e) => { - cb(e) - }) - }) - /** Wait for time to pass ( unaffected by jest.useFakeTimers() ) */ export async function sleepNoFakeTimers(time: number): Promise { return new Promise((resolve) => $.orgSetTimeout(resolve, time)) @@ -341,48 +297,6 @@ export function setup(): any { } /** Wait for time to pass ( unaffected by jest.useFakeTimers() ) */ -export function waitTimeNoFakeTimers(time: number): void { - waitForPromise(MeteorMock.sleepNoFakeTimers(time)) -} -export const waitForPromise: (p: Promise) => T = MeteorMock.wrapAsync(function waitForPromises( - p: Promise, - cb: (err: any | null, result?: any) => T -) { - if (MeteorMock.isClient) throw new MeteorMock.Error(500, `waitForPromise can't be used client-side`) - if (cb === undefined && typeof p === 'function') { - cb = p as any - p = undefined as any - } - - Promise.resolve(p) - .then((result) => { - cb(null, result) - }) - .catch((e) => { - cb(e) - }) -}) - -export async function runInFiber(fcn: () => T | Promise): Promise { - return new Promise((resolve, reject) => { - Fiber(() => { - try { - // Run the function - const out = fcn() - if (out instanceof Promise) { - out.then(resolve).catch((e) => { - console.log('Error: ' + e) - reject(e) - }) - } else { - // the function has finished - resolve(out) - } - } catch (e: any) { - // Note: we cannot use - console.log('Error: ' + stringifyError(e)) - reject(e) - } - }).run() - }) +export async function waitTimeNoFakeTimers(time: number): Promise { + return MeteorMock.sleepNoFakeTimers(time) } diff --git a/meteor/__mocks__/mongo.ts b/meteor/__mocks__/mongo.ts index cf17069139..d39e071ef0 100644 --- a/meteor/__mocks__/mongo.ts +++ b/meteor/__mocks__/mongo.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import * as _ from 'underscore' import { literal, ProtectedString, unprotectString, protectString, getRandomString } from '../server/lib/tempLib' -import { sleep } from '../server/lib/lib' import { RandomMock } from './random' import { MeteorMock } from './meteor' import { Random } from 'meteor/random' @@ -10,21 +9,19 @@ import type { AnyBulkWriteOperation } from 'mongodb' import { FindOneOptions, FindOptions, + MongoCursor, MongoReadOnlyCollection, ObserveCallbacks, ObserveChangesCallbacks, UpdateOptions, UpsertOptions, } from '@sofie-automation/meteor-lib/dist/collections/lib' -import { - mongoWhere, - mongoFindOptions, - mongoModify, - MongoQuery, - MongoModifier, -} from '@sofie-automation/corelib/dist/mongo' -import { Mongo } from 'meteor/mongo' +import { mongoWhere, mongoFindOptions, mongoModify, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { AsyncOnlyMongoCollection, AsyncOnlyReadOnlyMongoCollection } from '../server/collections/collection' +import type { + MinimalMeteorMongoCollection, + MinimalMongoCursor, +} from '../server/collections/implementations/asyncCollection' const clone = require('fast-clone') export namespace MongoMock { @@ -46,9 +43,9 @@ export namespace MongoMock { } const mockCollections: MockCollections = {} - export type MongoCollection = {} - export class Collection implements MongoCollection { + export class Collection implements Omit, 'find'> { public _name: string + private _isTemporaryCollection: boolean private _options: any = {} // @ts-expect-error used in test to check that it's a mock private _isMock = true as const @@ -59,11 +56,15 @@ export namespace MongoMock { constructor(name: string | null, options?: { transform?: never }) { this._options = options || {} this._name = name || getRandomString() // If `null`, then its an in memory unique collection + this._isTemporaryCollection = name === null if (this._options.transform) throw new Error('document transform is no longer supported') } - find(query: any, options?: FindOptions) { + find( + query: any, + options?: FindOptions + ): MinimalMongoCursor & { _fetchRaw: () => T[] } & Pick, 'fetch' | 'forEach'> { if (_.isString(query)) query = { _id: query } query = query || {} @@ -96,13 +97,28 @@ export namespace MongoMock { _fetchRaw: () => { return docs }, + fetchAsync: async () => { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + + return clone(docs) + }, fetch: () => { + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + return clone(docs) }, - count: () => { + countAsync: async () => { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + return docs.length }, - observe(clbs: ObserveCallbacks): Meteor.LiveQueryHandle { + async observeAsync(clbs: ObserveCallbacks): Promise { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + const id = Random.id(5) observers.push( literal>({ @@ -117,7 +133,10 @@ export namespace MongoMock { }, } }, - observeChanges(clbs: ObserveChangesCallbacks): Meteor.LiveQueryHandle { + async observeChangesAsync(clbs: ObserveChangesCallbacks): Promise { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + // todo - finish implementing uses of callbacks const id = Random.id(5) observers.push( @@ -133,18 +152,43 @@ export namespace MongoMock { }, } }, - forEach(f: any) { + forEach: (f: any) => { + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + docs.forEach(f) }, - map(f: any) { - return docs.map(f) - }, + // async mapAsync(f: any) { + // return docs.map(f) + // }, } } + async findOneAsync(query: MongoQuery, options?: FindOneOptions) { + const docs = await this.find(query, options).fetchAsync() + return docs[0] + } findOne(query: MongoQuery, options?: FindOneOptions) { - return this.find(query, options).fetch()[0] + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + + const docs = this.find(query, options).fetch() + return docs[0] + } + + async updateAsync(query: any, modifier: any, options?: UpdateOptions): Promise { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + + return this.updateRaw(query, modifier, options) + } + update(query: any, modifier: any, options?: UpdateOptions): number { + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + + return this.updateRaw(query, modifier, options) } - update(query: MongoQuery, modifier: MongoModifier, options?: UpdateOptions): number { + + private updateRaw(query: any, modifier: any, options?: UpdateOptions): number { const unimplementedUsedOptions = _.without(_.keys(options), 'multi') if (unimplementedUsedOptions.length > 0) { throw new Error(`update being performed using unimplemented options: ${unimplementedUsedOptions}`) @@ -178,7 +222,20 @@ export namespace MongoMock { return docs.length } - insert(doc: T): T['_id'] { + + async insertAsync(doc: any): Promise { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + + return this.insertRaw(doc) + } + insert(doc: any): string { + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + + return this.insertRaw(doc) + } + private insertRaw(doc: any): string { const d = _.clone(doc) if (!d._id) d._id = protectString(RandomMock.id()) @@ -207,25 +264,59 @@ export namespace MongoMock { return d._id } + + async upsertAsync( + query: any, + modifier: any, + options?: UpsertOptions + ): Promise<{ numberAffected: number | undefined; insertedId: string | undefined }> { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + + return this.upsertRaw(query, modifier, options) + } upsert( query: any, - modifier: MongoModifier, + modifier: any, + options?: UpsertOptions + ): { numberAffected: number | undefined; insertedId: string | undefined } { + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + + return this.upsertRaw(query, modifier, options) + } + private upsertRaw( + query: any, + modifier: any, options?: UpsertOptions - ): { numberAffected: number | undefined; insertedId: T['_id'] | undefined } { + ): { numberAffected: number | undefined; insertedId: string | undefined } { const id = _.isString(query) ? query : query._id const docs = this.find(id)._fetchRaw() if (docs.length) { - const count = this.update(docs[0]._id, modifier, options) + const count = this.updateRaw(docs[0]._id, modifier, options) return { insertedId: undefined, numberAffected: count } } else { const doc = mongoModify(query, { _id: id } as any, modifier) - const insertedId = this.insert(doc) + const insertedId = this.insertRaw(doc) return { insertedId: insertedId, numberAffected: undefined } } } + + async removeAsync(query: any): Promise { + // Force this to be performed async + await MeteorMock.sleepNoFakeTimers(0) + + return this.removeRaw(query) + } remove(query: any): number { + if (!this._isTemporaryCollection) + throw new Meteor.Error(500, 'sync methods can only be used for unnamed collections') + + return this.removeRaw(query) + } + private removeRaw(query: any): number { const docs = this.find(query)._fetchRaw() _.each(docs, (doc) => { @@ -247,41 +338,53 @@ export namespace MongoMock { return docs.length } - _ensureIndex(_obj: any) { + createIndex(_obj: any) { // todo } allow() { // todo } - rawCollection() { + + rawDatabase(): any { + throw new Error('Not implemented') + } + rawCollection(): any { return { bulkWrite: async (updates: AnyBulkWriteOperation[], _options: unknown) => { - await sleep(this.asyncBulkWriteDelay) + await MeteorMock.sleepNoFakeTimers(this.asyncBulkWriteDelay) for (const update of updates) { if ('insertOne' in update) { - this.insert(update.insertOne.document) + await this.insertAsync(update.insertOne.document) } else if ('updateOne' in update) { if (update.updateOne.upsert) { - this.upsert(update.updateOne.filter, update.updateOne.update as any, { multi: false }) + await this.upsertAsync(update.updateOne.filter, update.updateOne.update as any, { + multi: false, + }) } else { - this.update(update.updateOne.filter, update.updateOne.update as any, { multi: false }) + await this.updateAsync(update.updateOne.filter, update.updateOne.update as any, { + multi: false, + }) } } else if ('updateMany' in update) { if (update.updateMany.upsert) { - this.upsert(update.updateMany.filter, update.updateMany.update as any, { multi: true }) + await this.upsertAsync(update.updateMany.filter, update.updateMany.update as any, { + multi: true, + }) } else { - this.update(update.updateMany.filter, update.updateMany.update as any, { multi: true }) + await this.updateAsync(update.updateMany.filter, update.updateMany.update as any, { + multi: true, + }) } } else if ('deleteOne' in update) { - const docs = this.find(update.deleteOne.filter).fetch() + const docs = await this.find(update.deleteOne.filter).fetchAsync() if (docs.length) { - this.remove(docs[0]._id) + await this.removeAsync(docs[0]._id) } } else if ('deleteMany' in update) { - this.remove(update.deleteMany.filter) + await this.removeAsync(update.deleteMany.filter) } else if (update['replaceOne']) { - this.upsert(update.replaceOne.filter, update.replaceOne.replacement) + await this.upsertAsync(update.replaceOne.filter, update.replaceOne.replacement) } } }, @@ -341,7 +444,7 @@ export namespace MongoMock { export function getInnerMockCollection }>( collection: MongoReadOnlyCollection | AsyncOnlyReadOnlyMongoCollection - ): Mongo.Collection { + ): MinimalMeteorMongoCollection { return (collection as any).mockCollection } } diff --git a/meteor/__mocks__/plugins/meteor-async-await.js b/meteor/__mocks__/plugins/meteor-async-await.js deleted file mode 100644 index 2b633255eb..0000000000 --- a/meteor/__mocks__/plugins/meteor-async-await.js +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint-disable */ -// Copied from: https://github.com/meteor/meteor/blob/7a168776b444a48f18c9ba5ce72363360e59e678/npm-packages/meteor-babel/plugins/async-await.js - -'use strict' - -module.exports = function (babel) { - const t = babel.types - - return { - name: 'transform-meteor-async-await', - visitor: { - Function: { - exit: function (path) { - const node = path.node - if (!node.async) { - return - } - - // The original function becomes a non-async function that - // returns a Promise. - node.async = false - - // The inner function should inherit lexical environment items - // like `this`, `super`, and `arguments` from the outer - // function, and arrow functions provide exactly that behavior. - const innerFn = t.arrowFunctionExpression( - // The inner function has no parameters of its own, but can - // refer to the outer parameters of the original function. - [], - node.body, - // The inner function called by Promise.asyncApply should be - // async if we have native async/await support. - !!this.opts.useNativeAsyncAwait - ) - - const promiseResultExpression = t.callExpression( - t.memberExpression(t.identifier('Promise'), t.identifier('asyncApply'), false), - [innerFn] - ) - - // Calling the async function with Promise.asyncApply is - // important to ensure that the part before the first await - // expression runs synchronously in its own Fiber, even when - // there is native support for async/await. - if (node.type === 'ArrowFunctionExpression') { - node.body = promiseResultExpression - } else { - node.body = t.blockStatement([t.returnStatement(promiseResultExpression)]) - } - }, - }, - - AwaitExpression: function (path) { - if (this.opts.useNativeAsyncAwait) { - // No need to transform await expressions if we have native - // support for them. - return - } - - const node = path.node - path.replaceWith( - t.callExpression( - t.memberExpression( - t.identifier('Promise'), - t.identifier(node.all ? 'awaitAll' : 'await'), - false - ), - [node.argument] - ) - ) - }, - }, - } -} diff --git a/meteor/__mocks__/reactive-var.ts b/meteor/__mocks__/reactive-var.ts deleted file mode 100644 index 3edc490a63..0000000000 --- a/meteor/__mocks__/reactive-var.ts +++ /dev/null @@ -1,18 +0,0 @@ -class ReactiveVar { - val: T - constructor(initVal: T) { - this.val = initVal - } - get = () => { - return this.val - } - set = (newVal: T) => { - this.val = newVal - } -} - -export function setup(): any { - return { - ReactiveVar, - } -} diff --git a/meteor/__mocks__/suppressLogging.ts b/meteor/__mocks__/suppressLogging.ts index ef64368ede..a50865afe7 100644 --- a/meteor/__mocks__/suppressLogging.ts +++ b/meteor/__mocks__/suppressLogging.ts @@ -40,6 +40,7 @@ export class SupressLogMessages { static expectAllMessagesToHaveBeenHandled(): void { const unhandledSuppressMessages = [...SupressLogMessages.suppressMessages] SupressLogMessages.suppressMessages.length = 0 + // eslint-disable-next-line jest/no-standalone-expect expect(unhandledSuppressMessages).toHaveLength(0) } } diff --git a/meteor/eslint-rules/README.md b/meteor/eslint-rules/README.md deleted file mode 100644 index a69061ec9c..0000000000 --- a/meteor/eslint-rules/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The typescript in this folder needs compiling before use, so it is easiest to commit the compiled js too. - -It can be recompiled with `meteor npx --no-install tsc eslint-rules/*.ts --module commonjs --skipLibCheck`, then the rules can be referenced as if they are npm installed. diff --git a/meteor/eslint-rules/index.js b/meteor/eslint-rules/index.js deleted file mode 100644 index b982cd1c4b..0000000000 --- a/meteor/eslint-rules/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const noFocusedTestRule = require('./noFocusedTestRule') - -module.exports = { - rules: { - 'no-focused-test': noFocusedTestRule.default, - }, - configs: { - all: { - plugins: ['custom-rules'], - rules: { - 'custom-rules/no-focused-test': 'error', - } - } - } -} diff --git a/meteor/eslint-rules/noFocusedTestRule.js b/meteor/eslint-rules/noFocusedTestRule.js deleted file mode 100644 index 53b1e5fffe..0000000000 --- a/meteor/eslint-rules/noFocusedTestRule.js +++ /dev/null @@ -1,99 +0,0 @@ -"use strict"; -exports.__esModule = true; -/** Based on https://github.com/jest-community/eslint-plugin-jest/blob/7cba106d0ade884a231b61098fa0bf33af2a1ad7/src/rules/no-focused-tests.ts */ -var experimental_utils_1 = require("@typescript-eslint/utils"); -var utils_1 = require("./utils"); -var findOnlyNode = function (node) { - var callee = node.callee.type === experimental_utils_1.AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee.type === experimental_utils_1.AST_NODE_TYPES.CallExpression - ? node.callee.callee - : node.callee; - if (callee.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression) { - if (callee.object.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression) { - if (utils_1.isSupportedAccessor(callee.object.property, 'only')) { - return callee.object.property; - } - } - if (utils_1.isSupportedAccessor(callee.property, 'only')) { - return callee.property; - } - } - return null; -}; -exports["default"] = utils_1.createRule({ - name: __filename, - meta: { - docs: { - // category: 'Best Practices', - description: 'Disallow focused tests', - recommended: 'error', - suggestion: true - }, - messages: { - focusedTest: 'Unexpected focused test.', - suggestRemoveFocus: 'Remove focus from test.' - }, - schema: [], - type: 'suggestion', - hasSuggestions: true - }, - defaultOptions: [], - create: function (context) { return ({ - CallExpression: function (node) { - if (node.callee.type === experimental_utils_1.AST_NODE_TYPES.Identifier && node.callee.name === 'testInFiberOnly') { - context.report({ - messageId: 'focusedTest', - node: node, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix: function (fixer) { - return fixer.removeRange([node.range[0], node.range[0] + 1]); - } - }, - ] - }); - return; - } - if (!utils_1.isDescribeCall(node) && !utils_1.isTestCaseCall(node)) { - return; - } - if (utils_1.getNodeName(node).startsWith('f')) { - context.report({ - messageId: 'focusedTest', - node: node, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix: function (fixer) { - return fixer.removeRange([node.range[0], node.range[0] + 1]); - } - }, - ] - }); - return; - } - var onlyNode = findOnlyNode(node); - if (!onlyNode) { - return; - } - context.report({ - messageId: 'focusedTest', - node: onlyNode, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix: function (fixer) { - return fixer.removeRange([ - onlyNode.range[0] - 1, - onlyNode.range[1] + - Number(onlyNode.type !== experimental_utils_1.AST_NODE_TYPES.Identifier), - ]); - } - }, - ] - }); - } - }); } -}); diff --git a/meteor/eslint-rules/noFocusedTestRule.ts b/meteor/eslint-rules/noFocusedTestRule.ts deleted file mode 100644 index 11f3aabc5e..0000000000 --- a/meteor/eslint-rules/noFocusedTestRule.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** Based on https://github.com/jest-community/eslint-plugin-jest/blob/7cba106d0ade884a231b61098fa0bf33af2a1ad7/src/rules/no-focused-tests.ts */ -import { AST_NODE_TYPES } from '@typescript-eslint/utils' -import { - AccessorNode, - JestFunctionCallExpression, - createRule, - getNodeName, - isDescribeCall, - isSupportedAccessor, - isTestCaseCall, -} from './utils' - -const findOnlyNode = (node: JestFunctionCallExpression): AccessorNode<'only'> | null => { - const callee = - node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee.type === AST_NODE_TYPES.CallExpression - ? node.callee.callee - : node.callee - - if (callee.type === AST_NODE_TYPES.MemberExpression) { - if (callee.object.type === AST_NODE_TYPES.MemberExpression) { - if (isSupportedAccessor(callee.object.property, 'only')) { - return callee.object.property - } - } - - if (isSupportedAccessor(callee.property, 'only')) { - return callee.property - } - } - - return null -} - -export default createRule({ - name: __filename, - meta: { - docs: { - // category: 'Best Practices', - description: 'Disallow focused tests', - recommended: 'error', - suggestion: true, - }, - messages: { - focusedTest: 'Unexpected focused test.', - suggestRemoveFocus: 'Remove focus from test.', - }, - schema: [], - type: 'suggestion', - hasSuggestions: true, - }, - defaultOptions: [], - create: (context) => ({ - CallExpression(node) { - if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'testInFiberOnly') { - context.report({ - messageId: 'focusedTest', - node, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix: (fixer) => fixer.removeRange([node.range[0], node.range[0] + 1]), - }, - ], - }) - - return - } - - if (!isDescribeCall(node) && !isTestCaseCall(node)) { - return - } - - if (getNodeName(node).startsWith('f')) { - context.report({ - messageId: 'focusedTest', - node, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix: (fixer) => fixer.removeRange([node.range[0], node.range[0] + 1]), - }, - ], - }) - - return - } - - const onlyNode = findOnlyNode(node) - - if (!onlyNode) { - return - } - - context.report({ - messageId: 'focusedTest', - node: onlyNode, - suggest: [ - { - messageId: 'suggestRemoveFocus', - fix: (fixer) => - fixer.removeRange([ - onlyNode.range[0] - 1, - onlyNode.range[1] + Number(onlyNode.type !== AST_NODE_TYPES.Identifier), - ]), - }, - ], - }) - }, - }), -}) diff --git a/meteor/eslint-rules/package.json b/meteor/eslint-rules/package.json deleted file mode 100644 index 724013b78b..0000000000 --- a/meteor/eslint-rules/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "eslint-plugin-custom-rules", - "version": "0.0.1", - "license": "MIT", - "main": "index.js" -} diff --git a/meteor/eslint-rules/utils.js b/meteor/eslint-rules/utils.js deleted file mode 100644 index 88293c70e8..0000000000 --- a/meteor/eslint-rules/utils.js +++ /dev/null @@ -1,484 +0,0 @@ -"use strict"; -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; -exports.__esModule = true; -exports.scopeHasLocalReference = exports.isDescribeCall = exports.isTestCaseCall = exports.getTestCallExpressionsFromDeclaredVariables = exports.isHook = exports.isFunction = exports.getNodeName = exports.TestCaseProperty = exports.DescribeProperty = exports.HookName = exports.TestCaseName = exports.DescribeAlias = exports.parseExpectCall = exports.isParsedEqualityMatcherCall = exports.EqualityMatcher = exports.ModifierName = exports.isExpectMember = exports.isExpectCall = exports.getAccessorValue = exports.isSupportedAccessor = exports.isIdentifier = exports.hasOnlyOneArgument = exports.getStringValue = exports.isStringNode = exports.followTypeAssertionChain = exports.createRule = void 0; -/** https://github.com/jest-community/eslint-plugin-jest/blob/540326879df242daa3d96f43903178e36ba6b546/src/rules/utils.ts */ -// import { parse as parsePath } from 'path'; -var experimental_utils_1 = require("@typescript-eslint/utils"); -// import { version } from '../../package.json'; -// const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest'; -exports.createRule = experimental_utils_1.ESLintUtils.RuleCreator(function (name) { - return "local:" + name; - // const ruleName = parsePath(name).name; - // return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`; -}); -var isTypeCastExpression = function (node) { - return node.type === experimental_utils_1.AST_NODE_TYPES.TSAsExpression || - node.type === experimental_utils_1.AST_NODE_TYPES.TSTypeAssertion; -}; -var followTypeAssertionChain = function (expression) { - return isTypeCastExpression(expression) - ? exports.followTypeAssertionChain(expression.expression) - : expression; -}; -exports.followTypeAssertionChain = followTypeAssertionChain; -/** - * Checks if the given `node` is a `StringLiteral`. - * - * If a `value` is provided & the `node` is a `StringLiteral`, - * the `value` will be compared to that of the `StringLiteral`. - * - * @param {Node} node - * @param {V} [value] - * - * @return {node is StringLiteral} - * - * @template V - */ -var isStringLiteral = function (node, value) { - return node.type === experimental_utils_1.AST_NODE_TYPES.Literal && - typeof node.value === 'string' && - (value === undefined || node.value === value); -}; -/** - * Checks if the given `node` is a `TemplateLiteral`. - * - * Complex `TemplateLiteral`s are not considered specific, and so will return `false`. - * - * If a `value` is provided & the `node` is a `TemplateLiteral`, - * the `value` will be compared to that of the `TemplateLiteral`. - * - * @param {Node} node - * @param {V} [value] - * - * @return {node is TemplateLiteral} - * - * @template V - */ -var isTemplateLiteral = function (node, value) { - return node.type === experimental_utils_1.AST_NODE_TYPES.TemplateLiteral && - node.quasis.length === 1 && // bail out if not simple - (value === undefined || node.quasis[0].value.raw === value); -}; -/** - * Checks if the given `node` is a {@link StringNode}. - * - * @param {Node} node - * @param {V} [specifics] - * - * @return {node is StringNode} - * - * @template V - */ -var isStringNode = function (node, specifics) { - return isStringLiteral(node, specifics) || isTemplateLiteral(node, specifics); -}; -exports.isStringNode = isStringNode; -/** - * Gets the value of the given `StringNode`. - * - * If the `node` is a `TemplateLiteral`, the `raw` value is used; - * otherwise, `value` is returned instead. - * - * @param {StringNode} node - * - * @return {S} - * - * @template S - */ -var getStringValue = function (node) { - return isTemplateLiteral(node) ? node.quasis[0].value.raw : node.value; -}; -exports.getStringValue = getStringValue; -/** - * Guards that the given `call` has only one `argument`. - * - * @param {CallExpression} call - * - * @return {call is CallExpressionWithSingleArgument} - */ -var hasOnlyOneArgument = function (call) { return call.arguments.length === 1; }; -exports.hasOnlyOneArgument = hasOnlyOneArgument; -/** - * Checks if the given `node` is an `Identifier`. - * - * If a `name` is provided, & the `node` is an `Identifier`, - * the `name` will be compared to that of the `identifier`. - * - * @param {Node} node - * @param {V} [name] - * - * @return {node is KnownIdentifier} - * - * @template V - */ -var isIdentifier = function (node, name) { - return node.type === experimental_utils_1.AST_NODE_TYPES.Identifier && - (name === undefined || node.name === name); -}; -exports.isIdentifier = isIdentifier; -/** - * Checks if the given `node` is a "supported accessor". - * - * This means that it's a node can be used to access properties, - * and who's "value" can be statically determined. - * - * `MemberExpression` nodes most commonly contain accessors, - * but it's possible for other nodes to contain them. - * - * If a `value` is provided & the `node` is an `AccessorNode`, - * the `value` will be compared to that of the `AccessorNode`. - * - * Note that `value` here refers to the normalised value. - * The property that holds the value is not always called `name`. - * - * @param {Node} node - * @param {V} [value] - * - * @return {node is AccessorNode} - * - * @template V - */ -var isSupportedAccessor = function (node, value) { - return exports.isIdentifier(node, value) || exports.isStringNode(node, value); -}; -exports.isSupportedAccessor = isSupportedAccessor; -/** - * Gets the value of the given `AccessorNode`, - * account for the different node types. - * - * @param {AccessorNode} accessor - * - * @return {S} - * - * @template S - */ -var getAccessorValue = function (accessor) { - return accessor.type === experimental_utils_1.AST_NODE_TYPES.Identifier - ? accessor.name - : exports.getStringValue(accessor); -}; -exports.getAccessorValue = getAccessorValue; -/** - * Checks if the given `node` is a valid `ExpectCall`. - * - * In order to be an `ExpectCall`, the `node` must: - * * be a `CallExpression`, - * * have an accessor named 'expect', - * * have a `parent`. - * - * @param {Node} node - * - * @return {node is ExpectCall} - */ -var isExpectCall = function (node) { - return node.type === experimental_utils_1.AST_NODE_TYPES.CallExpression && - exports.isSupportedAccessor(node.callee, 'expect') && - node.parent !== undefined; -}; -exports.isExpectCall = isExpectCall; -var isExpectMember = function (node, name) { - return node.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression && - exports.isSupportedAccessor(node.property, name); -}; -exports.isExpectMember = isExpectMember; -var ModifierName; -(function (ModifierName) { - ModifierName["not"] = "not"; - ModifierName["rejects"] = "rejects"; - ModifierName["resolves"] = "resolves"; -})(ModifierName = exports.ModifierName || (exports.ModifierName = {})); -var EqualityMatcher; -(function (EqualityMatcher) { - EqualityMatcher["toBe"] = "toBe"; - EqualityMatcher["toEqual"] = "toEqual"; - EqualityMatcher["toStrictEqual"] = "toStrictEqual"; -})(EqualityMatcher = exports.EqualityMatcher || (exports.EqualityMatcher = {})); -var isParsedEqualityMatcherCall = function (matcher, name) { - return (name - ? matcher.name === name - : EqualityMatcher.hasOwnProperty(matcher.name)) && - matcher.arguments !== null && - matcher.arguments.length === 1; -}; -exports.isParsedEqualityMatcherCall = isParsedEqualityMatcherCall; -var parseExpectMember = function (expectMember) { return ({ - name: exports.getAccessorValue(expectMember.property), - node: expectMember -}); }; -var reparseAsMatcher = function (parsedMember) { return (__assign(__assign({}, parsedMember), { - /** - * The arguments being passed to this `Matcher`, if any. - * - * If this matcher isn't called, this will be `null`. - */ - arguments: parsedMember.node.parent.type === experimental_utils_1.AST_NODE_TYPES.CallExpression - ? parsedMember.node.parent.arguments - : null })); }; -/** - * Re-parses the given `parsedMember` as a `ParsedExpectModifier`. - * - * If the given `parsedMember` does not have a `name` of a valid `Modifier`, - * an exception will be thrown. - * - * @param {ParsedExpectMember} parsedMember - * - * @return {ParsedExpectModifier} - */ -var reparseMemberAsModifier = function (parsedMember) { - if (isSpecificMember(parsedMember, ModifierName.not)) { - return parsedMember; - } - /* istanbul ignore if */ - if (!isSpecificMember(parsedMember, ModifierName.resolves) && - !isSpecificMember(parsedMember, ModifierName.rejects)) { - // ts doesn't think that the ModifierName.not check is the direct inverse as the above two checks - // todo: impossible at runtime, but can't be typed w/o negation support - throw new Error("modifier name must be either \"" + ModifierName.resolves + "\" or \"" + ModifierName.rejects + "\" (got \"" + parsedMember.name + "\")"); - } - var negation = exports.isExpectMember(parsedMember.node.parent, ModifierName.not) - ? parsedMember.node.parent - : undefined; - return __assign(__assign({}, parsedMember), { negation: negation }); -}; -var isSpecificMember = function (member, specific) { return member.name === specific; }; -/** - * Checks if the given `ParsedExpectMember` should be re-parsed as an `ParsedExpectModifier`. - * - * @param {ParsedExpectMember} member - * - * @return {member is ParsedExpectMember} - */ -var shouldBeParsedExpectModifier = function (member) { - return ModifierName.hasOwnProperty(member.name); -}; -var parseExpectCall = function (expect) { - var expectation = { - expect: expect - }; - if (!exports.isExpectMember(expect.parent)) { - return expectation; - } - var parsedMember = parseExpectMember(expect.parent); - if (!shouldBeParsedExpectModifier(parsedMember)) { - expectation.matcher = reparseAsMatcher(parsedMember); - return expectation; - } - var modifier = (expectation.modifier = - reparseMemberAsModifier(parsedMember)); - var memberNode = modifier.negation || modifier.node; - if (!exports.isExpectMember(memberNode.parent)) { - return expectation; - } - expectation.matcher = reparseAsMatcher(parseExpectMember(memberNode.parent)); - return expectation; -}; -exports.parseExpectCall = parseExpectCall; -var DescribeAlias; -(function (DescribeAlias) { - DescribeAlias["describe"] = "describe"; - DescribeAlias["fdescribe"] = "fdescribe"; - DescribeAlias["xdescribe"] = "xdescribe"; -})(DescribeAlias = exports.DescribeAlias || (exports.DescribeAlias = {})); -var TestCaseName; -(function (TestCaseName) { - TestCaseName["fit"] = "fit"; - TestCaseName["it"] = "it"; - TestCaseName["test"] = "test"; - TestCaseName["xit"] = "xit"; - TestCaseName["xtest"] = "xtest"; -})(TestCaseName = exports.TestCaseName || (exports.TestCaseName = {})); -var HookName; -(function (HookName) { - HookName["beforeAll"] = "beforeAll"; - HookName["beforeEach"] = "beforeEach"; - HookName["afterAll"] = "afterAll"; - HookName["afterEach"] = "afterEach"; -})(HookName = exports.HookName || (exports.HookName = {})); -var DescribeProperty; -(function (DescribeProperty) { - DescribeProperty["each"] = "each"; - DescribeProperty["only"] = "only"; - DescribeProperty["skip"] = "skip"; -})(DescribeProperty = exports.DescribeProperty || (exports.DescribeProperty = {})); -var TestCaseProperty; -(function (TestCaseProperty) { - TestCaseProperty["each"] = "each"; - TestCaseProperty["concurrent"] = "concurrent"; - TestCaseProperty["only"] = "only"; - TestCaseProperty["skip"] = "skip"; - TestCaseProperty["todo"] = "todo"; -})(TestCaseProperty = exports.TestCaseProperty || (exports.TestCaseProperty = {})); -var joinNames = function (a, b) { - return a && b ? a + "." + b : null; -}; -function getNodeName(node) { - if (exports.isSupportedAccessor(node)) { - return exports.getAccessorValue(node); - } - switch (node.type) { - case experimental_utils_1.AST_NODE_TYPES.TaggedTemplateExpression: - return getNodeName(node.tag); - case experimental_utils_1.AST_NODE_TYPES.MemberExpression: - return joinNames(getNodeName(node.object), getNodeName(node.property)); - case experimental_utils_1.AST_NODE_TYPES.NewExpression: - case experimental_utils_1.AST_NODE_TYPES.CallExpression: - return getNodeName(node.callee); - } - return null; -} -exports.getNodeName = getNodeName; -var isFunction = function (node) { - return node.type === experimental_utils_1.AST_NODE_TYPES.FunctionExpression || - node.type === experimental_utils_1.AST_NODE_TYPES.ArrowFunctionExpression; -}; -exports.isFunction = isFunction; -var isHook = function (node) { - return node.callee.type === experimental_utils_1.AST_NODE_TYPES.Identifier && - HookName.hasOwnProperty(node.callee.name); -}; -exports.isHook = isHook; -var getTestCallExpressionsFromDeclaredVariables = function (declaredVariables) { - return declaredVariables.reduce(function (acc, _a) { - var references = _a.references; - return acc.concat(references - .map(function (_a) { - var identifier = _a.identifier; - return identifier.parent; - }) - .filter(function (node) { - return !!node && - node.type === experimental_utils_1.AST_NODE_TYPES.CallExpression && - exports.isTestCaseCall(node); - })); - }, []); -}; -exports.getTestCallExpressionsFromDeclaredVariables = getTestCallExpressionsFromDeclaredVariables; -var isTestCaseName = function (node) { - return node.type === experimental_utils_1.AST_NODE_TYPES.Identifier && - TestCaseName.hasOwnProperty(node.name); -}; -var isTestCaseProperty = function (node) { - return exports.isSupportedAccessor(node) && - TestCaseProperty.hasOwnProperty(exports.getAccessorValue(node)); -}; -/** - * Checks if the given `node` is a *call* to a test case function that would - * result in tests being run by `jest`. - * - * Note that `.each()` does not count as a call in this context, as it will not - * result in `jest` running any tests. - * - * @param {TSESTree.CallExpression} node - * - * @return {node is JestFunctionCallExpression} - */ -var isTestCaseCall = function (node) { - if (isTestCaseName(node.callee)) { - return true; - } - var callee = node.callee.type === experimental_utils_1.AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee.type === experimental_utils_1.AST_NODE_TYPES.CallExpression - ? node.callee.callee - : node.callee; - if (callee.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression && - isTestCaseProperty(callee.property)) { - // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) - if (exports.getAccessorValue(callee.property) === 'each' && - node.callee.type !== experimental_utils_1.AST_NODE_TYPES.TaggedTemplateExpression && - node.callee.type !== experimental_utils_1.AST_NODE_TYPES.CallExpression) { - return false; - } - return callee.object.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression - ? isTestCaseName(callee.object.object) - : isTestCaseName(callee.object); - } - return false; -}; -exports.isTestCaseCall = isTestCaseCall; -var isDescribeAlias = function (node) { - return node.type === experimental_utils_1.AST_NODE_TYPES.Identifier && - DescribeAlias.hasOwnProperty(node.name); -}; -var isDescribeProperty = function (node) { - return exports.isSupportedAccessor(node) && - DescribeProperty.hasOwnProperty(exports.getAccessorValue(node)); -}; -/** - * Checks if the given `node` is a *call* to a `describe` function that would - * result in a `describe` block being created by `jest`. - * - * Note that `.each()` does not count as a call in this context, as it will not - * result in `jest` creating any `describe` blocks. - * - * @param {TSESTree.CallExpression} node - * - * @return {node is JestFunctionCallExpression} - */ -var isDescribeCall = function (node) { - if (isDescribeAlias(node.callee)) { - return true; - } - var callee = node.callee.type === experimental_utils_1.AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee.type === experimental_utils_1.AST_NODE_TYPES.CallExpression - ? node.callee.callee - : node.callee; - if (callee.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression && - isDescribeProperty(callee.property)) { - // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) - if (exports.getAccessorValue(callee.property) === 'each' && - node.callee.type !== experimental_utils_1.AST_NODE_TYPES.TaggedTemplateExpression && - node.callee.type !== experimental_utils_1.AST_NODE_TYPES.CallExpression) { - return false; - } - return callee.object.type === experimental_utils_1.AST_NODE_TYPES.MemberExpression - ? isDescribeAlias(callee.object.object) - : isDescribeAlias(callee.object); - } - return false; -}; -exports.isDescribeCall = isDescribeCall; -var collectReferences = function (scope) { - var locals = new Set(); - var unresolved = new Set(); - var currentScope = scope; - while (currentScope !== null) { - for (var _i = 0, _a = currentScope.variables; _i < _a.length; _i++) { - var ref = _a[_i]; - var isReferenceDefined = ref.defs.some(function (def) { - return def.type !== 'ImplicitGlobalVariable'; - }); - if (isReferenceDefined) { - locals.add(ref.name); - } - } - for (var _b = 0, _c = currentScope.through; _b < _c.length; _b++) { - var ref = _c[_b]; - unresolved.add(ref.identifier.name); - } - currentScope = currentScope.upper; - } - return { locals: locals, unresolved: unresolved }; -}; -var scopeHasLocalReference = function (scope, referenceName) { - var references = collectReferences(scope); - return ( - // referenceName was found as a local variable or function declaration. - references.locals.has(referenceName) || - // referenceName was not found as an unresolved reference, - // meaning it is likely not an implicit global reference. - !references.unresolved.has(referenceName)); -}; -exports.scopeHasLocalReference = scopeHasLocalReference; diff --git a/meteor/eslint-rules/utils.ts b/meteor/eslint-rules/utils.ts deleted file mode 100644 index 854447c335..0000000000 --- a/meteor/eslint-rules/utils.ts +++ /dev/null @@ -1,724 +0,0 @@ -/** https://github.com/jest-community/eslint-plugin-jest/blob/7cba106d0ade884a231b61098fa0bf33af2a1ad7/src/rules/utils.ts */ -// import { parse as parsePath } from 'path'; -import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils' -// import { version } from '../../package.json'; - -// const REPO_URL = 'https://github.com/jest-community/eslint-plugin-jest'; - -export const createRule = ESLintUtils.RuleCreator((name) => { - return `local:${name}` - // const ruleName = parsePath(name).name; - - // return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`; -}) - -export type MaybeTypeCast = TSTypeCastExpression | Expression - -type TSTypeCastExpression = - | AsExpressionChain - | TypeAssertionChain - -interface AsExpressionChain - extends TSESTree.TSAsExpression { - expression: AsExpressionChain | Expression -} - -interface TypeAssertionChain - extends TSESTree.TSTypeAssertion { - expression: TypeAssertionChain | Expression -} - -const isTypeCastExpression = ( - node: MaybeTypeCast -): node is TSTypeCastExpression => - node.type === AST_NODE_TYPES.TSAsExpression || node.type === AST_NODE_TYPES.TSTypeAssertion - -export const followTypeAssertionChain = ( - expression: MaybeTypeCast -): Expression => (isTypeCastExpression(expression) ? followTypeAssertionChain(expression.expression) : expression) - -/** - * A `Literal` with a `value` of type `string`. - */ -interface StringLiteral extends TSESTree.StringLiteral { - value: Value -} - -/** - * Checks if the given `node` is a `StringLiteral`. - * - * If a `value` is provided & the `node` is a `StringLiteral`, - * the `value` will be compared to that of the `StringLiteral`. - * - * @param {Node} node - * @param {V} [value] - * - * @return {node is StringLiteral} - * - * @template V - */ -const isStringLiteral = (node: TSESTree.Node, value?: V): node is StringLiteral => - node.type === AST_NODE_TYPES.Literal && - typeof node.value === 'string' && - (value === undefined || node.value === value) - -interface TemplateLiteral extends TSESTree.TemplateLiteral { - quasis: [TSESTree.TemplateElement & { value: { raw: Value; cooked: Value } }] -} - -/** - * Checks if the given `node` is a `TemplateLiteral`. - * - * Complex `TemplateLiteral`s are not considered specific, and so will return `false`. - * - * If a `value` is provided & the `node` is a `TemplateLiteral`, - * the `value` will be compared to that of the `TemplateLiteral`. - * - * @param {Node} node - * @param {V} [value] - * - * @return {node is TemplateLiteral} - * - * @template V - */ -const isTemplateLiteral = (node: TSESTree.Node, value?: V): node is TemplateLiteral => - node.type === AST_NODE_TYPES.TemplateLiteral && - node.quasis.length === 1 && // bail out if not simple - (value === undefined || node.quasis[0].value.raw === value) - -export type StringNode = StringLiteral | TemplateLiteral - -/** - * Checks if the given `node` is a {@link StringNode}. - * - * @param {Node} node - * @param {V} [specifics] - * - * @return {node is StringNode} - * - * @template V - */ -export const isStringNode = (node: TSESTree.Node, specifics?: V): node is StringNode => - isStringLiteral(node, specifics) || isTemplateLiteral(node, specifics) - -/** - * Gets the value of the given `StringNode`. - * - * If the `node` is a `TemplateLiteral`, the `raw` value is used; - * otherwise, `value` is returned instead. - * - * @param {StringNode} node - * - * @return {S} - * - * @template S - */ -export const getStringValue = (node: StringNode): S => - isTemplateLiteral(node) ? node.quasis[0].value.raw : node.value - -/** - * Represents a `MemberExpression` with a "known" `property`. - */ -interface KnownMemberExpression extends TSESTree.MemberExpressionComputedName { - property: AccessorNode -} - -/** - * Represents a `CallExpression` with a "known" `property` accessor. - * - * i.e `KnownCallExpression<'includes'>` represents `.includes()`. - */ -export interface KnownCallExpression extends TSESTree.CallExpression { - callee: CalledKnownMemberExpression -} - -/** - * Represents a `MemberExpression` with a "known" `property`, that is called. - * - * This is `KnownCallExpression` from the perspective of the `MemberExpression` node. - */ -export interface CalledKnownMemberExpression extends KnownMemberExpression { - parent: KnownCallExpression -} - -/** - * Represents a `CallExpression` with a single argument. - */ -export interface CallExpressionWithSingleArgument - extends TSESTree.CallExpression { - arguments: [Argument] -} - -/** - * Guards that the given `call` has only one `argument`. - * - * @param {CallExpression} call - * - * @return {call is CallExpressionWithSingleArgument} - */ -export const hasOnlyOneArgument = (call: TSESTree.CallExpression): call is CallExpressionWithSingleArgument => - call.arguments.length === 1 - -/** - * An `Identifier` with a known `name` value - i.e `expect`. - */ -interface KnownIdentifier extends TSESTree.Identifier { - name: Name -} - -/** - * Checks if the given `node` is an `Identifier`. - * - * If a `name` is provided, & the `node` is an `Identifier`, - * the `name` will be compared to that of the `identifier`. - * - * @param {Node} node - * @param {V} [name] - * - * @return {node is KnownIdentifier} - * - * @template V - */ -export const isIdentifier = (node: TSESTree.Node, name?: V): node is KnownIdentifier => - node.type === AST_NODE_TYPES.Identifier && (name === undefined || node.name === name) - -/** - * Checks if the given `node` is a "supported accessor". - * - * This means that it's a node can be used to access properties, - * and who's "value" can be statically determined. - * - * `MemberExpression` nodes most commonly contain accessors, - * but it's possible for other nodes to contain them. - * - * If a `value` is provided & the `node` is an `AccessorNode`, - * the `value` will be compared to that of the `AccessorNode`. - * - * Note that `value` here refers to the normalised value. - * The property that holds the value is not always called `name`. - * - * @param {Node} node - * @param {V} [value] - * - * @return {node is AccessorNode} - * - * @template V - */ -export const isSupportedAccessor = (node: TSESTree.Node, value?: V): node is AccessorNode => - isIdentifier(node, value) || isStringNode(node, value) - -/** - * Gets the value of the given `AccessorNode`, - * account for the different node types. - * - * @param {AccessorNode} accessor - * - * @return {S} - * - * @template S - */ -export const getAccessorValue = (accessor: AccessorNode): S => - accessor.type === AST_NODE_TYPES.Identifier ? accessor.name : getStringValue(accessor) - -export type AccessorNode = StringNode | KnownIdentifier - -interface ExpectCall extends TSESTree.CallExpression { - callee: AccessorNode<'expect'> - parent: TSESTree.Node -} - -/** - * Checks if the given `node` is a valid `ExpectCall`. - * - * In order to be an `ExpectCall`, the `node` must: - * * be a `CallExpression`, - * * have an accessor named 'expect', - * * have a `parent`. - * - * @param {Node} node - * - * @return {node is ExpectCall} - */ -export const isExpectCall = (node: TSESTree.Node): node is ExpectCall => - node.type === AST_NODE_TYPES.CallExpression && - isSupportedAccessor(node.callee, 'expect') && - node.parent !== undefined - -interface ParsedExpectMember< - Name extends ExpectPropertyName = ExpectPropertyName, - Node extends ExpectMember = ExpectMember -> { - name: Name - node: Node -} - -/** - * Represents a `MemberExpression` that comes after an `ExpectCall`. - */ -interface ExpectMember - extends KnownMemberExpression { - object: ExpectCall | ExpectMember - parent: TSESTree.Node -} - -export const isExpectMember = ( - node: TSESTree.Node, - name?: Name -): node is ExpectMember => - node.type === AST_NODE_TYPES.MemberExpression && isSupportedAccessor(node.property, name) - -/** - * Represents all the jest matchers. - */ -type MatcherName = string /* & not ModifierName */ -type ExpectPropertyName = ModifierName | MatcherName - -export type ParsedEqualityMatcherCall< - Argument extends TSESTree.Expression = TSESTree.Expression, - Matcher extends EqualityMatcher = EqualityMatcher -> = Omit, 'arguments'> & { - parent: TSESTree.CallExpression - arguments: [Argument] -} - -export enum ModifierName { - not = 'not', - rejects = 'rejects', - resolves = 'resolves', -} - -export enum EqualityMatcher { - toBe = 'toBe', - toEqual = 'toEqual', - toStrictEqual = 'toStrictEqual', -} - -export const isParsedEqualityMatcherCall = ( - matcher: ParsedExpectMatcher, - name?: MatcherName -): matcher is ParsedEqualityMatcherCall => - (name ? matcher.name === name : EqualityMatcher.hasOwnProperty(matcher.name)) && - matcher.arguments !== null && - matcher.arguments.length === 1 - -/** - * Represents a parsed expect matcher, such as `toBe`, `toContain`, and so on. - */ -export interface ParsedExpectMatcher< - Matcher extends MatcherName = MatcherName, - Node extends ExpectMember = ExpectMember -> extends ParsedExpectMember { - /** - * The arguments being passed to the matcher. - * A value of `null` means the matcher isn't being called. - */ - arguments: TSESTree.CallExpression['arguments'] | null -} - -type BaseParsedModifier = ParsedExpectMember - -type NegatableModifierName = ModifierName.rejects | ModifierName.resolves -type NotNegatableModifierName = ModifierName.not - -/** - * Represents a parsed modifier that can be followed by a `not` negation modifier. - */ -interface NegatableParsedModifier - extends BaseParsedModifier { - negation?: ExpectMember -} - -/** - * Represents a parsed modifier that cannot be followed by a `not` negation modifier. - */ -export interface NotNegatableParsedModifier - extends BaseParsedModifier { - negation?: never -} - -export type ParsedExpectModifier = NotNegatableParsedModifier | NegatableParsedModifier - -interface Expectation { - expect: ExpectNode - modifier?: ParsedExpectModifier - matcher?: ParsedExpectMatcher -} - -const parseExpectMember = (expectMember: ExpectMember): ParsedExpectMember => ({ - name: getAccessorValue(expectMember.property), - node: expectMember, -}) - -const reparseAsMatcher = (parsedMember: ParsedExpectMember): ParsedExpectMatcher => ({ - ...parsedMember, - /** - * The arguments being passed to this `Matcher`, if any. - * - * If this matcher isn't called, this will be `null`. - */ - arguments: - parsedMember.node.parent.type === AST_NODE_TYPES.CallExpression ? parsedMember.node.parent.arguments : null, -}) - -/** - * Re-parses the given `parsedMember` as a `ParsedExpectModifier`. - * - * If the given `parsedMember` does not have a `name` of a valid `Modifier`, - * an exception will be thrown. - * - * @param {ParsedExpectMember} parsedMember - * - * @return {ParsedExpectModifier} - */ -const reparseMemberAsModifier = (parsedMember: ParsedExpectMember): ParsedExpectModifier => { - if (isSpecificMember(parsedMember, ModifierName.not)) { - return parsedMember - } - - /* istanbul ignore if */ - if ( - !isSpecificMember(parsedMember, ModifierName.resolves) && - !isSpecificMember(parsedMember, ModifierName.rejects) - ) { - // ts doesn't think that the ModifierName.not check is the direct inverse as the above two checks - // todo: impossible at runtime, but can't be typed w/o negation support - throw new Error( - `modifier name must be either "${ModifierName.resolves}" or "${ModifierName.rejects}" (got "${parsedMember.name}")` - ) - } - - const negation = isExpectMember(parsedMember.node.parent, ModifierName.not) ? parsedMember.node.parent : undefined - - return { - ...parsedMember, - negation, - } -} - -const isSpecificMember = ( - member: ParsedExpectMember, - specific: Name -): member is ParsedExpectMember => member.name === specific - -/** - * Checks if the given `ParsedExpectMember` should be re-parsed as an `ParsedExpectModifier`. - * - * @param {ParsedExpectMember} member - * - * @return {member is ParsedExpectMember} - */ -const shouldBeParsedExpectModifier = (member: ParsedExpectMember): member is ParsedExpectMember => - ModifierName.hasOwnProperty(member.name) - -export const parseExpectCall = (expect: ExpectNode): Expectation => { - const expectation: Expectation = { - expect, - } - - if (!isExpectMember(expect.parent)) { - return expectation - } - - const parsedMember = parseExpectMember(expect.parent) - - if (!shouldBeParsedExpectModifier(parsedMember)) { - expectation.matcher = reparseAsMatcher(parsedMember) - - return expectation - } - - const modifier = (expectation.modifier = reparseMemberAsModifier(parsedMember)) - - const memberNode = modifier.negation || modifier.node - - if (!isExpectMember(memberNode.parent)) { - return expectation - } - - expectation.matcher = reparseAsMatcher(parseExpectMember(memberNode.parent)) - - return expectation -} - -export enum DescribeAlias { - 'describe' = 'describe', - 'fdescribe' = 'fdescribe', - 'xdescribe' = 'xdescribe', -} - -export enum TestCaseName { - 'fit' = 'fit', - 'it' = 'it', - 'test' = 'test', - 'xit' = 'xit', - 'xtest' = 'xtest', -} - -export enum HookName { - 'beforeAll' = 'beforeAll', - 'beforeEach' = 'beforeEach', - 'afterAll' = 'afterAll', - 'afterEach' = 'afterEach', -} - -export enum DescribeProperty { - 'each' = 'each', - 'only' = 'only', - 'skip' = 'skip', -} - -export enum TestCaseProperty { - 'each' = 'each', - 'concurrent' = 'concurrent', - 'only' = 'only', - 'skip' = 'skip', - 'todo' = 'todo', -} - -type JestFunctionName = DescribeAlias | TestCaseName | HookName -type JestPropertyName = DescribeProperty | TestCaseProperty - -interface JestFunctionIdentifier extends TSESTree.Identifier { - name: FunctionName -} - -interface JestFunctionMemberExpression< - FunctionName extends JestFunctionName, - PropertyName extends JestPropertyName = JestPropertyName -> extends KnownMemberExpression { - object: JestFunctionIdentifier -} - -interface JestFunctionCallExpressionWithMemberExpressionCallee< - FunctionName extends JestFunctionName, - PropertyName extends JestPropertyName = JestPropertyName -> extends TSESTree.CallExpression { - callee: JestFunctionMemberExpression -} - -export interface JestFunctionCallExpressionWithIdentifierCallee - extends TSESTree.CallExpression { - callee: JestFunctionIdentifier -} - -interface JestEachMemberExpression> - extends KnownMemberExpression<'each'> { - object: KnownIdentifier | (KnownMemberExpression & { object: KnownIdentifier }) -} - -export interface JestCalledEachCallExpression> - extends TSESTree.CallExpression { - callee: TSESTree.CallExpression & { - callee: JestEachMemberExpression - } -} - -export interface JestTaggedEachCallExpression> - extends TSESTree.CallExpression { - callee: TSESTree.TaggedTemplateExpression & { - tag: JestEachMemberExpression - } -} - -type JestEachCallExpression> = - | JestCalledEachCallExpression - | JestTaggedEachCallExpression - -export type JestFunctionCallExpression< - FunctionName extends Exclude = Exclude -> = - | JestEachCallExpression - | JestFunctionCallExpressionWithMemberExpressionCallee - | JestFunctionCallExpressionWithIdentifierCallee - -const joinNames = (a: string | null, b: string | null): string | null => (a && b ? `${a}.${b}` : null) - -export function getNodeName( - node: - | JestFunctionCallExpression - | JestFunctionMemberExpression - | JestFunctionIdentifier - | TSESTree.TaggedTemplateExpression -): string -export function getNodeName(node: TSESTree.Node): string | null -export function getNodeName(node: TSESTree.Node): string | null { - if (isSupportedAccessor(node)) { - return getAccessorValue(node) - } - - switch (node.type) { - case AST_NODE_TYPES.TaggedTemplateExpression: - return getNodeName(node.tag) - case AST_NODE_TYPES.MemberExpression: - return joinNames(getNodeName(node.object), getNodeName(node.property)) - case AST_NODE_TYPES.NewExpression: - case AST_NODE_TYPES.CallExpression: - return getNodeName(node.callee) - } - - return null -} - -export type FunctionExpression = TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression - -export const isFunction = (node: TSESTree.Node): node is FunctionExpression => - node.type === AST_NODE_TYPES.FunctionExpression || node.type === AST_NODE_TYPES.ArrowFunctionExpression - -export const isHook = ( - node: TSESTree.CallExpression -): node is JestFunctionCallExpressionWithIdentifierCallee => - node.callee.type === AST_NODE_TYPES.Identifier && HookName.hasOwnProperty(node.callee.name) - -export const getTestCallExpressionsFromDeclaredVariables = ( - declaredVariables: readonly TSESLint.Scope.Variable[] -): Array> => { - return declaredVariables.reduce>>( - (acc, { references }) => - acc.concat( - references - .map(({ identifier }) => identifier.parent) - .filter( - (node): node is JestFunctionCallExpression => - !!node && node.type === AST_NODE_TYPES.CallExpression && isTestCaseCall(node) - ) - ), - [] - ) -} - -const isTestCaseName = (node: TSESTree.LeftHandSideExpression) => - node.type === AST_NODE_TYPES.Identifier && TestCaseName.hasOwnProperty(node.name) - -const isTestCaseProperty = ( - node: TSESTree.Expression | TSESTree.PrivateIdentifier -): node is AccessorNode => - isSupportedAccessor(node) && TestCaseProperty.hasOwnProperty(getAccessorValue(node)) - -/** - * Checks if the given `node` is a *call* to a test case function that would - * result in tests being run by `jest`. - * - * Note that `.each()` does not count as a call in this context, as it will not - * result in `jest` running any tests. - * - * @param {TSESTree.CallExpression} node - * - * @return {node is JestFunctionCallExpression} - */ -export const isTestCaseCall = (node: TSESTree.CallExpression): node is JestFunctionCallExpression => { - if (isTestCaseName(node.callee)) { - return true - } - - const callee = - node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee.type === AST_NODE_TYPES.CallExpression - ? node.callee.callee - : node.callee - - if (callee.type === AST_NODE_TYPES.MemberExpression && isTestCaseProperty(callee.property)) { - // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) - if ( - getAccessorValue(callee.property) === 'each' && - node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression && - node.callee.type !== AST_NODE_TYPES.CallExpression - ) { - return false - } - - return callee.object.type === AST_NODE_TYPES.MemberExpression - ? isTestCaseName(callee.object.object) - : isTestCaseName(callee.object) - } - - return false -} - -const isDescribeAlias = (node: TSESTree.LeftHandSideExpression) => - node.type === AST_NODE_TYPES.Identifier && DescribeAlias.hasOwnProperty(node.name) - -const isDescribeProperty = ( - node: TSESTree.Expression | TSESTree.PrivateIdentifier -): node is AccessorNode => - isSupportedAccessor(node) && DescribeProperty.hasOwnProperty(getAccessorValue(node)) - -/** - * Checks if the given `node` is a *call* to a `describe` function that would - * result in a `describe` block being created by `jest`. - * - * Note that `.each()` does not count as a call in this context, as it will not - * result in `jest` creating any `describe` blocks. - * - * @param {TSESTree.CallExpression} node - * - * @return {node is JestFunctionCallExpression} - */ -export const isDescribeCall = (node: TSESTree.CallExpression): node is JestFunctionCallExpression => { - if (isDescribeAlias(node.callee)) { - return true - } - - const callee = - node.callee.type === AST_NODE_TYPES.TaggedTemplateExpression - ? node.callee.tag - : node.callee.type === AST_NODE_TYPES.CallExpression - ? node.callee.callee - : node.callee - - if (callee.type === AST_NODE_TYPES.MemberExpression && isDescribeProperty(callee.property)) { - // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) - if ( - getAccessorValue(callee.property) === 'each' && - node.callee.type !== AST_NODE_TYPES.TaggedTemplateExpression && - node.callee.type !== AST_NODE_TYPES.CallExpression - ) { - return false - } - - return callee.object.type === AST_NODE_TYPES.MemberExpression - ? isDescribeAlias(callee.object.object) - : isDescribeAlias(callee.object) - } - - return false -} - -const collectReferences = (scope: TSESLint.Scope.Scope) => { - const locals = new Set() - const unresolved = new Set() - - let currentScope: TSESLint.Scope.Scope | null = scope - - while (currentScope !== null) { - for (const ref of currentScope.variables) { - const isReferenceDefined = ref.defs.some((def) => { - return def.type !== 'ImplicitGlobalVariable' - }) - - if (isReferenceDefined) { - locals.add(ref.name) - } - } - - for (const ref of currentScope.through) { - unresolved.add(ref.identifier.name) - } - - currentScope = currentScope.upper - } - - return { locals, unresolved } -} - -export const scopeHasLocalReference = (scope: TSESLint.Scope.Scope, referenceName: string) => { - const references = collectReferences(scope) - - return ( - // referenceName was found as a local variable or function declaration. - references.locals.has(referenceName) || - // referenceName was not found as an unresolved reference, - // meaning it is likely not an implicit global reference. - !references.unresolved.has(referenceName) - ) -} diff --git a/meteor/jest.config.js b/meteor/jest.config.js index 2c93ee5633..b39804ae23 100644 --- a/meteor/jest.config.js +++ b/meteor/jest.config.js @@ -5,19 +5,13 @@ const commonConfig = { moduleNameMapper: {}, unmockedModulePathPatterns: ['/^imports\\/.*\\.jsx?$/', '/^node_modules/'], globals: {}, - moduleFileExtensions: ['ts', 'js'], + moduleFileExtensions: ['ts', 'js', 'json'], transform: { '^.+\\.(ts|tsx)$': [ 'ts-jest', { isolatedModules: true, // Skip type check to reduce memory impact, as we are already do a yarn check-types tsconfig: 'tsconfig.json', - babelConfig: { - plugins: [ - // Fibers and await do not work well together. This transpiles await calls to something that works - './__mocks__/plugins/meteor-async-await.js', - ], - }, diagnostics: { ignoreCodes: ['TS151001'], }, @@ -34,16 +28,6 @@ const commonConfig = { module.exports = { projects: [ Object.assign({}, commonConfig, { - displayName: 'lib', - testMatch: [ - '/lib/__tests__/**/*.(spec|test).(ts|js)', - '/lib/**/__tests__/**/*.(spec|test).(ts|js)', - '!.meteor/*.*', - ], - testEnvironment: 'node', - }), - Object.assign({}, commonConfig, { - displayName: 'server', testMatch: [ '/server/__tests__/**/*.(spec|test).(ts|js)', '/server/**/__tests__/**/*.(spec|test).(ts|js)', diff --git a/meteor/package.json b/meteor/package.json index f23ba75397..c8ed1b5128 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -3,25 +3,23 @@ "version": "1.52.0-in-development", "private": true, "engines": { - "node": ">=14.19.1" + "node": ">=20.18" }, "scripts": { "preinstall": "node -v", "debug": "meteor run", "libs:syncVersions": "node scripts/libs-sync-version.js", "libs:syncVersionsAndChangelogs": "node scripts/libs-sync-version-and-changelog.js", - "postinstall": "meteor npm run prepareForTest", - "prepareForTest": "node ../scripts/fixTestFibers.js", "inject-git-hash": "node ./scripts/generate-version-file.js", "unit": "jest", "unitci": "jest --maxWorkers 2 --coverage", "unitcov": "jest --coverage", - "test": "meteor npm run check-types && meteor npm run unit", + "test": "yarn check-types && yarn unit", "watch": "jest --watch", "update-snapshots": "jest --updateSnapshot", - "ci:lint": "meteor yarn check-types && meteor yarn lint", + "ci:lint": "yarn check-types && yarn lint", "cov-open": "open-cli coverage/lcov-report/index.html", - "cov": "meteor npm run unitcov && meteor npm run cov-open", + "cov": "yarn unitcov && yarn cov-open", "license-validate": "node ../scripts/checkLicenses.js --allowed=\"MIT,BSD,ISC,Apache,Unlicense,CC0,LGPL,CC BY 3.0,CC BY 4.0,MPL 2.0,Python 2.0\" --excludePackages=timecode,rxjs/ajax,rxjs/fetch,rxjs/internal-compatibility,nw-pre-gyp-module-test,rxjs/operators,rxjs/testing,rxjs/webSocket,undefined,i18next-conv,@fortawesome/fontawesome-common-types,argv,indexof,custom-license,private,public-domain-module,@sofie-automation/corelib,@sofie-automation/shared-lib,@sofie-automation/job-worker", "lint": "run lint:raw .", "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx", @@ -54,6 +52,7 @@ "body-parser": "^1.20.2", "deep-extend": "0.6.0", "deepmerge": "^4.3.1", + "elastic-apm-node": "^4.8.0", "i18next": "^21.10.0", "indexof": "0.0.1", "koa": "^2.15.0", @@ -83,14 +82,13 @@ "@types/app-root-path": "^1.2.8", "@types/body-parser": "^1.19.5", "@types/deep-extend": "^0.6.2", - "@types/fibers": "^3.1.4", "@types/jest": "^29.5.11", "@types/koa": "^2.14.0", "@types/koa-bodyparser": "^4.3.12", "@types/koa-static": "^4.0.4", "@types/koa__cors": "^5.0.0", "@types/koa__router": "^12.0.4", - "@types/node": "^14.18.63", + "@types/node": "^20.17.6", "@types/request": "^2.48.12", "@types/semver": "^7.5.6", "@types/underscore": "^1.11.15", @@ -101,18 +99,15 @@ "ejson": "^2.2.3", "eslint": "^8.56.0", "eslint-config-prettier": "^8.10.0", - "eslint-plugin-custom-rules": "link:eslint-rules", "eslint-plugin-jest": "^27.6.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "fast-clone": "^1.5.13", - "fibers-npm": "npm:fibers@5.0.3", "glob": "^8.1.0", "i18next-conv": "^10.2.0", "i18next-scanner": "^4.4.0", "jest": "^29.7.0", "legally": "^3.5.10", - "meteor-promise": "0.9.0", "open-cli": "^7.2.0", "prettier": "^2.8.8", "standard-version": "^9.5.0", diff --git a/meteor/server/__tests__/_testEnvironment.test.ts b/meteor/server/__tests__/_testEnvironment.test.ts index 549cba2c9b..385c946810 100644 --- a/meteor/server/__tests__/_testEnvironment.test.ts +++ b/meteor/server/__tests__/_testEnvironment.test.ts @@ -1,9 +1,7 @@ -import { Meteor } from 'meteor/meteor' import { RandomMock } from '../../__mocks__/random' import { MongoMock } from '../../__mocks__/mongo' import { protectString, getRandomString } from '../lib/tempLib' -import { waitForPromise, sleep } from '../lib/lib' -import { testInFiber } from '../../__mocks__/helpers/jest' +import { sleep } from '../lib/lib' import { AdLibPieces, Blueprints, @@ -31,31 +29,11 @@ import { UserActionsLog, } from '../collections' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { isInFiber } from '../../__mocks__/Fibers' import { Mongo } from 'meteor/mongo' import { defaultStudio } from '../../__mocks__/defaultCollectionObjects' +import { MinimalMeteorMongoCollection } from '../collections/implementations/asyncCollection' describe('Basic test of test environment', () => { - testInFiber('Check that tests will run in fibers correctly', () => { - // This code runs in a fiber - expect(isInFiber()).toBeTruthy() - - const val = asynchronousFibersFunction(1, 2, 3) - expect(val).toEqual(1 + 2 + 3) - - const p = Promise.resolve() - .then(() => { - expect(isInFiber()).toBeTruthy() - return 'a' - }) - .then(async (innerVal) => { - return new Promise((resolve) => { - expect(isInFiber()).toBeTruthy() - resolve(innerVal) - }) - }) - expect(waitForPromise(p)).toEqual('a') - }) test('Meteor Random mock', () => { RandomMock.mockIds = ['superRandom'] expect(tempTestRandom()).toEqual('superRandom') @@ -168,64 +146,53 @@ describe('Basic test of test environment', () => { MongoMock.mockSetData(Studios, null) expect(await Studios.findFetchAsync({})).toHaveLength(0) }) - testInFiber('Promises in fibers', () => { - const p = new Promise((resolve) => { - setTimeout(() => { - resolve('yup') - }, 10) - }) - - const result = waitForPromise(p) - - expect(result).toEqual('yup') - }) - testInFiber('Mongo mock', async () => { + test('Mongo mock', async () => { const mockAdded = jest.fn() const mockChanged = jest.fn() const mockRemoved = jest.fn() - const collection = new Mongo.Collection('testmock') + const collection = new Mongo.Collection('testmock') as any as MinimalMeteorMongoCollection - collection + await collection .find({ prop: 'b', }) - .observeChanges({ + .observeChangesAsync({ added: mockAdded, changed: mockChanged, removed: mockRemoved, }) - expect(collection.find({}).fetch()).toHaveLength(0) + expect(await collection.find({}).fetchAsync()).toHaveLength(0) - const id = collection.insert({ prop: 'a' }) + const id = await collection.insertAsync({ prop: 'a' }) expect(id).toBeTruthy() - expect(collection.find({}).fetch()).toHaveLength(1) - expect(collection.findOne(id)).toMatchObject({ - prop: 'a', - }) - expect(collection.remove(id)).toEqual(1) - expect(collection.find({}).fetch()).toHaveLength(0) + expect(await collection.find({}).fetchAsync()).toHaveLength(1) + // expect(collection.findOne(id)).toMatchObject({ + // prop: 'a', + // }) + expect(await collection.removeAsync(id)).toEqual(1) + expect(await collection.find({}).fetchAsync()).toHaveLength(0) expect(mockAdded).toHaveBeenCalledTimes(0) expect(mockChanged).toHaveBeenCalledTimes(0) expect(mockRemoved).toHaveBeenCalledTimes(0) - const id2 = collection.insert({ prop: 'b' }) + const id2 = await collection.insertAsync({ prop: 'b' }) await sleep(10) expect(mockAdded).toHaveBeenCalledTimes(1) expect(mockChanged).toHaveBeenCalledTimes(0) expect(mockRemoved).toHaveBeenCalledTimes(0) mockAdded.mockClear() - collection.update(id2, { $set: { name: 'test' } }) + await collection.updateAsync(id2, { $set: { name: 'test' } }) await sleep(10) expect(mockAdded).toHaveBeenCalledTimes(0) expect(mockChanged).toHaveBeenCalledTimes(1) expect(mockRemoved).toHaveBeenCalledTimes(0) mockChanged.mockClear() - collection.remove(id2) + await collection.removeAsync(id2) await sleep(10) expect(mockAdded).toHaveBeenCalledTimes(0) expect(mockChanged).toHaveBeenCalledTimes(0) @@ -233,18 +200,6 @@ describe('Basic test of test environment', () => { }) }) -function asynchronousFibersFunction(a: number, b: number, c: number): number { - return innerAsynchronousFiberFunction(a, b) + c -} - -const innerAsynchronousFiberFunction = Meteor.wrapAsync( - (val0: number, val1: number, cb: (err: any, result: number) => void) => { - setTimeout(() => { - cb(undefined, val0 + val1) - }, 10) - } -) - function tempTestRandom() { return getRandomString() } diff --git a/meteor/server/__tests__/coreSystem.test.ts b/meteor/server/__tests__/coreSystem.test.ts index 3d200e3f8c..147f26199a 100644 --- a/meteor/server/__tests__/coreSystem.test.ts +++ b/meteor/server/__tests__/coreSystem.test.ts @@ -1,8 +1,7 @@ -import { testInFiber } from '../../__mocks__/helpers/jest' import { RelevantSystemVersions } from '../coreSystem' describe('coreSystem', () => { - testInFiber('RelevantSystemVersions', async () => { + test('RelevantSystemVersions', async () => { const versions = await RelevantSystemVersions expect(versions).toEqual({ diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 2c189d38af..65bd80d24c 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -1,5 +1,5 @@ import '../../__mocks__/_extendJest' -import { testInFiber, runAllTimers, beforeAllInFiber, waitUntil } from '../../__mocks__/helpers/jest' +import { runAllTimers, waitUntil } from '../../__mocks__/helpers/jest' import { MeteorMock } from '../../__mocks__/meteor' import { logger } from '../logging' import { getRandomId, getRandomString, protectString } from '../lib/tempLib' @@ -69,7 +69,7 @@ describe('cronjobs', () => { let env: DefaultEnvironment let rundownId: RundownId - beforeAllInFiber(async () => { + beforeAll(async () => { env = await setupDefaultStudioEnvironment() const o = await setupDefaultRundownPlaylist(env) @@ -88,7 +88,7 @@ describe('cronjobs', () => { jest.useFakeTimers() // set time to 2020/07/19 00:00 Local Time mockCurrentTime = new Date(2020, 6, 19, 0, 0, 0).getTime() - MeteorMock.mockRunMeteorStartup() + await MeteorMock.mockRunMeteorStartup() origGetCurrentTime = lib.getCurrentTime //@ts-ignore Mock getCurrentTime for tests lib.getCurrentTime = jest.fn(() => { @@ -101,22 +101,22 @@ describe('cronjobs', () => { await CoreSystem.removeAsync(SYSTEM_ID) }) describe('Runs at the appropriate time', () => { - testInFiber("Doesn't run during the day", async () => { + test("Doesn't run during the day", async () => { // set time to 2020/07/19 12:00 Local Time mockCurrentTime = new Date(2020, 6, 19, 12, 0, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) expect(lib.getCurrentTime).toHaveBeenCalled() await runAllTimers() expect(logger.info).toHaveBeenCalledTimes(0) }) - testInFiber("Runs at 4 o'clock", async () => { + test("Runs at 4 o'clock", async () => { // set time to 2020/07/20 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, 20, 4, 5, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) expect(lib.getCurrentTime).toHaveBeenCalled() expect(logger.info).not.toHaveBeenLastCalledWith('Nightly cronjob: done') @@ -127,11 +127,11 @@ describe('cronjobs', () => { expect(logger.info).toHaveBeenLastCalledWith('Nightly cronjob: done') }, MAX_WAIT_TIME) }) - testInFiber("Doesn't run if less than 20 hours have passed since last run", async () => { + test("Doesn't run if less than 20 hours have passed since last run", async () => { // set time to 2020/07/21 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, 21, 4, 5, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) expect(lib.getCurrentTime).toHaveBeenCalled() expect(logger.info).not.toHaveBeenLastCalledWith('Nightly cronjob: done') @@ -145,7 +145,7 @@ describe('cronjobs', () => { ;(logger.info as jest.Mock).mockClear() mockCurrentTime = new Date(2020, 6, 20, 4, 50, 0).getTime() - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) await runAllTimers() // less than 24 hours have passed so we do not expect the cronjob to run @@ -159,7 +159,7 @@ describe('cronjobs', () => { // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) expect(logger.info).not.toHaveBeenLastCalledWith('Nightly cronjob: done') await waitUntil(async () => { @@ -173,7 +173,7 @@ describe('cronjobs', () => { await PeripheralDevices.removeAsync({}) }) - testInFiber('Remove NrcsIngestDataCache objects that are not connected to any Rundown', async () => { + test('Remove NrcsIngestDataCache objects that are not connected to any Rundown', async () => { // Set up a mock rundown, a detached NrcsIngestDataCache object and an object attached to the mock rundown // Detached NrcsIngestDataCache object 0 const dataCache0Id = protectString(getRandomString()) @@ -217,7 +217,7 @@ describe('cronjobs', () => { }) expect(await NrcsIngestDataCache.findOneAsync(dataCache0Id)).toBeUndefined() }) - testInFiber('Remove SofieIngestDataCache objects that are not connected to any Rundown', async () => { + test('Remove SofieIngestDataCache objects that are not connected to any Rundown', async () => { // Set up a mock rundown, a detached SofieIngestDataCache object and an object attached to the mock rundown // Detached SofieIngestDataCache object 0 const dataCache0Id = protectString(getRandomString()) @@ -263,7 +263,7 @@ describe('cronjobs', () => { }) expect(await SofieIngestDataCache.findOneAsync(dataCache0Id)).toBeUndefined() }) - testInFiber('Removes old PartInstances and PieceInstances', async () => { + test('Removes old PartInstances and PieceInstances', async () => { // nightlyCronjobInner() const segment0: DBSegment = { @@ -396,7 +396,7 @@ describe('cronjobs', () => { expect(await PieceInstances.findOneAsync(pieceInstance0._id)).toBeDefined() expect(await PieceInstances.findOneAsync(pieceInstance1._id)).toBeUndefined() // Removed, since owned by non-existent partInstance2 }) - testInFiber('Removes old entries in UserActionsLog', async () => { + test('Removes old entries in UserActionsLog', async () => { // reasonably fresh entry const userAction0 = protectString(getRandomString()) await UserActionsLog.insertAsync({ @@ -430,7 +430,7 @@ describe('cronjobs', () => { }) expect(await UserActionsLog.findOneAsync(userAction1)).toBeUndefined() }) - testInFiber('Removes old entries in Snapshots', async () => { + test('Removes old entries in Snapshots', async () => { // reasonably fresh entry const snapshot0 = protectString(getRandomString()) await Snapshots.insertAsync({ @@ -524,14 +524,14 @@ describe('cronjobs', () => { } } - testInFiber('Attempts to restart CasparCG when job is enabled', async () => { + test('Attempts to restart CasparCG when job is enabled', async () => { const { mockCasparCg } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold ;(logger.info as jest.Mock).mockClear() // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) await runAllTimers() // check if the correct PeripheralDevice command has been issued, and only for CasparCG devices @@ -543,16 +543,18 @@ describe('cronjobs', () => { }) // Emulate that the restart was successful: - pendingCommands.forEach((cmd) => { - Meteor.call( - 'peripheralDevice.functionReply', - cmd.deviceId, // deviceId - '', // deviceToken - cmd._id, // commandId - null, // err - null // result + await Promise.all( + pendingCommands.map(async (cmd) => + Meteor.callAsync( + 'peripheralDevice.functionReply', + cmd.deviceId, // deviceId + '', // deviceToken + cmd._id, // commandId + null, // err + null // result + ) ) - }) + ) expect(logger.info).not.toHaveBeenLastCalledWith('Nightly cronjob: done') await waitUntil(async () => { @@ -561,7 +563,7 @@ describe('cronjobs', () => { expect(logger.info).toHaveBeenLastCalledWith('Nightly cronjob: done') }, MAX_WAIT_TIME) }) - testInFiber('Skips offline CasparCG when job is enabled', async () => { + test('Skips offline CasparCG when job is enabled', async () => { const { mockCasparCg } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold await PeripheralDevices.updateAsync(mockCasparCg, { $set: { @@ -572,7 +574,7 @@ describe('cronjobs', () => { // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) await waitUntil(async () => { // Run timers, so that all promises in the cronjob has a chance to resolve: @@ -587,7 +589,7 @@ describe('cronjobs', () => { expect(logger.info).toHaveBeenLastCalledWith('Nightly cronjob: done') }, MAX_WAIT_TIME) }) - testInFiber('Does not attempt to restart CasparCG when job is disabled', async () => { + test('Does not attempt to restart CasparCG when job is disabled', async () => { await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold await CoreSystem.updateAsync( {}, @@ -602,7 +604,7 @@ describe('cronjobs', () => { // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime() // cronjob is checked every 5 minutes, so advance 6 minutes - jest.advanceTimersByTime(6 * 60 * 1000) + await jest.advanceTimersByTimeAsync(6 * 60 * 1000) jest.runOnlyPendingTimers() // check if the no PeripheralDevice command have been issued diff --git a/meteor/server/__tests__/logging.test.ts b/meteor/server/__tests__/logging.test.ts index 0485711eff..d1dcc59cd1 100644 --- a/meteor/server/__tests__/logging.test.ts +++ b/meteor/server/__tests__/logging.test.ts @@ -1,10 +1,9 @@ -import { testInFiber } from '../../__mocks__/helpers/jest' import { supressLogging } from '../../__mocks__/helpers/lib' import { SupressLogMessages } from '../../__mocks__/suppressLogging' import { logger } from '../logging' describe('server/logger', () => { - testInFiber('supress errors', async () => { + test('supress errors', async () => { const logMessages = () => { logger.debug('This is a debug message') logger.info('This is an info message') @@ -20,7 +19,7 @@ describe('server/logger', () => { expect(1).toBe(1) }) - testInFiber('logger', () => { + test('logger', () => { expect(typeof logger.error).toEqual('function') expect(typeof logger.warn).toEqual('function') // expect(typeof logger.help).toEqual('function') diff --git a/meteor/server/__tests__/systemTime.test.ts b/meteor/server/__tests__/systemTime.test.ts index 4c9a1a2c1b..5f341fc817 100644 --- a/meteor/server/__tests__/systemTime.test.ts +++ b/meteor/server/__tests__/systemTime.test.ts @@ -1,8 +1,8 @@ -import { runTimersUntilNow, testInFiber } from '../../__mocks__/helpers/jest' +import { runTimersUntilNow } from '../../__mocks__/helpers/jest' import { TimeJumpDetector } from '../systemTime' describe('lib/systemTime', () => { - testInFiber('TimeJumpDetector', async () => { + test('TimeJumpDetector', async () => { jest.useFakeTimers() const mockCallback = jest.fn() let now = Date.now() diff --git a/meteor/server/api/ExternalMessageQueue.ts b/meteor/server/api/ExternalMessageQueue.ts index 51e0800985..5d90abb7e3 100644 --- a/meteor/server/api/ExternalMessageQueue.ts +++ b/meteor/server/api/ExternalMessageQueue.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor' import { check } from '../lib/check' import { StatusCode } from '@sofie-automation/blueprints-integration' -import { deferAsync, getCurrentTime, MeteorStartupAsync } from '../lib/lib' +import { deferAsync, getCurrentTime } from '../lib/lib' import { registerClassToMeteorMethods } from '../methods' import { NewExternalMessageQueueAPI, @@ -50,7 +50,7 @@ function updateExternalMessageQueueStatus(): void { } } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { await ExternalMessageQueue.observeChanges( { sent: { $not: { $gt: 0 } }, diff --git a/meteor/server/api/__tests__/cleanup.test.ts b/meteor/server/api/__tests__/cleanup.test.ts index 6aecf74097..03ffdb054b 100644 --- a/meteor/server/api/__tests__/cleanup.test.ts +++ b/meteor/server/api/__tests__/cleanup.test.ts @@ -1,5 +1,4 @@ import { getRandomId } from '../../lib/tempLib' -import { beforeEachInFiber, testInFiber } from '../../../__mocks__/helpers/jest' import '../../collections' // include this in order to get all of the collection set up import { cleanupOldDataInner } from '../cleanup' @@ -54,12 +53,12 @@ import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/s describe('Cleanup', () => { let env: DefaultEnvironment - beforeEachInFiber(async () => { + beforeEach(async () => { await clearAllDBCollections() env = await setupDefaultStudioEnvironment() }) - testInFiber('Check that all collections are covered', async () => { + test('Check that all collections are covered', async () => { expect(Collections.size).toBeGreaterThan(10) const result = await cleanupOldDataInner(false) @@ -71,7 +70,7 @@ describe('Cleanup', () => { } }) - testInFiber('No bad removals', async () => { + test('No bad removals', async () => { // Check that cleanupOldDataInner() doesn't remove any data when the default data set is in the DB. await setDefaultDatatoDB(env, Date.now()) @@ -92,7 +91,7 @@ describe('Cleanup', () => { expect(await RundownPlaylists.countDocuments()).toBe(1) expect(await Rundowns.countDocuments()).toBe(1) }) - testInFiber('All dependants should be removed', async () => { + test('All dependants should be removed', async () => { // Check that cleanupOldDataInner() cleans up all data from the database. await setDefaultDatatoDB(env, 0) @@ -135,7 +134,7 @@ describe('Cleanup', () => { } } }) - testInFiber('PieceInstance should be removed when PartInstance is removed', async () => { + test('PieceInstance should be removed when PartInstance is removed', async () => { // Check that cleanupOldDataInner() cleans up all data from the database. await setDefaultDatatoDB(env, 0) diff --git a/meteor/server/api/__tests__/client.test.ts b/meteor/server/api/__tests__/client.test.ts index 018b2bc936..78fcfcc4dc 100644 --- a/meteor/server/api/__tests__/client.test.ts +++ b/meteor/server/api/__tests__/client.test.ts @@ -3,10 +3,8 @@ import { MeteorMock } from '../../../__mocks__/meteor' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' import { ClientAPIMethods } from '@sofie-automation/meteor-lib/dist/api/client' import { protectString, LogLevel } from '../../lib/tempLib' -import { makePromise } from '../../lib/lib' import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' import { setLogLevel } from '../../logging' -import { testInFiber, beforeAllInFiber } from '../../../__mocks__/helpers/jest' import { PeripheralDeviceCategory, PeripheralDeviceType, @@ -26,7 +24,7 @@ const orgSetTimeout = setTimeout describe('ClientAPI', () => { let mockDeviceId: PeripheralDeviceId = protectString('not set yet') - beforeAllInFiber(async () => { + beforeAll(async () => { const studio = await setupMockStudio() const mockDevice = await setupMockPeripheralDevice( PeripheralDeviceCategory.PLAYOUT, @@ -37,10 +35,10 @@ describe('ClientAPI', () => { mockDeviceId = mockDevice._id }) describe('clientErrorReport', () => { - testInFiber('Exports a Meteor method to the client', () => { + test('Exports a Meteor method to the client', () => { expect(MeteorMock.mockMethods[ClientAPIMethods.clientErrorReport]).toBeTruthy() }) - testInFiber('Returns a success response to the client', async () => { + test('Returns a success response to the client', async () => { SupressLogMessages.suppressLogMessage(/Uncaught error happened in GUI/i) // should not throw: await MeteorCall.client.clientErrorReport(1000, 'MockString', 'MockLocation') @@ -53,14 +51,14 @@ describe('ClientAPI', () => { const mockContext = 'Context description' const mockArgs = ['mockArg1', 'mockArg2'] - testInFiber('Exports a Meteor method to the client', () => { + test('Exports a Meteor method to the client', () => { expect(MeteorMock.mockMethods[ClientAPIMethods.callPeripheralDeviceFunction]).toBeTruthy() }) describe('Call a method on the peripheralDevice', () => { let logMethodName = `not set yet` let promise: Promise - beforeAllInFiber(async () => { + beforeAll(async () => { logMethodName = `${mockDeviceId}: ${mockFunctionName}` promise = MeteorCall.client.callPeripheralDeviceFunction( mockContext, @@ -72,7 +70,7 @@ describe('ClientAPI', () => { promise.catch(() => null) // Dismiss uncaught promise warning await new Promise((resolve) => orgSetTimeout(resolve, 100)) }) - testInFiber('Logs the call in UserActionsLog', async () => { + test('Logs the call in UserActionsLog', async () => { const log = (await UserActionsLog.findOneAsync({ method: logMethodName, })) as UserActionsLogItem @@ -82,7 +80,7 @@ describe('ClientAPI', () => { expect(log.userId).toBeDefined() }) - testInFiber('Sends a call to the peripheralDevice', async () => { + test('Sends a call to the peripheralDevice', async () => { const pdc = (await PeripheralDeviceCommands.findOneAsync({ deviceId: mockDeviceId, functionName: mockFunctionName, @@ -93,57 +91,52 @@ describe('ClientAPI', () => { expect(pdc.functionName).toBe(mockFunctionName) expect(pdc.args).toMatchObject(mockArgs) }) - testInFiber( - 'Resolves the returned promise once a response from the peripheralDevice is received', - async () => { - await PeripheralDeviceCommands.updateAsync( - { - deviceId: mockDeviceId, - functionName: mockFunctionName, + test('Resolves the returned promise once a response from the peripheralDevice is received', async () => { + await PeripheralDeviceCommands.updateAsync( + { + deviceId: mockDeviceId, + functionName: mockFunctionName, + }, + { + $set: { + hasReply: true, + reply: 'OK', }, - { - $set: { - hasReply: true, - reply: 'OK', - }, - }, - { multi: true } - ) - return promise.then(async (value) => { - const log = (await UserActionsLog.findOneAsync({ - method: logMethodName, - })) as UserActionsLogItem - expect(log).toBeTruthy() - - expect(log.success).toBe(true) - expect(log.doneTime).toBeDefined() - expect(value).toBe('OK') - }) - } - ) + }, + { multi: true } + ) + return promise.then(async (value) => { + const log = (await UserActionsLog.findOneAsync({ + method: logMethodName, + })) as UserActionsLogItem + expect(log).toBeTruthy() + + expect(log.success).toBe(true) + expect(log.doneTime).toBeDefined() + expect(value).toBe('OK') + }) + }) }) describe('Call a failing method on the peripheralDevice', () => { let logMethodName = `not set yet` let promise: Promise - beforeAllInFiber(async () => { + beforeAll(async () => { logMethodName = `${mockDeviceId}: ${mockFailingFunctionName}` - promise = makePromise(() => { - return Meteor.call( - ClientAPIMethods.callPeripheralDeviceFunction, - mockContext, - mockDeviceId, - undefined, - mockFailingFunctionName, - ...mockArgs - ) - }) + promise = Meteor.callAsync( + ClientAPIMethods.callPeripheralDeviceFunction, + mockContext, + mockDeviceId, + undefined, + mockFailingFunctionName, + ...mockArgs + ) promise.catch(() => null) // Dismiss uncaught promise warning await new Promise((resolve) => orgSetTimeout(resolve, 100)) }) - testInFiber('Logs the call in UserActionsLog', async () => { + test('Logs the call in UserActionsLog', async () => { const log = (await UserActionsLog.findOneAsync({ method: logMethodName, })) as UserActionsLogItem @@ -152,7 +145,7 @@ describe('ClientAPI', () => { expect(log.method).toBe(logMethodName) expect(log.userId).toBeDefined() }) - testInFiber('Sends a call to the peripheralDevice', async () => { + test('Sends a call to the peripheralDevice', async () => { const pdc = (await PeripheralDeviceCommands.findOneAsync({ deviceId: mockDeviceId, functionName: mockFailingFunctionName, @@ -163,38 +156,35 @@ describe('ClientAPI', () => { expect(pdc.functionName).toBe(mockFailingFunctionName) expect(pdc.args).toMatchObject(mockArgs) }) - testInFiber( - 'Resolves the returned promise once a response from the peripheralDevice is received', - async () => { - SupressLogMessages.suppressLogMessage(/Failed/i) - SupressLogMessages.suppressLogMessage(/Failed/i) - await PeripheralDeviceCommands.updateAsync( - { - deviceId: mockDeviceId, - functionName: mockFailingFunctionName, + test('Resolves the returned promise once a response from the peripheralDevice is received', async () => { + SupressLogMessages.suppressLogMessage(/Failed/i) + SupressLogMessages.suppressLogMessage(/Failed/i) + await PeripheralDeviceCommands.updateAsync( + { + deviceId: mockDeviceId, + functionName: mockFailingFunctionName, + }, + { + $set: { + hasReply: true, + replyError: 'Failed', }, - { - $set: { - hasReply: true, - replyError: 'Failed', - }, - }, - { multi: true } - ) + }, + { multi: true } + ) - // This will probably resolve after around 3s, since that is the timeout time - // of checkReply and the observeChanges is not implemented in the mock - await expect(promise).rejects.toBe('Failed') + // This will probably resolve after around 3s, since that is the timeout time + // of checkReply and the observeChanges is not implemented in the mock + await expect(promise).rejects.toBe('Failed') - const log = (await UserActionsLog.findOneAsync({ - method: logMethodName, - })) as UserActionsLogItem - expect(log).toBeTruthy() + const log = (await UserActionsLog.findOneAsync({ + method: logMethodName, + })) as UserActionsLogItem + expect(log).toBeTruthy() - expect(log.success).toBe(false) - expect(log.doneTime).toBeDefined() - } - ) + expect(log.success).toBe(false) + expect(log.doneTime).toBeDefined() + }) }) }) }) diff --git a/meteor/server/api/__tests__/externalMessageQueue.test.ts b/meteor/server/api/__tests__/externalMessageQueue.test.ts index 65fca90ee3..402efa0958 100644 --- a/meteor/server/api/__tests__/externalMessageQueue.test.ts +++ b/meteor/server/api/__tests__/externalMessageQueue.test.ts @@ -2,7 +2,6 @@ import '../../../__mocks__/_extendJest' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { ExternalMessageQueue, RundownPlaylists, Rundowns } from '../../collections' import { IBlueprintExternalMessageQueueType, PlaylistTimingType } from '@sofie-automation/blueprints-integration' -import { testInFiber } from '../../../__mocks__/helpers/jest' import { DefaultEnvironment, setupDefaultStudioEnvironment } from '../../../__mocks__/helpers/database' import { getRandomId, protectString } from '../../lib/tempLib' import { getCurrentTime } from '../../lib/lib' @@ -84,7 +83,7 @@ describe('Test external message queue static methods', () => { }) }) - testInFiber('toggleHold', async () => { + test('toggleHold', async () => { let message = (await ExternalMessageQueue.findOneAsync({})) as ExternalMessageQueueObj expect(message).toBeTruthy() expect(message.hold).toBeUndefined() @@ -100,7 +99,7 @@ describe('Test external message queue static methods', () => { expect(message.hold).toBe(false) }) - testInFiber('toggleHold unknown id', async () => { + test('toggleHold unknown id', async () => { SupressLogMessages.suppressLogMessage(/ExternalMessage/i) await expect(MeteorCall.externalMessages.toggleHold(protectString('cake'))).rejects.toThrowMeteor( 404, @@ -108,7 +107,7 @@ describe('Test external message queue static methods', () => { ) }) - testInFiber('retry', async () => { + test('retry', async () => { let message = (await ExternalMessageQueue.findOneAsync({})) as ExternalMessageQueueObj expect(message).toBeTruthy() @@ -123,7 +122,7 @@ describe('Test external message queue static methods', () => { }) }) - testInFiber('retry unknown id', async () => { + test('retry unknown id', async () => { SupressLogMessages.suppressLogMessage(/ExternalMessage/i) await expect(MeteorCall.externalMessages.retry(protectString('is_a_lie'))).rejects.toThrowMeteor( 404, @@ -131,7 +130,7 @@ describe('Test external message queue static methods', () => { ) }) - testInFiber('remove', async () => { + test('remove', async () => { const message = (await ExternalMessageQueue.findOneAsync({})) as ExternalMessageQueueObj expect(message).toBeTruthy() diff --git a/meteor/server/api/__tests__/methods.test.ts b/meteor/server/api/__tests__/methods.test.ts deleted file mode 100644 index 5b0e3be3e6..0000000000 --- a/meteor/server/api/__tests__/methods.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import '../../../__mocks__/_extendJest' -import { MeteorDebugMethods } from '../../methods' -import { Settings } from '../../Settings' -import { MeteorPromiseApply } from '../methods' -import { testInFiber } from '../../../__mocks__/helpers/jest' - -testInFiber('MeteorPromiseApply', async () => { - // set up method: - Settings.enableUserAccounts = false - MeteorDebugMethods({ - myMethod: async (value1: string, value2: string) => { - // Do an async operation, to ensure that asynchronous operations work: - const v = await new Promise((resolve) => { - setTimeout(() => { - resolve(value1 + value2) - }, 10) - }) - return v - }, - }) - const pValue: any = MeteorPromiseApply('myMethod', ['myValue', 'AAA']).catch((e) => { - throw e - }) - expect(pValue).toHaveProperty('then') // be a promise - const value = await pValue - expect(value).toEqual('myValueAAA') -}) diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 7299084e52..6efe7a1596 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -8,7 +8,7 @@ import { import { EmptyPieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { literal, protectString, ProtectedString, getRandomId, LogLevel, getRandomString } from '../../lib/tempLib' import { getCurrentTime } from '../../lib/lib' -import { testInFiber, waitUntil } from '../../../__mocks__/helpers/jest' +import { waitUntil } from '../../../__mocks__/helpers/jest' import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../__mocks__/helpers/database' import { setLogLevel } from '../../logging' import { @@ -177,7 +177,7 @@ describe('test peripheralDevice general API methods', () => { QueueStudioJobSpy.mockClear() }) - testInFiber('initialize', async () => { + test('initialize', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) expect(await PeripheralDevices.findOneAsync(device._id)).toBeTruthy() @@ -202,7 +202,7 @@ describe('test peripheralDevice general API methods', () => { expect(initDevice.subType).toBe(options.subType) }) - testInFiber('setStatus', async () => { + test('setStatus', async () => { expect(await PeripheralDevices.findOneAsync(device._id)).toBeTruthy() expect(((await PeripheralDevices.findOneAsync(device._id)) as PeripheralDevice).status).toMatchObject({ statusCode: StatusCode.GOOD, @@ -217,7 +217,7 @@ describe('test peripheralDevice general API methods', () => { }) }) - testInFiber('getPeripheralDevice', async () => { + test('getPeripheralDevice', async () => { const gotDevice: PeripheralDeviceForDevice = await MeteorCall.peripheralDevice.getPeripheralDevice( device._id, device.token @@ -226,7 +226,7 @@ describe('test peripheralDevice general API methods', () => { expect(gotDevice._id).toBe(device._id) }) - testInFiber('ping', async () => { + test('ping', async () => { jest.useFakeTimers() const EPOCH = 10000 jest.setSystemTime(EPOCH) @@ -253,7 +253,7 @@ describe('test peripheralDevice general API methods', () => { jest.useRealTimers() }) - testInFiber('determineDiffTime', async () => { + test('determineDiffTime', async () => { const response = await MeteorCall.peripheralDevice.determineDiffTime() expect(response).toBeTruthy() expect(Math.abs(response.mean - 400)).toBeLessThan(10) // be about 400 @@ -261,7 +261,7 @@ describe('test peripheralDevice general API methods', () => { expect(response.stdDev).toBeGreaterThan(0.1) }) - testInFiber('getTimeDiff', async () => { + test('getTimeDiff', async () => { const response = await MeteorCall.peripheralDevice.getTimeDiff() const now = getCurrentTime() expect(response).toBeTruthy() @@ -273,14 +273,14 @@ describe('test peripheralDevice general API methods', () => { expect(response.good).toBeDefined() }) - testInFiber('getTime', async () => { + test('getTime', async () => { const response = await MeteorCall.peripheralDevice.getTime() const now = getCurrentTime() expect(response).toBeGreaterThan(now - 30) expect(response).toBeLessThan(now + 30) }) - testInFiber('pingWithCommand and functionReply', async () => { + test('pingWithCommand and functionReply', async () => { jest.useFakeTimers() const EPOCH = 10000 jest.setSystemTime(EPOCH) @@ -329,7 +329,7 @@ describe('test peripheralDevice general API methods', () => { expect(resultMessage).toBeUndefined() const replyMessage = 'Waving back!' - Meteor.call( + await Meteor.callAsync( PeripheralDeviceAPIMethods.functionReply, device._id, device.token, @@ -357,7 +357,7 @@ describe('test peripheralDevice general API methods', () => { jest.useRealTimers() }) - testInFiber('playoutPlaybackChanged', async () => { + test('playoutPlaybackChanged', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) QueueStudioJobSpy.mockImplementation(async () => CreateFakeResult(Promise.resolve(null))) @@ -455,7 +455,7 @@ describe('test peripheralDevice general API methods', () => { ) }) - testInFiber('timelineTriggerTime', async () => { + test('timelineTriggerTime', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) QueueStudioJobSpy.mockImplementation(async () => CreateFakeResult(Promise.resolve(null))) @@ -481,7 +481,7 @@ describe('test peripheralDevice general API methods', () => { ) }) - testInFiber('killProcess with a rundown present', async () => { + test('killProcess with a rundown present', async () => { // test this does not shutdown because Rundown stored if (DEBUG) setLogLevel(LogLevel.DEBUG) SupressLogMessages.suppressLogMessage(/Unable to run killProcess/i) @@ -491,7 +491,7 @@ describe('test peripheralDevice general API methods', () => { ) }) - testInFiber('testMethod', async () => { + test('testMethod', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) const result = await MeteorCall.peripheralDevice.testMethod(device._id, device.token, 'european') expect(result).toBe('european') @@ -502,7 +502,7 @@ describe('test peripheralDevice general API methods', () => { }) /* - testInFiber('timelineTriggerTime', () => { + test('timelineTriggerTime', () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) let timelineTriggerTimeResult: PeripheralDeviceAPI.TimelineTriggerTimeResult = [ { id: 'wibble', time: getCurrentTime() }, { id: 'wobble', time: getCurrentTime() - 100 }] @@ -510,7 +510,7 @@ describe('test peripheralDevice general API methods', () => { }) */ - testInFiber('requestUserAuthToken', async () => { + test('requestUserAuthToken', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) SupressLogMessages.suppressLogMessage(/can only request user auth token/i) @@ -536,7 +536,7 @@ describe('test peripheralDevice general API methods', () => { }) // Should only really work for SpreadsheetDevice - testInFiber('storeAccessToken', async () => { + test('storeAccessToken', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) SupressLogMessages.suppressLogMessage(/can only store access token/i) await expect( @@ -559,7 +559,7 @@ describe('test peripheralDevice general API methods', () => { expect((deviceWithSecretToken.settings as IngestDeviceSettings).secretAccessToken).toBe(true) }) - testInFiber('uninitialize', async () => { + test('uninitialize', async () => { if (DEBUG) setLogLevel(LogLevel.DEBUG) await MeteorCall.peripheralDevice.unInitialize(device._id, device.token) expect(await PeripheralDevices.findOneAsync({})).toBeFalsy() @@ -569,7 +569,7 @@ describe('test peripheralDevice general API methods', () => { }) // Note: this test fails, due to a backwards-compatibility hack in #c579c8f0 - // testInFiber('initialize with bad arguments', () => { + // test('initialize with bad arguments', () => { // let options: PeripheralDeviceInitOptions = { // category: PeripheralDeviceCategory.INGEST, // type: PeripheralDeviceType.MOS, @@ -590,7 +590,7 @@ describe('test peripheralDevice general API methods', () => { // } // }) - // testInFiber('setStatus with bad arguments', () => { + // test('setStatus with bad arguments', () => { // try { // Meteor.call(PeripheralDeviceAPIMethods.setStatus, 'wibbly', device.token, { statusCode: 0 }) // fail('expected to throw') @@ -613,7 +613,7 @@ describe('test peripheralDevice general API methods', () => { // } // }) - testInFiber('removePeripheralDevice', async () => { + test('removePeripheralDevice', async () => { { const deviceObj = await PeripheralDevices.findOneAsync(device?._id) expect(deviceObj).toBeDefined() @@ -697,7 +697,7 @@ describe('test peripheralDevice general API methods', () => { workFlowId: workFlowId, }) }) - testInFiber('getMediaWorkFlowRevisions', async () => { + test('getMediaWorkFlowRevisions', async () => { const workFlows = ( await MediaWorkFlows.findFetchAsync({ studioId: device.studioId, @@ -711,7 +711,7 @@ describe('test peripheralDevice general API methods', () => { expect(res).toHaveLength(workFlows.length) expect(res).toMatchObject(workFlows) }) - testInFiber('getMediaWorkFlowStepRevisions', async () => { + test('getMediaWorkFlowStepRevisions', async () => { const workFlowSteps = ( await MediaWorkFlowSteps.findFetchAsync({ studioId: device.studioId, @@ -726,7 +726,7 @@ describe('test peripheralDevice general API methods', () => { expect(res).toMatchObject(workFlowSteps) }) describe('updateMediaWorkFlow', () => { - testInFiber('update', async () => { + test('update', async () => { const workFlow = await MediaWorkFlows.findOneAsync(workFlowId) expect(workFlow).toBeTruthy() @@ -744,7 +744,7 @@ describe('test peripheralDevice general API methods', () => { const updatedWorkFlow = await MediaWorkFlows.findOneAsync(workFlowId) expect(updatedWorkFlow).toMatchObject(newWorkFlow) }) - testInFiber('remove', async () => { + test('remove', async () => { const workFlow = (await MediaWorkFlows.findOneAsync(workFlowId)) as MediaWorkFlow expect(workFlow).toBeTruthy() @@ -755,7 +755,7 @@ describe('test peripheralDevice general API methods', () => { }) }) describe('updateMediaWorkFlowStep', () => { - testInFiber('update', async () => { + test('update', async () => { const workStep = await MediaWorkFlowSteps.findOneAsync(workStepIds[0]) expect(workStep).toBeTruthy() @@ -773,7 +773,7 @@ describe('test peripheralDevice general API methods', () => { const updatedWorkFlow = await MediaWorkFlowSteps.findOneAsync(workStepIds[0]) expect(updatedWorkFlow).toMatchObject(newWorkStep) }) - testInFiber('remove', async () => { + test('remove', async () => { const workStep = (await MediaWorkFlowSteps.findOneAsync(workStepIds[0])) as MediaWorkFlowStep expect(workStep).toBeTruthy() @@ -840,7 +840,7 @@ describe('test peripheralDevice general API methods', () => { tinf: '', }) }) - testInFiber('getMediaObjectRevisions', async () => { + test('getMediaObjectRevisions', async () => { const mobjects = ( await MediaObjects.findFetchAsync({ studioId: device.studioId, @@ -861,7 +861,7 @@ describe('test peripheralDevice general API methods', () => { expect(mobjects).toMatchObject(mobjects) }) describe('updateMediaObject', () => { - testInFiber('update', async () => { + test('update', async () => { const mo = (await MediaObjects.findOneAsync({ collectionId: MOCK_COLLECTION, studioId: device.studioId!, @@ -886,7 +886,7 @@ describe('test peripheralDevice general API methods', () => { }) expect(updateMo).toMatchObject(newMo) }) - testInFiber('remove', async () => { + test('remove', async () => { const mo = (await MediaObjects.findOneAsync({ collectionId: MOCK_COLLECTION, studioId: device.studioId!, diff --git a/meteor/server/api/__tests__/rundownLayouts.test.ts b/meteor/server/api/__tests__/rundownLayouts.test.ts index 7661c10bcd..fd1e2c982a 100644 --- a/meteor/server/api/__tests__/rundownLayouts.test.ts +++ b/meteor/server/api/__tests__/rundownLayouts.test.ts @@ -1,5 +1,4 @@ import '../../../__mocks__/_extendJest' -import { testInFiber } from '../../../__mocks__/helpers/jest' import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../__mocks__/helpers/database' import { protectString, literal, getRandomString } from '../../lib/tempLib' import { @@ -20,7 +19,7 @@ describe('Rundown Layouts', () => { env = await setupDefaultStudioEnvironment() }) let rundownLayoutId: RundownLayoutId - testInFiber('Create rundown layout', async () => { + test('Create rundown layout', async () => { const res = await MeteorCall.rundownLayout.createRundownLayout( 'Test', RundownLayoutType.RUNDOWN_LAYOUT, @@ -35,7 +34,7 @@ describe('Rundown Layouts', () => { _id: rundownLayoutId, }) }) - testInFiber('Remove rundown layout', async () => { + test('Remove rundown layout', async () => { const item0 = await RundownLayouts.findOneAsync(rundownLayoutId) expect(item0).toMatchObject({ _id: rundownLayoutId, diff --git a/meteor/server/api/__tests__/userActions/buckets.test.ts2 b/meteor/server/api/__tests__/userActions/buckets.test.ts2 index fd4beed3df..cc01b68930 100644 --- a/meteor/server/api/__tests__/userActions/buckets.test.ts2 +++ b/meteor/server/api/__tests__/userActions/buckets.test.ts2 @@ -1,5 +1,4 @@ import '../../../../__mocks__/_extendJest' -import { testInFiber } from '../../../../__mocks__/helpers/jest' import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../../__mocks__/helpers/database' import { getRandomId, protectString } from '../../../../lib/lib' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' @@ -69,7 +68,7 @@ describe('User Actions - Buckets', () => { throw new Error('Not implemented') }) }) - testInFiber('createBucket', async () => { + test('createBucket', async () => { const NAME = 'Test bucket' // should fail if the studio doesn't exist @@ -96,7 +95,7 @@ describe('User Actions - Buckets', () => { }) } }) - testInFiber('removeBucket', async () => { + test('removeBucket', async () => { const { bucketId } = setUpMockBucket() expect( @@ -123,7 +122,7 @@ describe('User Actions - Buckets', () => { ).toHaveLength(0) } }) - testInFiber('modifyBucket', async () => { + test('modifyBucket', async () => { const { bucketId } = setUpMockBucket() // should throw if the bucket doesn't exist @@ -149,7 +148,7 @@ describe('User Actions - Buckets', () => { }) } }) - testInFiber('emptyBucket', async () => { + test('emptyBucket', async () => { const { bucketId } = setUpMockBucket() // should throw if the bucket doesn't exist @@ -170,7 +169,7 @@ describe('User Actions - Buckets', () => { ).toHaveLength(0) } }) - testInFiber('removeBucketAdLib', async () => { + test('removeBucketAdLib', async () => { const { bucketAdlibs } = setUpMockBucket() // should throw if the adlib doesn't exits @@ -187,7 +186,7 @@ describe('User Actions - Buckets', () => { expect(BucketAdLibs.findOne(bucketAdlibs[0]._id)).toBeUndefined() } }) - testInFiber('modifyBucketAdLib', async () => { + test('modifyBucketAdLib', async () => { const { bucketAdlibs } = setUpMockBucket() // check that the adlib exists diff --git a/meteor/server/api/__tests__/userActions/general.test.ts b/meteor/server/api/__tests__/userActions/general.test.ts index 823cd6c80c..22eaadac68 100644 --- a/meteor/server/api/__tests__/userActions/general.test.ts +++ b/meteor/server/api/__tests__/userActions/general.test.ts @@ -1,8 +1,7 @@ import '../../../../__mocks__/_extendJest' -import { testInFiber } from '../../../../__mocks__/helpers/jest' import { setupDefaultStudioEnvironment } from '../../../../__mocks__/helpers/database' import { hashSingleUseToken } from '../../deviceTriggers/triggersContext' -import { getCurrentTime } from '../../../lib/lib' +import { getCurrentTime, sleep } from '../../../lib/lib' import { MeteorCall } from '../../methods' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { UserActionsLog } from '../../../collections' @@ -18,7 +17,7 @@ describe('User Actions - General', () => { await setupDefaultStudioEnvironment() }) - testInFiber('Restart Core', async () => { + test('Restart Core', async () => { jest.useFakeTimers() // Generate restart token @@ -47,10 +46,11 @@ describe('User Actions - General', () => { jest.useRealTimers() }) - testInFiber('GUI Status', async () => { + test('GUI Status', async () => { await expect(MeteorCall.userAction.guiFocused('click', getCurrentTime())).resolves.toMatchObject({ success: 200, }) + await sleep(0) const logs0 = await UserActionsLog.findFetchAsync({ method: 'guiFocused', }) @@ -62,6 +62,7 @@ describe('User Actions - General', () => { await expect(MeteorCall.userAction.guiBlurred('click', getCurrentTime())).resolves.toMatchObject({ success: 200, }) + await sleep(0) const logs1 = await UserActionsLog.findFetchAsync({ method: 'guiBlurred', }) diff --git a/meteor/server/api/__tests__/userActions/mediaManager.test.ts b/meteor/server/api/__tests__/userActions/mediaManager.test.ts index b4806df71f..3680cffde2 100644 --- a/meteor/server/api/__tests__/userActions/mediaManager.test.ts +++ b/meteor/server/api/__tests__/userActions/mediaManager.test.ts @@ -1,5 +1,5 @@ import '../../../../__mocks__/_extendJest' -import { testInFiber, waitUntil } from '../../../../__mocks__/helpers/jest' +import { waitUntil } from '../../../../__mocks__/helpers/jest' import { getRandomId, protectString } from '../../../lib/tempLib' import { getCurrentTime } from '../../../lib/lib' import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../../__mocks__/helpers/database' @@ -46,7 +46,7 @@ describe('User Actions - Media Manager', () => { env = await setupDefaultStudioEnvironment() jest.resetAllMocks() }) - testInFiber('Restart workflow', async () => { + test('Restart workflow', async () => { const { workFlowId } = await setupMockWorkFlow() // should fail if the workflow doesn't exist @@ -76,7 +76,7 @@ describe('User Actions - Media Manager', () => { await p } }) - testInFiber('Abort worfklow', async () => { + test('Abort worfklow', async () => { const { workFlowId } = await setupMockWorkFlow() // should fail if the workflow doesn't exist @@ -107,7 +107,7 @@ describe('User Actions - Media Manager', () => { await p } }) - testInFiber('Prioritize workflow', async () => { + test('Prioritize workflow', async () => { const { workFlowId } = await setupMockWorkFlow() // should fail if the workflow doesn't exist @@ -138,7 +138,7 @@ describe('User Actions - Media Manager', () => { await p } }) - testInFiber('Restart all workflows', async () => { + test('Restart all workflows', async () => { await setupMockWorkFlow() { @@ -160,7 +160,7 @@ describe('User Actions - Media Manager', () => { await p } }) - testInFiber('Abort all workflows', async () => { + test('Abort all workflows', async () => { await setupMockWorkFlow() { diff --git a/meteor/server/api/__tests__/userActions/system.test.ts b/meteor/server/api/__tests__/userActions/system.test.ts index a011ef0a4c..29bf0161c9 100644 --- a/meteor/server/api/__tests__/userActions/system.test.ts +++ b/meteor/server/api/__tests__/userActions/system.test.ts @@ -13,7 +13,6 @@ import { setupMockPeripheralDevice, } from '../../../../__mocks__/helpers/database' import '../../../../__mocks__/_extendJest' -import { testInFiber } from '../../../../__mocks__/helpers/jest' import { Studios } from '../../../collections' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { @@ -85,7 +84,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { jest.resetAllMocks() }) - testInFiber('disable existing subDevice', async () => { + test('disable existing subDevice', async () => { await expect( MeteorCall.userAction.disablePeripheralSubDevice('e', getCurrentTime(), pDevice._id, mockSubDeviceId, true) ).resolves.toMatchObject({ @@ -97,7 +96,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { const playoutDevices = applyAndValidateOverrides(studio.peripheralDeviceSettings.playoutDevices).obj expect(playoutDevices[mockSubDeviceId].options.disable).toBe(true) }) - testInFiber('enable existing subDevice', async () => { + test('enable existing subDevice', async () => { { await expect( MeteorCall.userAction.disablePeripheralSubDevice( @@ -136,7 +135,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { expect(playoutDevices[mockSubDeviceId].options.disable).toBe(false) } }) - testInFiber('edit missing subDevice throws an error', async () => { + test('edit missing subDevice throws an error', async () => { await expect( MeteorCall.userAction.disablePeripheralSubDevice( 'e', @@ -147,7 +146,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { ) ).resolves.toMatchUserRawError(/is not configured/) }) - testInFiber('edit missing device throws an error', async () => { + test('edit missing device throws an error', async () => { await expect( MeteorCall.userAction.disablePeripheralSubDevice( 'e', @@ -158,7 +157,7 @@ describe('User Actions - Disable Peripheral SubDevice', () => { ) ).resolves.toMatchUserRawError(/not found/) }) - testInFiber("edit device that doesn't support the disable property throws an error", async () => { + test("edit device that doesn't support the disable property throws an error", async () => { const pDeviceUnsupported = await setupMockPeripheralDevice( PeripheralDeviceCategory.PLAYOUT, PeripheralDeviceType.PLAYOUT, diff --git a/meteor/server/api/blueprintConfigPresets.ts b/meteor/server/api/blueprintConfigPresets.ts index c3fe8e70ef..d705c63702 100644 --- a/meteor/server/api/blueprintConfigPresets.ts +++ b/meteor/server/api/blueprintConfigPresets.ts @@ -5,7 +5,7 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ObserveChangesHelper } from '../collections/lib' -import { MeteorStartupAsync } from '../lib/lib' +import { Meteor } from 'meteor/meteor' const ObserveChangeBufferTimeout = 100 @@ -18,7 +18,7 @@ const ObserveChangeBufferTimeout = 100 * Whenever the Studio changes the blueprint or config preset, ensure the config is synced across * We want it synced across, so that if the config-preset is removed, then there is some config that can be used */ -MeteorStartupAsync(async () => { +Meteor.startup(async () => { const doUpdate = async (doc: DBStudio): Promise => { const markUnlinked = async () => { await Studios.updateAsync(doc._id, { @@ -69,7 +69,7 @@ MeteorStartupAsync(async () => { * Whenever the ShowStyleBase changes the blueprint or config preset, ensure the config is synced across * We want it synced across, so that if the config-preset is removed, then there is some config that can be used */ -MeteorStartupAsync(async () => { +Meteor.startup(async () => { const doUpdate = async (doc: DBShowStyleBase): Promise => { const markUnlinked = async () => { await Promise.all([ @@ -168,7 +168,7 @@ MeteorStartupAsync(async () => { * Whenever the ShowStyleVariant changes the config preset, ensure the config is synced across * We want it synced across, so that if the config-preset is removed, then there is some config that can be used */ -MeteorStartupAsync(async () => { +Meteor.startup(async () => { const doUpdate = async (doc: DBShowStyleVariant): Promise => { const markUnlinked = async () => { await ShowStyleVariants.updateAsync(doc._id, { diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index 9d22018e26..b92bf0a0ac 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -1,6 +1,5 @@ import * as _ from 'underscore' import { setupDefaultStudioEnvironment, packageBlueprint } from '../../../../__mocks__/helpers/database' -import { testInFiber } from '../../../../__mocks__/helpers/jest' import { literal, getRandomId, protectString } from '../../../lib/tempLib' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' @@ -84,7 +83,7 @@ describe('Test blueprint management api', () => { return core.blueprintId } - testInFiber('empty id', async () => { + test('empty id', async () => { const initialBlueprintId = await getActiveSystemBlueprintId() SupressLogMessages.suppressLogMessage(/Blueprint not found/i) @@ -95,7 +94,7 @@ describe('Test blueprint management api', () => { expect(await getActiveSystemBlueprintId()).toEqual(initialBlueprintId) }) - testInFiber('unknown id', async () => { + test('unknown id', async () => { const blueprint = await ensureSystemBlueprint() const initialBlueprintId = await getActiveSystemBlueprintId() @@ -106,7 +105,7 @@ describe('Test blueprint management api', () => { expect(await getActiveSystemBlueprintId()).toEqual(initialBlueprintId) }) - testInFiber('good', async () => { + test('good', async () => { const blueprint = await ensureSystemBlueprint() // Ensure starts off 'wrong' @@ -117,7 +116,7 @@ describe('Test blueprint management api', () => { // Ensure ends up good expect(await getActiveSystemBlueprintId()).toEqual(blueprint._id) }) - testInFiber('unassign', async () => { + test('unassign', async () => { // Ensure starts off 'wrong' expect(await getActiveSystemBlueprintId()).toBeTruthy() @@ -126,7 +125,7 @@ describe('Test blueprint management api', () => { // Ensure ends up good expect(await getActiveSystemBlueprintId()).toBeFalsy() }) - testInFiber('wrong type', async () => { + test('wrong type', async () => { const blueprint = (await Blueprints.findOneAsync({ blueprintType: BlueprintManifestType.SHOWSTYLE, })) as Blueprint @@ -148,25 +147,25 @@ describe('Test blueprint management api', () => { }) describe('removeBlueprint', () => { - testInFiber('undefined id', async () => { + test('undefined id', async () => { SupressLogMessages.suppressLogMessage(/Match error/i) await expect(MeteorCall.blueprint.removeBlueprint(undefined as any)).rejects.toThrow( 'Match error: Expected string, got undefined' ) }) - testInFiber('empty id', async () => { + test('empty id', async () => { SupressLogMessages.suppressLogMessage(/Blueprint id/i) await expect(MeteorCall.blueprint.removeBlueprint(protectString(''))).rejects.toThrowMeteor( 404, 'Blueprint id "" was not found' ) }) - testInFiber('missing id', async () => { + test('missing id', async () => { // Should not error await MeteorCall.blueprint.removeBlueprint(protectString('not_a_real_blueprint')) }) - testInFiber('good', async () => { + test('good', async () => { const blueprint = await ensureSystemBlueprint() expect(await Blueprints.findOneAsync(blueprint._id)).toBeTruthy() @@ -177,7 +176,7 @@ describe('Test blueprint management api', () => { }) describe('insertBlueprint', () => { - testInFiber('no params', async () => { + test('no params', async () => { const initialBlueprints = await getCurrentBlueprintIds() const newId = await MeteorCall.blueprint.insertBlueprint() @@ -194,7 +193,7 @@ describe('Test blueprint management api', () => { expect(blueprint.name).toBeTruthy() expect(blueprint.blueprintType).toBeFalsy() }) - testInFiber('with name', async () => { + test('with name', async () => { const rawName = 'some_fake_name' const newId = await insertBlueprint(DEFAULT_CONTEXT, undefined, rawName) expect(newId).toBeTruthy() @@ -205,7 +204,7 @@ describe('Test blueprint management api', () => { expect(blueprint.name).toEqual(rawName) expect(blueprint.blueprintType).toBeFalsy() }) - testInFiber('with type', async () => { + test('with type', async () => { const type = BlueprintManifestType.STUDIO const newId = await insertBlueprint(DEFAULT_CONTEXT, type) expect(newId).toBeTruthy() @@ -219,24 +218,24 @@ describe('Test blueprint management api', () => { }) describe('uploadBlueprint', () => { - testInFiber('empty id', async () => { + test('empty id', async () => { await expect(uploadBlueprint(DEFAULT_CONTEXT, protectString(''), '0')).rejects.toThrowMeteor( 400, 'Blueprint id "" is not valid' ) }) - testInFiber('empty body', async () => { + test('empty body', async () => { await expect(uploadBlueprint(DEFAULT_CONTEXT, protectString('blueprint99'), '')).rejects.toThrowMeteor( 400, 'Blueprint blueprint99 failed to parse' ) }) - testInFiber('body not a manifest', async () => { + test('body not a manifest', async () => { await expect( uploadBlueprint(DEFAULT_CONTEXT, protectString('blueprint99'), `({default: (() => 5)()})`) ).rejects.toThrowMeteor(400, 'Blueprint blueprint99 returned a manifest of type number') }) - testInFiber('manifest missing blueprintType', async () => { + test('manifest missing blueprintType', async () => { const blueprintStr = packageBlueprint({}, () => { return { blueprintType: undefined as any, @@ -261,7 +260,7 @@ describe('Test blueprint management api', () => { `Blueprint blueprint99 returned a manifest of unknown blueprintType "undefined"` ) }) - testInFiber('replace existing with different type', async () => { + test('replace existing with different type', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.STUDIO const blueprintStr = packageBlueprint( { @@ -287,7 +286,7 @@ describe('Test blueprint management api', () => { `Cannot replace old blueprint (of type "showstyle") with new blueprint of type "studio"` ) }) - testInFiber('success - showstyle', async () => { + test('success - showstyle', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.SHOWSTYLE const blueprintStr = packageBlueprint( { @@ -326,7 +325,7 @@ describe('Test blueprint management api', () => { ) expect(blueprint.studioConfigSchema).toBeUndefined() }) - testInFiber('success - studio', async () => { + test('success - studio', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.STUDIO const blueprintStr = packageBlueprint( { @@ -369,7 +368,7 @@ describe('Test blueprint management api', () => { ) expect(blueprint.showStyleConfigSchema).toBeUndefined() }) - testInFiber('success - system', async () => { + test('success - system', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.SYSTEM const blueprintStr = packageBlueprint( { @@ -413,7 +412,7 @@ describe('Test blueprint management api', () => { expect(blueprint.showStyleConfigSchema).toBeUndefined() expect(blueprint.studioConfigSchema).toBeUndefined() }) - testInFiber('update - studio', async () => { + test('update - studio', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.STUDIO const blueprintStr = packageBlueprint( { @@ -457,7 +456,7 @@ describe('Test blueprint management api', () => { ) expect(blueprint.showStyleConfigSchema).toBeUndefined() }) - testInFiber('update - matching blueprintId', async () => { + test('update - matching blueprintId', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.SHOWSTYLE const blueprintStr = packageBlueprint( { @@ -503,7 +502,7 @@ describe('Test blueprint management api', () => { ) expect(blueprint.studioConfigSchema).toBeUndefined() }) - testInFiber('update - change blueprintId', async () => { + test('update - change blueprintId', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.SHOWSTYLE const blueprintStr = packageBlueprint( { @@ -534,7 +533,7 @@ describe('Test blueprint management api', () => { `Cannot replace old blueprint "${existingBlueprint._id}" ("ss1") with new blueprint "show2"` ) }) - testInFiber('update - drop blueprintId', async () => { + test('update - drop blueprintId', async () => { const BLUEPRINT_TYPE = BlueprintManifestType.SHOWSTYLE const blueprintStr = packageBlueprint( { diff --git a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts index 47fce54f94..419a13b9a7 100644 --- a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts +++ b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts @@ -1,50 +1,23 @@ -import * as _ from 'underscore' +import '../../../../__mocks__/_extendJest' import { setupDefaultStudioEnvironment } from '../../../../__mocks__/helpers/database' -import { testInFiber } from '../../../../__mocks__/helpers/jest' +import { literal, unprotectString } from '../../../lib/tempLib' import { - PeripheralDevice, - PeripheralDeviceCategory, - PeripheralDeviceType, - PERIPHERAL_SUBTYPE_PROCESS, -} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { literal, getRandomId, protectString, unprotectString } from '../../../lib/tempLib' -import { - LookaheadMode, - BlueprintMapping, - ISourceLayer, - SourceLayerType, - IOutputLayer, - TSR, - IBlueprintShowStyleVariant, - IBlueprintConfig, TriggerType, ClientActions, PlayoutActions, IBlueprintTriggeredActions, } from '@sofie-automation/blueprints-integration' -import { DBStudio, MappingExt } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { MigrationContextStudio, MigrationContextShowStyle, MigrationContextSystem } from '../migrationContext' -import { DBShowStyleBase, SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { - applyAndValidateOverrides, - wrapDefaultObject, -} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { - CoreSystem, - PeripheralDevices, - ShowStyleBases, - ShowStyleVariants, - Studios, - TriggeredActions, -} from '../../../collections' -import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' +import { MigrationContextSystem } from '../migrationContext' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { CoreSystem, TriggeredActions } from '../../../collections' describe('Test blueprint migrationContext', () => { beforeAll(async () => { await setupDefaultStudioEnvironment() }) + // eslint-disable-next-line jest/no-commented-out-tests + /* describe('MigrationContextStudio', () => { async function getContext() { const studio = (await Studios.findOneAsync({})) as DBStudio @@ -63,17 +36,17 @@ describe('Test blueprint migrationContext', () => { return studio2.mappingsWithOverrides.defaults[mappingId] } - testInFiber('getMapping: no id', async () => { + test('getMapping: no id', async () => { const ctx = await getContext() const mapping = ctx.getMapping('') expect(mapping).toBeFalsy() }) - testInFiber('getMapping: missing', async () => { + test('getMapping: missing', async () => { const ctx = await getContext() const mapping = ctx.getMapping('fake_mapping') expect(mapping).toBeFalsy() }) - testInFiber('getMapping: good', async () => { + test('getMapping: good', async () => { const ctx = await getContext() const studio = getStudio(ctx) const rawMapping: MappingExt = { @@ -92,7 +65,7 @@ describe('Test blueprint migrationContext', () => { expect(mapping).not.toEqual(studio.mappingsWithOverrides.defaults['mapping1']) }) - testInFiber('insertMapping: good', async () => { + test('insertMapping: good', async () => { const ctx = await getContext() const rawMapping: BlueprintMapping = { @@ -113,7 +86,7 @@ describe('Test blueprint migrationContext', () => { const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') expect(dbMapping).toEqual(rawMapping) }) - testInFiber('insertMapping: no id', async () => { + test('insertMapping: no id', async () => { const ctx = await getContext() const rawMapping: BlueprintMapping = { @@ -133,7 +106,7 @@ describe('Test blueprint migrationContext', () => { const dbMapping = await getMappingFromDb(getStudio(ctx), '') expect(dbMapping).toBeFalsy() }) - testInFiber('insertMapping: existing', async () => { + test('insertMapping: existing', async () => { const ctx = await getContext() const existingMapping = ctx.getMapping('mapping2') expect(existingMapping).toBeTruthy() @@ -159,7 +132,7 @@ describe('Test blueprint migrationContext', () => { expect(dbMapping).toEqual(existingMapping) }) - testInFiber('updateMapping: good', async () => { + test('updateMapping: good', async () => { const ctx = await getContext() const existingMapping = ctx.getMapping('mapping2') as BlueprintMapping expect(existingMapping).toBeTruthy() @@ -183,7 +156,7 @@ describe('Test blueprint migrationContext', () => { const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') expect(dbMapping).toEqual(expectedMapping) }) - testInFiber('updateMapping: no props', async () => { + test('updateMapping: no props', async () => { const ctx = await getContext() const existingMapping = ctx.getMapping('mapping2') as BlueprintMapping expect(existingMapping).toBeTruthy() @@ -191,7 +164,7 @@ describe('Test blueprint migrationContext', () => { // Should not error ctx.updateMapping('mapping2', {}) }) - testInFiber('updateMapping: no id', async () => { + test('updateMapping: no id', async () => { const ctx = await getContext() const existingMapping = ctx.getMapping('') as BlueprintMapping expect(existingMapping).toBeFalsy() @@ -200,7 +173,7 @@ describe('Test blueprint migrationContext', () => { `[404] Mapping "" cannot be updated as it does not exist` ) }) - testInFiber('updateMapping: missing', async () => { + test('updateMapping: missing', async () => { const ctx = await getContext() expect(ctx.getMapping('mapping1')).toBeFalsy() @@ -222,14 +195,14 @@ describe('Test blueprint migrationContext', () => { expect(dbMapping).toBeFalsy() }) - testInFiber('removeMapping: missing', async () => { + test('removeMapping: missing', async () => { const ctx = await getContext() expect(ctx.getMapping('mapping1')).toBeFalsy() // Should not error ctx.removeMapping('mapping1') }) - testInFiber('removeMapping: no id', async () => { + test('removeMapping: no id', async () => { const ctx = await getContext() expect(ctx.getMapping('')).toBeFalsy() expect(ctx.getMapping('mapping2')).toBeTruthy() @@ -240,7 +213,7 @@ describe('Test blueprint migrationContext', () => { // ensure other mappings still exist expect(await getMappingFromDb(getStudio(ctx), 'mapping2')).toBeTruthy() }) - testInFiber('removeMapping: good', async () => { + test('removeMapping: good', async () => { const ctx = await getContext() expect(ctx.getMapping('mapping2')).toBeTruthy() @@ -259,17 +232,17 @@ describe('Test blueprint migrationContext', () => { return studio2.blueprintConfigWithOverrides.defaults } - testInFiber('getConfig: no id', async () => { + test('getConfig: no id', async () => { const ctx = await getContext() expect(ctx.getConfig('')).toBeFalsy() }) - testInFiber('getConfig: missing', async () => { + test('getConfig: missing', async () => { const ctx = await getContext() expect(ctx.getConfig('conf1')).toBeFalsy() }) - testInFiber('getConfig: good', async () => { + test('getConfig: good', async () => { const ctx = await getContext() const studio = getStudio(ctx) @@ -280,7 +253,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getConfig('conf2')).toEqual('af') }) - testInFiber('setConfig: no id', async () => { + test('setConfig: no id', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -291,7 +264,7 @@ describe('Test blueprint migrationContext', () => { expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('setConfig: insert', async () => { + test('setConfig: insert', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -310,7 +283,7 @@ describe('Test blueprint migrationContext', () => { expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('setConfig: insert undefined', async () => { + test('setConfig: insert undefined', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -330,7 +303,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('setConfig: update', async () => { + test('setConfig: update', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -349,7 +322,7 @@ describe('Test blueprint migrationContext', () => { expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('setConfig: update undefined', async () => { + test('setConfig: update undefined', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -369,7 +342,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('removeConfig: no id', async () => { + test('removeConfig: no id', async () => { const ctx = await getContext() const studio = getStudio(ctx) ctx.setConfig('conf1', true) @@ -383,7 +356,7 @@ describe('Test blueprint migrationContext', () => { expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('removeConfig: missing', async () => { + test('removeConfig: missing', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -397,7 +370,7 @@ describe('Test blueprint migrationContext', () => { expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) }) - testInFiber('removeConfig: good', async () => { + test('removeConfig: good', async () => { const ctx = await getContext() const studio = getStudio(ctx) const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) @@ -469,17 +442,17 @@ describe('Test blueprint migrationContext', () => { return device as PeripheralDevice } - testInFiber('getDevice: no id', async () => { + test('getDevice: no id', async () => { const ctx = await getContext() const device = ctx.getDevice('') expect(device).toBeFalsy() }) - testInFiber('getDevice: missing', async () => { + test('getDevice: missing', async () => { const ctx = await getContext() const device = ctx.getDevice('fake_device') expect(device).toBeFalsy() }) - testInFiber('getDevice: missing with parent', async () => { + test('getDevice: missing with parent', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const playoutId = await createPlayoutDevice(studio) @@ -488,7 +461,7 @@ describe('Test blueprint migrationContext', () => { const device = ctx.getDevice('fake_device') expect(device).toBeFalsy() }) - testInFiber('getDevice: good', async () => { + test('getDevice: good', async () => { const ctx = await getContext() const peripheral = getPlayoutDevice(await getStudio(ctx)) expect(peripheral).toBeTruthy() @@ -501,7 +474,7 @@ describe('Test blueprint migrationContext', () => { expect(device2).toBeFalsy() }) - testInFiber('insertDevice: no id', async () => { + test('insertDevice: no id', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -514,7 +487,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getDevice('')).toBeFalsy() expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('insertDevice: already exists', async () => { + test('insertDevice: already exists', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -526,7 +499,7 @@ describe('Test blueprint migrationContext', () => { expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('insertDevice: ok', async () => { + test('insertDevice: ok', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -546,7 +519,7 @@ describe('Test blueprint migrationContext', () => { expect(device).toEqual(rawDevice) }) - testInFiber('updateDevice: no id', async () => { + test('updateDevice: no id', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -559,7 +532,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getDevice('')).toBeFalsy() expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('updateDevice: missing', async () => { + test('updateDevice: missing', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -571,7 +544,7 @@ describe('Test blueprint migrationContext', () => { expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('Device: good', async () => { + test('Device: good', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -593,7 +566,7 @@ describe('Test blueprint migrationContext', () => { expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('removeDevice: no id', async () => { + test('removeDevice: no id', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -604,7 +577,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getDevice('')).toBeFalsy() expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('removeDevice: missing', async () => { + test('removeDevice: missing', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -615,7 +588,7 @@ describe('Test blueprint migrationContext', () => { expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) }) - testInFiber('removeDevice: good', async () => { + test('removeDevice: good', async () => { const ctx = await getContext() const studio = await getStudio(ctx) const initialSettings = studio.peripheralDeviceSettings.playoutDevices @@ -659,18 +632,18 @@ describe('Test blueprint migrationContext', () => { } describe('variants', () => { - testInFiber('getAllVariants: good', async () => { + test('getAllVariants: good', async () => { const ctx = await getContext() const variants = ctx.getAllVariants() expect(variants).toHaveLength(1) }) - testInFiber('getAllVariants: missing base', () => { + test('getAllVariants: missing base', () => { const ctx = new MigrationContextShowStyle({ _id: 'fakeStyle' } as any) const variants = ctx.getAllVariants() expect(variants).toHaveLength(0) }) - testInFiber('getVariantId: consistent', async () => { + test('getVariantId: consistent', async () => { const ctx = await getContext() const id1 = ctx.getVariantId('variant1') @@ -680,7 +653,7 @@ describe('Test blueprint migrationContext', () => { const id3 = ctx.getVariantId('variant2') expect(id3).not.toEqual(id1) }) - testInFiber('getVariantId: different base', async () => { + test('getVariantId: different base', async () => { const ctx = await getContext() const ctx2 = new MigrationContextShowStyle({ _id: 'fakeStyle' } as any) @@ -689,7 +662,7 @@ describe('Test blueprint migrationContext', () => { expect(id2).not.toEqual(id1) }) - testInFiber('getVariant: good', async () => { + test('getVariant: good', async () => { const ctx = await getContext() const rawVariant = await createVariant(ctx, 'variant1') @@ -697,19 +670,19 @@ describe('Test blueprint migrationContext', () => { expect(variant).toBeTruthy() expect(variant).toEqual(rawVariant) }) - testInFiber('getVariant: no id', async () => { + test('getVariant: no id', async () => { const ctx = await getContext() expect(() => ctx.getVariant('')).toThrow(`[500] Variant id "" is invalid`) }) - testInFiber('getVariant: missing', async () => { + test('getVariant: missing', async () => { const ctx = await getContext() const variant = ctx.getVariant('fake_variant') expect(variant).toBeFalsy() }) - testInFiber('insertVariant: no id', async () => { + test('insertVariant: no id', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) @@ -721,7 +694,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('insertVariant: already exists', async () => { + test('insertVariant: already exists', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) expect(ctx.getVariant('variant1')).toBeTruthy() @@ -730,11 +703,11 @@ describe('Test blueprint migrationContext', () => { ctx.insertVariant('variant1', { name: 'test2', }) - ).toThrow(/*`[500] Variant id "variant1" already exists`*/) + ).toThrow(/*`[500] Variant id "variant1" already exists`* /) expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('insertVariant: good', async () => { + test('insertVariant: good', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) expect(ctx.getVariant('variant2')).toBeFalsy() @@ -758,7 +731,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('updateVariant: no id', async () => { + test('updateVariant: no id', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) @@ -770,7 +743,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('updateVariant: missing', async () => { + test('updateVariant: missing', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) expect(ctx.getVariant('variant11')).toBeFalsy() @@ -779,12 +752,12 @@ describe('Test blueprint migrationContext', () => { ctx.updateVariant('variant11', { name: 'test2', }) - ).toThrow(/*`[404] Variant id "variant1" does not exist`*/) + ).toThrow(/*`[404] Variant id "variant1" does not exist`* /) // TODO - tidy up the error type expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('updateVariant: good', async () => { + test('updateVariant: good', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) expect(ctx.getVariant('variant1')).toBeTruthy() @@ -801,7 +774,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('removeVariant: no id', async () => { + test('removeVariant: no id', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) @@ -809,7 +782,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('removeVariant: missing', async () => { + test('removeVariant: missing', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) expect(ctx.getVariant('variant11')).toBeFalsy() @@ -819,7 +792,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getAllVariants()).toEqual(initialVariants) }) - testInFiber('removeVariant: good', async () => { + test('removeVariant: good', async () => { const ctx = await getContext() const initialVariants = _.clone(ctx.getAllVariants()) expect(ctx.getVariant('variant1')).toBeTruthy() @@ -842,18 +815,18 @@ describe('Test blueprint migrationContext', () => { return showStyle2.sourceLayersWithOverrides.defaults } - testInFiber('getSourceLayer: no id', async () => { + test('getSourceLayer: no id', async () => { const ctx = await getContext() expect(() => ctx.getSourceLayer('')).toThrow(`[500] SourceLayer id "" is invalid`) }) - testInFiber('getSourceLayer: missing', async () => { + test('getSourceLayer: missing', async () => { const ctx = await getContext() const layer = ctx.getSourceLayer('fake_source_layer') expect(layer).toBeFalsy() }) - testInFiber('getSourceLayer: good', async () => { + test('getSourceLayer: good', async () => { const ctx = await getContext() const layer = ctx.getSourceLayer('cam0') as ISourceLayer @@ -865,7 +838,7 @@ describe('Test blueprint migrationContext', () => { expect(layer2._id).toEqual('vt0') }) - testInFiber('insertSourceLayer: no id', async () => { + test('insertSourceLayer: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -881,7 +854,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('insertSourceLayer: existing', async () => { + test('insertSourceLayer: existing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -897,7 +870,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('insertSourceLayer: good', async () => { + test('insertSourceLayer: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -918,7 +891,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('updateSourceLayer: no id', async () => { + test('updateSourceLayer: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -934,7 +907,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('updateSourceLayer: missing', async () => { + test('updateSourceLayer: missing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -950,7 +923,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('updateSourceLayer: good', async () => { + test('updateSourceLayer: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -971,7 +944,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('removeSourceLayer: no id', async () => { + test('removeSourceLayer: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -981,7 +954,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('removeSourceLayer: missing', async () => { + test('removeSourceLayer: missing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -993,7 +966,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) }) - testInFiber('removeSourceLayer: good', async () => { + test('removeSourceLayer: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) @@ -1017,18 +990,18 @@ describe('Test blueprint migrationContext', () => { return showStyle2.outputLayersWithOverrides.defaults } - testInFiber('getOutputLayer: no id', async () => { + test('getOutputLayer: no id', async () => { const ctx = await getContext() expect(() => ctx.getOutputLayer('')).toThrow(`[500] OutputLayer id "" is invalid`) }) - testInFiber('getOutputLayer: missing', async () => { + test('getOutputLayer: missing', async () => { const ctx = await getContext() const layer = ctx.getOutputLayer('fake_source_layer') expect(layer).toBeFalsy() }) - testInFiber('getOutputLayer: good', async () => { + test('getOutputLayer: good', async () => { const ctx = await getContext() const layer = ctx.getOutputLayer('pgm') as IOutputLayer @@ -1036,7 +1009,7 @@ describe('Test blueprint migrationContext', () => { expect(layer._id).toEqual('pgm') }) - testInFiber('insertOutputLayer: no id', async () => { + test('insertOutputLayer: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1052,7 +1025,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('insertOutputLayer: existing', async () => { + test('insertOutputLayer: existing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1068,7 +1041,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('insertOutputLayer: good', async () => { + test('insertOutputLayer: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1089,7 +1062,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('updateOutputLayer: no id', async () => { + test('updateOutputLayer: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1105,7 +1078,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('updateOutputLayer: missing', async () => { + test('updateOutputLayer: missing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1121,7 +1094,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('updateOutputLayer: good', async () => { + test('updateOutputLayer: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1142,7 +1115,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('removeOutputLayer: no id', async () => { + test('removeOutputLayer: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1152,7 +1125,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('removeOutputLayer: missing', async () => { + test('removeOutputLayer: missing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1164,7 +1137,7 @@ describe('Test blueprint migrationContext', () => { expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) }) - testInFiber('removeOutputLayer: good', async () => { + test('removeOutputLayer: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) @@ -1186,17 +1159,17 @@ describe('Test blueprint migrationContext', () => { return showStyle2.blueprintConfigWithOverrides.defaults } - testInFiber('getBaseConfig: no id', async () => { + test('getBaseConfig: no id', async () => { const ctx = await getContext() expect(ctx.getBaseConfig('')).toBeFalsy() }) - testInFiber('getBaseConfig: missing', async () => { + test('getBaseConfig: missing', async () => { const ctx = await getContext() expect(ctx.getBaseConfig('conf1')).toBeFalsy() }) - testInFiber('getBaseConfig: good', async () => { + test('getBaseConfig: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) @@ -1207,7 +1180,7 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getBaseConfig('conf2')).toEqual('af') }) - testInFiber('setBaseConfig: no id', async () => { + test('setBaseConfig: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1218,7 +1191,7 @@ describe('Test blueprint migrationContext', () => { expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('setBaseConfig: insert', async () => { + test('setBaseConfig: insert', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1237,7 +1210,7 @@ describe('Test blueprint migrationContext', () => { expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('setBaseConfig: insert undefined', async () => { + test('setBaseConfig: insert undefined', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1252,7 +1225,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('setBaseConfig: update', async () => { + test('setBaseConfig: update', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1271,7 +1244,7 @@ describe('Test blueprint migrationContext', () => { expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('setBaseConfig: update undefined', async () => { + test('setBaseConfig: update undefined', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1286,7 +1259,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('removeBaseConfig: no id', async () => { + test('removeBaseConfig: no id', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) ctx.setBaseConfig('conf1', true) @@ -1300,7 +1273,7 @@ describe('Test blueprint migrationContext', () => { expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('removeBaseConfig: missing', async () => { + test('removeBaseConfig: missing', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1314,7 +1287,7 @@ describe('Test blueprint migrationContext', () => { expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) }) - testInFiber('removeBaseConfig: good', async () => { + test('removeBaseConfig: good', async () => { const ctx = await getContext() const showStyle = getShowStyle(ctx) const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) @@ -1341,25 +1314,25 @@ describe('Test blueprint migrationContext', () => { return variant.blueprintConfigWithOverrides.defaults } - testInFiber('getVariantConfig: no variant id', async () => { + test('getVariantConfig: no variant id', async () => { const ctx = await getContext() expect(() => ctx.getVariantConfig('', 'conf1')).toThrow(`[404] ShowStyleVariant "" not found`) }) - testInFiber('getVariantConfig: missing variant', async () => { + test('getVariantConfig: missing variant', async () => { const ctx = await getContext() expect(() => ctx.getVariantConfig('fake_variant', 'conf1')).toThrow( `[404] ShowStyleVariant "fake_variant" not found` ) }) - testInFiber('getVariantConfig: missing', async () => { + test('getVariantConfig: missing', async () => { const ctx = await getContext() await createVariant(ctx, 'configVariant', { conf1: 5, conf2: ' af ' }) expect(ctx.getVariantConfig('configVariant', 'conf11')).toBeFalsy() }) - testInFiber('getVariantConfig: good', async () => { + test('getVariantConfig: good', async () => { const ctx = await getContext() expect(ctx.getVariant('configVariant')).toBeTruthy() @@ -1367,19 +1340,19 @@ describe('Test blueprint migrationContext', () => { expect(ctx.getVariantConfig('configVariant', 'conf2')).toEqual('af') }) - testInFiber('setVariantConfig: no variant id', async () => { + test('setVariantConfig: no variant id', async () => { const ctx = await getContext() expect(() => ctx.setVariantConfig('', 'conf1', 5)).toThrow(`[404] ShowStyleVariant "" not found`) }) - testInFiber('setVariantConfig: missing variant', async () => { + test('setVariantConfig: missing variant', async () => { const ctx = await getContext() expect(() => ctx.setVariantConfig('fake_variant', 'conf1', 5)).toThrow( `[404] ShowStyleVariant "fake_variant" not found` ) }) - testInFiber('setVariantConfig: no id', async () => { + test('setVariantConfig: no id', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariant('configVariant')).toBeTruthy() @@ -1389,7 +1362,7 @@ describe('Test blueprint migrationContext', () => { // VariantConfig should not have changed expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('setVariantConfig: insert', async () => { + test('setVariantConfig: insert', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariantConfig('configVariant', 'conf19')).toBeFalsy() @@ -1406,7 +1379,7 @@ describe('Test blueprint migrationContext', () => { initialVariantConfig[expectedItem._id] = expectedItem.value expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('setVariantConfig: insert undefined', async () => { + test('setVariantConfig: insert undefined', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariantConfig('configVariant', 'confUndef')).toBeFalsy() @@ -1419,7 +1392,7 @@ describe('Test blueprint migrationContext', () => { expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('setVariantConfig: update', async () => { + test('setVariantConfig: update', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() @@ -1436,7 +1409,7 @@ describe('Test blueprint migrationContext', () => { initialVariantConfig[expectedItem._id] = expectedItem.value expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('setVariantConfig: update undefined', async () => { + test('setVariantConfig: update undefined', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() @@ -1449,19 +1422,19 @@ describe('Test blueprint migrationContext', () => { expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('removeVariantConfig: no variant id', async () => { + test('removeVariantConfig: no variant id', async () => { const ctx = await getContext() expect(() => ctx.removeVariantConfig('', 'conf1')).toThrow(`[404] ShowStyleVariant "" not found`) }) - testInFiber('removeVariantConfig: missing variant', async () => { + test('removeVariantConfig: missing variant', async () => { const ctx = await getContext() expect(() => ctx.removeVariantConfig('fake_variant', 'conf1')).toThrow( `[404] ShowStyleVariant "fake_variant" not found` ) }) - testInFiber('removeVariantConfig: no id', async () => { + test('removeVariantConfig: no id', async () => { const ctx = await getContext() ctx.setVariantConfig('configVariant', 'conf1', true) const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) @@ -1473,7 +1446,7 @@ describe('Test blueprint migrationContext', () => { // VariantConfig should not have changed expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('removeVariantConfig: missing', async () => { + test('removeVariantConfig: missing', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() @@ -1485,7 +1458,7 @@ describe('Test blueprint migrationContext', () => { // VariantConfig should not have changed expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) }) - testInFiber('removeVariantConfig: good', async () => { + test('removeVariantConfig: good', async () => { const ctx = await getContext() const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() @@ -1499,6 +1472,7 @@ describe('Test blueprint migrationContext', () => { }) }) }) + */ describe('MigrationContextSystem', () => { async function getContext() { @@ -1522,60 +1496,65 @@ describe('Test blueprint migrationContext', () => { ) } describe('triggeredActions', () => { - testInFiber('getAllTriggeredActions: return all triggeredActions', async () => { + test('getAllTriggeredActions: return all triggeredActions', async () => { const ctx = await getContext() // default studio environment should have 3 core-level actions - expect(ctx.getAllTriggeredActions()).toHaveLength(3) + expect(await ctx.getAllTriggeredActions()).toHaveLength(3) }) - testInFiber('getTriggeredAction: no id', async () => { + test('getTriggeredAction: no id', async () => { const ctx = await getContext() - expect(() => ctx.getTriggeredAction('')).toThrow('[500] Triggered actions Id "" is invalid') + await expect(ctx.getTriggeredAction('')).rejects.toThrowMeteor( + 500, + 'Triggered actions Id "" is invalid' + ) }) - testInFiber('getTriggeredAction: missing id', async () => { + test('getTriggeredAction: missing id', async () => { const ctx = await getContext() - expect(ctx.getTriggeredAction('abc')).toBeFalsy() + expect(await ctx.getTriggeredAction('abc')).toBeFalsy() }) - testInFiber('getTriggeredAction: existing id', async () => { + test('getTriggeredAction: existing id', async () => { const ctx = await getContext() const existingTriggeredActions = (await getSystemTriggeredActions())[0] expect(existingTriggeredActions).toBeTruthy() - expect(ctx.getTriggeredAction(existingTriggeredActions._id)).toMatchObject(existingTriggeredActions) + expect(await ctx.getTriggeredAction(existingTriggeredActions._id)).toMatchObject( + existingTriggeredActions + ) }) - testInFiber('setTriggeredAction: set undefined', async () => { + test('setTriggeredAction: set undefined', async () => { const ctx = await getContext() - expect(() => ctx.setTriggeredAction(undefined as any)).toThrow(/Match error/) + await expect(ctx.setTriggeredAction(undefined as any)).rejects.toThrow(/Match error/) }) - testInFiber('setTriggeredAction: set without id', async () => { + test('setTriggeredAction: set without id', async () => { const ctx = await getContext() - expect(() => + await expect( ctx.setTriggeredAction({ _rank: 0, actions: [], triggers: [], } as any) - ).toThrow(/Match error/) + ).rejects.toThrow(/Match error/) }) - testInFiber('setTriggeredAction: set without actions', async () => { + test('setTriggeredAction: set without actions', async () => { const ctx = await getContext() - expect(() => + await expect( ctx.setTriggeredAction({ _id: 'test1', _rank: 0, triggers: [], } as any) - ).toThrow(/Match error/) + ).rejects.toThrow(/Match error/) }) - testInFiber('setTriggeredAction: set with null as name', async () => { + test('setTriggeredAction: set with null as name', async () => { const ctx = await getContext() - expect(() => + await expect( ctx.setTriggeredAction({ _id: 'test1', _rank: 0, @@ -1583,14 +1562,14 @@ describe('Test blueprint migrationContext', () => { triggers: [], name: null, } as any) - ).toThrow(/Match error/) + ).rejects.toThrow(/Match error/) }) - testInFiber('setTriggeredAction: set non-existing id', async () => { + test('setTriggeredAction: set non-existing id', async () => { const ctx = await getContext() const blueprintLocalId = 'test0' - ctx.setTriggeredAction({ + await ctx.setTriggeredAction({ _id: blueprintLocalId, _rank: 1001, actions: { @@ -1611,20 +1590,20 @@ describe('Test blueprint migrationContext', () => { }, }, }) - const insertedTriggeredAction = ctx.getTriggeredAction(blueprintLocalId) + const insertedTriggeredAction = await ctx.getTriggeredAction(blueprintLocalId) expect(insertedTriggeredAction).toBeTruthy() // the actual id in the database should not be the same as the one provided // in the setTriggeredAction method expect(insertedTriggeredAction?._id !== blueprintLocalId).toBe(true) }) - testInFiber('setTriggeredAction: set existing id', async () => { + test('setTriggeredAction: set existing id', async () => { const ctx = await getContext() - const oldCoreAction = ctx.getTriggeredAction('mockTriggeredAction_core0') + const oldCoreAction = await ctx.getTriggeredAction('mockTriggeredAction_core0') expect(oldCoreAction).toBeTruthy() expect(oldCoreAction?.actions[0].action).toBe(PlayoutActions.adlib) - ctx.setTriggeredAction({ + await ctx.setTriggeredAction({ _id: 'mockTriggeredAction_core0', _rank: 0, actions: { @@ -1646,23 +1625,26 @@ describe('Test blueprint migrationContext', () => { }, }) - const newCoreAction = ctx.getTriggeredAction('mockTriggeredAction_core0') + const newCoreAction = await ctx.getTriggeredAction('mockTriggeredAction_core0') expect(newCoreAction).toBeTruthy() expect(newCoreAction?.actions[0].action).toBe(PlayoutActions.activateRundownPlaylist) }) - testInFiber('removeTriggeredAction: remove empty id', async () => { + test('removeTriggeredAction: remove empty id', async () => { const ctx = await getContext() - expect(() => ctx.removeTriggeredAction('')).toThrow('[500] Triggered actions Id "" is invalid') + await expect(ctx.removeTriggeredAction('')).rejects.toThrowMeteor( + 500, + 'Triggered actions Id "" is invalid' + ) }) - testInFiber('removeTriggeredAction: remove existing id', async () => { + test('removeTriggeredAction: remove existing id', async () => { const ctx = await getContext() - const oldCoreAction = ctx.getTriggeredAction('mockTriggeredAction_core0') + const oldCoreAction = await ctx.getTriggeredAction('mockTriggeredAction_core0') expect(oldCoreAction).toBeTruthy() - ctx.removeTriggeredAction('mockTriggeredAction_core0') - expect(ctx.getTriggeredAction('mockTriggeredAction_core0')).toBeFalsy() + await ctx.removeTriggeredAction('mockTriggeredAction_core0') + expect(await ctx.getTriggeredAction('mockTriggeredAction_core0')).toBeFalsy() }) }) }) diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index fe742fdc9d..a273f24bd1 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -1,52 +1,20 @@ -import * as _ from 'underscore' -import { - getHash, - unprotectObject, - protectString, - unprotectString, - objectPathGet, - objectPathSet, - clone, - Complete, - objectPathDelete, -} from '../../lib/tempLib' -import { waitForPromise } from '../../lib/lib' -import { DBStudio, StudioPlayoutDevice } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { getHash, protectString, unprotectString, clone, Complete } from '../../lib/tempLib' import { Meteor } from 'meteor/meteor' import { - ConfigItemValue, - MigrationContextStudio as IMigrationContextStudio, - MigrationContextShowStyle as IMigrationContextShowStyle, MigrationContextSystem as IMigrationContextSystem, - BlueprintMapping, - IOutputLayer, - ISourceLayer, - ShowStyleVariantPart, - IBlueprintShowStyleVariant, - TSR, - OmitId, IBlueprintTriggeredActions, } from '@sofie-automation/blueprints-integration' - -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { check } from '../../lib/check' -import { - PERIPHERAL_SUBTYPE_PROCESS, - PeripheralDeviceType, -} from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { TriggeredActionsObj } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' import { Match } from 'meteor/check' -import { MongoModifier } from '@sofie-automation/corelib/dist/mongo' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ShowStyleBaseId, ShowStyleVariantId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDevices, ShowStyleBases, ShowStyleVariants, Studios, TriggeredActions } from '../../collections' -import { literal } from '@sofie-automation/shared-lib/dist/lib/lib' +import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { TriggeredActions } from '../../collections' -function trimIfString(value: T): T | string { - if (_.isString(value)) return value.trim() - return value -} +// function trimIfString(value: T): T | string { +// if (_.isString(value)) return value.trim() +// return value +// } function convertTriggeredActionToBlueprints(triggeredAction: TriggeredActionsObj): IBlueprintTriggeredActions { const obj: Complete = { @@ -69,40 +37,36 @@ class AbstractMigrationContextWithTriggeredActions { private getProtectedTriggeredActionId(triggeredActionId: string): TriggeredActionId { return protectString(this.getTriggeredActionId(triggeredActionId)) } - getAllTriggeredActions(): IBlueprintTriggeredActions[] { - return waitForPromise( - TriggeredActions.findFetchAsync({ + async getAllTriggeredActions(): Promise { + return ( + await TriggeredActions.findFetchAsync({ showStyleBaseId: this.showStyleBaseId, }) ).map(convertTriggeredActionToBlueprints) } - private getTriggeredActionFromDb(triggeredActionId: string): TriggeredActionsObj | undefined { - const triggeredAction = waitForPromise( - TriggeredActions.findOneAsync({ - showStyleBaseId: this.showStyleBaseId, - _id: this.getProtectedTriggeredActionId(triggeredActionId), - }) - ) + private async getTriggeredActionFromDb(triggeredActionId: string): Promise { + const triggeredAction = await TriggeredActions.findOneAsync({ + showStyleBaseId: this.showStyleBaseId, + _id: this.getProtectedTriggeredActionId(triggeredActionId), + }) if (triggeredAction) return triggeredAction // Assume we were given the full id - return waitForPromise( - TriggeredActions.findOneAsync({ - showStyleBaseId: this.showStyleBaseId, - _id: protectString(triggeredActionId), - }) - ) + return TriggeredActions.findOneAsync({ + showStyleBaseId: this.showStyleBaseId, + _id: protectString(triggeredActionId), + }) } - getTriggeredAction(triggeredActionId: string): IBlueprintTriggeredActions | undefined { + async getTriggeredAction(triggeredActionId: string): Promise { check(triggeredActionId, String) if (!triggeredActionId) { throw new Meteor.Error(500, `Triggered actions Id "${triggeredActionId}" is invalid`) } - const obj = this.getTriggeredActionFromDb(triggeredActionId) + const obj = await this.getTriggeredActionFromDb(triggeredActionId) return obj ? convertTriggeredActionToBlueprints(obj) : undefined } - setTriggeredAction(triggeredActions: IBlueprintTriggeredActions) { + async setTriggeredAction(triggeredActions: IBlueprintTriggeredActions): Promise { check(triggeredActions, Object) check(triggeredActions._id, String) check(triggeredActions._rank, Number) @@ -123,43 +87,37 @@ class AbstractMigrationContextWithTriggeredActions { blueprintUniqueId: triggeredActions._id, } - const currentTriggeredAction = this.getTriggeredActionFromDb(triggeredActions._id) + const currentTriggeredAction = await this.getTriggeredActionFromDb(triggeredActions._id) if (!currentTriggeredAction) { - waitForPromise( - TriggeredActions.insertAsync({ - ...newObj, - showStyleBaseId: this.showStyleBaseId, - _id: this.getProtectedTriggeredActionId(triggeredActions._id), - }) - ) + await TriggeredActions.insertAsync({ + ...newObj, + showStyleBaseId: this.showStyleBaseId, + _id: this.getProtectedTriggeredActionId(triggeredActions._id), + }) } else { - waitForPromise( - TriggeredActions.updateAsync( - { - _id: currentTriggeredAction._id, - }, - { - $set: newObj, - }, - { multi: true } - ) + await TriggeredActions.updateAsync( + { + _id: currentTriggeredAction._id, + }, + { + $set: newObj, + }, + { multi: true } ) } } - removeTriggeredAction(triggeredActionId: string) { + async removeTriggeredAction(triggeredActionId: string): Promise { check(triggeredActionId, String) if (!triggeredActionId) { throw new Meteor.Error(500, `Triggered actions Id "${triggeredActionId}" is invalid`) } - const currentTriggeredAction = this.getTriggeredActionFromDb(triggeredActionId) + const currentTriggeredAction = await this.getTriggeredActionFromDb(triggeredActionId) if (currentTriggeredAction) { - waitForPromise( - TriggeredActions.removeAsync({ - _id: currentTriggeredAction._id, - showStyleBaseId: this.showStyleBaseId, - }) - ) + await TriggeredActions.removeAsync({ + _id: currentTriggeredAction._id, + showStyleBaseId: this.showStyleBaseId, + }) } } } @@ -168,6 +126,7 @@ export class MigrationContextSystem extends AbstractMigrationContextWithTriggeredActions implements IMigrationContextSystem {} +/* export class MigrationContextStudio implements IMigrationContextStudio { private studio: DBStudio @@ -774,3 +733,4 @@ export class MigrationContextShowStyle } } } +*/ diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index e9b7250963..eea90f3f77 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -10,7 +10,6 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import EventEmitter from 'events' import { Meteor } from 'meteor/meteor' import _ from 'underscore' -import { MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' @@ -21,6 +20,7 @@ import { RundownContentObserver } from './RundownContentObserver' import { RundownsObserver } from './RundownsObserver' import { RundownPlaylists, Rundowns, ShowStyleBases } from '../../collections' import { PromiseDebounce } from '../../publications/lib/PromiseDebounce' +import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection' type ChangedHandler = (showStyleBaseId: ShowStyleBaseId, cache: ContentCache) => () => void @@ -84,7 +84,7 @@ export class StudioObserver extends EventEmitter { { projection: rundownPlaylistFieldSpecifier, } - ) as Promise>> + ) as Promise>> ) .end(this.updatePlaylistInStudio) } @@ -137,7 +137,7 @@ export class StudioObserver extends EventEmitter { 'currentRundown', async () => Rundowns.findWithCursor({ _id: rundownId }, { fields: rundownFieldSpecifier, limit: 1 }) as Promise< - MongoCursor> + MinimalMongoCursor> > ) .next('showStyleBase', async (chain) => @@ -148,7 +148,7 @@ export class StudioObserver extends EventEmitter { fields: showStyleBaseFieldSpecifier, limit: 1, } - ) as Promise>>) + ) as Promise>>) : null ) .end(this.updateShowStyle.call) diff --git a/meteor/server/api/deviceTriggers/observer.ts b/meteor/server/api/deviceTriggers/observer.ts index fb1448f24e..aa6bc4bc86 100644 --- a/meteor/server/api/deviceTriggers/observer.ts +++ b/meteor/server/api/deviceTriggers/observer.ts @@ -18,14 +18,13 @@ import { StudioObserver } from './StudioObserver' import { Studios } from '../../collections' import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { MeteorStartupAsync } from '../../lib/lib' type ObserverAndManager = { observer: StudioObserver manager: StudioDeviceTriggerManager } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { const studioObserversAndManagers = new Map() const jobQueue = new JobQueueWithClasses({ autoStart: true, diff --git a/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts b/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts index 5de794580a..f5fd0c2022 100644 --- a/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts +++ b/meteor/server/api/ingest/mosDevice/__tests__/actions.test.ts @@ -3,7 +3,6 @@ import { Meteor } from 'meteor/meteor' import { MOS } from '@sofie-automation/meteor-lib/dist/mos' import { setupDefaultStudioEnvironment } from '../../../../../__mocks__/helpers/database' -import { testInFiber } from '../../../../../__mocks__/helpers/jest' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MOSDeviceActions } from '../actions' import { PeripheralDeviceCommand } from '@sofie-automation/corelib/dist/dataModel/PeripheralDeviceCommand' @@ -47,7 +46,7 @@ describe('Test sending mos actions', () => { } }) - testInFiber('reloadRundown: expect error', async () => { + test('reloadRundown: expect error', async () => { // setLogLevel(LogLevel.DEBUG) const rundownId: RundownId = getRandomId() @@ -82,7 +81,7 @@ describe('Test sending mos actions', () => { await expect(MOSDeviceActions.reloadRundown(device, fakeRundown)).rejects.toMatch(`unknown annoying error`) }) - testInFiber('reloadRundown: valid payload', async () => { + test('reloadRundown: valid payload', async () => { // setLogLevel(LogLevel.DEBUG) const roData = fakeMinimalRo() @@ -140,7 +139,7 @@ describe('Test sending mos actions', () => { ) }) - testInFiber('reloadRundown: receive incorrect response rundown id', async () => { + test('reloadRundown: receive incorrect response rundown id', async () => { // setLogLevel(LogLevel.DEBUG) const roData = fakeMinimalRo() diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 6534daaf34..8b05552eba 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -3,7 +3,7 @@ import { check } from '../../lib/check' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { NrcsIngestDataCache, MediaObjects, Parts, Rundowns, Segments } from '../../collections' import { literal } from '../../lib/tempLib' -import { lazyIgnore, MeteorStartupAsync } from '../../lib/lib' +import { lazyIgnore } from '../../lib/lib' import { IngestRundown, IngestSegment, IngestPart, IngestPlaylist } from '@sofie-automation/blueprints-integration' import { logger } from '../../logging' import { RundownIngestDataCache } from './ingestCache' @@ -363,7 +363,7 @@ async function listIngestRundowns(peripheralDevice: PeripheralDevice): Promise { +Meteor.startup(async () => { await MediaObjects.observe( {}, { diff --git a/meteor/server/api/methods.ts b/meteor/server/api/methods.ts index 10de2b2be3..b8566a5db2 100644 --- a/meteor/server/api/methods.ts +++ b/meteor/server/api/methods.ts @@ -1,24 +1,4 @@ import { Meteor } from 'meteor/meteor' import { MakeMeteorCall } from '@sofie-automation/meteor-lib/dist/api/methods' -export const MeteorCall = MakeMeteorCall(MeteorPromiseApply) - -/** - * Convenience method to convert a Meteor.apply() into a Promise - * @param callName {string} Method name - * @param args {Array} An array of arguments for the method call - * @param options (Optional) An object with options for the call. See Meteor documentation. - * @returns {Promise} A promise containing the result of the called method. - */ -export async function MeteorPromiseApply( - callName: Parameters[0], - args: Parameters[1], - options?: Parameters[2] -): Promise { - return new Promise((resolve, reject) => { - Meteor.apply(callName, args, options, (err, res) => { - if (err) reject(err) - else resolve(res) - }) - }) -} +export const MeteorCall = MakeMeteorCall(Meteor.applyAsync) diff --git a/meteor/server/api/profiler/apm.ts b/meteor/server/api/profiler/apm.ts new file mode 100644 index 0000000000..6e2eb99d70 --- /dev/null +++ b/meteor/server/api/profiler/apm.ts @@ -0,0 +1,52 @@ +import { Meteor } from 'meteor/meteor' +// const shimmer = require('shimmer') +import Agent, { AgentConfigOptions } from 'elastic-apm-node' + +// const { Session, Subscription, MongoCursor } = require('./meteorx') + +// Only the ones of these we use have been copied across. +// The others can be found at https://github.com/Meteor-Community-Packages/meteor-elastic-apm/tree/master/instrumenting +// const instrumentMethods = require('./instrumenting/methods') +// const instrumentHttpOut = require('./instrumenting/http-out') +// const instrumentSession = require('./instrumenting/session') +// const instrumentSubscription = require('./instrumenting/subscription') +// const instrumentDB = require('./instrumenting/db') +// const startMetrics = require('./metrics') + +// const hackDB = require('./hacks') + +const [framework, version] = Meteor.release.split('@') + +Agent.setFramework({ + name: framework, + version, + overwrite: true, +}) + +export const RawAgent = Agent + +export function startAgent(config: AgentConfigOptions): void { + if (config.active !== false) { + try { + // Must be called before any other route is registered on WebApp. + // http-in has been moved to be part of where the koa router is mounted + // instrumentHttpOut(Agent) + + Agent.start(config) + + // instrumentMethods(Agent, Meteor), + // instrumentSession(Agent, Session), + // instrumentSubscription(Agent, Subscription), + // hackDB() // TODO: what is this doing? https://github.com/Meteor-Community-Packages/meteor-elastic-apm/blob/master/hacks.js + // instrumentDB replaced by manual wrapping in WrappedAsyncMongoCollection + // startMetrics(Agent), + + Agent.logger.info('meteor-elastic-apm completed instrumenting') + } catch (e) { + Agent.logger.error('Could not start meteor-elastic-apm') + throw e + } + } else { + Agent.logger.warn('meteor-elastic-apm is not active') + } +} diff --git a/meteor/server/api/profiler.ts b/meteor/server/api/profiler/index.ts similarity index 68% rename from meteor/server/api/profiler.ts rename to meteor/server/api/profiler/index.ts index ef57004990..6faa03270c 100644 --- a/meteor/server/api/profiler.ts +++ b/meteor/server/api/profiler/index.ts @@ -1,16 +1,16 @@ -import Agent from 'meteor/julusian:meteor-elastic-apm' +import { RawAgent } from './apm' class Profiler { private active = false startSpan(_name: string) { if (!this.active) return - return Agent.startSpan(_name) + return RawAgent.startSpan(_name) } startTransaction(description: string, name: string) { if (!this.active) return - return Agent.startTransaction(description, name) + return RawAgent.startTransaction(description, name) } setActive(active: boolean) { diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index 3a8c54dc0a..6fc91e8706 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -9,6 +9,7 @@ import { public_dir } from '../../lib' import staticServe from 'koa-static' import { logger } from '../../logging' import { PackageInfo } from '../../coreSystem' +import { profiler } from '../profiler' declare module 'http' { interface IncomingMessage { @@ -46,6 +47,30 @@ Meteor.startup(() => { // Expose the API at the url WebApp.rawConnectHandlers.use((req, res) => { + const transaction = profiler.startTransaction(`${req.method}:${req.url}`, 'http.incoming') + if (transaction) { + transaction.setLabel('url', `${req.url}`) + transaction.setLabel('method', `${req.method}`) + + res.on('finish', () => { + let route = req.originalUrl + if (req.originalUrl && req.url && req.originalUrl.endsWith(req.url.slice(1)) && req.url.length > 1) { + route = req.originalUrl.slice(0, -1 * (req.url.length - 1)) + } + + if (route && route.endsWith('/')) { + route = route.slice(0, -1) + } + + if (route) { + transaction.name = `${req.method}:${route}` + transaction.setLabel('route', `${route}`) + } + + transaction.end() + }) + } + const callback = Meteor.bindEnvironment(koaApp.callback()) callback(req, res).catch(() => res.end()) }) diff --git a/meteor/server/api/rest/v0/__tests__/rest.test.ts b/meteor/server/api/rest/v0/__tests__/rest.test.ts index e40b92664e..41d9e876c7 100644 --- a/meteor/server/api/rest/v0/__tests__/rest.test.ts +++ b/meteor/server/api/rest/v0/__tests__/rest.test.ts @@ -1,4 +1,3 @@ -import { beforeEachInFiber } from '../../../../../__mocks__/helpers/jest' import { MeteorMock } from '../../../../../__mocks__/meteor' import { Meteor } from 'meteor/meteor' import { UserActionAPIMethods } from '@sofie-automation/meteor-lib/dist/api/userActions' @@ -15,8 +14,8 @@ import '../index.ts' describe('REST API', () => { describe('UNSTABLE v0', () => { - beforeEachInFiber(() => { - MeteorMock.mockRunMeteorStartup() + beforeEach(async () => { + await MeteorMock.mockRunMeteorStartup() }) const legacyApiRouter = createLegacyApiRouter() diff --git a/meteor/server/api/rest/v0/index.ts b/meteor/server/api/rest/v0/index.ts index a2a150a51f..b1b007dbbe 100644 --- a/meteor/server/api/rest/v0/index.ts +++ b/meteor/server/api/rest/v0/index.ts @@ -78,9 +78,9 @@ export function createLegacyApiRouter(): KoaRouter { index.POST.push(docString) - assignRoute(router, 'POST', resource, signature.length, (args) => { + assignRoute(router, 'POST', resource, signature.length, async (args) => { const convArgs = typeConvertUrlParameters(args) - return Meteor.call(methodValue, ...convArgs) + return Meteor.callAsync(methodValue, ...convArgs) }) } @@ -159,7 +159,7 @@ function assignRoute( routeType: 'POST' | 'GET', resource: string, paramCount: number, - fcn: (p: any[]) => any + fcn: (p: any[]) => Promise ) { const route = routeType === 'POST' ? router.post.bind(router) : router.get.bind(router) diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index 938567dca5..f3809e77c7 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -283,7 +283,6 @@ async function createDebugSnapshot(studioId: StudioId, organizationId: Organizat if (device.connected && device.subType === PERIPHERAL_SUBTYPE_PROCESS) { const startTime = getCurrentTime() - // defer to another fiber const deviceSnapshot = await executePeripheralDeviceFunction(device._id, 'getSnapshot') logger.info('Got snapshot from device "' + device._id + '"') diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 4b79da80ff..8ba11ff832 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -4,7 +4,7 @@ import { registerClassToMeteorMethods } from '../../methods' import { NewStudiosAPI, StudiosAPIMethods } from '@sofie-automation/meteor-lib/dist/api/studios' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { literal, getRandomId } from '../../lib/tempLib' -import { lazyIgnore, MeteorStartupAsync } from '../../lib/lib' +import { lazyIgnore } from '../../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { ExpectedPackages, @@ -135,7 +135,7 @@ function triggerUpdateStudioMappingsHash(studioId: StudioId) { ) } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { await Studios.observeChanges( {}, { diff --git a/meteor/server/api/system.ts b/meteor/server/api/system.ts index a2c7aa9806..1668f76720 100644 --- a/meteor/server/api/system.ts +++ b/meteor/server/api/system.ts @@ -67,7 +67,7 @@ async function setupIndexes(removeOldIndexes = false): Promise { _.each(i.indexes, (index) => { - i.collection._ensureIndex(index) + i.collection.createIndex(index) }) }) } @@ -119,7 +119,7 @@ let mongoTest: AsyncOnlyMongoCollection | undefined = undefined async function doSystemBenchmarkInner() { if (!mongoTest) { mongoTest = createAsyncOnlyMongoCollection('benchmark-test' as any, false) - mongoTest._ensureIndex({ + mongoTest.createIndex({ indexedProp: 1, }) } diff --git a/meteor/server/api/user.ts b/meteor/server/api/user.ts index 50dd833241..9b3649abdd 100644 --- a/meteor/server/api/user.ts +++ b/meteor/server/api/user.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor' -import * as _ from 'underscore' import { Accounts } from 'meteor/accounts-base' import { unprotectString, protectString } from '../lib/tempLib' import { sleep, deferAsync } from '../lib/lib' @@ -25,7 +24,7 @@ async function enrollUser(email: string, name: string): Promise { profile: { name: name }, }) try { - Accounts.sendEnrollmentEmail(unprotectString(id), email) + await Accounts.sendEnrollmentEmail(unprotectString(id), email) } catch (error) { logger.error('Accounts.sendEnrollmentEmail') logger.error(error) @@ -63,11 +62,13 @@ async function sendVerificationEmail(userId: UserId) { const user = await Users.findOneAsync(userId) if (!user) throw new Meteor.Error(404, `User "${userId}" not found!`) try { - _.each(user.emails, (email) => { - if (!email.verified) { - Accounts.sendVerificationEmail(unprotectString(user._id), email.address) - } - }) + await Promise.all( + user.emails.map(async (email) => { + if (!email.verified) { + await Accounts.sendVerificationEmail(unprotectString(user._id), email.address) + } + }) + ) } catch (error) { logger.error('ERROR sending email verification') logger.error(error) @@ -79,7 +80,7 @@ async function requestResetPassword(email: string): Promise { const meteorUser = Accounts.findUserByEmail(email) as unknown const user = meteorUser as User if (!user) return false - Accounts.sendResetPasswordEmail(unprotectString(user._id)) + await Accounts.sendResetPasswordEmail(unprotectString(user._id)) return true } diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index 7f13f1839a..5a81d597c2 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -5,7 +5,6 @@ import { Meteor } from 'meteor/meteor' import { Mongo } from 'meteor/mongo' import { NpmModuleMongodb } from 'meteor/npm-mongo' import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' -import { waitForPromise } from '../lib/lib' import type { AnyBulkWriteOperation, Collection as RawCollection } from 'mongodb' import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' import { registerCollection } from './lib' @@ -15,22 +14,22 @@ import { WrappedReadOnlyMongoCollection } from './implementations/readonlyWrappe import { FieldNames, IndexSpecifier, - MongoCursor, ObserveCallbacks, ObserveChangesCallbacks, UpdateOptions, UpsertOptions, } from '@sofie-automation/meteor-lib/dist/collections/lib' +import { MinimalMongoCursor } from './implementations/asyncCollection' export interface MongoAllowRules { - insert?: (userId: UserId, doc: DBInterface) => Promise | boolean + insert?: (userId: UserId | null, doc: DBInterface) => Promise | boolean update?: ( - userId: UserId, + userId: UserId | null, doc: DBInterface, fieldNames: FieldNames, modifier: MongoModifier ) => Promise | boolean - remove?: (userId: UserId, doc: DBInterface) => Promise | boolean + remove?: (userId: UserId | null, doc: DBInterface) => Promise | boolean } /** @@ -117,7 +116,7 @@ function wrapMeteorCollectionIntoAsyncCollection(collection, name) } else { // Override the default mongodb methods, because the errors thrown by them doesn't contain the proper call stack @@ -129,33 +128,32 @@ function setupCollectionAllowRules, args: MongoAllowRules | false ) { - if (args) { - const { insert: origInsert, update: origUpdate, remove: origRemove } = args - - const options: Parameters['allow']>[0] = { - insert: origInsert ? (userId, doc) => waitForPromise(origInsert(protectString(userId), doc)) : () => false, - update: origUpdate - ? (userId, doc, fieldNames, modifier) => - waitForPromise(origUpdate(protectString(userId), doc, fieldNames as any, modifier)) - : () => false, - remove: origRemove ? (userId, doc) => waitForPromise(origRemove(protectString(userId), doc)) : () => false, - } - - collection.allow(options) - } else { - // Block all client mutations - collection.allow({ - insert(): boolean { - return false - }, - update() { - return false - }, - remove() { - return false - }, - }) + if (!args) { + // Mutations are disabled by default + return + } + + const { insert: origInsert, update: origUpdate, remove: origRemove } = args + + // These methods behave weirdly, we need to mangle this a bit. + // See https://github.com/meteor/meteor/issues/13444 for a full explanation + const options: any /*Parameters['allow']>[0]*/ = { + insert: () => false, + insertAsync: origInsert + ? (userId: string | null, doc: DBInterface) => origInsert(protectString(userId), doc) as any + : () => false, + update: () => false, + updateAsync: origUpdate + ? (userId: string | null, doc: DBInterface, fieldNames: string[], modifier: any) => + origUpdate(protectString(userId), doc, fieldNames as any, modifier) as any + : () => false, + remove: () => false, + removeAsync: origRemove + ? (userId: string | null, doc: DBInterface) => origRemove(protectString(userId), doc) as any + : () => false, } + + collection.allow(options) } /** @@ -273,7 +271,7 @@ export interface AsyncOnlyReadOnlyMongoCollection | DBInterface['_id'], options?: FindOptions - ): Promise> + ): Promise> /** * Observe changes on this collection @@ -301,5 +299,5 @@ export interface AsyncOnlyReadOnlyMongoCollection, options?: FindOptions): Promise - _ensureIndex(keys: IndexSpecifier | string, options?: NpmModuleMongodb.CreateIndexesOptions): void + createIndex(indexSpec: IndexSpecifier, options?: NpmModuleMongodb.CreateIndexesOptions): void } diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index 864748a5b7..db05a469ee 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -1,51 +1,137 @@ import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import { ProtectedString, protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' +import { Mongo } from 'meteor/mongo' import { UpdateOptions, UpsertOptions, - FindOptions, + IndexSpecifier, MongoCursor, + FindOptions, ObserveChangesCallbacks, ObserveCallbacks, } from '@sofie-automation/meteor-lib/dist/collections/lib' +import type { AnyBulkWriteOperation, Collection as RawCollection, Db as RawDb } from 'mongodb' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { NpmModuleMongodb } from 'meteor/npm-mongo' +import { profiler } from '../../api/profiler' import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' -import { makePromise } from '../../lib/lib' -import type { AnyBulkWriteOperation } from 'mongodb' -import { WrappedMongoCollectionBase, dePromiseObjectOfFunctions } from './base' import { AsyncOnlyMongoCollection } from '../collection' +export type MinimalMongoCursor }> = Pick< + MongoCursor, + 'fetchAsync' | 'observeChangesAsync' | 'observeAsync' | 'countAsync' + // | 'forEach' | 'map' | +> + +export type MinimalMeteorMongoCollection }> = Pick< + Mongo.Collection, + // | 'find' + 'insertAsync' | 'removeAsync' | 'updateAsync' | 'upsertAsync' | 'rawCollection' | 'rawDatabase' | 'createIndex' +> & { + find: (...args: Parameters['find']>) => MinimalMongoCursor +} + export class WrappedAsyncMongoCollection }> - extends WrappedMongoCollectionBase implements AsyncOnlyMongoCollection { + protected readonly _collection: MinimalMeteorMongoCollection + + public readonly name: string | null + + constructor(collection: Mongo.Collection, name: string | null) { + this._collection = collection as any + this.name = name + } + + protected get _isMock(): boolean { + // @ts-expect-error re-export private property + return this._collection._isMock + } + + public get mockCollection(): MinimalMeteorMongoCollection { + return this._collection + } + get mutableCollection(): AsyncOnlyMongoCollection { return this } + protected wrapMongoError(e: unknown): never { + const str = stringifyError(e) || 'Unknown MongoDB Error' + throw new Meteor.Error(e instanceof Meteor.Error ? e.error : 500, `Collection "${this.name}": ${str}`) + } + + rawCollection(): RawCollection { + return this._collection.rawCollection() as any + } + protected rawDatabase(): RawDb { + return this._collection.rawDatabase() as any + } + async findFetchAsync( selector: MongoQuery | DBInterface['_id'], options?: FindOptions ): Promise> { - // Make the collection fethcing in another Fiber: - return makePromise(() => { - return this.find(selector as any, options).fetch() - }) + const span = profiler.startSpan(`MongoCollection.${this.name}.findFetch`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = await this._collection.find((selector ?? {}) as any, options as any).fetchAsync() + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } async findOneAsync( selector: MongoQuery | DBInterface['_id'], options?: FindOptions ): Promise { - const arr = await this.findFetchAsync(selector, { ...options, limit: 1 }) - return arr[0] + const span = profiler.startSpan(`MongoCollection.${this.name}.findOne`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const arr = await this._collection + .find((selector ?? {}) as any, { ...(options as any), limit: 1 }) + .fetchAsync() + if (span) span.end() + return arr[0] + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } async findWithCursor( selector?: MongoQuery | DBInterface['_id'], options?: FindOptions - ): Promise> { - return this.find(selector as any, options) + ): Promise> { + const span = profiler.startSpan(`MongoCollection.${this.name}.findCursor`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = this._collection.find((selector ?? {}) as any, options as any) + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } async observeChanges( @@ -53,7 +139,23 @@ export class WrappedAsyncMongoCollection>, options?: FindOptions ): Promise { - return this.find(selector as any, options).observeChangesAsync(dePromiseObjectOfFunctions(callbacks)) + const span = profiler.startSpan(`MongoCollection.${this.name}.observeChanges`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = await this._collection + .find((selector ?? {}) as any, options as any) + .observeChangesAsync(callbacks) + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } async observe( @@ -61,37 +163,130 @@ export class WrappedAsyncMongoCollection>, options?: FindOptions ): Promise { - return this.find(selector as any, options).observeAsync(dePromiseObjectOfFunctions(callbacks)) + const span = profiler.startSpan(`MongoCollection.${this.name}.observe`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = await this._collection.find((selector ?? {}) as any, options as any).observeAsync(callbacks) + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } - async insertAsync(doc: DBInterface): Promise { - return makePromise(() => { - return this.insert(doc) - }) + public async countDocuments( + selector?: MongoQuery, + options?: FindOptions + ): Promise { + const span = profiler.startSpan(`MongoCollection.${this.name}.countDocuments`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = await this._collection.find((selector ?? {}) as any, options as any).countAsync() + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } + } + + public async insertAsync(doc: DBInterface): Promise { + const span = profiler.startSpan(`MongoCollection.${this.name}.insert`) + if (span) { + span.addLabels({ + collection: this.name, + id: unprotectString(doc._id), + }) + } + try { + const resultId = await this._collection.insertAsync(doc as unknown as Mongo.OptionalId) + if (span) span.end() + return protectString(resultId) + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } async insertManyAsync(docs: DBInterface[]): Promise> { - return Promise.all(docs.map((doc) => this.insert(doc))) + return Promise.all(docs.map(async (doc) => this.insertAsync(doc))) } - async updateAsync( + public async removeAsync(selector: MongoQuery | DBInterface['_id']): Promise { + const span = profiler.startSpan(`MongoCollection.${this.name}.remove`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = await this._collection.removeAsync(selector as any) + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } + } + public async updateAsync( selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, modifier: MongoModifier, options?: UpdateOptions ): Promise { - return makePromise(() => { - return this.update(selector, modifier, options) - }) + const span = profiler.startSpan(`MongoCollection.${this.name}.update`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const res = await this._collection.updateAsync(selector as any, modifier as any, options) + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } - - async upsertAsync( + public async upsertAsync( selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, modifier: MongoModifier, options?: UpsertOptions - ): Promise<{ numberAffected?: number; insertedId?: DBInterface['_id'] }> { - return makePromise(() => { - return this.upsert(selector, modifier, options) - }) + ): Promise<{ + numberAffected?: number + insertedId?: DBInterface['_id'] + }> { + const span = profiler.startSpan(`MongoCollection.${this.name}.upsert`) + if (span) { + span.addLabels({ + collection: this.name, + query: JSON.stringify(selector), + }) + } + try { + const result = await this._collection.upsertAsync(selector as any, modifier as any, options) + if (span) span.end() + return { + numberAffected: result.numberAffected, + insertedId: protectString(result.insertedId), + } + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } async upsertManyAsync(docs: DBInterface[]): Promise<{ numberAffected: number; insertedIds: DBInterface['_id'][] }> { @@ -113,13 +308,15 @@ export class WrappedAsyncMongoCollection | DBInterface['_id']): Promise { - return makePromise(() => { - return this.remove(selector) - }) - } - async bulkWriteAsync(ops: Array>): Promise { + const span = profiler.startSpan(`MongoCollection.${this.name}.bulkWrite`) + if (span) { + span.addLabels({ + collection: this.name, + opCount: ops.length, + }) + } + if (ops.length > 0) { const rawCollection = this.rawCollection() const bulkWriteResult = await rawCollection.bulkWrite(ops, { @@ -131,15 +328,25 @@ export class WrappedAsyncMongoCollection, options?: FindOptions): Promise { - return makePromise(() => { - try { - return this._collection.find((selector ?? {}) as any, options as any).count() - } catch (e) { - this.wrapMongoError(e) - } - }) + createIndex(keys: IndexSpecifier | string, options?: NpmModuleMongodb.CreateIndexesOptions): void { + const span = profiler.startSpan(`MongoCollection.${this.name}.createIndex`) + if (span) { + span.addLabels({ + collection: this.name, + keys: JSON.stringify(keys), + }) + } + try { + const res = this._collection.createIndex(keys as any, options) + if (span) span.end() + return res + } catch (e) { + if (span) span.end() + this.wrapMongoError(e) + } } } diff --git a/meteor/server/collections/implementations/base.ts b/meteor/server/collections/implementations/base.ts deleted file mode 100644 index a6c731ba73..0000000000 --- a/meteor/server/collections/implementations/base.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/protectedString' -import { Meteor } from 'meteor/meteor' -import { Mongo } from 'meteor/mongo' -import { - UpdateOptions, - UpsertOptions, - FindOptions, - IndexSpecifier, - MongoCursor, -} from '@sofie-automation/meteor-lib/dist/collections/lib' -import type { Collection as RawCollection, Db as RawDb } from 'mongodb' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' -import { waitForPromise } from '../../lib/lib' -import { NpmModuleMongodb } from 'meteor/npm-mongo' - -export class WrappedMongoCollectionBase }> { - protected readonly _collection: Mongo.Collection - - public readonly name: string | null - - constructor(collection: Mongo.Collection, name: string | null) { - this._collection = collection - this.name = name - } - - protected get _isMock(): boolean { - // @ts-expect-error re-export private property - return this._collection._isMock - } - - public get mockCollection(): Mongo.Collection { - return this._collection - } - - protected wrapMongoError(e: unknown): never { - const str = stringifyError(e) || 'Unknown MongoDB Error' - throw new Meteor.Error(e instanceof Meteor.Error ? e.error : 500, `Collection "${this.name}": ${str}`) - } - - rawCollection(): RawCollection { - return this._collection.rawCollection() as any - } - rawDatabase(): RawDb { - return this._collection.rawDatabase() as any - } - - protected find( - selector?: MongoQuery | DBInterface['_id'], - options?: FindOptions - ): MongoCursor { - try { - return this._collection.find((selector ?? {}) as any, options as any) as MongoCursor - } catch (e) { - this.wrapMongoError(e) - } - } - - protected insert(doc: DBInterface): DBInterface['_id'] { - try { - const resultId = this._collection.insert(doc as unknown as Mongo.OptionalId) - return protectString(resultId) - } catch (e) { - this.wrapMongoError(e) - } - } - - protected remove(selector: MongoQuery | DBInterface['_id']): number { - try { - return this._collection.remove(selector as any) - } catch (e) { - this.wrapMongoError(e) - } - } - protected update( - selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, - modifier: MongoModifier, - options?: UpdateOptions - ): number { - try { - return this._collection.update(selector as any, modifier as any, options) - } catch (e) { - this.wrapMongoError(e) - } - } - protected upsert( - selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, - modifier: MongoModifier, - options?: UpsertOptions - ): { - numberAffected?: number - insertedId?: DBInterface['_id'] - } { - try { - const result = this._collection.upsert(selector as any, modifier as any, options) - return { - numberAffected: result.numberAffected, - insertedId: protectString(result.insertedId), - } - } catch (e) { - this.wrapMongoError(e) - } - } - - _ensureIndex(keys: IndexSpecifier | string, options?: NpmModuleMongodb.CreateIndexesOptions): void { - try { - return this._collection._ensureIndex(keys as any, options) - } catch (e) { - this.wrapMongoError(e) - } - } -} - -export function dePromiseObjectOfFunctions(input: PromisifyCallbacks): T { - return Object.fromEntries( - Object.entries(input).map(([id, fn]) => { - const fn2 = (...args: any[]) => { - try { - return waitForPromise(fn(...args)) - } catch (e) { - console.trace(e) - throw e - } - } - - return [id, fn2] - }) - ) as any -} diff --git a/meteor/server/collections/implementations/mock.ts b/meteor/server/collections/implementations/mock.ts index d8f0b6abbe..af5536967a 100644 --- a/meteor/server/collections/implementations/mock.ts +++ b/meteor/server/collections/implementations/mock.ts @@ -1,136 +1,43 @@ -import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' -import { Mongo } from 'meteor/mongo' -import { - UpdateOptions, - UpsertOptions, - FindOptions, - MongoCursor, - ObserveChangesCallbacks, - ObserveCallbacks, -} from '@sofie-automation/meteor-lib/dist/collections/lib' -import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' -import type { AnyBulkWriteOperation } from 'mongodb' +import { FindOptions, MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' +import type { AnyBulkWriteOperation, Db as RawDb } from 'mongodb' import { AsyncOnlyMongoCollection } from '../collection' -import { WrappedMongoCollectionBase, dePromiseObjectOfFunctions } from './base' +import { WrappedAsyncMongoCollection } from './asyncCollection' +import { Mongo } from 'meteor/mongo' /** This is for the mock mongo collection, as internally it is sync and so we dont need or want to play around with fibers */ export class WrappedMockCollection }> - extends WrappedMongoCollectionBase + extends WrappedAsyncMongoCollection implements AsyncOnlyMongoCollection { - private readonly realSleep: (time: number) => Promise - constructor(collection: Mongo.Collection, name: string | null) { super(collection, name) if (!this._isMock) throw new Meteor.Error(500, 'WrappedMockCollection is only valid for a mock collection') - - const realSleep = (Meteor as any).sleepNoFakeTimers - if (!realSleep) throw new Error('Missing Meteor.sleepNoFakeTimers, looks like the mock is broken?') - this.realSleep = realSleep } get mutableCollection(): AsyncOnlyMongoCollection { return this } - async findFetchAsync( - selector: MongoQuery | DBInterface['_id'], - options?: FindOptions - ): Promise> { - await this.realSleep(0) - return this.find(selector as any, options).fetch() - } - - async findOneAsync( - selector: MongoQuery | DBInterface['_id'], - options?: FindOptions - ): Promise { - const arr = await this.findFetchAsync(selector, { ...options, limit: 1 }) - return arr[0] + protected override rawDatabase(): RawDb { + throw new Error('rawDatabase not supported in tests') } /** * Retrieve a cursor for use in a publication * @param selector A query describing the documents to find */ - async findWithCursor( + override async findWithCursor( _selector?: MongoQuery | DBInterface['_id'], _options?: FindOptions ): Promise> { throw new Error('findWithCursor not supported in tests') } - async observeChanges( - selector: MongoQuery | DBInterface['_id'], - callbacks: PromisifyCallbacks>, - options?: FindOptions - ): Promise { - return this.find(selector, options).observeChanges(dePromiseObjectOfFunctions(callbacks)) - } - - async observe( - selector: MongoQuery | DBInterface['_id'], - callbacks: PromisifyCallbacks>, - options?: FindOptions - ): Promise { - return this.find(selector, options).observe(dePromiseObjectOfFunctions(callbacks)) - } - - async insertAsync(doc: DBInterface): Promise { - await this.realSleep(0) - return this.insert(doc) - } - - async insertManyAsync(docs: DBInterface[]): Promise> { - await this.realSleep(0) - return Promise.all(docs.map((doc) => this.insert(doc))) - } - - async updateAsync( - selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, - modifier: MongoModifier, - options?: UpdateOptions - ): Promise { - await this.realSleep(0) - return this.update(selector, modifier, options) - } - - async upsertAsync( - selector: MongoQuery | DBInterface['_id'] | { _id: DBInterface['_id'] }, - modifier: MongoModifier, - options?: UpsertOptions - ): Promise<{ numberAffected?: number; insertedId?: DBInterface['_id'] }> { - await this.realSleep(0) - return this.upsert(selector, modifier, options) - } - - async upsertManyAsync(docs: DBInterface[]): Promise<{ numberAffected: number; insertedIds: DBInterface['_id'][] }> { - const result: { - numberAffected: number - insertedIds: DBInterface['_id'][] - } = { - numberAffected: 0, - insertedIds: [], - } - await Promise.all( - docs.map(async (doc) => { - const r = this.upsert(doc._id, { $set: doc }) - if (r.numberAffected) result.numberAffected += r.numberAffected - if (r.insertedId) result.insertedIds.push(r.insertedId) - }) - ) - return result - } - - async removeAsync(selector: MongoQuery | DBInterface['_id']): Promise { - await this.realSleep(0) - return this.remove(selector) - } - - async bulkWriteAsync(ops: Array>): Promise { + override async bulkWriteAsync(ops: Array>): Promise { if (ops.length > 0) { const rawCollection = this.rawCollection() const bulkWriteResult = await rawCollection.bulkWrite(ops, { @@ -144,13 +51,4 @@ export class WrappedMockCollection, options?: FindOptions): Promise { - await this.realSleep(0) - try { - return this._collection.find((selector ?? {}) as any, options as any).count() - } catch (e) { - this.wrapMongoError(e) - } - } } diff --git a/meteor/server/collections/implementations/readonlyWrapper.ts b/meteor/server/collections/implementations/readonlyWrapper.ts index d2e0cd8949..a4147afbd5 100644 --- a/meteor/server/collections/implementations/readonlyWrapper.ts +++ b/meteor/server/collections/implementations/readonlyWrapper.ts @@ -1,13 +1,13 @@ import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' -import { MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' import type { Collection } from 'mongodb' -import { AsyncOnlyMongoCollection, AsyncOnlyReadOnlyMongoCollection } from '../collection' +import type { AsyncOnlyMongoCollection, AsyncOnlyReadOnlyMongoCollection } from '../collection' +import type { MinimalMongoCursor } from './asyncCollection' export class WrappedReadOnlyMongoCollection }> implements AsyncOnlyReadOnlyMongoCollection { - #mutableCollection: AsyncOnlyMongoCollection + readonly #mutableCollection: AsyncOnlyMongoCollection constructor(collection: AsyncOnlyMongoCollection) { this.#mutableCollection = collection @@ -49,7 +49,7 @@ export class WrappedReadOnlyMongoCollection['findWithCursor']> - ): Promise> { + ): Promise> { return this.#mutableCollection.findWithCursor(...args) } @@ -71,7 +71,7 @@ export class WrappedReadOnlyMongoCollection['_ensureIndex']>): void { - return this.#mutableCollection._ensureIndex(...args) + createIndex(...args: Parameters['createIndex']>): void { + return this.#mutableCollection.createIndex(...args) } } diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index 112093c97b..25d56a1745 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -28,7 +28,6 @@ import { DBUser } from '@sofie-automation/meteor-lib/dist/collections/Users' import { WorkerStatus } from '@sofie-automation/meteor-lib/dist/collections/Workers' import { registerIndex } from './indices' import { getCurrentTime } from '../lib/lib' -import { MeteorStartupAsync } from '../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { createAsyncOnlyMongoCollection, @@ -299,7 +298,7 @@ const removeOldCommands = () => { logger.error(`Failed to cleanup old PeripheralDeviceCommands: ${stringifyError(e)}`) }) } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { Meteor.setInterval(() => removeOldCommands(), 5 * 60 * 1000) await Promise.allSettled([ diff --git a/meteor/server/coreSystem/index.ts b/meteor/server/coreSystem/index.ts index fa1bb84d46..95f4b74080 100644 --- a/meteor/server/coreSystem/index.ts +++ b/meteor/server/coreSystem/index.ts @@ -1,6 +1,5 @@ import { SYSTEM_ID, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { parseVersion } from '../systemStatus/semverUtils' -import { MeteorStartupAsync } from '../lib/lib' import { getCurrentTime } from '../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { Meteor } from 'meteor/meteor' @@ -9,7 +8,7 @@ import { CURRENT_SYSTEM_VERSION } from '../migration/currentSystemVersion' import { Blueprints, CoreSystem } from '../collections' import { getEnvLogLevel, logger, LogLevel, setLogLevel } from '../logging' const PackageInfo = require('../../package.json') -import Agent from 'meteor/julusian:meteor-elastic-apm' +import { startAgent } from '../api/profiler/apm' import { profiler } from '../api/profiler' import { TMP_TSR_VERSION } from '@sofie-automation/blueprints-integration' import { getAbsolutePath } from '../lib' @@ -146,7 +145,6 @@ async function startupMessage() { logger.info(`Core starting up`) logger.info(`Core system version: "${CURRENT_SYSTEM_VERSION}"`) - // @ts-expect-error Its not always defined if (global.gc) { logger.info(`Manual garbage-collection is enabled`) } else { @@ -173,22 +171,20 @@ async function startInstrumenting() { if (APM_HOST && system && system.apm) { logger.info(`APM agent starting up`) - Agent.start({ + startAgent({ serviceName: KIBANA_INDEX || 'tv-automation-server-core', hostname: APP_HOST, serverUrl: APM_HOST, secretToken: APM_SECRET, active: system.apm.enabled, transactionSampleRate: system.apm.transactionSampleRate, - disableMeteorInstrumentations: ['methods', 'http-out', 'session', 'async', 'metrics'], }) profiler.setActive(system.apm.enabled || false) } else { logger.info(`APM agent inactive`) - Agent.start({ + startAgent({ serviceName: 'tv-automation-server-core', active: false, - disableMeteorInstrumentations: ['methods', 'http-out', 'session', 'async', 'metrics'], }) } } @@ -203,7 +199,7 @@ async function updateLoggerLevel(startup: boolean) { } } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { if (Meteor.isServer) { await startupMessage() await updateLoggerLevel(true) diff --git a/meteor/server/lib.ts b/meteor/server/lib.ts index 8b337fc8c7..a58d10c534 100644 --- a/meteor/server/lib.ts +++ b/meteor/server/lib.ts @@ -4,12 +4,11 @@ import fs from 'fs' import path from 'path' import { logger } from './logging' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { Meteor } from 'meteor/meteor' /** Returns absolute path to programs/server directory of your compiled application, without trailing slash. */ export function getAbsolutePath(): string { - // @ts-expect-error Meteor.absolutePath is injected by the package ostrio:meteor-root - return Meteor.absolutePath + const rootPath = path.resolve('.') + return rootPath.split(`${path.sep}.meteor`)[0] } export function extractFunctionSignature(f: Function): string[] | undefined { if (f) { diff --git a/meteor/server/lib/__tests__/lib.test.ts b/meteor/server/lib/__tests__/lib.test.ts index 0975875449..61b68f5957 100644 --- a/meteor/server/lib/__tests__/lib.test.ts +++ b/meteor/server/lib/__tests__/lib.test.ts @@ -8,14 +8,11 @@ import { serializeTimelineBlob, } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { protectString } from '../tempLib' -import { testInFiber } from '../../../__mocks__/helpers/jest' import { Timeline } from '../../collections' import { SaveIntoDbHooks, saveIntoDb, sumChanges, anythingChanged } from '../database' -import { makePromise } from '../lib' -import { Meteor } from 'meteor/meteor' describe('server/lib', () => { - testInFiber('saveIntoDb', async () => { + test('saveIntoDb', async () => { const mystudioObjs: Array = [ { id: 'abc', @@ -164,7 +161,7 @@ describe('server/lib', () => { removed: 3, }) }) - testInFiber('anythingChanged', () => { + test('anythingChanged', () => { expect( anythingChanged({ added: 0, @@ -194,38 +191,4 @@ describe('server/lib', () => { }) ).toBeTruthy() }) - - testInFiber('makePromise', async () => { - let a = 0 - // Check that they are executed in order: - expect( - await Promise.all([ - makePromise(() => { - return a++ - }), - makePromise(() => { - return a++ - }), - ]) - ).toStrictEqual([0, 1]) - - // Handle an instant throw: - await expect( - makePromise(() => { - throw new Error('asdf') - }) - ).rejects.toMatchToString(/asdf/) - - // Handle a delayed throw: - const delayedThrow = Meteor.wrapAsync((callback: (err: any, result: any) => void) => { - setTimeout(() => { - callback(new Error('asdf'), null) - }, 10) - }) - await expect( - makePromise(() => { - delayedThrow() - }) - ).rejects.toMatchToString(/asdf/) - }) }) diff --git a/meteor/server/lib/lib.ts b/meteor/server/lib/lib.ts index dee6d8aa8a..5e4a955ba2 100644 --- a/meteor/server/lib/lib.ts +++ b/meteor/server/lib/lib.ts @@ -23,39 +23,6 @@ export function fixValidPath(path: string): string { return path.replace(/([^a-z0-9_.@()-])/gi, '_') } -/** - * Make Meteor.wrapAsync a bit more type safe - * The original version makes the callback be after the last non-undefined parameter, rather than after or replacing the last parameter. - * Which makes it incredibly hard to find without iterating over all the parameters. This does that for you, so you dont need to check as many places - */ -export function MeteorWrapAsync(func: Function, context?: Object): any { - // A variant of Meteor.wrapAsync to fix the bug - // https://github.com/meteor/meteor/issues/11120 - - return Meteor.wrapAsync((...args: any[]) => { - // Find the callback-function: - for (let i = args.length - 1; i >= 0; i--) { - if (typeof args[i] === 'function') { - if (i < args.length - 1) { - // The callback is not the last argument, make it so then: - const callback = args[i] - const fixedArgs = args - fixedArgs[i] = undefined - fixedArgs.push(callback) - - func.apply(context, fixedArgs) - return - } else { - // The callback is the last argument, that's okay - func.apply(context, args) - return - } - } - } - throw new Meteor.Error(500, `Error in MeteorWrapAsync: No callback found!`) - }) -} - const lazyIgnoreCache: { [name: string]: number } = {} export function lazyIgnore(name: string, f1: () => void, t: number): void { // Don't execute the function f1 until the time t has passed. @@ -75,60 +42,6 @@ export function lazyIgnore(name: string, f1: () => void, t: number): void { }, t) } -/** - * Make Meteor.startup support async functions - */ -export function MeteorStartupAsync(fcn: () => Promise): void { - Meteor.startup(() => waitForPromise(fcn())) -} - -/** - * Convert a promise to a "synchronous" Fiber function - * Makes the Fiber wait for the promise to resolve, then return the value of the promise. - * If the fiber rejects, the function in the Fiber will "throw" - */ -export const waitForPromise: (p: Promise | T) => Awaited = Meteor.wrapAsync(function waitForPromise( - p: Promise | T, - cb: (err: any | null, result?: any) => Awaited -) { - if (Meteor.isClient) throw new Meteor.Error(500, `waitForPromise can't be used client-side`) - if (cb === undefined && typeof p === 'function') { - cb = p as any - p = undefined as any - } - - Promise.resolve(p) - .then((result) => { - cb(null, result) - }) - .catch((e) => { - cb(e) - }) -}) as (p: Promise | T) => Awaited // `wrapAsync` has opaque `Function` type -/** - * Convert a Fiber function into a promise - * Makes the Fiber function to run in its own fiber and return a promise - */ -export async function makePromise(fcn: () => T): Promise { - const p = new Promise((resolve, reject) => { - Meteor.defer(() => { - try { - resolve(fcn()) - } catch (e) { - reject(e) - } - }) - }) - - return ( - await Promise.all([ - p, - // Pause the current Fiber briefly, in order to allow for the deferred Fiber to start executing: - sleep(0), - ]) - )[0] -} - export function deferAsync(fcn: () => Promise): void { Meteor.defer(() => { fcn().catch((e) => logger.error(stringifyError(e))) diff --git a/meteor/server/methods.ts b/meteor/server/methods.ts index 865928cb85..49bee70b1a 100644 --- a/meteor/server/methods.ts +++ b/meteor/server/methods.ts @@ -3,7 +3,6 @@ import * as _ from 'underscore' import { logger } from './logging' import { extractFunctionSignature } from './lib' import { MethodContext, MethodContextAPI } from './api/methodContext' -import { waitForPromise } from './lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { Settings } from './Settings' import { isPromise } from '@sofie-automation/shared-lib/dist/lib/lib' @@ -155,7 +154,7 @@ export function MeteorDebugMethods(methods: { [key: string]: MeteorDebugMethod } for (const [key, fn] of Object.entries(methods)) { if (key && !!fn) { fiberMethods[key] = function (this: Meteor.MethodThisType, ...args: any[]) { - return waitForPromise(fn.call(this, ...args)) + return fn.call(this, ...args) } } } diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 5e9168964d..788fdaf33a 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -1,6 +1,5 @@ import * as _ from 'underscore' import { setupEmptyEnvironment } from '../../../__mocks__/helpers/database' -import { testInFiber } from '../../../__mocks__/helpers/jest' import { ICoreSystem, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigration } from '../databaseMigration' import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion' @@ -63,7 +62,7 @@ describe('Migrations', () => { }) ) } - testInFiber('System migrations, initial setup', async () => { + test('System migrations, initial setup', async () => { expect((await getSystem()).version).toEqual(GENESIS_SYSTEM_VERSION) const migrationStatus0: GetMigrationStatusResult = await MeteorCall.migration.getMigrationStatus() @@ -101,7 +100,7 @@ describe('Migrations', () => { expect((await getSystem()).version).toEqual(CURRENT_SYSTEM_VERSION) }) - testInFiber('Ensure migrations run in correct order', async () => { + test('Ensure migrations run in correct order', async () => { await MeteorCall.migration.resetDatabaseVersions() expect((await getSystem()).version).toEqual(GENESIS_SYSTEM_VERSION) diff --git a/meteor/server/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index b01bdfe5fe..42b0d76b1e 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -43,11 +43,7 @@ import { GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collec import { clone, getHash, omit, protectString, unprotectString } from '../lib/tempLib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { evalBlueprint } from '../api/blueprints/cache' -import { - MigrationContextShowStyle, - MigrationContextStudio, - MigrationContextSystem, -} from '../api/blueprints/migrationContext' +import { MigrationContextSystem } from '../api/blueprints/migrationContext' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' import { SnapshotId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprints, CoreSystem, ShowStyleBases, Studios } from '../collections' @@ -387,7 +383,7 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { if (chunk.sourceType !== MigrationStepType.SHOWSTYLE) @@ -820,5 +817,6 @@ async function getMigrationShowStyleContext(chunk: MigrationChunk): Promise { } describe('validateConfigForShowStyleBase', () => { - testInFiber('Missing id', async () => { + test('Missing id', async () => { await expect(validateConfigForShowStyleBase(protectString('fakeId'))).rejects.toThrowMeteor( 404, `ShowStyleBase "fakeId" not found!` ) }) - testInFiber('Missing config preset', async () => { + test('Missing config preset', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = await setupMockShowStyleBase(blueprint._id) @@ -83,7 +82,7 @@ describe('ShowStyleBase upgrades', () => { ) }) - testInFiber('Missing blueprint', async () => { + test('Missing blueprint', async () => { const showStyleBase = await setupMockShowStyleBase(protectString('fakeId'), { blueprintConfigPresetId: 'fake-preset', }) @@ -94,7 +93,7 @@ describe('ShowStyleBase upgrades', () => { ) }) - testInFiber('Unsupported blueprint', async () => { + test('Unsupported blueprint', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = await setupMockShowStyleBase(blueprint._id, { blueprintConfigPresetId: 'fake-preset', @@ -106,7 +105,7 @@ describe('ShowStyleBase upgrades', () => { ) }) - testInFiber('Success: no messages', async () => { + test('Success: no messages', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = await setupMockShowStyleBase(blueprint._id, { blueprintConfigPresetId: 'fake-preset', @@ -123,7 +122,7 @@ describe('ShowStyleBase upgrades', () => { expect(result.messages).toHaveLength(0) }) - testInFiber('Success: some messages', async () => { + test('Success: some messages', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = await setupMockShowStyleBase(blueprint._id, { blueprintConfigPresetId: 'fake-preset', @@ -167,14 +166,14 @@ describe('ShowStyleBase upgrades', () => { }) describe('runUpgradeForShowStyleBase', () => { - testInFiber('Missing id', async () => { + test('Missing id', async () => { await expect(runUpgradeForShowStyleBase(protectString('fakeId'))).rejects.toThrowMeteor( 404, `ShowStyleBase "fakeId" not found!` ) }) - testInFiber('Missing config preset', async () => { + test('Missing config preset', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = await setupMockShowStyleBase(blueprint._id) @@ -184,7 +183,7 @@ describe('ShowStyleBase upgrades', () => { ) }) - testInFiber('Missing blueprint', async () => { + test('Missing blueprint', async () => { const showStyleBase = await setupMockShowStyleBase(protectString('fakeId'), { blueprintConfigPresetId: 'fake-preset', }) @@ -195,7 +194,7 @@ describe('ShowStyleBase upgrades', () => { ) }) - testInFiber('Unsupported blueprint', async () => { + test('Unsupported blueprint', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = await setupMockShowStyleBase(blueprint._id, { blueprintConfigPresetId: 'fake-preset', @@ -207,7 +206,7 @@ describe('ShowStyleBase upgrades', () => { ) }) - testInFiber('Success', async () => { + test('Success', async () => { const blueprint = await setupMockShowStyleBlueprint(protectString('')) const showStyleBase = clone( await setupMockShowStyleBase(blueprint._id, { diff --git a/meteor/server/performanceMonitor.ts b/meteor/server/performanceMonitor.ts index ea875c300f..50ad29cafa 100644 --- a/meteor/server/performanceMonitor.ts +++ b/meteor/server/performanceMonitor.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor' import * as _ from 'underscore' -import { MeteorStartupAsync } from './lib/lib' import { getCoreSystemAsync } from './coreSystem/collection' import { logger } from './logging' import { getRunningMethods, resetRunningMethods } from './methods' @@ -198,7 +197,7 @@ const monitorBlockedThread = () => { }, PERMORMANCE_CHECK_INTERVAL) } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { const coreSystem = await getCoreSystemAsync() if (coreSystem?.enableMonitorBlockedThread) { Meteor.setTimeout(() => { diff --git a/meteor/server/publications/lib/ReactiveCacheCollection.ts b/meteor/server/publications/lib/ReactiveCacheCollection.ts index e78a3e1827..ab90549212 100644 --- a/meteor/server/publications/lib/ReactiveCacheCollection.ts +++ b/meteor/server/publications/lib/ReactiveCacheCollection.ts @@ -7,6 +7,10 @@ import { MongoModifier, MongoQuery } from '@sofie-automation/corelib/dist/mongo' type Reaction = () => void export class ReactiveCacheCollection }> { + /** + * The collection still works in sync mode when operating on `null` in Meteor 3.0 + * It may break in a later update, but this is fine for now. + */ readonly #collection: Mongo.Collection constructor(public collectionName: string, private reaction?: Reaction) { diff --git a/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts b/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts index d07d69cf51..ffeb44577b 100644 --- a/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts +++ b/meteor/server/publications/lib/__tests__/rundownsObserver.test.ts @@ -2,7 +2,7 @@ import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/coreli import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { Rundowns } from '../../../collections' -import { runAllTimers, runTimersUntilNow, testInFiber, waitUntil } from '../../../../__mocks__/helpers/jest' +import { runAllTimers, runTimersUntilNow, waitUntil } from '../../../../__mocks__/helpers/jest' import { MongoMock } from '../../../../__mocks__/mongo' import { RundownsObserver } from '../rundownsObserver' @@ -15,7 +15,7 @@ describe('RundownsObserver', () => { jest.useFakeTimers() }) - testInFiber('create and destroy observer', async () => { + test('create and destroy observer', async () => { const studioId = protectString('studio0') const playlistId = protectString('playlist0') @@ -68,7 +68,7 @@ describe('RundownsObserver', () => { } }) - testInFiber('add a document', async () => { + test('add a document', async () => { const studioId = protectString('studio0') const playlistId = protectString('playlist0') @@ -122,7 +122,7 @@ describe('RundownsObserver', () => { } }) - testInFiber('change a document', async () => { + test('change a document', async () => { const studioId = protectString('studio0') const playlistId = protectString('playlist0') @@ -176,7 +176,7 @@ describe('RundownsObserver', () => { } }) - testInFiber('sequence of updates', async () => { + test('sequence of updates', async () => { const studioId = protectString('studio0') const playlistId = protectString('playlist0') diff --git a/meteor/server/publications/lib/lib.ts b/meteor/server/publications/lib/lib.ts index bbb2d0dc32..b0fda6dcc6 100644 --- a/meteor/server/publications/lib/lib.ts +++ b/meteor/server/publications/lib/lib.ts @@ -5,7 +5,6 @@ import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { ResolvedCredentials, resolveCredentials } from '../../security/lib/credentials' import { Settings } from '../../Settings' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' import { OrganizationId, PeripheralDeviceId, @@ -16,6 +15,7 @@ import { protectStringObject } from '../../lib/tempLib' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { PeripheralDevices, ShowStyleBases } from '../../collections' import { MetricsGauge } from '@sofie-automation/corelib/dist/prometheus' +import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection' export const MeteorPublicationSignatures: { [key: string]: string[] } = {} export const MeteorPublications: { [key: string]: Function } = {} @@ -77,7 +77,7 @@ export function meteorPublish( callback: ( this: SubscriptionContext, ...args: Parameters - ) => Promise> | null> + ) => Promise> | null> ): void { meteorPublishUnsafe(name, callback) } diff --git a/meteor/server/publications/lib/observerChain.ts b/meteor/server/publications/lib/observerChain.ts index abbbd49467..76d73ed7b2 100644 --- a/meteor/server/publications/lib/observerChain.ts +++ b/meteor/server/publications/lib/observerChain.ts @@ -1,10 +1,10 @@ import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' -import { MongoCursor } from '@sofie-automation/meteor-lib/dist/collections/lib' import { Simplify } from 'type-fest' import { assertNever } from '../../lib/tempLib' import { logger } from '../../logging' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { MinimalMongoCursor } from '../../collections/implementations/asyncCollection' /** * https://stackoverflow.com/a/66011942 @@ -19,7 +19,7 @@ type Not = Yes extends Not ? never : Yes type Link = { next: }>( key: Not, - cursorChain: (state: T) => Promise | null> + cursorChain: (state: T) => Promise | null> ) => Link]: K }>> end: (complete: (state: T | null) => void) => Meteor.LiveQueryHandle @@ -28,7 +28,7 @@ type Link = { export function observerChain(): Pick, 'next'> { function createNextLink(baseCollectorObject: Record, liveQueryHandle: Meteor.LiveQueryHandle) { let mode: 'next' | 'end' | undefined - let chainedCursor: (state: Record) => Promise | null> + let chainedCursor: (state: Record) => Promise | null> let completeFunction: (state: Record | null) => void let chainedKey: string | undefined = undefined let previousObserver: Meteor.LiveQueryHandle | null = null diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index 3f7c1027da..c4196d32f2 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -33,7 +33,6 @@ import { MediaStreamType, } from '@sofie-automation/shared-lib/dist/core/model/MediaObjects' import { defaultStudio } from '../../../../__mocks__/defaultCollectionObjects' -import { testInFiber } from '../../../../__mocks__/helpers/jest' import { MediaObjects } from '../../../collections' import { PieceDependencies } from '../common' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' @@ -243,7 +242,7 @@ describe('lib/mediaObjects', () => { expect(mediaId3).toEqual(undefined) }) - testInFiber('checkPieceContentStatus', async () => { + test('checkPieceContentStatus', async () => { const mockStudioSettings: IStudioSettings = { supportedMediaFormats: '1920x1080i5000, 1280x720, i5000, i5000tff', mediaPreviewsUrl: '', @@ -266,7 +265,7 @@ describe('lib/mediaObjects', () => { packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, } - mockMediaObjectsCollection.insert( + await mockMediaObjectsCollection.insertAsync( literal({ _id: protectString(''), _attachments: {}, @@ -353,7 +352,7 @@ describe('lib/mediaObjects', () => { type: SourceLayerType.LIVE_SPEAK, }) - mockMediaObjectsCollection.insert( + await mockMediaObjectsCollection.insertAsync( literal({ _id: protectString(''), _attachments: {}, diff --git a/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts b/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts index a7f9023aae..f232e38371 100644 --- a/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts +++ b/meteor/server/publications/segmentPartNotesUI/__tests__/publication.test.ts @@ -1,7 +1,6 @@ import { RundownId, RundownPlaylistId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/protectedString' import { UISegmentPartNote } from '@sofie-automation/meteor-lib/dist/api/rundownNotifications' -import { testInFiber } from '../../../../__mocks__/helpers/jest' import { CustomPublishCollection } from '../../../lib/customPublication' import { ReactiveCacheCollection } from '../../lib/ReactiveCacheCollection' import { manipulateUISegmentPartNotesPublicationData, UISegmentPartNotesState } from '../publication' @@ -120,7 +119,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { }, ] - testInFiber('basic call', async () => { + test('basic call', async () => { const state: Partial = {} const collection = createSpyPublishCollection() @@ -142,7 +141,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { expect(collection.remove).toHaveBeenLastCalledWith(null) }) - testInFiber('first cache', async () => { + test('first cache', async () => { const playlistId = protectString('playlist0') const state: Partial = {} const collection = createSpyPublishCollection() @@ -167,7 +166,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { expect(collection.replace).toHaveBeenCalledTimes(2) }) - testInFiber('replace cache', async () => { + test('replace cache', async () => { const playlistId = protectString('playlist0') const state: Partial = {} const collection = createSpyPublishCollection() @@ -212,7 +211,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { expect(generateNotesForSegment.generateNotesForSegment).toHaveBeenCalledTimes(3) }) - testInFiber('update no reported changes', async () => { + test('update no reported changes', async () => { const playlistId = protectString('playlist0') const state: Partial = {} const collection = createSpyPublishCollection() @@ -249,7 +248,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { expect(generateNotesForSegment.generateNotesForSegment).toHaveBeenCalledTimes(2) }) - testInFiber('rundown changed', async () => { + test('rundown changed', async () => { const playlistId = protectString('playlist0') const state: Partial = {} const collection = createSpyPublishCollection() @@ -287,7 +286,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { expect(generateNotesForSegment.generateNotesForSegment).toHaveBeenCalledTimes(4) }) - testInFiber('segment changed', async () => { + test('segment changed', async () => { const playlistId = protectString('playlist0') const state: Partial = {} const collection = createSpyPublishCollection() @@ -366,7 +365,7 @@ describe('manipulateUISegmentPartNotesPublicationData', () => { part: 'part' as any, }) - testInFiber('segment changed', async () => { + test('segment changed', async () => { const playlistId = protectString('playlist0') const state: Partial = {} const collection = createSpyPublishCollection() diff --git a/meteor/server/security/__tests__/security.test.ts b/meteor/server/security/__tests__/security.test.ts index ebecf11b98..595791d812 100644 --- a/meteor/server/security/__tests__/security.test.ts +++ b/meteor/server/security/__tests__/security.test.ts @@ -6,7 +6,6 @@ import { User } from '@sofie-automation/meteor-lib/dist/collections/Users' import { protectString } from '../../lib/tempLib' import { Settings } from '../../Settings' import { DefaultEnvironment, setupDefaultStudioEnvironment } from '../../../__mocks__/helpers/database' -import { beforeAllInFiber, testInFiber } from '../../../__mocks__/helpers/jest' import { BucketsAPI } from '../../api/buckets' import { storeSystemSnapshot } from '../../api/snapshot' import { BucketSecurity } from '../buckets' @@ -128,7 +127,7 @@ describe('Security', () => { return expect(fcn()).resolves.not.toBeUndefined() } let env: DefaultEnvironment - beforeAllInFiber(async () => { + beforeAll(async () => { env = await setupDefaultStudioEnvironment(org0._id) await Organizations.insertAsync(org0) @@ -142,7 +141,8 @@ describe('Security', () => { await Users.insertAsync({ ...getUser(idSuperAdminInOtherOrg, org2._id), superAdmin: true }) }) - testInFiber('Buckets', async () => { + // eslint-disable-next-line jest/expect-expect + test('Buckets', async () => { const access = await StudioContentWriteAccess.bucket(creator, env.studio._id) const bucket = await BucketsAPI.createNewBucket(access, 'myBucket') @@ -187,12 +187,14 @@ describe('Security', () => { }) }) - testInFiber('NoSecurity', async () => { + // eslint-disable-next-line jest/expect-expect + test('NoSecurity', async () => { await changeEnableUserAccounts(async () => { await expectAllowed(async () => NoSecurityReadAccess.any()) }) }) - testInFiber('Organization', async () => { + // eslint-disable-next-line jest/expect-expect + test('Organization', async () => { const token = generateToken() const snapshotId = await storeSystemSnapshot(superAdmin, hashSingleUseToken(token), env.studio._id, 'for test') diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts index 2406c6430e..ed27ed1846 100644 --- a/meteor/server/security/lib/security.ts +++ b/meteor/server/security/lib/security.ts @@ -53,7 +53,7 @@ export async function allowAccessToCoreSystem(cred: ResolvedCredentials): Promis */ export async function allowAccessToCurrentUser( cred0: Credentials | ResolvedCredentials, - userId: UserId + userId: UserId | null ): Promise> { if (!Settings.enableUserAccounts) return allAccess(null, 'No security') if (!userId) return noAccess('userId missing') diff --git a/meteor/server/security/lib/securityVerify.ts b/meteor/server/security/lib/securityVerify.ts index 7124987417..edde48cb35 100644 --- a/meteor/server/security/lib/securityVerify.ts +++ b/meteor/server/security/lib/securityVerify.ts @@ -62,7 +62,7 @@ export async function verifyAllMethods(): Promise { // Verify all Meteor methods let ok = true for (const methodName of AllMeteorMethods) { - ok = ok && verifyMethod(methodName) + ok = ok && (await verifyMethod(methodName)) if (!ok) return false // Bail on first error @@ -70,7 +70,7 @@ export async function verifyAllMethods(): Promise { } return ok } -function verifyMethod(methodName: string) { +async function verifyMethod(methodName: string) { let ok = true suppressExtraErrorLogging(true) try { @@ -78,7 +78,7 @@ function verifyMethod(methodName: string) { testWriteAccess() // Pass some fake args, to ensure that any trying to do a `arg.val` don't throw const fakeArgs = [{}, {}, {}, {}, {}] - Meteor.call(methodName, ...fakeArgs) + await Meteor.callAsync(methodName, ...fakeArgs) } catch (e) { const errStr = stringifyError(e) if (errStr.match(/triggerWriteAccess/i)) { diff --git a/meteor/server/security/system.ts b/meteor/server/security/system.ts index 5163c362e6..d7d13b760e 100644 --- a/meteor/server/security/system.ts +++ b/meteor/server/security/system.ts @@ -48,7 +48,7 @@ export namespace SystemWriteAccess { return true } /** Check if access is allowed to modify a User, and that user is the current User */ - export async function currentUser(userId: UserId, cred: Credentials): Promise { + export async function currentUser(userId: UserId | null, cred: Credentials): Promise { const access = await allowAccessToCurrentUser(cred, userId) if (!access.update) return logNotAllowed('Current user', access.reason) diff --git a/meteor/server/systemStatus/__tests__/api.test.ts b/meteor/server/systemStatus/__tests__/api.test.ts index 66ade93a28..690be9cf62 100644 --- a/meteor/server/systemStatus/__tests__/api.test.ts +++ b/meteor/server/systemStatus/__tests__/api.test.ts @@ -38,7 +38,7 @@ describe('systemStatus API', () => { test('REST /health with state BAD', async () => { env = await setupDefaultStudioEnvironment() - MeteorMock.mockRunMeteorStartup() + await MeteorMock.mockRunMeteorStartup() await MeteorMock.sleepNoFakeTimers(200) // The system is uninitialized, the status will be BAD @@ -73,7 +73,7 @@ describe('systemStatus API', () => { test('REST /health with state GOOD', async () => { env = await setupDefaultStudioEnvironment() - MeteorMock.mockRunMeteorStartup() + await MeteorMock.mockRunMeteorStartup() await MeteorMock.sleepNoFakeTimers(200) // simulate initialized system diff --git a/meteor/server/systemStatus/__tests__/systemStatus.test.ts b/meteor/server/systemStatus/__tests__/systemStatus.test.ts index 6b62e100b7..bd3b77146c 100644 --- a/meteor/server/systemStatus/__tests__/systemStatus.test.ts +++ b/meteor/server/systemStatus/__tests__/systemStatus.test.ts @@ -1,5 +1,4 @@ import '../../../__mocks__/_extendJest' -import { testInFiber } from '../../../__mocks__/helpers/jest' import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../__mocks__/helpers/database' import { generateTranslation, literal, protectString, unprotectString } from '../../lib/tempLib' import { MeteorMock } from '../../../__mocks__/meteor' @@ -34,7 +33,7 @@ describe('systemStatus', () => { }) let env: DefaultEnvironment - testInFiber('getSystemStatus: before startup', async () => { + test('getSystemStatus: before startup', async () => { // Before starting the system up, the system status will be unknown const expectedStatus0 = StatusCode.UNKNOWN const result0: StatusResponse = await MeteorCall.systemStatus.getSystemStatus() @@ -44,9 +43,9 @@ describe('systemStatus', () => { }) expect(result0.checks).toHaveLength(0) }) - testInFiber('getSystemStatus: after startup', async () => { + test('getSystemStatus: after startup', async () => { env = await setupDefaultStudioEnvironment() - MeteorMock.mockRunMeteorStartup() + await MeteorMock.mockRunMeteorStartup() await MeteorMock.sleepNoFakeTimers(200) const result0: StatusResponse = await MeteorCall.systemStatus.getSystemStatus() @@ -64,7 +63,7 @@ describe('systemStatus', () => { status: status2ExternalStatus(StatusCode.BAD), }) }) - testInFiber('getSystemStatus: after all migrations completed', async () => { + test('getSystemStatus: after all migrations completed', async () => { // simulate migrations completed setSystemStatus('databaseVersion', { statusCode: StatusCode.GOOD, @@ -84,7 +83,7 @@ describe('systemStatus', () => { status: status2ExternalStatus(StatusCode.GOOD), }) }) - testInFiber('getSystemStatus: a component has a fault', async () => { + test('getSystemStatus: a component has a fault', async () => { // simulate device failure await PeripheralDevices.updateAsync(env.ingestDevice._id, { $set: { @@ -110,7 +109,7 @@ describe('systemStatus', () => { status: status2ExternalStatus(expectedStatus0), }) }) - testInFiber('getSystemStatus: a component has a library version mismatch', async () => { + test('getSystemStatus: a component has a library version mismatch', async () => { // simulate device failure await PeripheralDevices.updateAsync(env.ingestDevice._id, { $set: { @@ -238,7 +237,7 @@ describe('systemStatus', () => { }) }) - testInFiber('getSystemStatus: blueprint upgrades need running', async () => { + test('getSystemStatus: blueprint upgrades need running', async () => { { // Ensure we start with a status of GOOD const result: StatusResponse = await MeteorCall.systemStatus.getSystemStatus() diff --git a/meteor/server/typings/meteor-kschingiz-elastic-apm.d.ts b/meteor/server/typings/meteor-kschingiz-elastic-apm.d.ts deleted file mode 100644 index f5e6619843..0000000000 --- a/meteor/server/typings/meteor-kschingiz-elastic-apm.d.ts +++ /dev/null @@ -1,355 +0,0 @@ -/** - * This is mostly copied from https://github.com/elastic/apm-agent-nodejs/blob/master/index.d.ts - * As they do not export any of the inner types, adding `disableMeteorInstrumentations` to `AgentConfigOptions` is not trivial without just copying it all - */ - -declare module 'meteor/julusian:meteor-elastic-apm' { - /// - - import { IncomingMessage, ServerResponse } from 'http' - - export = agent - - declare const agent: Agent - - declare class Agent implements Taggable, StartSpanFn { - // Configuration - start(options?: AgentConfigOptions): Agent - isStarted(): boolean - setFramework(options: { name?: string; version?: string; overwrite?: boolean }): void - addPatch(modules: string | Array, handler: string | PatchHandler): void - removePatch(modules: string | Array, handler: string | PatchHandler): void - clearPatches(modules: string | Array): void - - // Data collection hooks - middleware: { connect(): Connect.ErrorHandleFunction } - lambda(handler: AwsLambda.Handler): AwsLambda.Handler - lambda(type: string, handler: AwsLambda.Handler): AwsLambda.Handler - handleUncaughtExceptions(fn?: (err: Error) => void): void - - // Errors - captureError(err: Error | string | ParameterizedMessageObject, callback?: CaptureErrorCallback): void - captureError( - err: Error | string | ParameterizedMessageObject, - options?: CaptureErrorOptions, - callback?: CaptureErrorCallback - ): void - - // Distributed Tracing - currentTraceparent: string | null - currentTraceIds: { - 'trace.id'?: string - 'transaction.id'?: string - 'span.id'?: string - } - - // Transactions - startTransaction(name?: string | null, options?: TransactionOptions): Transaction | null - startTransaction(name: string | null, type: string | null, options?: TransactionOptions): Transaction | null - startTransaction( - name: string | null, - type: string | null, - subtype: string | null, - options?: TransactionOptions - ): Transaction | null - startTransaction( - name: string | null, - type: string | null, - subtype: string | null, - action: string | null, - options?: TransactionOptions - ): Transaction | null - setTransactionName(name: string): void - endTransaction(result?: string | number, endTime?: number): void - currentTransaction: Transaction | null - - // Spans - startSpan(name?: string | null, options?: SpanOptions): Span | null - startSpan(name: string | null, type: string | null, options?: SpanOptions): Span | null - startSpan(name: string | null, type: string | null, subtype: string | null, options?: SpanOptions): Span | null - startSpan( - name: string | null, - type: string | null, - subtype: string | null, - action: string | null, - options?: SpanOptions - ): Span | null - currentSpan: Span | null - - // Context - setLabel(name: string, value: LabelValue): boolean - addLabels(labels: Labels): boolean - setUserContext(user: UserObject): void - setCustomContext(custom: object): void - - // Transport - addFilter(fn: FilterFn): void - addErrorFilter(fn: FilterFn): void - addSpanFilter(fn: FilterFn): void - addTransactionFilter(fn: FilterFn): void - flush(callback?: Function): void - destroy(): void - - // Utils - logger: Logger - - // Custom metrics - registerMetric(name: string, callback: Function): void - registerMetric(name: string, labels: Labels, callback: Function): void - } - - declare class GenericSpan implements Taggable { - // The following properties and methods are currently not documented as their API isn't considered official: - // timestamp, ended, id, traceId, parentId, sampled, duration() - - type: string | null - subtype: string | null - action: string | null - traceparent: string - - setType(type?: string | null, subtype?: string | null, action?: string | null): void - setLabel(name: string, value: LabelValue): boolean - addLabels(labels: Labels): boolean - } - - declare class Transaction extends GenericSpan implements StartSpanFn { - // The following properties and methods are currently not documented as their API isn't considered official: - // setUserContext(), setCustomContext(), toJSON(), setDefaultName(), setDefaultNameFromRequest() - - name: string - result: string | number - - startSpan(name?: string | null, options?: SpanOptions): Span | null - startSpan(name: string | null, type: string | null, options?: SpanOptions): Span | null - startSpan(name: string | null, type: string | null, subtype: string | null, options?: SpanOptions): Span | null - startSpan( - name: string | null, - type: string | null, - subtype: string | null, - action: string | null, - options?: SpanOptions - ): Span | null - ensureParentId(): string - end(result?: string | number | null, endTime?: number): void - } - - declare class Span extends GenericSpan { - // The following properties and methods are currently not documented as their API isn't considered official: - // customStackTrace(), setDbContext() - - transaction: Transaction - name: string - - end(endTime?: number): void - } - - interface AgentConfigOptions { - disableMeteorInstrumentations?: string[] - abortedErrorThreshold?: string // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it - active?: boolean - addPatch?: KeyValueConfig - apiRequestSize?: string // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it - apiRequestTime?: string // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it - asyncHooks?: boolean - captureBody?: CaptureBody - captureErrorLogStackTraces?: CaptureErrorLogStackTraces - captureExceptions?: boolean - captureHeaders?: boolean - captureSpanStackTraces?: boolean - containerId?: string - disableInstrumentations?: string | string[] - environment?: string - errorMessageMaxLength?: string // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it - errorOnAbortedRequests?: boolean - filterHttpHeaders?: boolean - frameworkName?: string - frameworkVersion?: string - globalLabels?: KeyValueConfig - hostname?: string - ignoreUrls?: Array - ignoreUserAgents?: Array - instrument?: boolean - instrumentIncomingHTTPRequests?: boolean - kubernetesNamespace?: string - kubernetesNodeName?: string - kubernetesPodName?: string - kubernetesPodUID?: string - logLevel?: LogLevel - logUncaughtExceptions?: boolean - logger?: Logger - metricsInterval?: string // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it - payloadLogFile?: string - centralConfig?: boolean - secretToken?: string - serverCaCertFile?: string - serverTimeout?: string // Also support `number`, but as we're removing this functionality soon, there's no need to advertise it - serverUrl?: string - serviceName?: string - serviceVersion?: string - sourceLinesErrorAppFrames?: number - sourceLinesErrorLibraryFrames?: number - sourceLinesSpanAppFrames?: number - sourceLinesSpanLibraryFrames?: number - stackTraceLimit?: number - transactionMaxSpans?: number - transactionSampleRate?: number - usePathAsTransactionName?: boolean - verifyServerCert?: boolean - } - - interface CaptureErrorOptions { - request?: IncomingMessage - response?: ServerResponse - timestamp?: number - handled?: boolean - user?: UserObject - labels?: Labels - tags?: Labels - custom?: object - message?: string - } - - interface Labels { - [key: string]: LabelValue - } - - interface UserObject { - id?: string | number - username?: string - email?: string - } - - interface ParameterizedMessageObject { - message: string - params: Array - } - - interface Logger { - fatal(msg: string, ...args: any[]): void - fatal(obj: {}, msg?: string, ...args: any[]): void - error(msg: string, ...args: any[]): void - error(obj: {}, msg?: string, ...args: any[]): void - warn(msg: string, ...args: any[]): void - warn(obj: {}, msg?: string, ...args: any[]): void - info(msg: string, ...args: any[]): void - info(obj: {}, msg?: string, ...args: any[]): void - debug(msg: string, ...args: any[]): void - debug(obj: {}, msg?: string, ...args: any[]): void - trace(msg: string, ...args: any[]): void - trace(obj: {}, msg?: string, ...args: any[]): void - [propName: string]: any - } - - interface TransactionOptions { - startTime?: number - childOf?: Transaction | Span | string - } - - interface SpanOptions { - childOf?: Transaction | Span | string - } - - type CaptureBody = 'off' | 'errors' | 'transactions' | 'all' - type CaptureErrorLogStackTraces = 'never' | 'messages' | 'always' - type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' - - type CaptureErrorCallback = (err: Error | null, id: string) => void - type FilterFn = (payload: Payload) => Payload | boolean | void - type LabelValue = string | number | boolean | null | undefined - type KeyValueConfig = string | Labels | Array> - - type Payload = { [propName: string]: any } - - type PatchHandler = (exports: any, agent: Agent, options: PatchOptions) => any - - interface PatchOptions { - version: string | undefined - enabled: boolean - } - - interface Taggable { - setLabel(name: string, value: LabelValue): boolean - addLabels(labels: Labels): boolean - } - - interface StartSpanFn { - startSpan(name?: string | null, options?: SpanOptions): Span | null - startSpan(name: string | null, type: string | null, options?: SpanOptions): Span | null - startSpan(name: string | null, type: string | null, subtype: string | null, options?: SpanOptions): Span | null - startSpan( - name: string | null, - type: string | null, - subtype: string | null, - action: string | null, - options?: SpanOptions - ): Span | null - } - - // Inlined from @types/aws-lambda - start - declare namespace AwsLambda { - interface CognitoIdentity { - cognitoIdentityId: string - cognitoIdentityPoolId: string - } - - interface ClientContext { - client: ClientContextClient - custom?: any - env: ClientContextEnv - } - - interface ClientContextClient { - installationId: string - appTitle: string - appVersionName: string - appVersionCode: string - appPackageName: string - } - - interface ClientContextEnv { - platformVersion: string - platform: string - make: string - model: string - locale: string - } - - type Callback = (error?: Error | null | string, result?: TResult) => void - - interface Context { - // Properties - callbackWaitsForEmptyEventLoop: boolean - functionName: string - functionVersion: string - invokedFunctionArn: string - memoryLimitInMB: number - awsRequestId: string - logGroupName: string - logStreamName: string - identity?: CognitoIdentity - clientContext?: ClientContext - - // Functions - getRemainingTimeInMillis(): number - - // Functions for compatibility with earlier Node.js Runtime v0.10.42 - // For more details see http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-using-old-runtime.html#nodejs-prog-model-oldruntime-context-methods - done(error?: Error, result?: any): void - fail(error: Error | string): void - succeed(messageOrObject: any): void - succeed(message: string, object: any): void - } - - type Handler = ( - event: TEvent, - context: Context, - callback: Callback - ) => void | Promise - } - - // Inlined from @types/connect - start - declare namespace Connect { - type NextFunction = (err?: any) => void - type ErrorHandleFunction = (err: any, req: IncomingMessage, res: ServerResponse, next: NextFunction) => void - } -} diff --git a/meteor/server/worker/worker.ts b/meteor/server/worker/worker.ts index f5e3c82885..6a4b8651cf 100644 --- a/meteor/server/worker/worker.ts +++ b/meteor/server/worker/worker.ts @@ -9,7 +9,7 @@ import { threadedClass, Promisify, ThreadedClassManager } from 'threadedclass' import type { JobSpec } from '@sofie-automation/job-worker/dist/main' import type { IpcJobWorker } from '@sofie-automation/job-worker/dist/ipc' import { createManualPromise, getRandomString, ManualPromise, Time } from '../lib/tempLib' -import { MeteorStartupAsync, getCurrentTime } from '../lib/lib' +import { getCurrentTime } from '../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' import { triggerFastTrackObserver, FastTrackObservers } from '../publications/fastTrack' @@ -262,7 +262,7 @@ async function logLine(msg: LogEntry): Promise { } let worker: Promisify | undefined -MeteorStartupAsync(async () => { +Meteor.startup(async () => { if (Meteor.isDevelopment) { // Ensure meteor restarts when the _force_restart file changes try { diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 233d3a8179..a01cc3dc27 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -473,21 +473,19 @@ __metadata: languageName: node linkType: hard -"@elastic/ecs-helpers@npm:^1.1.0": - version: 1.1.0 - resolution: "@elastic/ecs-helpers@npm:1.1.0" - dependencies: - fast-json-stringify: ^2.4.1 - checksum: 8f64e86fe3cfe67540fd8c2a62e0b7db1f4e8cab8c4a63e2f49ce295c3d3b629d0f3363b0107fe530650898a89b5b1d86190717447a481932856651911e6ef61 +"@elastic/ecs-helpers@npm:^2.1.1": + version: 2.1.1 + resolution: "@elastic/ecs-helpers@npm:2.1.1" + checksum: 80db727963e26a28312c67e47cbc40bfe5441ff8937dc27157a7f968fcd20475345a19a6588cc1c6b97b2eeaf5990f2285eaf2f03e206f3c2cd9965160d3fdaa languageName: node linkType: hard -"@elastic/ecs-pino-format@npm:^1.2.0": - version: 1.3.0 - resolution: "@elastic/ecs-pino-format@npm:1.3.0" +"@elastic/ecs-pino-format@npm:^1.5.0": + version: 1.5.0 + resolution: "@elastic/ecs-pino-format@npm:1.5.0" dependencies: - "@elastic/ecs-helpers": ^1.1.0 - checksum: 1543b80b84e3f35b6be35b73b5d0153d267c357df750ba77af2c8d7b07097df8095f104d283e46b6500c902815327a1ad0005aa2e3855afbddbac0b683b72c6c + "@elastic/ecs-helpers": ^2.1.1 + checksum: e66a1801ecafa5d1f56037df8dafa9da9c302440c8254c636374e489a5a50e85166b77c951c1f8d3edd99cb77f43b5967c3f31a68cac23b2a4b95a87ce72b066 languageName: node linkType: hard @@ -1205,7 +1203,7 @@ __metadata: "@sofie-automation/shared-lib": 1.52.0-in-development amqplib: ^0.10.3 deepmerge: ^4.3.1 - elastic-apm-node: ^3.51.0 + elastic-apm-node: ^4.8.0 eventemitter3: ^4.0.7 mongodb: ^5.9.2 node-fetch: ^2.7.0 @@ -1395,13 +1393,6 @@ __metadata: languageName: node linkType: hard -"@types/fibers@npm:^3.1.4": - version: 3.1.4 - resolution: "@types/fibers@npm:3.1.4" - checksum: 36bb70198fb5b7f99b010c006ad0e77d473061cf03d4f63f1842040b7ecb62e8800041c9509b927ec59a1d5ac374d5e7c86b2fe0de2a9813f1538ab0248e5dea - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.6 resolution: "@types/graceful-fs@npm:4.1.6" @@ -1573,10 +1564,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.18.63": - version: 14.18.63 - resolution: "@types/node@npm:14.18.63" - checksum: be909061a54931778c71c49dc562586c32f909c4b6197e3d71e6dac726d8bd9fccb9f599c0df99f52742b68153712b5097c0f00cac4e279fa894b0ea6719a8fd +"@types/node@npm:^20.17.6": + version: 20.17.6 + resolution: "@types/node@npm:20.17.6" + dependencies: + undici-types: ~6.19.2 + checksum: d51dbb9881c94d0310b32b5fd8013e3261595c61bc888fa27258469c93c3dc0b3c4d20a9f28f3f5f79562f6737e28e7f3dd04940dc8b4d966d34aaf318f7f69b languageName: node linkType: hard @@ -1849,6 +1842,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: ^5.0.0 + checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 + languageName: node + linkType: hard + "accepts@npm:^1.3.5, accepts@npm:^1.3.7": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -1870,12 +1872,12 @@ __metadata: languageName: node linkType: hard -"acorn-import-assertions@npm:^1.9.0": - version: 1.9.0 - resolution: "acorn-import-assertions@npm:1.9.0" +"acorn-import-attributes@npm:^1.9.5": + version: 1.9.5 + resolution: "acorn-import-attributes@npm:1.9.5" peerDependencies: acorn: ^8 - checksum: 944fb2659d0845c467066bdcda2e20c05abe3aaf11972116df457ce2627628a81764d800dd55031ba19de513ee0d43bb771bc679cc0eda66dc8b4fade143bc0c + checksum: 1c0c49b6a244503964ae46ae850baccf306e84caf99bc2010ed6103c69a423987b07b520a6c619f075d215388bd4923eccac995886a54309eda049ab78a4be95 languageName: node linkType: hard @@ -1990,7 +1992,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.11.0, ajv@npm:^6.12.4": +"ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -2230,15 +2232,6 @@ __metadata: languageName: node linkType: hard -"async-cache@npm:^1.1.0": - version: 1.1.0 - resolution: "async-cache@npm:1.1.0" - dependencies: - lru-cache: ^4.0.0 - checksum: 3f55cc78b3ddc745b6604dd144fc7bca2e21c7ba4c5ea18d312234dc625133511723dff6c71b2283582421f95d591bdb24bf89ce4c4869151e4ecedbdad4acf2 - languageName: node - linkType: hard - "async-value-promise@npm:^1.1.1": version: 1.1.1 resolution: "async-value-promise@npm:1.1.1" @@ -2298,14 +2291,13 @@ __metadata: "@types/app-root-path": ^1.2.8 "@types/body-parser": ^1.19.5 "@types/deep-extend": ^0.6.2 - "@types/fibers": ^3.1.4 "@types/jest": ^29.5.11 "@types/koa": ^2.14.0 "@types/koa-bodyparser": ^4.3.12 "@types/koa-static": ^4.0.4 "@types/koa__cors": ^5.0.0 "@types/koa__router": ^12.0.4 - "@types/node": ^14.18.63 + "@types/node": ^20.17.6 "@types/request": ^2.48.12 "@types/semver": ^7.5.6 "@types/underscore": ^1.11.15 @@ -2319,14 +2311,13 @@ __metadata: deep-extend: 0.6.0 deepmerge: ^4.3.1 ejson: ^2.2.3 + elastic-apm-node: ^4.8.0 eslint: ^8.56.0 eslint-config-prettier: ^8.10.0 - eslint-plugin-custom-rules: "link:eslint-rules" eslint-plugin-jest: ^27.6.3 eslint-plugin-node: ^11.1.0 eslint-plugin-prettier: ^4.2.1 fast-clone: ^1.5.13 - fibers-npm: "npm:fibers@5.0.3" glob: ^8.1.0 i18next: ^21.10.0 i18next-conv: ^10.2.0 @@ -2338,7 +2329,6 @@ __metadata: koa-static: ^5.0.0 legally: ^3.5.10 meteor-node-stubs: ^1.2.7 - meteor-promise: 0.9.0 moment: ^2.30.1 nanoid: ^3.3.7 node-gyp: ^9.4.1 @@ -2493,6 +2483,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + "binary-search@npm:^1.3.3": version: 1.3.6 resolution: "binary-search@npm:1.3.6" @@ -3468,10 +3465,10 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 +"cookie@npm:^0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e languageName: node linkType: hard @@ -3819,15 +3816,6 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^1.0.3": - version: 1.0.3 - resolution: "detect-libc@npm:1.0.3" - bin: - detect-libc: ./bin/detect-libc.js - checksum: daaaed925ffa7889bd91d56e9624e6c8033911bb60f3a50a74a87500680652969dbaab9526d1e200a4c94acf80fc862a22131841145a0a8482d60a99c24f4a3e - languageName: node - linkType: hard - "detect-libc@npm:^2.0.0": version: 2.0.2 resolution: "detect-libc@npm:2.0.2" @@ -3935,21 +3923,20 @@ __metadata: languageName: node linkType: hard -"elastic-apm-node@npm:^3.51.0": - version: 3.51.0 - resolution: "elastic-apm-node@npm:3.51.0" +"elastic-apm-node@npm:^4.8.0": + version: 4.8.0 + resolution: "elastic-apm-node@npm:4.8.0" dependencies: - "@elastic/ecs-pino-format": ^1.2.0 + "@elastic/ecs-pino-format": ^1.5.0 "@opentelemetry/api": ^1.4.1 "@opentelemetry/core": ^1.11.0 "@opentelemetry/sdk-metrics": ^1.12.0 after-all-results: ^2.0.0 agentkeepalive: ^4.2.1 - async-cache: ^1.1.0 async-value-promise: ^1.1.1 basic-auth: ^2.0.1 breadth-filter: ^2.0.0 - cookie: ^0.5.0 + cookie: ^0.7.1 core-util-is: ^1.0.2 end-of-stream: ^1.4.4 error-callsites: ^2.0.4 @@ -3958,26 +3945,26 @@ __metadata: fast-safe-stringify: ^2.0.7 fast-stream-to-buffer: ^1.0.0 http-headers: ^3.0.2 - import-in-the-middle: 1.4.2 - is-native: ^1.0.1 - lru-cache: ^6.0.0 + import-in-the-middle: 1.11.2 + json-bigint: ^1.0.0 + lru-cache: 10.2.0 measured-reporting: ^1.51.1 module-details-from-path: ^1.0.3 monitor-event-loop-delay: ^1.0.0 object-filter-sequence: ^1.0.0 object-identity-map: ^1.0.2 original-url: ^1.2.3 - pino: ^6.11.2 - readable-stream: ^3.4.0 + pino: ^8.15.0 + readable-stream: ^3.6.2 relative-microtime: ^2.0.0 require-in-the-middle: ^7.1.1 - semver: ^6.3.1 + semver: ^7.5.4 shallow-clone-shim: ^2.0.0 source-map: ^0.8.0-beta.0 sql-summary: ^1.0.1 stream-chopper: ^3.0.1 unicode-byte-truncate: ^1.0.0 - checksum: e6a801e731d6a5178e7450c76e88b9a519823129986365b2f59c4ee8e02c2a0e624deacebce6e85ae0964f6ea876a9f562901ca0b3538f4b0452a24d7f1b0303 + checksum: 4c6534481540b08412096ff192c67b8dcc9501672b57bcc2f6ad159e84a61f5c32a63c6a7edfe8fb289a6854e33e1278e0aa04b5cadcbb1c9cc51325761be45d languageName: node linkType: hard @@ -4224,12 +4211,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-custom-rules@link:eslint-rules::locator=automation-core%40workspace%3A.": - version: 0.0.0-use.local - resolution: "eslint-plugin-custom-rules@link:eslint-rules::locator=automation-core%40workspace%3A." - languageName: node - linkType: soft - "eslint-plugin-es@npm:^3.0.0": version: 3.0.1 resolution: "eslint-plugin-es@npm:3.0.1" @@ -4452,6 +4433,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.4, eventemitter3@npm:^4.0.7": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -4593,18 +4581,6 @@ __metadata: languageName: node linkType: hard -"fast-json-stringify@npm:^2.4.1": - version: 2.7.13 - resolution: "fast-json-stringify@npm:2.7.13" - dependencies: - ajv: ^6.11.0 - deepmerge: ^4.2.2 - rfdc: ^1.2.0 - string-similarity: ^4.0.1 - checksum: f78ab25047c790de5b521c369e0b18c595055d48a106add36e9f86fe45be40226f168ff4708a226e187d0b46f1d6b32129842041728944bd9a03ca5efbbe4ccb - languageName: node - linkType: hard - "fast-levenshtein@npm:^2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" @@ -4612,14 +4588,14 @@ __metadata: languageName: node linkType: hard -"fast-redact@npm:^3.0.0": - version: 3.3.0 - resolution: "fast-redact@npm:3.3.0" - checksum: 3f7becc70a5a2662a9cbfdc52a4291594f62ae998806ee00315af307f32d9559dbf512146259a22739ee34401950ef47598c1f4777d33b0ed5027203d67f549c +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: ef03f0d1849da074a520a531ad299bf346417b790a643931ab4e01cb72275c8d55b60dc8512fb1f1818647b696790edefaa96704228db9f012da935faa1940af languageName: node linkType: hard -"fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.0.8": +"fast-safe-stringify@npm:^2.0.7": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: a851cbddc451745662f8f00ddb622d6766f9bd97642dabfd9a405fb0d646d69fc0b9a1243cbf67f5f18a39f40f6fa821737651ff1bceeba06c9992ca2dc5bd3d @@ -4660,15 +4636,6 @@ __metadata: languageName: node linkType: hard -"fibers-npm@npm:fibers@5.0.3": - version: 5.0.3 - resolution: "fibers@npm:5.0.3" - dependencies: - detect-libc: ^1.0.3 - checksum: d66c5e18a911aab3480b846e1c837e5c7cfacb27a2a5fe512919865eaecef33cdd4abc14d777191a6a93473dc52356d48549c91a2a7b8b3450544c44104b23f3 - languageName: node - linkType: hard - "figures@npm:^3.1.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -4766,13 +4733,6 @@ __metadata: languageName: node linkType: hard -"flatstr@npm:^1.0.12": - version: 1.0.12 - resolution: "flatstr@npm:1.0.12" - checksum: e1bb562c94b119e958bf37e55738b172b5f8aaae6532b9660ecd877779f8559dbbc89613ba6b29ccc13447e14c59277d41450f785cf75c30df9fce62f459e9a8 - languageName: node - linkType: hard - "flatted@npm:^3.2.7": version: 3.2.9 resolution: "flatted@npm:3.2.9" @@ -5617,15 +5577,15 @@ __metadata: languageName: node linkType: hard -"import-in-the-middle@npm:1.4.2": - version: 1.4.2 - resolution: "import-in-the-middle@npm:1.4.2" +"import-in-the-middle@npm:1.11.2": + version: 1.11.2 + resolution: "import-in-the-middle@npm:1.11.2" dependencies: acorn: ^8.8.2 - acorn-import-assertions: ^1.9.0 + acorn-import-attributes: ^1.9.5 cjs-module-lexer: ^1.2.2 module-details-from-path: ^1.0.3 - checksum: 52971f821e9a3c94834cd5cf0ab5178321c07d4f4babd547b3cb24c4de21670d05b42ca1523890e7e90525c3bba6b7db7e54cf45421919b0b2712a34faa96ea5 + checksum: 06fb73100a918e00778779713119236cc8d3d4656aae9076a18159cfcd28eb0cc26e0a5040d11da309c5f8f8915c143b8d74e73c0734d3f5549b1813d1008bb9 languageName: node linkType: hard @@ -5933,16 +5893,6 @@ __metadata: languageName: node linkType: hard -"is-native@npm:^1.0.1": - version: 1.0.1 - resolution: "is-native@npm:1.0.1" - dependencies: - is-nil: ^1.0.0 - to-source-code: ^1.0.0 - checksum: 4967af8c4d7a06076cb16ef70fba5a5a2b61ef0a83d4d5dce437cf4c6b5315255cccf07db37d487bcdf2f0ded86edb166a62c46a712cfda1227532b70015029c - languageName: node - linkType: hard - "is-negated-glob@npm:^1.0.0": version: 1.0.0 resolution: "is-negated-glob@npm:1.0.0" @@ -5957,13 +5907,6 @@ __metadata: languageName: node linkType: hard -"is-nil@npm:^1.0.0": - version: 1.0.1 - resolution: "is-nil@npm:1.0.1" - checksum: e5b89c3b82068e719372381c5aaa5f3f28d09e6d501d7f7e4365f136433de1ae92f9f82eeedcb3c3282da1ccf374aad46cc06feab2647d2820067c4a35484760 - languageName: node - linkType: hard - "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -6702,6 +6645,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: ^9.0.0 + checksum: c67bb93ccb3c291e60eb4b62931403e378906aab113ec1c2a8dd0f9a7f065ad6fd9713d627b732abefae2e244ac9ce1721c7a3142b2979532f12b258634ce6f6 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -7113,13 +7065,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^4.0.0": - version: 4.1.5 - resolution: "lru-cache@npm:4.1.5" - dependencies: - pseudomap: ^1.0.2 - yallist: ^2.1.2 - checksum: 4bb4b58a36cd7dc4dcec74cbe6a8f766a38b7426f1ff59d4cf7d82a2aa9b9565cd1cb98f6ff60ce5cd174524868d7bc9b7b1c294371851356066ca9ac4cf135a +"lru-cache@npm:10.2.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: eee7ddda4a7475deac51ac81d7dd78709095c6fa46e8350dc2d22462559a1faa3b81ed931d5464b13d48cbd7e08b46100b6f768c76833912bc444b99c37e25db languageName: node linkType: hard @@ -7396,13 +7345,6 @@ __metadata: languageName: node linkType: hard -"meteor-promise@npm:0.9.0": - version: 0.9.0 - resolution: "meteor-promise@npm:0.9.0" - checksum: 2837518debf173a2946d55c270f3d799fa9a9421c63291be42b06a24071b40f2e81120ac88feb268a49819e726b2c1e0fc9413f8aa747d945aab3d6e9ffaea2e - languageName: node - linkType: hard - "methods@npm:^1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -8118,6 +8060,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 6ce7acdc7b9ceb51cf029b5239cbf41937ee4c8dcd9d4e475e1777b41702564d46caa1150a744e00da0ac6d923ab83471646a39a4470f97481cf6e2d8d253c3f + languageName: node + linkType: hard + "on-finished@npm:2.4.1, on-finished@npm:^2.3.0": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -8587,27 +8536,41 @@ __metadata: languageName: node linkType: hard -"pino-std-serializers@npm:^3.1.0": - version: 3.2.0 - resolution: "pino-std-serializers@npm:3.2.0" - checksum: 77e29675b116e42ae9fe6d4ef52ef3a082ffc54922b122d85935f93ddcc20277f0b0c873c5c6c5274a67b0409c672aaae3de6bcea10a2d84699718dda55ba95b +"pino-abstract-transport@npm:^1.2.0": + version: 1.2.0 + resolution: "pino-abstract-transport@npm:1.2.0" + dependencies: + readable-stream: ^4.0.0 + split2: ^4.0.0 + checksum: 3336c51fb91ced5ef8a4bfd70a96e41eb6deb905698e83350dc71eedffb34795db1286d2d992ce1da2f6cd330a68be3f7e2748775a6b8a2ee3416796070238d6 languageName: node linkType: hard -"pino@npm:^6.11.2": - version: 6.14.0 - resolution: "pino@npm:6.14.0" +"pino-std-serializers@npm:^6.0.0": + version: 6.2.2 + resolution: "pino-std-serializers@npm:6.2.2" + checksum: aeb0662edc46ec926de9961ed4780a4f0586bb7c37d212cd469c069639e7816887a62c5093bc93f260a4e0900322f44fc8ab1343b5a9fa2864a888acccdb22a4 + languageName: node + linkType: hard + +"pino@npm:^8.15.0": + version: 8.21.0 + resolution: "pino@npm:8.21.0" dependencies: - fast-redact: ^3.0.0 - fast-safe-stringify: ^2.0.8 - flatstr: ^1.0.12 - pino-std-serializers: ^3.1.0 - process-warning: ^1.0.0 + atomic-sleep: ^1.0.0 + fast-redact: ^3.1.1 + on-exit-leak-free: ^2.1.0 + pino-abstract-transport: ^1.2.0 + pino-std-serializers: ^6.0.0 + process-warning: ^3.0.0 quick-format-unescaped: ^4.0.3 - sonic-boom: ^1.0.2 + real-require: ^0.2.0 + safe-stable-stringify: ^2.3.1 + sonic-boom: ^3.7.0 + thread-stream: ^2.6.0 bin: pino: bin.js - checksum: eb13e12e3a3d682abe4a4da426455a9f4e041e55e4fa57d72d9677ee8d188a9c952f69347e728a3761c8262cdce76ef24bee29e1a53ab15aa9c5e851099163d0 + checksum: d895c37cfcb7ade33ad7ac4ca54c0497ab719ec726e42b7c7b9697e07572a09a7c7de18d751440769c3ea5ecbac2075fdac720cf182720a4764defe3de8a1411 languageName: node linkType: hard @@ -8670,10 +8633,10 @@ __metadata: languageName: node linkType: hard -"process-warning@npm:^1.0.0": - version: 1.0.0 - resolution: "process-warning@npm:1.0.0" - checksum: c708a03241deec3cabaeee39c4f9ee8c4d71f1c5ef9b746c8252cdb952a6059068cfcdaf348399775244cbc441b6ae5e26a9c87ed371f88335d84f26d19180f9 +"process-warning@npm:^3.0.0": + version: 3.0.0 + resolution: "process-warning@npm:3.0.0" + checksum: 1fc2eb4524041de3c18423334cc8b4e36bec5ad5472640ca1a936122c6e01da0864c1a4025858ef89aea93eabe7e77db93ccea225b10858617821cb6a8719efe languageName: node linkType: hard @@ -8720,13 +8683,6 @@ __metadata: languageName: node linkType: hard -"pseudomap@npm:^1.0.2": - version: 1.0.2 - resolution: "pseudomap@npm:1.0.2" - checksum: 856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 - languageName: node - linkType: hard - "public-encrypt@npm:^4.0.0": version: 4.0.3 resolution: "public-encrypt@npm:4.0.3" @@ -8990,7 +8946,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -9001,6 +8957,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: ^3.0.0 + buffer: ^6.0.3 + events: ^3.3.0 + process: ^0.11.10 + string_decoder: ^1.3.0 + checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a + languageName: node + linkType: hard + "readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" @@ -9037,6 +9006,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: fa060f19f2f447adf678d1376928c76379dce5f72bd334da301685ca6cdcb7b11356813332cc243c88470796bc2e2b1e2917fc10df9143dd93c2ea608694971d + languageName: node + linkType: hard + "rechoir@npm:^0.6.2": version: 0.6.2 resolution: "rechoir@npm:0.6.2" @@ -9236,7 +9212,7 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.2.0, rfdc@npm:^1.3.0": +"rfdc@npm:^1.3.0": version: 1.3.0 resolution: "rfdc@npm:1.3.0" checksum: fb2ba8512e43519983b4c61bd3fa77c0f410eff6bae68b08614437bc3f35f91362215f7b4a73cbda6f67330b5746ce07db5dd9850ad3edc91271ad6deea0df32 @@ -9558,13 +9534,12 @@ __metadata: languageName: node linkType: hard -"sonic-boom@npm:^1.0.2": - version: 1.4.1 - resolution: "sonic-boom@npm:1.4.1" +"sonic-boom@npm:^3.7.0": + version: 3.8.1 + resolution: "sonic-boom@npm:3.8.1" dependencies: atomic-sleep: ^1.0.0 - flatstr: ^1.0.12 - checksum: 189fa8fe5c2dc05d3513fc1a4926a2f16f132fa6fa0b511745a436010cdcd9c1d3b3cb6a9d7c05bd32a965dc77673a5ac0eb0992e920bdedd16330d95323124f + checksum: 79c90d7a2f928489fd3d4b68d8f8d747a426ca6ccf83c3b102b36f899d4524463dd310982ab7ab6d6bcfd34b7c7c281ad25e495ad71fbff8fd6fa86d6273fc6b languageName: node linkType: hard @@ -9682,6 +9657,13 @@ __metadata: languageName: node linkType: hard +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 05d54102546549fe4d2455900699056580cca006c0275c334611420f854da30ac999230857a85fdd9914dc2109ae50f80fda43d2a445f2aa86eccdc1dfce779d + languageName: node + linkType: hard + "split@npm:^1.0.0": version: 1.0.1 resolution: "split@npm:1.0.1" @@ -9851,13 +9833,6 @@ __metadata: languageName: node linkType: hard -"string-similarity@npm:^4.0.1": - version: 4.0.4 - resolution: "string-similarity@npm:4.0.4" - checksum: 797b41b24e1eb6b3b0ab896950b58c295a19a82933479c75f7b5279ffb63e0b456a8c8d10329c02f607ca1a50370e961e83d552aa468ff3b0fa15809abc9eff7 - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -10152,6 +10127,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^2.6.0": + version: 2.7.0 + resolution: "thread-stream@npm:2.7.0" + dependencies: + real-require: ^0.2.0 + checksum: 75ab019cda628344c7779e5f5a88f7759764efd29d320327ad2e6c2622778b5f1c43a3966d76a9ee5744086d61c680b413548f5521030f9e9055487684436165 + languageName: node + linkType: hard + "threadedclass@npm:^1.2.2": version: 1.2.2 resolution: "threadedclass@npm:1.2.2" @@ -10245,15 +10229,6 @@ __metadata: languageName: node linkType: hard -"to-source-code@npm:^1.0.0": - version: 1.0.2 - resolution: "to-source-code@npm:1.0.2" - dependencies: - is-nil: ^1.0.0 - checksum: 24fd24767f185ad11f81c1e020c2f789fba29471195227731530ec39b2697bb680c16e1f6f7d0d68bffba81e3d95e68dd6014f8c88371399bddcf8c4ad036de3 - languageName: node - linkType: hard - "to-through@npm:^3.0.0": version: 3.0.0 resolution: "to-through@npm:3.0.0" @@ -10596,6 +10571,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 + languageName: node + linkType: hard + "unicode-byte-truncate@npm:^1.0.0": version: 1.0.0 resolution: "unicode-byte-truncate@npm:1.0.0" @@ -11058,13 +11040,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^2.1.2": - version: 2.1.2 - resolution: "yallist@npm:2.1.2" - checksum: 9ba99409209f485b6fcb970330908a6d41fa1c933f75e08250316cce19383179a6b70a7e0721b89672ebb6199cc377bf3e432f55100da6a7d6e11902b0a642cb - languageName: node - linkType: hard - "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" diff --git a/package.json b/package.json index eb20c670c5..8a279813ee 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "license": "MIT", "private": true, "engines": { - "node": "^14.19 || ^16.14 || ^18.12" + "node": ">=20.18" }, "scripts": { "postinstall": "run install:packages && run install:meteor", - "install:meteor": "cd meteor && meteor --version && meteor npm install -g yarn && node ../scripts/fix-windows-yarn.js && meteor yarn install", - "install:packages": "cd packages && (node is_node_14.js && yarn lerna run --ignore openapi install || yarn install)", + "install:meteor": "cd meteor && meteor --version && meteor npm install -g yarn && node ../scripts/fix-windows-yarn.js && yarn install", + "install:packages": "cd packages && yarn install", "start": "yarn install && run install-and-build && run dev", "install-and-build": "node ./scripts/install-and-build.mjs", "dev": "node ./scripts/run.mjs", @@ -20,13 +20,13 @@ "unit:packages": "cd packages && run unit", "check-types:meteor": "cd meteor && run check-types", "test:meteor": "cd meteor && run test", - "lint:meteor": "cd meteor && meteor npm run lint", - "unit:meteor": "cd meteor && meteor npm run unit", - "meteor:run": "cd meteor && meteor npm run start", + "lint:meteor": "cd meteor && yarn lint", + "unit:meteor": "cd meteor && yarn unit", + "meteor:run": "cd meteor && yarn start", "lint": "run lint:meteor && run lint:packages", "unit": "run unit:meteor && run unit:packages", "validate:release": "yarn install && run install-and-build && run validate:release:packages && run validate:release:meteor", - "validate:release:meteor": "cd meteor && meteor npm run validate:prod-dependencies && meteor npm run license-validate && meteor npm run lint && meteor npm run test", + "validate:release:meteor": "cd meteor && yarn validate:prod-dependencies && yarn license-validate && yarn lint && yarn test", "validate:release:packages": "cd packages && run validate:dependencies && run test", "meteor": "cd meteor && meteor", "docs:serve": "cd packages && run docs:serve", diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 7594583839..d32f58ee43 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -29,7 +29,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=14.19" + "node": ">=20.18" }, "files": [ "/dist", diff --git a/packages/blueprints-integration/src/migrations.ts b/packages/blueprints-integration/src/migrations.ts index d7eb39e622..6309e8cc90 100644 --- a/packages/blueprints-integration/src/migrations.ts +++ b/packages/blueprints-integration/src/migrations.ts @@ -24,7 +24,10 @@ export interface MigrationStepInputFilteredResult { } export type ValidateFunctionCore = (afterMigration: boolean) => Promise -export type ValidateFunctionSystem = (context: MigrationContextSystem, afterMigration: boolean) => boolean | string +export type ValidateFunctionSystem = ( + context: MigrationContextSystem, + afterMigration: boolean +) => Promise export type ValidateFunctionStudio = (context: MigrationContextStudio, afterMigration: boolean) => boolean | string export type ValidateFunctionShowStyle = ( context: MigrationContextShowStyle, @@ -37,7 +40,10 @@ export type ValidateFunction = | ValidateFunctionCore export type MigrateFunctionCore = (input: MigrationStepInputFilteredResult) => Promise -export type MigrateFunctionSystem = (context: MigrationContextSystem, input: MigrationStepInputFilteredResult) => void +export type MigrateFunctionSystem = ( + context: MigrationContextSystem, + input: MigrationStepInputFilteredResult +) => Promise export type MigrateFunctionStudio = (context: MigrationContextStudio, input: MigrationStepInputFilteredResult) => void export type MigrateFunctionShowStyle = ( context: MigrationContextShowStyle, @@ -77,11 +83,11 @@ export interface ShowStyleVariantPart { } interface MigrationContextWithTriggeredActions { - getAllTriggeredActions: () => IBlueprintTriggeredActions[] - getTriggeredAction: (triggeredActionId: string) => IBlueprintTriggeredActions | undefined + getAllTriggeredActions: () => Promise + getTriggeredAction: (triggeredActionId: string) => Promise getTriggeredActionId: (triggeredActionId: string) => string - setTriggeredAction: (triggeredActions: IBlueprintTriggeredActions) => void - removeTriggeredAction: (triggeredActionId: string) => void + setTriggeredAction: (triggeredActions: IBlueprintTriggeredActions) => Promise + removeTriggeredAction: (triggeredActionId: string) => Promise } export interface MigrationContextShowStyle extends MigrationContextWithTriggeredActions { diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 5d3fb3a56b..52533e7332 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -30,7 +30,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": "^14.19 || ^16.14 || ^18.12" + "node": ">=20.18" }, "files": [ "/dist", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index d144d94516..7fabd6dc65 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -15,7 +15,7 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "engines": { - "node": ">=18.0" + "node": ">=20.18" }, "devDependencies": { "@docusaurus/core": "3.2.1", diff --git a/packages/is_node_14.js b/packages/is_node_14.js deleted file mode 100644 index 21623c3f46..0000000000 --- a/packages/is_node_14.js +++ /dev/null @@ -1,5 +0,0 @@ -if (process.version.match(/^v14/) !== null) { - process.exit(0) -} else { - process.exit(1) -} \ No newline at end of file diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 4f42b539d2..8a7e17f85b 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -31,7 +31,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": "^14.19 || ^16.14 || ^18.12" + "node": ">=20.18" }, "files": [ "/dist", @@ -46,7 +46,7 @@ "@sofie-automation/shared-lib": "1.52.0-in-development", "amqplib": "^0.10.3", "deepmerge": "^4.3.1", - "elastic-apm-node": "^3.51.0", + "elastic-apm-node": "^4.8.0", "eventemitter3": "^4.0.7", "mongodb": "^5.9.2", "node-fetch": "^2.7.0", diff --git a/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts b/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts index 9d9e512932..51d53812c3 100644 --- a/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts +++ b/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts @@ -338,7 +338,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(ExternalMessageQueue.findOne()).toBeFalsy() }) - testInFiber('fail to send a slack-type message', async () => { + test('fail to send a slack-type message', async () => { // setLogLevel(LogLevel.DEBUG) expect(ExternalMessageQueue.findOne()).toBeFalsy() @@ -372,14 +372,14 @@ describe('Test sending messages to mocked endpoints', () => { expect(message.sent).toBeUndefined() }) - testInFiber('does not try to send again immediately', async () => { + test('does not try to send again immediately', async () => { // setLogLevel(LogLevel.DEBUG) await runAllTimers() // Does not try to send again yet ... too close to lastTry expect(sendSlackMessageToWebhook).toHaveBeenCalledTimes(2) }) - testInFiber('after a minute, tries to resend', async () => { + test('after a minute, tries to resend', async () => { // setLogLevel(LogLevel.DEBUG) // Reset the last try clock const sendTime = getCurrentTime() @@ -400,7 +400,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(message.sent).toBeUndefined() }) - testInFiber('does not retry to send if on hold', async () => { + test('does not retry to send if on hold', async () => { // setLogLevel(LogLevel.DEBUG) Meteor.call(ExternalMessageQueueAPIMethods.toggleHold, message._id) @@ -422,7 +422,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(message.hold).toBe(false) }) - testInFiber('does not retry after retryUntil time', async () => { + test('does not retry after retryUntil time', async () => { // setLogLevel(LogLevel.DEBUG) ExternalMessageQueue.update(message._id, { @@ -435,7 +435,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(sendSlackMessageToWebhook).toHaveBeenCalledTimes(3) }) - testInFiber('can be forced to retry manually once', async () => { + test('can be forced to retry manually once', async () => { // setLogLevel(LogLevel.DEBUG) Meteor.call(ExternalMessageQueueAPIMethods.toggleHold, message._id) @@ -460,7 +460,7 @@ describe('Test sending messages to mocked endpoints', () => { }) }) - testInFiber('send a soap-type message', async () => { + test('send a soap-type message', async () => { // setLogLevel(LogLevel.DEBUG) expect( ExternalMessageQueue.findOne({ @@ -504,7 +504,7 @@ describe('Test sending messages to mocked endpoints', () => { ).toBeFalsy() }) - testInFiber('fail to send a soap message', async () => { + test('fail to send a soap message', async () => { // setLogLevel(LogLevel.DEBUG) expect(ExternalMessageQueue.findOne()).toBeFalsy() @@ -546,7 +546,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(ExternalMessageQueue.findOne()).toBeFalsy() }) - testInFiber('fatal error when sending a soap-type message', async () => { + test('fatal error when sending a soap-type message', async () => { // setLogLevel(LogLevel.DEBUG) expect(ExternalMessageQueue.findOne()).toBeFalsy() @@ -597,7 +597,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(ExternalMessageQueue.findOne()).toBeFalsy() }) - testInFiber('send a rabbit MQ-type message', async () => { + test('send a rabbit MQ-type message', async () => { // setLogLevel(LogLevel.DEBUG) expect(ExternalMessageQueue.findOne()).toBeFalsy() @@ -634,7 +634,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(ExternalMessageQueue.findOne()).toBeFalsy() }) - testInFiber('fail to send a rabbitMQ-type message', async () => { + test('fail to send a rabbitMQ-type message', async () => { // setLogLevel(LogLevel.DEBUG) expect(ExternalMessageQueue.findOne()).toBeFalsy() @@ -679,7 +679,7 @@ describe('Test sending messages to mocked endpoints', () => { expect(ExternalMessageQueue.findOne()).toBeFalsy() }) - testInFiber('does not send expired messages', async () => { + test('does not send expired messages', async () => { // setLogLevel(LogLevel.DEBUG) expect(ExternalMessageQueue.findOne()).toBeFalsy() diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index c3f7d174a7..0256447f51 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -341,7 +341,7 @@ describe('Lookahead', () => { await expectLookaheadForLayerMock(playlistId, [expectedCurrent, expectedNext], expectedPrevious, fakeParts) }) - // testInFiber('Pieces', () => { + // test('Pieces', () => { // const fakeParts = partIds.map((p) => ({ _id: p })) as Part[] // getOrderedPartsAfterPlayheadMock.mockReturnValue(fakeParts) diff --git a/packages/job-worker/src/playout/timings/events.ts b/packages/job-worker/src/playout/timings/events.ts index aa3b147f2a..eeca604b2f 100644 --- a/packages/job-worker/src/playout/timings/events.ts +++ b/packages/job-worker/src/playout/timings/events.ts @@ -6,7 +6,7 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE const EVENT_WAIT_TIME = 500 -const partInstanceTimingDebounceFunctions = new Map() +const partInstanceTimingDebounceFunctions = new Map() /** * Queue a PartInstanceTimings event to be sent diff --git a/packages/live-status-gateway/Dockerfile b/packages/live-status-gateway/Dockerfile index bf5043af45..42e11dafa6 100644 --- a/packages/live-status-gateway/Dockerfile +++ b/packages/live-status-gateway/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE -FROM node:18 +FROM node:20 WORKDIR /opt COPY package.json lerna.json yarn.lock tsconfig.json ./ @@ -15,7 +15,7 @@ RUN yarn build RUN yarn install --check-files --frozen-lockfile --production --force --ignore-scripts # purge dev-dependencies # DEPLOY IMAGE -FROM node:18-alpine +FROM node:20-alpine RUN apk add --no-cache tzdata COPY --from=0 /opt/package.json /opt/package.json diff --git a/packages/live-status-gateway/Dockerfile.circle b/packages/live-status-gateway/Dockerfile.circle index 974fead09d..637941030a 100644 --- a/packages/live-status-gateway/Dockerfile.circle +++ b/packages/live-status-gateway/Dockerfile.circle @@ -1,4 +1,4 @@ -FROM node:18-alpine +FROM node:20-alpine RUN apk add --no-cache tzdata COPY package.json /opt/ diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index e5d15d6575..bf768b847e 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -37,7 +37,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": "^14.18 || ^16.14 || ^18.5" + "node": ">=20.18" }, "keywords": [ "broadcast", diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index e66255cdfe..0abbc9d479 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -30,7 +30,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": "^14.19 || ^16.14 || ^18.12" + "node": ">=20.18" }, "files": [ "/dist", diff --git a/packages/mos-gateway/Dockerfile b/packages/mos-gateway/Dockerfile index 1efa36de62..fe28949d7a 100644 --- a/packages/mos-gateway/Dockerfile +++ b/packages/mos-gateway/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE -FROM node:18 +FROM node:20 WORKDIR /opt COPY . . @@ -13,7 +13,7 @@ RUN yarn plugin import workspace-tools RUN yarn workspaces focus mos-gateway --production # purge dev-dependencies # DEPLOY IMAGE -FROM node:18-alpine +FROM node:20-alpine RUN apk add --no-cache tzdata COPY --from=0 /opt/package.json /opt/package.json diff --git a/packages/mos-gateway/Dockerfile.circle b/packages/mos-gateway/Dockerfile.circle index 8f2d445450..10373c3df9 100644 --- a/packages/mos-gateway/Dockerfile.circle +++ b/packages/mos-gateway/Dockerfile.circle @@ -1,4 +1,4 @@ -FROM node:18-alpine +FROM node:20-alpine RUN apk add --no-cache tzdata COPY package.json /opt/ diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index c9e13e2fa4..3b93188b06 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -48,7 +48,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": "^14.19 || ^16.14 || ^18.12" + "node": ">=20.18" }, "keywords": [ "mos", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 474841a024..78e975d6f4 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -28,7 +28,7 @@ }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "engines": { - "node": ">=16.0.0" + "node": ">=20.18" }, "files": [ "/api", diff --git a/packages/package.json b/packages/package.json index 13435f2a65..fbe2eeb83b 100644 --- a/packages/package.json +++ b/packages/package.json @@ -14,7 +14,7 @@ ] }, "scripts": { - "prepare": "(node is_node_14.js && lerna run prepare --ignore @sofie-automation/openapi || lerna run prepare)", + "prepare": "lerna run prepare", "postinstall": "cd .. && \"$PROJECT_CWD/node_modules/.bin/husky\" install", "build": "lerna run build --ignore @sofie-automation/openapi", "build:try": "lerna run --no-bail build --ignore @sofie-automation/openapi", @@ -45,7 +45,7 @@ "@types/ejson": "^2.2.2", "@types/got": "^9.6.12", "@types/jest": "^29.5.11", - "@types/node": "^14.18.63", + "@types/node": "^20.17.6", "@types/node-fetch": "^2.6.11", "@types/object-path": "^0.11.4", "@types/underscore": "^1.11.15", diff --git a/packages/playout-gateway/Dockerfile b/packages/playout-gateway/Dockerfile index cf3d4516fd..604a358748 100644 --- a/packages/playout-gateway/Dockerfile +++ b/packages/playout-gateway/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE -FROM node:18 +FROM node:20 WORKDIR /opt COPY . . @@ -13,7 +13,7 @@ RUN yarn plugin import workspace-tools RUN yarn workspaces focus playout-gateway --production # purge dev-dependencies # DEPLOY IMAGE -FROM node:18-alpine +FROM node:20-alpine RUN apk add --no-cache tzdata COPY --from=0 /opt/package.json /opt/package.json diff --git a/packages/playout-gateway/Dockerfile.circle b/packages/playout-gateway/Dockerfile.circle index 3dc8d9470e..1d2821e54d 100644 --- a/packages/playout-gateway/Dockerfile.circle +++ b/packages/playout-gateway/Dockerfile.circle @@ -1,4 +1,4 @@ -FROM node:18-alpine +FROM node:20-alpine RUN apk add --no-cache tzdata COPY package.json /opt/ diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index cb6bfdc773..a4864865f4 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -40,7 +40,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": "^14.19 || ^16.14 || ^18.12" + "node": ">=20.18" }, "keywords": [ "broadcast", diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index 40def38103..f1c647977d 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -828,7 +828,7 @@ export class TSRHandler { } private changedResults: PeripheralDeviceAPI.PlayoutChangedResults | undefined = undefined - private sendCallbacksTimeout: NodeJS.Timer | undefined = undefined + private sendCallbacksTimeout: NodeJS.Timeout | undefined = undefined private sendChangedResults = (): void => { this.sendCallbacksTimeout = undefined diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 98605c6d68..9b2a3cc289 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -48,7 +48,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=14.19" + "node": ">=20.18" }, "files": [ "/dist", diff --git a/packages/server-core-integration/src/lib/methods.ts b/packages/server-core-integration/src/lib/methods.ts index d20d2248e9..9c2c616d50 100644 --- a/packages/server-core-integration/src/lib/methods.ts +++ b/packages/server-core-integration/src/lib/methods.ts @@ -53,7 +53,7 @@ interface QueuedMethodCall { } export class ConnectionMethodsQueue { - private _triggerDoQueueTimer: NodeJS.Timer | null = null + private _triggerDoQueueTimer: NodeJS.Timeout | null = null private _timeLastMethodCall = 0 private _timeLastMethodReply = 0 private queuedMethodCalls: Array = [] diff --git a/packages/server-core-integration/src/lib/ping.ts b/packages/server-core-integration/src/lib/ping.ts index 56c8eb1cf2..3c00502083 100644 --- a/packages/server-core-integration/src/lib/ping.ts +++ b/packages/server-core-integration/src/lib/ping.ts @@ -1,5 +1,5 @@ export class CorePinger { - private _pingTimeout: NodeJS.Timer | null = null + private _pingTimeout: NodeJS.Timeout | null = null private _connected = false private _destroyed = false diff --git a/packages/server-core-integration/src/lib/watchDog.ts b/packages/server-core-integration/src/lib/watchDog.ts index 1809058737..17fc1e59a8 100644 --- a/packages/server-core-integration/src/lib/watchDog.ts +++ b/packages/server-core-integration/src/lib/watchDog.ts @@ -15,8 +15,8 @@ export type WatchDogEvents = { */ export class WatchDog extends EventEmitter { public timeout: number - private _checkTimeout: NodeJS.Timer | null = null - private _dieTimeout: NodeJS.Timer | null = null + private _checkTimeout: NodeJS.Timeout | null = null + private _dieTimeout: NodeJS.Timeout | null = null private _watching = false private _checkFunctions: WatchDogCheckFunction[] = [] private _runningChecks = false diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 40c506ca0c..ca62e23353 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -29,7 +29,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=14.19" + "node": ">=20.18" }, "files": [ "/dist", diff --git a/packages/webui/.eslintrc.cjs b/packages/webui/.eslintrc.cjs index 17d7af8b27..f2437f7255 100644 --- a/packages/webui/.eslintrc.cjs +++ b/packages/webui/.eslintrc.cjs @@ -70,7 +70,6 @@ const tsBase = { allowModules: ['meteor', 'mongodb'], }, ], - 'jest/no-standalone-expect': 'off', // testInFiber confuses the rule ...tmpRules, 'react/react-in-jsx-scope': 'off', diff --git a/packages/webui/package.json b/packages/webui/package.json index a0761dac9f..90458dd48e 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -108,6 +108,6 @@ "xml2js": "^0.6.2" }, "engines": { - "node": ">=18" + "node": ">=20.18" } } diff --git a/packages/webui/src/__mocks__/mongo.ts b/packages/webui/src/__mocks__/mongo.ts index 160a603b46..2f31d6400b 100644 --- a/packages/webui/src/__mocks__/mongo.ts +++ b/packages/webui/src/__mocks__/mongo.ts @@ -246,7 +246,7 @@ export namespace MongoMock { return docs.length } - _ensureIndex(_obj: any) { + createIndex(_obj: any) { // todo } allow() { diff --git a/packages/webui/src/client/lib/data/mos/__tests__/plugin-support.test.ts b/packages/webui/src/client/lib/data/mos/__tests__/plugin-support.test.ts index 91fdfb7d94..e6e8dadc32 100644 --- a/packages/webui/src/client/lib/data/mos/__tests__/plugin-support.test.ts +++ b/packages/webui/src/client/lib/data/mos/__tests__/plugin-support.test.ts @@ -42,6 +42,7 @@ describe('createMosAppInfoXmlString', () => { let ncsAppInfo: any beforeAll(async () => { ncsAppInfo = mos.ncsAppInfo + // eslint-disable-next-line jest/no-standalone-expect expect(ncsAppInfo).toHaveLength(1) ncsAppInfo = ncsAppInfo[0] }) @@ -49,6 +50,7 @@ describe('createMosAppInfoXmlString', () => { let ncsInformation: any beforeAll(async () => { ncsInformation = ncsAppInfo.ncsInformation + // eslint-disable-next-line jest/no-standalone-expect expect(ncsInformation).toHaveLength(1) ncsInformation = ncsInformation[0] }) diff --git a/packages/webui/src/client/lib/viewPort.ts b/packages/webui/src/client/lib/viewPort.ts index 61edc9e6e5..30ecfed3b7 100644 --- a/packages/webui/src/client/lib/viewPort.ts +++ b/packages/webui/src/client/lib/viewPort.ts @@ -9,7 +9,7 @@ import { logger } from './logging' const HEADER_MARGIN = 24 // TODOSYNC: TV2 uses 15. If it's needed to be different, it needs to be made generic somehow.. const FALLBACK_HEADER_HEIGHT = 65 -let focusInterval: NodeJS.Timer | undefined +let focusInterval: NodeJS.Timeout | undefined let _dontClearInterval = false export function maintainFocusOnPartInstance( diff --git a/packages/webui/src/client/ui/Prompter/PrompterView.tsx b/packages/webui/src/client/ui/Prompter/PrompterView.tsx index 0a473fa970..535881934e 100644 --- a/packages/webui/src/client/ui/Prompter/PrompterView.tsx +++ b/packages/webui/src/client/ui/Prompter/PrompterView.tsx @@ -636,7 +636,7 @@ const PrompterContent = withTranslation()( Translated & IPrompterTrackedProps>, {} > { - private _debounceUpdate: NodeJS.Timer | undefined + private _debounceUpdate: NodeJS.Timeout | undefined constructor(props: Translated & IPrompterTrackedProps>) { super(props) diff --git a/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx b/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx index 1d8f31ce31..a34cffc5a0 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx @@ -245,7 +245,7 @@ export class SegmentTimelinePartClass extends React.Component { if (e && e.partId === this.props.part.partId && !e.pieceId) { diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index d360961568..57a64c969e 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -293,7 +293,7 @@ export class SegmentTimelineClass extends React.Component { if (e.segmentId === this.props.segment._id && !e.partId && !e.pieceId) { diff --git a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx index 5e45667218..94b71a6590 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SourceLayerItem.tsx @@ -411,7 +411,7 @@ export const SourceLayerItem = withTranslation()( // } // } - private highlightTimeout: NodeJS.Timer | undefined + private highlightTimeout: NodeJS.Timeout | undefined private onHighlight = (e: HighlightEvent) => { if ( diff --git a/packages/webui/src/client/ui/Shelf/TimelineDashboardPanel.tsx b/packages/webui/src/client/ui/Shelf/TimelineDashboardPanel.tsx index 79512a894d..da19f6b97e 100644 --- a/packages/webui/src/client/ui/Shelf/TimelineDashboardPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/TimelineDashboardPanel.tsx @@ -39,7 +39,7 @@ export const TimelineDashboardPanel = React.memo( const TimelineDashboardPanelContent = withTranslation()( class TimelineDashboardPanelContent extends DashboardPanelInner { liveLine: HTMLDivElement | null = null - scrollIntoViewTimeout: NodeJS.Timer | undefined = undefined + scrollIntoViewTimeout: NodeJS.Timeout | undefined = undefined constructor(props: Translated) { super(props) diff --git a/packages/webui/src/meteor/meteor.js b/packages/webui/src/meteor/meteor.js index 7213f22d1c..ada0224220 100644 --- a/packages/webui/src/meteor/meteor.js +++ b/packages/webui/src/meteor/meteor.js @@ -3,13 +3,6 @@ const Meteor = { console.debug(...args) }, - _suppressed_log_expected: () => { - return true - }, - _suppress_log: (i) => { - // - }, - _setImmediate: (cb) => { return setTimeout(cb, 0) }, diff --git a/packages/webui/src/meteor/tracker.js b/packages/webui/src/meteor/tracker.js index 632352b1a5..297ff7fc66 100644 --- a/packages/webui/src/meteor/tracker.js +++ b/packages/webui/src/meteor/tracker.js @@ -51,18 +51,6 @@ function _debugFunc() { : function () {} } -function _maybeSuppressMoreLogs(messagesLength) { - // Sometimes when running tests, we intentionally suppress logs on expected - // printed errors. Since the current implementation of _throwOrLog can log - // multiple separate log messages, suppress all of them if at least one suppress - // is expected as we still want them to count as one. - if (typeof Meteor !== 'undefined') { - if (Meteor._suppressed_log_expected()) { - Meteor._suppress_log(messagesLength - 1) - } - } -} - function _throwOrLog(from, e) { if (throwFirstError) { throw e @@ -78,7 +66,6 @@ function _throwOrLog(from, e) { } } printArgs.push(e.stack) - _maybeSuppressMoreLogs(printArgs.length) for (var i = 0; i < printArgs.length; i++) { _debugFunc()(printArgs[i]) diff --git a/packages/yarn.lock b/packages/yarn.lock index 194ac02054..a56b8fdcd7 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -2637,21 +2637,19 @@ __metadata: languageName: node linkType: hard -"@elastic/ecs-helpers@npm:^1.1.0": - version: 1.1.0 - resolution: "@elastic/ecs-helpers@npm:1.1.0" - dependencies: - fast-json-stringify: ^2.4.1 - checksum: 8f64e86fe3cfe67540fd8c2a62e0b7db1f4e8cab8c4a63e2f49ce295c3d3b629d0f3363b0107fe530650898a89b5b1d86190717447a481932856651911e6ef61 +"@elastic/ecs-helpers@npm:^2.1.1": + version: 2.1.1 + resolution: "@elastic/ecs-helpers@npm:2.1.1" + checksum: 80db727963e26a28312c67e47cbc40bfe5441ff8937dc27157a7f968fcd20475345a19a6588cc1c6b97b2eeaf5990f2285eaf2f03e206f3c2cd9965160d3fdaa languageName: node linkType: hard -"@elastic/ecs-pino-format@npm:^1.2.0": - version: 1.3.0 - resolution: "@elastic/ecs-pino-format@npm:1.3.0" +"@elastic/ecs-pino-format@npm:^1.5.0": + version: 1.5.0 + resolution: "@elastic/ecs-pino-format@npm:1.5.0" dependencies: - "@elastic/ecs-helpers": ^1.1.0 - checksum: 1543b80b84e3f35b6be35b73b5d0153d267c357df750ba77af2c8d7b07097df8095f104d283e46b6500c902815327a1ad0005aa2e3855afbddbac0b683b72c6c + "@elastic/ecs-helpers": ^2.1.1 + checksum: e66a1801ecafa5d1f56037df8dafa9da9c302440c8254c636374e489a5a50e85166b77c951c1f8d3edd99cb77f43b5967c3f31a68cac23b2a4b95a87ce72b066 languageName: node linkType: hard @@ -5216,7 +5214,7 @@ __metadata: "@sofie-automation/shared-lib": 1.52.0-in-development amqplib: ^0.10.3 deepmerge: ^4.3.1 - elastic-apm-node: ^3.51.0 + elastic-apm-node: ^4.8.0 eventemitter3: ^4.0.7 jest-mock-extended: ^3.0.5 mongodb: ^5.9.2 @@ -6467,19 +6465,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0": - version: 20.11.22 - resolution: "@types/node@npm:20.11.22" +"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:^20.17.6": + version: 20.17.6 + resolution: "@types/node@npm:20.17.6" dependencies: - undici-types: ~5.26.4 - checksum: ef8fd0b561c3c9810f3c23c990c856619232934e54308c84e79d4e39d44b66668eceb6eca89c64ebcbc78fb514446661ad58b0f8e6b5fa3d9ed9ff0983aac4ef - languageName: node - linkType: hard - -"@types/node@npm:^14.18.63": - version: 14.18.63 - resolution: "@types/node@npm:14.18.63" - checksum: be909061a54931778c71c49dc562586c32f909c4b6197e3d71e6dac726d8bd9fccb9f599c0df99f52742b68153712b5097c0f00cac4e279fa894b0ea6719a8fd + undici-types: ~6.19.2 + checksum: d51dbb9881c94d0310b32b5fd8013e3261595c61bc888fa27258469c93c3dc0b3c4d20a9f28f3f5f79562f6737e28e7f3dd04940dc8b4d966d34aaf318f7f69b languageName: node linkType: hard @@ -7367,6 +7358,15 @@ __metadata: languageName: node linkType: hard +"acorn-import-attributes@npm:^1.9.5": + version: 1.9.5 + resolution: "acorn-import-attributes@npm:1.9.5" + peerDependencies: + acorn: ^8 + checksum: 1c0c49b6a244503964ae46ae850baccf306e84caf99bc2010ed6103c69a423987b07b520a6c619f075d215388bd4923eccac995886a54309eda049ab78a4be95 + languageName: node + linkType: hard + "acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -7547,7 +7547,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.12.2, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -8027,15 +8027,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"async-cache@npm:^1.1.0": - version: 1.1.0 - resolution: "async-cache@npm:1.1.0" - dependencies: - lru-cache: ^4.0.0 - checksum: 3f55cc78b3ddc745b6604dd144fc7bca2e21c7ba4c5ea18d312234dc625133511723dff6c71b2283582421f95d591bdb24bf89ce4c4869151e4ecedbdad4acf2 - languageName: node - linkType: hard - "async-value-promise@npm:^1.1.1": version: 1.1.1 resolution: "async-value-promise@npm:1.1.1" @@ -8430,6 +8421,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + "bin-links@npm:^3.0.3": version: 3.0.3 resolution: "bin-links@npm:3.0.3" @@ -9997,13 +9995,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cookie@npm:0.5.0, cookie@npm:^0.5.0": +"cookie@npm:0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 languageName: node linkType: hard +"cookie@npm:^0.7.1": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e + languageName: node + linkType: hard + "copy-text-to-clipboard@npm:^3.2.0": version: 3.2.0 resolution: "copy-text-to-clipboard@npm:3.2.0" @@ -11792,21 +11797,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"elastic-apm-node@npm:^3.51.0": - version: 3.51.0 - resolution: "elastic-apm-node@npm:3.51.0" +"elastic-apm-node@npm:^4.8.0": + version: 4.8.0 + resolution: "elastic-apm-node@npm:4.8.0" dependencies: - "@elastic/ecs-pino-format": ^1.2.0 + "@elastic/ecs-pino-format": ^1.5.0 "@opentelemetry/api": ^1.4.1 "@opentelemetry/core": ^1.11.0 "@opentelemetry/sdk-metrics": ^1.12.0 after-all-results: ^2.0.0 agentkeepalive: ^4.2.1 - async-cache: ^1.1.0 async-value-promise: ^1.1.1 basic-auth: ^2.0.1 breadth-filter: ^2.0.0 - cookie: ^0.5.0 + cookie: ^0.7.1 core-util-is: ^1.0.2 end-of-stream: ^1.4.4 error-callsites: ^2.0.4 @@ -11815,26 +11819,26 @@ asn1@evs-broadcast/node-asn1: fast-safe-stringify: ^2.0.7 fast-stream-to-buffer: ^1.0.0 http-headers: ^3.0.2 - import-in-the-middle: 1.4.2 - is-native: ^1.0.1 - lru-cache: ^6.0.0 + import-in-the-middle: 1.11.2 + json-bigint: ^1.0.0 + lru-cache: 10.2.0 measured-reporting: ^1.51.1 module-details-from-path: ^1.0.3 monitor-event-loop-delay: ^1.0.0 object-filter-sequence: ^1.0.0 object-identity-map: ^1.0.2 original-url: ^1.2.3 - pino: ^6.11.2 - readable-stream: ^3.4.0 + pino: ^8.15.0 + readable-stream: ^3.6.2 relative-microtime: ^2.0.0 require-in-the-middle: ^7.1.1 - semver: ^6.3.1 + semver: ^7.5.4 shallow-clone-shim: ^2.0.0 source-map: ^0.8.0-beta.0 sql-summary: ^1.0.1 stream-chopper: ^3.0.1 unicode-byte-truncate: ^1.0.0 - checksum: e6a801e731d6a5178e7450c76e88b9a519823129986365b2f59c4ee8e02c2a0e624deacebce6e85ae0964f6ea876a9f562901ca0b3538f4b0452a24d7f1b0303 + checksum: 4c6534481540b08412096ff192c67b8dcc9501672b57bcc2f6ad159e84a61f5c32a63c6a7edfe8fb289a6854e33e1278e0aa04b5cadcbb1c9cc51325761be45d languageName: node linkType: hard @@ -13116,18 +13120,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fast-json-stringify@npm:^2.4.1": - version: 2.7.13 - resolution: "fast-json-stringify@npm:2.7.13" - dependencies: - ajv: ^6.11.0 - deepmerge: ^4.2.2 - rfdc: ^1.2.0 - string-similarity: ^4.0.1 - checksum: f78ab25047c790de5b521c369e0b18c595055d48a106add36e9f86fe45be40226f168ff4708a226e187d0b46f1d6b32129842041728944bd9a03ca5efbbe4ccb - languageName: node - linkType: hard - "fast-levenshtein@npm:^2.0.6": version: 2.0.6 resolution: "fast-levenshtein@npm:2.0.6" @@ -13142,14 +13134,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fast-redact@npm:^3.0.0": - version: 3.3.0 - resolution: "fast-redact@npm:3.3.0" - checksum: 3f7becc70a5a2662a9cbfdc52a4291594f62ae998806ee00315af307f32d9559dbf512146259a22739ee34401950ef47598c1f4777d33b0ed5027203d67f549c +"fast-redact@npm:^3.1.1": + version: 3.5.0 + resolution: "fast-redact@npm:3.5.0" + checksum: ef03f0d1849da074a520a531ad299bf346417b790a643931ab4e01cb72275c8d55b60dc8512fb1f1818647b696790edefaa96704228db9f012da935faa1940af languageName: node linkType: hard -"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.0.8": +"fast-safe-stringify@npm:2.1.1, fast-safe-stringify@npm:^2.0.7": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: a851cbddc451745662f8f00ddb622d6766f9bd97642dabfd9a405fb0d646d69fc0b9a1243cbf67f5f18a39f40f6fa821737651ff1bceeba06c9992ca2dc5bd3d @@ -13426,13 +13418,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"flatstr@npm:^1.0.12": - version: 1.0.12 - resolution: "flatstr@npm:1.0.12" - checksum: e1bb562c94b119e958bf37e55738b172b5f8aaae6532b9660ecd877779f8559dbbc89613ba6b29ccc13447e14c59277d41450f785cf75c30df9fce62f459e9a8 - languageName: node - linkType: hard - "flatted@npm:^3.2.7": version: 3.2.9 resolution: "flatted@npm:3.2.9" @@ -15129,15 +15114,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"import-in-the-middle@npm:1.4.2": - version: 1.4.2 - resolution: "import-in-the-middle@npm:1.4.2" +"import-in-the-middle@npm:1.11.2": + version: 1.11.2 + resolution: "import-in-the-middle@npm:1.11.2" dependencies: acorn: ^8.8.2 - acorn-import-assertions: ^1.9.0 + acorn-import-attributes: ^1.9.5 cjs-module-lexer: ^1.2.2 module-details-from-path: ^1.0.3 - checksum: 52971f821e9a3c94834cd5cf0ab5178321c07d4f4babd547b3cb24c4de21670d05b42ca1523890e7e90525c3bba6b7db7e54cf45421919b0b2712a34faa96ea5 + checksum: 06fb73100a918e00778779713119236cc8d3d4656aae9076a18159cfcd28eb0cc26e0a5040d11da309c5f8f8915c143b8d74e73c0734d3f5549b1813d1008bb9 languageName: node linkType: hard @@ -15686,16 +15671,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-native@npm:^1.0.1": - version: 1.0.1 - resolution: "is-native@npm:1.0.1" - dependencies: - is-nil: ^1.0.0 - to-source-code: ^1.0.0 - checksum: 4967af8c4d7a06076cb16ef70fba5a5a2b61ef0a83d4d5dce437cf4c6b5315255cccf07db37d487bcdf2f0ded86edb166a62c46a712cfda1227532b70015029c - languageName: node - linkType: hard - "is-negative-zero@npm:^2.0.3": version: 2.0.3 resolution: "is-negative-zero@npm:2.0.3" @@ -15703,13 +15678,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-nil@npm:^1.0.0": - version: 1.0.1 - resolution: "is-nil@npm:1.0.1" - checksum: e5b89c3b82068e719372381c5aaa5f3f28d09e6d501d7f7e4365f136433de1ae92f9f82eeedcb3c3282da1ccf374aad46cc06feab2647d2820067c4a35484760 - languageName: node - linkType: hard - "is-npm@npm:^6.0.0": version: 6.0.0 resolution: "is-npm@npm:6.0.0" @@ -16834,6 +16802,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: ^9.0.0 + checksum: c67bb93ccb3c291e60eb4b62931403e378906aab113ec1c2a8dd0f9a7f065ad6fd9713d627b732abefae2e244ac9ce1721c7a3142b2979532f12b258634ce6f6 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -17699,13 +17676,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lru-cache@npm:^4.0.0": - version: 4.1.5 - resolution: "lru-cache@npm:4.1.5" - dependencies: - pseudomap: ^1.0.2 - yallist: ^2.1.2 - checksum: 4bb4b58a36cd7dc4dcec74cbe6a8f766a38b7426f1ff59d4cf7d82a2aa9b9565cd1cb98f6ff60ce5cd174524868d7bc9b7b1c294371851356066ca9ac4cf135a +"lru-cache@npm:10.2.0, lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: eee7ddda4a7475deac51ac81d7dd78709095c6fa46e8350dc2d22462559a1faa3b81ed931d5464b13d48cbd7e08b46100b6f768c76833912bc444b99c37e25db languageName: node linkType: hard @@ -17734,13 +17708,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.1 - resolution: "lru-cache@npm:10.0.1" - checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 - languageName: node - linkType: hard - "lru-queue@npm:^0.1.0": version: 0.1.0 resolution: "lru-queue@npm:0.1.0" @@ -20583,6 +20550,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 6ce7acdc7b9ceb51cf029b5239cbf41937ee4c8dcd9d4e475e1777b41702564d46caa1150a744e00da0ac6d923ab83471646a39a4470f97481cf6e2d8d253c3f + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -21052,7 +21026,7 @@ asn1@evs-broadcast/node-asn1: "@types/ejson": ^2.2.2 "@types/got": ^9.6.12 "@types/jest": ^29.5.11 - "@types/node": ^14.18.63 + "@types/node": ^20.17.6 "@types/node-fetch": ^2.6.11 "@types/object-path": ^0.11.4 "@types/underscore": ^1.11.15 @@ -21625,27 +21599,41 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pino-std-serializers@npm:^3.1.0": - version: 3.2.0 - resolution: "pino-std-serializers@npm:3.2.0" - checksum: 77e29675b116e42ae9fe6d4ef52ef3a082ffc54922b122d85935f93ddcc20277f0b0c873c5c6c5274a67b0409c672aaae3de6bcea10a2d84699718dda55ba95b +"pino-abstract-transport@npm:^1.2.0": + version: 1.2.0 + resolution: "pino-abstract-transport@npm:1.2.0" + dependencies: + readable-stream: ^4.0.0 + split2: ^4.0.0 + checksum: 3336c51fb91ced5ef8a4bfd70a96e41eb6deb905698e83350dc71eedffb34795db1286d2d992ce1da2f6cd330a68be3f7e2748775a6b8a2ee3416796070238d6 languageName: node linkType: hard -"pino@npm:^6.11.2": - version: 6.14.0 - resolution: "pino@npm:6.14.0" +"pino-std-serializers@npm:^6.0.0": + version: 6.2.2 + resolution: "pino-std-serializers@npm:6.2.2" + checksum: aeb0662edc46ec926de9961ed4780a4f0586bb7c37d212cd469c069639e7816887a62c5093bc93f260a4e0900322f44fc8ab1343b5a9fa2864a888acccdb22a4 + languageName: node + linkType: hard + +"pino@npm:^8.15.0": + version: 8.21.0 + resolution: "pino@npm:8.21.0" dependencies: - fast-redact: ^3.0.0 - fast-safe-stringify: ^2.0.8 - flatstr: ^1.0.12 - pino-std-serializers: ^3.1.0 - process-warning: ^1.0.0 + atomic-sleep: ^1.0.0 + fast-redact: ^3.1.1 + on-exit-leak-free: ^2.1.0 + pino-abstract-transport: ^1.2.0 + pino-std-serializers: ^6.0.0 + process-warning: ^3.0.0 quick-format-unescaped: ^4.0.3 - sonic-boom: ^1.0.2 + real-require: ^0.2.0 + safe-stable-stringify: ^2.3.1 + sonic-boom: ^3.7.0 + thread-stream: ^2.6.0 bin: pino: bin.js - checksum: eb13e12e3a3d682abe4a4da426455a9f4e041e55e4fa57d72d9677ee8d188a9c952f69347e728a3761c8262cdce76ef24bee29e1a53ab15aa9c5e851099163d0 + checksum: d895c37cfcb7ade33ad7ac4ca54c0497ab719ec726e42b7c7b9697e07572a09a7c7de18d751440769c3ea5ecbac2075fdac720cf182720a4764defe3de8a1411 languageName: node linkType: hard @@ -22298,10 +22286,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"process-warning@npm:^1.0.0": - version: 1.0.0 - resolution: "process-warning@npm:1.0.0" - checksum: c708a03241deec3cabaeee39c4f9ee8c4d71f1c5ef9b746c8252cdb952a6059068cfcdaf348399775244cbc441b6ae5e26a9c87ed371f88335d84f26d19180f9 +"process-warning@npm:^3.0.0": + version: 3.0.0 + resolution: "process-warning@npm:3.0.0" + checksum: 1fc2eb4524041de3c18423334cc8b4e36bec5ad5472640ca1a936122c6e01da0864c1a4025858ef89aea93eabe7e77db93ccea225b10858617821cb6a8719efe languageName: node linkType: hard @@ -22477,13 +22465,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pseudomap@npm:^1.0.2": - version: 1.0.2 - resolution: "pseudomap@npm:1.0.2" - checksum: 856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 - languageName: node - linkType: hard - "psl@npm:^1.1.33": version: 1.9.0 resolution: "psl@npm:1.9.0" @@ -23402,7 +23383,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.2, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -23428,16 +23409,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"readable-stream@npm:^4.1.0": - version: 4.4.2 - resolution: "readable-stream@npm:4.4.2" +"readable-stream@npm:^4.0.0, readable-stream@npm:^4.1.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" dependencies: abort-controller: ^3.0.0 buffer: ^6.0.3 events: ^3.3.0 process: ^0.11.10 string_decoder: ^1.3.0 - checksum: 6f4063763dbdb52658d22d3f49ca976420e1fbe16bbd241f744383715845350b196a2f08b8d6330f8e219153dff34b140aeefd6296da828e1041a7eab1f20d5e + checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a languageName: node linkType: hard @@ -23490,6 +23471,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: fa060f19f2f447adf678d1376928c76379dce5f72bd334da301685ca6cdcb7b11356813332cc243c88470796bc2e2b1e2917fc10df9143dd93c2ea608694971d + languageName: node + linkType: hard + "rechoir@npm:^0.6.2": version: 0.6.2 resolution: "rechoir@npm:0.6.2" @@ -23998,7 +23986,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rfdc@npm:^1.2.0, rfdc@npm:^1.3.0": +"rfdc@npm:^1.3.0": version: 1.3.0 resolution: "rfdc@npm:1.3.0" checksum: fb2ba8512e43519983b4c61bd3fa77c0f410eff6bae68b08614437bc3f35f91362215f7b4a73cbda6f67330b5746ce07db5dd9850ad3edc91271ad6deea0df32 @@ -24947,13 +24935,12 @@ asn1@evs-broadcast/node-asn1: languageName: unknown linkType: soft -"sonic-boom@npm:^1.0.2": - version: 1.4.1 - resolution: "sonic-boom@npm:1.4.1" +"sonic-boom@npm:^3.7.0": + version: 3.8.1 + resolution: "sonic-boom@npm:3.8.1" dependencies: atomic-sleep: ^1.0.0 - flatstr: ^1.0.12 - checksum: 189fa8fe5c2dc05d3513fc1a4926a2f16f132fa6fa0b511745a436010cdcd9c1d3b3cb6a9d7c05bd32a965dc77673a5ac0eb0992e920bdedd16330d95323124f + checksum: 79c90d7a2f928489fd3d4b68d8f8d747a426ca6ccf83c3b102b36f899d4524463dd310982ab7ab6d6bcfd34b7c7c281ad25e495ad71fbff8fd6fa86d6273fc6b languageName: node linkType: hard @@ -25159,6 +25146,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 05d54102546549fe4d2455900699056580cca006c0275c334611420f854da30ac999230857a85fdd9914dc2109ae50f80fda43d2a445f2aa86eccdc1dfce779d + languageName: node + linkType: hard + "split@npm:^1.0.0": version: 1.0.1 resolution: "split@npm:1.0.1" @@ -25344,13 +25338,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"string-similarity@npm:^4.0.1": - version: 4.0.4 - resolution: "string-similarity@npm:4.0.4" - checksum: 797b41b24e1eb6b3b0ab896950b58c295a19a82933479c75f7b5279ffb63e0b456a8c8d10329c02f607ca1a50370e961e83d552aa468ff3b0fa15809abc9eff7 - languageName: node - linkType: hard - "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -25976,6 +25963,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"thread-stream@npm:^2.6.0": + version: 2.7.0 + resolution: "thread-stream@npm:2.7.0" + dependencies: + real-require: ^0.2.0 + checksum: 75ab019cda628344c7779e5f5a88f7759764efd29d320327ad2e6c2622778b5f1c43a3966d76a9ee5744086d61c680b413548f5521030f9e9055487684436165 + languageName: node + linkType: hard + "threadedclass@npm:^1.2.1, threadedclass@npm:^1.2.2": version: 1.2.2 resolution: "threadedclass@npm:1.2.2" @@ -26164,15 +26160,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"to-source-code@npm:^1.0.0": - version: 1.0.2 - resolution: "to-source-code@npm:1.0.2" - dependencies: - is-nil: ^1.0.0 - checksum: 24fd24767f185ad11f81c1e020c2f789fba29471195227731530ec39b2697bb680c16e1f6f7d0d68bffba81e3d95e68dd6014f8c88371399bddcf8c4ad036de3 - languageName: node - linkType: hard - "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -26847,10 +26834,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"undici-types@npm:~5.26.4": - version: 5.26.5 - resolution: "undici-types@npm:5.26.5" - checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 languageName: node linkType: hard @@ -28356,13 +28343,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"yallist@npm:^2.1.2": - version: 2.1.2 - resolution: "yallist@npm:2.1.2" - checksum: 9ba99409209f485b6fcb970330908a6d41fa1c933f75e08250316cce19383179a6b70a7e0721b89672ebb6199cc377bf3e432f55100da6a7d6e11902b0a642cb - languageName: node - linkType: hard - "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" diff --git a/scripts/fixTestFibers.js b/scripts/fixTestFibers.js deleted file mode 100644 index 2cad9c0e6b..0000000000 --- a/scripts/fixTestFibers.js +++ /dev/null @@ -1,21 +0,0 @@ -// Fix fibers implementation, so it runs with Jest: - -const fs = require('fs') - -const filePath = './node_modules/fibers-npm/fibers.js' - -const stringToInsert = '\t\tif (process.env.JEST_WORKER_ID !== undefined ) modPath += \'.node\'' -const insertLineNumber = 13 - -const lines = fs.readFileSync(filePath).toString().split('\n') - -// Insert line: -if (lines[insertLineNumber].trim() !== stringToInsert.trim() ) { - console.log(`Inserting Jest-fix line into ${filePath}`) - lines.splice(insertLineNumber, 0, stringToInsert) -} -const text = lines.join('\n') - -fs.writeFile(filePath, text, function (err) { - if (err) return console.log(err) -}) diff --git a/scripts/run.mjs b/scripts/run.mjs index ee59438a36..344814f7ba 100644 --- a/scripts/run.mjs +++ b/scripts/run.mjs @@ -31,14 +31,15 @@ function watchWorker() { function watchMeteor() { return [ { - command: "meteor npm run watch-types --preserveWatchOutput", + command: "yarn watch-types --preserveWatchOutput", cwd: "meteor", name: "METEOR-TSC", prefixColor: "blue", }, { - command: `meteor npm run debug${config.inspectMeteor ? " --inspect" : ""}${config.verbose ? " --verbose" : "" - }`, + command: `yarn debug${config.inspectMeteor ? " --inspect" : ""}${ + config.verbose ? " --verbose" : "" + }`, cwd: "meteor", name: "METEOR", prefixColor: "cyan", diff --git a/sonar-project.properties b/sonar-project.properties index f07f1552af..3811d59372 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,7 +11,7 @@ sonar.organization=nrkno # Encoding of the source code. Default is default system encoding #sonar.sourceEncoding=UTF-8 -sonar.exclusions=meteor/__mocks__/check/**,meteor/eslint-rules/*.js,packages/documentation/** +sonar.exclusions=meteor/__mocks__/check/**,packages/documentation/** sonar.issue.ignore.multicriteria=ternary,todo,nullish,redundantalias,switchstatement3cases,preferoptionalchain From 61b6854261a03f04e81416b9d37ee995e3a10108 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 25 Nov 2024 12:18:21 +0000 Subject: [PATCH 02/18] feat: update meteor to 3.1 and node to 22 --- .github/actions/setup-meteor/action.yaml | 2 +- .github/workflows/node.yaml | 16 ++++---- .github/workflows/prerelease-libs.yml | 2 +- .node-version | 2 +- meteor/.meteor/release | 2 +- meteor/.meteor/versions | 31 +++++++-------- meteor/Dockerfile | 8 ++-- meteor/Dockerfile.circle | 2 +- meteor/package.json | 4 +- meteor/yarn.lock | 39 +++++-------------- package.json | 2 +- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 2 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 2 +- packages/live-status-gateway/Dockerfile | 4 +- .../live-status-gateway/Dockerfile.circle | 2 +- packages/live-status-gateway/package.json | 2 +- packages/meteor-lib/package.json | 2 +- packages/mos-gateway/Dockerfile | 4 +- packages/mos-gateway/Dockerfile.circle | 2 +- packages/mos-gateway/package.json | 2 +- packages/openapi/package.json | 2 +- packages/package.json | 2 +- packages/playout-gateway/Dockerfile | 4 +- packages/playout-gateway/Dockerfile.circle | 2 +- packages/playout-gateway/package.json | 2 +- packages/server-core-integration/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/webui/package.json | 2 +- packages/yarn.lock | 14 +++---- 31 files changed, 73 insertions(+), 95 deletions(-) diff --git a/.github/actions/setup-meteor/action.yaml b/.github/actions/setup-meteor/action.yaml index b96960585d..68a7305e4c 100644 --- a/.github/actions/setup-meteor/action.yaml +++ b/.github/actions/setup-meteor/action.yaml @@ -3,5 +3,5 @@ description: "Setup Meteor" runs: using: "composite" steps: - - run: curl "https://install.meteor.com/?release=3.0.4" | sh + - run: curl "https://install.meteor.com/?release=3.1" | sh shell: bash diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 188b49af71..c616662038 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -481,27 +481,27 @@ jobs: - server-core-integration - shared-lib - openapi - node-version: [20.x, 22.x] + node-version: [22.x] include: # include additional configs, to run certain packages only for a certain version of node - - node-version: 20.x + - node-version: 22.x package-name: corelib send-coverage: true - - node-version: 20.x + - node-version: 22.x package-name: job-worker send-coverage: true # No tests for the gateways yet - # - node-version: 20.x + # - node-version: 22.x # package-name: playout-gateway - # - node-version: 20.x + # - node-version: 22.x # package-name: mos-gateway - - node-version: 20.x + - node-version: 22.x package-name: live-status-gateway send-coverage: true - - node-version: 20.x + - node-version: 22.x package-name: webui # manual meteor-lib as it only needs a couple of versions - - node-version: 20.x + - node-version: 22.x package-name: meteor-lib send-coverage: true diff --git a/.github/workflows/prerelease-libs.yml b/.github/workflows/prerelease-libs.yml index 7ca1a31f2a..cfbb129dda 100644 --- a/.github/workflows/prerelease-libs.yml +++ b/.github/workflows/prerelease-libs.yml @@ -53,7 +53,7 @@ jobs: - blueprints-integration - server-core-integration - shared-lib - node-version: [20.x, 22.x] + node-version: [22.x] steps: - uses: actions/checkout@v4 diff --git a/.node-version b/.node-version index 10fef252a9..8b84b727be 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.18 +22.11 diff --git a/meteor/.meteor/release b/meteor/.meteor/release index b1e86a359f..8d20e1a2d3 100644 --- a/meteor/.meteor/release +++ b/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.0.4 +METEOR@3.1 diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions index 6048cd7897..b49eda45ce 100644 --- a/meteor/.meteor/versions +++ b/meteor/.meteor/versions @@ -1,7 +1,7 @@ accounts-base@3.0.3 -accounts-password@3.0.2 +accounts-password@3.0.3 allow-deny@2.0.0 -babel-compiler@7.11.1 +babel-compiler@7.11.2 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 @@ -10,18 +10,18 @@ callback-hook@1.6.0 check@1.4.4 core-runtime@1.0.0 ddp@1.4.2 -ddp-client@3.0.2 +ddp-client@3.0.3 ddp-common@1.4.4 ddp-rate-limiter@1.2.2 -ddp-server@3.0.2 +ddp-server@3.0.3 diff-sequence@1.1.3 dynamic-import@0.7.4 -ecmascript@0.16.9 +ecmascript@0.16.10 ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.2 ecmascript-runtime-server@0.11.1 ejson@1.1.4 -email@3.1.0 +email@3.1.1 facts-base@1.0.2 fetch@0.1.5 geojson-utils@1.0.12 @@ -29,16 +29,16 @@ id-map@1.2.0 inter-process-messaging@0.1.2 localstorage@1.2.1 logging@1.3.5 -meteor@2.0.1 -minimongo@2.0.1 +meteor@2.0.2 +minimongo@2.0.2 modern-browsers@0.1.11 -modules@0.20.2 +modules@0.20.3 modules-runtime@0.13.2 -mongo@2.0.2 -mongo-decimal@0.1.4 +mongo@2.0.3 +mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 -npm-mongo@4.17.4 +npm-mongo@6.10.0 ordered-dict@1.2.0 promise@1.0.0 random@1.2.2 @@ -51,9 +51,8 @@ routepolicy@1.1.2 sha@1.0.10 socket-stream-client@0.5.3 tracker@1.3.4 -typescript@5.4.3 -underscore@1.6.4 -url@1.3.4 -webapp@2.0.3 +typescript@5.6.3 +url@1.3.5 +webapp@2.0.4 webapp-hashing@1.1.2 zodern:types@1.0.13 diff --git a/meteor/Dockerfile b/meteor/Dockerfile index cee205aede..13c52fa295 100644 --- a/meteor/Dockerfile +++ b/meteor/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:experimental # BUILD WEBUI -FROM node:20 +FROM node:22 COPY packages /opt/core/packages WORKDIR /opt/core/packages @@ -14,8 +14,8 @@ RUN yarn install && yarn build # RUN yarn workspaces focus --production @sofie-automation/job-worker @sofie-automation/corelib # BUILD IMAGE -FROM node:20 -RUN curl "https://install.meteor.com/?release=3.0.4" | sh +FROM node:22 +RUN curl "https://install.meteor.com/?release=3.1" | sh # Temporary change the NODE_ENV env variable, so that all libraries are installed: ENV NODE_ENV_TMP $NODE_ENV @@ -50,7 +50,7 @@ RUN npm install RUN mv /opt/bundle/programs/web.browser/assets /opt/bundle/programs/web.browser/app/assets || true # DEPLOY IMAGE -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata diff --git a/meteor/Dockerfile.circle b/meteor/Dockerfile.circle index 1e39e80f81..8cb4d3971f 100644 --- a/meteor/Dockerfile.circle +++ b/meteor/Dockerfile.circle @@ -1,5 +1,5 @@ # DEPLOY IMAGE -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata diff --git a/meteor/package.json b/meteor/package.json index c8ed1b5128..6b63793d08 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -3,7 +3,7 @@ "version": "1.52.0-in-development", "private": true, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "scripts": { "preinstall": "node -v", @@ -88,7 +88,7 @@ "@types/koa-static": "^4.0.4", "@types/koa__cors": "^5.0.0", "@types/koa__router": "^12.0.4", - "@types/node": "^20.17.6", + "@types/node": "^22.9.3", "@types/request": "^2.48.12", "@types/semver": "^7.5.6", "@types/underscore": "^1.11.15", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index a01cc3dc27..34729e9fbe 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1557,19 +1557,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.0.0": - version: 20.6.3 - resolution: "@types/node@npm:20.6.3" - checksum: 444a6f1f41cfa8d3e20ce0108e6e43960fb2ae0e481f233bb1c14d6252aa63a92e021de561cd317d9fdb411688f871065f40175a1f18763282dee2613a08f8a3 - languageName: node - linkType: hard - -"@types/node@npm:^20.17.6": - version: 20.17.6 - resolution: "@types/node@npm:20.17.6" +"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:^22.9.3": + version: 22.9.3 + resolution: "@types/node@npm:22.9.3" dependencies: - undici-types: ~6.19.2 - checksum: d51dbb9881c94d0310b32b5fd8013e3261595c61bc888fa27258469c93c3dc0b3c4d20a9f28f3f5f79562f6737e28e7f3dd04940dc8b4d966d34aaf318f7f69b + undici-types: ~6.19.8 + checksum: 274cced37a8a11cd89827c551de73980a174e00bef0768c10c1fb7d3887a26b4fade25f870e3fd870432b93546e092cdbe0979e65110c0839982dc2b5938aabc languageName: node linkType: hard @@ -2297,7 +2290,7 @@ __metadata: "@types/koa-static": ^4.0.4 "@types/koa__cors": ^5.0.0 "@types/koa__router": ^12.0.4 - "@types/node": ^20.17.6 + "@types/node": ^22.9.3 "@types/request": ^2.48.12 "@types/semver": ^7.5.6 "@types/underscore": ^1.11.15 @@ -7065,7 +7058,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:10.2.0": +"lru-cache@npm:10.2.0, lru-cache@npm:^9.1.1 || ^10.0.0": version: 10.2.0 resolution: "lru-cache@npm:10.2.0" checksum: eee7ddda4a7475deac51ac81d7dd78709095c6fa46e8350dc2d22462559a1faa3b81ed931d5464b13d48cbd7e08b46100b6f768c76833912bc444b99c37e25db @@ -7097,13 +7090,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.1 - resolution: "lru-cache@npm:10.0.1" - checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 - languageName: node - linkType: hard - "make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -10348,14 +10334,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad - languageName: node - linkType: hard - -"tslib@npm:^2.6.3": +"tslib@npm:^2.5.3, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3": version: 2.7.0 resolution: "tslib@npm:2.7.0" checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 @@ -10571,7 +10550,7 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.2": +"undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 diff --git a/package.json b/package.json index 8a279813ee..95a4ef24bc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "private": true, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "scripts": { "postinstall": "run install:packages && run install:meteor", diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index d32f58ee43..39a9a5eafc 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -29,7 +29,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "files": [ "/dist", diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 52533e7332..dd45f266e6 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -30,7 +30,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "files": [ "/dist", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 7fabd6dc65..50a06e3144 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -15,7 +15,7 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "devDependencies": { "@docusaurus/core": "3.2.1", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 8a7e17f85b..605e132301 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -31,7 +31,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "files": [ "/dist", diff --git a/packages/live-status-gateway/Dockerfile b/packages/live-status-gateway/Dockerfile index 42e11dafa6..00617c036a 100644 --- a/packages/live-status-gateway/Dockerfile +++ b/packages/live-status-gateway/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE -FROM node:20 +FROM node:22 WORKDIR /opt COPY package.json lerna.json yarn.lock tsconfig.json ./ @@ -15,7 +15,7 @@ RUN yarn build RUN yarn install --check-files --frozen-lockfile --production --force --ignore-scripts # purge dev-dependencies # DEPLOY IMAGE -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata COPY --from=0 /opt/package.json /opt/package.json diff --git a/packages/live-status-gateway/Dockerfile.circle b/packages/live-status-gateway/Dockerfile.circle index 637941030a..cbbb344049 100644 --- a/packages/live-status-gateway/Dockerfile.circle +++ b/packages/live-status-gateway/Dockerfile.circle @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata COPY package.json /opt/ diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index bf768b847e..f3e8b76c79 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -37,7 +37,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "keywords": [ "broadcast", diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 0abbc9d479..f02c35e95a 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -30,7 +30,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "files": [ "/dist", diff --git a/packages/mos-gateway/Dockerfile b/packages/mos-gateway/Dockerfile index fe28949d7a..52e575565e 100644 --- a/packages/mos-gateway/Dockerfile +++ b/packages/mos-gateway/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE -FROM node:20 +FROM node:22 WORKDIR /opt COPY . . @@ -13,7 +13,7 @@ RUN yarn plugin import workspace-tools RUN yarn workspaces focus mos-gateway --production # purge dev-dependencies # DEPLOY IMAGE -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata COPY --from=0 /opt/package.json /opt/package.json diff --git a/packages/mos-gateway/Dockerfile.circle b/packages/mos-gateway/Dockerfile.circle index 10373c3df9..a648ccbb25 100644 --- a/packages/mos-gateway/Dockerfile.circle +++ b/packages/mos-gateway/Dockerfile.circle @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata COPY package.json /opt/ diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 3b93188b06..084e6869ee 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -48,7 +48,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "keywords": [ "mos", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 78e975d6f4..06d6429b40 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -28,7 +28,7 @@ }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", "engines": { - "node": ">=20.18" + "node": ">=18.18" }, "files": [ "/api", diff --git a/packages/package.json b/packages/package.json index fbe2eeb83b..9320553c24 100644 --- a/packages/package.json +++ b/packages/package.json @@ -45,7 +45,7 @@ "@types/ejson": "^2.2.2", "@types/got": "^9.6.12", "@types/jest": "^29.5.11", - "@types/node": "^20.17.6", + "@types/node": "^22.9.3", "@types/node-fetch": "^2.6.11", "@types/object-path": "^0.11.4", "@types/underscore": "^1.11.15", diff --git a/packages/playout-gateway/Dockerfile b/packages/playout-gateway/Dockerfile index 604a358748..8b83b08e24 100644 --- a/packages/playout-gateway/Dockerfile +++ b/packages/playout-gateway/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental # BUILD IMAGE -FROM node:20 +FROM node:22 WORKDIR /opt COPY . . @@ -13,7 +13,7 @@ RUN yarn plugin import workspace-tools RUN yarn workspaces focus playout-gateway --production # purge dev-dependencies # DEPLOY IMAGE -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata COPY --from=0 /opt/package.json /opt/package.json diff --git a/packages/playout-gateway/Dockerfile.circle b/packages/playout-gateway/Dockerfile.circle index 1d2821e54d..f9007eab3d 100644 --- a/packages/playout-gateway/Dockerfile.circle +++ b/packages/playout-gateway/Dockerfile.circle @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:22-alpine RUN apk add --no-cache tzdata COPY package.json /opt/ diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index a4864865f4..591e2d1d34 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -40,7 +40,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "keywords": [ "broadcast", diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 9b2a3cc289..48e8f81660 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -48,7 +48,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "files": [ "/dist", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index ca62e23353..8b28c643c3 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -29,7 +29,7 @@ "license-validate": "run -T sofie-licensecheck" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" }, "files": [ "/dist", diff --git a/packages/webui/package.json b/packages/webui/package.json index 90458dd48e..7bb1ce4a49 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -108,6 +108,6 @@ "xml2js": "^0.6.2" }, "engines": { - "node": ">=20.18" + "node": ">=22.11" } } diff --git a/packages/yarn.lock b/packages/yarn.lock index a56b8fdcd7..345bc72e4f 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6465,12 +6465,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:^20.17.6": - version: 20.17.6 - resolution: "@types/node@npm:20.17.6" +"@types/node@npm:*, @types/node@npm:>=12.0.0, @types/node@npm:>=13.7.0, @types/node@npm:^22.9.3": + version: 22.9.3 + resolution: "@types/node@npm:22.9.3" dependencies: - undici-types: ~6.19.2 - checksum: d51dbb9881c94d0310b32b5fd8013e3261595c61bc888fa27258469c93c3dc0b3c4d20a9f28f3f5f79562f6737e28e7f3dd04940dc8b4d966d34aaf318f7f69b + undici-types: ~6.19.8 + checksum: 274cced37a8a11cd89827c551de73980a174e00bef0768c10c1fb7d3887a26b4fade25f870e3fd870432b93546e092cdbe0979e65110c0839982dc2b5938aabc languageName: node linkType: hard @@ -21026,7 +21026,7 @@ asn1@evs-broadcast/node-asn1: "@types/ejson": ^2.2.2 "@types/got": ^9.6.12 "@types/jest": ^29.5.11 - "@types/node": ^20.17.6 + "@types/node": ^22.9.3 "@types/node-fetch": ^2.6.11 "@types/object-path": ^0.11.4 "@types/underscore": ^1.11.15 @@ -26834,7 +26834,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"undici-types@npm:~6.19.2": +"undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 From aaaa3d500af8290c4e46124e9cbf18d0d94c25b5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 19 Nov 2024 12:46:47 +0000 Subject: [PATCH 03/18] chore: improve when webui is built --- packages/webui/package.json | 1 + scripts/install-and-build.mjs | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/webui/package.json b/packages/webui/package.json index 7bb1ce4a49..0a1ee41445 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -16,6 +16,7 @@ "scripts": { "dev": "vite --port=3005", "build": "tsc -b && vite build", + "build:main": "tsc -p tsconfig.app.json --noEmit", "check-types": "tsc -p tsconfig.app.json --noEmit", "watch-types": "run check-types --watch", "preview": "vite preview", diff --git a/scripts/install-and-build.mjs b/scripts/install-and-build.mjs index 72d1e5993b..5b1fa8124f 100644 --- a/scripts/install-and-build.mjs +++ b/scripts/install-and-build.mjs @@ -41,14 +41,15 @@ try { console.log(" 🪛 Build packages..."); console.log(hr()); + const buildArgs = ['--ignore @sofie-automation/webui'] + if (config.uiOnly) { + buildArgs.push(...EXTRA_PACKAGES.map((pkg) => `--ignore ${pkg}`)) + } + await concurrently( [ { - command: config.uiOnly - ? `yarn build:try ${EXTRA_PACKAGES.map( - (pkg) => `--ignore ${pkg}` - ).join(" ")}` - : "yarn build:try", + command: `yarn build:try ${buildArgs.join(" ")}`, cwd: "packages", name: "PACKAGES-BUILD", prefixColor: "yellow", From ef14c8f4f139be273ce8a8a3c21e6cc0f580d297 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 20 Nov 2024 17:37:02 +0000 Subject: [PATCH 04/18] feat: configure Core system/studio settings via blueprints --- meteor/__mocks__/defaultCollectionObjects.ts | 4 +- meteor/__mocks__/helpers/database.ts | 19 + .../serviceMessagesApi.test.ts | 3 + meteor/server/__tests__/cronjobs.test.ts | 38 +- meteor/server/api/evaluations.ts | 4 +- meteor/server/api/rest/v1/typeConversion.ts | 10 +- meteor/server/api/studio/api.ts | 4 +- meteor/server/collections/index.ts | 5 +- meteor/server/coreSystem/index.ts | 25 +- meteor/server/cronjobs.ts | 21 +- meteor/server/logo.ts | 2 +- meteor/server/migration/0_1_0.ts | 444 +---------- meteor/server/migration/X_X_X.ts | 152 +++- .../migration/__tests__/migrations.test.ts | 12 +- meteor/server/migration/api.ts | 9 +- meteor/server/migration/databaseMigration.ts | 4 + meteor/server/migration/upgrades/context.ts | 16 +- .../upgrades/defaultSystemActionTriggers.ts | 416 ++++++++++ meteor/server/migration/upgrades/lib.ts | 76 ++ .../migration/upgrades/showStyleBase.ts | 80 +- meteor/server/migration/upgrades/system.ts | 108 +++ .../blueprintUpgradeStatus/checkStatus.ts | 68 +- .../blueprintUpgradeStatus/publication.ts | 58 +- .../reactiveContentCache.ts | 21 + .../upgradesContentObserver.ts | 6 +- meteor/server/publications/lib/quickLoop.ts | 12 +- .../partInstancesUI/publication.ts | 8 +- .../partInstancesUI/reactiveContentCache.ts | 16 +- .../partInstancesUI/rundownContentObserver.ts | 27 +- .../publications/partsUI/publication.ts | 8 +- .../partsUI/reactiveContentCache.ts | 16 +- .../partsUI/rundownContentObserver.ts | 27 +- .../checkPieceContentStatus.ts | 4 +- .../pieceContentStatusUI/common.ts | 6 +- meteor/server/publications/studioUI.ts | 6 +- meteor/server/publications/system.ts | 4 +- .../blueprints-integration/src/api/studio.ts | 5 +- .../blueprints-integration/src/api/system.ts | 22 +- .../src/context/systemApplyConfigContext.ts | 6 + packages/blueprints-integration/src/index.ts | 2 + .../blueprints-integration/src/triggers.ts | 24 + packages/corelib/src/dataModel/Blueprint.ts | 2 +- .../corelib/src/dataModel/RundownPlaylist.ts | 10 +- packages/corelib/src/dataModel/Studio.ts | 82 +- packages/corelib/src/studio/baseline.ts | 4 +- packages/job-worker/src/__mocks__/context.ts | 12 +- .../src/__mocks__/defaultCollectionObjects.ts | 4 +- .../src/blueprints/__tests__/config.test.ts | 16 +- .../src/blueprints/__tests__/context.test.ts | 3 +- packages/job-worker/src/blueprints/config.ts | 9 +- .../context/OnTimelineGenerateContext.ts | 5 +- .../blueprints/context/PartEventContext.ts | 5 +- .../src/blueprints/context/RundownContext.ts | 5 +- .../blueprints/context/RundownEventContext.ts | 5 +- .../blueprints/context/ShowStyleContext.ts | 5 +- .../src/blueprints/context/StudioContext.ts | 13 +- .../blueprints/context/StudioUserContext.ts | 4 +- .../SyncIngestUpdateToPartInstanceContext.ts | 5 +- .../src/blueprints/context/adlibActions.ts | 4 +- .../src/ingest/__tests__/ingest.test.ts | 7 +- .../__tests__/selectShowStyleVariant.test.ts | 18 +- .../job-worker/src/ingest/expectedPackages.ts | 17 +- .../mosDevice/__tests__/mosIngest.test.ts | 2 +- packages/job-worker/src/jobs/index.ts | 9 +- packages/job-worker/src/jobs/studio.ts | 58 ++ .../src/playout/__tests__/playout.test.ts | 7 +- .../playout/__tests__/selectNextPart.test.ts | 3 +- .../src/playout/abPlayback/index.ts | 3 +- .../playout/abPlayback/routeSetDisabling.ts | 5 +- .../lookahead/__tests__/lookahead.test.ts | 8 +- .../playout/lookahead/__tests__/util.test.ts | 5 +- .../job-worker/src/playout/lookahead/index.ts | 5 +- .../model/services/QuickLoopService.ts | 2 +- .../job-worker/src/playout/selectNextPart.ts | 7 +- .../src/playout/timeline/generate.ts | 5 +- packages/job-worker/src/playout/upgrade.ts | 18 +- packages/job-worker/src/rundownPlaylists.ts | 12 +- packages/job-worker/src/workers/caches.ts | 40 +- .../src/workers/context/JobContextImpl.ts | 10 +- .../workers/context/StudioCacheContextImpl.ts | 24 +- .../workers/context/StudioRouteSetUpdater.ts | 59 +- .../__tests__/StudioRouteSetUpdater.spec.ts | 64 +- .../job-worker/src/workers/events/child.ts | 4 +- .../job-worker/src/workers/ingest/child.ts | 4 +- .../job-worker/src/workers/studio/child.ts | 4 +- packages/meteor-lib/src/api/migration.ts | 18 +- packages/meteor-lib/src/api/upgradeStatus.ts | 16 +- .../meteor-lib/src/collections/CoreSystem.ts | 36 +- .../src/core/model/CoreSystemSettings.ts | 23 + .../src/core/model/StudioSettings.ts | 84 +++ ...{sofie-logo.svg => sofie-logo-default.svg} | 0 .../src/__mocks__/defaultCollectionObjects.ts | 4 +- .../webui/src/__mocks__/helpers/database.ts | 19 + .../lib/Components/LabelAndOverrides.tsx | 12 +- .../lib/Components/MultiLineTextInput.tsx | 18 +- .../lib/__tests__/rundownTiming.test.ts | 7 +- .../lib/forms/SchemaFormWithOverrides.tsx | 2 - .../src/client/ui/AfterBroadcastForm.tsx | 9 +- .../BlueprintConfiguration/index.tsx | 3 +- .../ui/Settings/ShowStyle/OutputLayer.tsx | 12 +- .../ui/Settings/ShowStyle/SourceLayer.tsx | 23 +- .../Studio/BlueprintConfiguration/index.tsx | 3 +- .../Studio/Devices/GenericSubDevices.tsx | 2 - .../src/client/ui/Settings/Studio/Generic.tsx | 708 ++++++++++-------- .../client/ui/Settings/Studio/Mappings.tsx | 7 - .../PackageManager/AccessorTableRow.tsx | 23 - .../PackageManager/PackageContainers.tsx | 2 - .../Studio/Routings/ExclusivityGroups.tsx | 1 - .../Studio/Routings/RouteSetAbPlayers.tsx | 2 - .../ui/Settings/Studio/Routings/RouteSets.tsx | 11 - .../client/ui/Settings/SystemManagement.tsx | 260 ++++--- .../Settings/SystemManagement/Blueprint.tsx | 99 +++ .../ui/Settings/Upgrades/Components.tsx | 74 ++ .../src/client/ui/Settings/Upgrades/View.tsx | 26 +- .../TriggeredActionsEditor.tsx | 14 +- packages/webui/src/client/ui/SupportPopUp.tsx | 7 +- 116 files changed, 2469 insertions(+), 1447 deletions(-) create mode 100644 meteor/server/migration/upgrades/defaultSystemActionTriggers.ts create mode 100644 meteor/server/migration/upgrades/lib.ts create mode 100644 meteor/server/migration/upgrades/system.ts create mode 100644 packages/blueprints-integration/src/context/systemApplyConfigContext.ts create mode 100644 packages/job-worker/src/jobs/studio.ts create mode 100644 packages/shared-lib/src/core/model/CoreSystemSettings.ts create mode 100644 packages/shared-lib/src/core/model/StudioSettings.ts rename packages/webui/public/images/{sofie-logo.svg => sofie-logo-default.svg} (100%) create mode 100644 packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index 2831f96b14..052ede4a90 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -105,7 +105,7 @@ export function defaultStudio(_id: StudioId): DBStudio { mappingsWithOverrides: wrapDefaultObject({}), supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, @@ -113,7 +113,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowHold: false, allowPieceDirectPlay: false, enableBuckets: false, - }, + }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 6abd5a60bf..7c4bd82c41 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -171,6 +171,25 @@ export async function setupMockCore(doc?: Partial): Promise { diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 2c189d38af..f5c5aab339 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -2,7 +2,7 @@ import '../../__mocks__/_extendJest' import { testInFiber, runAllTimers, beforeAllInFiber, waitUntil } from '../../__mocks__/helpers/jest' import { MeteorMock } from '../../__mocks__/meteor' import { logger } from '../logging' -import { getRandomId, getRandomString, protectString } from '../lib/tempLib' +import { getRandomId, getRandomString, literal, protectString } from '../lib/tempLib' import { SnapshotType } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' import { IBlueprintPieceType, PieceLifespan, StatusCode, TSR } from '@sofie-automation/blueprints-integration' import { @@ -64,26 +64,36 @@ import { import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { Settings } from '../Settings' import { SofieIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/SofieIngestDataCache' +import { ObjectOverrideSetOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' describe('cronjobs', () => { let env: DefaultEnvironment let rundownId: RundownId - beforeAllInFiber(async () => { - env = await setupDefaultStudioEnvironment() - - const o = await setupDefaultRundownPlaylist(env) - rundownId = o.rundownId - + async function setCasparCGCronEnabled(enabled: boolean) { await CoreSystem.updateAsync( {}, { - $set: { - 'cron.casparCGRestart.enabled': true, + // This is a little bit of a hack, as it will result in duplicate ops, but it's fine for unit tests + $push: { + 'settingsWithOverrides.overrides': literal({ + op: 'set', + path: 'cron.casparCGRestart.enabled', + value: enabled, + }), }, }, { multi: true } ) + } + + beforeAllInFiber(async () => { + env = await setupDefaultStudioEnvironment() + + const o = await setupDefaultRundownPlaylist(env) + rundownId = o.rundownId + + await setCasparCGCronEnabled(true) jest.useFakeTimers() // set time to 2020/07/19 00:00 Local Time @@ -589,15 +599,7 @@ describe('cronjobs', () => { }) testInFiber('Does not attempt to restart CasparCG when job is disabled', async () => { await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold - await CoreSystem.updateAsync( - {}, - { - $set: { - 'cron.casparCGRestart.enabled': false, - }, - }, - { multi: true } - ) + await setCasparCGCronEnabled(false) ;(logger.info as jest.Mock).mockClear() // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC mockCurrentTime = new Date(2020, 6, date++, 4, 5, 0).getTime() diff --git a/meteor/server/api/evaluations.ts b/meteor/server/api/evaluations.ts index 3034110a65..3be9d57818 100644 --- a/meteor/server/api/evaluations.ts +++ b/meteor/server/api/evaluations.ts @@ -10,6 +10,7 @@ import { sendSlackMessageToWebhook } from './integration/slack' import { OrganizationId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Evaluations, RundownPlaylists } from '../collections' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' export async function saveEvaluation( credentials: { @@ -33,8 +34,9 @@ export async function saveEvaluation( deferAsync(async () => { const studio = await fetchStudioLight(evaluation.studioId) if (!studio) throw new Meteor.Error(500, `Studio ${evaluation.studioId} not found!`) + const studioSettings = applyAndValidateOverrides(studio.settingsWithOverrides).obj - const webhookUrls = _.compact((studio.settings.slackEvaluationUrls || '').split(',')) + const webhookUrls = _.compact((studioSettings.slackEvaluationUrls || '').split(',')) if (webhookUrls.length) { // Only send notes if not everything is OK diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index 75a38a37b2..24481613bf 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -53,7 +53,7 @@ import { DEFAULT_FALLBACK_PART_DURATION, } from '@sofie-automation/shared-lib/dist/core/constants' import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' -import { ForceQuickLoopAutoNext } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /* This file contains functions that convert between the internal Sofie-Core types and types exposed to the external API. @@ -307,13 +307,17 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P : convertObjectIntoOverrides(await StudioBlueprintConfigFromAPI(apiStudio, blueprintManifest)) } + const studioSettings = studioSettingsFrom(apiStudio.settings) + return { _id: existingId ?? getRandomId(), name: apiStudio.name, blueprintId: blueprint?._id, blueprintConfigPresetId: apiStudio.blueprintConfigPresetId, blueprintConfigWithOverrides: blueprintConfig, - settings: studioSettingsFrom(apiStudio.settings), + settingsWithOverrides: studio + ? updateOverrides(studio.settingsWithOverrides, studioSettings) + : wrapDefaultObject(studioSettings), supportedShowStyleBase: apiStudio.supportedShowStyleBase?.map((id) => protectString(id)) ?? [], organizationId: null, mappingsWithOverrides: wrapDefaultObject({}), @@ -334,7 +338,7 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P } export async function APIStudioFrom(studio: DBStudio): Promise> { - const studioSettings = APIStudioSettingsFrom(studio.settings) + const studioSettings = APIStudioSettingsFrom(applyAndValidateOverrides(studio.settingsWithOverrides).obj) return { name: studio.name, diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 4b79da80ff..de33b5b917 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -44,14 +44,14 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), // testToolsConfig?: ITestToolsConfig - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: false, allowPieceDirectPlay: false, enableBuckets: true, - }, + }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index 112093c97b..5a01de39bd 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -67,14 +67,13 @@ export const CoreSystem = createAsyncOnlyMongoCollection(Collection if (!access.update) return logNotAllowed('CoreSystem', access.reason) return allowOnlyFields(doc, fields, [ - 'support', 'systemInfo', 'name', 'logLevel', 'apm', - 'cron', 'logo', - 'evaluations', + 'blueprintId', + 'settingsWithOverrides', ]) }, }) diff --git a/meteor/server/coreSystem/index.ts b/meteor/server/coreSystem/index.ts index fa1bb84d46..f623c747ed 100644 --- a/meteor/server/coreSystem/index.ts +++ b/meteor/server/coreSystem/index.ts @@ -11,13 +11,14 @@ import { getEnvLogLevel, logger, LogLevel, setLogLevel } from '../logging' const PackageInfo = require('../../package.json') import Agent from 'meteor/julusian:meteor-elastic-apm' import { profiler } from '../api/profiler' -import { TMP_TSR_VERSION } from '@sofie-automation/blueprints-integration' +import { ICoreSystemSettings, TMP_TSR_VERSION } from '@sofie-automation/blueprints-integration' import { getAbsolutePath } from '../lib' import * as fs from 'fs/promises' import path from 'path' import { checkDatabaseVersions } from './checkDatabaseVersions' import PLazy from 'p-lazy' import { getCoreSystemAsync } from './collection' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' export { PackageInfo } @@ -60,11 +61,25 @@ async function initializeCoreSystem() { enabled: false, transactionSampleRate: -1, }, - cron: { - casparCGRestart: { - enabled: true, + settingsWithOverrides: wrapDefaultObject({ + cron: { + casparCGRestart: { + enabled: true, + }, + storeRundownSnapshots: { + enabled: false, + }, }, - }, + support: { + message: '', + }, + evaluationsMessage: { + enabled: false, + heading: '', + message: '', + }, + }), + lastBlueprintConfig: undefined, }) if (!isRunningInJest()) { diff --git a/meteor/server/cronjobs.ts b/meteor/server/cronjobs.ts index 88bb16c851..7b9d0d1ffb 100644 --- a/meteor/server/cronjobs.ts +++ b/meteor/server/cronjobs.ts @@ -18,13 +18,14 @@ import { deferAsync, normalizeArrayToMap } from '@sofie-automation/corelib/dist/ import { getCoreSystemAsync } from './coreSystem/collection' import { cleanupOldDataInner } from './api/cleanup' import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system' -import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' import { executePeripheralDeviceFunctionWithCustomTimeout } from './api/peripheralDevice/executeFunction' import { interpollateTranslation, isTranslatableMessage, translateMessage, } from '@sofie-automation/corelib/dist/TranslatableMessage' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' const lowPrioFcn = (fcn: () => any) => { // Do it at a random time in the future: @@ -49,15 +50,17 @@ export async function nightlyCronjobInner(): Promise { logger.info('Nightly cronjob: starting...') const system = await getCoreSystemAsync() + const systemSettings = system && applyAndValidateOverrides(system.settingsWithOverrides).obj + await Promise.allSettled([ cleanupOldDataCronjob().catch((error) => { logger.error(`Cronjob: Error when cleaning up old data: ${stringifyError(error)}`) logger.error(error) }), - restartCasparCG(system, previousLastNightlyCronjob).catch((e) => { + restartCasparCG(systemSettings, previousLastNightlyCronjob).catch((e) => { logger.error(`Cron: Restart CasparCG error: ${stringifyError(e)}`) }), - storeSnapshots(system).catch((e) => { + storeSnapshots(systemSettings).catch((e) => { logger.error(`Cron: Rundown Snapshots error: ${stringifyError(e)}`) }), ]) @@ -81,8 +84,8 @@ async function cleanupOldDataCronjob() { const CASPARCG_LAST_SEEN_PERIOD_MS = 3 * 60 * 1000 // Note: this must be higher than the ping interval used by playout-gateway -async function restartCasparCG(system: ICoreSystem | undefined, previousLastNightlyCronjob: number) { - if (!system?.cron?.casparCGRestart?.enabled) return +async function restartCasparCG(systemSettings: ICoreSystemSettings | undefined, previousLastNightlyCronjob: number) { + if (!systemSettings?.cron?.casparCGRestart?.enabled) return let shouldRetryAttempt = false const ps: Array> = [] @@ -176,10 +179,10 @@ async function restartCasparCG(system: ICoreSystem | undefined, previousLastNigh } } -async function storeSnapshots(system: ICoreSystem | undefined) { - if (system?.cron?.storeRundownSnapshots?.enabled) { - const filter = system.cron.storeRundownSnapshots.rundownNames?.length - ? { name: { $in: system.cron.storeRundownSnapshots.rundownNames } } +async function storeSnapshots(systemSettings: ICoreSystemSettings | undefined) { + if (systemSettings?.cron?.storeRundownSnapshots?.enabled) { + const filter = systemSettings.cron.storeRundownSnapshots.rundownNames?.length + ? { name: { $in: systemSettings.cron.storeRundownSnapshots.rundownNames } } : {} const playlists = await RundownPlaylists.findFetchAsync(filter) diff --git a/meteor/server/logo.ts b/meteor/server/logo.ts index 26536e65b8..2e7910bae6 100644 --- a/meteor/server/logo.ts +++ b/meteor/server/logo.ts @@ -13,7 +13,7 @@ logoRouter.get('/', async (ctx) => { const logo = core?.logo ?? SofieLogo.Default const paths: Record = { - [SofieLogo.Default]: '/images/sofie-logo.svg', + [SofieLogo.Default]: '/images/sofie-logo-default.svg', [SofieLogo.Pride]: '/images/sofie-logo-pride.svg', [SofieLogo.Norway]: '/images/sofie-logo-norway.svg', [SofieLogo.Christmas]: '/images/sofie-logo-christmas.svg', diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index 461ce95d6f..72fba7b0c2 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -1,15 +1,9 @@ import { addMigrationSteps } from './databaseMigration' import { logger } from '../logging' -import { getRandomId, protectString, generateTranslation as t, getHash } from '../lib/tempLib' +import { getRandomId, protectString } from '../lib/tempLib' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ShowStyleBases, ShowStyleVariants, Studios, TriggeredActions } from '../collections' -import { - IBlueprintTriggeredActions, - ClientActions, - TriggerType, - PlayoutActions, -} from '@sofie-automation/blueprints-integration' +import { ShowStyleBases, ShowStyleVariants, Studios } from '../collections' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' /** @@ -17,408 +11,6 @@ import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/cor * These files are combined with / overridden by migration steps defined in the blueprints. */ -let j = 0 - -const DEFAULT_CORE_TRIGGERS: IBlueprintTriggeredActions[] = [ - { - _id: 'core_toggleShelf', - actions: { - '0': { - action: ClientActions.shelf, - filterChain: [ - { - object: 'view', - }, - ], - state: 'toggle', - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Tab', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Toggle Shelf'), - }, - { - _id: 'core_activateRundownPlaylist', - actions: { - '0': { - action: PlayoutActions.activateRundownPlaylist, - rehearsal: false, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Backquote', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Activate (On-Air)'), - }, - { - _id: 'core_activateRundownPlaylist_rehearsal', - actions: { - '0': { - action: PlayoutActions.activateRundownPlaylist, - rehearsal: true, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Backquote', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Activate (Rehearsal)'), - }, - { - _id: 'core_deactivateRundownPlaylist', - actions: { - '0': { - action: PlayoutActions.deactivateRundownPlaylist, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Shift+Backquote', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Deactivate'), - }, - { - _id: 'core_take', - actions: { - '0': { - action: PlayoutActions.take, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'NumpadEnter', - up: true, - }, - '1': { - type: TriggerType.hotkey, - keys: 'F12', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Take'), - }, - { - _id: 'core_hold', - actions: { - '0': { - action: PlayoutActions.hold, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'KeyH', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Hold'), - }, - { - _id: 'core_hold_undo', - actions: { - '0': { - action: PlayoutActions.hold, - undo: true, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+KeyH', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Undo Hold'), - }, - { - _id: 'core_reset_rundown_playlist', - actions: { - '0': { - action: PlayoutActions.resetRundownPlaylist, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Shift+F12', - up: true, - }, - '1': { - type: TriggerType.hotkey, - keys: 'Control+Shift+AnyEnter', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Reset Rundown'), - }, - { - _id: 'core_disable_next_piece', - actions: { - '0': { - action: PlayoutActions.disableNextPiece, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'KeyG', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Disable the next element'), - }, - { - _id: 'core_disable_next_piece_undo', - actions: { - '0': { - action: PlayoutActions.disableNextPiece, - filterChain: [ - { - object: 'view', - }, - ], - undo: true, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+KeyG', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Undo Disable the next element'), - }, - { - _id: 'core_create_snapshot_for_debug', - actions: { - '0': { - action: PlayoutActions.createSnapshotForDebug, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Backspace', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Store Snapshot'), - }, - { - _id: 'core_move_next_part', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: 1, - segments: 0, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'F9', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next forwards'), - }, - { - _id: 'core_move_next_segment', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: 0, - segments: 1, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'F10', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next to the following segment'), - }, - { - _id: 'core_move_previous_part', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: -1, - segments: 0, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+F9', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next backwards'), - }, - { - _id: 'core_move_previous_segment', - actions: { - '0': { - action: PlayoutActions.moveNext, - filterChain: [ - { - object: 'view', - }, - ], - parts: 0, - segments: -1, - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+F10', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Move Next to the previous segment'), - }, - { - _id: 'core_go_to_onAir_line', - actions: { - '0': { - action: ClientActions.goToOnAirLine, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Home', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Go to On Air line'), - }, - { - _id: 'core_rewind_segments', - actions: { - '0': { - action: ClientActions.rewindSegments, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Shift+Home', - up: true, - }, - }, - _rank: ++j * 1000, - name: t('Rewind segments to start'), - }, -] - // 0.1.0: These are the "base" migration steps, setting up a default system export const addSteps = addMigrationSteps('0.1.0', [ { @@ -437,14 +29,14 @@ export const addSteps = addMigrationSteps('0.1.0', [ name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: false, allowPieceDirectPlay: false, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -541,32 +133,4 @@ export const addSteps = addMigrationSteps('0.1.0', [ } }, }, - { - id: 'TriggeredActions.core', - canBeRunAutomatically: true, - validate: async () => { - const coreTriggeredActionsCount = await TriggeredActions.countDocuments({ - showStyleBaseId: null, - }) - - if (coreTriggeredActionsCount === 0) { - return `No system-wide triggered actions set up.` - } - - return false - }, - migrate: async () => { - for (const triggeredAction of DEFAULT_CORE_TRIGGERS) { - await TriggeredActions.insertAsync({ - _id: protectString(getHash(triggeredAction._id)), - _rank: triggeredAction._rank, - name: triggeredAction.name, - blueprintUniqueId: null, - showStyleBaseId: null, - actionsWithOverrides: wrapDefaultObject(triggeredAction.actions), - triggersWithOverrides: wrapDefaultObject(triggeredAction.triggers), - }) - } - }, - }, ]) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 0b7409ea77..1abf3e12d0 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,12 +1,19 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { Studios } from '../collections' -import { convertObjectIntoOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { CoreSystem, Studios, TriggeredActions } from '../collections' +import { + convertObjectIntoOverrides, + wrapDefaultObject, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { StudioRouteSet, StudioRouteSetExclusivityGroup, StudioPackageContainer, + IStudioSettings, } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DEFAULT_CORE_TRIGGER_IDS } from './upgrades/defaultSystemActionTriggers' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' /* * ************************************************************************************** @@ -224,4 +231,145 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + + { + id: 'TriggeredActions.remove old systemwide', + canBeRunAutomatically: true, + validate: async () => { + const coreTriggeredActionsCount = await TriggeredActions.countDocuments({ + showStyleBaseId: null, + blueprintUniqueId: null, + _id: { $in: DEFAULT_CORE_TRIGGER_IDS }, + }) + + if (coreTriggeredActionsCount > 0) { + return `System-wide triggered actions needing removal.` + } + + return false + }, + migrate: async () => { + await TriggeredActions.removeAsync({ + showStyleBaseId: null, + blueprintUniqueId: null, + _id: { $in: DEFAULT_CORE_TRIGGER_IDS }, + }) + }, + }, + + { + id: `convert studio.settings to ObjectWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const studios = await Studios.findFetchAsync({ + settings: { $exists: true }, + settingsWithOverrides: { $exists: false }, + }) + + for (const studio of studios) { + //@ts-expect-error settings is not typed as ObjectWithOverrides + if (studio.settings) { + return 'settings must be converted to an ObjectWithOverrides' + } + } + + return false + }, + migrate: async () => { + const studios = await Studios.findFetchAsync({ + settings: { $exists: true }, + settingsWithOverrides: { $exists: false }, + }) + + for (const studio of studios) { + //@ts-expect-error settings is typed as Record + const oldSettings = studio.settings + + const newSettings = wrapDefaultObject(oldSettings || {}) + + await Studios.updateAsync(studio._id, { + $set: { + settingsWithOverrides: newSettings, + }, + $unset: { + // settings: 1, + }, + }) + } + }, + }, + + { + id: `convert CoreSystem.settingsWithOverrides`, + canBeRunAutomatically: true, + validate: async () => { + const systems = await CoreSystem.findFetchAsync({ + settingsWithOverrides: { $exists: false }, + }) + + if (systems.length > 0) { + return 'settings must be converted to an ObjectWithOverrides' + } + + return false + }, + migrate: async () => { + const systems = await CoreSystem.findFetchAsync({ + settingsWithOverrides: { $exists: false }, + }) + + for (const system of systems) { + const oldSystem = system as ICoreSystem as PartialOldICoreSystem + + const newSettings = wrapDefaultObject({ + cron: { + casparCGRestart: { + enabled: false, + }, + storeRundownSnapshots: { + enabled: false, + }, + ...oldSystem.cron, + }, + support: oldSystem.support ?? { message: '' }, + evaluationsMessage: oldSystem.evaluations ?? { enabled: false, heading: '', message: '' }, + }) + + await CoreSystem.updateAsync(system._id, { + $set: { + settingsWithOverrides: newSettings, + }, + $unset: { + cron: 1, + support: 1, + evaluations: 1, + }, + }) + } + }, + }, ]) + +interface PartialOldICoreSystem { + /** Support info */ + support?: { + message: string + } + + evaluations?: { + enabled: boolean + heading: string + message: string + } + + /** Cron jobs running nightly */ + cron?: { + casparCGRestart?: { + enabled: boolean + } + storeRundownSnapshots?: { + enabled: boolean + rundownNames?: string[] + } + } +} diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 5e9168964d..d106e99e41 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -122,14 +122,14 @@ describe('Migrations', () => { name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -163,14 +163,14 @@ describe('Migrations', () => { name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', @@ -204,14 +204,14 @@ describe('Migrations', () => { name: 'Default studio', organizationId: null, supportedShowStyleBase: [], - settings: { + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), mappingsWithOverrides: wrapDefaultObject({}), blueprintConfigWithOverrides: wrapDefaultObject({}), _rundownVersionHash: '', diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index fd4fac48e1..23a1169759 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -20,8 +20,9 @@ import { validateConfigForShowStyleBase, validateConfigForStudio, } from './upgrades' -import { ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { CoreSystemId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' +import { runUpgradeForCoreSystem } from './upgrades/system' class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async getMigrationStatus() { @@ -123,5 +124,11 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return runUpgradeForShowStyleBase(showStyleBaseId) } + + async runUpgradeForCoreSystem(coreSystemId: CoreSystemId): Promise { + await SystemWriteAccess.migrations(this) + + return runUpgradeForCoreSystem(coreSystemId) + } } registerClassToMeteorMethods(MigrationAPIMethods, ServerMigrationAPI, false) diff --git a/meteor/server/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index b01bdfe5fe..4dbbfc7ce5 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -258,6 +258,10 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise(DEFAULT_CORE_TRIGGERS).map( + (triggeredAction) => protectString(getHash(triggeredAction._id)) +) diff --git a/meteor/server/migration/upgrades/lib.ts b/meteor/server/migration/upgrades/lib.ts new file mode 100644 index 0000000000..ce825f2fd7 --- /dev/null +++ b/meteor/server/migration/upgrades/lib.ts @@ -0,0 +1,76 @@ +import type { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { TriggeredActions } from '../../collections' +import { Complete, getRandomId, literal, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' +import type { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' +import type { AnyBulkWriteOperation } from 'mongodb' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import type { IBlueprintTriggeredActions } from '@sofie-automation/blueprints-integration' + +export async function updateTriggeredActionsForShowStyleBaseId( + showStyleBaseId: ShowStyleBaseId | null, + triggeredActions: IBlueprintTriggeredActions[] +): Promise { + const oldTriggeredActionsArray = await TriggeredActions.findFetchAsync({ + showStyleBaseId: showStyleBaseId, + blueprintUniqueId: { $ne: null }, + }) + const oldTriggeredActions = normalizeArrayToMap(oldTriggeredActionsArray, 'blueprintUniqueId') + + const newDocIds: TriggeredActionId[] = [] + const bulkOps: AnyBulkWriteOperation[] = [] + + for (const newTriggeredAction of triggeredActions) { + const oldValue = oldTriggeredActions.get(newTriggeredAction._id) + if (oldValue) { + // Update an existing TriggeredAction + newDocIds.push(oldValue._id) + bulkOps.push({ + updateOne: { + filter: { + _id: oldValue._id, + }, + update: { + $set: { + _rank: newTriggeredAction._rank, + name: newTriggeredAction.name, + 'triggersWithOverrides.defaults': newTriggeredAction.triggers, + 'actionsWithOverrides.defaults': newTriggeredAction.actions, + }, + }, + }, + }) + } else { + // Insert a new TriggeredAction + const newDocId = getRandomId() + newDocIds.push(newDocId) + bulkOps.push({ + insertOne: { + document: literal>({ + _id: newDocId, + _rank: newTriggeredAction._rank, + name: newTriggeredAction.name, + showStyleBaseId: showStyleBaseId, + blueprintUniqueId: newTriggeredAction._id, + triggersWithOverrides: wrapDefaultObject(newTriggeredAction.triggers), + actionsWithOverrides: wrapDefaultObject(newTriggeredAction.actions), + styleClassNames: newTriggeredAction.styleClassNames, + }), + }, + }) + } + } + + // Remove any removed TriggeredAction + // Future: should this orphan them or something? Will that cause issues if they get re-added? + bulkOps.push({ + deleteMany: { + filter: { + showStyleBaseId: showStyleBaseId, + blueprintUniqueId: { $ne: null }, + _id: { $nin: newDocIds }, + }, + }, + }) + + await TriggeredActions.bulkWriteAsync(bulkOps) +} diff --git a/meteor/server/migration/upgrades/showStyleBase.ts b/meteor/server/migration/upgrades/showStyleBase.ts index d2281043ae..bcbe015617 100644 --- a/meteor/server/migration/upgrades/showStyleBase.ts +++ b/meteor/server/migration/upgrades/showStyleBase.ts @@ -3,25 +3,21 @@ import { JSONBlobParse, ShowStyleBlueprintManifest, } from '@sofie-automation/blueprints-integration' -import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { normalizeArray, normalizeArrayToMap, getRandomId, literal, Complete } from '@sofie-automation/corelib/dist/lib' -import { - applyAndValidateOverrides, - wrapDefaultObject, -} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { normalizeArray } from '@sofie-automation/corelib/dist/lib' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { wrapTranslatableMessageFromBlueprints } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' import { Meteor } from 'meteor/meteor' -import { Blueprints, ShowStyleBases, TriggeredActions } from '../../collections' +import { Blueprints, ShowStyleBases } from '../../collections' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' import { evalBlueprint } from '../../api/blueprints/cache' import { logger } from '../../logging' import { CommonContext } from './context' -import type { AnyBulkWriteOperation } from 'mongodb' import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { BlueprintFixUpConfigMessage } from '@sofie-automation/meteor-lib/dist/api/migration' +import { updateTriggeredActionsForShowStyleBaseId } from './lib' export async function fixupConfigForShowStyleBase( showStyleBaseId: ShowStyleBaseId @@ -100,7 +96,7 @@ export async function validateConfigForShowStyleBase( throwIfNeedsFixupConfigRunning(showStyleBase, blueprint, blueprintManifest) const blueprintContext = new CommonContext( - 'applyConfig', + 'validateConfig', `showStyleBase:${showStyleBaseId},blueprint:${blueprint._id}` ) const rawBlueprintConfig = applyAndValidateOverrides(showStyleBase.blueprintConfigWithOverrides).obj @@ -146,69 +142,7 @@ export async function runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseI }, }) - const oldTriggeredActionsArray = await TriggeredActions.findFetchAsync({ - showStyleBaseId: showStyleBaseId, - blueprintUniqueId: { $ne: null }, - }) - const oldTriggeredActions = normalizeArrayToMap(oldTriggeredActionsArray, 'blueprintUniqueId') - - const newDocIds: TriggeredActionId[] = [] - const bulkOps: AnyBulkWriteOperation[] = [] - - for (const newTriggeredAction of result.triggeredActions) { - const oldValue = oldTriggeredActions.get(newTriggeredAction._id) - if (oldValue) { - // Update an existing TriggeredAction - newDocIds.push(oldValue._id) - bulkOps.push({ - updateOne: { - filter: { - _id: oldValue._id, - }, - update: { - $set: { - _rank: newTriggeredAction._rank, - name: newTriggeredAction.name, - 'triggersWithOverrides.defaults': newTriggeredAction.triggers, - 'actionsWithOverrides.defaults': newTriggeredAction.actions, - }, - }, - }, - }) - } else { - // Insert a new TriggeredAction - const newDocId = getRandomId() - newDocIds.push(newDocId) - bulkOps.push({ - insertOne: { - document: literal>({ - _id: newDocId, - _rank: newTriggeredAction._rank, - name: newTriggeredAction.name, - showStyleBaseId: showStyleBaseId, - blueprintUniqueId: newTriggeredAction._id, - triggersWithOverrides: wrapDefaultObject(newTriggeredAction.triggers), - actionsWithOverrides: wrapDefaultObject(newTriggeredAction.actions), - styleClassNames: newTriggeredAction.styleClassNames, - }), - }, - }) - } - } - - // Remove any removed TriggeredAction - // Future: should this orphan them or something? Will that cause issues if they get re-added? - bulkOps.push({ - deleteMany: { - filter: { - showStyleBaseId: showStyleBaseId, - blueprintUniqueId: { $ne: null }, - _id: { $nin: newDocIds }, - }, - }, - }) - - await TriggeredActions.bulkWriteAsync(bulkOps) + await updateTriggeredActionsForShowStyleBaseId(showStyleBaseId, result.triggeredActions) } async function loadShowStyleAndBlueprint(showStyleBaseId: ShowStyleBaseId) { diff --git a/meteor/server/migration/upgrades/system.ts b/meteor/server/migration/upgrades/system.ts new file mode 100644 index 0000000000..15ae90bea8 --- /dev/null +++ b/meteor/server/migration/upgrades/system.ts @@ -0,0 +1,108 @@ +import { Meteor } from 'meteor/meteor' +import { logger } from '../../logging' +import { Blueprints, CoreSystem } from '../../collections' +import { + BlueprintManifestType, + BlueprintResultApplySystemConfig, + IBlueprintTriggeredActions, + SystemBlueprintManifest, +} from '@sofie-automation/blueprints-integration' +import { evalBlueprint } from '../../api/blueprints/cache' +import { CoreSystemApplyConfigContext } from './context' +import { updateTriggeredActionsForShowStyleBaseId } from './lib' +import { CoreSystemId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DEFAULT_CORE_TRIGGERS } from './defaultSystemActionTriggers' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' + +export async function runUpgradeForCoreSystem(coreSystemId: CoreSystemId): Promise { + logger.info(`Running upgrade for CoreSystem`) + + const { coreSystem, blueprint, blueprintManifest } = await loadCoreSystemAndBlueprint(coreSystemId) + + let result: BlueprintResultApplySystemConfig + + if (blueprintManifest && typeof blueprintManifest.applyConfig === 'function') { + const blueprintContext = new CoreSystemApplyConfigContext( + 'applyConfig', + `coreSystem:${coreSystem._id},blueprint:${blueprint.blueprintId}` + ) + + result = blueprintManifest.applyConfig(blueprintContext) + } else { + // Ensure some defaults are populated when no blueprint method is present + result = generateDefaultSystemConfig() + } + + const coreSystemSettings: ICoreSystemSettings = result.settings + + await CoreSystem.updateAsync(coreSystemId, { + $set: { + 'settingsWithOverrides.defaults': coreSystemSettings, + lastBlueprintConfig: { + blueprintHash: blueprint?.blueprintHash ?? protectString('default'), + blueprintId: blueprint?._id ?? protectString('default'), + blueprintConfigPresetId: undefined, + config: {}, + }, + }, + }) + + await updateTriggeredActionsForShowStyleBaseId(null, result.triggeredActions) +} + +async function loadCoreSystemAndBlueprint(coreSystemId: CoreSystemId) { + const coreSystem = await CoreSystem.findOneAsync(coreSystemId) + if (!coreSystem) throw new Meteor.Error(404, `CoreSystem "${coreSystemId}" not found!`) + + if (!coreSystem.blueprintId) { + // No blueprint is valid + return { + coreSystem, + blueprint: undefined, + blueprintHash: undefined, + } + } + + // if (!showStyleBase.blueprintConfigPresetId) throw new Meteor.Error(500, 'ShowStyleBase is missing config preset') + + const blueprint = await Blueprints.findOneAsync({ + _id: coreSystem.blueprintId, + blueprintType: BlueprintManifestType.SYSTEM, + }) + if (!blueprint) throw new Meteor.Error(404, `Blueprint "${coreSystem.blueprintId}" not found!`) + + if (!blueprint.blueprintHash) throw new Meteor.Error(500, 'Blueprint is not valid') + + const blueprintManifest = evalBlueprint(blueprint) as SystemBlueprintManifest + + return { + coreSystem, + blueprint, + blueprintManifest, + } +} + +function generateDefaultSystemConfig(): BlueprintResultApplySystemConfig { + return { + settings: { + cron: { + casparCGRestart: { + enabled: true, + }, + storeRundownSnapshots: { + enabled: false, + }, + }, + support: { + message: '', + }, + evaluationsMessage: { + enabled: false, + heading: '', + message: '', + }, + }, + triggeredActions: Object.values(DEFAULT_CORE_TRIGGERS), + } +} diff --git a/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts index 5567ab0fb3..5fde9762ab 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/checkStatus.ts @@ -14,12 +14,13 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { joinObjectPathFragments, objectPathGet } from '@sofie-automation/corelib/dist/lib' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { generateTranslation } from '../../lib/tempLib' +import { generateTranslation, protectString } from '../../lib/tempLib' import { logger } from '../../logging' -import { ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import { CoreSystemFields, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' import _ from 'underscore' import { UIBlueprintUpgradeStatusBase } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' export interface BlueprintMapEntry { _id: BlueprintId @@ -39,7 +40,7 @@ export function checkDocUpgradeStatus( // Studio blueprint is missing/invalid return { invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { - blueprintId: doc.blueprintId, + blueprintId: doc.blueprintId ?? 'undefined', }), pendingRunOfFixupFunction: false, changes: [], @@ -101,7 +102,7 @@ export function checkDocUpgradeStatus( changes.push(generateTranslation('Blueprint has a new version')) } - if (doc.lastBlueprintConfig) { + if (doc.lastBlueprintConfig && doc.blueprintConfigWithOverrides) { // Check if the config blob has changed since last run const newConfig = applyAndValidateOverrides(doc.blueprintConfigWithOverrides).obj const oldConfig = doc.lastBlueprintConfig.config @@ -135,6 +136,65 @@ export function checkDocUpgradeStatus( } } +export function checkSystemUpgradeStatus( + blueprintMap: Map, + doc: Pick +): Pick { + const changes: ITranslatableMessage[] = [] + + // Check the blueprintId is valid + if (doc.blueprintId) { + const blueprint = blueprintMap.get(doc.blueprintId) + if (!blueprint || !blueprint.configPresets) { + // Studio blueprint is missing/invalid + return { + invalidReason: generateTranslation('Invalid blueprint: "{{blueprintId}}"', { + blueprintId: doc.blueprintId ?? 'undefined', + }), + pendingRunOfFixupFunction: false, + changes: [], + } + } + + // Some basic property checks + if (!doc.lastBlueprintConfig) { + changes.push(generateTranslation('Config has not been applied before')) + } else if (doc.lastBlueprintConfig.blueprintId !== doc.blueprintId) { + changes.push( + generateTranslation('Blueprint has been changed. From "{{ oldValue }}", to "{{ newValue }}"', { + oldValue: doc.lastBlueprintConfig.blueprintId || '', + newValue: doc.blueprintId || '', + }) + ) + } else if (doc.lastBlueprintConfig.blueprintHash !== blueprint.blueprintHash) { + changes.push(generateTranslation('Blueprint has a new version')) + } + } else { + // No blueprint assigned + + const defaultId = protectString('default') + + // Some basic property checks + if (!doc.lastBlueprintConfig) { + changes.push(generateTranslation('Config has not been applied before')) + } else if (doc.lastBlueprintConfig.blueprintId !== defaultId) { + changes.push( + generateTranslation('Blueprint has been changed. From "{{ oldValue }}", to "{{ newValue }}"', { + oldValue: doc.lastBlueprintConfig.blueprintId || '', + newValue: defaultId, + }) + ) + } else if (doc.lastBlueprintConfig.blueprintHash !== defaultId) { + changes.push(generateTranslation('Blueprint has a new version')) + } + } + + return { + changes, + pendingRunOfFixupFunction: false, + } +} + /** * This is a slightly crude diffing of objects based on a jsonschema. Only keys in the schema will be compared. * For now this has some limitations such as not looking inside of arrays, but this could be expanded later on diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts index 568f9b0756..9ea8d72fe5 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/publication.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -13,9 +13,15 @@ import { import { logger } from '../../logging' import { resolveCredentials } from '../../security/lib/credentials' import { NoSecurityReadAccess } from '../../security/noSecurity' -import { ContentCache, createReactiveContentCache, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' +import { + ContentCache, + CoreSystemFields, + createReactiveContentCache, + ShowStyleBaseFields, + StudioFields, +} from './reactiveContentCache' import { UpgradesContentObserver } from './upgradesContentObserver' -import { BlueprintMapEntry, checkDocUpgradeStatus } from './checkStatus' +import { BlueprintMapEntry, checkDocUpgradeStatus, checkSystemUpgradeStatus } from './checkStatus' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' @@ -23,6 +29,7 @@ import { UIBlueprintUpgradeStatus, UIBlueprintUpgradeStatusId, } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' type BlueprintUpgradeStatusArgs = Record @@ -33,6 +40,7 @@ export interface BlueprintUpgradeStatusState { interface BlueprintUpgradeStatusUpdateProps { newCache: ContentCache + invalidateSystem: boolean invalidateStudioIds: StudioId[] invalidateShowStyleBaseIds: ShowStyleBaseId[] invalidateBlueprintIds: BlueprintId[] @@ -54,6 +62,11 @@ async function setupBlueprintUpgradeStatusPublicationObservers( return [ mongoObserver, + cache.CoreSystem.find({}).observeChanges({ + added: () => triggerUpdate({ invalidateSystem: true }), + changed: () => triggerUpdate({ invalidateSystem: true }), + removed: () => triggerUpdate({ invalidateSystem: true }), + }), cache.Studios.find({}).observeChanges({ added: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), changed: (id) => triggerUpdate({ invalidateStudioIds: [protectString(id)] }), @@ -72,7 +85,10 @@ async function setupBlueprintUpgradeStatusPublicationObservers( ] } -function getDocumentId(type: 'studio' | 'showStyle', id: ProtectedString): UIBlueprintUpgradeStatusId { +function getDocumentId( + type: 'coreSystem' | 'studio' | 'showStyle', + id: ProtectedString +): UIBlueprintUpgradeStatusId { return protectString(`${type}:${id}`) } @@ -100,6 +116,7 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( const studioBlueprintsMap = new Map() const showStyleBlueprintsMap = new Map() + const systemBlueprintsMap = new Map() state.contentCache.Blueprints.find({}).forEach((blueprint) => { switch (blueprint.blueprintType) { case BlueprintManifestType.SHOWSTYLE: @@ -120,6 +137,15 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( hasFixUpFunction: blueprint.hasFixUpFunction, }) break + case BlueprintManifestType.SYSTEM: + systemBlueprintsMap.set(blueprint._id, { + _id: blueprint._id, + configPresets: {}, + configSchema: undefined, // TODO + blueprintHash: blueprint.blueprintHash, + hasFixUpFunction: false, + }) + break // TODO - default? } }) @@ -136,6 +162,10 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( state.contentCache.ShowStyleBases.find({}).forEach((showStyleBase) => { updateShowStyleUpgradeStatus(collection, showStyleBlueprintsMap, showStyleBase) }) + + state.contentCache.CoreSystem.find({}).forEach((coreSystem) => { + updateCoreSystemUpgradeStatus(collection, systemBlueprintsMap, coreSystem) + }) } else { const regenerateForStudioIds = new Set(updateProps.invalidateStudioIds) const regenerateForShowStyleBaseIds = new Set(updateProps.invalidateShowStyleBaseIds) @@ -181,9 +211,31 @@ export async function manipulateBlueprintUpgradeStatusPublicationData( collection.remove(getDocumentId('showStyle', showStyleBaseId)) } } + + if (updateProps.invalidateSystem) { + state.contentCache.CoreSystem.find({}).forEach((coreSystem) => { + updateCoreSystemUpgradeStatus(collection, systemBlueprintsMap, coreSystem) + }) + } } } +function updateCoreSystemUpgradeStatus( + collection: CustomPublishCollection, + blueprintsMap: Map, + coreSystem: Pick +) { + const status = checkSystemUpgradeStatus(blueprintsMap, coreSystem) + + collection.replace({ + ...status, + _id: getDocumentId('coreSystem', coreSystem._id), + documentType: 'coreSystem', + documentId: coreSystem._id, + name: coreSystem.name ?? 'System', + }) +} + function updateStudioUpgradeStatus( collection: CustomPublishCollection, blueprintsMap: Map, diff --git a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts index 501e678062..1aa3474720 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/reactiveContentCache.ts @@ -4,6 +4,25 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' + +export type CoreSystemFields = + | '_id' + | 'blueprintId' + | 'blueprintConfigPresetId' + | 'lastBlueprintConfig' + | 'blueprintConfigWithOverrides' + | 'lastBlueprintFixUpHash' + | 'name' +export const coreSystemFieldsSpecifier = literal>>({ + _id: 1, + blueprintId: 1, + blueprintConfigPresetId: 1, + lastBlueprintConfig: 1, + lastBlueprintFixUpHash: 1, + blueprintConfigWithOverrides: 1, + name: 1, +}) export type StudioFields = | '_id' @@ -64,6 +83,7 @@ export const blueprintFieldSpecifier = literal> Studios: ReactiveCacheCollection> ShowStyleBases: ReactiveCacheCollection> Blueprints: ReactiveCacheCollection> @@ -71,6 +91,7 @@ export interface ContentCache { export function createReactiveContentCache(): ContentCache { const cache: ContentCache = { + CoreSystem: new ReactiveCacheCollection>('coreSystem'), Studios: new ReactiveCacheCollection>('studios'), ShowStyleBases: new ReactiveCacheCollection>('showStyleBases'), Blueprints: new ReactiveCacheCollection>('blueprints'), diff --git a/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts index a88ba8575b..e8f8d6281a 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/upgradesContentObserver.ts @@ -3,10 +3,11 @@ import { logger } from '../../logging' import { blueprintFieldSpecifier, ContentCache, + coreSystemFieldsSpecifier, showStyleFieldSpecifier, studioFieldSpecifier, } from './reactiveContentCache' -import { Blueprints, ShowStyleBases, Studios } from '../../collections' +import { Blueprints, CoreSystem, ShowStyleBases, Studios } from '../../collections' import { waitForAllObserversReady } from '../lib/lib' export class UpgradesContentObserver { @@ -22,6 +23,9 @@ export class UpgradesContentObserver { logger.silly(`Creating UpgradesContentObserver`) const observers = await waitForAllObserversReady([ + CoreSystem.observeChanges({}, cache.CoreSystem.link(), { + projection: coreSystemFieldsSpecifier, + }), Studios.observeChanges({}, cache.Studios.link(), { projection: studioFieldSpecifier, }), diff --git a/meteor/server/publications/lib/quickLoop.ts b/meteor/server/publications/lib/quickLoop.ts index 272a554ac9..9b4bb08374 100644 --- a/meteor/server/publications/lib/quickLoop.ts +++ b/meteor/server/publications/lib/quickLoop.ts @@ -1,16 +1,16 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBRundownPlaylist, - ForceQuickLoopAutoNext, QuickLoopMarker, QuickLoopMarkerType, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/corelib/dist/playout/playlist' import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DEFAULT_FALLBACK_PART_DURATION } from '@sofie-automation/shared-lib/dist/core/constants' import { getCurrentTime } from '../../lib/lib' import { generateTranslation } from '@sofie-automation/corelib/dist/lib' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' @@ -47,7 +47,7 @@ export function modifyPartForQuickLoop( segmentRanks: Record, rundownRanks: Record, playlist: Pick, - studio: Pick, + studioSettings: IStudioSettings, quickLoopStartPosition: MarkerPosition | undefined, quickLoopEndPosition: MarkerPosition | undefined, canSetAutoNext = () => true @@ -60,7 +60,7 @@ export function modifyPartForQuickLoop( compareMarkerPositions(quickLoopStartPosition, partPosition) >= 0 && compareMarkerPositions(partPosition, quickLoopEndPosition) >= 0 - const fallbackPartDuration = studio.settings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION + const fallbackPartDuration = studioSettings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION if (isLoopingOverriden && (part.expectedDuration ?? 0) < fallbackPartDuration) { if (playlist.quickLoop?.forceAutoNext === ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION) { @@ -82,7 +82,7 @@ export function modifyPartInstanceForQuickLoop( segmentRanks: Record, rundownRanks: Record, playlist: Pick, - studio: Pick, + studioSettings: IStudioSettings, quickLoopStartPosition: MarkerPosition | undefined, quickLoopEndPosition: MarkerPosition | undefined ): void { @@ -107,7 +107,7 @@ export function modifyPartInstanceForQuickLoop( segmentRanks, rundownRanks, playlist, - studio, + studioSettings, quickLoopStartPosition, quickLoopEndPosition, canAutoNext // do not adjust the part instance if we have passed the time where we can still enable auto next diff --git a/meteor/server/publications/partInstancesUI/publication.ts b/meteor/server/publications/partInstancesUI/publication.ts index 553dea9808..54b1da3fb7 100644 --- a/meteor/server/publications/partInstancesUI/publication.ts +++ b/meteor/server/publications/partInstancesUI/publication.ts @@ -106,7 +106,7 @@ async function setupUIPartInstancesPublicationObservers( changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), }), - cache.Studios.find({}).observeChanges({ + cache.StudioSettings.find({}).observeChanges({ added: () => triggerUpdate({ invalidateQuickLoop: true }), changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), @@ -148,8 +148,8 @@ export async function manipulateUIPartInstancesPublicationData( const playlist = state.contentCache.RundownPlaylists.findOne({}) if (!playlist) return - const studio = state.contentCache.Studios.findOne({}) - if (!studio) return + const studioSettings = state.contentCache.StudioSettings.findOne({}) + if (!studioSettings) return const rundownRanks = stringsToIndexLookup(playlist.rundownIdsInOrder as unknown as string[]) const segmentRanks = extractRanks(state.contentCache.Segments.find({}).fetch()) @@ -191,7 +191,7 @@ export async function manipulateUIPartInstancesPublicationData( segmentRanks, rundownRanks, playlist, - studio, + studioSettings.settings, quickLoopStartPosition, quickLoopEndPosition ) diff --git a/meteor/server/publications/partInstancesUI/reactiveContentCache.ts b/meteor/server/publications/partInstancesUI/reactiveContentCache.ts index b9356fb6a1..a647ffd79c 100644 --- a/meteor/server/publications/partInstancesUI/reactiveContentCache.ts +++ b/meteor/server/publications/partInstancesUI/reactiveContentCache.ts @@ -3,8 +3,9 @@ import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict, MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' export type RundownPlaylistCompact = Pick export const rundownPlaylistFieldSpecifier = literal>({ @@ -27,14 +28,19 @@ export const partInstanceFieldSpecifier = literal>>({ _id: 1, - settings: 1, + settingsWithOverrides: 1, }) +export interface StudioSettingsDoc { + _id: StudioId + settings: IStudioSettings +} + export interface ContentCache { - Studios: ReactiveCacheCollection> + StudioSettings: ReactiveCacheCollection Segments: ReactiveCacheCollection> PartInstances: ReactiveCacheCollection> RundownPlaylists: ReactiveCacheCollection @@ -42,7 +48,7 @@ export interface ContentCache { export function createReactiveContentCache(): ContentCache { const cache: ContentCache = { - Studios: new ReactiveCacheCollection>('studios'), + StudioSettings: new ReactiveCacheCollection('studioSettings'), Segments: new ReactiveCacheCollection>('segments'), PartInstances: new ReactiveCacheCollection>('partInstances'), RundownPlaylists: new ReactiveCacheCollection('rundownPlaylists'), diff --git a/meteor/server/publications/partInstancesUI/rundownContentObserver.ts b/meteor/server/publications/partInstancesUI/rundownContentObserver.ts index 09c782f709..bc6e06ebfd 100644 --- a/meteor/server/publications/partInstancesUI/rundownContentObserver.ts +++ b/meteor/server/publications/partInstancesUI/rundownContentObserver.ts @@ -6,10 +6,21 @@ import { partInstanceFieldSpecifier, rundownPlaylistFieldSpecifier, segmentFieldSpecifier, + StudioFields, studioFieldSpecifier, + StudioSettingsDoc, } from './reactiveContentCache' import { PartInstances, RundownPlaylists, Segments, Studios } from '../../collections' import { waitForAllObserversReady } from '../lib/lib' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' + +function convertStudioSettingsDoc(doc: Pick): StudioSettingsDoc { + return { + _id: doc._id, + settings: applyAndValidateOverrides(doc.settingsWithOverrides).obj, + } +} export class RundownContentObserver { readonly #cache: ContentCache @@ -30,11 +41,23 @@ export class RundownContentObserver { logger.silly(`Creating RundownContentObserver for rundowns "${rundownIds.join(',')}"`) const observers = await waitForAllObserversReady([ - Studios.observeChanges( + Studios.observe( { _id: studioId, }, - cache.Studios.link(), + { + added: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + changed: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + removed: (doc) => { + cache.StudioSettings.remove(doc._id) + }, + }, { fields: studioFieldSpecifier, } diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 31af1ed031..6e5b051553 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -93,7 +93,7 @@ async function setupUIPartsPublicationObservers( changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), }), - cache.Studios.find({}).observeChanges({ + cache.StudioSettings.find({}).observeChanges({ added: () => triggerUpdate({ invalidateQuickLoop: true }), changed: () => triggerUpdate({ invalidateQuickLoop: true }), removed: () => triggerUpdate({ invalidateQuickLoop: true }), @@ -135,8 +135,8 @@ export async function manipulateUIPartsPublicationData( const playlist = state.contentCache.RundownPlaylists.findOne({}) if (!playlist) return - const studio = state.contentCache.Studios.findOne({}) - if (!studio) return + const studioSettings = state.contentCache.StudioSettings.findOne({}) + if (!studioSettings) return const rundownRanks = stringsToIndexLookup(playlist.rundownIdsInOrder as unknown as string[]) const segmentRanks = extractRanks(state.contentCache.Segments.find({}).fetch()) @@ -178,7 +178,7 @@ export async function manipulateUIPartsPublicationData( segmentRanks, rundownRanks, playlist, - studio, + studioSettings.settings, quickLoopStartPosition, quickLoopEndPosition ) diff --git a/meteor/server/publications/partsUI/reactiveContentCache.ts b/meteor/server/publications/partsUI/reactiveContentCache.ts index 13361fd51c..12d9423e8e 100644 --- a/meteor/server/publications/partsUI/reactiveContentCache.ts +++ b/meteor/server/publications/partsUI/reactiveContentCache.ts @@ -4,7 +4,8 @@ import { ReactiveCacheCollection } from '../lib/ReactiveCacheCollection' import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict, MongoFieldSpecifierZeroes } from '@sofie-automation/corelib/dist/mongo' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' export type RundownPlaylistCompact = Pick export const rundownPlaylistFieldSpecifier = literal>({ @@ -26,14 +27,19 @@ export const partFieldSpecifier = literal>>({ _id: 1, - settings: 1, + settingsWithOverrides: 1, }) +export interface StudioSettingsDoc { + _id: StudioId + settings: IStudioSettings +} + export interface ContentCache { - Studios: ReactiveCacheCollection> + StudioSettings: ReactiveCacheCollection Segments: ReactiveCacheCollection> Parts: ReactiveCacheCollection> RundownPlaylists: ReactiveCacheCollection @@ -41,7 +47,7 @@ export interface ContentCache { export function createReactiveContentCache(): ContentCache { const cache: ContentCache = { - Studios: new ReactiveCacheCollection>('studios'), + StudioSettings: new ReactiveCacheCollection('studioSettings'), Segments: new ReactiveCacheCollection>('segments'), Parts: new ReactiveCacheCollection>('parts'), RundownPlaylists: new ReactiveCacheCollection('rundownPlaylists'), diff --git a/meteor/server/publications/partsUI/rundownContentObserver.ts b/meteor/server/publications/partsUI/rundownContentObserver.ts index ee7e92c7d6..8a8032ecf5 100644 --- a/meteor/server/publications/partsUI/rundownContentObserver.ts +++ b/meteor/server/publications/partsUI/rundownContentObserver.ts @@ -6,10 +6,21 @@ import { partFieldSpecifier, rundownPlaylistFieldSpecifier, segmentFieldSpecifier, + StudioFields, studioFieldSpecifier, + StudioSettingsDoc, } from './reactiveContentCache' import { Parts, RundownPlaylists, Segments, Studios } from '../../collections' import { waitForAllObserversReady } from '../lib/lib' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' + +function convertStudioSettingsDoc(doc: Pick): StudioSettingsDoc { + return { + _id: doc._id, + settings: applyAndValidateOverrides(doc.settingsWithOverrides).obj, + } +} export class RundownContentObserver { readonly #cache: ContentCache @@ -29,11 +40,23 @@ export class RundownContentObserver { logger.silly(`Creating RundownContentObserver for rundowns "${rundownIds.join(',')}"`) const observers = await waitForAllObserversReady([ - Studios.observeChanges( + Studios.observe( { _id: studioId, }, - cache.Studios.link(), + { + added: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + changed: (doc) => { + const newDoc = convertStudioSettingsDoc(doc) + cache.StudioSettings.upsert(doc._id, { $set: newDoc as Partial }) + }, + removed: (doc) => { + cache.StudioSettings.remove(doc._id) + }, + }, { fields: studioFieldSpecifier, } diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index dbeb8a4e9e..dbead8658e 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -187,7 +187,7 @@ export type PieceContentStatusPiece = Pick { + extends Pick { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ @@ -196,6 +196,8 @@ export interface PieceContentStatusStudio * (These are used by the Package Manager and the Expected Packages) */ packageContainers: Record + + settings: IStudioSettings } export async function checkPieceContentStatusAndDependencies( diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index 591f1eb16e..32cf9328d1 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -13,7 +13,7 @@ import { PieceContentStatusStudio } from './checkPieceContentStatus' export type StudioFields = | '_id' - | 'settings' + | 'settingsWithOverrides' | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' @@ -21,7 +21,7 @@ export type StudioFields = | 'routeSetsWithOverrides' export const studioFieldSpecifier = literal>>({ _id: 1, - settings: 1, + settingsWithOverrides: 1, packageContainersWithOverrides: 1, previewContainerIds: 1, thumbnailContainerIds: 1, @@ -112,7 +112,7 @@ export async function fetchStudio(studioId: StudioId): Promise): UIStudio { name: studio.name, mappings: applyAndValidateOverrides(studio.mappingsWithOverrides).obj, - settings: studio.settings, + settings: applyAndValidateOverrides(studio.settingsWithOverrides).obj, routeSets: applyAndValidateOverrides(studio.routeSetsWithOverrides).obj, routeSetExclusivityGroups: applyAndValidateOverrides(studio.routeSetExclusivityGroupsWithOverrides).obj, @@ -47,14 +47,14 @@ type StudioFields = | '_id' | 'name' | 'mappingsWithOverrides' - | 'settings' + | 'settingsWithOverrides' | 'routeSetsWithOverrides' | 'routeSetExclusivityGroupsWithOverrides' const fieldSpecifier = literal>>({ _id: 1, name: 1, mappingsWithOverrides: 1, - settings: 1, + settingsWithOverrides: 1, routeSetsWithOverrides: 1, routeSetExclusivityGroupsWithOverrides: 1, }) diff --git a/meteor/server/publications/system.ts b/meteor/server/publications/system.ts index 94a8969027..de22dc25ff 100644 --- a/meteor/server/publications/system.ts +++ b/meteor/server/publications/system.ts @@ -13,16 +13,14 @@ meteorPublish(MeteorPubSub.coreSystem, async function (token: string | undefined fields: { // Include only specific fields in the result documents: _id: 1, - support: 1, systemInfo: 1, apm: 1, name: 1, logLevel: 1, serviceMessages: 1, blueprintId: 1, - cron: 1, logo: 1, - evaluations: 1, + settingsWithOverrides: 1, }, }) } diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index fd0c49c5d2..eccaa0a5e1 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -27,7 +27,8 @@ import type { StudioRouteSet, StudioRouteSetExclusivityGroup, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' -import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import type { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import type { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' export interface StudioBlueprintManifest extends BlueprintManifestBase { @@ -160,6 +161,8 @@ export interface BlueprintResultApplyStudioConfig { routeSetExclusivityGroups?: Record /** Package Containers */ packageContainers?: Record + + studioSettings?: IStudioSettings } export interface IStudioConfigPreset { diff --git a/packages/blueprints-integration/src/api/system.ts b/packages/blueprints-integration/src/api/system.ts index 078ab6ffed..a4c544d571 100644 --- a/packages/blueprints-integration/src/api/system.ts +++ b/packages/blueprints-integration/src/api/system.ts @@ -1,12 +1,32 @@ +import type { IBlueprintTriggeredActions } from '../triggers' import type { MigrationStepSystem } from '../migrations' import type { BlueprintManifestBase, BlueprintManifestType } from './base' +import type { ICoreSystemApplyConfigContext } from '../context/systemApplyConfigContext' +import type { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' export interface SystemBlueprintManifest extends BlueprintManifestBase { blueprintType: BlueprintManifestType.SYSTEM - /** A list of Migration steps related to the Core system */ + /** A list of Migration steps related to the Core system + * @deprecated This has been replaced with `applyConfig` + */ coreMigrations: MigrationStepSystem[] /** Translations connected to the studio (as stringified JSON) */ translations?: string + + /** + * Apply the config by generating the data to be saved into the db. + * This should be written to give a predictable and stable result, it can be called with the same config multiple times + */ + applyConfig?: ( + context: ICoreSystemApplyConfigContext + // config: TRawConfig, + ) => BlueprintResultApplySystemConfig +} + +export interface BlueprintResultApplySystemConfig { + settings: ICoreSystemSettings + + triggeredActions: IBlueprintTriggeredActions[] } diff --git a/packages/blueprints-integration/src/context/systemApplyConfigContext.ts b/packages/blueprints-integration/src/context/systemApplyConfigContext.ts new file mode 100644 index 0000000000..c1878ed75c --- /dev/null +++ b/packages/blueprints-integration/src/context/systemApplyConfigContext.ts @@ -0,0 +1,6 @@ +import type { IBlueprintDefaultCoreSystemTriggers } from '../triggers' +import type { ICommonContext } from './baseContext' + +export interface ICoreSystemApplyConfigContext extends ICommonContext { + getDefaultSystemActionTriggers(): IBlueprintDefaultCoreSystemTriggers +} diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts index d5196e59f7..4eb1fa41a5 100644 --- a/packages/blueprints-integration/src/index.ts +++ b/packages/blueprints-integration/src/index.ts @@ -28,3 +28,5 @@ export { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaType export * from '@sofie-automation/shared-lib/dist/lib/JSONBlob' export * from '@sofie-automation/shared-lib/dist/lib/JSONSchemaUtil' export * from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' +export * from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +export * from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index 3b7a54db85..c360fa6567 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -340,3 +340,27 @@ export interface IBlueprintTriggeredActions { } export { SomeActionIdentifier, ClientActions, PlayoutActions } + +export enum IBlueprintDefaultCoreSystemTriggersType { + toggleShelf = 'toggleShelf', + activateRundownPlaylist = 'activateRundownPlaylist', + activateRundownPlaylistRehearsal = 'activateRundownPlaylistRehearsal', + deactivateRundownPlaylist = 'deactivateRundownPlaylist', + take = 'take', + hold = 'hold', + holdUndo = 'holdUndo', + resetRundownPlaylist = 'resetRundownPlaylist', + disableNextPiece = 'disableNextPiece', + disableNextPieceUndo = 'disableNextPieceUndo', + createSnapshotForDebug = 'createSnapshotForDebug', + moveNextPart = 'moveNextPart', + moveNextSegment = 'moveNextSegment', + movePreviousPart = 'movePreviousPart', + movePreviousSegment = 'movePreviousSegment', + goToOnAirLine = 'goToOnAirLine', + rewindSegments = 'rewindSegments', +} + +export type IBlueprintDefaultCoreSystemTriggers = { + [key in IBlueprintDefaultCoreSystemTriggersType]: IBlueprintTriggeredActions +} diff --git a/packages/corelib/src/dataModel/Blueprint.ts b/packages/corelib/src/dataModel/Blueprint.ts index 99e025bfdf..32ba8af5e5 100644 --- a/packages/corelib/src/dataModel/Blueprint.ts +++ b/packages/corelib/src/dataModel/Blueprint.ts @@ -64,7 +64,7 @@ export interface Blueprint { export interface LastBlueprintConfig { blueprintId: BlueprintId blueprintHash: BlueprintHash - blueprintConfigPresetId: string + blueprintConfigPresetId: string | undefined config: IBlueprintConfig } diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index 241e0c3895..f9ff938c02 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -11,6 +11,7 @@ import { RundownId, } from './Ids' import { RundownPlaylistNote } from './Notes' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' /** Details of an ab-session requested by the blueprints in onTimelineGenerate */ export interface ABSessionInfo { @@ -80,15 +81,6 @@ export type QuickLoopMarker = | QuickLoopRundownMarker | QuickLoopPlaylistMarker -export enum ForceQuickLoopAutoNext { - /** Parts will auto-next only when explicitly set by the NRCS/blueprints */ - DISABLED = 'disabled', - /** Parts will auto-next when the expected duration is set and within range */ - ENABLED_WHEN_VALID_DURATION = 'enabled_when_valid_duration', - /** All parts will auto-next. If expected duration is undefined or low, the default display duration will be used */ - ENABLED_FORCING_MIN_DURATION = 'enabled_forcing_min_duration', -} - export interface QuickLoopProps { /** The Start marker */ start?: QuickLoopMarker diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index ba6d7233d2..5a440f9d18 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -3,7 +3,6 @@ import { ObjectWithOverrides } from '../settings/objectWithOverrides' import { StudioId, OrganizationId, BlueprintId, ShowStyleBaseId, MappingsHash, PeripheralDeviceId } from './Ids' import { BlueprintHash, LastBlueprintConfig } from './Blueprint' import { MappingsExt, MappingExt } from '@sofie-automation/shared-lib/dist/core/model/Timeline' -import { ForceQuickLoopAutoNext } from './RundownPlaylist' import { ResultingMappingRoute, RouteMapping, @@ -15,8 +14,9 @@ import { StudioAbPlayerDisabling, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' -export { MappingsExt, MappingExt, MappingsHash } +export { MappingsExt, MappingExt, MappingsHash, IStudioSettings } // RouteSet functions has been moved to shared-lib: // So we need to re-export them here: @@ -32,82 +32,6 @@ export { StudioPackageContainer, } -export interface IStudioSettings { - /** The framerate (frames per second) used to convert internal timing information (in milliseconds) - * into timecodes and timecode-like strings and interpret timecode user input - * Default: 25 - */ - frameRate: number - - /** URL to endpoint where media preview are exposed */ - mediaPreviewsUrl: string // (former media_previews_url in config) - /** URLs for slack webhook to send evaluations */ - slackEvaluationUrls?: string // (former slack_evaluation in config) - - /** Media Resolutions supported by the studio for media playback */ - supportedMediaFormats?: string // (former mediaResolutions in config) - /** Audio Stream Formats supported by the studio for media playback */ - supportedAudioStreams?: string // (former audioStreams in config) - - /** Should the play from anywhere feature be enabled in this studio */ - enablePlayFromAnywhere?: boolean - - /** - * If set, forces the multi-playout-gateway mode (aka set "now"-time right away) - * for single playout-gateways setups - */ - forceMultiGatewayMode?: boolean - - /** How much extra delay to add to the Now-time (used for the "multi-playout-gateway" feature) . - * A higher value adds delays in playout, but reduces the risk of missed frames. */ - multiGatewayNowSafeLatency?: number - - /** Allow resets while a rundown is on-air */ - allowRundownResetOnAir?: boolean - - /** Preserve unsynced segments psoition in the rundown, relative to the other segments */ - preserveOrphanedSegmentPositionInRundown?: boolean - - /** - * The minimum amount of time, in milliseconds, that must pass after a take before another take may be performed. - * Default: 1000 - */ - minimumTakeSpan: number - - /** Whether to allow adlib testing mode, before a Part is playing in a Playlist */ - allowAdlibTestingSegment?: boolean - - /** Should QuickLoop context menu options be available to the users. It does not affect Playlist loop enabled by the NRCS. */ - enableQuickLoop?: boolean - - /** If and how to force auto-nexting in a looping Playlist */ - forceQuickLoopAutoNext?: ForceQuickLoopAutoNext - - /** - * The duration to apply on too short Parts Within QuickLoop when ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION is selected - * Default: 3000 - */ - fallbackPartDuration?: number - - /** - * Whether to allow hold operations for Rundowns in this Studio - * When disabled, any action-triggers that would normally trigger a hold operation will be silently ignored - * This should only block entering hold, to ensure Sofie doesn't get stuck if it somehow gets into hold - */ - allowHold: boolean - - /** - * Whether to allow direct playing of a piece in the rundown - * This behaviour is usally triggered by double-clicking on a piece in the GUI - */ - allowPieceDirectPlay: boolean - - /** - * Enable buckets - the default behavior is to have buckets. - */ - enableBuckets: boolean -} - export type StudioLight = Omit /** A set of available layer groups in a given installation */ @@ -140,7 +64,7 @@ export interface DBStudio { /** Config values are used by the Blueprints */ blueprintConfigWithOverrides: ObjectWithOverrides - settings: IStudioSettings + settingsWithOverrides: ObjectWithOverrides _rundownVersionHash: string diff --git a/packages/corelib/src/studio/baseline.ts b/packages/corelib/src/studio/baseline.ts index d1766e1245..86492cf75c 100644 --- a/packages/corelib/src/studio/baseline.ts +++ b/packages/corelib/src/studio/baseline.ts @@ -1,4 +1,4 @@ -import { StudioLight } from '../dataModel/Studio' +import { DBStudio } from '../dataModel/Studio' import { TimelineComplete } from '../dataModel/Timeline' import { ReadonlyDeep } from 'type-fest' import { unprotectString } from '../protectedString' @@ -6,7 +6,7 @@ import { Blueprint } from '../dataModel/Blueprint' export function shouldUpdateStudioBaselineInner( coreVersion: string, - studio: ReadonlyDeep, + studio: Pick, studioTimeline: ReadonlyDeep | null, studioBlueprint: Pick | null ): string | false { diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index 57a861bb9f..d06fac5154 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -42,6 +42,7 @@ import { IDirectCollections } from '../db' import { ApmSpan, JobContext, + JobStudio, ProcessedShowStyleBase, ProcessedShowStyleCompound, ProcessedShowStyleVariant, @@ -56,6 +57,7 @@ import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlo import { removeRundownPlaylistFromDb } from '../ingest/__tests__/lib' import { processShowStyleBase, processShowStyleVariant } from '../jobs/showStyle' import { defaultStudio } from './defaultCollectionObjects' +import { convertStudioToJobStudio } from '../jobs/studio' export function setupDefaultJobEnvironment(studioId?: StudioId): MockJobContext { const { mockCollections, jobCollections } = getMockCollections() @@ -75,6 +77,7 @@ export class MockJobContext implements JobContext { #jobCollections: Readonly #mockCollections: Readonly #studio: ReadonlyDeep + #jobStudio: ReadonlyDeep #studioBlueprint: ReadonlyDeep #showStyleBlueprint: ReadonlyDeep @@ -87,6 +90,7 @@ export class MockJobContext implements JobContext { this.#jobCollections = jobCollections this.#mockCollections = mockCollections this.#studio = studio + this.#jobStudio = convertStudioToJobStudio(clone(studio)) this.#studioBlueprint = MockStudioBlueprint() this.#showStyleBlueprint = MockShowStyleBlueprint() @@ -103,7 +107,10 @@ export class MockJobContext implements JobContext { get studioId(): StudioId { return this.#studio._id } - get studio(): ReadonlyDeep { + get studio(): ReadonlyDeep { + return this.#jobStudio + } + get rawStudio(): ReadonlyDeep { return this.#studio } @@ -219,7 +226,7 @@ export class MockJobContext implements JobContext { } } getShowStyleBlueprintConfig(showStyle: ReadonlyDeep): ProcessedShowStyleConfig { - return preprocessShowStyleConfig(showStyle, this.#showStyleBlueprint, this.#studio.settings) + return preprocessShowStyleConfig(showStyle, this.#showStyleBlueprint, this.studio.settings) } hackPublishTimelineToFastTrack(_newTimeline: TimelineComplete): void { @@ -244,6 +251,7 @@ export class MockJobContext implements JobContext { setStudio(studio: ReadonlyDeep): void { this.#studio = clone(studio) + this.#jobStudio = convertStudioToJobStudio(clone(studio)) } setShowStyleBlueprint(blueprint: ReadonlyDeep): void { this.#showStyleBlueprint = blueprint diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index adef4895e2..ebd9942335 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -102,7 +102,7 @@ export function defaultStudio(_id: StudioId): DBStudio { mappingsWithOverrides: wrapDefaultObject({}), supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, @@ -110,7 +110,7 @@ export function defaultStudio(_id: StudioId): DBStudio { allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), diff --git a/packages/job-worker/src/blueprints/__tests__/config.test.ts b/packages/job-worker/src/blueprints/__tests__/config.test.ts index c46b7a2fba..3e7e8bef50 100644 --- a/packages/job-worker/src/blueprints/__tests__/config.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/config.test.ts @@ -10,15 +10,15 @@ describe('Test blueprint config', () => { test('compileStudioConfig', () => { const jobContext = setupDefaultJobEnvironment() jobContext.setStudio({ - ...jobContext.studio, - settings: { + ...jobContext.rawStudio, + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }), }) jobContext.updateStudioBlueprint({ @@ -36,15 +36,15 @@ describe('Test blueprint config', () => { test('compileStudioConfig with function', () => { const jobContext = setupDefaultJobEnvironment() jobContext.setStudio({ - ...jobContext.studio, - settings: { + ...jobContext.rawStudio, + settingsWithOverrides: wrapDefaultObject({ mediaPreviewsUrl: '', frameRate: 25, minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }), }) jobContext.updateStudioBlueprint({ @@ -142,7 +142,7 @@ describe('Test blueprint config', () => { const studioId = jobContext.studioId jobContext.setStudio({ - ...jobContext.studio, + ...jobContext.rawStudio, blueprintConfigWithOverrides: wrapDefaultObject({ two: 'abc', number: 99, @@ -189,7 +189,7 @@ describe('Test blueprint config', () => { }, }) jobContext.setStudio({ - ...jobContext.studio, + ...jobContext.rawStudio, supportedShowStyleBase: [showStyle._id], }) jobContext.updateShowStyleBlueprint({ diff --git a/packages/job-worker/src/blueprints/__tests__/context.test.ts b/packages/job-worker/src/blueprints/__tests__/context.test.ts index 307289c2df..5388b21303 100644 --- a/packages/job-worker/src/blueprints/__tests__/context.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context.test.ts @@ -1,6 +1,5 @@ import { getHash } from '@sofie-automation/corelib/dist/lib' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/context' import { getShowStyleConfigRef, getStudioConfigRef } from '../configRefs' import { CommonContext } from '../context/CommonContext' @@ -81,7 +80,7 @@ describe('Test blueprint api context', () => { expect(context.studio).toBe(studio) expect(context.getStudioConfig()).toBe(studioConfig) - expect(context.getStudioMappings()).toEqual(applyAndValidateOverrides(studio.mappingsWithOverrides).obj) + expect(context.getStudioMappings()).toEqual(studio.mappings) }) test('getStudioConfigRef', () => { const context = new StudioContext( diff --git a/packages/job-worker/src/blueprints/config.ts b/packages/job-worker/src/blueprints/config.ts index 77ae34389d..78b755419f 100644 --- a/packages/job-worker/src/blueprints/config.ts +++ b/packages/job-worker/src/blueprints/config.ts @@ -11,10 +11,9 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE import _ = require('underscore') import { logger } from '../logging' import { CommonContext } from './context' -import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { ProcessedShowStyleCompound, StudioCacheContext } from '../jobs' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { JobStudio, ProcessedShowStyleCompound, StudioCacheContext } from '../jobs' /** * Parse a string containing BlueprintConfigRefs (`${studio.studio0.myConfigField}`) to replace the refs with the current values @@ -100,10 +99,10 @@ export function compileCoreConfigValues(studioSettings: ReadonlyDeep, + studio: ReadonlyDeep, blueprint: ReadonlyDeep ): ProcessedStudioConfig { - let res: any = applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj + let res: any = studio.blueprintConfig try { if (blueprint.preprocessConfig) { diff --git a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts index 6c3f8cd30d..c71a4d33fe 100644 --- a/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTimelineGenerateContext.ts @@ -4,7 +4,6 @@ import { ITimelineEventContext, } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { OnGenerateTimelineObjExt } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { clone } from '@sofie-automation/corelib/dist/lib' @@ -14,7 +13,7 @@ import { getCurrentTime } from '../../lib' import { PieceInstance, ResolvedPieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' import _ = require('underscore') -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { convertPartInstanceToBlueprints, createBlueprintQuickLoopInfo } from './lib' import { RundownContext } from './RundownContext' import { AbSessionHelper } from '../../playout/abPlayback/abSessionHelper' @@ -33,7 +32,7 @@ export class OnTimelineGenerateContext extends RundownContext implements ITimeli readonly #pieceInstanceCache = new Map>() constructor( - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/PartEventContext.ts b/packages/job-worker/src/blueprints/context/PartEventContext.ts index 880aa4b923..34722ec3ac 100644 --- a/packages/job-worker/src/blueprints/context/PartEventContext.ts +++ b/packages/job-worker/src/blueprints/context/PartEventContext.ts @@ -1,11 +1,10 @@ import { IBlueprintPartInstance, IPartEventContext } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getCurrentTime } from '../../lib' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { convertPartInstanceToBlueprints } from './lib' import { RundownContext } from './RundownContext' @@ -14,7 +13,7 @@ export class PartEventContext extends RundownContext implements IPartEventContex constructor( eventName: string, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/RundownContext.ts b/packages/job-worker/src/blueprints/context/RundownContext.ts index 8faaefeba1..c84a27ac70 100644 --- a/packages/job-worker/src/blueprints/context/RundownContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownContext.ts @@ -1,10 +1,9 @@ import { IRundownContext, IBlueprintSegmentRundown } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { convertRundownToBlueprintSegmentRundown } from './lib' import { ContextInfo } from './CommonContext' import { ShowStyleContext } from './ShowStyleContext' @@ -19,7 +18,7 @@ export class RundownContext extends ShowStyleContext implements IRundownContext constructor( contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/RundownEventContext.ts b/packages/job-worker/src/blueprints/context/RundownEventContext.ts index b28f533063..9852e0dd72 100644 --- a/packages/job-worker/src/blueprints/context/RundownEventContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownEventContext.ts @@ -1,15 +1,14 @@ import { IEventContext } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { getCurrentTime } from '../../lib' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { RundownContext } from './RundownContext' export class RundownEventContext extends RundownContext implements IEventContext { constructor( - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, showStyleCompound: ReadonlyDeep, showStyleBlueprintConfig: ProcessedShowStyleConfig, diff --git a/packages/job-worker/src/blueprints/context/ShowStyleContext.ts b/packages/job-worker/src/blueprints/context/ShowStyleContext.ts index 7a243320b5..1310c72f72 100644 --- a/packages/job-worker/src/blueprints/context/ShowStyleContext.ts +++ b/packages/job-worker/src/blueprints/context/ShowStyleContext.ts @@ -1,9 +1,8 @@ import { IOutputLayer, IShowStyleContext, ISourceLayer } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ProcessedStudioConfig, ProcessedShowStyleConfig } from '../config' import { getShowStyleConfigRef } from '../configRefs' -import { ProcessedShowStyleCompound } from '../../jobs' +import { JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { ContextInfo } from './CommonContext' import { StudioContext } from './StudioContext' @@ -12,7 +11,7 @@ import { StudioContext } from './StudioContext' export class ShowStyleContext extends StudioContext implements IShowStyleContext { constructor( contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig, public readonly showStyleCompound: ReadonlyDeep, public readonly showStyleBlueprintConfig: ProcessedShowStyleConfig diff --git a/packages/job-worker/src/blueprints/context/StudioContext.ts b/packages/job-worker/src/blueprints/context/StudioContext.ts index f1627c483b..8d5915d338 100644 --- a/packages/job-worker/src/blueprints/context/StudioContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioContext.ts @@ -1,21 +1,18 @@ import { IStudioContext, BlueprintMappings } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProcessedStudioConfig } from '../config' import { getStudioConfigRef } from '../configRefs' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { CommonContext, ContextInfo } from './CommonContext' +import { JobStudio } from '../../jobs' /** Studio */ export class StudioContext extends CommonContext implements IStudioContext { - #processedMappings: ReadonlyDeep | undefined - constructor( contextInfo: ContextInfo, - public readonly studio: ReadonlyDeep, + public readonly studio: ReadonlyDeep, public readonly studioBlueprintConfig: ProcessedStudioConfig ) { super(contextInfo) @@ -36,10 +33,8 @@ export class StudioContext extends CommonContext implements IStudioContext { return getStudioConfigRef(this.studio._id, configKey) } getStudioMappings(): Readonly { - if (!this.#processedMappings) { - this.#processedMappings = applyAndValidateOverrides(this.studio.mappingsWithOverrides).obj - } + const mappings = this.studio.mappings // @ts-expect-error ProtectedString deviceId not compatible with string - return this.#processedMappings + return mappings } } diff --git a/packages/job-worker/src/blueprints/context/StudioUserContext.ts b/packages/job-worker/src/blueprints/context/StudioUserContext.ts index be2c471dc4..fff5232cd1 100644 --- a/packages/job-worker/src/blueprints/context/StudioUserContext.ts +++ b/packages/job-worker/src/blueprints/context/StudioUserContext.ts @@ -1,17 +1,17 @@ import { IStudioUserContext, NoteSeverity } from '@sofie-automation/blueprints-integration' import { ReadonlyDeep } from 'type-fest' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { ProcessedStudioConfig } from '../config' import { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' import { ContextInfo } from './CommonContext' import { StudioContext } from './StudioContext' +import { JobStudio } from '../../jobs' export class StudioUserContext extends StudioContext implements IStudioUserContext { public readonly notes: INoteBase[] = [] constructor( contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, studioBlueprintConfig: ProcessedStudioConfig ) { super(contextInfo, studio, studioBlueprintConfig) diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 1515ae71ec..efef608cc8 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -25,8 +25,7 @@ import { convertPartialBlueprintMutablePartToCore, } from './lib' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { JobContext, ProcessedShowStyleCompound } from '../../jobs' +import { JobContext, JobStudio, ProcessedShowStyleCompound } from '../../jobs' import { PieceTimelineObjectsBlob, serializePieceTimelineObjectsBlob, @@ -44,7 +43,7 @@ export class SyncIngestUpdateToPartInstanceContext constructor( private readonly _context: JobContext, contextInfo: ContextInfo, - studio: ReadonlyDeep, + studio: ReadonlyDeep, showStyleCompound: ReadonlyDeep, rundown: ReadonlyDeep, partInstance: PlayoutPartInstanceModel, diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 444c8edc37..04d08f25c6 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -29,7 +29,6 @@ import { DatastorePersistenceMode } from '@sofie-automation/shared-lib/dist/core import { removeTimelineDatastoreValue, setTimelineDatastoreValue } from '../../playout/datastore' import { executePeripheralDeviceAction, listPlayoutDevices } from '../../peripheralDevice' import { ActionPartChange, PartAndPieceInstanceActionService } from './services/PartAndPieceInstanceActionService' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { setNextPartFromPart } from '../../playout/setNext' @@ -201,7 +200,8 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct } async listRouteSets(): Promise> { - return applyAndValidateOverrides(this._context.studio.routeSetsWithOverrides).obj + // Discard ReadonlyDeep wrapper + return this._context.studio.routeSets as Record } async switchRouteSet(routeSetId: string, state: boolean | 'toggle'): Promise { diff --git a/packages/job-worker/src/ingest/__tests__/ingest.test.ts b/packages/job-worker/src/ingest/__tests__/ingest.test.ts index e46a0f827b..a9d96c0c97 100644 --- a/packages/job-worker/src/ingest/__tests__/ingest.test.ts +++ b/packages/job-worker/src/ingest/__tests__/ingest.test.ts @@ -47,6 +47,7 @@ import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel' import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache' import { wrapGenericIngestJob, wrapGenericIngestJobWithPrecheck } from '../jobWrappers' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' const handleRemovedRundownWrapped = wrapGenericIngestJob(handleRemovedRundown) const handleUpdatedRundownWrapped = wrapGenericIngestJob(handleUpdatedRundown) @@ -121,11 +122,11 @@ describe('Test ingest actions for rundowns and segments', () => { const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, - settings: { + ...context.rawStudio, + settingsWithOverrides: wrapDefaultObject({ ...context.studio.settings, minimumTakeSpan: 0, - }, + }), supportedShowStyleBase: [showStyleCompound._id], }) diff --git a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts index 2c6e7d8ce4..cb63251f09 100644 --- a/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts +++ b/packages/job-worker/src/ingest/__tests__/selectShowStyleVariant.test.ts @@ -35,7 +35,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -57,7 +57,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [], }) @@ -76,7 +76,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -118,7 +118,7 @@ describe('selectShowStyleVariant', () => { const showStyleCompoundVariant2 = await setupMockShowStyleVariant(context, showStyleCompound._id) const showStyleCompound2 = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id, showStyleCompound2._id], }) @@ -153,7 +153,7 @@ describe('selectShowStyleVariant', () => { test('no show style bases', async () => { const context = setupDefaultJobEnvironment() context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [protectString('fakeId')], }) @@ -176,7 +176,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -201,7 +201,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -226,7 +226,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) @@ -251,7 +251,7 @@ describe('selectShowStyleVariant', () => { const context = setupDefaultJobEnvironment() const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) diff --git a/packages/job-worker/src/ingest/expectedPackages.ts b/packages/job-worker/src/ingest/expectedPackages.ts index c1d6099a9e..b94c6498b9 100644 --- a/packages/job-worker/src/ingest/expectedPackages.ts +++ b/packages/job-worker/src/ingest/expectedPackages.ts @@ -41,9 +41,8 @@ import { updateExpectedPlayoutItemsForPartModel, updateExpectedPlayoutItemsForRundownBaseline, } from './expectedPlayoutItems' -import { JobContext } from '../jobs' +import { JobContext, JobStudio } from '../jobs' import { ExpectedPackageForIngestModelBaseline, IngestModel } from './model/IngestModel' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { IngestPartModel } from './model/IngestPartModel' import { clone } from '@sofie-automation/corelib/dist/lib' @@ -160,7 +159,7 @@ export async function updateExpectedPackagesForRundownBaseline( } function generateExpectedPackagesForPiece( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, segmentId: SegmentId, pieces: ReadonlyDeep[], @@ -186,7 +185,7 @@ function generateExpectedPackagesForPiece( return packages } function generateExpectedPackagesForBaselineAdlibPiece( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, pieces: ReadonlyDeep ) { @@ -207,7 +206,7 @@ function generateExpectedPackagesForBaselineAdlibPiece( return packages } function generateExpectedPackagesForAdlibAction( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, segmentId: SegmentId, actions: ReadonlyDeep @@ -231,7 +230,7 @@ function generateExpectedPackagesForAdlibAction( return packages } function generateExpectedPackagesForBaselineAdlibAction( - studio: ReadonlyDeep, + studio: ReadonlyDeep, rundownId: RundownId, actions: ReadonlyDeep ) { @@ -251,7 +250,7 @@ function generateExpectedPackagesForBaselineAdlibAction( } return packages } -function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, adlibs: BucketAdLib[]) { +function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, adlibs: BucketAdLib[]) { const packages: ExpectedPackageDBFromBucketAdLib[] = [] for (const adlib of adlibs) { if (adlib.expectedPackages) { @@ -270,7 +269,7 @@ function generateExpectedPackagesForBucketAdlib(studio: ReadonlyDeep, return packages } function generateExpectedPackagesForBucketAdlibAction( - studio: ReadonlyDeep, + studio: ReadonlyDeep, adlibActions: BucketAdLibAction[] ) { const packages: ExpectedPackageDBFromBucketAdLibAction[] = [] @@ -291,7 +290,7 @@ function generateExpectedPackagesForBucketAdlibAction( return packages } function generateExpectedPackageBases( - studio: ReadonlyDeep, + studio: ReadonlyDeep, ownerId: | PieceId | AdLibActionId diff --git a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts index f251bf1586..9f1907ab44 100644 --- a/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts +++ b/packages/job-worker/src/ingest/mosDevice/__tests__/mosIngest.test.ts @@ -87,7 +87,7 @@ describe('Test recieved mos ingest payloads', () => { const showStyleCompound = await setupMockShowStyleCompound(context) context.setStudio({ - ...context.studio, + ...context.rawStudio, supportedShowStyleBase: [showStyleCompound._id], }) diff --git a/packages/job-worker/src/jobs/index.ts b/packages/job-worker/src/jobs/index.ts index f8ab90e9f4..41103a4c23 100644 --- a/packages/job-worker/src/jobs/index.ts +++ b/packages/job-worker/src/jobs/index.ts @@ -18,9 +18,11 @@ import { PlaylistLock, RundownLock } from './lock' import { BaseModel } from '../modelBase' import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timeline' import { ProcessedShowStyleBase, ProcessedShowStyleVariant, ProcessedShowStyleCompound } from './showStyle' +import { JobStudio } from './studio' export { ApmSpan } export { ProcessedShowStyleVariant, ProcessedShowStyleBase, ProcessedShowStyleCompound } +export { JobStudio } /** * Context for any job run in the job-worker @@ -104,7 +106,12 @@ export interface StudioCacheContext { /** * The Studio the job belongs to */ - readonly studio: ReadonlyDeep + readonly studio: ReadonlyDeep + + /** + * The Studio the job belongs to + */ + readonly rawStudio: ReadonlyDeep /** * Blueprint for the studio the job belongs to diff --git a/packages/job-worker/src/jobs/studio.ts b/packages/job-worker/src/jobs/studio.ts new file mode 100644 index 0000000000..00a2f87893 --- /dev/null +++ b/packages/job-worker/src/jobs/studio.ts @@ -0,0 +1,58 @@ +import type { + IBlueprintConfig, + StudioRouteSet, + StudioRouteSetExclusivityGroup, +} from '@sofie-automation/blueprints-integration' +import type { DBStudio, IStudioSettings, MappingsExt } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { omit } from '@sofie-automation/corelib/dist/lib' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' + +/** + * A lightly processed version of DBStudio, with any ObjectWithOverrides pre-flattened + */ +export interface JobStudio + extends Omit< + DBStudio, + | 'mappingsWithOverrides' + | 'blueprintConfigWithOverrides' + | 'settingsWithOverrides' + | 'routeSetsWithOverrides' + | 'routeSetExclusivityGroupsWithOverrides' + | 'packageContainersWithOverrides' + > { + /** Mappings between the physical devices / outputs and logical ones */ + mappings: MappingsExt + + /** Config values are used by the Blueprints */ + blueprintConfig: IBlueprintConfig + + settings: IStudioSettings + + routeSets: Record + routeSetExclusivityGroups: Record + + // /** Contains settings for which Package Containers are present in the studio. + // * (These are used by the Package Manager and the Expected Packages) + // */ + // packageContainers: Record +} + +export function convertStudioToJobStudio(studio: DBStudio): JobStudio { + return { + ...omit( + studio, + 'mappingsWithOverrides', + 'blueprintConfigWithOverrides', + 'settingsWithOverrides', + 'routeSetsWithOverrides', + 'routeSetExclusivityGroupsWithOverrides', + 'packageContainersWithOverrides' + ), + mappings: applyAndValidateOverrides(studio.mappingsWithOverrides).obj, + blueprintConfig: applyAndValidateOverrides(studio.blueprintConfigWithOverrides).obj, + settings: applyAndValidateOverrides(studio.settingsWithOverrides).obj, + routeSets: applyAndValidateOverrides(studio.routeSetsWithOverrides).obj, + routeSetExclusivityGroups: applyAndValidateOverrides(studio.routeSetExclusivityGroupsWithOverrides).obj, + // packageContainers: applyAndValidateOverrides(studio.packageContainersWithOverrides).obj, + } +} diff --git a/packages/job-worker/src/playout/__tests__/playout.test.ts b/packages/job-worker/src/playout/__tests__/playout.test.ts index 01c63b2d17..93ab3ef753 100644 --- a/packages/job-worker/src/playout/__tests__/playout.test.ts +++ b/packages/job-worker/src/playout/__tests__/playout.test.ts @@ -47,6 +47,7 @@ import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheral import { ProcessedShowStyleCompound } from '../../jobs' import { handleOnPlayoutPlaybackChanged } from '../timings' import { sleep } from '@sofie-automation/shared-lib/dist/lib/lib' +import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' // const mockGetCurrentTime = jest.spyOn(lib, 'getCurrentTime') const mockExecutePeripheralDeviceFunction = jest @@ -98,11 +99,11 @@ describe('Playout API', () => { context = setupDefaultJobEnvironment() context.setStudio({ - ...context.studio, - settings: { + ...context.rawStudio, + settingsWithOverrides: wrapDefaultObject({ ...context.studio.settings, minimumTakeSpan: 0, - }, + }), }) // Ignore event jobs diff --git a/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts b/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts index bdf8a6dbec..f4efb9d410 100644 --- a/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts +++ b/packages/job-worker/src/playout/__tests__/selectNextPart.test.ts @@ -8,7 +8,8 @@ import { MockJobContext, setupDefaultJobEnvironment } from '../../__mocks__/cont import { PlayoutSegmentModelImpl } from '../model/implementation/PlayoutSegmentModelImpl' import { PlayoutSegmentModel } from '../model/PlayoutSegmentModel' import { selectNextPart } from '../selectNextPart' -import { ForceQuickLoopAutoNext, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' class MockPart { constructor( diff --git a/packages/job-worker/src/playout/abPlayback/index.ts b/packages/job-worker/src/playout/abPlayback/index.ts index d803ecac46..6c2a1730c2 100644 --- a/packages/job-worker/src/playout/abPlayback/index.ts +++ b/packages/job-worker/src/playout/abPlayback/index.ts @@ -17,7 +17,6 @@ import { AbSessionHelper } from './abSessionHelper' import { ShowStyleContext } from '../../blueprints/context' import { logger } from '../../logging' import { ABPlayerDefinition } from '@sofie-automation/blueprints-integration' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { abPoolFilterDisabled, findPlayersInRouteSets } from './routeSetDisabling' /** @@ -73,7 +72,7 @@ export function applyAbPlaybackForTimeline( const now = getCurrentTime() const abConfiguration = blueprint.blueprint.getAbResolverConfiguration(blueprintContext) - const routeSetMembers = findPlayersInRouteSets(applyAndValidateOverrides(context.studio.routeSetsWithOverrides).obj) + const routeSetMembers = findPlayersInRouteSets(context.studio.routeSets) for (const [poolName, players] of Object.entries(abConfiguration.pools)) { // Filter out offline devices diff --git a/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts b/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts index d6be0a4ab6..9f71f1d0ac 100644 --- a/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts +++ b/packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts @@ -1,6 +1,7 @@ import type { ABPlayerDefinition } from '@sofie-automation/blueprints-integration' import type { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio' import { logger } from '../../logging' +import { ReadonlyDeep } from 'type-fest' /** * Map> @@ -8,9 +9,9 @@ import { logger } from '../../logging' */ type MembersOfRouteSets = Map> -export function findPlayersInRouteSets(routeSets: Record): MembersOfRouteSets { +export function findPlayersInRouteSets(routeSets: ReadonlyDeep>): MembersOfRouteSets { const routeSetEnabledPlayers: MembersOfRouteSets = new Map() - for (const [_key, routeSet] of Object.entries(routeSets)) { + for (const [_key, routeSet] of Object.entries>(routeSets)) { for (const abPlayer of routeSet.abPlayers) { let poolEntry = routeSetEnabledPlayers.get(abPlayer.poolName) if (!poolEntry) { diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index c3f7d174a7..e4f6d77bbe 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -51,7 +51,7 @@ describe('Lookahead', () => { } } context.setStudio({ - ...context.studio, + ...context.rawStudio, mappingsWithOverrides: wrapDefaultObject(mappings), }) @@ -222,7 +222,7 @@ describe('Lookahead', () => { // Set really low { - const studio = clone(context.studio) + const studio = clone(context.rawStudio) studio.mappingsWithOverrides.defaults['WHEN_CLEAR'].lookaheadMaxSearchDistance = 0 studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = 0 context.setStudio(studio) @@ -236,7 +236,7 @@ describe('Lookahead', () => { // really high getOrderedPartsAfterPlayheadMock.mockClear() { - const studio = clone(context.studio) + const studio = clone(context.rawStudio) studio.mappingsWithOverrides.defaults['WHEN_CLEAR'].lookaheadMaxSearchDistance = -1 studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = 2000 context.setStudio(studio) @@ -250,7 +250,7 @@ describe('Lookahead', () => { // unset getOrderedPartsAfterPlayheadMock.mockClear() { - const studio = clone(context.studio) + const studio = clone(context.rawStudio) studio.mappingsWithOverrides.defaults['WHEN_CLEAR'].lookaheadMaxSearchDistance = undefined studio.mappingsWithOverrides.defaults['PRELOAD'].lookaheadMaxSearchDistance = -1 context.setStudio(studio) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts index 4c3cc76bd8..969506ce1a 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/util.test.ts @@ -11,7 +11,8 @@ import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObje import _ = require('underscore') import { wrapPartToTemporaryInstance } from '../../../__mocks__/partinstance' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ForceQuickLoopAutoNext, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' describe('getOrderedPartsAfterPlayhead', () => { let context!: MockJobContext @@ -37,7 +38,7 @@ describe('getOrderedPartsAfterPlayhead', () => { } } context.setStudio({ - ...context.studio, + ...context.rawStudio, mappingsWithOverrides: wrapDefaultObject(mappings), }) diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 893d6eb174..12ac4935ec 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -22,7 +22,6 @@ import { LOOKAHEAD_DEFAULT_SEARCH_DISTANCE } from '@sofie-automation/shared-lib/ import { prefixSingleObjectId } from '../lib' import { LookaheadTimelineObject } from './findObjects' import { hasPieceInstanceDefinitelyEnded } from '../timeline/lib' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { ReadonlyDeep } from 'type-fest' @@ -65,8 +64,8 @@ export async function getLookeaheadObjects( partInstancesInfo0: SelectedPartInstancesTimelineInfo ): Promise> { const span = context.startSpan('getLookeaheadObjects') - const allMappings = applyAndValidateOverrides(context.studio.mappingsWithOverrides) - const mappingsToConsider = Object.entries(allMappings.obj).filter( + const allMappings = context.studio.mappings + const mappingsToConsider = Object.entries(allMappings).filter( ([_id, map]) => map.lookahead !== LookaheadMode.NONE && map.lookahead !== undefined ) if (mappingsToConsider.length === 0) { diff --git a/packages/job-worker/src/playout/model/services/QuickLoopService.ts b/packages/job-worker/src/playout/model/services/QuickLoopService.ts index eb7bb0c6e1..b9d252ca7a 100644 --- a/packages/job-worker/src/playout/model/services/QuickLoopService.ts +++ b/packages/job-worker/src/playout/model/services/QuickLoopService.ts @@ -1,11 +1,11 @@ import { MarkerPosition, compareMarkerPositions } from '@sofie-automation/corelib/dist/playout/playlist' import { PlayoutModelReadonly } from '../PlayoutModel' import { - ForceQuickLoopAutoNext, QuickLoopMarker, QuickLoopMarkerType, QuickLoopProps, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { ReadonlyObjectDeep } from 'type-fest/source/readonly-deep' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/packages/job-worker/src/playout/selectNextPart.ts b/packages/job-worker/src/playout/selectNextPart.ts index 9b0ae14dd7..892c88aecf 100644 --- a/packages/job-worker/src/playout/selectNextPart.ts +++ b/packages/job-worker/src/playout/selectNextPart.ts @@ -1,11 +1,8 @@ import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { JobContext } from '../jobs' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' -import { - DBRundownPlaylist, - ForceQuickLoopAutoNext, - QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' import { PlayoutSegmentModel } from './model/PlayoutSegmentModel' diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 30011cd545..1a49c202a9 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -1,5 +1,5 @@ import { BlueprintId, TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { JobContext } from '../../jobs' +import { JobContext, JobStudio } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { BlueprintResultBaseline, @@ -36,7 +36,6 @@ import { WatchedPackagesHelper } from '../../blueprints/context/watchedPackages' import { postProcessStudioBaselineObjects } from '../../blueprints/postProcess' import { updateBaselineExpectedPackagesOnStudio } from '../../ingest/expectedPackages' import { endTrace, sendTrace, startTrace } from '@sofie-automation/corelib/dist/influxdb' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { convertResolvedPieceInstanceToBlueprints } from '../../blueprints/context/lib' import { buildTimelineObjsForRundown, RundownTimelineTimingContext } from './rundown' @@ -54,7 +53,7 @@ function isModelForStudio(model: StudioPlayoutModelBase): model is StudioPlayout } function generateTimelineVersions( - studio: ReadonlyDeep, + studio: ReadonlyDeep, blueprintId: BlueprintId | undefined, blueprintVersion: string ): TimelineCompleteGenerationVersions { diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index 66fba34cfa..fda503f079 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -1,6 +1,7 @@ import { BlueprintMapping, BlueprintMappings, + IStudioSettings, JSONBlobParse, StudioRouteBehavior, TSR, @@ -26,6 +27,7 @@ import { compileCoreConfigValues } from '../blueprints/config' import { CommonContext } from '../blueprints/context' import { JobContext } from '../jobs' import { FixUpBlueprintConfigContext } from '@sofie-automation/corelib/dist/fixUpBlueprintConfig/context' +import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' /** * Run the Blueprint applyConfig for the studio @@ -41,7 +43,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data name: 'applyConfig', identifier: `studio:${context.studioId},blueprint:${blueprint.blueprintId}`, }) - const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj + const rawBlueprintConfig = context.studio.blueprintConfig const result = blueprint.blueprint.applyConfig( blueprintContext, @@ -109,8 +111,18 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data ]) ) + const studioSettings: IStudioSettings = result.studioSettings ?? { + frameRate: 25, + mediaPreviewsUrl: '', + minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, + allowHold: true, + allowPieceDirectPlay: true, + enableBuckets: true, + } + await context.directCollections.Studios.update(context.studioId, { $set: { + 'settingsWithOverrides.defaults': studioSettings, 'mappingsWithOverrides.defaults': translateMappings(result.mappings), 'peripheralDeviceSettings.playoutDevices.defaults': playoutDevices, 'peripheralDeviceSettings.ingestDevices.defaults': ingestDevices, @@ -158,7 +170,7 @@ export async function handleBlueprintValidateConfigForStudio( name: 'validateConfig', identifier: `studio:${context.studioId},blueprint:${blueprint.blueprintId}`, }) - const rawBlueprintConfig = applyAndValidateOverrides(context.studio.blueprintConfigWithOverrides).obj + const rawBlueprintConfig = applyAndValidateOverrides(context.rawStudio.blueprintConfigWithOverrides).obj // This clone seems excessive, but without it a DataCloneError is generated when posting the result to the parent const messages = clone(blueprint.blueprint.validateConfig(blueprintContext, rawBlueprintConfig)) @@ -200,7 +212,7 @@ export async function handleBlueprintFixUpConfigForStudio( const blueprintContext = new FixUpBlueprintConfigContext( commonContext, JSONBlobParse(blueprint.blueprint.studioConfigSchema), - context.studio.blueprintConfigWithOverrides + context.rawStudio.blueprintConfigWithOverrides ) blueprint.blueprint.fixUpConfig(blueprintContext) diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index c5efcae563..3bc545794b 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -1,10 +1,7 @@ import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundown, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { - DBRundownPlaylist, - ForceQuickLoopAutoNext, - QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { clone, getHash, @@ -27,12 +24,11 @@ import { IBlueprintRundown, NoteSeverity, } from '@sofie-automation/blueprints-integration' -import { JobContext } from './jobs' +import { JobContext, JobStudio } from './jobs' import { logger } from './logging' import { resetRundownPlaylist } from './playout/lib' import { runJobWithPlaylistLock, runWithPlayoutModel } from './playout/lock' import { updateTimeline } from './playout/timeline/generate' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { WrappedStudioBlueprint } from './blueprints/cache' import { StudioUserContext } from './blueprints/context' import { getCurrentTime } from './lib' @@ -315,7 +311,7 @@ export function produceRundownPlaylistInfoFromRundown( function defaultPlaylistForRundown( rundown: ReadonlyDeep, - studio: ReadonlyDeep, + studio: ReadonlyDeep, existingPlaylist?: ReadonlyDeep ): Omit { return { diff --git a/packages/job-worker/src/workers/caches.ts b/packages/job-worker/src/workers/caches.ts index f252a9a0de..6fee8f1aed 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -15,8 +15,9 @@ import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { clone, deepFreeze } from '@sofie-automation/corelib/dist/lib' import { logger } from '../logging' import deepmerge = require('deepmerge') -import { ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs' +import { JobStudio, ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs' import { StudioCacheContextImpl } from './context/StudioCacheContextImpl' +import { convertStudioToJobStudio } from '../jobs/studio' /** * A Wrapper to maintain a cache and provide a context using the cache when appropriate @@ -43,7 +44,7 @@ export class WorkerDataCacheWrapperImpl implements WorkerDataCacheWrapper { * The StudioId the cache is maintained for */ get studioId(): StudioId { - return this.#dataCache.studio._id + return this.#dataCache.rawStudio._id } constructor(collections: IDirectCollections, dataCache: WorkerDataCache) { @@ -99,7 +100,8 @@ export class WorkerDataCacheWrapperImpl implements WorkerDataCacheWrapper { * This is a reusable cache of these properties */ export interface WorkerDataCache { - studio: ReadonlyDeep + rawStudio: ReadonlyDeep + jobStudio: ReadonlyDeep studioBlueprint: ReadonlyDeep studioBlueprintConfig: ProcessedStudioConfig | undefined @@ -133,12 +135,16 @@ export async function loadWorkerDataCache( studioId: StudioId ): Promise { // Load some 'static' data from the db - const studio = deepFreeze(await collections.Studios.findOne(studioId)) - if (!studio) throw new Error('Missing studio') + const dbStudio = await collections.Studios.findOne(studioId) + if (!dbStudio) throw new Error('Missing studio') + const studio = deepFreeze(dbStudio) const studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, studio) + const jobStudio = deepFreeze(convertStudioToJobStudio(dbStudio)) + return { - studio, + rawStudio: studio, + jobStudio: jobStudio, studioBlueprint, studioBlueprintConfig: undefined, @@ -157,11 +163,12 @@ export async function invalidateWorkerDataCache( if (data.forceAll) { // Clear everything! - const newStudio = await collections.Studios.findOne(cache.studio._id) + const newStudio = await collections.Studios.findOne(cache.rawStudio._id) if (!newStudio) throw new Error(`Studio is missing during cache invalidation!`) - cache.studio = deepFreeze(newStudio) + cache.rawStudio = deepFreeze(newStudio) + cache.jobStudio = deepFreeze(convertStudioToJobStudio(newStudio)) - cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio) + cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.rawStudio) cache.studioBlueprintConfig = undefined cache.showStyleBases.clear() @@ -176,25 +183,26 @@ export async function invalidateWorkerDataCache( if (data.studio) { logger.debug('WorkerDataCache: Reloading studio') - const newStudio = await collections.Studios.findOne(cache.studio._id) + const newStudio = await collections.Studios.findOne(cache.rawStudio._id) if (!newStudio) throw new Error(`Studio is missing during cache invalidation!`) // If studio blueprintId changed, then force it to be reloaded - if (newStudio.blueprintId !== cache.studio.blueprintId) updateStudioBlueprint = true + if (newStudio.blueprintId !== cache.rawStudio.blueprintId) updateStudioBlueprint = true - cache.studio = deepFreeze(newStudio) + cache.rawStudio = deepFreeze(newStudio) + cache.jobStudio = deepFreeze(convertStudioToJobStudio(newStudio)) cache.studioBlueprintConfig = undefined } // Check if studio blueprint was in the changed list - if (!updateStudioBlueprint && cache.studio.blueprintId) { - updateStudioBlueprint = data.blueprints.includes(cache.studio.blueprintId) + if (!updateStudioBlueprint && cache.rawStudio.blueprintId) { + updateStudioBlueprint = data.blueprints.includes(cache.rawStudio.blueprintId) } // Reload studioBlueprint if (updateStudioBlueprint) { logger.debug('WorkerDataCache: Reloading studioBlueprint') - cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.studio) + cache.studioBlueprint = await loadStudioBlueprintOrPlaceholder(collections, cache.rawStudio) cache.studioBlueprintConfig = undefined } @@ -210,7 +218,7 @@ export async function invalidateWorkerDataCache( if (data.studio) { // Ensure showStyleBases & showStyleVariants are all still valid for the studio - const allowedBases = new Set(cache.studio.supportedShowStyleBase) + const allowedBases = new Set(cache.rawStudio.supportedShowStyleBase) for (const id of Array.from(cache.showStyleBases.keys())) { if (!allowedBases.has(id)) { diff --git a/packages/job-worker/src/workers/context/JobContextImpl.ts b/packages/job-worker/src/workers/context/JobContextImpl.ts index 7be35b55f2..8cd15572dc 100644 --- a/packages/job-worker/src/workers/context/JobContextImpl.ts +++ b/packages/job-worker/src/workers/context/JobContextImpl.ts @@ -1,5 +1,5 @@ import { IDirectCollections } from '../../db' -import { JobContext } from '../../jobs' +import { JobContext, JobStudio } from '../../jobs' import { WorkerDataCache } from '../caches' import { RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { getIngestQueueName, IngestJobFunc } from '@sofie-automation/corelib/dist/worker/ingest' @@ -41,8 +41,12 @@ export class JobContextImpl extends StudioCacheContextImpl implements JobContext this.studioRouteSetUpdater = new StudioRouteSetUpdater(directCollections, cacheData) } - get studio(): ReadonlyDeep { - return this.studioRouteSetUpdater.studioWithChanges ?? super.studio + get studio(): ReadonlyDeep { + return this.studioRouteSetUpdater.jobStudioWithChanges ?? super.studio + } + + get rawStudio(): ReadonlyDeep { + return this.studioRouteSetUpdater.rawStudioWithChanges ?? super.rawStudio } trackCache(cache: BaseModel): void { diff --git a/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts b/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts index dff38b6e88..183a89d01b 100644 --- a/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts +++ b/packages/job-worker/src/workers/context/StudioCacheContextImpl.ts @@ -4,6 +4,7 @@ import { ProcessedShowStyleVariant, ProcessedShowStyleCompound, StudioCacheContext, + JobStudio, } from '../../jobs' import { ReadonlyDeep } from 'type-fest' import { WorkerDataCache } from '../caches' @@ -30,9 +31,14 @@ export class StudioCacheContextImpl implements StudioCacheContext { protected readonly cacheData: WorkerDataCache ) {} - get studio(): ReadonlyDeep { + get studio(): ReadonlyDeep { // This is frozen at the point of populating the cache - return this.cacheData.studio + return this.cacheData.jobStudio + } + + get rawStudio(): ReadonlyDeep { + // This is frozen at the point of populating the cache + return this.cacheData.rawStudio } get studioId(): StudioId { @@ -47,7 +53,9 @@ export class StudioCacheContextImpl implements StudioCacheContext { getStudioBlueprintConfig(): ProcessedStudioConfig { if (!this.cacheData.studioBlueprintConfig) { this.cacheData.studioBlueprintConfig = deepFreeze( - clone(preprocessStudioConfig(this.cacheData.studio, this.cacheData.studioBlueprint.blueprint) ?? null) + clone( + preprocessStudioConfig(this.cacheData.jobStudio, this.cacheData.studioBlueprint.blueprint) ?? null + ) ) } @@ -59,7 +67,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { const loadedDocs: Array> = [] // Figure out what is already cached, and what needs loading - for (const id of this.cacheData.studio.supportedShowStyleBase) { + for (const id of this.cacheData.jobStudio.supportedShowStyleBase) { const doc = this.cacheData.showStyleBases.get(id) if (doc === undefined) { docsToLoad.push(id) @@ -95,7 +103,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { async getShowStyleBase(id: ShowStyleBaseId): Promise> { // Check if allowed - if (!this.cacheData.studio.supportedShowStyleBase.includes(id)) { + if (!this.cacheData.jobStudio.supportedShowStyleBase.includes(id)) { throw new Error(`ShowStyleBase "${id}" is not allowed in studio`) } @@ -123,7 +131,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { async getShowStyleVariants(id: ShowStyleBaseId): Promise>> { // Check if allowed - if (!this.cacheData.studio.supportedShowStyleBase.includes(id)) { + if (!this.cacheData.jobStudio.supportedShowStyleBase.includes(id)) { throw new Error(`ShowStyleBase "${id}" is not allowed in studio`) } @@ -172,7 +180,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { const doc0 = await this.directCollections.ShowStyleVariants.findOne(id) // Check allowed - if (doc0 && !this.cacheData.studio.supportedShowStyleBase.includes(doc0.showStyleBaseId)) { + if (doc0 && !this.cacheData.jobStudio.supportedShowStyleBase.includes(doc0.showStyleBaseId)) { throw new Error(`ShowStyleVariant "${id}" is not allowed in studio`) } @@ -187,7 +195,7 @@ export class StudioCacheContextImpl implements StudioCacheContext { if (doc) { // Check allowed - if (!this.cacheData.studio.supportedShowStyleBase.includes(doc.showStyleBaseId)) { + if (!this.cacheData.jobStudio.supportedShowStyleBase.includes(doc.showStyleBaseId)) { throw new Error(`ShowStyleVariant "${id}" is not allowed in studio`) } diff --git a/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts b/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts index cea5c9e53b..9de2f486f4 100644 --- a/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts +++ b/packages/job-worker/src/workers/context/StudioRouteSetUpdater.ts @@ -10,26 +10,39 @@ import { logger } from '../../logging' import type { ReadonlyDeep } from 'type-fest' import type { WorkerDataCache } from '../caches' import type { IDirectCollections } from '../../db' +import { JobStudio } from '../../jobs' +import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' export class StudioRouteSetUpdater { readonly #directCollections: Readonly - readonly #cacheData: Pick + readonly #cacheData: Pick - constructor(directCollections: Readonly, cacheData: Pick) { + constructor( + directCollections: Readonly, + cacheData: Pick + ) { this.#directCollections = directCollections this.#cacheData = cacheData } // Future: this could store a Map, if the context exposed a simplified view of DBStudio - #studioWithRouteSetChanges: ReadonlyDeep | undefined = undefined - - get studioWithChanges(): ReadonlyDeep | undefined { - return this.#studioWithRouteSetChanges + #studioWithRouteSetChanges: + | { + rawStudio: ReadonlyDeep + jobStudio: ReadonlyDeep + } + | undefined = undefined + + get rawStudioWithChanges(): ReadonlyDeep | undefined { + return this.#studioWithRouteSetChanges?.rawStudio + } + get jobStudioWithChanges(): ReadonlyDeep | undefined { + return this.#studioWithRouteSetChanges?.jobStudio } setRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean { - const currentStudio = this.#studioWithRouteSetChanges ?? this.#cacheData.studio - const currentRouteSets = getAllCurrentItemsFromOverrides(currentStudio.routeSetsWithOverrides, null) + const currentStudios = this.#studioWithRouteSetChanges ?? this.#cacheData + const currentRouteSets = getAllCurrentItemsFromOverrides(currentStudios.rawStudio.routeSetsWithOverrides, null) const routeSet = currentRouteSets.find((routeSet) => routeSet.id === routeSetId) if (!routeSet) throw new Error(`RouteSet "${routeSetId}" not found!`) @@ -41,10 +54,10 @@ export class StudioRouteSetUpdater { if (routeSet.computed.behavior === StudioRouteBehavior.ACTIVATE_ONLY && !isActive) throw new Error(`RouteSet "${routeSet.id}" is ACTIVATE_ONLY`) - const overrideHelper = new OverrideOpHelperImpl(null, currentStudio.routeSetsWithOverrides) + const overrideHelper = new OverrideOpHelperImpl(null, currentStudios.rawStudio.routeSetsWithOverrides) // Update the pending changes - logger.debug(`switchRouteSet "${this.#cacheData.studio._id}" "${routeSet.id}"=${isActive}`) + logger.debug(`switchRouteSet "${this.#cacheData.rawStudio._id}" "${routeSet.id}"=${isActive}`) overrideHelper.setItemValue(routeSetId, 'active', isActive) let mayAffectTimeline = couldRoutesetAffectTimelineGeneration(routeSet) @@ -54,7 +67,9 @@ export class StudioRouteSetUpdater { for (const otherRouteSet of Object.values>(currentRouteSets)) { if (otherRouteSet.id === routeSet.id) continue if (otherRouteSet.computed?.exclusivityGroup === routeSet.computed.exclusivityGroup) { - logger.debug(`switchRouteSet Other ID "${this.#cacheData.studio._id}" "${otherRouteSet.id}"=false`) + logger.debug( + `switchRouteSet Other ID "${this.#cacheData.rawStudio._id}" "${otherRouteSet.id}"=false` + ) overrideHelper.setItemValue(otherRouteSet.id, 'active', false) mayAffectTimeline = mayAffectTimeline || couldRoutesetAffectTimelineGeneration(otherRouteSet) @@ -65,13 +80,22 @@ export class StudioRouteSetUpdater { const updatedOverrideOps = overrideHelper.getPendingOps() // Update the cached studio - this.#studioWithRouteSetChanges = Object.freeze({ - ...currentStudio, + const updatedRawStudio: ReadonlyDeep = Object.freeze({ + ...currentStudios.rawStudio, routeSetsWithOverrides: Object.freeze({ - ...currentStudio.routeSetsWithOverrides, + ...currentStudios.rawStudio.routeSetsWithOverrides, overrides: deepFreeze(updatedOverrideOps), }), }) + const updatedJobStudio: ReadonlyDeep = Object.freeze({ + ...currentStudios.jobStudio, + routeSets: deepFreeze(applyAndValidateOverrides(updatedRawStudio.routeSetsWithOverrides).obj), + }) + + this.#studioWithRouteSetChanges = { + rawStudio: updatedRawStudio, + jobStudio: updatedJobStudio, + } return mayAffectTimeline } @@ -83,18 +107,19 @@ export class StudioRouteSetUpdater { // This is technically a little bit of a race condition, if someone uses the config pages but no more so than the rest of the system await this.#directCollections.Studios.update( { - _id: this.#cacheData.studio._id, + _id: this.#cacheData.rawStudio._id, }, { $set: { 'routeSetsWithOverrides.overrides': - this.#studioWithRouteSetChanges.routeSetsWithOverrides.overrides, + this.#studioWithRouteSetChanges.rawStudio.routeSetsWithOverrides.overrides, }, } ) // Pretend that the studio as reported by the database has changed, this will be fixed after this job by the ChangeStream firing - this.#cacheData.studio = this.#studioWithRouteSetChanges + this.#cacheData.rawStudio = this.#studioWithRouteSetChanges.rawStudio + this.#cacheData.jobStudio = this.#studioWithRouteSetChanges.jobStudio this.#studioWithRouteSetChanges = undefined } diff --git a/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts b/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts index 77692f4072..e6ef0fe40d 100644 --- a/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts +++ b/packages/job-worker/src/workers/context/__tests__/StudioRouteSetUpdater.spec.ts @@ -6,11 +6,15 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec function setupTest(routeSets: Record) { const context = setupDefaultJobEnvironment() - const mockCache: Pick = { - studio: { - ...context.studio, + const mockCache: Pick = { + rawStudio: { + ...context.rawStudio, routeSetsWithOverrides: wrapDefaultObject(routeSets), }, + jobStudio: { + ...context.studio, + routeSets: routeSets, + }, } const mockCollection = context.mockCollections.Studios const routeSetHelper = new StudioRouteSetUpdater(context.directCollections, mockCache) @@ -197,11 +201,13 @@ describe('StudioRouteSetUpdater', () => { routeSetHelper.setRouteSetActive('one', true) - expect(routeSetHelper.studioWithChanges).toBeTruthy() + expect(routeSetHelper.rawStudioWithChanges).toBeTruthy() + expect(routeSetHelper.jobStudioWithChanges).toBeTruthy() routeSetHelper.discardRouteSetChanges() - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() @@ -211,54 +217,70 @@ describe('StudioRouteSetUpdater', () => { it('save should update mockCache', async () => { const { mockCache, mockCollection, routeSetHelper } = setupTest(SINGLE_ROUTESET) - const studioBefore = mockCache.studio - expect(routeSetHelper.studioWithChanges).toBeFalsy() + const rawStudioBefore = mockCache.rawStudio + const jobStudioBefore = mockCache.jobStudio + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() routeSetHelper.setRouteSetActive('one', true) - expect(routeSetHelper.studioWithChanges).toBeTruthy() + expect(routeSetHelper.rawStudioWithChanges).toBeTruthy() + expect(routeSetHelper.jobStudioWithChanges).toBeTruthy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() expect(mockCollection.operations).toHaveLength(1) // Object should have changed - expect(mockCache.studio).not.toBe(studioBefore) + expect(mockCache.rawStudio).not.toBe(rawStudioBefore) + expect(mockCache.jobStudio).not.toBe(jobStudioBefore) // Object should not be equal - expect(mockCache.studio).not.toEqual(studioBefore) - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(mockCache.rawStudio).not.toEqual(rawStudioBefore) + expect(mockCache.jobStudio).not.toEqual(jobStudioBefore) + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() }) it('no changes should not update mockCache', async () => { const { mockCache, mockCollection, routeSetHelper } = setupTest(SINGLE_ROUTESET) - const studioBefore = mockCache.studio - expect(routeSetHelper.studioWithChanges).toBeFalsy() + const rawStudioBefore = mockCache.rawStudio + const jobStudioBefore = mockCache.jobStudio + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() expect(mockCollection.operations).toHaveLength(0) - expect(mockCache.studio).toBe(studioBefore) - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(mockCache.rawStudio).toBe(rawStudioBefore) + expect(mockCache.jobStudio).toBe(jobStudioBefore) + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() }) it('discard changes should not update mockCache', async () => { const { mockCache, mockCollection, routeSetHelper } = setupTest(SINGLE_ROUTESET) - const studioBefore = mockCache.studio - expect(routeSetHelper.studioWithChanges).toBeFalsy() + const rawStudioBefore = mockCache.rawStudio + const jobStudioBefore = mockCache.jobStudio + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() routeSetHelper.setRouteSetActive('one', true) - expect(routeSetHelper.studioWithChanges).toBeTruthy() + expect(routeSetHelper.rawStudioWithChanges).toBeTruthy() + expect(routeSetHelper.jobStudioWithChanges).toBeTruthy() routeSetHelper.discardRouteSetChanges() - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() expect(mockCollection.operations).toHaveLength(0) await routeSetHelper.saveRouteSetChanges() expect(mockCollection.operations).toHaveLength(0) - expect(mockCache.studio).toBe(studioBefore) - expect(routeSetHelper.studioWithChanges).toBeFalsy() + expect(mockCache.rawStudio).toBe(rawStudioBefore) + expect(mockCache.jobStudio).toBe(jobStudioBefore) + expect(routeSetHelper.rawStudioWithChanges).toBeFalsy() + expect(routeSetHelper.jobStudioWithChanges).toBeFalsy() }) it('ACTIVATE_ONLY routeset can be activated', async () => { diff --git a/packages/job-worker/src/workers/events/child.ts b/packages/job-worker/src/workers/events/child.ts index 76d95c4c31..d6c0a59da1 100644 --- a/packages/job-worker/src/workers/events/child.ts +++ b/packages/job-worker/src/workers/events/child.ts @@ -98,7 +98,7 @@ export class EventsWorkerChild { const transaction = startTransaction('invalidateCaches', 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } try { @@ -118,7 +118,7 @@ export class EventsWorkerChild { const trace = startTrace('studioWorker' + jobName) const transaction = startTransaction(jobName, 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } const context = new JobContextImpl( diff --git a/packages/job-worker/src/workers/ingest/child.ts b/packages/job-worker/src/workers/ingest/child.ts index 86af4b8634..41ebc90eaf 100644 --- a/packages/job-worker/src/workers/ingest/child.ts +++ b/packages/job-worker/src/workers/ingest/child.ts @@ -81,7 +81,7 @@ export class IngestWorkerChild { const transaction = startTransaction('invalidateCaches', 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } try { @@ -99,7 +99,7 @@ export class IngestWorkerChild { const trace = startTrace('ingestWorker:' + jobName) const transaction = startTransaction(jobName, 'worker-ingest') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) // transaction.setLabel('rundownId', unprotectString(staticData.rundownId)) } diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index 40903527c6..cd468780ee 100644 --- a/packages/job-worker/src/workers/studio/child.ts +++ b/packages/job-worker/src/workers/studio/child.ts @@ -82,7 +82,7 @@ export class StudioWorkerChild { const transaction = startTransaction('invalidateCaches', 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } try { @@ -100,7 +100,7 @@ export class StudioWorkerChild { const trace = startTrace('studioWorker:' + jobName) const transaction = startTransaction(jobName, 'worker-studio') if (transaction) { - transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.studio._id)) + transaction.setLabel('studioId', unprotectString(this.#staticData.dataCache.jobStudio._id)) } const context = new JobContextImpl( diff --git a/packages/meteor-lib/src/api/migration.ts b/packages/meteor-lib/src/api/migration.ts index 40df1b77a0..80f6485667 100644 --- a/packages/meteor-lib/src/api/migration.ts +++ b/packages/meteor-lib/src/api/migration.ts @@ -1,5 +1,11 @@ import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' -import { BlueprintId, ShowStyleBaseId, SnapshotId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + BlueprintId, + CoreSystemId, + ShowStyleBaseId, + SnapshotId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' import { ITranslatableMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' @@ -64,10 +70,15 @@ export interface NewMigrationAPI { validateConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise /** - * Run `applyConfig` on the blueprint for a Studio, and store the results into the db - * @param studioId Id of the Studio + * Run `applyConfig` on the blueprint for a ShowStyleBase, and store the results into the db + * @param showStyleBaseId Id of the ShowStyleBase */ runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise + + /** + * Run `applyConfig` on the blueprint for the CoreSystem, and store the results into the db + */ + runUpgradeForCoreSystem(coreSystemId: CoreSystemId): Promise } export enum MigrationAPIMethods { @@ -85,6 +96,7 @@ export enum MigrationAPIMethods { 'ignoreFixupConfigForShowStyleBase' = 'migration.ignoreFixupConfigForShowStyleBase', 'validateConfigForShowStyleBase' = 'migration.validateConfigForShowStyleBase', 'runUpgradeForShowStyleBase' = 'migration.runUpgradeForShowStyleBase', + 'runUpgradeForCoreSystem' = 'migration.runUpgradeForCoreSystem', } export interface GetMigrationStatusResult { diff --git a/packages/meteor-lib/src/api/upgradeStatus.ts b/packages/meteor-lib/src/api/upgradeStatus.ts index d50fdd81e1..2f6d025d3f 100644 --- a/packages/meteor-lib/src/api/upgradeStatus.ts +++ b/packages/meteor-lib/src/api/upgradeStatus.ts @@ -1,16 +1,19 @@ import { ITranslatableMessage } from '@sofie-automation/blueprints-integration' -import { StudioId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { StudioId, ShowStyleBaseId, CoreSystemId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' export type UIBlueprintUpgradeStatusId = ProtectedString<'UIBlueprintUpgradeStatus'> -export type UIBlueprintUpgradeStatus = UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle +export type UIBlueprintUpgradeStatus = + | UIBlueprintUpgradeStatusCoreSystem + | UIBlueprintUpgradeStatusStudio + | UIBlueprintUpgradeStatusShowStyle export interface UIBlueprintUpgradeStatusBase { _id: UIBlueprintUpgradeStatusId - documentType: 'studio' | 'showStyle' - documentId: StudioId | ShowStyleBaseId + documentType: 'coreSystem' | 'studio' | 'showStyle' + documentId: CoreSystemId | StudioId | ShowStyleBaseId name: string @@ -30,6 +33,11 @@ export interface UIBlueprintUpgradeStatusBase { changes: ITranslatableMessage[] } +export interface UIBlueprintUpgradeStatusCoreSystem extends UIBlueprintUpgradeStatusBase { + documentType: 'coreSystem' + documentId: CoreSystemId +} + export interface UIBlueprintUpgradeStatusStudio extends UIBlueprintUpgradeStatusBase { documentType: 'studio' documentId: StudioId diff --git a/packages/meteor-lib/src/collections/CoreSystem.ts b/packages/meteor-lib/src/collections/CoreSystem.ts index 5a8f58641b..e710091a16 100644 --- a/packages/meteor-lib/src/collections/CoreSystem.ts +++ b/packages/meteor-lib/src/collections/CoreSystem.ts @@ -1,6 +1,9 @@ +import { LastBlueprintConfig } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { LogLevel } from '../lib' import { CoreSystemId, BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { ObjectWithOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' export const SYSTEM_ID: CoreSystemId = protectString('core') @@ -54,22 +57,11 @@ export interface ICoreSystem { /** Id of the blueprint used by this system */ blueprintId?: BlueprintId - /** Support info */ - support?: { - message: string - } - systemInfo?: { message: string enabled: boolean } - evaluations?: { - enabled: boolean - heading: string - message: string - } - /** A user-defined name for the installation */ name?: string @@ -95,18 +87,20 @@ export interface ICoreSystem { } enableMonitorBlockedThread?: boolean - /** Cron jobs running nightly */ - cron?: { - casparCGRestart?: { - enabled: boolean - } - storeRundownSnapshots?: { - enabled: boolean - rundownNames?: string[] - } - } + settingsWithOverrides: ObjectWithOverrides logo?: SofieLogo + + /** Details on the last blueprint used to generate the defaults values for this + * Note: This doesn't currently have any 'config' which it relates to. + * The name is to be consistent with studio/showstyle, and in preparation for their being config/configpresets used here + */ + lastBlueprintConfig: LastBlueprintConfig | undefined + + /** These fields are to have type consistency with the full config driven upgrades flow, but we don't use them yet */ + blueprintConfigPresetId?: undefined + lastBlueprintFixUpHash?: undefined + blueprintConfigWithOverrides?: undefined } /** In the beginning, there was the database, and the database was with Sofie, and the database was Sofie. diff --git a/packages/shared-lib/src/core/model/CoreSystemSettings.ts b/packages/shared-lib/src/core/model/CoreSystemSettings.ts new file mode 100644 index 0000000000..e5392915a5 --- /dev/null +++ b/packages/shared-lib/src/core/model/CoreSystemSettings.ts @@ -0,0 +1,23 @@ +export interface ICoreSystemSettings { + /** Cron jobs running nightly */ + cron: { + casparCGRestart: { + enabled: boolean + } + storeRundownSnapshots?: { + enabled: boolean + rundownNames?: string[] + } + } + + /** Support info */ + support: { + message: string + } + + evaluationsMessage: { + enabled: boolean + heading: string + message: string + } +} diff --git a/packages/shared-lib/src/core/model/StudioSettings.ts b/packages/shared-lib/src/core/model/StudioSettings.ts new file mode 100644 index 0000000000..f964362679 --- /dev/null +++ b/packages/shared-lib/src/core/model/StudioSettings.ts @@ -0,0 +1,84 @@ +export enum ForceQuickLoopAutoNext { + /** Parts will auto-next only when explicitly set by the NRCS/blueprints */ + DISABLED = 'disabled', + /** Parts will auto-next when the expected duration is set and within range */ + ENABLED_WHEN_VALID_DURATION = 'enabled_when_valid_duration', + /** All parts will auto-next. If expected duration is undefined or low, the default display duration will be used */ + ENABLED_FORCING_MIN_DURATION = 'enabled_forcing_min_duration', +} + +export interface IStudioSettings { + /** The framerate (frames per second) used to convert internal timing information (in milliseconds) + * into timecodes and timecode-like strings and interpret timecode user input + * Default: 25 + */ + frameRate: number + + /** URL to endpoint where media preview are exposed */ + mediaPreviewsUrl: string // (former media_previews_url in config) + /** URLs for slack webhook to send evaluations */ + slackEvaluationUrls?: string // (former slack_evaluation in config) + + /** Media Resolutions supported by the studio for media playback */ + supportedMediaFormats?: string // (former mediaResolutions in config) + /** Audio Stream Formats supported by the studio for media playback */ + supportedAudioStreams?: string // (former audioStreams in config) + + /** Should the play from anywhere feature be enabled in this studio */ + enablePlayFromAnywhere?: boolean + + /** + * If set, forces the multi-playout-gateway mode (aka set "now"-time right away) + * for single playout-gateways setups + */ + forceMultiGatewayMode?: boolean + + /** How much extra delay to add to the Now-time (used for the "multi-playout-gateway" feature). + * A higher value adds delays in playout, but reduces the risk of missed frames. */ + multiGatewayNowSafeLatency?: number + + /** Allow resets while a rundown is on-air */ + allowRundownResetOnAir?: boolean + + /** Preserve unsynced segments position in the rundown, relative to the other segments */ + preserveOrphanedSegmentPositionInRundown?: boolean + + /** + * The minimum amount of time, in milliseconds, that must pass after a take before another take may be performed. + * Default: 1000 + */ + minimumTakeSpan: number + + /** Whether to allow adlib testing mode, before a Part is playing in a Playlist */ + allowAdlibTestingSegment?: boolean + + /** Should QuickLoop context menu options be available to the users. It does not affect Playlist loop enabled by the NRCS. */ + enableQuickLoop?: boolean + + /** If and how to force auto-nexting in a looping Playlist */ + forceQuickLoopAutoNext?: ForceQuickLoopAutoNext + + /** + * The duration to apply on too short Parts Within QuickLoop when ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION is selected + * Default: 3000 + */ + fallbackPartDuration?: number + + /** + * Whether to allow hold operations for Rundowns in this Studio + * When disabled, any action-triggers that would normally trigger a hold operation will be silently ignored + * This should only block entering hold, to ensure Sofie doesn't get stuck if it somehow gets into hold + */ + allowHold: boolean + + /** + * Whether to allow direct playing of a piece in the rundown + * This behaviour is usally triggered by double-clicking on a piece in the GUI + */ + allowPieceDirectPlay: boolean + + /** + * Enable buckets - the default behavior is to have buckets. + */ + enableBuckets: boolean +} diff --git a/packages/webui/public/images/sofie-logo.svg b/packages/webui/public/images/sofie-logo-default.svg similarity index 100% rename from packages/webui/public/images/sofie-logo.svg rename to packages/webui/public/images/sofie-logo-default.svg diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 89c7749232..edf6ddf181 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -101,14 +101,14 @@ export function defaultStudio(_id: StudioId): DBStudio { mappingsWithOverrides: wrapDefaultObject({}), supportedShowStyleBase: [], blueprintConfigWithOverrides: wrapDefaultObject({}), - settings: { + settingsWithOverrides: wrapDefaultObject({ frameRate: 25, mediaPreviewsUrl: '', minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, allowHold: true, allowPieceDirectPlay: true, enableBuckets: true, - }, + }), _rundownVersionHash: '', routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), diff --git a/packages/webui/src/__mocks__/helpers/database.ts b/packages/webui/src/__mocks__/helpers/database.ts index fbc094fa42..e5d1b62274 100644 --- a/packages/webui/src/__mocks__/helpers/database.ts +++ b/packages/webui/src/__mocks__/helpers/database.ts @@ -72,6 +72,25 @@ export async function setupMockCore(doc?: Partial): Promise { hint?: string item: WrappedOverridableItemNormal itemKey: keyof ReadonlyDeep - opPrefix: string overrideHelper: OverrideOpHelperForItemContents showClearButton?: boolean @@ -32,7 +31,6 @@ export function LabelAndOverrides({ hint, item, itemKey, - opPrefix, overrideHelper, showClearButton, formatDefaultValue, @@ -40,16 +38,16 @@ export function LabelAndOverrides({ const { t } = useTranslation() const clearOverride = useCallback(() => { - overrideHelper().clearItemOverrides(opPrefix, String(itemKey)).commit() - }, [overrideHelper, opPrefix, itemKey]) + overrideHelper().clearItemOverrides(item.id, String(itemKey)).commit() + }, [overrideHelper, item.id, itemKey]) const setValue = useCallback( (newValue: any) => { - overrideHelper().setItemValue(opPrefix, String(itemKey), newValue).commit() + overrideHelper().setItemValue(item.id, String(itemKey), newValue).commit() }, - [overrideHelper, opPrefix, itemKey] + [overrideHelper, item.id, itemKey] ) - const isOverridden = hasOpWithPath(item.overrideOps, opPrefix, String(itemKey)) + const isOverridden = hasOpWithPath(item.overrideOps, item.id, String(itemKey)) let displayValue: JSX.Element | string | null = '""' if (item.defaults) { diff --git a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx index 7a371661c6..b6aa6a0983 100644 --- a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx +++ b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import ClassNames from 'classnames' export function splitValueIntoLines(v: string | undefined): string[] { @@ -85,3 +85,19 @@ export function MultiLineTextInputControl({ /> ) } + +interface ICombinedMultiLineTextInputControlProps + extends Omit { + value: string + handleUpdate: (value: string) => void +} +export function CombinedMultiLineTextInputControl({ + value, + handleUpdate, + ...props +}: Readonly): JSX.Element { + const valueArray = useMemo(() => splitValueIntoLines(value), [value]) + const handleUpdateArray = useCallback((value: string[]) => handleUpdate(joinLines(value)), [handleUpdate]) + + return +} diff --git a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts index 262fe8922c..89c4b34f05 100644 --- a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts +++ b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts @@ -1,8 +1,5 @@ -import { - DBRundownPlaylist, - ForceQuickLoopAutoNext, - QuickLoopMarkerType, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, QuickLoopMarkerType } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import { PartInstance, wrapPartToTemporaryInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' diff --git a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx index bcd00e9afb..2e9779a076 100644 --- a/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx +++ b/packages/webui/src/client/lib/forms/SchemaFormWithOverrides.tsx @@ -45,7 +45,6 @@ interface FormComponentProps { item: WrappedOverridableItemNormal overrideHelper: OverrideOpHelperForItemContents itemKey: string - opPrefix: string /** Whether a clear button should be showed for fields not marked as "required" */ showClearButton: boolean @@ -68,7 +67,6 @@ function useChildPropsForFormComponent(props: Readonly CoreSystem.findOne(), []) + const coreSystemSettings = useTracker(() => { + const core = CoreSystem.findOne(SYSTEM_ID, { projection: { settingsWithOverrides: 1 } }) + return core && applyAndValidateOverrides(core.settingsWithOverrides).obj + }, []) - const message = coreSystem?.evaluations?.enabled ? coreSystem.evaluations : undefined + const message = coreSystemSettings?.evaluationsMessage?.enabled ? coreSystemSettings.evaluationsMessage : undefined if (!message) return null return ( diff --git a/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx b/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx index ef3e341451..9a0fae510e 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyle/BlueprintConfiguration/index.tsx @@ -12,6 +12,7 @@ import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { useSubscription, useTracker } from '../../../../lib/ReactMeteorData/ReactMeteorData' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' +import { UIBlueprintUpgradeStatusShowStyle } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' interface ShowStyleBaseBlueprintConfigurationSettingsProps { showStyleBase: DBShowStyleBase @@ -33,7 +34,7 @@ export function ShowStyleBaseBlueprintConfigurationSettings( UIBlueprintUpgradeStatuses.findOne({ documentId: props.showStyleBase._id, documentType: 'showStyle', - }), + }) as UIBlueprintUpgradeStatusShowStyle | undefined, [props.showStyleBase._id] ) const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') diff --git a/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx b/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx index c0c331ab7d..ad50735d12 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyle/OutputLayer.tsx @@ -270,13 +270,7 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }:
- + {(value, handleUpdate) => ( {(value, handleUpdate) => } @@ -309,7 +302,6 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display Rank')} item={item} itemKey={'_rank'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -325,7 +317,6 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is collapsed by default')} item={item} itemKey={'isDefaultCollapsed'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -334,7 +325,6 @@ function OutputLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is flattened')} item={item} itemKey={'isFlattened'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } diff --git a/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx b/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx index a7405ffe7a..9b31c7bac9 100644 --- a/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx +++ b/packages/webui/src/client/ui/Settings/ShowStyle/SourceLayer.tsx @@ -295,13 +295,7 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }:
- + {(value, handleUpdate) => ( {(value, handleUpdate) => ( @@ -341,7 +334,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Source Type')} item={item} itemKey={'type'} - opPrefix={item.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(SourceLayerType)} > @@ -358,7 +350,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is a Live Remote Input')} item={item} itemKey={'isRemoteInput'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -367,7 +358,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is a Guest Input')} item={item} itemKey={'isGuestInput'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -376,7 +366,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Is hidden')} item={item} itemKey={'isHidden'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -385,7 +374,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display Rank')} item={item} itemKey={'_rank'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -401,7 +389,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Treat as Main content')} item={item} itemKey={'onPresenterScreen'} - opPrefix={item.id} overrideHelper={overrideHelper} hint="When set, Pieces on this Source Layer will be used to display summaries, thumbnails etc for the Part in GUIs. " > @@ -411,7 +398,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display in a column in List View')} item={item} itemKey={'onListViewColumn'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -420,7 +406,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Display AdLibs in a column in List View')} item={item} itemKey={'onListViewAdLibColumn'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -429,7 +414,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Pieces on this layer can be cleared')} item={item} itemKey={'isClearable'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -438,7 +422,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Pieces on this layer are sticky')} item={item} itemKey={'isSticky'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -447,7 +430,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Only Pieces present in rundown are sticky')} item={item} itemKey={'stickyOriginalOnly'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -456,7 +438,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Allow disabling of Pieces')} item={item} itemKey={'allowDisable'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -465,7 +446,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('AdLibs on this layer can be queued')} item={item} itemKey={'isQueueable'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -474,7 +454,6 @@ function SourceLayerEntry({ item, isExpanded, toggleExpanded, overrideHelper }: label={t('Exclusivity group')} item={item} itemKey={'exclusiveGroup'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( diff --git a/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx b/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx index 9358e54068..78d28da1a0 100644 --- a/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/BlueprintConfiguration/index.tsx @@ -15,6 +15,7 @@ import { SelectBlueprint } from './SelectBlueprint' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../../Collections' import { getUpgradeStatusMessage, UpgradeStatusButtons } from '../../Upgrades/Components' +import { UIBlueprintUpgradeStatusStudio } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' interface StudioBlueprintConfigurationSettingsProps { studio: DBStudio @@ -31,7 +32,7 @@ export function StudioBlueprintConfigurationSettings( UIBlueprintUpgradeStatuses.findOne({ documentId: props.studio._id, documentType: 'studio', - }), + }) as UIBlueprintUpgradeStatusStudio | undefined, [props.studio._id] ) const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') diff --git a/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx b/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx index b3967bf83e..ae9b79ae30 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Devices/GenericSubDevices.tsx @@ -285,7 +285,6 @@ function SubDeviceEditRow({ label={t('Peripheral Device ID')} item={item} overrideHelper={overrideHelper} - opPrefix={item.id} itemKey={'peripheralDeviceId'} options={peripheralDeviceOptions} > @@ -379,7 +378,6 @@ function SubDeviceEditForm({ peripheralDevice, item, overrideHelper }: Readonly< label={t('Device Type')} item={item} overrideHelper={overrideHelper} - opPrefix={item.id} itemKey={'options.type'} options={subdeviceTypeOptions} > diff --git a/packages/webui/src/client/ui/Settings/Studio/Generic.tsx b/packages/webui/src/client/ui/Settings/Studio/Generic.tsx index 41bc9559c4..bb2302f832 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Generic.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Generic.tsx @@ -1,9 +1,8 @@ import * as React from 'react' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { Translated } from '../../../lib/ReactMeteorData/react-meteor-data' +import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { EditAttribute } from '../../../lib/EditAttribute' import { StudioBaselineStatus } from './Baseline' import { ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -11,9 +10,27 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { Studios } from '../../../collections' import { useHistory } from 'react-router-dom' import { MeteorCall } from '../../../lib/meteorApi' -import { LabelActual } from '../../../lib/Components/LabelAndOverrides' +import { + LabelActual, + LabelAndOverrides, + LabelAndOverridesForCheckbox, + LabelAndOverridesForDropdown, + LabelAndOverridesForInt, +} from '../../../lib/Components/LabelAndOverrides' import { catchError } from '../../../lib/lib' -import { ForceQuickLoopAutoNext } from '@sofie-automation/corelib/src/dataModel/RundownPlaylist' +import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' +import { + applyAndValidateOverrides, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { useOverrideOpHelper, WrappedOverridableItemNormal } from '../util/OverrideOpHelper' +import { IntInputControl } from '../../../lib/Components/IntInput' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { useMemo } from 'react' +import { CheckboxControl } from '../../../lib/Components/Checkbox' +import { TextInputControl } from '../../../lib/Components/TextInput' +import { DropdownInputControl, DropdownInputOption } from '../../../lib/Components/DropdownInput' interface IStudioGenericPropertiesProps { studio: DBStudio @@ -23,327 +40,76 @@ interface IStudioGenericPropertiesProps { showStyleBase: DBShowStyleBase }> } -interface IStudioGenericPropertiesState {} -export const StudioGenericProperties = withTranslation()( - class StudioGenericProperties extends React.Component< - Translated, - IStudioGenericPropertiesState - > { - constructor(props: Translated) { - super(props) - } - renderShowStyleEditButtons() { - const buttons: JSX.Element[] = [] - if (this.props.studio) { - for (const showStyleBaseId of this.props.studio.supportedShowStyleBase) { - const showStyleBase = this.props.availableShowStyleBases.find( - (base) => base.showStyleBase._id === showStyleBaseId - ) - if (showStyleBase) { - buttons.push( - - ) - } - } - } - return buttons - } +export function StudioGenericProperties({ + studio, + availableShowStyleBases, +}: IStudioGenericPropertiesProps): JSX.Element { + const { t } = useTranslation() - render(): JSX.Element { - const { t } = this.props - return ( -
-

{t('Generic Properties')}

- -
- {t('Select Compatible Show Styles')} -
- - {this.renderShowStyleEditButtons()} - -
- {!this.props.studio.supportedShowStyleBase.length ? ( -
- {t('Show style not set')} -
- ) : null} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ const showStyleEditButtons: JSX.Element[] = [] + for (const showStyleBaseId of studio.supportedShowStyleBase) { + const showStyleBase = availableShowStyleBases.find((base) => base.showStyleBase._id === showStyleBaseId) + if (showStyleBase) { + showStyleEditButtons.push( + ) } } -) + + return ( +
+

{t('Generic Properties')}

+ +
+ {t('Select Compatible Show Styles')} +
+ + {showStyleEditButtons} + +
+ {!studio.supportedShowStyleBase.length ? ( +
+ {t('Show style not set')} +
+ ) : null} +
+ + + + +
+ ) +} const NewShowStyleButton = React.memo(function NewShowStyleButton() { const history = useHistory() @@ -378,3 +144,303 @@ const RedirectToShowStyleButton = React.memo(function RedirectToShowStyleButton( ) }) + +function StudioSettings({ studio }: { studio: DBStudio }): JSX.Element { + const { t } = useTranslation() + + const saveOverrides = React.useCallback( + (newOps: SomeObjectOverrideOp[]) => { + Studios.update(studio._id, { + $set: { + 'settingsWithOverrides.overrides': newOps.map((op) => ({ + ...op, + path: op.path.startsWith('0.') ? op.path.slice(2) : op.path, + })), + }, + }) + }, + [studio._id] + ) + + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = studio.settingsWithOverrides.overrides.map((op) => ({ + ...op, + // TODO: can we avoid doing this hack? + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(studio.settingsWithOverrides).obj + + const wrappedItem = literal>({ + type: 'normal', + id: '0', + computed: computedValue, + defaults: studio.settingsWithOverrides.defaults, + overrideOps: prefixedOps, + }) + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: studio.settingsWithOverrides.defaults, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [studio.settingsWithOverrides]) + + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + + const autoNextOptions: DropdownInputOption[] = useMemo( + () => [ + { + name: t('Disabled'), + value: ForceQuickLoopAutoNext.DISABLED, + i: 0, + }, + { + name: t('Enabled, but skipping parts with undefined or 0 duration'), + value: ForceQuickLoopAutoNext.ENABLED_WHEN_VALID_DURATION, + i: 1, + }, + { + name: t('Enabled on all Parts, applying QuickLoop Fallback Part Duration if needed'), + value: ForceQuickLoopAutoNext.ENABLED_FORCING_MIN_DURATION, + i: 2, + }, + ], + [t] + ) + + return ( + <> + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate, options) => ( + + )} + + + + {(value, handleUpdate) => ( + + )} + + + + {(value, handleUpdate) => } + + + + {(value, handleUpdate) => } + + + ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx b/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx index f47a0bc4fc..5807d916e3 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx @@ -432,7 +432,6 @@ function StudioMappingsEntry({ hint={t('Human-readable name of the layer')} item={item} itemKey={'layerName'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -450,7 +449,6 @@ function StudioMappingsEntry({ hint={t('The type of device to use for the output')} item={item} itemKey={'device'} - opPrefix={item.id} overrideHelper={overrideHelper} options={deviceTypeOptions} > @@ -469,7 +467,6 @@ function StudioMappingsEntry({ hint={t('ID of the device (corresponds to the device ID in the peripheralDevice settings)')} item={item} itemKey={'deviceId'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -486,7 +483,6 @@ function StudioMappingsEntry({ label={t('Lookahead Mode')} item={item} itemKey={'lookahead'} - opPrefix={item.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(LookaheadMode)} > @@ -504,7 +500,6 @@ function StudioMappingsEntry({ label={t('Lookahead Target Objects (Undefined = 1)')} item={item} itemKey={'lookaheadDepth'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -523,7 +518,6 @@ function StudioMappingsEntry({ })} item={item} itemKey={'lookaheadMaxSearchDistance'} - opPrefix={item.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -543,7 +537,6 @@ function StudioMappingsEntry({ hint={t('The type of mapping to use')} item={item} itemKey={'options.mappingType'} - opPrefix={item.id} overrideHelper={overrideHelper} options={mappingTypeOptions} > diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx index eee92146cd..780a8afb12 100644 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/AccessorTableRow.tsx @@ -133,7 +133,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.label`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -150,7 +149,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.type`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(Accessor.AccessType)} > @@ -173,7 +171,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.folderPath`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -191,7 +188,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.resourceId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -212,7 +208,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.baseUrl`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -230,7 +225,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.isImmutable`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -249,7 +243,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.useGETinsteadOfHEAD`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -269,7 +262,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.networkId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -290,7 +282,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.baseUrl`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -310,7 +301,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.networkId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -331,7 +321,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.folderPath`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -349,7 +338,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.userName`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -367,7 +355,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.password`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -385,7 +372,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.networkId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -406,7 +392,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.quantelGatewayUrl`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -424,7 +409,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.ISAUrls`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -442,7 +426,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.zoneId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -462,7 +445,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.serverId`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -480,7 +462,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.transformerURL`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -498,7 +479,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.fileflowURL`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -516,7 +496,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.fileflowProfile`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -536,7 +515,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.allowRead`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -546,7 +524,6 @@ export function AccessorTableRow({ item={packageContainer} //@ts-expect-error can't be 4 levels deep itemKey={`container.accessors.${accessorId}.allowWrite`} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx index d92e3e31c8..ac915bfbab 100644 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainers.tsx @@ -285,7 +285,6 @@ function PackageContainerRow({ item={packageContainer} //@ts-expect-error can't be 2 levels deep itemKey={'container.label'} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -302,7 +301,6 @@ function PackageContainerRow({ hint={t('Select which playout devices are using this package container')} item={packageContainer} itemKey={'deviceIds'} - opPrefix={packageContainer.id} overrideHelper={overrideHelper} options={availablePlayoutDevicesOptions} > diff --git a/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx b/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx index 7b3a167749..ec442fd0cc 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Routings/ExclusivityGroups.tsx @@ -223,7 +223,6 @@ function ExclusivityGroupRow({ label={t('Exclusivity Group Name')} item={exclusivityGroup} itemKey={'name'} - opPrefix={exclusivityGroup.id} overrideHelper={exclusivityOverrideHelper} > {(value, handleUpdate) => ( diff --git a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx index f6dc204d50..83ec047d7a 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSetAbPlayers.tsx @@ -118,7 +118,6 @@ function AbPlayerRow({ label={t('Pool name')} item={player} itemKey={'poolName'} - opPrefix={player.id} overrideHelper={tableOverrideHelper} > {(value, handleUpdate) => ( @@ -134,7 +133,6 @@ function AbPlayerRow({ label={t('Pool PlayerId')} item={player} itemKey={'playerId'} - opPrefix={player.id} overrideHelper={tableOverrideHelper} > {(value, handleUpdate) => ( diff --git a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx index cf555b4611..fe82a55647 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Routings/RouteSets.tsx @@ -306,7 +306,6 @@ function RouteSetRow({ hint={t('he default state of this Route Set')} item={routeSet} itemKey={'defaultActive'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(DEFAULT_ACTIVE_OPTIONS)} > @@ -323,7 +322,6 @@ function RouteSetRow({ label={t('Active')} item={routeSet} itemKey={'active'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => } @@ -332,7 +330,6 @@ function RouteSetRow({ label={t('Route Set Name')} item={routeSet} itemKey={'name'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} > {(value, handleUpdate) => ( @@ -350,7 +347,6 @@ function RouteSetRow({ hint={t('If set, only one Route Set will be active per exclusivity group')} item={routeSet} itemKey={'exclusivityGroup'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} options={exclusivityGroupOptions} > @@ -369,7 +365,6 @@ function RouteSetRow({ hint={t('The way this Route Set should behave towards the user')} item={routeSet} itemKey={'behavior'} - opPrefix={routeSet.id} overrideHelper={overrideHelper} options={getDropdownInputOptions(StudioRouteBehavior)} > @@ -601,7 +596,6 @@ function RenderRoutesRow({ label={t('Original Layer')} item={route} itemKey={'mappedLayer'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={getDropdownInputOptions(Object.keys(studioMappings))} > @@ -619,7 +613,6 @@ function RenderRoutesRow({ label={t('New Layer')} item={route} itemKey={'outputMappedLayer'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} > {(value, handleUpdate) => ( @@ -636,7 +629,6 @@ function RenderRoutesRow({ label={t('Route Type')} item={route} itemKey={'routeType'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={getDropdownInputOptions(StudioRouteType)} > @@ -660,7 +652,6 @@ function RenderRoutesRow({ label={t('Device Type')} item={route} itemKey={'deviceType'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={getDropdownInputOptions(TSR.DeviceType)} > @@ -689,7 +680,6 @@ function RenderRoutesRow({ label={t('Mapping Type')} item={route} itemKey={'remapping.options.mappingType'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} options={mappingTypeOptions} > @@ -710,7 +700,6 @@ function RenderRoutesRow({ label={t('Device ID')} item={route} itemKey={'remapping.deviceId'} - opPrefix={route.id} overrideHelper={tableOverrideHelper} showClearButton={true} > diff --git a/packages/webui/src/client/ui/Settings/SystemManagement.tsx b/packages/webui/src/client/ui/Settings/SystemManagement.tsx index 742e04e5b2..b15eef1622 100644 --- a/packages/webui/src/client/ui/Settings/SystemManagement.tsx +++ b/packages/webui/src/client/ui/Settings/SystemManagement.tsx @@ -9,14 +9,30 @@ import { languageAnd } from '../../lib/language' import { TriggeredActionsEditor } from './components/triggeredActions/TriggeredActionsEditor' import { TFunction, useTranslation } from 'react-i18next' import { Meteor } from 'meteor/meteor' -import { LogLevel } from '../../lib/tempLib' +import { literal, LogLevel } from '../../lib/tempLib' import { CoreSystem } from '../../collections' import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system' -import { LabelActual } from '../../lib/Components/LabelAndOverrides' +import { + LabelActual, + LabelAndOverrides, + LabelAndOverridesForCheckbox, + LabelAndOverridesForMultiLineText, +} from '../../lib/Components/LabelAndOverrides' import { catchError } from '../../lib/lib' +import { SystemManagementBlueprint } from './SystemManagement/Blueprint' +import { + applyAndValidateOverrides, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { ICoreSystemSettings } from '@sofie-automation/blueprints-integration' +import { WrappedOverridableItemNormal, useOverrideOpHelper } from './util/OverrideOpHelper' +import { CheckboxControl } from '../../lib/Components/Checkbox' +import { CombinedMultiLineTextInputControl, MultiLineTextInputControl } from '../../lib/Components/MultiLineTextInput' +import { TextInputControl } from '../../lib/Components/TextInput' interface WithCoreSystemProps { - coreSystem: ICoreSystem | undefined + coreSystem: ICoreSystem } export default function SystemManagement(): JSX.Element | null { @@ -30,6 +46,8 @@ export default function SystemManagement(): JSX.Element | null {
+ + @@ -156,25 +174,29 @@ function SystemManagementNotificationMessage({ coreSystem }: Readonly) { const { t } = useTranslation() + const { wrappedItem, overrideHelper } = useCoreSystemSettingsWithOverrides(coreSystem) + return ( <>

{t('Support Panel')}

- + )} +
) @@ -183,50 +205,56 @@ function SystemManagementSupportPanel({ coreSystem }: Readonly) { const { t } = useTranslation() + const { wrappedItem, overrideHelper } = useCoreSystemSettingsWithOverrides(coreSystem) + return ( <>

{t('Evaluations')}

- - - + )} +
) @@ -304,55 +332,49 @@ function SystemManagementMonitoring({ coreSystem }: Readonly) { const { t } = useTranslation() + const { wrappedItem, overrideHelper } = useCoreSystemSettingsWithOverrides(coreSystem) + return ( <>

{t('Cron jobs')}

- - - + )} +
) @@ -571,3 +593,51 @@ function SystemManagementHeapSnapshot() { ) } + +function useCoreSystemSettingsWithOverrides(coreSystem: ICoreSystem) { + const saveOverrides = useCallback( + (newOps: SomeObjectOverrideOp[]) => { + CoreSystem.update(coreSystem._id, { + $set: { + 'settingsWithOverrides.overrides': newOps.map((op) => ({ + ...op, + path: op.path.startsWith('0.') ? op.path.slice(2) : op.path, + })), + }, + }) + }, + [coreSystem._id] + ) + + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = coreSystem.settingsWithOverrides.overrides.map((op) => ({ + ...op, + // TODO: can we avoid doing this hack? + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(coreSystem.settingsWithOverrides).obj + + const wrappedItem = literal>({ + type: 'normal', + id: '0', + computed: computedValue, + defaults: coreSystem.settingsWithOverrides.defaults, + overrideOps: prefixedOps, + }) + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: coreSystem.settingsWithOverrides.defaults, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [coreSystem.settingsWithOverrides]) + + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + + return { + wrappedItem, + overrideHelper, + } +} diff --git a/packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx b/packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx new file mode 100644 index 0000000000..ce87ee092e --- /dev/null +++ b/packages/webui/src/client/ui/Settings/SystemManagement/Blueprint.tsx @@ -0,0 +1,99 @@ +import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' +import { UIBlueprintUpgradeStatusCoreSystem } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { useTranslation } from 'react-i18next' +import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { UIBlueprintUpgradeStatuses } from '../../Collections' +import { getUpgradeStatusMessage, SystemUpgradeStatusButtons } from '../Upgrades/Components' +import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' +import { Blueprints, CoreSystem } from '../../../collections' +import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { useMemo } from 'react' +import { LabelActual } from '../../../lib/Components/LabelAndOverrides' +import { EditAttribute } from '../../../lib/EditAttribute' +import { RedirectToBlueprintButton } from '../../../lib/SettingsNavigation' + +interface SystemManagementBlueprintProps { + coreSystem: ICoreSystem | undefined +} +export function SystemManagementBlueprint({ coreSystem }: Readonly): JSX.Element { + const { t } = useTranslation() + + const isStatusReady = useSubscription(MeteorPubSub.uiBlueprintUpgradeStatuses) + const status = useTracker( + () => + coreSystem && + (UIBlueprintUpgradeStatuses.findOne({ + documentId: coreSystem._id, + documentType: 'coreSystem', + }) as UIBlueprintUpgradeStatusCoreSystem | undefined), + [coreSystem?._id] + ) + const statusMessage = isStatusReady && status ? getUpgradeStatusMessage(t, status) ?? t('OK') : t('Loading...') + + return ( +
+
+ + +

+ {t('Upgrade Status')}: {statusMessage} + {status && } +

+
+
+ ) +} + +interface SelectBlueprintProps { + coreSystem: ICoreSystem | undefined +} + +function SelectBlueprint({ coreSystem }: Readonly): JSX.Element { + const { t } = useTranslation() + + const allSystemBlueprints = useTracker(() => { + return Blueprints.find({ + blueprintType: BlueprintManifestType.SYSTEM, + }).fetch() + }, []) + const blueprintOptions: { name: string; value: BlueprintId | null }[] = useMemo(() => { + if (allSystemBlueprints) { + return allSystemBlueprints.map((blueprint) => { + return { + name: blueprint.name ? `${blueprint.name} (${blueprint._id})` : unprotectString(blueprint._id), + value: blueprint._id, + } + }) + } else { + return [] + } + }, [allSystemBlueprints]) + + return ( +
+ +
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx b/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx index 19e7fc1778..1034a6a91a 100644 --- a/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx +++ b/packages/webui/src/client/ui/Settings/Upgrades/Components.tsx @@ -10,6 +10,7 @@ import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { NotificationCenter, NoticeLevel, Notification } from '../../../lib/notifications/notifications' import { UIBlueprintUpgradeStatusBase, + UIBlueprintUpgradeStatusCoreSystem, UIBlueprintUpgradeStatusShowStyle, UIBlueprintUpgradeStatusStudio, } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' @@ -288,3 +289,76 @@ export function UpgradeStatusButtons({ upgradeResult }: Readonly ) } + +interface SystemUpgradeStatusButtonsProps { + upgradeResult: UIBlueprintUpgradeStatusCoreSystem +} +export function SystemUpgradeStatusButtons({ upgradeResult }: Readonly): JSX.Element { + const { t } = useTranslation() + + const applyConfig = useCallback( + async () => MeteorCall.migration.runUpgradeForCoreSystem(upgradeResult.documentId), + [upgradeResult.documentId, upgradeResult.documentType] + ) + + const clickApply = useCallback(() => { + applyConfig() + .then(() => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + t('Config for {{name}} upgraded successfully', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + .catch((e) => { + catchError('Upgrade applyConfig')(e) + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.WARNING, + t('Config for {{name}} upgraded failed', { name: upgradeResult.name }), + 'UpgradesView' + ) + ) + }) + }, [upgradeResult, applyConfig]) + + const clickShowChanges = useCallback(() => { + doModalDialog({ + title: t('Upgrade config for {{name}}', { name: upgradeResult.name }), + message: ( +
+ {upgradeResult.changes.length === 0 &&

{t('No changes')}

} + {upgradeResult.changes.map((msg, i) => ( +

{translateMessage(msg, i18nTranslator)}

+ ))} +
+ ), + acceptOnly: true, + yes: t('Dismiss'), + onAccept: () => { + // Do nothing + }, + }) + }, [upgradeResult]) + + return ( +
+ + +
+ ) +} diff --git a/packages/webui/src/client/ui/Settings/Upgrades/View.tsx b/packages/webui/src/client/ui/Settings/Upgrades/View.tsx index 2e6f2bbfd7..7302d2acd0 100644 --- a/packages/webui/src/client/ui/Settings/Upgrades/View.tsx +++ b/packages/webui/src/client/ui/Settings/Upgrades/View.tsx @@ -4,11 +4,8 @@ import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { UIBlueprintUpgradeStatuses } from '../../Collections' -import { - UIBlueprintUpgradeStatusShowStyle, - UIBlueprintUpgradeStatusStudio, -} from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' -import { getUpgradeStatusMessage, UpgradeStatusButtons } from './Components' +import { UIBlueprintUpgradeStatus } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { getUpgradeStatusMessage, SystemUpgradeStatusButtons, UpgradeStatusButtons } from './Components' export function UpgradesView(): JSX.Element { const { t } = useTranslation() @@ -39,6 +36,17 @@ export function UpgradesView(): JSX.Element { )} + {statuses?.map( + (document) => + document.documentType === 'coreSystem' && ( + + ) + )} + {statuses?.map( (document) => document.documentType === 'studio' && ( @@ -69,7 +77,7 @@ export function UpgradesView(): JSX.Element { interface ShowUpgradesRowProps { resourceName: string - upgradeResult: UIBlueprintUpgradeStatusStudio | UIBlueprintUpgradeStatusShowStyle + upgradeResult: UIBlueprintUpgradeStatus } function ShowUpgradesRow({ resourceName, upgradeResult }: Readonly) { const { t } = useTranslation() @@ -83,7 +91,11 @@ function ShowUpgradesRow({ resourceName, upgradeResult }: Readonly{getUpgradeStatusMessage(t, upgradeResult)} - + {upgradeResult.documentType === 'coreSystem' ? ( + + ) : ( + + )} ) diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx index 8a623bc773..d313b89a5c 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionsEditor.tsx @@ -437,7 +437,7 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction {showStyleBaseId !== null ? ( <>
- {(systemTriggeredActionIds?.length ?? 0) > 0 && !parsedTriggerFilter ? ( + {!parsedTriggerFilter ? (

setSystemWideCollapsed(!systemWideCollapsed)} @@ -470,13 +470,19 @@ export const TriggeredActionsEditor: React.FC = function TriggeredAction /> )) : null} + + {!systemWideCollapsed && !parsedTriggerFilter && systemTriggeredActionIds?.length === 0 && ( +

{t('No Action Triggers set up.')}

+ )}

) : null}
- + + + { - const core = CoreSystem.findOne() + const core = CoreSystem.findOne(SYSTEM_ID, { projection: { settingsWithOverrides: 1 } }) + const coreSettings = core && applyAndValidateOverrides(core.settingsWithOverrides).obj return { - supportMessage: core?.support?.message ?? '', + supportMessage: coreSettings?.support?.message ?? '', } }, [], From 5f77a1086993db81f1912a2a06e0fe8657c21fa6 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Dec 2024 15:28:17 +0000 Subject: [PATCH 05/18] chore: fix lint --- meteor/server/Connections.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/server/Connections.ts b/meteor/server/Connections.ts index a07bd33d3e..953fe220d3 100644 --- a/meteor/server/Connections.ts +++ b/meteor/server/Connections.ts @@ -1,4 +1,4 @@ -import { deferAsync, getCurrentTime, MeteorStartupAsync } from './lib/lib' +import { deferAsync, getCurrentTime } from './lib/lib' import { Meteor } from 'meteor/meteor' import { logger } from './logging' import { sendTrace } from './api/integration/influx' @@ -83,7 +83,7 @@ function traceConnections() { }, 1000) } -MeteorStartupAsync(async () => { +Meteor.startup(async () => { // Reset the connection status of the devices await PeripheralDevices.updateAsync( From c4b8da5357b56aa0a21ac4ede2e355cea2260c02 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 26 Nov 2024 13:18:43 +0000 Subject: [PATCH 06/18] feat: Sofie Core Groups with Trusted header SOFIE-95 --- .gitignore | 1 + meteor/.meteor/packages | 2 - meteor/.meteor/versions | 9 - meteor/__mocks__/_setupMocks.ts | 1 - meteor/__mocks__/accounts-base.ts | 81 ---- meteor/__mocks__/meteor.ts | 36 +- meteor/__mocks__/mongo.ts | 2 - meteor/server/Connections.ts | 20 + meteor/server/__tests__/cronjobs.test.ts | 13 +- meteor/server/api/ExternalMessageQueue.ts | 31 +- .../api/__tests__/peripheralDevice.test.ts | 2 +- .../userActions/mediaManager.test.ts | 28 +- .../api/blueprints/__tests__/api.test.ts | 54 +-- .../api/blueprints/__tests__/http.test.ts | 2 +- meteor/server/api/blueprints/api.ts | 46 +-- meteor/server/api/blueprints/http.ts | 27 +- meteor/server/api/buckets.ts | 146 +++---- meteor/server/api/cleanup.ts | 4 - meteor/server/api/client.ts | 74 ++-- meteor/server/api/deviceTriggers/observer.ts | 2 +- meteor/server/api/evaluations.ts | 11 +- meteor/server/api/heapSnapshot.ts | 25 +- meteor/server/api/ingest/actions.ts | 6 +- meteor/server/api/ingest/lib.ts | 23 -- .../api/ingest/mosDevice/mosIntegration.ts | 8 +- meteor/server/api/ingest/rundownInput.ts | 8 +- .../api/integration/expectedPackages.ts | 2 +- .../server/api/integration/media-scanner.ts | 2 +- .../server/api/integration/mediaWorkFlows.ts | 2 +- meteor/server/api/lib.ts | 67 ---- meteor/server/api/mediaManager.ts | 125 +++--- meteor/server/api/methodContext.ts | 13 +- meteor/server/api/organizations.ts | 22 +- meteor/server/api/packageManager.ts | 56 ++- meteor/server/api/peripheralDevice.ts | 28 +- meteor/server/api/playout/api.ts | 17 +- meteor/server/api/playout/playout.ts | 11 +- .../server/api/rest/v0/__tests__/rest.test.ts | 17 - meteor/server/api/rest/v1/buckets.ts | 43 +-- meteor/server/api/rest/v1/index.ts | 12 +- meteor/server/api/rest/v1/playlists.ts | 2 +- meteor/server/api/rest/v1/studios.ts | 10 +- meteor/server/api/rest/v1/types.ts | 2 - meteor/server/api/rundown.ts | 33 +- meteor/server/api/rundownLayouts.ts | 16 +- meteor/server/api/showStyles.ts | 52 +-- meteor/server/api/singleUseTokens.ts | 2 +- meteor/server/api/snapshot.ts | 170 ++++----- meteor/server/api/studio/api.ts | 18 +- meteor/server/api/system.ts | 22 +- meteor/server/api/triggeredActions.ts | 20 +- meteor/server/api/user.ts | 132 +------ meteor/server/api/userActions.ts | 184 +++++---- meteor/server/collections/collection.ts | 37 +- meteor/server/collections/index.ts | 87 ++--- meteor/server/email.ts | 12 - .../__tests__/optimizedObserver.test.ts | 4 - .../server/lib/customPublication/publish.ts | 5 - meteor/server/main.ts | 3 +- meteor/server/methods.ts | 21 +- meteor/server/migration/api.ts | 31 +- .../blueprintUpgradeStatus/publication.ts | 12 +- meteor/server/publications/buckets.ts | 97 +++-- .../publications/deviceTriggersPreview.ts | 10 +- meteor/server/publications/lib/lib.ts | 105 +---- meteor/server/publications/mountedTriggers.ts | 61 ++- meteor/server/publications/organization.ts | 87 ++--- .../expectedPackages/publication.ts | 53 ++- .../packageManager/packageContainers.ts | 49 +-- .../packageManager/playoutContext.ts | 49 +-- .../partInstancesUI/publication.ts | 38 +- .../publications/partsUI/publication.ts | 39 +- .../server/publications/peripheralDevice.ts | 121 +++--- .../publications/peripheralDeviceForDevice.ts | 41 +- .../bucket/publication.ts | 44 +-- .../rundown/publication.ts | 46 +-- meteor/server/publications/rundown.ts | 349 +++++++---------- meteor/server/publications/rundownPlaylist.ts | 35 +- .../segmentPartNotesUI/publication.ts | 46 +-- meteor/server/publications/showStyle.ts | 73 ++-- meteor/server/publications/showStyleUI.ts | 46 +-- meteor/server/publications/studio.ts | 106 +++--- meteor/server/publications/studioUI.ts | 25 +- meteor/server/publications/system.ts | 90 +---- meteor/server/publications/timeline.ts | 54 ++- .../publications/translationsBundles.ts | 18 +- .../server/publications/triggeredActionsUI.ts | 41 +- meteor/server/security/README.md | 53 --- .../security/__tests__/security.test.ts | 358 ------------------ meteor/server/security/_security.ts | 11 - .../security/{lib/lib.ts => allowDeny.ts} | 7 +- meteor/server/security/auth.ts | 87 +++++ meteor/server/security/buckets.ts | 80 ---- meteor/server/security/check.ts | 104 +++++ meteor/server/security/lib/access.ts | 64 ---- meteor/server/security/lib/credentials.ts | 171 --------- meteor/server/security/lib/security.ts | 349 ----------------- meteor/server/security/noSecurity.ts | 12 - meteor/server/security/organization.ts | 165 -------- meteor/server/security/peripheralDevice.ts | 180 --------- meteor/server/security/rundown.ts | 126 ------ meteor/server/security/rundownPlaylist.ts | 126 ------ .../security/{lib => }/securityVerify.ts | 4 +- meteor/server/security/showStyle.ts | 154 -------- meteor/server/security/studio.ts | 155 -------- meteor/server/security/system.ts | 65 ---- meteor/server/security/translationsBundles.ts | 8 - meteor/server/systemStatus/api.ts | 60 ++- meteor/server/systemStatus/systemStatus.ts | 40 +- meteor/server/worker/worker.ts | 2 + packages/corelib/src/dataModel/Collections.ts | 1 - packages/meteor-lib/src/Settings.ts | 6 +- packages/meteor-lib/src/api/pubsub.ts | 4 - packages/meteor-lib/src/api/user.ts | 31 +- packages/meteor-lib/src/api/userActions.ts | 3 + packages/meteor-lib/src/collections/Users.ts | 29 -- packages/meteor-lib/src/userPermissions.ts | 58 +++ .../src/peripheralDevice/methodsAPI.ts | 2 +- packages/webui/src/__mocks__/meteor.ts | 31 +- packages/webui/src/__mocks__/mongo.ts | 2 - packages/webui/src/client/ui/App.tsx | 2 +- .../src/client/ui/Status/MediaManager.tsx | 6 +- .../ui/Status/SystemStatus/SystemStatus.tsx | 19 +- .../webui/src/client/ui/UserPermissions.tsx | 79 +++- scripts/run.mjs | 33 +- 125 files changed, 1780 insertions(+), 4584 deletions(-) delete mode 100644 meteor/__mocks__/accounts-base.ts delete mode 100644 meteor/server/api/lib.ts delete mode 100644 meteor/server/email.ts delete mode 100644 meteor/server/security/README.md delete mode 100644 meteor/server/security/__tests__/security.test.ts delete mode 100644 meteor/server/security/_security.ts rename meteor/server/security/{lib/lib.ts => allowDeny.ts} (84%) create mode 100644 meteor/server/security/auth.ts delete mode 100644 meteor/server/security/buckets.ts create mode 100644 meteor/server/security/check.ts delete mode 100644 meteor/server/security/lib/access.ts delete mode 100644 meteor/server/security/lib/credentials.ts delete mode 100644 meteor/server/security/lib/security.ts delete mode 100644 meteor/server/security/noSecurity.ts delete mode 100644 meteor/server/security/organization.ts delete mode 100644 meteor/server/security/peripheralDevice.ts delete mode 100644 meteor/server/security/rundown.ts delete mode 100644 meteor/server/security/rundownPlaylist.ts rename meteor/server/security/{lib => }/securityVerify.ts (99%) delete mode 100644 meteor/server/security/showStyle.ts delete mode 100644 meteor/server/security/studio.ts delete mode 100644 meteor/server/security/system.ts delete mode 100644 meteor/server/security/translationsBundles.ts delete mode 100644 packages/meteor-lib/src/collections/Users.ts create mode 100644 packages/meteor-lib/src/userPermissions.ts diff --git a/.gitignore b/.gitignore index 5892e69d3e..6d86bbd070 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ meteor/.coverage/ node_modules **/yarn-error.log scratch/ +meteor-settings.json # Exclude JetBrains IDE specific files .idea diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 8d1724b1db..34ab0cf5f5 100644 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -19,6 +19,4 @@ typescript@5.4.3 # Enable TypeScript syntax in .ts and .tsx modules tracker@1.3.4 # Meteor's client-side reactive programming library -accounts-password@3.0.2 - zodern:types diff --git a/meteor/.meteor/versions b/meteor/.meteor/versions index b49eda45ce..93c057c752 100644 --- a/meteor/.meteor/versions +++ b/meteor/.meteor/versions @@ -1,5 +1,3 @@ -accounts-base@3.0.3 -accounts-password@3.0.3 allow-deny@2.0.0 babel-compiler@7.11.2 babel-runtime@1.5.2 @@ -12,7 +10,6 @@ core-runtime@1.0.0 ddp@1.4.2 ddp-client@3.0.3 ddp-common@1.4.4 -ddp-rate-limiter@1.2.2 ddp-server@3.0.3 diff-sequence@1.1.3 dynamic-import@0.7.4 @@ -21,13 +18,11 @@ ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.2 ecmascript-runtime-server@0.11.1 ejson@1.1.4 -email@3.1.1 facts-base@1.0.2 fetch@0.1.5 geojson-utils@1.0.12 id-map@1.2.0 inter-process-messaging@0.1.2 -localstorage@1.2.1 logging@1.3.5 meteor@2.0.2 minimongo@2.0.2 @@ -42,17 +37,13 @@ npm-mongo@6.10.0 ordered-dict@1.2.0 promise@1.0.0 random@1.2.2 -rate-limit@1.1.2 react-fast-refresh@0.2.9 -reactive-var@1.0.13 reload@1.3.2 retry@1.1.1 routepolicy@1.1.2 -sha@1.0.10 socket-stream-client@0.5.3 tracker@1.3.4 typescript@5.6.3 -url@1.3.5 webapp@2.0.4 webapp-hashing@1.1.2 zodern:types@1.0.13 diff --git a/meteor/__mocks__/_setupMocks.ts b/meteor/__mocks__/_setupMocks.ts index b4508a82bb..b9e7936792 100644 --- a/meteor/__mocks__/_setupMocks.ts +++ b/meteor/__mocks__/_setupMocks.ts @@ -14,7 +14,6 @@ jest.mock('meteor/meteor', (...args) => require('./meteor').setup(args), { virtu jest.mock('meteor/random', (...args) => require('./random').setup(args), { virtual: true }) jest.mock('meteor/check', (...args) => require('./check').setup(args), { virtual: true }) jest.mock('meteor/tracker', (...args) => require('./tracker').setup(args), { virtual: true }) -jest.mock('meteor/accounts-base', (...args) => require('./accounts-base').setup(args), { virtual: true }) jest.mock('meteor/ejson', (...args) => require('./ejson').setup(args), { virtual: true }) jest.mock('meteor/mdg:validated-method', (...args) => require('./validated-method').setup(args), { virtual: true }) diff --git a/meteor/__mocks__/accounts-base.ts b/meteor/__mocks__/accounts-base.ts deleted file mode 100644 index 468f6e8d78..0000000000 --- a/meteor/__mocks__/accounts-base.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { RandomMock } from './random' -import { MeteorMock } from './meteor' -import { Accounts } from 'meteor/accounts-base' - -export class AccountsBaseMock { - static mockUsers: any = {} - - // From https://docs.meteor.com/api/passwords.html - - static createUser( - options: Parameters[0], - cb: (err: any | undefined, result?: any) => void - ): void { - const user = { - _id: RandomMock.id(), - ...options, - } - AccountsBaseMock.mockUsers[user._id] = user - MeteorMock.setTimeout(() => { - cb(undefined, user._id) - }, 1) - throw new Error('Mocked function not implemented') - } - static setUsername(userId: string, newUsername: string): void { - AccountsBaseMock.mockUsers[userId].username = newUsername - throw new Error('Mocked function not implemented') - } - static removeEmail(_userId: string, _email: string): void { - throw new Error('Mocked function not implemented') - } - static verifyEmail(_token: string, _cb: (err: any | undefined, result?: any) => void): void { - throw new Error('Mocked function not implemented') - } - static findUserByUsername(_username: string): void { - throw new Error('Mocked function not implemented') - } - static findUserByEmail(_email: string): void { - throw new Error('Mocked function not implemented') - } - static changePassword( - _oldPassword: string, - _newPassword: string, - _cb: (err: any | undefined, result?: any) => void - ): void { - throw new Error('Mocked function not implemented') - } - static forgotPassword( - _options: { email?: string | undefined }, - _cb: (err: any | undefined, result?: any) => void - ): void { - throw new Error('Mocked function not implemented') - } - static resetPassword( - _token: string, - _newPassword: string, - _cb: (err: any | undefined, result?: any) => void - ): void { - throw new Error('Mocked function not implemented') - } - static setPassword(_userId: string, _newPassword: string, _options?: { logout?: Object | undefined }): void { - throw new Error('Mocked function not implemented') - } - static sendResetPasswordEmail(_userId: string, _email: string): void { - throw new Error('Mocked function not implemented') - } - static sendEnrollmentEmail(_userId: string, _email: string): void { - throw new Error('Mocked function not implemented') - } - static sendVerificationEmail(_userId: string, _email: string): void { - throw new Error('Mocked function not implemented') - } - static onResetPasswordLink?: () => void - static onEnrollmentLink?: () => void - static onEmailVerificationLink?: () => void - static emailTemplates?: () => void -} -export function setup(): any { - return { - Accounts: AccountsBaseMock, - } -} diff --git a/meteor/__mocks__/meteor.ts b/meteor/__mocks__/meteor.ts index 1b2ec69418..2de891f99e 100644 --- a/meteor/__mocks__/meteor.ts +++ b/meteor/__mocks__/meteor.ts @@ -1,4 +1,4 @@ -import { MongoMock } from './mongo' +import { USER_PERMISSIONS_HEADER } from '@sofie-automation/meteor-lib/dist/userPermissions' let controllableDefer = false @@ -9,7 +9,7 @@ export function useNextTickDefer(): void { controllableDefer = false } -namespace Meteor { +export namespace Meteor { export interface Settings { public: { [id: string]: any @@ -17,19 +17,6 @@ namespace Meteor { [id: string]: any } - export interface UserEmail { - address: string - verified: boolean - } - export interface User { - _id?: string - username?: string - emails?: UserEmail[] - createdAt?: number - profile?: any - services?: any - } - export interface ErrorStatic { new (error: string | number, reason?: string, details?: string): Error } @@ -103,22 +90,18 @@ export namespace MeteorMock { export const settings: any = {} export const mockMethods: { [name: string]: Function } = {} - export let mockUser: Meteor.User | undefined = undefined export const mockStartupFunctions: Function[] = [] export const absolutePath = process.cwd() - export function user(): Meteor.User | undefined { - return mockUser - } - export function userId(): string | undefined { - return mockUser ? mockUser._id : undefined - } function getMethodContext() { return { - userId: mockUser ? mockUser._id : undefined, connection: { clientAddress: '1.1.1.1', + httpHeaders: { + // Default to full permissions for tests + [USER_PERMISSIONS_HEADER]: 'admin', + }, }, unblock: () => { // noop @@ -256,7 +239,6 @@ export namespace MeteorMock { return fcn(...args) } } - export let users: MongoMock.Collection | undefined = undefined // -- Mock functions: -------------------------- /** @@ -269,12 +251,6 @@ export namespace MeteorMock { await waitTimeNoFakeTimers(10) // So that any observers or defers has had time to run. } - export function mockLoginUser(newUser: Meteor.User): void { - mockUser = newUser - } - export function mockSetUsersCollection(usersCollection: MongoMock.Collection): void { - users = usersCollection - } export function mockSetClientEnvironment(): void { mockIsClient = true } diff --git a/meteor/__mocks__/mongo.ts b/meteor/__mocks__/mongo.ts index d39e071ef0..fdd2074222 100644 --- a/meteor/__mocks__/mongo.ts +++ b/meteor/__mocks__/mongo.ts @@ -453,5 +453,3 @@ export function setup(): any { Mongo: MongoMock, } } - -MeteorMock.mockSetUsersCollection(new MongoMock.Collection('Meteor.users')) diff --git a/meteor/server/Connections.ts b/meteor/server/Connections.ts index 953fe220d3..0983e99878 100644 --- a/meteor/server/Connections.ts +++ b/meteor/server/Connections.ts @@ -4,6 +4,8 @@ import { logger } from './logging' import { sendTrace } from './api/integration/influx' import { PeripheralDevices } from './collections' import { MetricsGauge } from '@sofie-automation/corelib/dist/prometheus' +import { parseUserPermissions, USER_PERMISSIONS_HEADER } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { Settings } from './Settings' const connections = new Set() const connectionsGauge = new MetricsGauge({ @@ -14,6 +16,24 @@ const connectionsGauge = new MetricsGauge({ Meteor.onConnection((conn: Meteor.Connection) => { // This is called whenever a new ddp-connection is opened (ie a web-client or a peripheral-device) + if (Settings.enableHeaderAuth) { + const userLevel = parseUserPermissions(conn.httpHeaders[USER_PERMISSIONS_HEADER]) + + // HACK: force the userId of the connection before it can be used. + // This ensures we know the permissions of the connection before it can try to do anything + // This could probably be safely done inside a meteor method, as we only need it when directly modifying a collection in the client, + // but that will cause all the publications to restart when changing the userId. + const connSession = (Meteor as any).server.sessions.get(conn.id) + if (!connSession) { + logger.error(`Failed to find session for ddp connection! "${conn.id}"`) + // Close the connection, it won't be secure + conn.close() + return + } else { + connSession.userId = JSON.stringify(userLevel) + } + } + const connectionId: string = conn.id // var clientAddress = conn.clientAddress; // ip-adress diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 65bd80d24c..58cc06c469 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -466,7 +466,8 @@ describe('cronjobs', () => { expect(await Snapshots.findOneAsync(snapshot1)).toBeUndefined() }) async function insertPlayoutDevice( - props: Pick + props: Pick & + Partial> ): Promise { const deviceId = protectString(getRandomString()) await PeripheralDevices.insertAsync({ @@ -495,29 +496,35 @@ describe('cronjobs', () => { } async function createMockPlayoutGatewayAndDevices(lastSeen: number): Promise<{ + deviceToken: string mockPlayoutGw: PeripheralDeviceId mockCasparCg: PeripheralDeviceId mockAtem: PeripheralDeviceId }> { + const deviceToken = 'token1' const mockPlayoutGw = await insertPlayoutDevice({ deviceName: 'Playout Gateway', lastSeen: lastSeen, subType: PERIPHERAL_SUBTYPE_PROCESS, + token: deviceToken, }) const mockCasparCg = await insertPlayoutDevice({ deviceName: 'CasparCG', lastSeen: lastSeen, subType: TSR.DeviceType.CASPARCG, parentDeviceId: mockPlayoutGw, + token: deviceToken, }) const mockAtem = await insertPlayoutDevice({ deviceName: 'ATEM', lastSeen: lastSeen, subType: TSR.DeviceType.ATEM, parentDeviceId: mockPlayoutGw, + token: deviceToken, }) return { + deviceToken, mockPlayoutGw, mockCasparCg, mockAtem, @@ -525,7 +532,7 @@ describe('cronjobs', () => { } test('Attempts to restart CasparCG when job is enabled', async () => { - const { mockCasparCg } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold + const { mockCasparCg, deviceToken } = await createMockPlayoutGatewayAndDevices(Date.now()) // Some time after the threshold ;(logger.info as jest.Mock).mockClear() // set time to 2020/07/{date} 04:05 Local Time, should be more than 24 hours after 2020/07/19 00:00 UTC @@ -548,7 +555,7 @@ describe('cronjobs', () => { Meteor.callAsync( 'peripheralDevice.functionReply', cmd.deviceId, // deviceId - '', // deviceToken + deviceToken, // deviceToken cmd._id, // commandId null, // err null // result diff --git a/meteor/server/api/ExternalMessageQueue.ts b/meteor/server/api/ExternalMessageQueue.ts index 5d90abb7e3..0a5fdf7414 100644 --- a/meteor/server/api/ExternalMessageQueue.ts +++ b/meteor/server/api/ExternalMessageQueue.ts @@ -9,11 +9,14 @@ import { } from '@sofie-automation/meteor-lib/dist/api/ExternalMessageQueue' import { StatusObject, setSystemStatus } from '../systemStatus/systemStatus' import { MethodContextAPI, MethodContext } from './methodContext' -import { StudioContentWriteAccess } from '../security/studio' import { ExternalMessageQueueObjId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ExternalMessageQueue } from '../collections' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +const USER_PERMISSIONS_FOR_EXTERNAL_MESSAGES: Array = ['configure', 'studio', 'service'] let updateExternalMessageQueueStatusTimeout = 0 function updateExternalMessageQueueStatus(): void { @@ -69,28 +72,33 @@ Meteor.startup(async () => { async function removeExternalMessage(context: MethodContext, messageId: ExternalMessageQueueObjId): Promise { check(messageId, String) - await StudioContentWriteAccess.externalMessage(context, messageId) + + assertConnectionHasOneOfPermissions(context.connection, ...USER_PERMISSIONS_FOR_EXTERNAL_MESSAGES) // TODO - is this safe? what if it is in the middle of execution? await ExternalMessageQueue.removeAsync(messageId) } async function toggleHold(context: MethodContext, messageId: ExternalMessageQueueObjId): Promise { check(messageId, String) - const access = await StudioContentWriteAccess.externalMessage(context, messageId) - const m = access.message - if (!m) throw new Meteor.Error(404, `ExternalMessage "${messageId}" not found!`) + + assertConnectionHasOneOfPermissions(context.connection, ...USER_PERMISSIONS_FOR_EXTERNAL_MESSAGES) + + const existingMessage = await ExternalMessageQueue.findOneAsync(messageId) + if (!existingMessage) throw new Meteor.Error(404, `ExternalMessage "${messageId}" not found!`) await ExternalMessageQueue.updateAsync(messageId, { $set: { - hold: !m.hold, + hold: !existingMessage.hold, }, }) } async function retry(context: MethodContext, messageId: ExternalMessageQueueObjId): Promise { check(messageId, String) - const access = await StudioContentWriteAccess.externalMessage(context, messageId) - const m = access.message - if (!m) throw new Meteor.Error(404, `ExternalMessage "${messageId}" not found!`) + + assertConnectionHasOneOfPermissions(context.connection, ...USER_PERMISSIONS_FOR_EXTERNAL_MESSAGES) + + const existingMessage = await ExternalMessageQueue.findOneAsync(messageId) + if (!existingMessage) throw new Meteor.Error(404, `ExternalMessage "${messageId}" not found!`) const tryGap = getCurrentTime() - 1 * 60 * 1000 await ExternalMessageQueue.updateAsync(messageId, { @@ -98,7 +106,10 @@ async function retry(context: MethodContext, messageId: ExternalMessageQueueObjI manualRetry: true, hold: false, errorFatal: false, - lastTry: m.lastTry !== undefined && m.lastTry > tryGap ? tryGap : m.lastTry, + lastTry: + existingMessage.lastTry !== undefined && existingMessage.lastTry > tryGap + ? tryGap + : existingMessage.lastTry, }, }) // triggerdoMessageQueue(1000) diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 6efe7a1596..4a3b69fe5a 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -618,7 +618,7 @@ describe('test peripheralDevice general API methods', () => { const deviceObj = await PeripheralDevices.findOneAsync(device?._id) expect(deviceObj).toBeDefined() - await MeteorCall.peripheralDevice.removePeripheralDevice(device?._id) + await MeteorCall.peripheralDevice.removePeripheralDevice(device._id, device.token) } { diff --git a/meteor/server/api/__tests__/userActions/mediaManager.test.ts b/meteor/server/api/__tests__/userActions/mediaManager.test.ts index 3680cffde2..bf58417d6f 100644 --- a/meteor/server/api/__tests__/userActions/mediaManager.test.ts +++ b/meteor/server/api/__tests__/userActions/mediaManager.test.ts @@ -47,11 +47,16 @@ describe('User Actions - Media Manager', () => { jest.resetAllMocks() }) test('Restart workflow', async () => { - const { workFlowId } = await setupMockWorkFlow() + const { workFlowId, workFlow } = await setupMockWorkFlow() // should fail if the workflow doesn't exist await expect( - MeteorCall.userAction.mediaRestartWorkflow('', getCurrentTime(), protectString('FAKE_ID')) + MeteorCall.userAction.mediaRestartWorkflow( + '', + getCurrentTime(), + workFlow.deviceId, + protectString('FAKE_ID') + ) ).resolves.toMatchUserRawError(/not found/gi) { @@ -72,16 +77,16 @@ describe('User Actions - Media Manager', () => { }) }, MAX_WAIT_TIME) - await MeteorCall.userAction.mediaRestartWorkflow('', getCurrentTime(), workFlowId) + await MeteorCall.userAction.mediaRestartWorkflow('', getCurrentTime(), workFlow.deviceId, workFlowId) await p } }) test('Abort worfklow', async () => { - const { workFlowId } = await setupMockWorkFlow() + const { workFlowId, workFlow } = await setupMockWorkFlow() // should fail if the workflow doesn't exist await expect( - MeteorCall.userAction.mediaAbortWorkflow('', getCurrentTime(), protectString('FAKE_ID')) + MeteorCall.userAction.mediaAbortWorkflow('', getCurrentTime(), workFlow.deviceId, protectString('FAKE_ID')) ).resolves.toMatchUserRawError(/not found/gi) { @@ -103,16 +108,21 @@ describe('User Actions - Media Manager', () => { }) }, MAX_WAIT_TIME) - await MeteorCall.userAction.mediaAbortWorkflow('', getCurrentTime(), workFlowId) + await MeteorCall.userAction.mediaAbortWorkflow('', getCurrentTime(), workFlow.deviceId, workFlowId) await p } }) test('Prioritize workflow', async () => { - const { workFlowId } = await setupMockWorkFlow() + const { workFlowId, workFlow } = await setupMockWorkFlow() // should fail if the workflow doesn't exist await expect( - MeteorCall.userAction.mediaPrioritizeWorkflow('', getCurrentTime(), protectString('FAKE_ID')) + MeteorCall.userAction.mediaPrioritizeWorkflow( + '', + getCurrentTime(), + workFlow.deviceId, + protectString('FAKE_ID') + ) ).resolves.toMatchUserRawError(/not found/gi) { @@ -134,7 +144,7 @@ describe('User Actions - Media Manager', () => { }) }, MAX_WAIT_TIME) - await MeteorCall.userAction.mediaPrioritizeWorkflow('', getCurrentTime(), workFlowId) + await MeteorCall.userAction.mediaPrioritizeWorkflow('', getCurrentTime(), workFlow.deviceId, workFlowId) await p } }) diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index b92bf0a0ac..b2c60d3d4a 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -6,29 +6,23 @@ import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { SYSTEM_ID, ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { insertBlueprint, uploadBlueprint } from '../api' import { MeteorCall } from '../../methods' -import { MethodContext } from '../../methodContext' import '../../../../__mocks__/_extendJest' import { Blueprints, CoreSystem } from '../../../collections' import { SupressLogMessages } from '../../../../__mocks__/suppressLogging' import { JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' +import { Meteor } from 'meteor/meteor' // we don't want the deviceTriggers observer to start up at this time jest.mock('../../deviceTriggers/observer') require('../../peripheralDevice.ts') // include in order to create the Meteor methods needed -const DEFAULT_CONTEXT: MethodContext = { - userId: null, - isSimulation: false, - connection: { - id: 'mockConnectionId', - close: () => undefined, - onClose: () => undefined, - clientAddress: '127.0.0.1', - httpHeaders: {}, - }, - setUserId: () => undefined, - unblock: () => undefined, +const DEFAULT_CONNECTION: Meteor.Connection = { + id: 'mockConnectionId', + close: () => undefined, + onClose: () => undefined, + clientAddress: '127.0.0.1', + httpHeaders: {}, } describe('Test blueprint management api', () => { @@ -195,7 +189,7 @@ describe('Test blueprint management api', () => { }) test('with name', async () => { const rawName = 'some_fake_name' - const newId = await insertBlueprint(DEFAULT_CONTEXT, undefined, rawName) + const newId = await insertBlueprint(DEFAULT_CONNECTION, undefined, rawName) expect(newId).toBeTruthy() // Check some props @@ -206,7 +200,7 @@ describe('Test blueprint management api', () => { }) test('with type', async () => { const type = BlueprintManifestType.STUDIO - const newId = await insertBlueprint(DEFAULT_CONTEXT, type) + const newId = await insertBlueprint(DEFAULT_CONNECTION, type) expect(newId).toBeTruthy() // Check some props @@ -219,20 +213,20 @@ describe('Test blueprint management api', () => { describe('uploadBlueprint', () => { test('empty id', async () => { - await expect(uploadBlueprint(DEFAULT_CONTEXT, protectString(''), '0')).rejects.toThrowMeteor( + await expect(uploadBlueprint(DEFAULT_CONNECTION, protectString(''), '0')).rejects.toThrowMeteor( 400, 'Blueprint id "" is not valid' ) }) test('empty body', async () => { - await expect(uploadBlueprint(DEFAULT_CONTEXT, protectString('blueprint99'), '')).rejects.toThrowMeteor( + await expect(uploadBlueprint(DEFAULT_CONNECTION, protectString('blueprint99'), '')).rejects.toThrowMeteor( 400, 'Blueprint blueprint99 failed to parse' ) }) test('body not a manifest', async () => { await expect( - uploadBlueprint(DEFAULT_CONTEXT, protectString('blueprint99'), `({default: (() => 5)()})`) + uploadBlueprint(DEFAULT_CONNECTION, protectString('blueprint99'), `({default: (() => 5)()})`) ).rejects.toThrowMeteor(400, 'Blueprint blueprint99 returned a manifest of type number') }) test('manifest missing blueprintType', async () => { @@ -254,7 +248,7 @@ describe('Test blueprint management api', () => { } }) await expect( - uploadBlueprint(DEFAULT_CONTEXT, protectString('blueprint99'), blueprintStr) + uploadBlueprint(DEFAULT_CONNECTION, protectString('blueprint99'), blueprintStr) ).rejects.toThrowMeteor( 400, `Blueprint blueprint99 returned a manifest of unknown blueprintType "undefined"` @@ -281,7 +275,9 @@ describe('Test blueprint management api', () => { })) as Blueprint expect(existingBlueprint).toBeTruthy() - await expect(uploadBlueprint(DEFAULT_CONTEXT, existingBlueprint._id, blueprintStr)).rejects.toThrowMeteor( + await expect( + uploadBlueprint(DEFAULT_CONNECTION, existingBlueprint._id, blueprintStr) + ).rejects.toThrowMeteor( 400, `Cannot replace old blueprint (of type "showstyle") with new blueprint of type "studio"` ) @@ -305,7 +301,7 @@ describe('Test blueprint management api', () => { } ) - const blueprint = await uploadBlueprint(DEFAULT_CONTEXT, protectString('tmp_showstyle'), blueprintStr) + const blueprint = await uploadBlueprint(DEFAULT_CONNECTION, protectString('tmp_showstyle'), blueprintStr) expect(blueprint).toBeTruthy() expect(blueprint).toMatchObject( literal>({ @@ -344,7 +340,7 @@ describe('Test blueprint management api', () => { ) const blueprint = await uploadBlueprint( - DEFAULT_CONTEXT, + DEFAULT_CONNECTION, protectString('tmp_studio'), blueprintStr, 'tmp name' @@ -388,7 +384,7 @@ describe('Test blueprint management api', () => { ) const blueprint = await uploadBlueprint( - DEFAULT_CONTEXT, + DEFAULT_CONNECTION, protectString('tmp_system'), blueprintStr, 'tmp name' @@ -436,7 +432,7 @@ describe('Test blueprint management api', () => { expect(existingBlueprint).toBeTruthy() expect(existingBlueprint.blueprintId).toBeFalsy() - const blueprint = await uploadBlueprint(DEFAULT_CONTEXT, existingBlueprint._id, blueprintStr) + const blueprint = await uploadBlueprint(DEFAULT_CONNECTION, existingBlueprint._id, blueprintStr) expect(blueprint).toBeTruthy() expect(blueprint).toMatchObject( literal>({ @@ -482,7 +478,7 @@ describe('Test blueprint management api', () => { expect(existingBlueprint).toBeTruthy() expect(existingBlueprint.blueprintId).toBeTruthy() - const blueprint = await uploadBlueprint(DEFAULT_CONTEXT, existingBlueprint._id, blueprintStr) + const blueprint = await uploadBlueprint(DEFAULT_CONNECTION, existingBlueprint._id, blueprintStr) expect(blueprint).toBeTruthy() expect(blueprint).toMatchObject( literal>({ @@ -528,7 +524,9 @@ describe('Test blueprint management api', () => { expect(existingBlueprint).toBeTruthy() expect(existingBlueprint.blueprintId).toBeTruthy() - await expect(uploadBlueprint(DEFAULT_CONTEXT, existingBlueprint._id, blueprintStr)).rejects.toThrowMeteor( + await expect( + uploadBlueprint(DEFAULT_CONNECTION, existingBlueprint._id, blueprintStr) + ).rejects.toThrowMeteor( 422, `Cannot replace old blueprint "${existingBlueprint._id}" ("ss1") with new blueprint "show2"` ) @@ -558,7 +556,9 @@ describe('Test blueprint management api', () => { expect(existingBlueprint).toBeTruthy() expect(existingBlueprint.blueprintId).toBeTruthy() - await expect(uploadBlueprint(DEFAULT_CONTEXT, existingBlueprint._id, blueprintStr)).rejects.toThrowMeteor( + await expect( + uploadBlueprint(DEFAULT_CONNECTION, existingBlueprint._id, blueprintStr) + ).rejects.toThrowMeteor( 422, `Cannot replace old blueprint "${existingBlueprint._id}" ("ss1") with new blueprint ""` ) diff --git a/meteor/server/api/blueprints/__tests__/http.test.ts b/meteor/server/api/blueprints/__tests__/http.test.ts index 17d493b0e3..887b7d61d8 100644 --- a/meteor/server/api/blueprints/__tests__/http.test.ts +++ b/meteor/server/api/blueprints/__tests__/http.test.ts @@ -8,7 +8,7 @@ jest.mock('../../deviceTriggers/observer') import * as api from '../api' jest.mock('../api.ts') -const DEFAULT_CONTEXT = { userId: '' } +const DEFAULT_CONTEXT = expect.objectContaining({ req: expect.any(Object), res: expect.any(Object) }) require('../http.ts') // include in order to create the Meteor methods needed diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index e2f2ec7bc5..8a294bdfec 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -20,10 +20,6 @@ import { parseVersion } from '../../systemStatus/semverUtils' import { evalBlueprint } from './cache' import { removeSystemStatus } from '../../systemStatus/systemStatus' import { MethodContext, MethodContextAPI } from '../methodContext' -import { OrganizationContentWriteAccess, OrganizationReadAccess } from '../../security/organization' -import { SystemWriteAccess } from '../../security/system' -import { Credentials, isResolvedCredentials } from '../../security/lib/credentials' -import { Settings } from '../../Settings' import { generateTranslationBundleOriginId, upsertBundles } from '../translationsBundles' import { BlueprintId, OrganizationId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprints, CoreSystem, ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' @@ -32,21 +28,21 @@ import { getSystemStorePath } from '../../coreSystem' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions, RequestCredentials } from '../../security/auth' + +const PERMISSIONS_FOR_MANAGE_BLUEPRINTS: Array = ['configure'] export async function insertBlueprint( - methodContext: MethodContext, + cred: RequestCredentials | null, type?: BlueprintManifestType, name?: string ): Promise { - const { organizationId, cred } = await OrganizationContentWriteAccess.blueprint(methodContext) - if (Settings.enableUserAccounts && isResolvedCredentials(cred)) { - if (!cred.user || !cred.user.superAdmin) { - throw new Meteor.Error(401, 'Only super admins can create new blueprints') - } - } + assertConnectionHasOneOfPermissions(cred, ...PERMISSIONS_FOR_MANAGE_BLUEPRINTS) + return Blueprints.insertAsync({ _id: getRandomId(), - organizationId: organizationId, + organizationId: null, name: name || 'New Blueprint', hasCode: false, code: '', @@ -72,7 +68,9 @@ export async function insertBlueprint( } export async function removeBlueprint(methodContext: MethodContext, blueprintId: BlueprintId): Promise { check(blueprintId, String) - await OrganizationContentWriteAccess.blueprint(methodContext, blueprintId, true) + + assertConnectionHasOneOfPermissions(methodContext.connection, ...PERMISSIONS_FOR_MANAGE_BLUEPRINTS) + if (!blueprintId) throw new Meteor.Error(404, `Blueprint id "${blueprintId}" was not found`) await Blueprints.removeAsync(blueprintId) @@ -80,7 +78,7 @@ export async function removeBlueprint(methodContext: MethodContext, blueprintId: } export async function uploadBlueprint( - context: Credentials, + cred: RequestCredentials, blueprintId: BlueprintId, body: string, blueprintName?: string, @@ -90,19 +88,21 @@ export async function uploadBlueprint( check(body, String) check(blueprintName, Match.Maybe(String)) - // TODO: add access control here - const { organizationId } = await OrganizationContentWriteAccess.blueprint(context, blueprintId, true) + assertConnectionHasOneOfPermissions(cred, ...PERMISSIONS_FOR_MANAGE_BLUEPRINTS) + if (!Meteor.isTest) logger.info(`Got blueprint '${blueprintId}'. ${body.length} bytes`) if (!blueprintId) throw new Meteor.Error(400, `Blueprint id "${blueprintId}" is not valid`) const blueprint = await fetchBlueprintLight(blueprintId) - return innerUploadBlueprint(organizationId, blueprint, blueprintId, body, blueprintName, ignoreIdChange) + return innerUploadBlueprint(null, blueprint, blueprintId, body, blueprintName, ignoreIdChange) } -export async function uploadBlueprintAsset(_context: Credentials, fileId: string, body: string): Promise { +export async function uploadBlueprintAsset(cred: RequestCredentials, fileId: string, body: string): Promise { check(fileId, String) check(body, String) + assertConnectionHasOneOfPermissions(cred, ...PERMISSIONS_FOR_MANAGE_BLUEPRINTS) + const storePath = getSystemStorePath() // TODO: add access control here @@ -115,12 +115,11 @@ export async function uploadBlueprintAsset(_context: Credentials, fileId: string await fsp.mkdir(path.join(storePath, parsedPath.dir), { recursive: true }) await fsp.writeFile(path.join(storePath, fileId), data) } -export function retrieveBlueprintAsset(_context: Credentials, fileId: string): ReadStream { +export function retrieveBlueprintAsset(_cred: RequestCredentials, fileId: string): ReadStream { check(fileId, String) const storePath = getSystemStorePath() - // TODO: add access control here return createReadStream(path.join(storePath, fileId)) } /** Only to be called from internal functions */ @@ -363,7 +362,7 @@ async function syncConfigPresetsToStudios(blueprint: Blueprint): Promise { } async function assignSystemBlueprint(methodContext: MethodContext, blueprintId: BlueprintId | null): Promise { - await SystemWriteAccess.coreSystem(methodContext) + assertConnectionHasOneOfPermissions(methodContext.connection, ...PERMISSIONS_FOR_MANAGE_BLUEPRINTS) if (blueprintId !== undefined && blueprintId !== null) { check(blueprintId, String) @@ -371,9 +370,6 @@ async function assignSystemBlueprint(methodContext: MethodContext, blueprintId: const blueprint = await fetchBlueprintLight(blueprintId) if (!blueprint) throw new Meteor.Error(404, 'Blueprint not found') - if (blueprint.organizationId) - await OrganizationReadAccess.organizationContent(blueprint.organizationId, { userId: methodContext.userId }) - if (blueprint.blueprintType !== BlueprintManifestType.SYSTEM) throw new Meteor.Error(404, 'Blueprint not of type SYSTEM') @@ -393,7 +389,7 @@ async function assignSystemBlueprint(methodContext: MethodContext, blueprintId: class ServerBlueprintAPI extends MethodContextAPI implements ReplaceOptionalWithNullInMethodArguments { async insertBlueprint() { - return insertBlueprint(this) + return insertBlueprint(this.connection) } async removeBlueprint(blueprintId: BlueprintId) { return removeBlueprint(this, blueprintId) diff --git a/meteor/server/api/blueprints/http.ts b/meteor/server/api/blueprints/http.ts index 70a0bb520f..ae364dd81f 100644 --- a/meteor/server/api/blueprints/http.ts +++ b/meteor/server/api/blueprints/http.ts @@ -38,20 +38,12 @@ blueprintsRouter.post( check(blueprintId, String) check(blueprintName, Match.Maybe(String)) - const userId = ctx.headers.authorization ? ctx.headers.authorization.split(' ')[1] : '' - const body = ctx.request.body || ctx.req.body if (!body) throw new Meteor.Error(400, 'Restore Blueprint: Missing request body') if (typeof body !== 'string' || body.length < 10) throw new Meteor.Error(400, 'Restore Blueprint: Invalid request body') - await uploadBlueprint( - { userId: protectString(userId) }, - protectString(blueprintId), - body, - blueprintName, - force - ) + await uploadBlueprint(ctx, protectString(blueprintId), body, blueprintName, force) ctx.response.status = 200 ctx.body = '' @@ -89,13 +81,7 @@ blueprintsRouter.post( const errors: any[] = [] for (const id of _.keys(collection.blueprints)) { try { - const userId = ctx.headers.authorization ? ctx.headers.authorization.split(' ')[1] : '' - await uploadBlueprint( - { userId: protectString(userId) }, - protectString(id), - collection.blueprints[id], - id - ) + await uploadBlueprint(ctx, protectString(id), collection.blueprints[id], id) } catch (e) { logger.error('Blueprint restore failed: ' + e) errors.push(e) @@ -104,8 +90,7 @@ blueprintsRouter.post( if (collection.assets) { for (const id of _.keys(collection.assets)) { try { - const userId = ctx.headers.authorization ? ctx.headers.authorization.split(' ')[1] : '' - await uploadBlueprintAsset({ userId: protectString(userId) }, id, collection.assets[id]) + await uploadBlueprintAsset(ctx, id, collection.assets[id]) } catch (e) { logger.error('Blueprint assets upload failed: ' + e) errors.push(e) @@ -157,8 +142,7 @@ blueprintsRouter.post( const errors: any[] = [] for (const id of _.keys(collection)) { try { - const userId = ctx.headers.authorization ? ctx.headers.authorization.split(' ')[1] : '' - await uploadBlueprintAsset({ userId: protectString(userId) }, id, collection[id]) + await uploadBlueprintAsset(ctx, id, collection[id]) } catch (e) { logger.error('Blueprint assets upload failed: ' + e) errors.push(e) @@ -192,9 +176,8 @@ blueprintsRouter.get('/assets/(.*)', async (ctx) => { const filePath = ctx.params[0] if (filePath.match(/\.(png|svg|gif)?$/)) { - const userId = ctx.headers.authorization ? ctx.headers.authorization.split(' ')[1] : '' try { - const dataStream = retrieveBlueprintAsset({ userId: protectString(userId) }, filePath) + const dataStream = retrieveBlueprintAsset(ctx, filePath) const extension = path.extname(filePath) if (extension === '.svg') { ctx.response.type = 'image/svg+xml' diff --git a/meteor/server/api/buckets.ts b/meteor/server/api/buckets.ts index ac2ad69cbe..109ae82ba9 100644 --- a/meteor/server/api/buckets.ts +++ b/meteor/server/api/buckets.ts @@ -2,18 +2,23 @@ import * as _ from 'underscore' import { Meteor } from 'meteor/meteor' import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' import { getRandomId, getRandomString, literal } from '../lib/tempLib' -import { BucketSecurity } from '../security/buckets' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { AdLibAction, AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' -import { BucketAdLibActions, Buckets, Rundowns, ShowStyleVariants, Studios } from '../collections' +import { BucketAdLibActions, BucketAdLibs, Buckets, Rundowns, ShowStyleVariants, Studios } from '../collections' import { runIngestOperation } from './ingest/lib' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' -import { StudioContentAccess } from '../security/studio' -import { Settings } from '../Settings' import { IngestAdlib } from '@sofie-automation/blueprints-integration' import { getShowStyleCompound } from './showStyles' -import { ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { + BucketAdLibActionId, + BucketAdLibId, + BucketId, + ShowStyleBaseId, + ShowStyleVariantId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { fetchStudioLight } from '../optimizations' const DEFAULT_BUCKET_WIDTH = undefined @@ -25,18 +30,28 @@ function isBucketAdLibAction(action: AdLibActionCommon | BucketAdLibAction): act } export namespace BucketsAPI { - export async function removeBucketAdLib(access: BucketSecurity.BucketAdlibPieceContentAccess): Promise { - const adlib = access.adlib + export async function removeBucketAdLib(adLibId: BucketAdLibId): Promise { + const adlib = (await BucketAdLibs.findOneAsync(adLibId, { + projection: { + _id: 1, + studioId: 1, + }, + })) as Pick | undefined + if (!adlib) throw new Meteor.Error(404, `BucketAdLib "${adLibId}" not found`) await runIngestOperation(adlib.studioId, IngestJobs.BucketRemoveAdlibPiece, { pieceId: adlib._id, }) } - export async function removeBucketAdLibAction( - access: BucketSecurity.BucketAdlibActionContentAccess - ): Promise { - const adlib = access.action + export async function removeBucketAdLibAction(adLibActionId: BucketAdLibActionId): Promise { + const adlib = (await BucketAdLibActions.findOneAsync(adLibActionId, { + projection: { + _id: 1, + studioId: 1, + }, + })) as Pick | undefined + if (!adlib) throw new Meteor.Error(404, `BucketAdLibAction "${adLibActionId}" not found`) await runIngestOperation(adlib.studioId, IngestJobs.BucketRemoveAdlibAction, { actionId: adlib._id, @@ -44,22 +59,26 @@ export namespace BucketsAPI { } export async function modifyBucket( - access: BucketSecurity.BucketContentAccess, + bucketId: BucketId, bucketProps: Partial> ): Promise { - await Buckets.updateAsync(access.bucket._id, { + await Buckets.updateAsync(bucketId, { $set: _.omit(bucketProps, ['_id', 'studioId']), }) } - export async function emptyBucket(access: BucketSecurity.BucketContentAccess): Promise { - await runIngestOperation(access.studioId, IngestJobs.BucketEmpty, { - bucketId: access.bucket._id, + export async function emptyBucket(bucketId: BucketId): Promise { + const bucket = await Buckets.findOneAsync(bucketId) + if (!bucket) throw new Meteor.Error(404, `Bucket "${bucketId}" not found`) + + await runIngestOperation(bucket.studioId, IngestJobs.BucketEmpty, { + bucketId: bucket._id, }) } - export async function createNewBucket(access: StudioContentAccess, name: string): Promise { - const { studio } = access + export async function createNewBucket(studioId: StudioId, name: string): Promise { + const studio = await fetchStudioLight(studioId) + if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`) const heaviestBucket = ( await Buckets.findFetchAsync( @@ -99,28 +118,20 @@ export namespace BucketsAPI { } export async function modifyBucketAdLibAction( - access: BucketSecurity.BucketAdlibActionContentAccess, + adLibActionId: BucketAdLibActionId, actionProps: Partial> ): Promise { - const oldAction = access.action + const oldAction = await BucketAdLibActions.findOneAsync(adLibActionId) + if (!oldAction) throw new Meteor.Error(404, `BucketAdLibAction "${adLibActionId}" not found`) - let moveIntoBucket: Bucket | undefined if (actionProps.bucketId && actionProps.bucketId !== oldAction.bucketId) { - moveIntoBucket = await Buckets.findOneAsync(actionProps.bucketId) - if (!moveIntoBucket) throw new Meteor.Error(`Could not find bucket: "${actionProps.bucketId}"`) + const moveIntoBucket = await Buckets.countDocuments(actionProps.bucketId) + if (moveIntoBucket === 0) throw new Meteor.Error(`Could not find bucket: "${actionProps.bucketId}"`) } - // Check we are allowed to move into the new bucket - if (Settings.enableUserAccounts && moveIntoBucket) { - // Shouldn't be moved across orgs - const newBucketStudio = await Studios.findOneAsync(moveIntoBucket.studioId, { - fields: { organizationId: 1 }, - }) - if (!newBucketStudio) throw new Meteor.Error(`Could not find studio: "${moveIntoBucket.studioId}"`) - - if (newBucketStudio.organizationId !== access.studio.organizationId) { - throw new Meteor.Error(403, 'Access denied') - } + if (actionProps.studioId && actionProps.studioId !== oldAction.studioId) { + const newStudioCount = await Studios.countDocuments(actionProps.studioId) + if (newStudioCount === 0) throw new Meteor.Error(`Could not find studio: "${actionProps.studioId}"`) } await runIngestOperation(oldAction.studioId, IngestJobs.BucketActionModify, { @@ -130,25 +141,30 @@ export namespace BucketsAPI { } export async function saveAdLibActionIntoBucket( - access: BucketSecurity.BucketContentAccess, + bucketId: BucketId, action: AdLibActionCommon | BucketAdLibAction ): Promise { + const targetBucket = (await Buckets.findOneAsync(bucketId, { projection: { _id: 1, studioId: 1 } })) as + | Pick + | undefined + if (!targetBucket) throw new Meteor.Error(404, `Bucket "${bucketId}" not found`) + let adLibAction: BucketAdLibAction if (isBucketAdLibAction(action)) { if (action.showStyleVariantId && !(await ShowStyleVariants.findOneAsync(action.showStyleVariantId))) { throw new Meteor.Error(`Could not find show style variant: "${action.showStyleVariantId}"`) } - if (access.studioId !== action.studioId) { + if (targetBucket.studioId !== action.studioId) { throw new Meteor.Error( - `studioId is different than Action's studioId: "${access.studioId}" - "${action.studioId}"` + `studioId is different than Action's studioId: "${targetBucket.studioId}" - "${action.studioId}"` ) } adLibAction = { ...action, _id: getRandomId(), - bucketId: access.bucket._id, + bucketId: targetBucket._id, } } else { const rundown = await Rundowns.findOneAsync(action.rundownId) @@ -156,9 +172,9 @@ export namespace BucketsAPI { throw new Meteor.Error(`Could not find rundown: "${action.rundownId}"`) } - if (access.studioId !== rundown.studioId) { + if (targetBucket.studioId !== rundown.studioId) { throw new Meteor.Error( - `studioId is different than Rundown's studioId: "${access.studioId}" - "${rundown.studioId}"` + `studioId is different than Rundown's studioId: "${targetBucket.studioId}" - "${rundown.studioId}"` ) } @@ -166,8 +182,8 @@ export namespace BucketsAPI { ...(_.omit(action, ['partId', 'rundownId']) as Omit), _id: getRandomId(), externalId: getRandomString(), // This needs to be something unique, so that the regenerate logic doesn't get it mixed up with something else - bucketId: access.bucket._id, - studioId: access.studioId, + bucketId: targetBucket._id, + studioId: targetBucket.studioId, showStyleBaseId: rundown.showStyleBaseId, showStyleVariantId: action.allVariants ? null : rundown.showStyleVariantId, importVersions: rundown.importVersions, @@ -178,7 +194,7 @@ export namespace BucketsAPI { // We can insert it here, as it is a creation with a new id, so the only race risk we have is the bucket being deleted await BucketAdLibActions.insertAsync(adLibAction) - await runIngestOperation(access.studioId, IngestJobs.BucketActionRegenerateExpectedPackages, { + await runIngestOperation(targetBucket.studioId, IngestJobs.BucketActionRegenerateExpectedPackages, { actionId: adLibAction._id, }) @@ -186,28 +202,20 @@ export namespace BucketsAPI { } export async function modifyBucketAdLib( - access: BucketSecurity.BucketAdlibPieceContentAccess, + adLibId: BucketAdLibId, adlibProps: Partial> ): Promise { - const oldAdLib = access.adlib + const oldAdLib = await BucketAdLibs.findOneAsync(adLibId) + if (!oldAdLib) throw new Meteor.Error(404, `BucketAdLib "${adLibId}" not found`) - let moveIntoBucket: Bucket | undefined if (adlibProps.bucketId && adlibProps.bucketId !== oldAdLib.bucketId) { - moveIntoBucket = await Buckets.findOneAsync(adlibProps.bucketId) - if (!moveIntoBucket) throw new Meteor.Error(`Could not find bucket: "${adlibProps.bucketId}"`) + const moveIntoBucket = await Buckets.countDocuments(adlibProps.bucketId) + if (moveIntoBucket === 0) throw new Meteor.Error(`Could not find bucket: "${adlibProps.bucketId}"`) } - // Check we are allowed to move into the new bucket - if (Settings.enableUserAccounts && moveIntoBucket) { - // Shouldn't be moved across orgs - const newBucketStudio = await Studios.findOneAsync(moveIntoBucket.studioId, { - fields: { organizationId: 1 }, - }) - if (!newBucketStudio) throw new Meteor.Error(`Could not find studio: "${moveIntoBucket.studioId}"`) - - if (newBucketStudio.organizationId !== access.studio.organizationId) { - throw new Meteor.Error(403, 'Access denied') - } + if (adlibProps.studioId && adlibProps.studioId !== oldAdLib.studioId) { + const newStudioCount = await Studios.countDocuments(adlibProps.studioId) + if (newStudioCount === 0) throw new Meteor.Error(`Could not find studio: "${adlibProps.studioId}"`) } await runIngestOperation(oldAdLib.studioId, IngestJobs.BucketPieceModify, { @@ -216,8 +224,10 @@ export namespace BucketsAPI { }) } - export async function removeBucket(access: BucketSecurity.BucketContentAccess): Promise { - const bucket = access.bucket + export async function removeBucket(bucketId: BucketId): Promise { + const bucket = await Buckets.findOneAsync(bucketId) + if (!bucket) throw new Meteor.Error(404, `Bucket "${bucketId}" not found`) + await Promise.all([ Buckets.removeAsync(bucket._id), await runIngestOperation(bucket.studioId, IngestJobs.BucketEmpty, { @@ -227,13 +237,17 @@ export namespace BucketsAPI { } export async function importAdlibToBucket( - access: BucketSecurity.BucketContentAccess, + bucketId: BucketId, showStyleBaseId: ShowStyleBaseId, /** Optional: if set, only create adlib for this variant (otherwise: for all variants in ShowStyleBase)*/ showStyleVariantId: ShowStyleVariantId | undefined, ingestItem: IngestAdlib ): Promise { - const studioLight = access.studio + const bucket = await Buckets.findOneAsync(bucketId) + if (!bucket) throw new Meteor.Error(404, `Bucket "${bucketId}" not found`) + + const studioLight = await fetchStudioLight(bucket.studioId) + if (!studioLight) throw new Meteor.Error(404, `Studio "${bucket.studioId}" not found`) if (showStyleVariantId) { const showStyleCompound = await getShowStyleCompound(showStyleVariantId) @@ -249,12 +263,12 @@ export namespace BucketsAPI { if (studioLight.supportedShowStyleBase.indexOf(showStyleBaseId) === -1) { throw new Meteor.Error( 500, - `ShowStyle base "${showStyleBaseId}" not supported by studio "${access.studioId}"` + `ShowStyle base "${showStyleBaseId}" not supported by studio "${bucket.studioId}"` ) } - await runIngestOperation(access.studioId, IngestJobs.BucketItemImport, { - bucketId: access.bucket._id, + await runIngestOperation(bucket.studioId, IngestJobs.BucketItemImport, { + bucketId: bucket._id, showStyleBaseId: showStyleBaseId, showStyleVariantIds: showStyleVariantId ? [showStyleVariantId] : undefined, payload: ingestItem, diff --git a/meteor/server/api/cleanup.ts b/meteor/server/api/cleanup.ts index a15f3b49f1..139547344d 100644 --- a/meteor/server/api/cleanup.ts +++ b/meteor/server/api/cleanup.ts @@ -125,10 +125,6 @@ export async function cleanupOldDataInner(actuallyCleanup = false): Promise { + async (userActionMetadata) => { checkArgs() - const access = await checkAccessToPlaylist(context, playlistId) - return runStudioJob(access.playlist.studioId, jobName, jobArguments, userActionMetadata) + const playlist = await checkAccessToPlaylist(context.connection, playlistId) + return runStudioJob(playlist.studioId, jobName, jobArguments, userActionMetadata) } ) } @@ -92,11 +87,11 @@ export namespace ServerClientAPI { eventTime, `worker.${jobName}`, jobArguments as any, - async (_credentials, userActionMetadata) => { + async (userActionMetadata) => { checkArgs() - const access = await checkAccessToRundown(context, rundownId) - return runStudioJob(access.rundown.studioId, jobName, jobArguments, userActionMetadata) + const rundown = await checkAccessToRundown(context.connection, rundownId) + return runStudioJob(rundown.studioId, jobName, jobArguments, userActionMetadata) } ) } @@ -112,13 +107,13 @@ export namespace ServerClientAPI { checkArgs: () => void, methodName: string, args: Record, - fcn: (access: VerifiedRundownPlaylistContentAccess) => Promise + fcn: (playlist: VerifiedRundownPlaylistForUserAction) => Promise ): Promise> { return runUserActionInLog(context, userEvent, eventTime, methodName, args, async () => { checkArgs() - const access = await checkAccessToPlaylist(context, playlistId) - return fcn(access) + const playlist = await checkAccessToPlaylist(context.connection, playlistId) + return fcn(playlist) }) } @@ -133,13 +128,13 @@ export namespace ServerClientAPI { checkArgs: () => void, methodName: string, args: Record, - fcn: (access: VerifiedRundownContentAccess) => Promise + fcn: (rundown: VerifiedRundownForUserAction) => Promise ): Promise> { return runUserActionInLog(context, userEvent, eventTime, methodName, args, async () => { checkArgs() - const access = await checkAccessToRundown(context, rundownId) - return fcn(access) + const rundown = await checkAccessToRundown(context.connection, rundownId) + return fcn(rundown) }) } @@ -185,11 +180,11 @@ export namespace ServerClientAPI { eventTime: Time, methodName: string, methodArgs: Record, - fcn: (credentials: BasicAccessContext, userActionMetadata: UserActionMetadata) => Promise + fcn: (userActionMetadata: UserActionMetadata) => Promise ): Promise> { // If we are in the test write auth check mode, then bypass all special logic to ensure errors dont get mangled if (isInTestWrite()) { - const result = await fcn({ organizationId: null, userId: null }, {}) + const result = await fcn({}) return ClientAPI.responseSuccess(result) } @@ -203,23 +198,21 @@ export namespace ServerClientAPI { // Called internally from server-side. // Just run and return right away: try { - const result = await fcn({ organizationId: null, userId: null }, {}) + const result = await fcn({}) return ClientAPI.responseSuccess(result) } catch (e) { return rewrapError(methodName, e) } } else { - const credentials = await getLoggedInCredentials(context) - // Start the db entry, but don't wait for it const actionId: UserActionsLogItemId = getRandomId() const pInitialInsert = UserActionsLog.insertAsync( literal({ _id: actionId, clientAddress: context.connection.clientAddress, - organizationId: credentials.organizationId, - userId: credentials.userId, + organizationId: null, + userId: null, context: userEvent, method: methodName, args: JSON.stringify(methodArgs), @@ -233,7 +226,7 @@ export namespace ServerClientAPI { const userActionMetadata: UserActionMetadata = {} try { - const result = await fcn(credentials, userActionMetadata) + const result = await fcn(userActionMetadata) const completeTime = Date.now() pInitialInsert @@ -325,14 +318,15 @@ export namespace ServerClientAPI { }) } - const access = await PeripheralDeviceContentWriteAccess.executeFunction(methodContext, deviceId) + // TODO - check this. This probably needs to be moved out of this method, with the client using more targetted methods + assertConnectionHasOneOfPermissions(methodContext.connection, 'studio', 'configure', 'service') await UserActionsLog.insertAsync( literal({ _id: actionId, clientAddress: methodContext.connection ? methodContext.connection.clientAddress : '', - organizationId: access.organizationId, - userId: access.userId, + organizationId: null, + userId: null, context: context, method: `${deviceId}: ${method}`, args: JSON.stringify(args), @@ -395,7 +389,8 @@ export namespace ServerClientAPI { }) } - await PeripheralDeviceContentWriteAccess.executeFunction(methodContext, deviceId) + // TODO - check this. This probably needs to be moved out of this method, with the client using more targetted methods + assertConnectionHasOneOfPermissions(methodContext.connection, 'studio', 'configure', 'service') return executePeripheralDeviceFunctionWithCustomTimeout(deviceId, timeoutTime, { functionName, @@ -407,17 +402,6 @@ export namespace ServerClientAPI { return Promise.reject(err) }) } - - async function getLoggedInCredentials(methodContext: MethodContext): Promise { - let userId: UserId | null = null - let organizationId: OrganizationId | null = null - if (Settings.enableUserAccounts) { - const cred = await resolveCredentials({ userId: methodContext.userId }) - if (cred.user) userId = cred.user._id - organizationId = cred.organizationId - } - return { userId, organizationId } - } } class ServerClientAPIClass extends MethodContextAPI implements NewClientAPI { diff --git a/meteor/server/api/deviceTriggers/observer.ts b/meteor/server/api/deviceTriggers/observer.ts index aa6bc4bc86..c155bcb600 100644 --- a/meteor/server/api/deviceTriggers/observer.ts +++ b/meteor/server/api/deviceTriggers/observer.ts @@ -10,7 +10,7 @@ import { PreviewWrappedAdLib, } from '@sofie-automation/meteor-lib/dist/api/MountedTriggers' import { logger } from '../../logging' -import { checkAccessAndGetPeripheralDevice } from '../ingest/lib' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' import { StudioActionManagers } from './StudioActionManagers' import { JobQueueWithClasses } from '@sofie-automation/shared-lib/dist/lib/JobQueueWithClasses' import { StudioDeviceTriggerManager } from './StudioDeviceTriggerManager' diff --git a/meteor/server/api/evaluations.ts b/meteor/server/api/evaluations.ts index 3034110a65..386acba33d 100644 --- a/meteor/server/api/evaluations.ts +++ b/meteor/server/api/evaluations.ts @@ -7,22 +7,19 @@ import { Meteor } from 'meteor/meteor' import * as _ from 'underscore' import { fetchStudioLight } from '../optimizations' import { sendSlackMessageToWebhook } from './integration/slack' -import { OrganizationId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Evaluations, RundownPlaylists } from '../collections' +import { VerifiedRundownPlaylistForUserAction } from '../security/check' export async function saveEvaluation( - credentials: { - userId: UserId | null - organizationId: OrganizationId | null - }, + _playlist: VerifiedRundownPlaylistForUserAction, evaluation: EvaluationBase ): Promise { await Evaluations.insertAsync({ ...evaluation, _id: getRandomId(), - organizationId: credentials.organizationId, - userId: credentials.userId, + organizationId: null, + userId: null, timestamp: getCurrentTime(), }) logger.info({ diff --git a/meteor/server/api/heapSnapshot.ts b/meteor/server/api/heapSnapshot.ts index 1b9cb142a2..876b60be7a 100644 --- a/meteor/server/api/heapSnapshot.ts +++ b/meteor/server/api/heapSnapshot.ts @@ -7,14 +7,11 @@ import { fixValidPath } from '../lib/lib' import { sleep } from '../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { logger } from '../logging' -import { Settings } from '../Settings' -import { Credentials } from '../security/lib/credentials' -import { SystemWriteAccess } from '../security/system' +import { assertConnectionHasOneOfPermissions, RequestCredentials } from '../security/auth' + +async function retrieveHeapSnapshot(cred: RequestCredentials): Promise { + assertConnectionHasOneOfPermissions(cred, 'developer') -async function retrieveHeapSnapshot(cred0: Credentials): Promise { - if (Settings.enableUserAccounts) { - await SystemWriteAccess.coreSystem(cred0) - } logger.warn('Taking heap snapshot, expect system to be unresponsive for a few seconds..') await sleep(100) // Allow the logger to catch up before continuing.. @@ -51,19 +48,9 @@ async function handleKoaResponse(ctx: Koa.ParameterizedContext, snapshotFcn: () } } -// For backwards compatibility: -if (!Settings.enableUserAccounts) { - // Retrieve heap snapshot: - heapSnapshotPrivateApiRouter.get('/retrieve', async (ctx) => { - return handleKoaResponse(ctx, async () => { - return retrieveHeapSnapshot({ userId: null }) - }) - }) -} - // Retrieve heap snapshot: -heapSnapshotPrivateApiRouter.get('/:token/retrieve', async (ctx) => { +heapSnapshotPrivateApiRouter.get('/retrieve', async (ctx) => { return handleKoaResponse(ctx, async () => { - return retrieveHeapSnapshot({ userId: null, token: ctx.params.token }) + return retrieveHeapSnapshot(ctx) }) }) diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index dc46d4a3fa..6a6cf852bf 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -1,12 +1,12 @@ import { getPeripheralDeviceFromRundown, runIngestOperation } from './lib' import { MOSDeviceActions } from './mosDevice/actions' import { Meteor } from 'meteor/meteor' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { TriggerReloadDataResponse } from '@sofie-automation/meteor-lib/dist/api/userActions' import { GenericDeviceActions } from './genericDevice/actions' import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { VerifiedRundownForUserAction } from '../../security/check' /* This file contains actions that can be performed on an ingest-device @@ -15,9 +15,7 @@ export namespace IngestActions { /** * Trigger a reload of a rundown */ - export async function reloadRundown( - rundown: Pick - ): Promise { + export async function reloadRundown(rundown: VerifiedRundownForUserAction): Promise { const rundownSourceType = rundown.source.type switch (rundown.source.type) { case 'snapshot': diff --git a/meteor/server/api/ingest/lib.ts b/meteor/server/api/ingest/lib.ts index 15cd36b428..f3e761b887 100644 --- a/meteor/server/api/ingest/lib.ts +++ b/meteor/server/api/ingest/lib.ts @@ -5,9 +5,6 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE import { PeripheralDevice, PeripheralDeviceCategory } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Rundown, RundownSourceNrcs } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { logger } from '../../logging' -import { PeripheralDeviceContentWriteAccess } from '../../security/peripheralDevice' -import { MethodContext } from '../methodContext' -import { Credentials } from '../../security/lib/credentials' import { profiler } from '../profiler' import { IngestJobFunc } from '@sofie-automation/corelib/dist/worker/ingest' import { QueueIngestJob } from '../../worker/worker' @@ -64,26 +61,6 @@ export async function runIngestOperation( } } -/** Check Access and return PeripheralDevice, throws otherwise */ -export async function checkAccessAndGetPeripheralDevice( - deviceId: PeripheralDeviceId, - token: string | undefined, - context: Credentials | MethodContext -): Promise { - const span = profiler.startSpan('lib.checkAccessAndGetPeripheralDevice') - - const { device: peripheralDevice } = await PeripheralDeviceContentWriteAccess.peripheralDevice( - { userId: context.userId, token }, - deviceId - ) - if (!peripheralDevice) { - throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) - } - - span?.end() - return peripheralDevice -} - export function getRundownId(studioId: StudioId, rundownExternalId: string): RundownId { if (!studioId) throw new Meteor.Error(500, 'getRundownId: studio not set!') if (!rundownExternalId) throw new Meteor.Error(401, 'getRundownId: rundownExternalId must be set!') diff --git a/meteor/server/api/ingest/mosDevice/mosIntegration.ts b/meteor/server/api/ingest/mosDevice/mosIntegration.ts index ad226d6d84..6969159e89 100644 --- a/meteor/server/api/ingest/mosDevice/mosIntegration.ts +++ b/meteor/server/api/ingest/mosDevice/mosIntegration.ts @@ -1,16 +1,12 @@ import { MOS } from '@sofie-automation/corelib' import { logger } from '../../../logging' -import { - checkAccessAndGetPeripheralDevice, - fetchStudioIdFromDevice, - generateRundownSource, - runIngestOperation, -} from '../lib' +import { fetchStudioIdFromDevice, generateRundownSource, runIngestOperation } from '../lib' import { parseMosString } from './lib' import { MethodContext } from '../../methodContext' import { profiler } from '../../profiler' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { checkAccessAndGetPeripheralDevice } from '../../../security/check' const apmNamespace = 'mosIntegration' diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 8b05552eba..4013b465ef 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -7,18 +7,14 @@ import { lazyIgnore } from '../../lib/lib' import { IngestRundown, IngestSegment, IngestPart, IngestPlaylist } from '@sofie-automation/blueprints-integration' import { logger } from '../../logging' import { RundownIngestDataCache } from './ingestCache' -import { - checkAccessAndGetPeripheralDevice, - fetchStudioIdFromDevice, - generateRundownSource, - runIngestOperation, -} from './lib' +import { fetchStudioIdFromDevice, generateRundownSource, runIngestOperation } from './lib' import { MethodContext } from '../methodContext' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { MediaObject } from '@sofie-automation/shared-lib/dist/core/model/MediaObjects' import { PeripheralDeviceId, RundownId, SegmentId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' export namespace RundownInput { export async function dataPlaylistGet( diff --git a/meteor/server/api/integration/expectedPackages.ts b/meteor/server/api/integration/expectedPackages.ts index 1c3d489b9a..8e823b968f 100644 --- a/meteor/server/api/integration/expectedPackages.ts +++ b/meteor/server/api/integration/expectedPackages.ts @@ -1,7 +1,7 @@ import { check } from '../../lib/check' import { Meteor } from 'meteor/meteor' import { MethodContext } from '../methodContext' -import { checkAccessAndGetPeripheralDevice } from '../ingest/lib' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' import { ExpectedPackageStatusAPI, PackageInfo } from '@sofie-automation/blueprints-integration' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' import { assertNever, literal, protectString } from '../../lib/tempLib' diff --git a/meteor/server/api/integration/media-scanner.ts b/meteor/server/api/integration/media-scanner.ts index 4af257f01a..cda12b162e 100644 --- a/meteor/server/api/integration/media-scanner.ts +++ b/meteor/server/api/integration/media-scanner.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' import { protectString } from '../../lib/tempLib' -import { checkAccessAndGetPeripheralDevice } from '../ingest/lib' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' import { MethodContext } from '../methodContext' import { MediaObject } from '@sofie-automation/shared-lib/dist/core/model/MediaObjects' import { MediaObjId, PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' diff --git a/meteor/server/api/integration/mediaWorkFlows.ts b/meteor/server/api/integration/mediaWorkFlows.ts index 20c1a65bf6..36fb3e2461 100644 --- a/meteor/server/api/integration/mediaWorkFlows.ts +++ b/meteor/server/api/integration/mediaWorkFlows.ts @@ -9,7 +9,7 @@ import { } from '@sofie-automation/shared-lib/dist/peripheralDevice/mediaManager' import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { MethodContext } from '../methodContext' -import { checkAccessAndGetPeripheralDevice } from '../ingest/lib' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' import { MediaWorkFlowId, MediaWorkFlowStepId, PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MediaWorkFlows, MediaWorkFlowSteps } from '../../collections' diff --git a/meteor/server/api/lib.ts b/meteor/server/api/lib.ts deleted file mode 100644 index e0d4ed7dea..0000000000 --- a/meteor/server/api/lib.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { RundownId, RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Meteor } from 'meteor/meteor' -import { MethodContext } from './methodContext' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { - RundownContentAccess, - RundownPlaylistContentAccess, - RundownPlaylistContentWriteAccess, -} from '../security/rundownPlaylist' - -/** - * This is returned from a check of access to a playlist, when access is granted. - * Fields will be populated about the user. - * It is identical to RundownPlaylistContentAccess, except for confirming access is allowed - */ -export interface VerifiedRundownPlaylistContentAccess extends RundownPlaylistContentAccess { - playlist: DBRundownPlaylist - studioId: StudioId -} -/** - * This is returned from a check of access to a rundown, when access is granted. - * Fields will be populated about the user. - * It is identical to RundownContentAccess, except for confirming access is allowed - */ -export interface VerifiedRundownContentAccess extends RundownContentAccess { - rundown: Rundown - studioId: StudioId -} - -/** - * Check that the current user has write access to the specified playlist, and ensure that the playlist exists - * @param context - * @param playlistId Id of the playlist - */ -export async function checkAccessToPlaylist( - context: MethodContext, - playlistId: RundownPlaylistId -): Promise { - const access = await RundownPlaylistContentWriteAccess.playout(context, playlistId) - const playlist = access.playlist - if (!playlist) throw new Meteor.Error(404, `Rundown Playlist "${playlistId}" not found!`) - return { - ...access, - playlist, - studioId: playlist.studioId, - } -} - -/** - * Check that the current user has write access to the specified rundown, and ensure that the rundown exists - * @param context - * @param rundownId Id of the rundown - */ -export async function checkAccessToRundown( - context: MethodContext, - rundownId: RundownId -): Promise { - const access = await RundownPlaylistContentWriteAccess.rundown(context, rundownId) - const rundown = access.rundown - if (!rundown) throw new Meteor.Error(404, `Rundown "${rundownId}" not found!`) - return { - ...access, - rundown, - studioId: rundown.studioId, - } -} diff --git a/meteor/server/api/mediaManager.ts b/meteor/server/api/mediaManager.ts index 3370b85944..ea9caa8113 100644 --- a/meteor/server/api/mediaManager.ts +++ b/meteor/server/api/mediaManager.ts @@ -1,74 +1,77 @@ import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { MediaWorkFlowContentAccess } from '../security/peripheralDevice' -import { BasicAccessContext } from '../security/organization' import { MediaWorkFlows, PeripheralDevices } from '../collections' import { executePeripheralDeviceFunction } from './peripheralDevice/executeFunction' +import { MediaWorkFlowId, OrganizationId, PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -export namespace MediaManagerAPI { - export async function restartAllWorkflows(access: BasicAccessContext): Promise { - const devices: Array> = await PeripheralDevices.findFetchAsync( - access.organizationId ? { organizationId: access.organizationId } : {}, - { - fields: { - _id: 1, - }, - } - ) - const workflows: Array> = await MediaWorkFlows.findFetchAsync( - { - deviceId: { $in: devices.map((d) => d._id) }, +export async function restartAllWorkflows(organizationId: OrganizationId | null): Promise { + const devices: Array> = await PeripheralDevices.findFetchAsync( + organizationId ? { organizationId: organizationId } : {}, + { + fields: { + _id: 1, }, - { - fields: { - deviceId: 1, - }, - } - ) + } + ) + const workflows: Array> = await MediaWorkFlows.findFetchAsync( + { + deviceId: { $in: devices.map((d) => d._id) }, + }, + { + fields: { + deviceId: 1, + }, + } + ) - const deviceIds = Array.from(new Set(workflows.map((w) => w.deviceId))) + const deviceIds = Array.from(new Set(workflows.map((w) => w.deviceId))) - await Promise.all( - deviceIds.map(async (deviceId) => executePeripheralDeviceFunction(deviceId, 'restartAllWorkflows')) - ) - } - export async function abortAllWorkflows(access: BasicAccessContext): Promise { - const devices: Array> = await PeripheralDevices.findFetchAsync( - access.organizationId ? { organizationId: access.organizationId } : {}, - { - fields: { - _id: 1, - }, - } - ) - const workflows: Array> = await MediaWorkFlows.findFetchAsync( - { - deviceId: { $in: devices.map((d) => d._id) }, + await Promise.all( + deviceIds.map(async (deviceId) => executePeripheralDeviceFunction(deviceId, 'restartAllWorkflows')) + ) +} +export async function abortAllWorkflows(organizationId: OrganizationId | null): Promise { + const devices: Array> = await PeripheralDevices.findFetchAsync( + organizationId ? { organizationId: organizationId } : {}, + { + fields: { + _id: 1, + }, + } + ) + const workflows: Array> = await MediaWorkFlows.findFetchAsync( + { + deviceId: { $in: devices.map((d) => d._id) }, + }, + { + fields: { + deviceId: 1, }, - { - fields: { - deviceId: 1, - }, - } - ) + } + ) + + const deviceIds = Array.from(new Set(workflows.map((w) => w.deviceId))) + + await Promise.all(deviceIds.map(async (deviceId) => executePeripheralDeviceFunction(deviceId, 'abortAllWorkflows'))) +} - const deviceIds = Array.from(new Set(workflows.map((w) => w.deviceId))) +export async function restartWorkflow(deviceId: PeripheralDeviceId, workflowId: MediaWorkFlowId): Promise { + await ensureWorkflowExists(workflowId) - await Promise.all( - deviceIds.map(async (deviceId) => executePeripheralDeviceFunction(deviceId, 'abortAllWorkflows')) - ) - } + await executePeripheralDeviceFunction(deviceId, 'restartWorkflow', workflowId) +} +export async function abortWorkflow(deviceId: PeripheralDeviceId, workflowId: MediaWorkFlowId): Promise { + await ensureWorkflowExists(workflowId) + + await executePeripheralDeviceFunction(deviceId, 'abortWorkflow', workflowId) +} +export async function prioritizeWorkflow(deviceId: PeripheralDeviceId, workflowId: MediaWorkFlowId): Promise { + await ensureWorkflowExists(workflowId) + + await executePeripheralDeviceFunction(deviceId, 'prioritizeWorkflow', workflowId) +} - export async function restartWorkflow(access: MediaWorkFlowContentAccess): Promise { - const workflow = access.mediaWorkFlow - await executePeripheralDeviceFunction(workflow.deviceId, 'restartWorkflow', workflow._id) - } - export async function abortWorkflow(access: MediaWorkFlowContentAccess): Promise { - const workflow = access.mediaWorkFlow - await executePeripheralDeviceFunction(workflow.deviceId, 'abortWorkflow', workflow._id) - } - export async function prioritizeWorkflow(access: MediaWorkFlowContentAccess): Promise { - const workflow = access.mediaWorkFlow - await executePeripheralDeviceFunction(workflow.deviceId, 'prioritizeWorkflow', workflow._id) - } +async function ensureWorkflowExists(workflowId: MediaWorkFlowId): Promise { + const doc = await MediaWorkFlows.findOneAsync(workflowId, { projection: { _id: 1 } }) + if (!doc) throw new Error(`Workflow "${workflowId}" not found`) } diff --git a/meteor/server/api/methodContext.ts b/meteor/server/api/methodContext.ts index cd8e962aae..4c564f75c4 100644 --- a/meteor/server/api/methodContext.ts +++ b/meteor/server/api/methodContext.ts @@ -1,21 +1,10 @@ import { Meteor } from 'meteor/meteor' -import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' -export interface MethodContext extends Omit { - userId: UserId | null -} +export type MethodContext = Omit /** Abstarct class to be used when defining Mehod-classes */ export abstract class MethodContextAPI implements MethodContext { // These properties are added by Meteor to the `this` context when calling methods - public userId!: UserId | null - public isSimulation!: boolean - public setUserId(_userId: string | null): void { - throw new Meteor.Error( - 500, - `This shoulc never be called, there's something wrong in with 'this' in the calling method` - ) - } public unblock(): void { throw new Meteor.Error( 500, diff --git a/meteor/server/api/organizations.ts b/meteor/server/api/organizations.ts index df8e0cedfd..e56b615163 100644 --- a/meteor/server/api/organizations.ts +++ b/meteor/server/api/organizations.ts @@ -4,14 +4,15 @@ import { MethodContextAPI, MethodContext } from './methodContext' import { NewOrganizationAPI, OrganizationAPIMethods } from '@sofie-automation/meteor-lib/dist/api/organization' import { registerClassToMeteorMethods } from '../methods' import { DBOrganization, DBOrganizationBase } from '@sofie-automation/meteor-lib/dist/collections/Organization' -import { OrganizationContentWriteAccess } from '../security/organization' -import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify' import { insertStudioInner } from './studio/api' import { insertShowStyleBaseInner } from './showStyles' -import { resetCredentials } from '../security/lib/credentials' import { BlueprintId, OrganizationId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Blueprints, CoreSystem, Organizations, ShowStyleBases, Studios, Users } from '../collections' +import { Blueprints, CoreSystem, Organizations, ShowStyleBases, Studios } from '../collections' import { getCoreSystemAsync } from '../coreSystem/collection' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +const PERMISSIONS_FOR_MANAGE_ORGANIZATIONS: Array = ['configure'] async function createDefaultEnvironmentForOrg(orgId: OrganizationId) { let systemBlueprintId: BlueprintId | undefined @@ -43,8 +44,11 @@ async function createDefaultEnvironmentForOrg(orgId: OrganizationId) { await ShowStyleBases.updateAsync(showStyleId, { $set: { blueprintId: showStyleBlueprintId } }) } -export async function createOrganization(organization: DBOrganizationBase): Promise { - triggerWriteAccessBecauseNoCheckNecessary() +export async function createOrganization( + context: MethodContext, + organization: DBOrganizationBase +): Promise { + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_ORGANIZATIONS) const orgId = await Organizations.insertAsync( literal({ @@ -61,12 +65,8 @@ export async function createOrganization(organization: DBOrganizationBase): Prom } async function removeOrganization(context: MethodContext, organizationId: OrganizationId) { - await OrganizationContentWriteAccess.organization(context, organizationId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_ORGANIZATIONS) - const users = await Users.findFetchAsync({ organizationId }) - users.forEach((user) => { - resetCredentials({ userId: user._id }) - }) await Organizations.removeAsync(organizationId) } diff --git a/meteor/server/api/packageManager.ts b/meteor/server/api/packageManager.ts index 0988826393..c446852f25 100644 --- a/meteor/server/api/packageManager.ts +++ b/meteor/server/api/packageManager.ts @@ -3,43 +3,31 @@ import { PeripheralDeviceType, PERIPHERAL_SUBTYPE_PROCESS, } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' -import { StudioContentAccess } from '../security/studio' import { PeripheralDevices } from '../collections' import { executePeripheralDeviceFunction } from './peripheralDevice/executeFunction' +import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -export namespace PackageManagerAPI { - export async function restartExpectation( - access: PeripheralDeviceContentWriteAccess.ContentAccess, - workId: string - ): Promise { - await executePeripheralDeviceFunction(access.deviceId, 'restartExpectation', workId) - } - export async function abortExpectation( - access: PeripheralDeviceContentWriteAccess.ContentAccess, - workId: string - ): Promise { - await executePeripheralDeviceFunction(access.deviceId, 'abortExpectation', workId) - } +export async function restartExpectation(deviceId: PeripheralDeviceId, workId: string): Promise { + await executePeripheralDeviceFunction(deviceId, 'restartExpectation', workId) +} +export async function abortExpectation(deviceId: PeripheralDeviceId, workId: string): Promise { + await executePeripheralDeviceFunction(deviceId, 'abortExpectation', workId) +} - export async function restartAllExpectationsInStudio(access: StudioContentAccess): Promise { - const packageManagerDevices = await PeripheralDevices.findFetchAsync({ - studioId: access.studioId, - category: PeripheralDeviceCategory.PACKAGE_MANAGER, - type: PeripheralDeviceType.PACKAGE_MANAGER, - subType: PERIPHERAL_SUBTYPE_PROCESS, - }) +export async function restartAllExpectationsInStudio(studioId: StudioId): Promise { + const packageManagerDevices = await PeripheralDevices.findFetchAsync({ + studioId: studioId, + category: PeripheralDeviceCategory.PACKAGE_MANAGER, + type: PeripheralDeviceType.PACKAGE_MANAGER, + subType: PERIPHERAL_SUBTYPE_PROCESS, + }) - await Promise.all( - packageManagerDevices.map(async (packageManagerDevice) => { - return executePeripheralDeviceFunction(packageManagerDevice._id, 'restartAllExpectations') - }) - ) - } - export async function restartPackageContainer( - access: PeripheralDeviceContentWriteAccess.ContentAccess, - containerId: string - ): Promise { - await executePeripheralDeviceFunction(access.deviceId, 'restartPackageContainer', containerId) - } + await Promise.all( + packageManagerDevices.map(async (packageManagerDevice) => { + return executePeripheralDeviceFunction(packageManagerDevice._id, 'restartAllExpectations') + }) + ) +} +export async function restartPackageContainer(deviceId: PeripheralDeviceId, containerId: string): Promise { + await executePeripheralDeviceFunction(deviceId, 'restartPackageContainer', containerId) } diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index 0c6bed8857..90604fb61a 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -26,10 +26,9 @@ import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/ import { MOS } from '@sofie-automation/corelib' import { determineDiffTime } from './systemTime/systemTime' import { getTimeDiff } from './systemTime/api' -import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { MethodContextAPI, MethodContext } from './methodContext' -import { triggerWriteAccess, triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify' -import { checkAccessAndGetPeripheralDevice } from './ingest/lib' +import { triggerWriteAccess, triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { checkAccessAndGetPeripheralDevice } from '../security/check' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' import { PackageManagerIntegration } from './integration/expectedPackages' import { profiler } from './profiler' @@ -81,7 +80,9 @@ export namespace ServerPeripheralDeviceAPI { check(deviceId, String) const existingDevice = await PeripheralDevices.findOneAsync(deviceId) if (existingDevice) { - await PeripheralDeviceContentWriteAccess.peripheralDevice({ userId: context.userId, token }, deviceId) + await checkAccessAndGetPeripheralDevice(deviceId, token, context) + } else { + triggerWriteAccessBecauseNoCheckNecessary() } check(token, String) @@ -356,12 +357,12 @@ export namespace ServerPeripheralDeviceAPI { return false } export async function disableSubDevice( - access: PeripheralDeviceContentWriteAccess.ContentAccess, + deviceId: PeripheralDeviceId, subDeviceId: string, disable: boolean ): Promise { - const peripheralDevice = access.device - const deviceId = access.deviceId + const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + if (!peripheralDevice) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) // check that the peripheralDevice has subDevices if (peripheralDevice.type !== PeripheralDeviceType.PLAYOUT) @@ -432,18 +433,21 @@ export namespace ServerPeripheralDeviceAPI { }) } } - export async function getDebugStates(access: PeripheralDeviceContentWriteAccess.ContentAccess): Promise { + export async function getDebugStates(peripheralDeviceId: PeripheralDeviceId): Promise { + const peripheralDevice = await PeripheralDevices.findOneAsync(peripheralDeviceId) + if (!peripheralDevice) return {} + if ( // Debug states are only valid for Playout devices and must be enabled with the `debugState` option - access.device.type !== PeripheralDeviceType.PLAYOUT || - !access.device.settings || - !(access.device.settings as any)['debugState'] + peripheralDevice.type !== PeripheralDeviceType.PLAYOUT || + !peripheralDevice.settings || + !(peripheralDevice.settings as any)['debugState'] ) { return {} } try { - return await executePeripheralDeviceFunction(access.deviceId, 'getDebugStates') + return await executePeripheralDeviceFunction(peripheralDevice._id, 'getDebugStates') } catch (e) { logger.error(e) return {} diff --git a/meteor/server/api/playout/api.ts b/meteor/server/api/playout/api.ts index d290abc6d7..8b18fed8aa 100644 --- a/meteor/server/api/playout/api.ts +++ b/meteor/server/api/playout/api.ts @@ -6,20 +6,29 @@ import { logger } from '../../logging' import { MethodContextAPI } from '../methodContext' import { QueueStudioJob } from '../../worker/worker' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' -import { StudioContentWriteAccess } from '../../security/studio' + import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../../security/auth' +import { Studios } from '../../collections' +import { Meteor } from 'meteor/meteor' + +const PERMISSIONS_FOR_STUDIO_BASELINE: Array = ['configure', 'studio'] class ServerPlayoutAPIClass extends MethodContextAPI implements NewPlayoutAPI { async updateStudioBaseline(studioId: StudioId): Promise { - await StudioContentWriteAccess.baseline(this, studioId) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_STUDIO_BASELINE) const res = await QueueStudioJob(StudioJobs.UpdateStudioBaseline, studioId, undefined) return res.complete } async shouldUpdateStudioBaseline(studioId: StudioId) { - const access = await StudioContentWriteAccess.baseline(this, studioId) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_STUDIO_BASELINE) + + const studio = await Studios.findOneAsync(studioId) + if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`) - return ServerPlayoutAPI.shouldUpdateStudioBaseline(access) + return ServerPlayoutAPI.shouldUpdateStudioBaseline(studio) } } registerClassToMeteorMethods(PlayoutAPIMethods, ServerPlayoutAPIClass, false) diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index 20fb5e40c3..a9d36df0a9 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1,15 +1,14 @@ /* tslint:disable:no-use-before-declare */ import { PackageInfo } from '../../coreSystem' -import { StudioContentAccess } from '../../security/studio' import { shouldUpdateStudioBaselineInner } from '@sofie-automation/corelib/dist/studio/baseline' import { Blueprints, RundownPlaylists, Timeline } from '../../collections' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { QueueStudioJob } from '../../worker/worker' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' export namespace ServerPlayoutAPI { - export async function shouldUpdateStudioBaseline(access: StudioContentAccess): Promise { - const { studio } = access - + export async function shouldUpdateStudioBaseline(studio: DBStudio): Promise { // This is intentionally not in a lock/queue, as doing so will cause it to block playout performance, and being wrong is harmless if (studio) { @@ -34,11 +33,11 @@ export namespace ServerPlayoutAPI { } export async function switchRouteSet( - access: StudioContentAccess, + studioId: StudioId, routeSetId: string, state: boolean | 'toggle' ): Promise { - const queuedJob = await QueueStudioJob(StudioJobs.SwitchRouteSet, access.studioId, { + const queuedJob = await QueueStudioJob(StudioJobs.SwitchRouteSet, studioId, { routeSetId, state, }) diff --git a/meteor/server/api/rest/v0/__tests__/rest.test.ts b/meteor/server/api/rest/v0/__tests__/rest.test.ts index 41d9e876c7..10d3ed7204 100644 --- a/meteor/server/api/rest/v0/__tests__/rest.test.ts +++ b/meteor/server/api/rest/v0/__tests__/rest.test.ts @@ -20,23 +20,6 @@ describe('REST API', () => { const legacyApiRouter = createLegacyApiRouter() - test('registers endpoints for all UserActionAPI methods', async () => { - for (const [methodName, methodValue] of Object.entries(UserActionAPIMethods)) { - const signature = MeteorMethodSignatures[methodValue] - - let resource = `/action/${methodName}` - for (const paramName of signature || []) { - resource += `/${paramName}` - } - - const ctx = await callKoaRoute(legacyApiRouter, { - method: 'POST', - url: resource, - }) - expect(ctx.response.status).not.toBe(404) - } - }) - test('calls the UserActionAPI methods, when doing a POST to the endpoint', async () => { for (const [methodName, methodValue] of Object.entries(UserActionAPIMethods)) { const signature = MeteorMethodSignatures[methodValue] diff --git a/meteor/server/api/rest/v1/buckets.ts b/meteor/server/api/rest/v1/buckets.ts index 5a4b764267..8086ea5159 100644 --- a/meteor/server/api/rest/v1/buckets.ts +++ b/meteor/server/api/rest/v1/buckets.ts @@ -8,13 +8,15 @@ import { ServerClientAPI } from '../../client' import { protectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { getCurrentTime } from '../../../lib/lib' import { check } from 'meteor/check' -import { StudioContentWriteAccess } from '../../../security/studio' import { BucketsAPI } from '../../buckets' -import { BucketSecurity } from '../../../security/buckets' import { APIFactory, APIRegisterHook, ServerAPIContext } from './types' import { logger } from '../../../logging' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { IngestAdlib } from '@sofie-automation/blueprints-integration' +import { assertConnectionHasOneOfPermissions } from '../../../security/auth' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' + +const PERMISSIONS_FOR_BUCKET_MODIFICATION: Array = ['studio'] export class BucketsServerAPI implements BucketsRestAPI { constructor(private context: ServerAPIContext) {} @@ -57,11 +59,9 @@ export class BucketsServerAPI implements BucketsRestAPI { check(bucket.studioId, String) check(bucket.name, String) - const access = await StudioContentWriteAccess.bucket( - this.context.getCredentials(), - protectString(bucket.studioId) - ) - return BucketsAPI.createNewBucket(access, bucket.name) + assertConnectionHasOneOfPermissions(connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + + return BucketsAPI.createNewBucket(protectString(bucket.studioId), bucket.name) } ) if (ClientAPI.isClientResponseSuccess(createdBucketResponse)) { @@ -84,8 +84,9 @@ export class BucketsServerAPI implements BucketsRestAPI { async () => { check(bucketId, String) - const access = await BucketSecurity.allowWriteAccess(this.context.getCredentials(), bucketId) - return BucketsAPI.removeBucket(access) + assertConnectionHasOneOfPermissions(connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + + return BucketsAPI.removeBucket(bucketId) } ) } @@ -104,8 +105,9 @@ export class BucketsServerAPI implements BucketsRestAPI { async () => { check(bucketId, String) - const access = await BucketSecurity.allowWriteAccess(this.context.getCredentials(), bucketId) - return BucketsAPI.emptyBucket(access) + assertConnectionHasOneOfPermissions(connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + + return BucketsAPI.emptyBucket(bucketId) } ) } @@ -122,6 +124,8 @@ export class BucketsServerAPI implements BucketsRestAPI { 'bucketsRemoveBucketAdLib', { externalId }, async () => { + assertConnectionHasOneOfPermissions(connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + const bucketAdLibPiecePromise = BucketAdLibs.findOneAsync( { externalId }, { @@ -139,17 +143,9 @@ export class BucketsServerAPI implements BucketsRestAPI { bucketAdLibActionPromise, ]) if (bucketAdLibPiece) { - const access = await BucketSecurity.allowWriteAccessPiece( - this.context.getCredentials(), - bucketAdLibPiece._id - ) - return BucketsAPI.removeBucketAdLib(access) + return BucketsAPI.removeBucketAdLib(bucketAdLibPiece._id) } else if (bucketAdLibAction) { - const access = await BucketSecurity.allowWriteAccessAction( - this.context.getCredentials(), - bucketAdLibAction._id - ) - return BucketsAPI.removeBucketAdLibAction(access) + return BucketsAPI.removeBucketAdLibAction(bucketAdLibAction._id) } } ) @@ -173,8 +169,9 @@ export class BucketsServerAPI implements BucketsRestAPI { check(showStyleBaseId, String) check(ingestItem, Object) - const access = await BucketSecurity.allowWriteAccess(this.context.getCredentials(), bucketId) - return BucketsAPI.importAdlibToBucket(access, showStyleBaseId, undefined, ingestItem) + assertConnectionHasOneOfPermissions(connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + + return BucketsAPI.importAdlibToBucket(bucketId, showStyleBaseId, undefined, ingestItem) } ) } diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index c935f83fa4..c36553562d 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -9,8 +9,7 @@ import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { MethodContextAPI } from '../../methodContext' import { logger } from '../../../logging' import { CURRENT_SYSTEM_VERSION } from '../../../migration/currentSystemVersion' -import { Credentials } from '../../../security/lib/credentials' -import { triggerWriteAccess } from '../../../security/lib/securityVerify' +import { triggerWriteAccess } from '../../../security/securityVerify' import { makeMeteorConnectionFromKoa } from '../koa' import { registerRoutes as registerBlueprintsRoutes } from './blueprints' import { registerRoutes as registerDevicesRoutes } from './devices' @@ -35,21 +34,12 @@ function restAPIUserEvent( class APIContext implements ServerAPIContext { public getMethodContext(connection: Meteor.Connection): MethodContextAPI { return { - userId: null, connection, - isSimulation: false, - setUserId: () => { - /* no-op */ - }, unblock: () => { /* no-op */ }, } } - - public getCredentials(): Credentials { - return { userId: null } - } } export const koaRouter = new KoaRouter() diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 30513751c4..0e3193ad6e 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -33,7 +33,7 @@ import { QueueNextSegmentResult, StudioJobs } from '@sofie-automation/corelib/di import { getCurrentTime } from '../../../lib/lib' import { TriggerReloadDataResponse } from '@sofie-automation/meteor-lib/dist/api/userActions' import { ServerRundownAPI } from '../../rundown' -import { triggerWriteAccess } from '../../../security/lib/securityVerify' +import { triggerWriteAccess } from '../../../security/securityVerify' class PlaylistsServerAPI implements PlaylistsRestAPI { constructor(private context: ServerAPIContext) {} diff --git a/meteor/server/api/rest/v1/studios.ts b/meteor/server/api/rest/v1/studios.ts index 30e57b14ba..aaabedc693 100644 --- a/meteor/server/api/rest/v1/studios.ts +++ b/meteor/server/api/rest/v1/studios.ts @@ -17,9 +17,12 @@ import { getCurrentTime } from '../../../lib/lib' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { StudioContentWriteAccess } from '../../../security/studio' import { ServerPlayoutAPI } from '../../playout/playout' import { checkValidation } from '.' +import { assertConnectionHasOneOfPermissions } from '../../../security/auth' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' + +const PERMISSIONS_FOR_PLAYOUT_USERACTION: Array = ['studio'] class StudiosServerAPI implements StudiosRestAPI { constructor(private context: ServerAPIContext) {} @@ -215,8 +218,9 @@ class StudiosServerAPI implements StudiosRestAPI { check(routeSetId, String) check(state, Boolean) - const access = await StudioContentWriteAccess.routeSet(this.context.getCredentials(), studioId) - return ServerPlayoutAPI.switchRouteSet(access, routeSetId, state) + assertConnectionHasOneOfPermissions(connection, ...PERMISSIONS_FOR_PLAYOUT_USERACTION) + + return ServerPlayoutAPI.switchRouteSet(studioId, routeSetId, state) } ) } diff --git a/meteor/server/api/rest/v1/types.ts b/meteor/server/api/rest/v1/types.ts index f7bb0e7a43..a5a3a6316d 100644 --- a/meteor/server/api/rest/v1/types.ts +++ b/meteor/server/api/rest/v1/types.ts @@ -1,7 +1,6 @@ import { UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { Meteor } from 'meteor/meteor' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' -import { Credentials } from '../../../security/lib/credentials' import { MethodContextAPI } from '../../methodContext' export type APIRegisterHook = ( @@ -24,5 +23,4 @@ export interface APIFactory { export interface ServerAPIContext { getMethodContext(connection: Meteor.Connection): MethodContextAPI - getCredentials(): Credentials } diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index 51b9ec5a9d..b3dfb75734 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -12,36 +12,36 @@ import { TriggerReloadDataResponse, } from '@sofie-automation/meteor-lib/dist/api/userActions' import { MethodContextAPI, MethodContext } from './methodContext' -import { StudioContentWriteAccess } from '../security/studio' import { runIngestOperation } from './ingest/lib' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' -import { VerifiedRundownContentAccess, VerifiedRundownPlaylistContentAccess } from './lib' +import { VerifiedRundownForUserAction, VerifiedRundownPlaylistForUserAction } from '../security/check' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprints, Rundowns, ShowStyleBases, ShowStyleVariants, Studios } from '../collections' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' export namespace ServerRundownAPI { /** Remove an individual rundown */ - export async function removeRundown(access: VerifiedRundownContentAccess): Promise { - await runIngestOperation(access.rundown.studioId, IngestJobs.UserRemoveRundown, { - rundownId: access.rundown._id, + export async function removeRundown(rundown: VerifiedRundownForUserAction): Promise { + await runIngestOperation(rundown.studioId, IngestJobs.UserRemoveRundown, { + rundownId: rundown._id, force: true, }) } - export async function unsyncRundown(access: VerifiedRundownContentAccess): Promise { - await runIngestOperation(access.rundown.studioId, IngestJobs.UserUnsyncRundown, { - rundownId: access.rundown._id, + export async function unsyncRundown(rundown: VerifiedRundownForUserAction): Promise { + await runIngestOperation(rundown.studioId, IngestJobs.UserUnsyncRundown, { + rundownId: rundown._id, }) } /** Resync all rundowns in a rundownPlaylist */ export async function resyncRundownPlaylist( - access: VerifiedRundownPlaylistContentAccess + playlist: VerifiedRundownPlaylistForUserAction ): Promise { - logger.info('resyncRundownPlaylist ' + access.playlist._id) + logger.info('resyncRundownPlaylist ' + playlist._id) - const rundowns = await Rundowns.findFetchAsync({ playlistId: access.playlist._id }) + const rundowns = await Rundowns.findFetchAsync({ playlistId: playlist._id }) const responses = await Promise.all( rundowns.map(async (rundown) => { return { @@ -56,23 +56,22 @@ export namespace ServerRundownAPI { } } - export async function resyncRundown(access: VerifiedRundownContentAccess): Promise { - return IngestActions.reloadRundown(access.rundown) + export async function resyncRundown(rundown: VerifiedRundownForUserAction): Promise { + return IngestActions.reloadRundown(rundown) } } export namespace ClientRundownAPI { export async function rundownPlaylistNeedsResync( - context: MethodContext, + _context: MethodContext, playlistId: RundownPlaylistId ): Promise { check(playlistId, String) - const access = await StudioContentWriteAccess.rundownPlaylist(context, playlistId) - const playlist = access.playlist + triggerWriteAccessBecauseNoCheckNecessary() const rundowns = await Rundowns.findFetchAsync( { - playlistId: playlist._id, + playlistId: playlistId, }, { sort: { _id: 1 }, diff --git a/meteor/server/api/rundownLayouts.ts b/meteor/server/api/rundownLayouts.ts index db9eb108e0..62f4596762 100644 --- a/meteor/server/api/rundownLayouts.ts +++ b/meteor/server/api/rundownLayouts.ts @@ -10,12 +10,15 @@ import { import { literal, getRandomId, protectString } from '../lib/tempLib' import { logger } from '../logging' import { MethodContext, MethodContextAPI } from './methodContext' -import { ShowStyleContentWriteAccess } from '../security/showStyle' import { fetchShowStyleBaseLight } from '../optimizations' import { BlueprintId, RundownLayoutId, ShowStyleBaseId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownLayouts } from '../collections' import KoaRouter from '@koa/router' import bodyParser from 'koa-bodyparser' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +const PERMISSIONS_FOR_MANAGE_RUNDOWN_LAYOUTS: Array = ['configure'] export async function createRundownLayout( name: string, @@ -57,6 +60,8 @@ shelfLayoutsRouter.post( async (ctx) => { ctx.response.type = 'text/plain' + assertConnectionHasOneOfPermissions(ctx, ...PERMISSIONS_FOR_MANAGE_RUNDOWN_LAYOUTS) + const showStyleBaseId: ShowStyleBaseId = protectString(ctx.params.showStyleBaseId) check(showStyleBaseId, String) @@ -129,15 +134,16 @@ async function apiCreateRundownLayout( check(showStyleBaseId, String) check(regionId, String) - const access = await ShowStyleContentWriteAccess.anyContent(context, showStyleBaseId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_RUNDOWN_LAYOUTS) - return createRundownLayout(name, type, showStyleBaseId, regionId, undefined, access.userId || undefined) + return createRundownLayout(name, type, showStyleBaseId, regionId, undefined, undefined) } async function apiRemoveRundownLayout(context: MethodContext, id: RundownLayoutId) { check(id, String) - const access = await ShowStyleContentWriteAccess.rundownLayout(context, id) - const rundownLayout = access.rundownLayout + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_RUNDOWN_LAYOUTS) + + const rundownLayout = await RundownLayouts.findOneAsync(id) if (!rundownLayout) throw new Meteor.Error(404, `RundownLayout "${id}" not found`) await removeRundownLayout(id) diff --git a/meteor/server/api/showStyles.ts b/meteor/server/api/showStyles.ts index 8275c81836..fdf42e6e5a 100644 --- a/meteor/server/api/showStyles.ts +++ b/meteor/server/api/showStyles.ts @@ -10,9 +10,6 @@ import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowSt import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { protectString, getRandomId, omit } from '../lib/tempLib' import { MethodContextAPI, MethodContext } from './methodContext' -import { OrganizationContentWriteAccess } from '../security/organization' -import { ShowStyleContentWriteAccess } from '../security/showStyle' -import { Credentials } from '../security/lib/credentials' import deepmerge from 'deepmerge' import { applyAndValidateOverrides, @@ -23,6 +20,10 @@ import { IBlueprintConfig } from '@sofie-automation/blueprints-integration' import { OrganizationId, ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { RundownLayouts, ShowStyleBases, ShowStyleVariants, Studios } from '../collections' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +const PERMISSIONS_FOR_MANAGE_SHOWSTYLES: Array = ['configure'] export interface ShowStyleCompound extends Omit { showStyleVariantId: ShowStyleVariantId @@ -74,9 +75,10 @@ export function createShowStyleCompound( } } -export async function insertShowStyleBase(context: MethodContext | Credentials): Promise { - const access = await OrganizationContentWriteAccess.showStyleBase(context) - return insertShowStyleBaseInner(access.organizationId) +export async function insertShowStyleBase(context: MethodContext): Promise { + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) + + return insertShowStyleBaseInner(null) } export async function insertShowStyleBaseInner(organizationId: OrganizationId | null): Promise { @@ -97,20 +99,14 @@ export async function insertShowStyleBaseInner(organizationId: OrganizationId | await insertShowStyleVariantInner(showStyleBase._id, 'Default') return showStyleBase._id } -async function assertShowStyleBaseAccess(context: MethodContext | Credentials, showStyleBaseId: ShowStyleBaseId) { - check(showStyleBaseId, String) - - const access = await ShowStyleContentWriteAccess.anyContent(context, showStyleBaseId) - const showStyleBase = access.showStyleBase - if (!showStyleBase) throw new Meteor.Error(404, `showStyleBase "${showStyleBaseId}" not found`) -} export async function insertShowStyleVariant( - context: MethodContext | Credentials, + context: MethodContext, showStyleBaseId: ShowStyleBaseId, name?: string ): Promise { - await assertShowStyleBaseAccess(context, showStyleBaseId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) + return insertShowStyleVariantInner(showStyleBaseId, name) } @@ -150,19 +146,19 @@ async function insertShowStyleVariantInner( } export async function importShowStyleVariant( - context: MethodContext | Credentials, + context: MethodContext, showStyleVariant: DBShowStyleVariant ): Promise { - await assertShowStyleBaseAccess(context, showStyleVariant.showStyleBaseId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) return ShowStyleVariants.insertAsync(showStyleVariant) } export async function importShowStyleVariantAsNew( - context: MethodContext | Credentials, + context: MethodContext, showStyleVariant: Omit ): Promise { - await assertShowStyleBaseAccess(context, showStyleVariant.showStyleBaseId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) const newShowStyleVariant: DBShowStyleVariant = { ...showStyleVariant, @@ -173,7 +169,7 @@ export async function importShowStyleVariantAsNew( } export async function removeShowStyleBase(context: MethodContext, showStyleBaseId: ShowStyleBaseId): Promise { - await assertShowStyleBaseAccess(context, showStyleBaseId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) await Promise.allSettled([ ShowStyleBases.removeAsync(showStyleBaseId), @@ -192,8 +188,9 @@ export async function removeShowStyleVariant( ): Promise { check(showStyleVariantId, String) - const access = await ShowStyleContentWriteAccess.showStyleVariant(context, showStyleVariantId) - const showStyleVariant = access.showStyleVariant + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) + + const showStyleVariant = await ShowStyleVariants.findOneAsync(showStyleVariantId) if (!showStyleVariant) throw new Meteor.Error(404, `showStyleVariant "${showStyleVariantId}" not found`) await ShowStyleVariants.removeAsync(showStyleVariant._id) @@ -207,8 +204,9 @@ export async function reorderShowStyleVariant( check(showStyleVariantId, String) check(rank, Number) - const access = await ShowStyleContentWriteAccess.showStyleVariant(context, showStyleVariantId) - const showStyleVariant = access.showStyleVariant + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_SHOWSTYLES) + + const showStyleVariant = await ShowStyleVariants.findOneAsync(showStyleVariantId) if (!showStyleVariant) throw new Meteor.Error(404, `showStyleVariant "${showStyleVariantId}" not found`) await ShowStyleVariants.updateAsync(showStyleVariantId, { @@ -218,7 +216,9 @@ export async function reorderShowStyleVariant( }) } -async function getCreateAdlibTestingRundownOptions(): Promise { +async function getCreateAdlibTestingRundownOptions(context: MethodContext): Promise { + assertConnectionHasOneOfPermissions(context.connection, 'studio') + const [studios, showStyleBases, showStyleVariants] = await Promise.all([ Studios.findFetchAsync( {}, @@ -306,7 +306,7 @@ class ServerShowStylesAPI extends MethodContextAPI implements NewShowStylesAPI { } async getCreateAdlibTestingRundownOptions() { - return getCreateAdlibTestingRundownOptions() + return getCreateAdlibTestingRundownOptions(this) } } registerClassToMeteorMethods(ShowStylesAPIMethods, ServerShowStylesAPI, false) diff --git a/meteor/server/api/singleUseTokens.ts b/meteor/server/api/singleUseTokens.ts index d775a5e760..88fc57a45f 100644 --- a/meteor/server/api/singleUseTokens.ts +++ b/meteor/server/api/singleUseTokens.ts @@ -3,7 +3,7 @@ import { Time } from '@sofie-automation/blueprints-integration' import { getHash } from '@sofie-automation/corelib/dist/hash' import { getCurrentTime } from '../lib/lib' import { SINGLE_USE_TOKEN_SALT } from '@sofie-automation/meteor-lib/dist/api/userActions' -import { isInTestWrite } from '../security/lib/securityVerify' +import { isInTestWrite } from '../security/securityVerify' // The following code is taken from an NPM pacakage called "@sunknudsen/totp", but copied here, instead // of used as a dependency so that it's not vulnerable to a supply chain attack diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index f3809e77c7..f17871e12d 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -39,12 +39,7 @@ import { importIngestRundown } from './ingest/http' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' -import { Settings } from '../Settings' import { MethodContext, MethodContextAPI } from './methodContext' -import { Credentials, isResolvedCredentials } from '../security/lib/credentials' -import { OrganizationContentWriteAccess } from '../security/organization' -import { StudioContentWriteAccess } from '../security/studio' -import { SystemWriteAccess } from '../security/system' import { saveIntoDb, sumChanges } from '../lib/database' import * as fs from 'fs' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' @@ -56,8 +51,7 @@ import { PackageInfoDB, getPackageInfoId } from '@sofie-automation/corelib/dist/ import { CoreRundownPlaylistSnapshot } from '@sofie-automation/corelib/dist/snapshots' import { QueueStudioJob } from '../worker/worker' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' -import { ReadonlyDeep } from 'type-fest' -import { checkAccessToPlaylist, VerifiedRundownPlaylistContentAccess } from './lib' +import { checkAccessToPlaylist, VerifiedRundownPlaylistForUserAction } from '../security/check' import { getSystemStorePath, PackageInfo } from '../coreSystem' import { JSONBlobParse, JSONBlobStringify } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' import { @@ -97,6 +91,10 @@ import { NrcsIngestDataCacheObjSegment, NrcsIngestDataCacheObjPart, } from '@sofie-automation/corelib/dist/dataModel/NrcsIngestDataCache' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions, RequestCredentials } from '../security/auth' + +const PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT: Array = ['configure'] interface RundownPlaylistSnapshot extends CoreRundownPlaylistSnapshot { versionExtended: string | undefined @@ -159,9 +157,6 @@ async function createSystemSnapshot( const coreSystem = await getCoreSystemAsync() if (!coreSystem) throw new Meteor.Error(500, `coreSystem not set up`) - if (Settings.enableUserAccounts && !organizationId) - throw new Meteor.Error(500, 'Not able to create a systemSnaphost without organizationId') - let queryStudio: MongoQuery = {} let queryShowStyleBases: MongoQuery = {} let queryShowStyleVariants: MongoQuery = {} @@ -324,7 +319,7 @@ function getPiecesMediaObjects(pieces: PieceGeneric[]): string[] { } async function createRundownPlaylistSnapshot( - playlist: ReadonlyDeep, + playlist: VerifiedRundownPlaylistForUserAction, full = false ): Promise { /** Max count of one type of items to include in the snapshot */ @@ -456,24 +451,12 @@ async function storeSnaphot( return id } -async function retreiveSnapshot(snapshotId: SnapshotId, cred0: Credentials): Promise { +async function retreiveSnapshot(snapshotId: SnapshotId, cred: RequestCredentials | null): Promise { + assertConnectionHasOneOfPermissions(cred, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT) + const snapshot = await Snapshots.findOneAsync(snapshotId) if (!snapshot) throw new Meteor.Error(404, `Snapshot not found!`) - if (Settings.enableUserAccounts) { - if (snapshot.type === SnapshotType.RUNDOWNPLAYLIST) { - if (!snapshot.studioId) - throw new Meteor.Error(500, `Snapshot is of type "${snapshot.type}" but hase no studioId`) - await StudioContentWriteAccess.dataFromSnapshot(cred0, snapshot.studioId) - } else if (snapshot.type === SnapshotType.SYSTEM) { - if (!snapshot.organizationId) - throw new Meteor.Error(500, `Snapshot is of type "${snapshot.type}" but has no organizationId`) - await OrganizationContentWriteAccess.dataFromSnapshot(cred0, snapshot.organizationId) - } else { - await SystemWriteAccess.coreSystem(cred0) - } - } - const storePath = getSystemStorePath() const filePath = Path.join(storePath, snapshot.fileName) @@ -703,11 +686,9 @@ export async function storeSystemSnapshot( throw new Meteor.Error(401, `Restart token is invalid or has expired`) } - const { organizationId, cred } = await OrganizationContentWriteAccess.snapshot(context) - if (Settings.enableUserAccounts && isResolvedCredentials(cred)) { - if (cred.user && !cred.user.superAdmin) throw new Meteor.Error(401, 'Only Super Admins can store Snapshots') - } - return internalStoreSystemSnapshot(organizationId, studioId, reason) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT) + + return internalStoreSystemSnapshot(null, studioId, reason) } /** Take and store a system snapshot. For internal use only, performs no access control. */ export async function internalStoreSystemSnapshot( @@ -721,7 +702,7 @@ export async function internalStoreSystemSnapshot( return storeSnaphot(s, organizationId, reason) } export async function storeRundownPlaylistSnapshot( - access: VerifiedRundownPlaylistContentAccess, + playlist: VerifiedRundownPlaylistForUserAction, hashedToken: string, reason: string, full?: boolean @@ -731,8 +712,8 @@ export async function storeRundownPlaylistSnapshot( throw new Meteor.Error(401, `Restart token is invalid or has expired`) } - const s = await createRundownPlaylistSnapshot(access.playlist, full) - return storeSnaphot(s, access.organizationId, reason) + const s = await createRundownPlaylistSnapshot(playlist, full) + return storeSnaphot(s, playlist.organizationId ?? null, reason) } export async function internalStoreRundownPlaylistSnapshot( playlist: DBRundownPlaylist, @@ -754,12 +735,10 @@ export async function storeDebugSnapshot( throw new Meteor.Error(401, `Restart token is invalid or has expired`) } - const { organizationId, cred } = await OrganizationContentWriteAccess.snapshot(context) - if (Settings.enableUserAccounts && isResolvedCredentials(cred)) { - if (cred.user && !cred.user.superAdmin) throw new Meteor.Error(401, 'Only Super Admins can store Snapshots') - } - const s = await createDebugSnapshot(studioId, organizationId) - return storeSnaphot(s, organizationId, reason) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT) + + const s = await createDebugSnapshot(studioId, null) + return storeSnaphot(s, null, reason) } export async function restoreSnapshot( context: MethodContext, @@ -767,21 +746,18 @@ export async function restoreSnapshot( restoreDebugData: boolean ): Promise { check(snapshotId, String) - const { cred } = await OrganizationContentWriteAccess.snapshot(context) - if (Settings.enableUserAccounts && isResolvedCredentials(cred)) { - if (cred.user && !cred.user.superAdmin) throw new Meteor.Error(401, 'Only Super Admins can store Snapshots') - } - const snapshot = await retreiveSnapshot(snapshotId, context) + + const snapshot = await retreiveSnapshot(snapshotId, context.connection) return restoreFromSnapshot(snapshot, restoreDebugData) } export async function removeSnapshot(context: MethodContext, snapshotId: SnapshotId): Promise { check(snapshotId, String) - const { snapshot, cred } = await OrganizationContentWriteAccess.snapshot(context, snapshotId) - if (Settings.enableUserAccounts && isResolvedCredentials(cred)) { - if (cred.user && !cred.user.superAdmin) throw new Meteor.Error(401, 'Only Super Admins can store Snapshots') - } + + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT) + logger.info(`Removing snapshot ${snapshotId}`) + const snapshot = await Snapshots.findOneAsync(snapshotId) if (!snapshot) throw new Meteor.Error(404, `Snapshot "${snapshotId}" not found!`) if (snapshot.fileName) { @@ -829,66 +805,56 @@ async function handleKoaResponse( } } -// For backwards compatibility: -if (!Settings.enableUserAccounts) { - snapshotPrivateApiRouter.post( - '/restore', - bodyParser({ - jsonLimit: '200mb', // Arbitrary limit - }), - async (ctx) => { - const content = 'ok' - try { - ctx.response.type = 'text/plain' - - if (ctx.request.type !== 'application/json') - throw new Meteor.Error(400, 'Restore Snapshot: Invalid content-type') - - const snapshot = ctx.request.body as any - if (!snapshot) throw new Meteor.Error(400, 'Restore Snapshot: Missing request body') - - const restoreDebugData = ctx.headers['restore-debug-data'] === '1' - const ingestSnapshotData = ctx.headers['ingest-snapshot-data'] === '1' - - if (typeof snapshot !== 'object' || snapshot === null) - throw new Meteor.Error(500, `Restore input data is not an object`) - - if (ingestSnapshotData) { - await ingestFromSnapshot(snapshot) - } else { - await restoreFromSnapshot(snapshot, restoreDebugData) - } +snapshotPrivateApiRouter.post( + '/restore', + bodyParser({ + jsonLimit: '200mb', // Arbitrary limit + }), + async (ctx) => { + assertConnectionHasOneOfPermissions(ctx, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT) - ctx.response.status = 200 - ctx.response.body = content - } catch (e) { - ctx.response.type = 'text/plain' - ctx.response.status = e instanceof Meteor.Error && typeof e.error === 'number' ? e.error : 500 - ctx.response.body = 'Error: ' + stringifyError(e) + const content = 'ok' + try { + ctx.response.type = 'text/plain' - if (ctx.response.status !== 404) { - logger.error(stringifyError(e)) - } + if (ctx.request.type !== 'application/json') + throw new Meteor.Error(400, 'Restore Snapshot: Invalid content-type') + + const snapshot = ctx.request.body as any + if (!snapshot) throw new Meteor.Error(400, 'Restore Snapshot: Missing request body') + + const restoreDebugData = ctx.headers['restore-debug-data'] === '1' + const ingestSnapshotData = ctx.headers['ingest-snapshot-data'] === '1' + + if (typeof snapshot !== 'object' || snapshot === null) + throw new Meteor.Error(500, `Restore input data is not an object`) + + if (ingestSnapshotData) { + await ingestFromSnapshot(snapshot) + } else { + await restoreFromSnapshot(snapshot, restoreDebugData) } - } - ) - // Retrieve snapshot: - snapshotPrivateApiRouter.get('/retrieve/:snapshotId', async (ctx) => { - return handleKoaResponse(ctx, async () => { - const snapshotId = ctx.params.snapshotId - check(snapshotId, String) - return retreiveSnapshot(protectString(snapshotId), { userId: null }) - }) - }) -} + ctx.response.status = 200 + ctx.response.body = content + } catch (e) { + ctx.response.type = 'text/plain' + ctx.response.status = e instanceof Meteor.Error && typeof e.error === 'number' ? e.error : 500 + ctx.response.body = 'Error: ' + stringifyError(e) + + if (ctx.response.status !== 404) { + logger.error(stringifyError(e)) + } + } + } +) // Retrieve snapshot: -snapshotPrivateApiRouter.get('/:token/retrieve/:snapshotId', async (ctx) => { +snapshotPrivateApiRouter.get('/retrieve/:snapshotId', async (ctx) => { return handleKoaResponse(ctx, async () => { const snapshotId = ctx.params.snapshotId check(snapshotId, String) - return retreiveSnapshot(protectString(snapshotId), { userId: null, token: ctx.params.token }) + return retreiveSnapshot(protectString(snapshotId), ctx) }) }) @@ -898,8 +864,8 @@ class ServerSnapshotAPI extends MethodContextAPI implements NewSnapshotAPI { } async storeRundownPlaylist(hashedToken: string, playlistId: RundownPlaylistId, reason: string) { check(playlistId, String) - const access = await checkAccessToPlaylist(this, playlistId) - return storeRundownPlaylistSnapshot(access, hashedToken, reason) + const playlist = await checkAccessToPlaylist(this.connection, playlistId) + return storeRundownPlaylistSnapshot(playlist, hashedToken, reason) } async storeDebugSnapshot(hashedToken: string, studioId: StudioId, reason: string) { return storeDebugSnapshot(this, hashedToken, studioId, reason) diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 8ba11ff832..f0d121b154 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -20,18 +20,21 @@ import { Timeline, } from '../../collections' import { MethodContextAPI, MethodContext } from '../methodContext' -import { OrganizationContentWriteAccess } from '../../security/organization' -import { Credentials } from '../../security/lib/credentials' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { OrganizationId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { logger } from '../../logging' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../../security/auth' -async function insertStudio(context: MethodContext | Credentials, newId?: StudioId): Promise { +const PERMISSIONS_FOR_MANAGE_STUDIOS: Array = ['configure'] + +async function insertStudio(context: MethodContext, newId?: StudioId): Promise { if (newId) check(newId, String) - const access = await OrganizationContentWriteAccess.studio(context) - return insertStudioInner(access.organizationId, newId) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_STUDIOS) + + return insertStudioInner(null, newId) } export async function insertStudioInner(organizationId: OrganizationId | null, newId?: StudioId): Promise { return Studios.insertAsync( @@ -71,8 +74,9 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n async function removeStudio(context: MethodContext, studioId: StudioId): Promise { check(studioId, String) - const access = await OrganizationContentWriteAccess.studio(context, studioId) - const studio = access.studio + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_MANAGE_STUDIOS) + + const studio = await Studios.findOneAsync(studioId) if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`) // allowed to remove? diff --git a/meteor/server/api/system.ts b/meteor/server/api/system.ts index 1668f76720..3c8bd9c4b8 100644 --- a/meteor/server/api/system.ts +++ b/meteor/server/api/system.ts @@ -13,12 +13,10 @@ import { import { CollectionIndexes, getTargetRegisteredIndexes } from '../collections/indices' import { Meteor } from 'meteor/meteor' import { logger } from '../logging' -import { SystemWriteAccess } from '../security/system' import { check } from '../lib/check' import { IndexSpecifier } from '@sofie-automation/meteor-lib/dist/collections/lib' import { getBundle as getTranslationBundleInner } from './translationsBundles' import { TranslationsBundle } from '@sofie-automation/meteor-lib/dist/collections/TranslationsBundles' -import { OrganizationContentWriteAccess } from '../security/organization' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { cleanupOldDataInner } from './cleanup' import { IndexSpecification } from 'mongodb' @@ -26,8 +24,12 @@ import { nightlyCronjobInner } from '../cronjobs' import { TranslationsBundleId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { createAsyncOnlyMongoCollection, AsyncOnlyMongoCollection } from '../collections/collection' import { generateToken } from './singleUseTokens' -import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { assertConnectionHasOneOfPermissions } from '../security/auth' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' + +const PERMISSIONS_FOR_SYSTEM_CLEANUP: Array = ['configure'] async function setupIndexes(removeOldIndexes = false): Promise> { // Note: This function should NOT run on Meteor.startup, due to getCollectionIndexes failing if run before indexes have been created. @@ -95,7 +97,7 @@ async function cleanupIndexes( actuallyRemoveOldIndexes: boolean ): Promise> { check(actuallyRemoveOldIndexes, Boolean) - await SystemWriteAccess.coreSystem(context) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SYSTEM_CLEANUP) return setupIndexes(actuallyRemoveOldIndexes) } @@ -104,12 +106,13 @@ async function cleanupOldData( actuallyRemoveOldData: boolean ): Promise { check(actuallyRemoveOldData, Boolean) - await SystemWriteAccess.coreSystem(context) + + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SYSTEM_CLEANUP) return cleanupOldDataInner(actuallyRemoveOldData) } async function runCronjob(context: MethodContext): Promise { - await SystemWriteAccess.coreSystem(context) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SYSTEM_CLEANUP) return nightlyCronjobInner() } @@ -293,7 +296,7 @@ async function doSystemBenchmarkInner() { return result } async function doSystemBenchmark(context: MethodContext, runCount = 1): Promise { - await SystemWriteAccess.coreSystem(context) + assertConnectionHasOneOfPermissions(context.connection, 'developer') if (runCount < 1) throw new Error(`runCount must be >= 1`) @@ -361,10 +364,11 @@ CPU JSON stringifying: ${avg.cpuStringifying} ms (${comparison.cpuStringif } } -async function getTranslationBundle(context: MethodContext, bundleId: TranslationsBundleId) { +async function getTranslationBundle(_context: MethodContext, bundleId: TranslationsBundleId) { check(bundleId, String) - await OrganizationContentWriteAccess.translationBundle(context) + triggerWriteAccessBecauseNoCheckNecessary() + return ClientAPI.responseSuccess(await getTranslationBundleInner(bundleId)) } diff --git a/meteor/server/api/triggeredActions.ts b/meteor/server/api/triggeredActions.ts index f172786207..0f29b780d0 100644 --- a/meteor/server/api/triggeredActions.ts +++ b/meteor/server/api/triggeredActions.ts @@ -4,14 +4,12 @@ import { registerClassToMeteorMethods, ReplaceOptionalWithNullInMethodArguments import { literal, getRandomId, protectString, Complete } from '../lib/tempLib' import { logger } from '../logging' import { MethodContext, MethodContextAPI } from './methodContext' -import { ShowStyleContentWriteAccess } from '../security/showStyle' import { DBTriggeredActions, TriggeredActionsObj } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' import { CreateTriggeredActionsContent, NewTriggeredActionsAPI, TriggeredActionsAPIMethods, } from '@sofie-automation/meteor-lib/dist/api/triggeredActions' -import { SystemWriteAccess } from '../security/system' import { fetchShowStyleBaseLight } from '../optimizations' import { convertObjectIntoOverrides, @@ -21,6 +19,10 @@ import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/di import { TriggeredActions } from '../collections' import KoaRouter from '@koa/router' import bodyParser from 'koa-bodyparser' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +const PERMISSIONS_FOR_TRIGGERED_ACTIONS: Array = ['configure'] export async function createTriggeredActions( showStyleBaseId: ShowStyleBaseId | null, @@ -58,6 +60,8 @@ actionTriggersRouter.post( async (ctx) => { ctx.response.type = 'text/plain' + assertConnectionHasOneOfPermissions(ctx, ...PERMISSIONS_FOR_TRIGGERED_ACTIONS) + const showStyleBaseId: ShowStyleBaseId | undefined = protectString(ctx.params.showStyleBaseId) check(showStyleBaseId, Match.Optional(String)) @@ -161,22 +165,14 @@ async function apiCreateTriggeredActions( check(showStyleBaseId, Match.Maybe(String)) check(base, Match.Maybe(Object)) - if (!showStyleBaseId) { - const access = await SystemWriteAccess.coreSystem(context) - if (!access) throw new Meteor.Error(403, `Core System settings not writable`) - } else { - const access = await ShowStyleContentWriteAccess.anyContent(context, showStyleBaseId) - if (!access) throw new Meteor.Error(404, `ShowStyleBase "${showStyleBaseId}" not found`) - } + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_TRIGGERED_ACTIONS) return createTriggeredActions(showStyleBaseId, base || undefined) } async function apiRemoveTriggeredActions(context: MethodContext, id: TriggeredActionId) { check(id, String) - const access = await ShowStyleContentWriteAccess.triggeredActions(context, id) - const triggeredActions = typeof access === 'boolean' ? access : access.triggeredActions - if (!triggeredActions) throw new Meteor.Error(404, `Action Trigger "${id}" not found`) + assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_TRIGGERED_ACTIONS) await removeTriggeredActions(id) } diff --git a/meteor/server/api/user.ts b/meteor/server/api/user.ts index 9b3649abdd..e626071dd3 100644 --- a/meteor/server/api/user.ts +++ b/meteor/server/api/user.ts @@ -1,132 +1,14 @@ -import { Meteor } from 'meteor/meteor' -import { Accounts } from 'meteor/accounts-base' -import { unprotectString, protectString } from '../lib/tempLib' -import { sleep, deferAsync } from '../lib/lib' -import { MethodContextAPI, MethodContext } from './methodContext' -import { NewUserAPI, UserAPIMethods, createUser, CreateNewUserData } from '@sofie-automation/meteor-lib/dist/api/user' +import { MethodContextAPI } from './methodContext' +import { NewUserAPI, UserAPIMethods } from '@sofie-automation/meteor-lib/dist/api/user' import { registerClassToMeteorMethods } from '../methods' -import { SystemWriteAccess } from '../security/system' -import { triggerWriteAccess, triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify' -import { logNotAllowed } from '../../server/security/lib/lib' -import { User } from '@sofie-automation/meteor-lib/dist/collections/Users' -import { createOrganization } from './organizations' -import { DBOrganizationBase } from '@sofie-automation/meteor-lib/dist/collections/Organization' -import { resetCredentials } from '../security/lib/credentials' -import { OrganizationId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Organizations, Users } from '../collections' -import { logger } from '../logging' - -async function enrollUser(email: string, name: string): Promise { - triggerWriteAccessBecauseNoCheckNecessary() - - const id = await createUser({ - email: email, - profile: { name: name }, - }) - try { - await Accounts.sendEnrollmentEmail(unprotectString(id), email) - } catch (error) { - logger.error('Accounts.sendEnrollmentEmail') - logger.error(error) - } - - return id -} - -async function afterCreateNewUser(userId: UserId, organization: DBOrganizationBase): Promise { - triggerWriteAccessBecauseNoCheckNecessary() - - await sendVerificationEmail(userId) - - // Create an organization for the user: - const orgId = await createOrganization(organization) - // Add user to organization: - await Users.updateAsync(userId, { $set: { organizationId: orgId } }) - await Organizations.updateAsync(orgId, { - $set: { - userRoles: { - [unprotectString(userId)]: { - admin: true, - studio: true, - configurator: true, - }, - }, - }, - }) - - resetCredentials({ userId }) - - return orgId -} -async function sendVerificationEmail(userId: UserId) { - const user = await Users.findOneAsync(userId) - if (!user) throw new Meteor.Error(404, `User "${userId}" not found!`) - try { - await Promise.all( - user.emails.map(async (email) => { - if (!email.verified) { - await Accounts.sendVerificationEmail(unprotectString(user._id), email.address) - } - }) - ) - } catch (error) { - logger.error('ERROR sending email verification') - logger.error(error) - } -} - -async function requestResetPassword(email: string): Promise { - triggerWriteAccessBecauseNoCheckNecessary() - const meteorUser = Accounts.findUserByEmail(email) as unknown - const user = meteorUser as User - if (!user) return false - await Accounts.sendResetPasswordEmail(unprotectString(user._id)) - return true -} - -async function removeUser(context: MethodContext) { - triggerWriteAccess() - if (!context.userId) throw new Meteor.Error(403, `Not logged in`) - const access = await SystemWriteAccess.currentUser(context.userId, context) - if (!access) return logNotAllowed('Current user', 'Invalid user id or permissions') - await Users.removeAsync(context.userId) - return true -} +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { parseUserPermissions, USER_PERMISSIONS_HEADER } from '@sofie-automation/meteor-lib/dist/userPermissions' class ServerUserAPI extends MethodContextAPI implements NewUserAPI { - async enrollUser(email: string, name: string) { - return enrollUser(email, name) - } - async requestPasswordReset(email: string) { - return requestResetPassword(email) - } - async removeUser() { - return removeUser(this) + async getUserPermissions() { + triggerWriteAccessBecauseNoCheckNecessary() + return parseUserPermissions(this.connection?.httpHeaders?.[USER_PERMISSIONS_HEADER]) } } registerClassToMeteorMethods(UserAPIMethods, ServerUserAPI, false) - -Accounts.onCreateUser((options0, user) => { - const options = options0 as Partial - user.profile = options.profile - - const createOrganization = options.createOrganization - if (createOrganization) { - deferAsync(async () => { - // To be run after the user has been inserted: - for (let t = 10; t < 200; t *= 1.5) { - const dbUser = await Users.findOneAsync(protectString(user._id)) - if (dbUser) { - await afterCreateNewUser(dbUser._id, createOrganization) - return - } else { - // User has not been inserted into db (yet), wait - await sleep(t) - } - } - }) - } - // The user to-be-inserted: - return user -}) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 82a399f5cd..0816952ca1 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -10,25 +10,19 @@ import { storeRundownPlaylistSnapshot } from './snapshot' import { registerClassToMeteorMethods, ReplaceOptionalWithNullInMethodArguments } from '../methods' import { ServerRundownAPI } from './rundown' import { saveEvaluation } from './evaluations' -import { MediaManagerAPI } from './mediaManager' +import * as MediaManagerAPI from './mediaManager' import { MOSDeviceActions } from './ingest/mosDevice/actions' import { MethodContextAPI } from './methodContext' import { ServerClientAPI } from './client' -import { OrganizationContentWriteAccess } from '../security/organization' -import { SystemWriteAccess } from '../security/system' -import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/lib/securityVerify' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' import { BucketsAPI } from './buckets' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' -import { VerifiedRundownPlaylistContentAccess } from './lib' -import { PackageManagerAPI } from './packageManager' +import * as PackageManagerAPI from './packageManager' import { ServerPeripheralDeviceAPI } from './peripheralDevice' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' -import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' -import { StudioContentWriteAccess } from '../security/studio' -import { BucketSecurity } from '../security/buckets' import { AdLibActionId, BucketId, @@ -53,9 +47,16 @@ import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/Rundow import { runIngestOperation } from './ingest/lib' import { RundownPlaylistContentWriteAccess } from '../security/rundownPlaylist' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +const PERMISSIONS_FOR_PLAYOUT_USERACTION: Array = ['studio'] +const PERMISSIONS_FOR_BUCKET_MODIFICATION: Array = ['studio'] +const PERMISSIONS_FOR_MEDIA_MANAGEMENT: Array = ['studio', 'service', 'configure'] +const PERMISSIONS_FOR_SYSTEM_ACTION: Array = ['service', 'configure'] async function pieceSetInOutPoints( - access: VerifiedRundownPlaylistContentAccess, + playlistId: RundownPlaylistId, partId: PartId, pieceId: PieceId, inPoint: number, @@ -66,7 +67,7 @@ async function pieceSetInOutPoints( const rundown = await Rundowns.findOneAsync({ _id: part.rundownId, - playlistId: access.playlist._id, + playlistId: playlistId, }) if (!rundown) throw new Meteor.Error(501, `Rundown "${part.rundownId}" not found!`) @@ -387,8 +388,8 @@ class ServerUserActionAPI }, 'pieceSetInOutPoints', { rundownPlaylistId, partId, pieceId, inPoint, duration }, - async (access) => { - return pieceSetInOutPoints(access, partId, pieceId, inPoint, duration) + async (playlist) => { + return pieceSetInOutPoints(playlist._id, partId, pieceId, inPoint, duration) } ) } @@ -546,8 +547,8 @@ class ServerUserActionAPI check(showStyleBaseId, String) check(ingestItem, Object) - const access = await BucketSecurity.allowWriteAccess(this, bucketId) - return BucketsAPI.importAdlibToBucket(access, showStyleBaseId, undefined, ingestItem) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.importAdlibToBucket(bucketId, showStyleBaseId, undefined, ingestItem) } ) } @@ -622,8 +623,8 @@ class ServerUserActionAPI }, 'saveEvaluation', { evaluation }, - async (access) => { - return saveEvaluation(access, evaluation) + async (playlist) => { + return saveEvaluation(playlist, evaluation) } ) } @@ -646,8 +647,8 @@ class ServerUserActionAPI }, 'storeRundownSnapshot', { playlistId, reason, full }, - async (access) => { - return storeRundownPlaylistSnapshot(access, hashedToken, reason, full) + async (playlist) => { + return storeRundownPlaylistSnapshot(playlist, hashedToken, reason, full) } ) } @@ -695,8 +696,8 @@ class ServerUserActionAPI }, 'resyncRundownPlaylist', { playlistId }, - async (access) => { - return ServerRundownAPI.resyncRundownPlaylist(access) + async (playlist) => { + return ServerRundownAPI.resyncRundownPlaylist(playlist) } ) } @@ -711,8 +712,8 @@ class ServerUserActionAPI }, 'unsyncRundown', { rundownId }, - async (access) => { - return ServerRundownAPI.unsyncRundown(access) + async (rundown) => { + return ServerRundownAPI.unsyncRundown(rundown) } ) } @@ -727,8 +728,8 @@ class ServerUserActionAPI }, 'removeRundown', { rundownId }, - async (access) => { - return ServerRundownAPI.removeRundown(access) + async (rundown) => { + return ServerRundownAPI.removeRundown(rundown) } ) } @@ -743,53 +744,71 @@ class ServerUserActionAPI }, 'resyncRundown', { rundownId }, - async (access) => { - return ServerRundownAPI.resyncRundown(access) + async (rundown) => { + return ServerRundownAPI.resyncRundown(rundown) } ) } - async mediaRestartWorkflow(userEvent: string, eventTime: Time, workflowId: MediaWorkFlowId) { + async mediaRestartWorkflow( + userEvent: string, + eventTime: Time, + deviceId: PeripheralDeviceId, + workflowId: MediaWorkFlowId + ) { return ServerClientAPI.runUserActionInLog( this, userEvent, eventTime, 'mediaRestartWorkflow', - { workflowId }, + { deviceId, workflowId }, async () => { check(workflowId, String) - const access = await PeripheralDeviceContentWriteAccess.mediaWorkFlow(this, workflowId) - return MediaManagerAPI.restartWorkflow(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return MediaManagerAPI.restartWorkflow(deviceId, workflowId) } ) } - async mediaAbortWorkflow(userEvent: string, eventTime: Time, workflowId: MediaWorkFlowId) { + async mediaAbortWorkflow( + userEvent: string, + eventTime: Time, + deviceId: PeripheralDeviceId, + workflowId: MediaWorkFlowId + ) { return ServerClientAPI.runUserActionInLog( this, userEvent, eventTime, 'mediaAbortWorkflow', - { workflowId }, + { deviceId, workflowId }, async () => { check(workflowId, String) - const access = await PeripheralDeviceContentWriteAccess.mediaWorkFlow(this, workflowId) - return MediaManagerAPI.abortWorkflow(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return MediaManagerAPI.abortWorkflow(deviceId, workflowId) } ) } - async mediaPrioritizeWorkflow(userEvent: string, eventTime: Time, workflowId: MediaWorkFlowId) { + async mediaPrioritizeWorkflow( + userEvent: string, + eventTime: Time, + deviceId: PeripheralDeviceId, + workflowId: MediaWorkFlowId + ) { return ServerClientAPI.runUserActionInLog( this, userEvent, eventTime, 'mediaPrioritizeWorkflow', - { workflowId }, + { deviceId, workflowId }, async () => { check(workflowId, String) - const access = await PeripheralDeviceContentWriteAccess.mediaWorkFlow(this, workflowId) - return MediaManagerAPI.prioritizeWorkflow(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return MediaManagerAPI.prioritizeWorkflow(deviceId, workflowId) } ) } @@ -801,8 +820,9 @@ class ServerUserActionAPI 'mediaRestartAllWorkflows', {}, async () => { - const access = await OrganizationContentWriteAccess.mediaWorkFlows(this) - return MediaManagerAPI.restartAllWorkflows(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return MediaManagerAPI.restartAllWorkflows(null) } ) } @@ -814,8 +834,9 @@ class ServerUserActionAPI 'mediaAbortAllWorkflows', {}, async () => { - const access = await OrganizationContentWriteAccess.mediaWorkFlows(this) - return MediaManagerAPI.abortAllWorkflows(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return MediaManagerAPI.abortAllWorkflows(null) } ) } @@ -835,8 +856,9 @@ class ServerUserActionAPI check(deviceId, String) check(workId, String) - const access = await PeripheralDeviceContentWriteAccess.executeFunction(this, deviceId) - return PackageManagerAPI.restartExpectation(access, workId) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return PackageManagerAPI.restartExpectation(deviceId, workId) } ) } @@ -850,8 +872,9 @@ class ServerUserActionAPI async () => { check(studioId, String) - const access = await StudioContentWriteAccess.executeFunction(this, studioId) - return PackageManagerAPI.restartAllExpectationsInStudio(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return PackageManagerAPI.restartAllExpectationsInStudio(studioId) } ) } @@ -871,8 +894,9 @@ class ServerUserActionAPI check(deviceId, String) check(workId, String) - const access = await PeripheralDeviceContentWriteAccess.executeFunction(this, deviceId) - return PackageManagerAPI.abortExpectation(access, workId) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return PackageManagerAPI.abortExpectation(deviceId, workId) } ) } @@ -892,8 +916,9 @@ class ServerUserActionAPI check(deviceId, String) check(containerId, String) - const access = await PeripheralDeviceContentWriteAccess.executeFunction(this, deviceId) - return PackageManagerAPI.restartPackageContainer(access, containerId) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MEDIA_MANAGEMENT) + + return PackageManagerAPI.restartPackageContainer(deviceId, containerId) } ) } @@ -922,7 +947,7 @@ class ServerUserActionAPI async () => { check(hashedToken, String) - await SystemWriteAccess.systemActions(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_SYSTEM_ACTION) if (!verifyHashedToken(hashedToken)) { throw new Meteor.Error(401, `Restart token is invalid or has expired`) @@ -958,8 +983,8 @@ class ServerUserActionAPI async () => { check(bucketId, String) - const access = await BucketSecurity.allowWriteAccess(this, bucketId) - return BucketsAPI.removeBucket(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.removeBucket(bucketId) } ) } @@ -979,8 +1004,8 @@ class ServerUserActionAPI check(bucketId, String) check(bucketProps, Object) - const access = await BucketSecurity.allowWriteAccess(this, bucketId) - return BucketsAPI.modifyBucket(access, bucketProps) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.modifyBucket(bucketId, bucketProps) } ) } @@ -994,8 +1019,8 @@ class ServerUserActionAPI async () => { check(bucketId, String) - const access = await BucketSecurity.allowWriteAccess(this, bucketId) - return BucketsAPI.emptyBucket(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.emptyBucket(bucketId) } ) } @@ -1010,8 +1035,8 @@ class ServerUserActionAPI check(studioId, String) check(name, String) - const access = await StudioContentWriteAccess.bucket(this, studioId) - return BucketsAPI.createNewBucket(access, name) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.createNewBucket(studioId, name) } ) } @@ -1025,8 +1050,8 @@ class ServerUserActionAPI 'bucketsRemoveBucketAdLib', { adlibId }, async () => { - const access = await BucketSecurity.allowWriteAccessPiece(this, adlibId) - return BucketsAPI.removeBucketAdLib(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.removeBucketAdLib(adlibId) } ) } @@ -1040,8 +1065,8 @@ class ServerUserActionAPI async () => { check(actionId, String) - const access = await BucketSecurity.allowWriteAccessAction(this, actionId) - return BucketsAPI.removeBucketAdLibAction(access) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.removeBucketAdLibAction(actionId) } ) } @@ -1061,8 +1086,8 @@ class ServerUserActionAPI check(adlibId, String) check(adlibProps, Object) - const access = await BucketSecurity.allowWriteAccessPiece(this, adlibId) - return BucketsAPI.modifyBucketAdLib(access, adlibProps) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.modifyBucketAdLib(adlibId, adlibProps) } ) } @@ -1082,8 +1107,8 @@ class ServerUserActionAPI check(actionId, String) check(actionProps, Object) - const access = await BucketSecurity.allowWriteAccessAction(this, actionId) - return BucketsAPI.modifyBucketAdLibAction(access, actionProps) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.modifyBucketAdLibAction(actionId, actionProps) } ) } @@ -1105,8 +1130,8 @@ class ServerUserActionAPI check(bucketId, String) check(action, Object) - const access = await BucketSecurity.allowWriteAccess(this, bucketId) - return BucketsAPI.saveAdLibActionIntoBucket(access, action) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_BUCKET_MODIFICATION) + return BucketsAPI.saveAdLibActionIntoBucket(bucketId, action) } ) } @@ -1121,15 +1146,16 @@ class ServerUserActionAPI this, userEvent, eventTime, - 'packageManagerRestartAllExpectations', + 'switchRouteSet', { studioId, routeSetId, state }, async () => { check(studioId, String) check(routeSetId, String) check(state, Match.OneOf('toggle', Boolean)) - const access = await StudioContentWriteAccess.routeSet(this, studioId) - return ServerPlayoutAPI.switchRouteSet(access, routeSetId, state) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_PLAYOUT_USERACTION) + + return ServerPlayoutAPI.switchRouteSet(studioId, routeSetId, state) } ) } @@ -1195,8 +1221,13 @@ class ServerUserActionAPI check(subDeviceId, String) check(disable, Boolean) - const access = await PeripheralDeviceContentWriteAccess.peripheralDevice(this, peripheralDeviceId) - return ServerPeripheralDeviceAPI.disableSubDevice(access, subDeviceId, disable) + assertConnectionHasOneOfPermissions( + this.connection, + ...PERMISSIONS_FOR_PLAYOUT_USERACTION, + ...PERMISSIONS_FOR_SYSTEM_ACTION + ) + + return ServerPeripheralDeviceAPI.disableSubDevice(peripheralDeviceId, subDeviceId, disable) } ) } @@ -1332,7 +1363,8 @@ class ServerUserActionAPI check(studioId, String) check(showStyleVariantId, String) - // TODO - checkAccessToStudio? + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_PLAYOUT_USERACTION) + return runIngestOperation(studioId, IngestJobs.CreateAdlibTestingRundownForShowStyleVariant, { showStyleVariantId, }) diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index 5a81d597c2..700ceda53d 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -22,14 +22,14 @@ import { import { MinimalMongoCursor } from './implementations/asyncCollection' export interface MongoAllowRules { - insert?: (userId: UserId | null, doc: DBInterface) => Promise | boolean + // insert?: (userId: UserId | null, doc: DBInterface) => Promise | boolean update?: ( userId: UserId | null, doc: DBInterface, fieldNames: FieldNames, modifier: MongoModifier ) => Promise | boolean - remove?: (userId: UserId | null, doc: DBInterface) => Promise | boolean + // remove?: (userId: UserId | null, doc: DBInterface) => Promise | boolean } /** @@ -48,29 +48,6 @@ export function getOrCreateMongoCollection(name: string): Mongo.Collection return newCollection } -/** - * Wrap an existing Mongo.Collection to have async methods. Primarily to convert the built-in Users collection - * @param collection Collection to wrap - * @param name Name of the collection - * @param allowRules The 'allow' rules for publications. Set to `false` to make readonly - */ -export function wrapMongoCollection }>( - collection: Mongo.Collection, - name: CollectionName, - allowRules: MongoAllowRules | false -): AsyncOnlyMongoCollection { - if (collectionsCache.has(name)) throw new Meteor.Error(500, `Collection "${name}" has already been created`) - collectionsCache.set(name, collection) - - setupCollectionAllowRules(collection, allowRules) - - const wrapped = new WrappedAsyncMongoCollection(collection, name) - - registerCollection(name, wrapped as WrappedAsyncMongoCollection) - - return wrapped -} - /** * Create a fully featured MongoCollection * @param name Name of the collection in mongodb @@ -133,24 +110,16 @@ function setupCollectionAllowRules['allow']>[0]*/ = { - insert: () => false, - insertAsync: origInsert - ? (userId: string | null, doc: DBInterface) => origInsert(protectString(userId), doc) as any - : () => false, update: () => false, updateAsync: origUpdate ? (userId: string | null, doc: DBInterface, fieldNames: string[], modifier: any) => origUpdate(protectString(userId), doc, fieldNames as any, modifier) as any : () => false, - remove: () => false, - removeAsync: origRemove - ? (userId: string | null, doc: DBInterface) => origRemove(protectString(userId), doc) as any - : () => false, } collection.allow(options) diff --git a/meteor/server/collections/index.ts b/meteor/server/collections/index.ts index 25d56a1745..1e326b4366 100644 --- a/meteor/server/collections/index.ts +++ b/meteor/server/collections/index.ts @@ -24,34 +24,24 @@ import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataMod import { TranslationsBundle } from '@sofie-automation/meteor-lib/dist/collections/TranslationsBundles' import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' -import { DBUser } from '@sofie-automation/meteor-lib/dist/collections/Users' import { WorkerStatus } from '@sofie-automation/meteor-lib/dist/collections/Workers' import { registerIndex } from './indices' import { getCurrentTime } from '../lib/lib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { - createAsyncOnlyMongoCollection, - createAsyncOnlyReadOnlyMongoCollection, - wrapMongoCollection, -} from './collection' +import { createAsyncOnlyMongoCollection, createAsyncOnlyReadOnlyMongoCollection } from './collection' import { ObserveChangesForHash } from './lib' import { logger } from '../logging' -import { resolveCredentials } from '../security/lib/credentials' -import { logNotAllowed, allowOnlyFields, rejectFields } from '../security/lib/lib' -import { - allowAccessToCoreSystem, - allowAccessToOrganization, - allowAccessToShowStyleBase, - allowAccessToStudio, -} from '../security/lib/security' -import { SystemWriteAccess } from '../security/system' +import { allowOnlyFields, rejectFields } from '../security/allowDeny' +import { checkUserIdHasOneOfPermissions } from '../security/auth' export * from './bucket' export * from './packages-media' export * from './rundown' export const Blueprints = createAsyncOnlyMongoCollection(CollectionName.Blueprints, { - update(_userId, doc, fields, _modifier) { + update(userId, doc, fields, _modifier) { + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.Blueprints, 'configure')) return false + return allowOnlyFields(doc, fields, ['name', 'disableVersionChecks']) }, }) @@ -61,9 +51,7 @@ registerIndex(Blueprints, { export const CoreSystem = createAsyncOnlyMongoCollection(CollectionName.CoreSystem, { async update(userId, doc, fields, _modifier) { - const cred = await resolveCredentials({ userId: userId }) - const access = await allowAccessToCoreSystem(cred) - if (!access.update) return logNotAllowed('CoreSystem', access.reason) + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.CoreSystem, 'configure')) return false return allowOnlyFields(doc, fields, [ 'support', @@ -103,8 +91,8 @@ registerIndex(ExternalMessageQueue, { export const Organizations = createAsyncOnlyMongoCollection(CollectionName.Organizations, { async update(userId, doc, fields, _modifier) { - const access = await allowAccessToOrganization({ userId: userId }, doc._id) - if (!access.update) return logNotAllowed('Organization', access.reason) + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.Organizations, 'configure')) return false + return allowOnlyFields(doc, fields, ['userRoles']) }, }) @@ -118,7 +106,9 @@ registerIndex(PeripheralDeviceCommands, { }) export const PeripheralDevices = createAsyncOnlyMongoCollection(CollectionName.PeripheralDevices, { - update(_userId, doc, fields, _modifier) { + update(userId, doc, fields, _modifier) { + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.PeripheralDevices, 'configure')) return false + return rejectFields(doc, fields, [ 'type', 'parentDeviceId', @@ -147,8 +137,8 @@ registerIndex(PeripheralDevices, { export const RundownLayouts = createAsyncOnlyMongoCollection(CollectionName.RundownLayouts, { async update(userId, doc, fields) { - const access = await allowAccessToShowStyleBase({ userId: userId }, doc.showStyleBaseId) - if (!access.update) return logNotAllowed('ShowStyleBase', access.reason) + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.RundownLayouts, 'configure')) return false + return rejectFields(doc, fields, ['_id', 'showStyleBaseId']) }, }) @@ -164,8 +154,8 @@ registerIndex(RundownLayouts, { export const ShowStyleBases = createAsyncOnlyMongoCollection(CollectionName.ShowStyleBases, { async update(userId, doc, fields) { - const access = await allowAccessToShowStyleBase({ userId: userId }, doc._id) - if (!access.update) return logNotAllowed('ShowStyleBase', access.reason) + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.ShowStyleBases, 'configure')) return false + return rejectFields(doc, fields, ['_id']) }, }) @@ -175,8 +165,7 @@ registerIndex(ShowStyleBases, { export const ShowStyleVariants = createAsyncOnlyMongoCollection(CollectionName.ShowStyleVariants, { async update(userId, doc, fields) { - const access = await allowAccessToShowStyleBase({ userId: userId }, doc.showStyleBaseId) - if (!access.update) return logNotAllowed('ShowStyleBase', access.reason) + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.ShowStyleVariants, 'configure')) return false return rejectFields(doc, fields, ['showStyleBaseId']) }, @@ -187,7 +176,9 @@ registerIndex(ShowStyleVariants, { }) export const Snapshots = createAsyncOnlyMongoCollection(CollectionName.Snapshots, { - update(_userId, doc, fields, _modifier) { + update(userId, doc, fields, _modifier) { + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.Snapshots, 'configure')) return false + return allowOnlyFields(doc, fields, ['comment']) }, }) @@ -200,8 +191,8 @@ registerIndex(Snapshots, { export const Studios = createAsyncOnlyMongoCollection(CollectionName.Studios, { async update(userId, doc, fields, _modifier) { - const access = await allowAccessToStudio({ userId: userId }, doc._id) - if (!access.update) return logNotAllowed('Studio', access.reason) + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.Studios, 'configure')) return false + return rejectFields(doc, fields, ['_id']) }, }) @@ -229,17 +220,9 @@ export const TranslationsBundles = createAsyncOnlyMongoCollection(CollectionName.TriggeredActions, { async update(userId, doc, fields) { - const cred = await resolveCredentials({ userId: userId }) - - if (doc.showStyleBaseId) { - const access = await allowAccessToShowStyleBase(cred, doc.showStyleBaseId) - if (!access.update) return logNotAllowed('ShowStyleBase', access.reason) - return rejectFields(doc, fields, ['_id']) - } else { - const access = await allowAccessToCoreSystem(cred) - if (!access.update) return logNotAllowed('CoreSystem', access.reason) - return rejectFields(doc, fields, ['_id']) - } + if (!checkUserIdHasOneOfPermissions(userId, CollectionName.TriggeredActions, 'configure')) return false + + return rejectFields(doc, fields, ['_id']) }, }) registerIndex(TriggeredActions, { @@ -255,26 +238,6 @@ registerIndex(UserActionsLog, { timelineHash: 1, }) -// This is a somewhat special collection, as it draws from the Meteor.users collection from the Accounts package -export const Users = wrapMongoCollection(Meteor.users as any, CollectionName.Users, { - async update(userId, doc, fields, _modifier) { - const access = await SystemWriteAccess.currentUser(userId, { userId }) - if (!access) return logNotAllowed('CurrentUser', '') - return rejectFields(doc, fields, [ - '_id', - 'createdAt', - 'services', - 'emails', - 'profile', - 'organizationId', - 'superAdmin', - ]) - }, -}) -registerIndex(Users, { - organizationId: 1, -}) - export const Workers = createAsyncOnlyMongoCollection(CollectionName.Workers, false) export const WorkerThreadStatuses = createAsyncOnlyMongoCollection( diff --git a/meteor/server/email.ts b/meteor/server/email.ts deleted file mode 100644 index 0dd0b2d795..0000000000 --- a/meteor/server/email.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { Accounts } from 'meteor/accounts-base' - -Meteor.startup(function () { - process.env.MAIL_URL = Meteor.settings.MAIL_URL - Accounts.urls.verifyEmail = function (token) { - return Meteor.absoluteUrl('login/verify-email/' + token) - } - Accounts.urls.resetPassword = function (token) { - return Meteor.absoluteUrl('reset/' + token) - } -}) diff --git a/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts b/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts index c77442a391..e2c4cafb1f 100644 --- a/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts +++ b/meteor/server/lib/customPublication/__tests__/optimizedObserver.test.ts @@ -1,4 +1,3 @@ -import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { createManualPromise } from '@sofie-automation/corelib/dist/lib' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { optimizedObserverCountSubscribers, setUpOptimizedObserverInner, TriggerUpdate } from '../optimizedObserverBase' @@ -20,9 +19,6 @@ class CustomPublishMock }> get isReady(): boolean { return false } - get userId(): UserId | null { - return null - } stop?: () => void diff --git a/meteor/server/lib/customPublication/publish.ts b/meteor/server/lib/customPublication/publish.ts index b9ac5fc402..fb8622b516 100644 --- a/meteor/server/lib/customPublication/publish.ts +++ b/meteor/server/lib/customPublication/publish.ts @@ -1,4 +1,3 @@ -import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Meteor } from 'meteor/meteor' import { AllPubSubTypes } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { ProtectedString, unprotectString } from '../tempLib' @@ -43,10 +42,6 @@ export class CustomPublishMeteor }> { return this.#isReady } - get userId(): UserId | null { - return this._meteorSubscription.userId - } - /** * Register a function to be called when the subscriber unsubscribes */ diff --git a/meteor/server/main.ts b/meteor/server/main.ts index 30c7678f5c..06cb00b026 100644 --- a/meteor/server/main.ts +++ b/meteor/server/main.ts @@ -46,7 +46,6 @@ import './api/rest/api' import './Connections' import './coreSystem' import './cronjobs' -import './email' import './prometheus' import './api/deviceTriggers/observer' import './logo' @@ -55,4 +54,4 @@ import './systemTime' // Setup publications and security: import './publications/_publications' -import './security/_security' +import './security/securityVerify' diff --git a/meteor/server/methods.ts b/meteor/server/methods.ts index 49bee70b1a..3a3c1da46b 100644 --- a/meteor/server/methods.ts +++ b/meteor/server/methods.ts @@ -4,8 +4,8 @@ import { logger } from './logging' import { extractFunctionSignature } from './lib' import { MethodContext, MethodContextAPI } from './api/methodContext' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { Settings } from './Settings' import { isPromise } from '@sofie-automation/shared-lib/dist/lib/lib' +import { assertConnectionHasOneOfPermissions } from './security/auth' type MeteorMethod = (this: MethodContext, ...args: any[]) => any @@ -142,25 +142,24 @@ function setMeteorMethods(orgMethods: MethodsInner, secret?: boolean): void { AllMeteorMethods.push(methodName) } }) - // @ts-expect-error: incompatible due to userId Meteor.methods(methods) } export type MeteorDebugMethod = (this: Meteor.MethodThisType, ...args: any[]) => Promise | any export function MeteorDebugMethods(methods: { [key: string]: MeteorDebugMethod }): void { - if (!Settings.enableUserAccounts) { - const fiberMethods: { [key: string]: (this: Meteor.MethodThisType, ...args: any[]) => any } = {} + const fiberMethods: { [key: string]: (this: Meteor.MethodThisType, ...args: any[]) => any } = {} - for (const [key, fn] of Object.entries(methods)) { - if (key && !!fn) { - fiberMethods[key] = function (this: Meteor.MethodThisType, ...args: any[]) { - return fn.call(this, ...args) - } + for (const [key, fn] of Object.entries(methods)) { + if (key && !!fn) { + fiberMethods[key] = function (this: Meteor.MethodThisType, ...args: any[]) { + assertConnectionHasOneOfPermissions(this.connection, 'developer') + + return fn.call(this, ...args) } } - - Meteor.methods(fiberMethods) } + + Meteor.methods(fiberMethods) } export function getRunningMethods(): RunningMethods { diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index fd4fac48e1..16d6cab3ab 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -9,7 +9,6 @@ import { import * as Migrations from './databaseMigration' import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { MethodContextAPI } from '../api/methodContext' -import { SystemWriteAccess } from '../security/system' import { fixupConfigForShowStyleBase, fixupConfigForStudio, @@ -22,10 +21,15 @@ import { } from './upgrades' import { ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { BlueprintValidateConfigForStudioResult } from '@sofie-automation/corelib/dist/worker/studio' +import { assertConnectionHasOneOfPermissions } from '../security/auth' +import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' + +const PERMISSIONS_FOR_MIGRATIONS: Array = ['configure'] class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async getMigrationStatus() { - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) + return Migrations.getMigrationStatus() } @@ -40,20 +44,21 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { check(inputResults, Array) check(isFirstOfPartialMigrations, Match.Maybe(Boolean)) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return Migrations.runMigration(chunks, hash, inputResults, isFirstOfPartialMigrations || false) } async forceMigration(chunks: Array) { check(chunks, Array) - await SystemWriteAccess.migrations(this) + + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return Migrations.forceMigration(chunks) } async resetDatabaseVersions() { - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return Migrations.resetDatabaseVersions() } @@ -61,7 +66,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async fixupConfigForStudio(studioId: StudioId): Promise { check(studioId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return fixupConfigForStudio(studioId) } @@ -69,7 +74,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async ignoreFixupConfigForStudio(studioId: StudioId): Promise { check(studioId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return ignoreFixupConfigForStudio(studioId) } @@ -77,7 +82,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async validateConfigForStudio(studioId: StudioId): Promise { check(studioId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return validateConfigForStudio(studioId) } @@ -85,7 +90,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async runUpgradeForStudio(studioId: StudioId): Promise { check(studioId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return runUpgradeForStudio(studioId) } @@ -93,7 +98,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async fixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { check(showStyleBaseId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return fixupConfigForShowStyleBase(showStyleBaseId) } @@ -101,7 +106,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async ignoreFixupConfigForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { check(showStyleBaseId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return ignoreFixupConfigForShowStyleBase(showStyleBaseId) } @@ -111,7 +116,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { ): Promise { check(showStyleBaseId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return validateConfigForShowStyleBase(showStyleBaseId) } @@ -119,7 +124,7 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { async runUpgradeForShowStyleBase(showStyleBaseId: ShowStyleBaseId): Promise { check(showStyleBaseId, String) - await SystemWriteAccess.migrations(this) + assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) return runUpgradeForShowStyleBase(showStyleBaseId) } diff --git a/meteor/server/publications/blueprintUpgradeStatus/publication.ts b/meteor/server/publications/blueprintUpgradeStatus/publication.ts index 568f9b0756..1bebd28c49 100644 --- a/meteor/server/publications/blueprintUpgradeStatus/publication.ts +++ b/meteor/server/publications/blueprintUpgradeStatus/publication.ts @@ -10,9 +10,6 @@ import { SetupObserversResult, TriggerUpdate, } from '../../lib/customPublication' -import { logger } from '../../logging' -import { resolveCredentials } from '../../security/lib/credentials' -import { NoSecurityReadAccess } from '../../security/noSecurity' import { ContentCache, createReactiveContentCache, ShowStyleBaseFields, StudioFields } from './reactiveContentCache' import { UpgradesContentObserver } from './upgradesContentObserver' import { BlueprintMapEntry, checkDocUpgradeStatus } from './checkStatus' @@ -23,6 +20,7 @@ import { UIBlueprintUpgradeStatus, UIBlueprintUpgradeStatusId, } from '@sofie-automation/meteor-lib/dist/api/upgradeStatus' +import { assertConnectionHasOneOfPermissions } from '../../security/auth' type BlueprintUpgradeStatusArgs = Record @@ -238,12 +236,8 @@ meteorCustomPublish( MeteorPubSub.uiBlueprintUpgradeStatuses, CustomCollectionName.UIBlueprintUpgradeStatuses, async function (pub) { - const cred = await resolveCredentials({ userId: this.userId, token: undefined }) + assertConnectionHasOneOfPermissions(this.connection, 'configure', 'service') - if (!cred || NoSecurityReadAccess.any()) { - await createBlueprintUpgradeStatusSubscriptionHandle(pub) - } else { - logger.warn(`Pub.${CustomCollectionName.UIBlueprintUpgradeStatuses}: Not allowed`) - } + await createBlueprintUpgradeStatusSubscriptionHandle(pub) } ) diff --git a/meteor/server/publications/buckets.ts b/meteor/server/publications/buckets.ts index 3801f8b467..589f63d3bb 100644 --- a/meteor/server/publications/buckets.ts +++ b/meteor/server/publications/buckets.ts @@ -1,14 +1,12 @@ import { FindOptions } from '@sofie-automation/meteor-lib/dist/collections/lib' -import { BucketSecurity } from '../security/buckets' import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' -import { StudioReadAccess } from '../security/studio' -import { isProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { BucketAdLibActions, BucketAdLibs, Buckets } from '../collections' import { check, Match } from 'meteor/check' import { StudioId, BucketId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' meteorPublish( MeteorPubSub.buckets, @@ -16,26 +14,23 @@ meteorPublish( check(studioId, String) check(bucketId, Match.Maybe(String)) + triggerWriteAccessBecauseNoCheckNecessary() + const modifier: FindOptions = { fields: {}, } - if ( - (await StudioReadAccess.studioContent(studioId, this)) || - (isProtectedString(bucketId) && bucketId && (await BucketSecurity.allowReadAccess(this, bucketId))) - ) { - return Buckets.findWithCursor( - bucketId - ? { - _id: bucketId, - studioId, - } - : { - studioId, - }, - modifier - ) - } - return null + + return Buckets.findWithCursor( + bucketId + ? { + _id: bucketId, + studioId, + } + : { + studioId, + }, + modifier + ) } ) @@ -46,23 +41,22 @@ meteorPublish( check(bucketId, String) check(showStyleVariantIds, Array) - if (isProtectedString(bucketId) && (await BucketSecurity.allowReadAccess(this, bucketId))) { - return BucketAdLibs.findWithCursor( - { - studioId: studioId, - bucketId: bucketId, - showStyleVariantId: { - $in: [null, ...showStyleVariantIds], // null = valid for all variants - }, + triggerWriteAccessBecauseNoCheckNecessary() + + return BucketAdLibs.findWithCursor( + { + studioId: studioId, + bucketId: bucketId, + showStyleVariantId: { + $in: [null, ...showStyleVariantIds], // null = valid for all variants }, - { - fields: { - ingestInfo: 0, // This is a large blob, and is not of interest to the UI - }, - } - ) - } - return null + }, + { + fields: { + ingestInfo: 0, // This is a large blob, and is not of interest to the UI + }, + } + ) } ) @@ -73,22 +67,21 @@ meteorPublish( check(bucketId, String) check(showStyleVariantIds, Array) - if (isProtectedString(bucketId) && (await BucketSecurity.allowReadAccess(this, bucketId))) { - return BucketAdLibActions.findWithCursor( - { - studioId: studioId, - bucketId: bucketId, - showStyleVariantId: { - $in: [null, ...showStyleVariantIds], // null = valid for all variants - }, + triggerWriteAccessBecauseNoCheckNecessary() + + return BucketAdLibActions.findWithCursor( + { + studioId: studioId, + bucketId: bucketId, + showStyleVariantId: { + $in: [null, ...showStyleVariantIds], // null = valid for all variants }, - { - fields: { - ingestInfo: 0, // This is a large blob, and is not of interest to the UI - }, - } - ) - } - return null + }, + { + fields: { + ingestInfo: 0, // This is a large blob, and is not of interest to the UI + }, + } + ) } ) diff --git a/meteor/server/publications/deviceTriggersPreview.ts b/meteor/server/publications/deviceTriggersPreview.ts index 67e9edbf03..c8352ba51f 100644 --- a/meteor/server/publications/deviceTriggersPreview.ts +++ b/meteor/server/publications/deviceTriggersPreview.ts @@ -9,8 +9,8 @@ import { DeviceTriggerArguments, UIDeviceTriggerPreview } from '@sofie-automatio import { getCurrentTime } from '../lib/lib' import { SetupObserversResult, setUpOptimizedObserverArray, TriggerUpdate } from '../lib/customPublication' import { CustomPublish, meteorCustomPublish } from '../lib/customPublication/publish' -import { StudioReadAccess } from '../security/studio' import { PeripheralDevices } from '../collections' +import { assertConnectionHasOneOfPermissions } from '../security/auth' /** IDEA: This could potentially be a Capped Collection, thus enabling scaling Core horizontally: * https://www.mongodb.com/docs/manual/core/capped-collections/ */ @@ -19,14 +19,12 @@ const lastTriggers: Record { - /** - * The id of the logged-in user, or `null` if no user is logged in. - * This is constant. However, if the logged-in user changes, the publish function - * is rerun with the new value, assuming it didn’t throw an error at the previous run. - */ - userId: UserId | null -} +export type SubscriptionContext = Omit /** * Unsafe wrapper around Meteor.publish @@ -82,90 +63,6 @@ export function meteorPublish( meteorPublishUnsafe(name, callback) } -export namespace AutoFillSelector { - /** Autofill an empty selector {} with organizationId of the current user */ - export async function organizationId( - userId: UserId | null, - selector: MongoQuery, - token: string | undefined - ): Promise<{ - cred: ResolvedCredentials | null - selector: MongoQuery - }> { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - - let cred: ResolvedCredentials | null = null - if (Settings.enableUserAccounts) { - if (!selector.organizationId) { - cred = await resolveCredentials({ userId: userId, token }) - if (cred.organizationId) selector.organizationId = cred.organizationId as any - // TODO - should this block all access if cred.organizationId is not set - } - } - return { cred, selector } - } - /** Autofill an empty selector {} with deviceId of the current user's peripheralDevices */ - export async function deviceId( - userId: UserId | null, - selector: MongoQuery, - token: string | undefined - ): Promise<{ - cred: ResolvedCredentials | null - selector: MongoQuery - }> { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - - let cred: ResolvedCredentials | null = null - if (Settings.enableUserAccounts) { - if (!selector.deviceId) { - cred = await resolveCredentials({ userId: userId, token }) - if (cred.organizationId) { - const devices = (await PeripheralDevices.findFetchAsync( - { - organizationId: cred.organizationId, - }, - { projection: { _id: 1 } } - )) as Array> - - selector.deviceId = { $in: devices.map((d) => d._id) } as any - } - // TODO - should this block all access if cred.organizationId is not set - } - } - return { cred, selector } - } - /** Autofill an empty selector {} with showStyleBaseId of the current user's showStyleBases */ - export async function showStyleBaseId( - userId: UserId | null, - selector: MongoQuery, - token: string | undefined - ): Promise<{ - cred: ResolvedCredentials | null - selector: MongoQuery - }> { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - - let cred: ResolvedCredentials | null = null - if (Settings.enableUserAccounts) { - if (!selector.showStyleBaseId) { - cred = await resolveCredentials({ userId: userId, token }) - if (cred.organizationId) { - const showStyleBases = (await ShowStyleBases.findFetchAsync( - { - organizationId: cred.organizationId, - }, - { projection: { _id: 1 } } - )) as Array> - - selector.showStyleBaseId = { $in: showStyleBases.map((d) => d._id) } as any - } - // TODO - should this block all access if cred.organizationId is not set - } - } - return { cred, selector } - } -} - /** * Await each observer, and return the handles * If an observer throws, this will make sure to stop all the ones that were successfully started, to avoid leaking memory diff --git a/meteor/server/publications/mountedTriggers.ts b/meteor/server/publications/mountedTriggers.ts index 9976677de6..13c520221b 100644 --- a/meteor/server/publications/mountedTriggers.ts +++ b/meteor/server/publications/mountedTriggers.ts @@ -1,19 +1,18 @@ import { Meteor } from 'meteor/meteor' import { CustomPublish, meteorCustomPublish } from '../lib/customPublication' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { logger } from '../logging' import { DeviceTriggerMountedActionAdlibsPreview, DeviceTriggerMountedActions } from '../api/deviceTriggers/observer' import { Mongo } from 'meteor/mongo' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import _ from 'underscore' -import { PeripheralDevices } from '../collections' import { check } from 'meteor/check' import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { checkAccessAndGetPeripheralDevice } from '../security/check' const PUBLICATION_DEBOUNCE = 20 @@ -24,26 +23,20 @@ meteorCustomPublish( check(deviceId, String) check(deviceIds, [String]) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) - - if (!peripheralDevice) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) - - const studioId = peripheralDevice.studioId - if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) - - cursorCustomPublish( - pub, - DeviceTriggerMountedActions.find({ - studioId, - deviceId: { - $in: deviceIds, - }, - }) - ) - } else { - logger.warn(`Pub.mountedTriggersForDevice: Not allowed: "${deviceId}"`) - } + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + + const studioId = peripheralDevice.studioId + if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) + + cursorCustomPublish( + pub, + DeviceTriggerMountedActions.find({ + studioId, + deviceId: { + $in: deviceIds, + }, + }) + ) } ) @@ -53,23 +46,17 @@ meteorCustomPublish( async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) + const studioId = peripheralDevice.studioId + if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) - const studioId = peripheralDevice.studioId - if (!studioId) throw new Meteor.Error(400, `Peripheral Device "${deviceId}" not attached to a studio`) - - cursorCustomPublish( - pub, - DeviceTriggerMountedActionAdlibsPreview.find({ - studioId, - }) - ) - } else { - logger.warn(`Pub.mountedTriggersForDevicePreview: Not allowed: "${deviceId}"`) - } + cursorCustomPublish( + pub, + DeviceTriggerMountedActionAdlibsPreview.find({ + studioId, + }) + ) } ) diff --git a/meteor/server/publications/organization.ts b/meteor/server/publications/organization.ts index f596d8b3c6..489f3edc46 100644 --- a/meteor/server/publications/organization.ts +++ b/meteor/server/publications/organization.ts @@ -1,26 +1,29 @@ -import { meteorPublish, AutoFillSelector } from './lib/lib' +import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import { Evaluation } from '@sofie-automation/meteor-lib/dist/collections/Evaluations' import { SnapshotItem } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' import { UserActionsLogItem } from '@sofie-automation/meteor-lib/dist/collections/UserActionsLog' -import { OrganizationReadAccess } from '../security/organization' import { FindOptions } from '@sofie-automation/meteor-lib/dist/collections/lib' import { DBOrganization } from '@sofie-automation/meteor-lib/dist/collections/Organization' -import { isProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Blueprints, Evaluations, Organizations, Snapshots, UserActionsLog } from '../collections' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { BlueprintId, OrganizationId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { check, Match } from '../lib/check' import { getCurrentTime } from '../lib/lib' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { assertConnectionHasOneOfPermissions } from '../security/auth' meteorPublish( MeteorPubSub.organization, - async function (organizationId: OrganizationId | null, token: string | undefined) { + async function (organizationId: OrganizationId | null, _token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + if (!organizationId) return null - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, { _id: organizationId }, token) + const selector: MongoQuery = { _id: organizationId } + const modifier: FindOptions = { fields: { name: 1, @@ -29,83 +32,69 @@ meteorPublish( userRoles: 1, // to not expose too much information consider [`userRoles.${this.userId}`]: 1, and a method/publication for getting all the roles, or limiting the returned roles based on requesting user's role }, } - if ( - isProtectedString(selector.organizationId) && - (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) - ) { - return Organizations.findWithCursor({ _id: selector.organizationId }, modifier) - } - return null + + return Organizations.findWithCursor({ _id: selector.organizationId }, modifier) } ) -meteorPublish(CorelibPubSub.blueprints, async function (blueprintIds: BlueprintId[] | null, token: string | undefined) { - check(blueprintIds, Match.Maybe(Array)) +meteorPublish( + CorelibPubSub.blueprints, + async function (blueprintIds: BlueprintId[] | null, _token: string | undefined) { + assertConnectionHasOneOfPermissions(this.connection, 'configure') - // If values were provided, they must have values - if (blueprintIds && blueprintIds.length === 0) return null + check(blueprintIds, Match.Maybe(Array)) - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) + // If values were provided, they must have values + if (blueprintIds && blueprintIds.length === 0) return null - // Add the requested filter - if (blueprintIds) selector._id = { $in: blueprintIds } + // Add the requested filter + const selector: MongoQuery = {} + if (blueprintIds) selector._id = { $in: blueprintIds } - if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { return Blueprints.findWithCursor(selector, { fields: { code: 0, }, }) } - return null -}) -meteorPublish(MeteorPubSub.evaluations, async function (dateFrom: number, dateTo: number, token: string | undefined) { - const selector0: MongoQuery = { +) +meteorPublish(MeteorPubSub.evaluations, async function (dateFrom: number, dateTo: number, _token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + + const selector: MongoQuery = { timestamp: { $gte: dateFrom, $lt: dateTo, }, } - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) - if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { - return Evaluations.findWithCursor(selector) - } - return null + return Evaluations.findWithCursor(selector) }) -meteorPublish(MeteorPubSub.snapshots, async function (token: string | undefined) { - const selector0: MongoQuery = { +meteorPublish(MeteorPubSub.snapshots, async function (_token: string | undefined) { + assertConnectionHasOneOfPermissions(this.connection, 'configure') + + const selector: MongoQuery = { created: { $gt: getCurrentTime() - 30 * 24 * 3600 * 1000, // last 30 days }, } - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, selector0, token) - if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { - return Snapshots.findWithCursor(selector) - } - return null + return Snapshots.findWithCursor(selector) }) meteorPublish( MeteorPubSub.userActionsLog, - async function (dateFrom: number, dateTo: number, token: string | undefined) { - const selector0: MongoQuery = { + async function (dateFrom: number, dateTo: number, _token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + + const selector: MongoQuery = { timestamp: { $gte: dateFrom, $lt: dateTo, }, } - const { cred, selector } = await AutoFillSelector.organizationId( - this.userId, - selector0, - token - ) - if (!cred || (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) { - return UserActionsLog.findWithCursor(selector, { - limit: 10_000, // this is to prevent having a publication that produces a very large array - }) - } - return null + return UserActionsLog.findWithCursor(selector, { + limit: 10_000, // this is to prevent having a publication that produces a very large array + }) } ) diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index 1952fb7057..66ee316ae7 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -1,5 +1,3 @@ -import { Meteor } from 'meteor/meteor' -import { PeripheralDeviceReadAccess } from '../../../security/peripheralDevice' import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' import { TriggerUpdate, @@ -19,7 +17,7 @@ import { PieceInstanceId, StudioId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDevices, Studios } from '../../../collections' +import { Studios } from '../../../collections' import { check, Match } from 'meteor/check' import { PackageManagerExpectedPackage } from '@sofie-automation/shared-lib/dist/package-manager/publications' import { ExpectedPackagesContentObserver } from './contentObserver' @@ -30,6 +28,7 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import { checkAccessAndGetPeripheralDevice } from '../../../security/check' interface ExpectedPackagesPublicationArgs { readonly studioId: StudioId @@ -206,34 +205,28 @@ meteorCustomPublish( check(deviceId, String) check(filterPlayoutDeviceIds, Match.Maybe([String])) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') - - const studioId = peripheralDevice.studioId - if (!studioId) { - logger.warn(`Pub.packageManagerExpectedPackages: device "${peripheralDevice._id}" has no studioId`) - return this.ready() - } - - await setUpCollectionOptimizedObserver< - PackageManagerExpectedPackage, - ExpectedPackagesPublicationArgs, - ExpectedPackagesPublicationState, - ExpectedPackagesPublicationUpdateProps - >( - `${PeripheralDevicePubSub.packageManagerExpectedPackages}_${studioId}_${deviceId}_${JSON.stringify( - (filterPlayoutDeviceIds || []).sort() - )}`, - { studioId, deviceId, filterPlayoutDeviceIds }, - setupExpectedPackagesPublicationObservers, - manipulateExpectedPackagesPublicationData, - pub, - 500 // ms, wait this time before sending an update - ) - } else { - logger.warn(`Pub.packageManagerExpectedPackages: Not allowed: "${deviceId}"`) + const studioId = peripheralDevice.studioId + if (!studioId) { + logger.warn(`Pub.packageManagerExpectedPackages: device "${peripheralDevice._id}" has no studioId`) + return this.ready() } + + await setUpCollectionOptimizedObserver< + PackageManagerExpectedPackage, + ExpectedPackagesPublicationArgs, + ExpectedPackagesPublicationState, + ExpectedPackagesPublicationUpdateProps + >( + `${PeripheralDevicePubSub.packageManagerExpectedPackages}_${studioId}_${deviceId}_${JSON.stringify( + (filterPlayoutDeviceIds || []).sort() + )}`, + { studioId, deviceId, filterPlayoutDeviceIds }, + setupExpectedPackagesPublicationObservers, + manipulateExpectedPackagesPublicationData, + pub, + 500 // ms, wait this time before sending an update + ) } ) diff --git a/meteor/server/publications/packageManager/packageContainers.ts b/meteor/server/publications/packageManager/packageContainers.ts index 0accf66181..133569a882 100644 --- a/meteor/server/publications/packageManager/packageContainers.ts +++ b/meteor/server/publications/packageManager/packageContainers.ts @@ -5,9 +5,8 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import { PackageContainer } from '@sofie-automation/shared-lib/dist/package-manager/package' import { PackageManagerPackageContainers } from '@sofie-automation/shared-lib/dist/package-manager/publications' import { check } from 'meteor/check' -import { Meteor } from 'meteor/meteor' import { ReadonlyDeep } from 'type-fest' -import { PeripheralDevices, Studios } from '../../collections' +import { Studios } from '../../collections' import { meteorCustomPublish, SetupObserversResult, @@ -15,12 +14,12 @@ import { TriggerUpdate, } from '../../lib/customPublication' import { logger } from '../../logging' -import { PeripheralDeviceReadAccess } from '../../security/peripheralDevice' import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' type StudioFields = '_id' | 'packageContainersWithOverrides' const studioFieldSpecifier = literal>>({ @@ -96,32 +95,26 @@ meteorCustomPublish( async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') - - const studioId = peripheralDevice.studioId - if (!studioId) { - logger.warn(`Pub.packageManagerPackageContainers: device "${peripheralDevice._id}" has no studioId`) - return this.ready() - } - - await setUpOptimizedObserverArray< - PackageManagerPackageContainers, - PackageManagerPackageContainersArgs, - PackageManagerPackageContainersState, - PackageManagerPackageContainersUpdateProps - >( - `${PeripheralDevicePubSub.packageManagerPackageContainers}_${studioId}_${deviceId}`, - { studioId, deviceId }, - setupExpectedPackagesPublicationObservers, - manipulateExpectedPackagesPublicationData, - pub, - 500 // ms, wait this time before sending an update - ) - } else { - logger.warn(`Pub.packageManagerPackageContainers: Not allowed: "${deviceId}"`) + const studioId = peripheralDevice.studioId + if (!studioId) { + logger.warn(`Pub.packageManagerPackageContainers: device "${peripheralDevice._id}" has no studioId`) + return this.ready() } + + await setUpOptimizedObserverArray< + PackageManagerPackageContainers, + PackageManagerPackageContainersArgs, + PackageManagerPackageContainersState, + PackageManagerPackageContainersUpdateProps + >( + `${PeripheralDevicePubSub.packageManagerPackageContainers}_${studioId}_${deviceId}`, + { studioId, deviceId }, + setupExpectedPackagesPublicationObservers, + manipulateExpectedPackagesPublicationData, + pub, + 500 // ms, wait this time before sending an update + ) } ) diff --git a/meteor/server/publications/packageManager/playoutContext.ts b/meteor/server/publications/packageManager/playoutContext.ts index 08c881fafe..70b55955ca 100644 --- a/meteor/server/publications/packageManager/playoutContext.ts +++ b/meteor/server/publications/packageManager/playoutContext.ts @@ -5,9 +5,8 @@ import { literal } from '@sofie-automation/corelib/dist/lib' import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo' import { PackageManagerPlayoutContext } from '@sofie-automation/shared-lib/dist/package-manager/publications' import { check } from 'meteor/check' -import { Meteor } from 'meteor/meteor' import { ReadonlyDeep } from 'type-fest' -import { PeripheralDevices, RundownPlaylists, Rundowns } from '../../collections' +import { RundownPlaylists, Rundowns } from '../../collections' import { meteorCustomPublish, SetupObserversResult, @@ -15,11 +14,11 @@ import { TriggerUpdate, } from '../../lib/customPublication' import { logger } from '../../logging' -import { PeripheralDeviceReadAccess } from '../../security/peripheralDevice' import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import { checkAccessAndGetPeripheralDevice } from '../../security/check' export type RundownPlaylistCompact = Pick const rundownPlaylistFieldSpecifier = literal>({ @@ -114,32 +113,26 @@ meteorCustomPublish( async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') - - const studioId = peripheralDevice.studioId - if (!studioId) { - logger.warn(`Pub.packageManagerPlayoutContext: device "${peripheralDevice._id}" has no studioId`) - return this.ready() - } - - await setUpOptimizedObserverArray< - PackageManagerPlayoutContext, - PackageManagerPlayoutContextArgs, - PackageManagerPlayoutContextState, - PackageManagerPlayoutContextUpdateProps - >( - `${PeripheralDevicePubSub.packageManagerPlayoutContext}_${studioId}_${deviceId}`, - { studioId, deviceId }, - setupExpectedPackagesPublicationObservers, - manipulateExpectedPackagesPublicationData, - pub, - 500 // ms, wait this time before sending an update - ) - } else { - logger.warn(`Pub.packageManagerPlayoutContext: Not allowed: "${deviceId}"`) + const studioId = peripheralDevice.studioId + if (!studioId) { + logger.warn(`Pub.packageManagerPlayoutContext: device "${peripheralDevice._id}" has no studioId`) + return this.ready() } + + await setUpOptimizedObserverArray< + PackageManagerPlayoutContext, + PackageManagerPlayoutContextArgs, + PackageManagerPlayoutContextState, + PackageManagerPlayoutContextUpdateProps + >( + `${PeripheralDevicePubSub.packageManagerPlayoutContext}_${studioId}_${deviceId}`, + { studioId, deviceId }, + setupExpectedPackagesPublicationObservers, + manipulateExpectedPackagesPublicationData, + pub, + 500 // ms, wait this time before sending an update + ) } ) diff --git a/meteor/server/publications/partInstancesUI/publication.ts b/meteor/server/publications/partInstancesUI/publication.ts index 553dea9808..8a16d6af5f 100644 --- a/meteor/server/publications/partInstancesUI/publication.ts +++ b/meteor/server/publications/partInstancesUI/publication.ts @@ -9,8 +9,6 @@ import { } from '../../lib/customPublication' import { logger } from '../../logging' import { CustomCollectionName, MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { resolveCredentials } from '../../security/lib/credentials' -import { NoSecurityReadAccess } from '../../security/noSecurity' import { ContentCache, PartInstanceOmitedFields, createReactiveContentCache } from './reactiveContentCache' import { ReadonlyDeep } from 'type-fest' import { RundownPlaylists } from '../../collections' @@ -28,6 +26,7 @@ import { modifyPartInstanceForQuickLoop, stringsToIndexLookup, } from '../lib/quickLoop' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../../security/securityVerify' interface UIPartInstancesArgs { readonly playlistActivationId: RundownPlaylistActivationId @@ -206,23 +205,24 @@ meteorCustomPublish( async function (pub, playlistActivationId: RundownPlaylistActivationId | null) { check(playlistActivationId, Match.Maybe(String)) - const credentials = await resolveCredentials({ userId: this.userId, token: undefined }) - - if (playlistActivationId && (!credentials || NoSecurityReadAccess.any())) { - await setUpCollectionOptimizedObserver< - Omit, - UIPartInstancesArgs, - UIPartInstancesState, - UIPartInstancesUpdateProps - >( - `pub_${MeteorPubSub.uiPartInstances}_${playlistActivationId}`, - { playlistActivationId }, - setupUIPartInstancesPublicationObservers, - manipulateUIPartInstancesPublicationData, - pub - ) - } else { - logger.warn(`Pub.uiPartInstances: Not allowed:"${playlistActivationId}"`) + triggerWriteAccessBecauseNoCheckNecessary() + + if (!playlistActivationId) { + logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistActivationId`) + return } + + await setUpCollectionOptimizedObserver< + Omit, + UIPartInstancesArgs, + UIPartInstancesState, + UIPartInstancesUpdateProps + >( + `pub_${MeteorPubSub.uiPartInstances}_${playlistActivationId}`, + { playlistActivationId }, + setupUIPartInstancesPublicationObservers, + manipulateUIPartInstancesPublicationData, + pub + ) } ) diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 31af1ed031..69bfc890be 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -9,9 +9,6 @@ import { } from '../../lib/customPublication' import { logger } from '../../logging' import { CustomCollectionName, MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { RundownPlaylistReadAccess } from '../../security/rundownPlaylist' -import { resolveCredentials } from '../../security/lib/credentials' -import { NoSecurityReadAccess } from '../../security/noSecurity' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { ContentCache, PartOmitedFields, createReactiveContentCache } from './reactiveContentCache' import { ReadonlyDeep } from 'type-fest' @@ -23,6 +20,7 @@ import { RundownsObserver } from '../lib/rundownsObserver' import { RundownContentObserver } from './rundownContentObserver' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { extractRanks, findMarkerPosition, modifyPartForQuickLoop, stringsToIndexLookup } from '../lib/quickLoop' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../../security/securityVerify' interface UIPartsArgs { readonly playlistId: RundownPlaylistId @@ -193,27 +191,24 @@ meteorCustomPublish( async function (pub, playlistId: RundownPlaylistId | null) { check(playlistId, String) - const credentials = await resolveCredentials({ userId: this.userId, token: undefined }) + triggerWriteAccessBecauseNoCheckNecessary() - if ( - !credentials || - NoSecurityReadAccess.any() || - (playlistId && (await RundownPlaylistReadAccess.rundownPlaylistContent(playlistId, credentials))) - ) { - await setUpCollectionOptimizedObserver< - Omit, - UIPartsArgs, - UIPartsState, - UIPartsUpdateProps - >( - `pub_${MeteorPubSub.uiParts}_${playlistId}`, - { playlistId }, - setupUIPartsPublicationObservers, - manipulateUIPartsPublicationData, - pub - ) - } else { + if (!playlistId) { logger.warn(`Pub.uiParts: Not allowed: "${playlistId}"`) + return } + + await setUpCollectionOptimizedObserver< + Omit, + UIPartsArgs, + UIPartsState, + UIPartsUpdateProps + >( + `pub_${MeteorPubSub.uiParts}_${playlistId}`, + { playlistId }, + setupUIPartsPublicationObservers, + manipulateUIPartsPublicationData, + pub + ) } ) diff --git a/meteor/server/publications/peripheralDevice.ts b/meteor/server/publications/peripheralDevice.ts index 1ead8e0e6d..a6add93fdc 100644 --- a/meteor/server/publications/peripheralDevice.ts +++ b/meteor/server/publications/peripheralDevice.ts @@ -1,38 +1,20 @@ -import { Meteor } from 'meteor/meteor' import { check, Match } from '../lib/check' -import { meteorPublish, AutoFillSelector } from './lib/lib' +import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { OrganizationReadAccess } from '../security/organization' -import { StudioReadAccess } from '../security/studio' import { MongoFieldSpecifierZeroes, MongoQuery } from '@sofie-automation/corelib/dist/mongo' -import { Credentials, ResolvedCredentials } from '../security/lib/credentials' -import { NoSecurityReadAccess } from '../security/noSecurity' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { MediaWorkFlows, MediaWorkFlowSteps, PeripheralDeviceCommands, PeripheralDevices } from '../collections' -import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' -import { MediaWorkFlowStep } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlowSteps' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PeripheralDevicePubSub } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { clone } from '@sofie-automation/corelib/dist/lib' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { checkAccessAndGetPeripheralDevice } from '../security/check' /* * This file contains publications for the peripheralDevices, such as playout-gateway, mos-gateway and package-manager */ -async function checkAccess(cred: Credentials | ResolvedCredentials | null, selector: MongoQuery) { - if (!selector) throw new Meteor.Error(400, 'selector argument missing') - return ( - !cred || - NoSecurityReadAccess.any() || - (selector._id && (await PeripheralDeviceReadAccess.peripheralDevice(selector._id, cred))) || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) - ) -} - const peripheralDeviceFields: MongoFieldSpecifierZeroes = { token: 0, secretSettings: 0, @@ -43,78 +25,67 @@ meteorPublish( async function (peripheralDeviceIds: PeripheralDeviceId[] | null, token: string | undefined) { check(peripheralDeviceIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (peripheralDeviceIds && peripheralDeviceIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) - // Add the requested filter + const selector: MongoQuery = {} if (peripheralDeviceIds) selector._id = { $in: peripheralDeviceIds } - if (await checkAccess(cred, selector)) { - const fields = clone(peripheralDeviceFields) - if (selector._id && token) { - // in this case, send the secretSettings: - delete fields.secretSettings - } - return PeripheralDevices.findWithCursor(selector, { - fields, - }) + const fields = clone(peripheralDeviceFields) + if (selector._id && token) { + // in this case, send the secretSettings: + delete fields.secretSettings } - return null + return PeripheralDevices.findWithCursor(selector, { + fields, + }) } ) meteorPublish(CorelibPubSub.peripheralDevicesAndSubDevices, async function (studioId: StudioId) { - const { cred, selector } = await AutoFillSelector.organizationId( - this.userId, - { studioId }, - undefined - ) - if (await checkAccess(cred, selector)) { - // TODO - this is not correctly reactive when changing the `studioId` property of a parent device - const parents = (await PeripheralDevices.findFetchAsync(selector, { projection: { _id: 1 } })) as Array< - Pick - > + triggerWriteAccessBecauseNoCheckNecessary() - return PeripheralDevices.findWithCursor( - { - $or: [ - { - parentDeviceId: { $in: parents.map((i) => i._id) }, - }, - selector, - ], - }, - { - fields: peripheralDeviceFields, - } - ) + const selector: MongoQuery = { + studioId, } - return null + + // TODO - this is not correctly reactive when changing the `studioId` property of a parent device + const parents = (await PeripheralDevices.findFetchAsync(selector, { projection: { _id: 1 } })) as Array< + Pick + > + + return PeripheralDevices.findWithCursor( + { + $or: [ + { + parentDeviceId: { $in: parents.map((i) => i._id) }, + }, + selector, + ], + }, + { + fields: peripheralDeviceFields, + } + ) }) meteorPublish( PeripheralDevicePubSub.peripheralDeviceCommands, async function (deviceId: PeripheralDeviceId, token: string | undefined) { - if (!deviceId) throw new Meteor.Error(400, 'deviceId argument missing') - check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - return PeripheralDeviceCommands.findWithCursor({ deviceId: deviceId }) - } - return null + await checkAccessAndGetPeripheralDevice(deviceId, token, this) + + return PeripheralDeviceCommands.findWithCursor({ deviceId: deviceId }) } ) -meteorPublish(MeteorPubSub.mediaWorkFlows, async function (token: string | undefined) { - const { cred, selector } = await AutoFillSelector.deviceId(this.userId, {}, token) - if (!cred || (await PeripheralDeviceReadAccess.peripheralDeviceContent(selector.deviceId, cred))) { - return MediaWorkFlows.findWithCursor(selector) - } - return null +meteorPublish(MeteorPubSub.mediaWorkFlows, async function (_token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + + return MediaWorkFlows.findWithCursor({}) }) -meteorPublish(MeteorPubSub.mediaWorkFlowSteps, async function (token: string | undefined) { - const { cred, selector } = await AutoFillSelector.deviceId(this.userId, {}, token) - if (!cred || (await PeripheralDeviceReadAccess.peripheralDeviceContent(selector.deviceId, cred))) { - return MediaWorkFlowSteps.findWithCursor(selector) - } - return null +meteorPublish(MeteorPubSub.mediaWorkFlowSteps, async function (_token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + + return MediaWorkFlowSteps.findWithCursor({}) }) diff --git a/meteor/server/publications/peripheralDeviceForDevice.ts b/meteor/server/publications/peripheralDeviceForDevice.ts index f98b37e6ff..cb45ec57ee 100644 --- a/meteor/server/publications/peripheralDeviceForDevice.ts +++ b/meteor/server/publications/peripheralDeviceForDevice.ts @@ -1,5 +1,3 @@ -import { Meteor } from 'meteor/meteor' -import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { PeripheralDevice, PeripheralDeviceCategory } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { PeripheralDevices, Studios } from '../collections' @@ -26,6 +24,7 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import { checkAccessAndGetPeripheralDevice } from '../security/check' interface PeripheralDeviceForDeviceArgs { readonly deviceId: PeripheralDeviceId @@ -207,26 +206,22 @@ meteorCustomPublish( async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) - - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') - - const studioId = peripheralDevice.studioId - if (!studioId) return - - await setUpOptimizedObserverArray< - PeripheralDeviceForDevice, - PeripheralDeviceForDeviceArgs, - PeripheralDeviceForDeviceState, - PeripheralDeviceForDeviceUpdateProps - >( - `${PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice}_${deviceId}`, - { deviceId }, - setupPeripheralDevicePublicationObservers, - manipulatePeripheralDevicePublicationData, - pub - ) - } + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) + + const studioId = peripheralDevice.studioId + if (!studioId) return + + await setUpOptimizedObserverArray< + PeripheralDeviceForDevice, + PeripheralDeviceForDeviceArgs, + PeripheralDeviceForDeviceState, + PeripheralDeviceForDeviceUpdateProps + >( + `${PeripheralDevicePubSubCollectionsNames.peripheralDeviceForDevice}_${deviceId}`, + { deviceId }, + setupPeripheralDevicePublicationObservers, + manipulatePeripheralDevicePublicationData, + pub + ) } ) diff --git a/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts b/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts index 8661244883..8942d5f75d 100644 --- a/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/bucket/publication.ts @@ -20,11 +20,7 @@ import { TriggerUpdate, SetupObserversResult, } from '../../../lib/customPublication' -import { logger } from '../../../logging' -import { resolveCredentials } from '../../../security/lib/credentials' -import { NoSecurityReadAccess } from '../../../security/noSecurity' import { BucketContentCache, createReactiveContentCache } from './bucketContentCache' -import { StudioReadAccess } from '../../../security/studio' import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' import { addItemsWithDependenciesChangesToChangedSet, @@ -39,8 +35,8 @@ import { import { BucketContentObserver } from './bucketContentObserver' import { regenerateForBucketActionIds, regenerateForBucketAdLibIds } from './regenerateForItem' import { PieceContentStatusStudio } from '../checkPieceContentStatus' -import { BucketSecurity } from '../../../security/buckets' import { check } from 'meteor/check' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../../../security/securityVerify' interface UIBucketContentStatusesArgs { readonly studioId: StudioId @@ -250,30 +246,20 @@ meteorCustomPublish( check(studioId, String) check(bucketId, String) - const cred = await resolveCredentials({ userId: this.userId, token: undefined }) + triggerWriteAccessBecauseNoCheckNecessary() - if ( - NoSecurityReadAccess.any() || - (studioId && - bucketId && - (await StudioReadAccess.studioContent(studioId, cred)) && - (await BucketSecurity.allowReadAccess(cred, bucketId))) - ) { - await setUpCollectionOptimizedObserver< - UIBucketContentStatus, - UIBucketContentStatusesArgs, - UIBucketContentStatusesState, - UIBucketContentStatusesUpdateProps - >( - `pub_${MeteorPubSub.uiBucketContentStatuses}_${studioId}_${bucketId}`, - { studioId, bucketId }, - setupUIBucketContentStatusesPublicationObservers, - manipulateUIBucketContentStatusesPublicationData, - pub, - 100 - ) - } else { - logger.warn(`Pub.${CustomCollectionName.UIBucketContentStatuses}: Not allowed: "${studioId}" "${bucketId}"`) - } + await setUpCollectionOptimizedObserver< + UIBucketContentStatus, + UIBucketContentStatusesArgs, + UIBucketContentStatusesState, + UIBucketContentStatusesUpdateProps + >( + `pub_${MeteorPubSub.uiBucketContentStatuses}_${studioId}_${bucketId}`, + { studioId, bucketId }, + setupUIBucketContentStatusesPublicationObservers, + manipulateUIBucketContentStatusesPublicationData, + pub, + 100 + ) } ) diff --git a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts index 1b20d6de6a..a190378eac 100644 --- a/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts +++ b/meteor/server/publications/pieceContentStatusUI/rundown/publication.ts @@ -32,9 +32,6 @@ import { TriggerUpdate, } from '../../../lib/customPublication' import { logger } from '../../../logging' -import { resolveCredentials } from '../../../security/lib/credentials' -import { NoSecurityReadAccess } from '../../../security/noSecurity' -import { RundownPlaylistReadAccess } from '../../../security/rundownPlaylist' import { ContentCache, PartInstanceFields, createReactiveContentCache } from './reactiveContentCache' import { RundownContentObserver } from './rundownContentObserver' import { RundownsObserver } from '../../lib/rundownsObserver' @@ -59,6 +56,7 @@ import { import { PieceContentStatusStudio } from '../checkPieceContentStatus' import { check, Match } from 'meteor/check' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../../../security/securityVerify' interface UIPieceContentStatusesArgs { readonly rundownPlaylistId: RundownPlaylistId @@ -476,29 +474,25 @@ meteorCustomPublish( async function (pub, rundownPlaylistId: RundownPlaylistId | null) { check(rundownPlaylistId, Match.Maybe(String)) - const cred = await resolveCredentials({ userId: this.userId, token: undefined }) - - if ( - rundownPlaylistId && - (!cred || - NoSecurityReadAccess.any() || - (await RundownPlaylistReadAccess.rundownPlaylistContent(rundownPlaylistId, cred))) - ) { - await setUpCollectionOptimizedObserver< - UIPieceContentStatus, - UIPieceContentStatusesArgs, - UIPieceContentStatusesState, - UIPieceContentStatusesUpdateProps - >( - `pub_${MeteorPubSub.uiPieceContentStatuses}_${rundownPlaylistId}`, - { rundownPlaylistId }, - setupUIPieceContentStatusesPublicationObservers, - manipulateUIPieceContentStatusesPublicationData, - pub, - 100 - ) - } else { - logger.warn(`Pub.${CustomCollectionName.UIPieceContentStatuses}: Not allowed: "${rundownPlaylistId}"`) + triggerWriteAccessBecauseNoCheckNecessary() + + if (!rundownPlaylistId) { + logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistId`) + return } + + await setUpCollectionOptimizedObserver< + UIPieceContentStatus, + UIPieceContentStatusesArgs, + UIPieceContentStatusesState, + UIPieceContentStatusesUpdateProps + >( + `pub_${MeteorPubSub.uiPieceContentStatuses}_${rundownPlaylistId}`, + { rundownPlaylistId }, + setupUIPieceContentStatusesPublicationObservers, + manipulateUIPieceContentStatusesPublicationData, + pub, + 100 + ) } ) diff --git a/meteor/server/publications/rundown.ts b/meteor/server/publications/rundown.ts index f939a9baff..e4be7f6dac 100644 --- a/meteor/server/publications/rundown.ts +++ b/meteor/server/publications/rundown.ts @@ -1,16 +1,12 @@ import { Meteor } from 'meteor/meteor' -import { meteorPublish, AutoFillSelector } from './lib/lib' +import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { MongoFieldSpecifierZeroes, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece' -import { RundownReadAccess } from '../security/rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { Piece } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { NoSecurityReadAccess } from '../security/noSecurity' -import { OrganizationReadAccess } from '../security/organization' -import { StudioReadAccess } from '../security/studio' import { check, Match } from 'meteor/check' import { FindOptions } from '@sofie-automation/meteor-lib/dist/collections/lib' import { @@ -20,7 +16,6 @@ import { NrcsIngestDataCache, PartInstances, Parts, - PeripheralDevices, PieceInstances, Pieces, RundownBaselineAdLibActions, @@ -44,58 +39,52 @@ import { import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { PeripheralDevicePubSub } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' -import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece' import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { PieceLifespan } from '@sofie-automation/blueprints-integration' -import { resolveCredentials } from '../security/lib/credentials' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { checkAccessAndGetPeripheralDevice } from '../security/check' -meteorPublish(PeripheralDevicePubSub.rundownsForDevice, async function (deviceId, token: string | undefined) { - check(deviceId, String) - check(token, String) - - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) - - // Future: this should be reactive to studioId changes, but this matches how the other *ForDevice publications behave - - // The above auth check may return nothing when security is disabled, but we need the return value - const resolvedCred = cred?.device ? cred : await resolveCredentials({ userId: this.userId, token }) - if (!resolvedCred || !resolvedCred.device) - throw new Meteor.Error(403, 'Publication can only be used by authorized PeripheralDevices') +meteorPublish( + PeripheralDevicePubSub.rundownsForDevice, + async function (deviceId: PeripheralDeviceId, token: string | undefined) { + check(deviceId, String) + check(token, String) - // No studio, then no rundowns - if (!resolvedCred.device.studioId) return null + // Future: this should be reactive to studioId changes, but this matches how the other *ForDevice publications behave - selector.studioId = resolvedCred.device.studioId + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - const modifier: FindOptions = { - fields: { - privateData: 0, - }, - } + // No studio, then no rundowns + if (!peripheralDevice.studioId) return null - if (NoSecurityReadAccess.any() || (await StudioReadAccess.studioContent(selector.studioId, resolvedCred))) { - return Rundowns.findWithCursor(selector, modifier) + return Rundowns.findWithCursor( + { + studioId: peripheralDevice.studioId, + }, + { + fields: { + privateData: 0, + }, + } + ) } - return null -}) +) meteorPublish( CorelibPubSub.rundownsInPlaylists, - async function (playlistIds: RundownPlaylistId[], token: string | undefined) { + async function (playlistIds: RundownPlaylistId[], _token: string | undefined) { check(playlistIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (playlistIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId( - this.userId, - { - playlistId: { $in: playlistIds }, - }, - token - ) + const selector: MongoQuery = { + playlistId: { $in: playlistIds }, + } const modifier: FindOptions = { fields: { @@ -103,33 +92,21 @@ meteorPublish( }, } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || - (selector._id && (await RundownReadAccess.rundown(selector._id, cred))) - ) { - return Rundowns.findWithCursor(selector, modifier) - } - return null + return Rundowns.findWithCursor(selector, modifier) } ) meteorPublish( CorelibPubSub.rundownsWithShowStyleBases, - async function (showStyleBaseIds: ShowStyleBaseId[], token: string | undefined) { + async function (showStyleBaseIds: ShowStyleBaseId[], _token: string | undefined) { check(showStyleBaseIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (showStyleBaseIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId( - this.userId, - { - showStyleBaseId: { $in: showStyleBaseIds }, - }, - token - ) + const selector: MongoQuery = { + showStyleBaseId: { $in: showStyleBaseIds }, + } const modifier: FindOptions = { fields: { @@ -137,25 +114,17 @@ meteorPublish( }, } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || - (selector._id && (await RundownReadAccess.rundown(selector._id, cred))) - ) { - return Rundowns.findWithCursor(selector, modifier) - } - return null + return Rundowns.findWithCursor(selector, modifier) } ) meteorPublish( CorelibPubSub.segments, - async function (rundownIds: RundownId[], filter: { omitHidden?: boolean } | undefined, token: string | undefined) { + async function (rundownIds: RundownId[], filter: { omitHidden?: boolean } | undefined, _token: string | undefined) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { @@ -163,26 +132,22 @@ meteorPublish( } if (filter?.omitHidden) selector.isHidden = { $ne: true } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return Segments.findWithCursor(selector, { - fields: { - privateData: 0, - }, - }) - } - return null + return Segments.findWithCursor(selector, { + fields: { + privateData: 0, + }, + }) } ) meteorPublish( CorelibPubSub.parts, - async function (rundownIds: RundownId[], segmentIds: SegmentId[] | null, token: string | undefined) { + async function (rundownIds: RundownId[], segmentIds: SegmentId[] | null, _token: string | undefined) { check(rundownIds, Array) check(segmentIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null if (segmentIds && segmentIds.length === 0) return null @@ -198,15 +163,7 @@ meteorPublish( } if (segmentIds) selector.segmentId = { $in: segmentIds } - if ( - NoSecurityReadAccess.any() || - (selector.rundownId && - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token }))) // || - // (selector._id && await RundownReadAccess.pieces(selector._id, { userId: this.userId, token })) // TODO - the types for this did not match - ) { - return Parts.findWithCursor(selector, modifier) - } - return null + return Parts.findWithCursor(selector, modifier) } ) meteorPublish( @@ -214,11 +171,13 @@ meteorPublish( async function ( rundownIds: RundownId[], playlistActivationId: RundownPlaylistActivationId | null, - token: string | undefined + _token: string | undefined ) { check(rundownIds, Array) check(playlistActivationId, Match.Maybe(String)) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0 || !playlistActivationId) return null const modifier: FindOptions = { @@ -234,13 +193,7 @@ meteorPublish( } if (playlistActivationId) selector.playlistActivationId = playlistActivationId - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PartInstances.findWithCursor(selector, modifier) - } - return null + return PartInstances.findWithCursor(selector, modifier) } ) meteorPublish( @@ -248,10 +201,12 @@ meteorPublish( async function ( rundownIds: RundownId[], playlistActivationId: RundownPlaylistActivationId | null, - token: string | undefined + _token: string | undefined ) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { @@ -261,20 +216,14 @@ meteorPublish( } if (playlistActivationId) selector.playlistActivationId = playlistActivationId - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PartInstances.findWithCursor(selector, { - fields: literal>({ - // @ts-expect-error Mongo typings aren't clever enough yet - 'part.privateData': 0, - isTaken: 0, - timings: 0, - }), - }) - } - return null + return PartInstances.findWithCursor(selector, { + fields: literal>({ + // @ts-expect-error Mongo typings aren't clever enough yet + 'part.privateData': 0, + isTaken: 0, + timings: 0, + }), + }) } ) @@ -285,10 +234,12 @@ const piecesSubFields: MongoFieldSpecifierZeroes = { meteorPublish( CorelibPubSub.pieces, - async function (rundownIds: RundownId[], partIds: PartId[] | null, token: string | undefined) { + async function (rundownIds: RundownId[], partIds: PartId[] | null, _token: string | undefined) { check(rundownIds, Array) check(partIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (partIds && partIds.length === 0) return null @@ -297,15 +248,9 @@ meteorPublish( } if (partIds) selector.startPartId = { $in: partIds } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.startRundownId, { userId: this.userId, token })) - ) { - return Pieces.findWithCursor(selector, { - fields: piecesSubFields, - }) - } - return null + return Pieces.findWithCursor(selector, { + fields: piecesSubFields, + }) } ) @@ -317,8 +262,7 @@ meteorPublish( rundownIdsBefore: RundownId[], _token: string | undefined ) { - // TODO - Fix this when security is enabled - if (!NoSecurityReadAccess.any()) return null + triggerWriteAccessBecauseNoCheckNecessary() const selector: MongoQuery = { invalid: { @@ -358,31 +302,26 @@ const adlibPiecesSubFields: MongoFieldSpecifierZeroes = { timelineObjectsString: 0, } -meteorPublish(CorelibPubSub.adLibPieces, async function (rundownIds: RundownId[], token: string | undefined) { +meteorPublish(CorelibPubSub.adLibPieces, async function (rundownIds: RundownId[], _token: string | undefined) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { rundownId: { $in: rundownIds }, } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return AdLibPieces.findWithCursor(selector, { - fields: adlibPiecesSubFields, - }) - } - return null + return AdLibPieces.findWithCursor(selector, { + fields: adlibPiecesSubFields, + }) }) meteorPublish(MeteorPubSub.adLibPiecesForPart, async function (partId: PartId, sourceLayerIds: string[]) { - if (!partId) throw new Meteor.Error(400, 'partId argument missing') - if (!sourceLayerIds) throw new Meteor.Error(400, 'sourceLayerIds argument missing') + check(partId, String) + check(sourceLayerIds, Array) - // Future: This needs some thought for a security enabled environment - if (!NoSecurityReadAccess.any()) return null + triggerWriteAccessBecauseNoCheckNecessary() return AdLibPieces.findWithCursor( { @@ -411,11 +350,13 @@ meteorPublish( onlyPlayingAdlibsOrWithTags?: boolean } | undefined, - token: string | undefined + _token: string | undefined ) { check(rundownIds, Array) check(partInstanceIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (rundownIds.length === 0) return null if (partInstanceIds && partInstanceIds.length === 0) return null @@ -464,15 +405,9 @@ meteorPublish( ] } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PieceInstances.findWithCursor(selector, { - fields: pieceInstanceFields, - }) - } - return null + return PieceInstances.findWithCursor(selector, { + fields: pieceInstanceFields, + }) } ) @@ -481,10 +416,12 @@ meteorPublish( async function ( rundownIds: RundownId[], playlistActivationId: RundownPlaylistActivationId | null, - token: string | undefined + _token: string | undefined ) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { @@ -494,81 +431,62 @@ meteorPublish( } if (playlistActivationId) selector.playlistActivationId = playlistActivationId - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return PieceInstances.findWithCursor(selector, { - fields: literal>({ - ...pieceInstanceFields, - plannedStartedPlayback: 0, - plannedStoppedPlayback: 0, - }), - }) - } - return null + return PieceInstances.findWithCursor(selector, { + fields: literal>({ + ...pieceInstanceFields, + plannedStartedPlayback: 0, + plannedStoppedPlayback: 0, + }), + }) } ) meteorPublish( PeripheralDevicePubSub.expectedPlayoutItemsForDevice, async function (deviceId: PeripheralDeviceId, token: string | undefined) { - if (!deviceId) throw new Meteor.Error(400, 'deviceId argument missing') check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error(`PeripheralDevice "${deviceId}" not found`) + const studioId = peripheralDevice.studioId + if (!studioId) return null - const studioId = peripheralDevice.studioId - if (!studioId) return null - - return ExpectedPlayoutItems.findWithCursor({ studioId }) - } - return null + return ExpectedPlayoutItems.findWithCursor({ studioId }) } ) // Note: this publication is for dev purposes only: meteorPublish( CorelibPubSub.ingestDataCache, - async function (selector: MongoQuery, token: string | undefined) { + async function (selector: MongoQuery, _token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return NrcsIngestDataCache.findWithCursor(selector, modifier) - } - return null + + return NrcsIngestDataCache.findWithCursor(selector, modifier) } ) meteorPublish( CorelibPubSub.rundownBaselineAdLibPieces, - async function (rundownIds: RundownId[], token: string | undefined) { + async function (rundownIds: RundownId[], _token: string | undefined) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { rundownId: { $in: rundownIds }, } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return RundownBaselineAdLibPieces.findWithCursor(selector, { - fields: { - timelineObjectsString: 0, - privateData: 0, - }, - }) - } - return null + return RundownBaselineAdLibPieces.findWithCursor(selector, { + fields: { + timelineObjectsString: 0, + privateData: 0, + }, + }) } ) @@ -576,31 +494,26 @@ const adlibActionSubFields: MongoFieldSpecifierZeroes = { privateData: 0, } -meteorPublish(CorelibPubSub.adLibActions, async function (rundownIds: RundownId[], token: string | undefined) { +meteorPublish(CorelibPubSub.adLibActions, async function (rundownIds: RundownId[], _token: string | undefined) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { rundownId: { $in: rundownIds }, } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return AdLibActions.findWithCursor(selector, { - fields: adlibActionSubFields, - }) - } - return null + return AdLibActions.findWithCursor(selector, { + fields: adlibActionSubFields, + }) }) meteorPublish(MeteorPubSub.adLibActionsForPart, async function (partId: PartId, sourceLayerIds: string[]) { - if (!partId) throw new Meteor.Error(400, 'partId argument missing') - if (!sourceLayerIds) throw new Meteor.Error(400, 'sourceLayerIds argument missing') + check(partId, String) + check(sourceLayerIds, Array) - // Future: This needs some thought for a security enabled environment - if (!NoSecurityReadAccess.any()) return null + triggerWriteAccessBecauseNoCheckNecessary() return AdLibActions.findWithCursor( { @@ -615,23 +528,19 @@ meteorPublish(MeteorPubSub.adLibActionsForPart, async function (partId: PartId, meteorPublish( CorelibPubSub.rundownBaselineAdLibActions, - async function (rundownIds: RundownId[], token: string | undefined) { + async function (rundownIds: RundownId[], _token: string | undefined) { check(rundownIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (rundownIds.length === 0) return null const selector: MongoQuery = { rundownId: { $in: rundownIds }, } - if ( - NoSecurityReadAccess.any() || - (await RundownReadAccess.rundownContent(selector.rundownId, { userId: this.userId, token })) - ) { - return RundownBaselineAdLibActions.findWithCursor(selector, { - fields: adlibActionSubFields, - }) - } - return null + return RundownBaselineAdLibActions.findWithCursor(selector, { + fields: adlibActionSubFields, + }) } ) diff --git a/meteor/server/publications/rundownPlaylist.ts b/meteor/server/publications/rundownPlaylist.ts index 89378b1587..c52efc85d3 100644 --- a/meteor/server/publications/rundownPlaylist.ts +++ b/meteor/server/publications/rundownPlaylist.ts @@ -1,59 +1,40 @@ -import { RundownPlaylistReadAccess } from '../security/rundownPlaylist' -import { meteorPublish, AutoFillSelector } from './lib/lib' -import { StudioReadAccess } from '../security/studio' -import { OrganizationReadAccess } from '../security/organization' -import { NoSecurityReadAccess } from '../security/noSecurity' -import { isProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import { meteorPublish } from './lib/lib' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { RundownPlaylists } from '../collections' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { RundownPlaylistId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { resolveCredentials } from '../security/lib/credentials' import { check, Match } from '../lib/check' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' meteorPublish( CorelibPubSub.rundownPlaylists, async function ( rundownPlaylistIds: RundownPlaylistId[] | null, studioIds: StudioId[] | null, - token: string | undefined + _token: string | undefined ) { check(rundownPlaylistIds, Match.Maybe(Array)) check(studioIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (rundownPlaylistIds && rundownPlaylistIds.length === 0) return null if (studioIds && studioIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) - // Add the requested filter + const selector: MongoQuery = {} if (rundownPlaylistIds) selector._id = { $in: rundownPlaylistIds } if (studioIds) selector.studioId = { $in: studioIds } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector.studioId && (await StudioReadAccess.studioContent(selector.studioId, cred))) || - (isProtectedString(selector._id) && (await RundownPlaylistReadAccess.rundownPlaylist(selector._id, cred))) - ) { - return RundownPlaylists.findWithCursor(selector) - } - return null + return RundownPlaylists.findWithCursor(selector) } ) meteorPublish(MeteorPubSub.rundownPlaylistForStudio, async function (studioId: StudioId, isActive: boolean) { - if (!NoSecurityReadAccess.any()) { - const cred = await resolveCredentials({ userId: this.userId }) - if (!cred) return null - - if (!(await StudioReadAccess.studioContent(studioId, cred))) return null - } + triggerWriteAccessBecauseNoCheckNecessary() const selector: MongoQuery = { studioId, diff --git a/meteor/server/publications/segmentPartNotesUI/publication.ts b/meteor/server/publications/segmentPartNotesUI/publication.ts index 5ab2a86a44..d01a55c66a 100644 --- a/meteor/server/publications/segmentPartNotesUI/publication.ts +++ b/meteor/server/publications/segmentPartNotesUI/publication.ts @@ -16,9 +16,6 @@ import { TriggerUpdate, } from '../../lib/customPublication' import { logger } from '../../logging' -import { resolveCredentials } from '../../security/lib/credentials' -import { NoSecurityReadAccess } from '../../security/noSecurity' -import { RundownPlaylistReadAccess } from '../../security/rundownPlaylist' import { ContentCache, createReactiveContentCache, @@ -33,6 +30,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { generateNotesForSegment } from './generateNotesForSegment' import { RundownPlaylists } from '../../collections' import { check, Match } from 'meteor/check' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../../security/securityVerify' interface UISegmentPartNotesArgs { readonly playlistId: RundownPlaylistId @@ -215,29 +213,25 @@ meteorCustomPublish( async function (pub, playlistId: RundownPlaylistId | null) { check(playlistId, Match.Maybe(String)) - const cred = await resolveCredentials({ userId: this.userId, token: undefined }) - - if ( - playlistId && - (!cred || - NoSecurityReadAccess.any() || - (await RundownPlaylistReadAccess.rundownPlaylistContent(playlistId, cred))) - ) { - await setUpCollectionOptimizedObserver< - UISegmentPartNote, - UISegmentPartNotesArgs, - UISegmentPartNotesState, - UISegmentPartNotesUpdateProps - >( - `pub_${MeteorPubSub.uiSegmentPartNotes}_${playlistId}`, - { playlistId }, - setupUISegmentPartNotesPublicationObservers, - manipulateUISegmentPartNotesPublicationData, - pub, - 100 - ) - } else { - logger.warn(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not allowed: "${playlistId}"`) + triggerWriteAccessBecauseNoCheckNecessary() + + if (!playlistId) { + logger.info(`Pub.${CustomCollectionName.UISegmentPartNotes}: Not playlistId`) + return } + + await setUpCollectionOptimizedObserver< + UISegmentPartNote, + UISegmentPartNotesArgs, + UISegmentPartNotesState, + UISegmentPartNotesUpdateProps + >( + `pub_${MeteorPubSub.uiSegmentPartNotes}_${playlistId}`, + { playlistId }, + setupUISegmentPartNotesPublicationObservers, + manipulateUISegmentPartNotesPublicationData, + pub, + 100 + ) } ) diff --git a/meteor/server/publications/showStyle.ts b/meteor/server/publications/showStyle.ts index 99b3099e50..ee3cbf0803 100644 --- a/meteor/server/publications/showStyle.ts +++ b/meteor/server/publications/showStyle.ts @@ -1,41 +1,31 @@ -import { meteorPublish, AutoFillSelector } from './lib/lib' +import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { ShowStyleReadAccess } from '../security/showStyle' -import { OrganizationReadAccess } from '../security/organization' -import { NoSecurityReadAccess } from '../security/noSecurity' import { RundownLayouts, ShowStyleBases, ShowStyleVariants, TriggeredActions } from '../collections' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { DBTriggeredActions } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { ShowStyleBaseId, ShowStyleVariantId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { check, Match } from '../lib/check' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' meteorPublish( CorelibPubSub.showStyleBases, - async function (showStyleBaseIds: ShowStyleBaseId[] | null, token: string | undefined) { + async function (showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) { check(showStyleBaseIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (showStyleBaseIds && showStyleBaseIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) - // Add the requested filter + const selector: MongoQuery = {} if (showStyleBaseIds) selector._id = { $in: showStyleBaseIds } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector._id && (await ShowStyleReadAccess.showStyleBase(selector.id, cred))) - ) { - return ShowStyleBases.findWithCursor(selector) - } - return null + return ShowStyleBases.findWithCursor(selector) } ) @@ -44,59 +34,51 @@ meteorPublish( async function ( showStyleBaseIds: ShowStyleBaseId[] | null, showStyleVariantIds: ShowStyleVariantId[] | null, - token: string | undefined + _token: string | undefined ) { check(showStyleBaseIds, Match.Maybe(Array)) check(showStyleVariantIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (showStyleBaseIds && showStyleBaseIds.length === 0) return null if (showStyleVariantIds && showStyleVariantIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, {}, token) - // Add the requested filter + const selector: MongoQuery = {} if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } if (showStyleVariantIds) selector._id = { $in: showStyleVariantIds } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.showStyleBaseId && (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) || - (selector._id && (await ShowStyleReadAccess.showStyleVariant(selector._id, cred))) - ) { - return ShowStyleVariants.findWithCursor(selector) - } - return null + return ShowStyleVariants.findWithCursor(selector) } ) meteorPublish( MeteorPubSub.rundownLayouts, - async function (showStyleBaseIds: ShowStyleBaseId[] | null, token: string | undefined) { + async function (showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) { check(showStyleBaseIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (showStyleBaseIds && showStyleBaseIds.length === 0) return null - const selector0: MongoQuery = {} - if (showStyleBaseIds) selector0.showStyleBaseId = { $in: showStyleBaseIds } - - const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) + const selector: MongoQuery = {} + if (showStyleBaseIds) selector.showStyleBaseId = { $in: showStyleBaseIds } - if (!cred || (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) { - return RundownLayouts.findWithCursor(selector) - } - return null + return RundownLayouts.findWithCursor(selector) } ) meteorPublish( MeteorPubSub.triggeredActions, - async function (showStyleBaseIds: ShowStyleBaseId[] | null, token: string | undefined) { + async function (showStyleBaseIds: ShowStyleBaseId[] | null, _token: string | undefined) { check(showStyleBaseIds, Match.Maybe(Array)) - const selector0: MongoQuery = + triggerWriteAccessBecauseNoCheckNecessary() + + const selector: MongoQuery = showStyleBaseIds && showStyleBaseIds.length > 0 ? { $or: [ @@ -110,15 +92,6 @@ meteorPublish( } : { showStyleBaseId: null } - const { cred, selector } = await AutoFillSelector.showStyleBaseId(this.userId, selector0, token) - - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.showStyleBaseId && (await ShowStyleReadAccess.showStyleBaseContent(selector, cred))) - ) { - return TriggeredActions.findWithCursor(selector) - } - return null + return TriggeredActions.findWithCursor(selector) } ) diff --git a/meteor/server/publications/showStyleUI.ts b/meteor/server/publications/showStyleUI.ts index 68309db7d9..2b6ce26ccc 100644 --- a/meteor/server/publications/showStyleUI.ts +++ b/meteor/server/publications/showStyleUI.ts @@ -12,13 +12,9 @@ import { setUpOptimizedObserverArray, TriggerUpdate, } from '../lib/customPublication' -import { logger } from '../logging' -import { NoSecurityReadAccess } from '../security/noSecurity' -import { OrganizationReadAccess } from '../security/organization' -import { ShowStyleReadAccess } from '../security/showStyle' import { ShowStyleBases } from '../collections' -import { AutoFillSelector } from './lib/lib' import { check } from 'meteor/check' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' interface UIShowStyleBaseArgs { readonly showStyleBaseId: ShowStyleBaseId @@ -92,33 +88,19 @@ meteorCustomPublish( async function (pub, showStyleBaseId: ShowStyleBaseId) { check(showStyleBaseId, String) - const { cred, selector } = await AutoFillSelector.organizationId( - this.userId, - { _id: showStyleBaseId }, - undefined - ) + triggerWriteAccessBecauseNoCheckNecessary() - if ( - !cred || - NoSecurityReadAccess.any() || - (selector.organizationId && - (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) || - (selector._id && (await ShowStyleReadAccess.showStyleBase(selector._id, cred))) - ) { - await setUpOptimizedObserverArray< - UIShowStyleBase, - UIShowStyleBaseArgs, - UIShowStyleBaseState, - UIShowStyleBaseUpdateProps - >( - `pub_${MeteorPubSub.uiShowStyleBase}_${showStyleBaseId}`, - { showStyleBaseId }, - setupUIShowStyleBasePublicationObservers, - manipulateUIShowStyleBasePublicationData, - pub - ) - } else { - logger.warn(`Pub.${CustomCollectionName.UIShowStyleBase}: Not allowed: "${showStyleBaseId}"`) - } + await setUpOptimizedObserverArray< + UIShowStyleBase, + UIShowStyleBaseArgs, + UIShowStyleBaseState, + UIShowStyleBaseUpdateProps + >( + `pub_${MeteorPubSub.uiShowStyleBase}_${showStyleBaseId}`, + { showStyleBaseId }, + setupUIShowStyleBasePublicationObservers, + manipulateUIShowStyleBasePublicationData, + pub + ) } ) diff --git a/meteor/server/publications/studio.ts b/meteor/server/publications/studio.ts index 08002e6938..633f2bd393 100644 --- a/meteor/server/publications/studio.ts +++ b/meteor/server/publications/studio.ts @@ -1,13 +1,9 @@ import { Meteor } from 'meteor/meteor' import { check, Match } from '../lib/check' -import { meteorPublish, AutoFillSelector } from './lib/lib' +import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { getActiveRoutes, getRoutedMappings } from '@sofie-automation/meteor-lib/dist/collections/Studios' -import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' -import { StudioReadAccess } from '../security/studio' -import { OrganizationReadAccess } from '../security/organization' -import { NoSecurityReadAccess } from '../security/noSecurity' import { CustomPublish, meteorCustomPublish, @@ -26,7 +22,6 @@ import { ExternalMessageQueue, PackageContainerStatuses, PackageInfos, - PeripheralDevices, Studios, } from '../collections' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' @@ -37,94 +32,85 @@ import { PeripheralDevicePubSub, PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { checkAccessAndGetPeripheralDevice } from '../security/check' +import { assertConnectionHasOneOfPermissions } from '../security/auth' -meteorPublish(CorelibPubSub.studios, async function (studioIds: StudioId[] | null, token: string | undefined) { +meteorPublish(CorelibPubSub.studios, async function (studioIds: StudioId[] | null, _token: string | undefined) { check(studioIds, Match.Maybe(Array)) + triggerWriteAccessBecauseNoCheckNecessary() + // If values were provided, they must have values if (studioIds && studioIds.length === 0) return null - const { cred, selector } = await AutoFillSelector.organizationId(this.userId, {}, token) - // Add the requested filter + const selector: MongoQuery = {} if (studioIds) selector._id = { $in: studioIds } - if ( - !cred || - NoSecurityReadAccess.any() || - (selector._id && (await StudioReadAccess.studio(selector._id, cred))) || - (selector.organizationId && (await OrganizationReadAccess.organizationContent(selector.organizationId, cred))) - ) { - return Studios.findWithCursor(selector) - } - return null + return Studios.findWithCursor(selector) }) meteorPublish( CorelibPubSub.externalMessageQueue, - async function (selector: MongoQuery, token: string | undefined) { + async function (selector: MongoQuery, _token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() + if (!selector) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, } - if (await StudioReadAccess.studioContent(selector.studioId, { userId: this.userId, token })) { - return ExternalMessageQueue.findWithCursor(selector, modifier) - } - return null + + return ExternalMessageQueue.findWithCursor(selector, modifier) } ) -meteorPublish(CorelibPubSub.expectedPackages, async function (studioIds: StudioId[], token: string | undefined) { +meteorPublish(CorelibPubSub.expectedPackages, async function (studioIds: StudioId[], _token: string | undefined) { // Note: This differs from the expected packages sent to the Package Manager, instead @see PubSub.expectedPackagesForDevice check(studioIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (studioIds.length === 0) return null - if (await StudioReadAccess.studioContent(studioIds, { userId: this.userId, token })) { - return ExpectedPackages.findWithCursor({ - studioId: { $in: studioIds }, - }) - } - return null + return ExpectedPackages.findWithCursor({ + studioId: { $in: studioIds }, + }) }) meteorPublish( CorelibPubSub.expectedPackageWorkStatuses, - async function (studioIds: StudioId[], token: string | undefined) { + async function (studioIds: StudioId[], _token: string | undefined) { check(studioIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() if (studioIds.length === 0) return null - if (await StudioReadAccess.studioContent(studioIds, { userId: this.userId, token })) { - return ExpectedPackageWorkStatuses.findWithCursor({ - studioId: { $in: studioIds }, - }) - } - return null + return ExpectedPackageWorkStatuses.findWithCursor({ + studioId: { $in: studioIds }, + }) } ) meteorPublish( CorelibPubSub.packageContainerStatuses, - async function (studioIds: StudioId[], token: string | undefined) { + async function (studioIds: StudioId[], _token: string | undefined) { check(studioIds, Array) + triggerWriteAccessBecauseNoCheckNecessary() + if (studioIds.length === 0) return null - if (await StudioReadAccess.studioContent(studioIds, { userId: this.userId, token })) { - return PackageContainerStatuses.findWithCursor({ - studioId: { $in: studioIds }, - }) - } - return null + return PackageContainerStatuses.findWithCursor({ + studioId: { $in: studioIds }, + }) } ) -meteorPublish(CorelibPubSub.packageInfos, async function (deviceId: PeripheralDeviceId, token: string | undefined) { - if (!deviceId) throw new Meteor.Error(400, 'deviceId argument missing') +meteorPublish(CorelibPubSub.packageInfos, async function (deviceId: PeripheralDeviceId, _token: string | undefined) { + check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - return PackageInfos.findWithCursor({ deviceId }) - } - return null + triggerWriteAccessBecauseNoCheckNecessary() + + return PackageInfos.findWithCursor({ deviceId }) }) meteorCustomPublish( @@ -133,28 +119,24 @@ meteorCustomPublish( async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) - - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - const studioId = peripheralDevice.studioId - if (!studioId) return + const studioId = peripheralDevice.studioId + if (!studioId) return - await createObserverForMappingsPublication(pub, studioId) - } + await createObserverForMappingsPublication(pub, studioId) } ) meteorCustomPublish( MeteorPubSub.mappingsForStudio, PeripheralDevicePubSubCollectionsNames.studioMappings, - async function (pub, studioId: StudioId, token: string | undefined) { + async function (pub, studioId: StudioId, _token: string | undefined) { check(studioId, String) - if (await StudioReadAccess.studio(studioId, { userId: this.userId, token })) { - await createObserverForMappingsPublication(pub, studioId) - } + assertConnectionHasOneOfPermissions(this.connection, 'testing') + + await createObserverForMappingsPublication(pub, studioId) } ) diff --git a/meteor/server/publications/studioUI.ts b/meteor/server/publications/studioUI.ts index b8de6f1b7d..d91037896f 100644 --- a/meteor/server/publications/studioUI.ts +++ b/meteor/server/publications/studioUI.ts @@ -13,12 +13,9 @@ import { SetupObserversResult, TriggerUpdate, } from '../lib/customPublication' -import { logger } from '../logging' -import { resolveCredentials } from '../security/lib/credentials' -import { NoSecurityReadAccess } from '../security/noSecurity' -import { StudioReadAccess } from '../security/studio' import { Studios } from '../collections' import { check, Match } from 'meteor/check' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' interface UIStudioArgs { readonly studioId: StudioId | null @@ -131,18 +128,14 @@ meteorCustomPublish( async function (pub, studioId: StudioId | null) { check(studioId, Match.Maybe(String)) - const cred = await resolveCredentials({ userId: this.userId, token: undefined }) + triggerWriteAccessBecauseNoCheckNecessary() - if (!cred || NoSecurityReadAccess.any() || (studioId && (await StudioReadAccess.studio(studioId, cred)))) { - await setUpCollectionOptimizedObserver( - `pub_${MeteorPubSub.uiStudio}_${studioId}`, - { studioId }, - setupUIStudioPublicationObservers, - manipulateUIStudioPublicationData, - pub - ) - } else { - logger.warn(`Pub.${CustomCollectionName.UIStudio}: Not allowed: "${studioId}"`) - } + await setUpCollectionOptimizedObserver( + `pub_${MeteorPubSub.uiStudio}_${studioId}`, + { studioId }, + setupUIStudioPublicationObservers, + manipulateUIStudioPublicationData, + pub + ) } ) diff --git a/meteor/server/publications/system.ts b/meteor/server/publications/system.ts index 94a8969027..b1629b6425 100644 --- a/meteor/server/publications/system.ts +++ b/meteor/server/publications/system.ts @@ -1,76 +1,26 @@ -import { Meteor } from 'meteor/meteor' import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { SystemReadAccess } from '../security/system' -import { OrganizationReadAccess } from '../security/organization' -import { CoreSystem, Users } from '../collections' +import { CoreSystem } from '../collections' import { SYSTEM_ID } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { OrganizationId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' -meteorPublish(MeteorPubSub.coreSystem, async function (token: string | undefined) { - if (await SystemReadAccess.coreSystem({ userId: this.userId, token })) { - return CoreSystem.findWithCursor(SYSTEM_ID, { - fields: { - // Include only specific fields in the result documents: - _id: 1, - support: 1, - systemInfo: 1, - apm: 1, - name: 1, - logLevel: 1, - serviceMessages: 1, - blueprintId: 1, - cron: 1, - logo: 1, - evaluations: 1, - }, - }) - } - return null -}) - -meteorPublish(MeteorPubSub.loggedInUser, async function (token: string | undefined) { - const currentUserId = this.userId +meteorPublish(MeteorPubSub.coreSystem, async function (_token: string | undefined) { + triggerWriteAccessBecauseNoCheckNecessary() - if (!currentUserId) return null - if (await SystemReadAccess.currentUser(currentUserId, { userId: this.userId, token })) { - return Users.findWithCursor( - { - _id: currentUserId, - }, - { - fields: { - _id: 1, - username: 1, - emails: 1, - profile: 1, - organizationId: 1, - superAdmin: 1, - }, - } - ) - } - return null + return CoreSystem.findWithCursor(SYSTEM_ID, { + fields: { + // Include only specific fields in the result documents: + _id: 1, + support: 1, + systemInfo: 1, + apm: 1, + name: 1, + logLevel: 1, + serviceMessages: 1, + blueprintId: 1, + cron: 1, + logo: 1, + evaluations: 1, + }, + }) }) -meteorPublish( - MeteorPubSub.usersInOrganization, - async function (organizationId: OrganizationId, token: string | undefined) { - if (!organizationId) throw new Meteor.Error(400, 'organizationId argument missing') - if (await OrganizationReadAccess.adminUsers(organizationId, { userId: this.userId, token })) { - return Users.findWithCursor( - { organizationId }, - { - fields: { - _id: 1, - username: 1, - emails: 1, - profile: 1, - organizationId: 1, - superAdmin: 1, - }, - } - ) - } - return null - } -) diff --git a/meteor/server/publications/timeline.ts b/meteor/server/publications/timeline.ts index 15cf679157..c32c42b938 100644 --- a/meteor/server/publications/timeline.ts +++ b/meteor/server/publications/timeline.ts @@ -19,8 +19,6 @@ import { TriggerUpdate, } from '../lib/customPublication' import { getActiveRoutes } from '@sofie-automation/meteor-lib/dist/collections/Studios' -import { PeripheralDeviceReadAccess } from '../security/peripheralDevice' -import { StudioReadAccess } from '../security/studio' import { fetchStudioLight } from '../optimizations' import { FastTrackObservers, setupFastTrackObserver } from './fastTrack' import { logger } from '../logging' @@ -29,7 +27,7 @@ import { Time } from '../lib/tempLib' import { ReadonlyDeep } from 'type-fest' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBTimelineDatastoreEntry } from '@sofie-automation/corelib/dist/dataModel/TimelineDatastore' -import { PeripheralDevices, Studios, Timeline, TimelineDatastore } from '../collections' +import { Studios, Timeline, TimelineDatastore } from '../collections' import { check } from 'meteor/check' import { ResultingMappingRoutes, StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' @@ -38,16 +36,18 @@ import { PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { checkAccessAndGetPeripheralDevice } from '../security/check' +import { assertConnectionHasOneOfPermissions } from '../security/auth' + +meteorPublish(CorelibPubSub.timelineDatastore, async function (studioId: StudioId, _token: string | undefined) { + assertConnectionHasOneOfPermissions(this.connection, 'testing') -meteorPublish(CorelibPubSub.timelineDatastore, async function (studioId: StudioId, token: string | undefined) { if (!studioId) throw new Meteor.Error(400, 'selector argument missing') const modifier: FindOptions = { fields: {}, } - if (await StudioReadAccess.studioContent(studioId, { userId: this.userId, token })) { - return TimelineDatastore.findWithCursor({ studioId }, modifier) - } - return null + + return TimelineDatastore.findWithCursor({ studioId }, modifier) }) meteorCustomPublish( @@ -56,16 +56,12 @@ meteorCustomPublish( async function (pub, deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') + const studioId = peripheralDevice.studioId + if (!studioId) return - const studioId = peripheralDevice.studioId - if (!studioId) return - - await createObserverForTimelinePublication(pub, studioId) - } + await createObserverForTimelinePublication(pub, studioId) } ) meteorPublish( @@ -73,30 +69,26 @@ meteorPublish( async function (deviceId: PeripheralDeviceId, token: string | undefined) { check(deviceId, String) - if (await PeripheralDeviceReadAccess.peripheralDeviceContent(deviceId, { userId: this.userId, token })) { - const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, this) - if (!peripheralDevice) throw new Meteor.Error('PeripheralDevice "' + deviceId + '" not found') + const studioId = peripheralDevice.studioId + if (!studioId) return null - const studioId = peripheralDevice.studioId - if (!studioId) return null - const modifier: FindOptions = { - fields: {}, - } - - return TimelineDatastore.findWithCursor({ studioId }, modifier) + const modifier: FindOptions = { + fields: {}, } - return null + + return TimelineDatastore.findWithCursor({ studioId }, modifier) } ) meteorCustomPublish( MeteorPubSub.timelineForStudio, PeripheralDevicePubSubCollectionsNames.studioTimeline, - async function (pub, studioId: StudioId, token: string | undefined) { - if (await StudioReadAccess.studio(studioId, { userId: this.userId, token })) { - await createObserverForTimelinePublication(pub, studioId) - } + async function (pub, studioId: StudioId, _token: string | undefined) { + assertConnectionHasOneOfPermissions(this.connection, 'testing') + + await createObserverForTimelinePublication(pub, studioId) } ) diff --git a/meteor/server/publications/translationsBundles.ts b/meteor/server/publications/translationsBundles.ts index 8173fd3ec5..fbb2d625fd 100644 --- a/meteor/server/publications/translationsBundles.ts +++ b/meteor/server/publications/translationsBundles.ts @@ -1,20 +1,18 @@ -import { TranslationsBundlesSecurity } from '../security/translationsBundles' import { meteorPublish } from './lib/lib' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { TranslationsBundles } from '../collections' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { TranslationsBundle } from '@sofie-automation/meteor-lib/dist/collections/TranslationsBundles' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' -meteorPublish(MeteorPubSub.translationsBundles, async (token: string | undefined) => { +meteorPublish(MeteorPubSub.translationsBundles, async (_token: string | undefined) => { const selector: MongoQuery = {} - if (TranslationsBundlesSecurity.allowReadAccess(selector, token, this)) { - return TranslationsBundles.findWithCursor(selector, { - fields: { - data: 0, - }, - }) - } + triggerWriteAccessBecauseNoCheckNecessary() - return null + return TranslationsBundles.findWithCursor(selector, { + fields: { + data: 0, + }, + }) }) diff --git a/meteor/server/publications/triggeredActionsUI.ts b/meteor/server/publications/triggeredActionsUI.ts index 5a431daf10..6eaeb0f52e 100644 --- a/meteor/server/publications/triggeredActionsUI.ts +++ b/meteor/server/publications/triggeredActionsUI.ts @@ -14,13 +14,10 @@ import { SetupObserversResult, TriggerUpdate, } from '../lib/customPublication' -import { logger } from '../logging' -import { resolveCredentials } from '../security/lib/credentials' -import { NoSecurityReadAccess } from '../security/noSecurity' -import { ShowStyleReadAccess } from '../security/showStyle' import { TriggeredActions } from '../collections' import { check, Match } from 'meteor/check' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' interface UITriggeredActionsArgs { readonly showStyleBaseId: ShowStyleBaseId | null @@ -114,27 +111,19 @@ meteorCustomPublish( async function (pub, showStyleBaseId: ShowStyleBaseId | null) { check(showStyleBaseId, Match.Maybe(String)) - const cred = await resolveCredentials({ userId: this.userId, token: undefined }) - - if ( - !cred || - NoSecurityReadAccess.any() || - (showStyleBaseId && (await ShowStyleReadAccess.showStyleBase(showStyleBaseId, cred))) - ) { - await setUpCollectionOptimizedObserver< - UITriggeredActionsObj, - UITriggeredActionsArgs, - UITriggeredActionsState, - UITriggeredActionsUpdateProps - >( - `pub_${MeteorPubSub.uiTriggeredActions}_${showStyleBaseId}`, - { showStyleBaseId }, - setupUITriggeredActionsPublicationObservers, - manipulateUITriggeredActionsPublicationData, - pub - ) - } else { - logger.warn(`Pub.${CustomCollectionName.UITriggeredActions}: Not allowed: "${showStyleBaseId}"`) - } + triggerWriteAccessBecauseNoCheckNecessary() + + await setUpCollectionOptimizedObserver< + UITriggeredActionsObj, + UITriggeredActionsArgs, + UITriggeredActionsState, + UITriggeredActionsUpdateProps + >( + `pub_${MeteorPubSub.uiTriggeredActions}_${showStyleBaseId}`, + { showStyleBaseId }, + setupUITriggeredActionsPublicationObservers, + manipulateUITriggeredActionsPublicationData, + pub + ) } ) diff --git a/meteor/server/security/README.md b/meteor/server/security/README.md deleted file mode 100644 index b66a4adb58..0000000000 --- a/meteor/server/security/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Data Ownership: - -## System - -- CoreSystem -- Users -- **Organizations** - -## Organization - -- UserActionsLog -- Evaluations -- Snapshots -- Blueprints -- **Studios** -- **ShowStyleBases** -- **PeripheralDevices** - -## ShowStyleBase - -- ShowStyleVariants -- RundownLayouts - -## Studio - -- ExternalMessageQueue -- RecordedFiles -- MediaObjects -- Timeline -- **RundownPlaylists** - -## RundownPlaylist - -- Rundowns - -## Rundown - -- Segments -- Parts -- PartInstances -- Pieces -- PieceInstances -- AdLibPieces -- RundownBaselineAdLibPieces -- IngestDataCache -- ExpectedMediaItems -- ExpectedPlayoutItems - -## PeripheralDevice - -- PeripheralDeviceCommands -- MediaWorkFlowSteps -- MediaWorkFlows diff --git a/meteor/server/security/__tests__/security.test.ts b/meteor/server/security/__tests__/security.test.ts deleted file mode 100644 index 595791d812..0000000000 --- a/meteor/server/security/__tests__/security.test.ts +++ /dev/null @@ -1,358 +0,0 @@ -import '../../../__mocks__/_extendJest' - -import { MethodContext } from '../../api/methodContext' -import { DBOrganization } from '@sofie-automation/meteor-lib/dist/collections/Organization' -import { User } from '@sofie-automation/meteor-lib/dist/collections/Users' -import { protectString } from '../../lib/tempLib' -import { Settings } from '../../Settings' -import { DefaultEnvironment, setupDefaultStudioEnvironment } from '../../../__mocks__/helpers/database' -import { BucketsAPI } from '../../api/buckets' -import { storeSystemSnapshot } from '../../api/snapshot' -import { BucketSecurity } from '../buckets' -import { Credentials } from '../lib/credentials' -import { NoSecurityReadAccess } from '../noSecurity' -import { OrganizationContentWriteAccess, OrganizationReadAccess } from '../organization' -import { StudioContentWriteAccess } from '../studio' -import { OrganizationId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Organizations, Users } from '../../collections' -import { SupressLogMessages } from '../../../__mocks__/suppressLogging' -import { generateToken } from '../../api/singleUseTokens' -import { hashSingleUseToken } from '../../api/deviceTriggers/triggersContext' - -describe('Security', () => { - function getContext(cred: Credentials): MethodContext { - return { - ...cred, - - isSimulation: false, - connection: null, - setUserId: (_userId: string) => { - // Nothing - }, - unblock: () => { - // Nothing - }, - } - } - function getUser(userId: UserId, orgId: OrganizationId): User { - return { - _id: userId, - organizationId: orgId, - - createdAt: '', - services: { - password: { - bcrypt: 'abc', - }, - }, - username: 'username', - emails: [{ address: 'email.com', verified: false }], - profile: { - name: 'John Doe', - }, - } - } - function getOrg(id: string): DBOrganization { - return { - _id: protectString(id), - name: 'The Company', - - userRoles: { - userA: { - admin: true, - }, - }, - - created: 0, - modified: 0, - - applications: [], - broadcastMediums: [], - } - } - async function changeEnableUserAccounts(fcn: () => Promise) { - try { - Settings.enableUserAccounts = false - await fcn() - Settings.enableUserAccounts = true - await fcn() - } catch (e) { - console.log(`Error happened when Settings.enableUserAccounts = ${Settings.enableUserAccounts}`) - throw e - } - } - - const idCreator: UserId = protectString('userCreator') - const idUserB: UserId = protectString('userB') - const idNonExisting: UserId = protectString('userNonExistant') - const idInWrongOrg: UserId = protectString('userInWrongOrg') - const idSuperAdmin: UserId = protectString('userSuperAdmin') - const idSuperAdminInOtherOrg: UserId = protectString('userSuperAdminOther') - - // Credentials for various users: - const nothing: MethodContext = getContext({ userId: null }) - const creator: MethodContext = getContext({ userId: idCreator }) - const userB: MethodContext = getContext({ userId: idUserB }) - const nonExisting: MethodContext = getContext({ userId: idNonExisting }) - const wrongOrg: MethodContext = getContext({ userId: idInWrongOrg }) - const superAdmin: MethodContext = getContext({ userId: idSuperAdmin }) - const otherSuperAdmin: MethodContext = getContext({ userId: idSuperAdminInOtherOrg }) - - const unknownId = protectString('unknown') - - const org0: DBOrganization = getOrg('org0') - const org1: DBOrganization = getOrg('org1') - const org2: DBOrganization = getOrg('org2') - - async function expectReadNotAllowed(fcn: () => Promise) { - if (Settings.enableUserAccounts === false) return expectReadAllowed(fcn) - return expect(fcn()).resolves.toEqual(false) - } - async function expectReadAllowed(fcn: () => Promise) { - return expect(fcn()).resolves.toEqual(true) - } - async function expectNotAllowed(fcn: () => Promise) { - if (Settings.enableUserAccounts === false) return expectAllowed(fcn) - return expect(fcn()).rejects.toBeTruthy() - } - async function expectNotLoggedIn(fcn: () => Promise) { - if (Settings.enableUserAccounts === false) return expectAllowed(fcn) - return expect(fcn()).rejects.toMatchToString(/not logged in/i) - } - async function expectNotFound(fcn: () => Promise) { - // if (Settings.enableUserAccounts === false) return expectAllowed(fcn) - return expect(fcn()).rejects.toMatchToString(/not found/i) - } - async function expectAllowed(fcn: () => Promise) { - return expect(fcn()).resolves.not.toBeUndefined() - } - let env: DefaultEnvironment - beforeAll(async () => { - env = await setupDefaultStudioEnvironment(org0._id) - - await Organizations.insertAsync(org0) - await Organizations.insertAsync(org1) - await Organizations.insertAsync(org2) - - await Users.insertAsync(getUser(idCreator, org0._id)) - await Users.insertAsync(getUser(idUserB, org0._id)) - await Users.insertAsync(getUser(idInWrongOrg, org1._id)) - await Users.insertAsync({ ...getUser(idSuperAdmin, org0._id), superAdmin: true }) - await Users.insertAsync({ ...getUser(idSuperAdminInOtherOrg, org2._id), superAdmin: true }) - }) - - // eslint-disable-next-line jest/expect-expect - test('Buckets', async () => { - const access = await StudioContentWriteAccess.bucket(creator, env.studio._id) - const bucket = await BucketsAPI.createNewBucket(access, 'myBucket') - - await changeEnableUserAccounts(async () => { - await expectReadAllowed(async () => BucketSecurity.allowReadAccess(creator, bucket._id)) - await expectAllowed(async () => BucketSecurity.allowWriteAccess(creator, bucket._id)) - // expectAccessAllowed(() => BucketSecurity.allowWriteAccessPiece({ _id: bucket._id }, credUserA)) - - // Unknown bucket: - await expectNotFound(async () => BucketSecurity.allowReadAccess(creator, unknownId)) - await expectNotFound(async () => BucketSecurity.allowWriteAccess(creator, unknownId)) - await expectNotFound(async () => BucketSecurity.allowWriteAccessPiece(creator, unknownId)) - - // Not logged in: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => BucketSecurity.allowReadAccess(nothing, bucket._id)) - await expectNotLoggedIn(async () => BucketSecurity.allowWriteAccess(nothing, bucket._id)) - // expectAccessNotLoggedIn(() => BucketSecurity.allowWriteAccessPiece({ _id: bucket._id }, credNothing)) - - // Non existing user: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => BucketSecurity.allowReadAccess(nonExisting, bucket._id)) - await expectNotLoggedIn(async () => BucketSecurity.allowWriteAccess(nonExisting, bucket._id)) - // expectAccess(() => BucketSecurity.allowWriteAccessPiece({ _id: bucket._id }, credNonExistingUser)) - - // Other user in same org: - await expectReadAllowed(async () => BucketSecurity.allowReadAccess(userB, bucket._id)) - await expectAllowed(async () => BucketSecurity.allowWriteAccess(userB, bucket._id)) - // expectAccess(() => BucketSecurity.allowWriteAccessPiece({ _id: bucket._id }, credUserB)) - - // Other user in other org: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the same organization as the studio/i) - } - await expectReadNotAllowed(async () => BucketSecurity.allowReadAccess(wrongOrg, bucket._id)) - await expectNotAllowed(async () => BucketSecurity.allowWriteAccess(wrongOrg, bucket._id)) - // expectAccess(() => BucketSecurity.allowWriteAccessPiece({ _id: bucket._id }, credUserInWrongOrganization)) - }) - }) - - // eslint-disable-next-line jest/expect-expect - test('NoSecurity', async () => { - await changeEnableUserAccounts(async () => { - await expectAllowed(async () => NoSecurityReadAccess.any()) - }) - }) - // eslint-disable-next-line jest/expect-expect - test('Organization', async () => { - const token = generateToken() - const snapshotId = await storeSystemSnapshot(superAdmin, hashSingleUseToken(token), env.studio._id, 'for test') - - await changeEnableUserAccounts(async () => { - const selectorId = org0._id - const selectorOrg = { organizationId: org0._id } - - // === Read access: === - - // No user credentials: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.adminUsers(selectorId, nothing)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organization(selectorId, nothing)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organizationContent(selectorId, nothing)) - // Normal user: - await expectReadAllowed(async () => OrganizationReadAccess.adminUsers(selectorId, creator)) - await expectReadAllowed(async () => OrganizationReadAccess.organization(selectorId, creator)) - await expectReadAllowed(async () => OrganizationReadAccess.organizationContent(selectorId, creator)) - // Other normal user: - await expectReadAllowed(async () => OrganizationReadAccess.adminUsers(selectorId, userB)) - await expectReadAllowed(async () => OrganizationReadAccess.organization(selectorId, userB)) - await expectReadAllowed(async () => OrganizationReadAccess.organizationContent(selectorId, userB)) - // Non-existing user: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.adminUsers(selectorId, nonExisting)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organization(selectorId, nonExisting)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/No organization in credentials/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organizationContent(selectorId, nonExisting)) - // User in wrong organization: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the organization/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.adminUsers(selectorId, wrongOrg)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the organization/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organization(selectorId, wrongOrg)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the organization/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organizationContent(selectorId, wrongOrg)) - // SuperAdmin: - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the organization/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.adminUsers(selectorId, otherSuperAdmin)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the organization/i) - } - await expectReadNotAllowed(async () => OrganizationReadAccess.organization(selectorId, otherSuperAdmin)) - if (Settings.enableUserAccounts) { - SupressLogMessages.suppressLogMessage(/User is not in the organization/i) - } - await expectReadNotAllowed(async () => - OrganizationReadAccess.organizationContent(selectorId, otherSuperAdmin) - ) - - // === Write access: === - - // No user credentials: - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.organization(nothing, org0._id)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.studio(nothing, env.studio)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.evaluation(nothing)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.mediaWorkFlows(nothing)) - await expectNotLoggedIn(async () => - OrganizationContentWriteAccess.blueprint(nothing, env.studioBlueprint._id) - ) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.snapshot(nothing, snapshotId)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.dataFromSnapshot(nothing, org0._id)) - await expectNotLoggedIn(async () => - OrganizationContentWriteAccess.showStyleBase(nothing, env.showStyleBaseId) - ) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.translationBundle(nothing, selectorOrg)) - - // Normal user: - await expectAllowed(async () => OrganizationContentWriteAccess.organization(creator, org0._id)) - await expectAllowed(async () => OrganizationContentWriteAccess.studio(creator, env.studio)) - await expectAllowed(async () => OrganizationContentWriteAccess.evaluation(creator)) - await expectAllowed(async () => OrganizationContentWriteAccess.mediaWorkFlows(creator)) - await expectAllowed(async () => OrganizationContentWriteAccess.blueprint(creator, env.studioBlueprint._id)) - await expectAllowed(async () => OrganizationContentWriteAccess.snapshot(creator, snapshotId)) - await expectAllowed(async () => OrganizationContentWriteAccess.dataFromSnapshot(creator, org0._id)) - await expectAllowed(async () => OrganizationContentWriteAccess.showStyleBase(creator, env.showStyleBaseId)) - await expectAllowed(async () => OrganizationContentWriteAccess.translationBundle(creator, selectorOrg)) - // Other normal user: - await expectAllowed(async () => OrganizationContentWriteAccess.organization(userB, org0._id)) - await expectAllowed(async () => OrganizationContentWriteAccess.studio(userB, env.studio)) - await expectAllowed(async () => OrganizationContentWriteAccess.evaluation(userB)) - await expectAllowed(async () => OrganizationContentWriteAccess.mediaWorkFlows(userB)) - await expectAllowed(async () => OrganizationContentWriteAccess.blueprint(userB, env.studioBlueprint._id)) - await expectAllowed(async () => OrganizationContentWriteAccess.snapshot(userB, snapshotId)) - await expectAllowed(async () => OrganizationContentWriteAccess.dataFromSnapshot(userB, org0._id)) - await expectAllowed(async () => OrganizationContentWriteAccess.showStyleBase(userB, env.showStyleBaseId)) - await expectAllowed(async () => OrganizationContentWriteAccess.translationBundle(userB, selectorOrg)) - // Non-existing user: - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.organization(nonExisting, org0._id)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.studio(nonExisting, env.studio)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.evaluation(nonExisting)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.mediaWorkFlows(nonExisting)) - await expectNotLoggedIn(async () => - OrganizationContentWriteAccess.blueprint(nonExisting, env.studioBlueprint._id) - ) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.snapshot(nonExisting, snapshotId)) - await expectNotLoggedIn(async () => OrganizationContentWriteAccess.dataFromSnapshot(nonExisting, org0._id)) - await expectNotLoggedIn(async () => - OrganizationContentWriteAccess.showStyleBase(nonExisting, env.showStyleBaseId) - ) - await expectNotLoggedIn(async () => - OrganizationContentWriteAccess.translationBundle(nonExisting, selectorOrg) - ) - // User in wrong organization: - await expectNotAllowed(async () => OrganizationContentWriteAccess.organization(wrongOrg, org0._id)) - await expectNotAllowed(async () => OrganizationContentWriteAccess.studio(wrongOrg, env.studio)) - // expectNotAllowed(async() => OrganizationContentWriteAccess.evaluation(wrongOrg)) - // expectNotAllowed(async() => OrganizationContentWriteAccess.mediaWorkFlows(wrongOrg)) - await expectNotAllowed(async () => - OrganizationContentWriteAccess.blueprint(wrongOrg, env.studioBlueprint._id) - ) - await expectNotAllowed(async () => OrganizationContentWriteAccess.snapshot(wrongOrg, snapshotId)) - await expectNotAllowed(async () => OrganizationContentWriteAccess.dataFromSnapshot(wrongOrg, org0._id)) - await expectNotAllowed(async () => - OrganizationContentWriteAccess.showStyleBase(wrongOrg, env.showStyleBaseId) - ) - await expectNotAllowed(async () => OrganizationContentWriteAccess.translationBundle(wrongOrg, selectorOrg)) - - // Other SuperAdmin - await expectNotAllowed(async () => OrganizationContentWriteAccess.organization(otherSuperAdmin, org0._id)) - await expectNotAllowed(async () => OrganizationContentWriteAccess.studio(otherSuperAdmin, env.studio)) - // expectNotAllowed(async() => OrganizationContentWriteAccess.evaluation(otherSuperAdmin)) - // expectNotAllowed(async() => OrganizationContentWriteAccess.mediaWorkFlows(otherSuperAdmin)) - await expectNotAllowed(async () => - OrganizationContentWriteAccess.blueprint(otherSuperAdmin, env.studioBlueprint._id) - ) - await expectNotAllowed(async () => OrganizationContentWriteAccess.snapshot(otherSuperAdmin, snapshotId)) - await expectNotAllowed(async () => - OrganizationContentWriteAccess.dataFromSnapshot(otherSuperAdmin, org0._id) - ) - await expectNotAllowed(async () => - OrganizationContentWriteAccess.showStyleBase(otherSuperAdmin, env.showStyleBaseId) - ) - await expectNotAllowed(async () => - OrganizationContentWriteAccess.translationBundle(otherSuperAdmin, selectorOrg) - ) - }) - }) -}) diff --git a/meteor/server/security/_security.ts b/meteor/server/security/_security.ts deleted file mode 100644 index 320d5b5bbb..0000000000 --- a/meteor/server/security/_security.ts +++ /dev/null @@ -1,11 +0,0 @@ -import './lib/lib' - -import './buckets' -import './noSecurity' -import './organization' -import './peripheralDevice' -import './rundown' -import './rundownPlaylist' -import './showStyle' -import './studio' -import './system' diff --git a/meteor/server/security/lib/lib.ts b/meteor/server/security/allowDeny.ts similarity index 84% rename from meteor/server/security/lib/lib.ts rename to meteor/server/security/allowDeny.ts index a5c0d244d9..089032c9f8 100644 --- a/meteor/server/security/lib/lib.ts +++ b/meteor/server/security/allowDeny.ts @@ -1,5 +1,5 @@ import { FieldNames } from '@sofie-automation/meteor-lib/dist/collections/lib' -import { logger } from '../../logging' + /** * Allow only edits to the fields specified. Edits to any other fields will be rejected * @param doc @@ -32,8 +32,3 @@ export function rejectFields(_doc: T, fieldNames: FieldNames, rejectFields return true } - -export function logNotAllowed(area: string, reason: string): false { - logger.warn(`Not allowed access to ${area}: ${reason}`) - return false -} diff --git a/meteor/server/security/auth.ts b/meteor/server/security/auth.ts new file mode 100644 index 0000000000..60702838fa --- /dev/null +++ b/meteor/server/security/auth.ts @@ -0,0 +1,87 @@ +import { + parseUserPermissions, + USER_PERMISSIONS_HEADER, + UserPermissions, +} from '@sofie-automation/meteor-lib/dist/userPermissions' +import { Settings } from '../Settings' +import { Meteor } from 'meteor/meteor' +import Koa from 'koa' +import { triggerWriteAccess } from './securityVerify' +import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { unprotectString } from '../lib/tempLib' +import { logger } from '../logging' +import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections' + +export type RequestCredentials = Meteor.Connection | Koa.ParameterizedContext + +export function parseConnectionPermissions(conn: RequestCredentials): UserPermissions { + if (!Settings.enableHeaderAuth) { + // If auth is disabled, return all permissions + return { + studio: true, + configure: true, + developer: true, + testing: true, + service: true, + gateway: true, + } + } + + let header: string | string[] | undefined + if ('httpHeaders' in conn) { + header = conn.httpHeaders[USER_PERMISSIONS_HEADER] + } else { + header = conn.request.headers[USER_PERMISSIONS_HEADER] + } + + // This shouldn't happen, but take the first header if it does + if (Array.isArray(header)) header = header[0] + + return parseUserPermissions(header) +} + +export function assertConnectionHasOneOfPermissions( + conn: RequestCredentials | null, + ...allowedPermissions: Array +): void { + if (allowedPermissions.length === 0) throw new Meteor.Error(403, 'No permissions specified') + + triggerWriteAccess() + + if (!conn) throw new Meteor.Error(403, 'Can only be invoked by clients') + + // Skip if auth is disabled + if (!Settings.enableHeaderAuth) return + + const permissions = parseConnectionPermissions(conn) + for (const permission of allowedPermissions) { + if (permissions[permission]) return + } + + // Nothing matched + throw new Meteor.Error(403, 'Not authorized') +} + +export function checkUserIdHasOneOfPermissions( + userId: UserId | null, + collectionName: CollectionName, + ...allowedPermissions: Array +): boolean { + if (allowedPermissions.length === 0) throw new Meteor.Error(403, 'No permissions specified') + + triggerWriteAccess() + + // Skip if auth is disabled + if (!Settings.enableHeaderAuth) return true + + if (!userId) throw new Meteor.Error(403, 'UserId is null') + + const permissions: UserPermissions = JSON.parse(unprotectString(userId)) + for (const permission of allowedPermissions) { + if (permissions[permission]) return true + } + + // Nothing matched + logger.warn(`Not allowed access to ${collectionName}`) + return false +} diff --git a/meteor/server/security/buckets.ts b/meteor/server/security/buckets.ts deleted file mode 100644 index d7160f974c..0000000000 --- a/meteor/server/security/buckets.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Bucket } from '@sofie-automation/meteor-lib/dist/collections/Buckets' -import { Credentials, ResolvedCredentials } from './lib/credentials' -import { triggerWriteAccess } from './lib/securityVerify' -import { check } from '../lib/check' -import { Meteor } from 'meteor/meteor' -import { StudioReadAccess, StudioContentWriteAccess, StudioContentAccess } from './studio' -import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' -import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' -import { AdLibActionId, BucketId, PieceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { BucketAdLibActions, BucketAdLibs, Buckets } from '../collections' - -export namespace BucketSecurity { - export interface BucketContentAccess extends StudioContentAccess { - bucket: Bucket - } - export interface BucketAdlibPieceContentAccess extends StudioContentAccess { - adlib: BucketAdLib - } - export interface BucketAdlibActionContentAccess extends StudioContentAccess { - action: BucketAdLibAction - } - - // Sometimes a studio ID is passed, others the peice / bucket id - export async function allowReadAccess( - cred: Credentials | ResolvedCredentials, - bucketId: BucketId - ): Promise { - check(bucketId, String) - - const bucket = await Buckets.findOneAsync(bucketId) - if (!bucket) throw new Meteor.Error(404, `Bucket "${bucketId}" not found!`) - - return StudioReadAccess.studioContent(bucket.studioId, cred) - } - export async function allowWriteAccess(cred: Credentials, bucketId: BucketId): Promise { - triggerWriteAccess() - - check(bucketId, String) - - const bucket = await Buckets.findOneAsync(bucketId) - if (!bucket) throw new Meteor.Error(404, `Bucket "${bucketId}" not found!`) - - return { - ...(await StudioContentWriteAccess.bucket(cred, bucket.studioId)), - bucket, - } - } - export async function allowWriteAccessPiece( - cred: Credentials, - pieceId: PieceId - ): Promise { - triggerWriteAccess() - - check(pieceId, String) - - const bucketAdLib = await BucketAdLibs.findOneAsync(pieceId) - if (!bucketAdLib) throw new Meteor.Error(404, `Bucket AdLib "${pieceId}" not found!`) - - return { - ...(await StudioContentWriteAccess.bucket(cred, bucketAdLib.studioId)), - adlib: bucketAdLib, - } - } - export async function allowWriteAccessAction( - cred: Credentials, - actionId: AdLibActionId - ): Promise { - triggerWriteAccess() - - check(actionId, String) - - const bucketAdLibAction = await BucketAdLibActions.findOneAsync(actionId) - if (!bucketAdLibAction) throw new Meteor.Error(404, `Bucket AdLib Actions "${actionId}" not found!`) - - return { - ...(await StudioContentWriteAccess.bucket(cred, bucketAdLibAction.studioId)), - action: bucketAdLibAction, - } - } -} diff --git a/meteor/server/security/check.ts b/meteor/server/security/check.ts new file mode 100644 index 0000000000..da6d38ad1d --- /dev/null +++ b/meteor/server/security/check.ts @@ -0,0 +1,104 @@ +import { PeripheralDeviceId, RundownId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Meteor } from 'meteor/meteor' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { assertConnectionHasOneOfPermissions, RequestCredentials } from './auth' +import { PeripheralDevices, RundownPlaylists, Rundowns } from '../collections' +import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' +import { MethodContext } from '../api/methodContext' +import { profiler } from '../api/profiler' +import { SubscriptionContext } from '../publications/lib/lib' + +/** + * Check that the current user has write access to the specified playlist, and ensure that the playlist exists + * @param context + * @param playlistId Id of the playlist + */ +export async function checkAccessToPlaylist( + cred: RequestCredentials | null, + playlistId: RundownPlaylistId +): Promise { + assertConnectionHasOneOfPermissions(cred, 'studio') + + const playlist = (await RundownPlaylists.findOneAsync(playlistId, { + projection: { + _id: 1, + studioId: 1, + organizationId: 1, + name: 1, + }, + })) as Pick | undefined + if (!playlist) throw new Meteor.Error(404, `RundownPlaylist "${playlistId}" not found`) + + return playlist +} +export type VerifiedRundownPlaylistForUserAction = Pick< + DBRundownPlaylist, + '_id' | 'studioId' | 'organizationId' | 'name' +> + +/** + * Check that the current user has write access to the specified rundown, and ensure that the rundown exists + * @param context + * @param rundownId Id of the rundown + */ +export async function checkAccessToRundown( + cred: RequestCredentials | null, + rundownId: RundownId +): Promise { + assertConnectionHasOneOfPermissions(cred, 'studio') + + const rundown = (await Rundowns.findOneAsync(rundownId, { + projection: { + _id: 1, + studioId: 1, + externalId: 1, + showStyleVariantId: 1, + source: 1, + }, + })) as Pick | undefined + if (!rundown) throw new Meteor.Error(404, `Rundown "${rundownId}" not found`) + + return rundown +} +export type VerifiedRundownForUserAction = Pick< + DBRundown, + '_id' | 'studioId' | 'externalId' | 'showStyleVariantId' | 'source' +> + +/** Check Access and return PeripheralDevice, throws otherwise */ +export async function checkAccessAndGetPeripheralDevice( + deviceId: PeripheralDeviceId, + token: string | undefined, + context: MethodContext | SubscriptionContext +): Promise { + const span = profiler.startSpan('lib.checkAccessAndGetPeripheralDevice') + + assertConnectionHasOneOfPermissions(context.connection, 'gateway') + + // If no token, we will never match + if (!token) throw new Meteor.Error(401, `Not allowed access to peripheralDevice`) + + const device = await PeripheralDevices.findOneAsync({ _id: deviceId }) + if (!device) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) + + // Check if the device has a token, and if it matches: + if (device.token && device.token === token) { + span?.end() + return device + } + + // If the device has a parent, try that for access control: + const parentDevice = device.parentDeviceId ? await PeripheralDevices.findOneAsync(device.parentDeviceId) : device + if (!parentDevice) throw new Meteor.Error(404, `PeripheralDevice parentDevice "${device.parentDeviceId}" not found`) + + // Check if the parent device has a token, and if it matches: + if (parentDevice.token && parentDevice.token === token) { + span?.end() + return device + } + + // No match for token found + span?.end() + throw new Meteor.Error(401, `Not allowed access to peripheralDevice`) +} diff --git a/meteor/server/security/lib/access.ts b/meteor/server/security/lib/access.ts deleted file mode 100644 index 2f913afb5b..0000000000 --- a/meteor/server/security/lib/access.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as _ from 'underscore' - -export interface Access { - // Direct database access: - read: boolean - insert: boolean - update: boolean - remove: boolean - - // Methods access: - playout: boolean - configure: boolean - - // For debugging - reason: string - - // The document in question - document: T | null -} - -/** - * Grant all access to all of the document - * @param document The document - * @param reason The reason for the access being granted - */ -export function allAccess(document: T | null, reason?: string): Access { - return { - read: true, - insert: true, - update: true, - remove: true, - - playout: true, - configure: true, - reason: reason || '', - document: document, - } -} - -/** - * Deny all access to all of the document - * @param reason The reason for the access being denied - */ -export function noAccess(reason: string): Access { - return combineAccess({}, allAccess(null, reason)) -} - -/** - * Combine access objects to find the minimum common overlap - * @param access0 - * @param access1 - */ -export function combineAccess( - access0: Access | { reason?: string; document?: null }, - access1: Access -): Access { - const a: any = {} - _.each(_.keys(access0).concat(_.keys(access1)), (key) => { - a[key] = (access0 as any)[key] && (access1 as any)[key] - }) - a.reason = _.compact([access0.reason, access1.reason]).join(',') - a.document = access0.document || access1.document || null - return a -} diff --git a/meteor/server/security/lib/credentials.ts b/meteor/server/security/lib/credentials.ts deleted file mode 100644 index b9b480a712..0000000000 --- a/meteor/server/security/lib/credentials.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { User } from '@sofie-automation/meteor-lib/dist/collections/Users' -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { cacheResult, clearCacheResult } from '../../lib/cacheResult' -import { LIMIT_CACHE_TIME } from './security' -import { profiler } from '../../api/profiler' -import { OrganizationId, UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDevices, Users } from '../../collections' -import { isProtectedString } from '@sofie-automation/shared-lib/dist/lib/protectedString' - -export interface Credentials { - userId: UserId | null - token?: string -} - -/** - * A minimal set of properties about the user. - * We keep it small so that we don't cache too much in memory or have to invalidate the credentials when something insignificant changes - */ -export type ResolvedUser = Pick - -/** - * A minimal set of properties about the OeripheralDevice. - * We keep it small so that we don't cache too much in memory or have to invalidate the credentials when something insignificant changes - */ -export type ResolvedPeripheralDevice = Pick - -export interface ResolvedCredentials { - organizationId: OrganizationId | null - user?: ResolvedUser - device?: ResolvedPeripheralDevice -} -export interface ResolvedUserCredentials { - organizationId: OrganizationId - user: ResolvedUser -} -export interface ResolvedPeripheralDeviceCredentials { - organizationId: OrganizationId - device: ResolvedPeripheralDevice -} - -/** - * Resolve the provided credentials, and retrieve the PeripheralDevice and Organization for the provided credentials. - * @returns null if the PeripheralDevice was not found - */ -export async function resolveAuthenticatedPeripheralDevice( - cred: Credentials -): Promise { - const resolved = await resolveCredentials({ userId: null, token: cred.token }) - - if (resolved.device && resolved.organizationId) { - return { - organizationId: resolved.organizationId, - device: resolved.device, - } - } else { - return null - } -} - -/** - * Resolve the provided credentials, and retrieve the User and Organization for the provided credentials. - * Note: this requies that the UserId came from a trusted source,it must not be from user input - * @returns null if the user was not found - */ -export async function resolveAuthenticatedUser(cred: Credentials): Promise { - const resolved = await resolveCredentials({ userId: cred.userId }) - - if (resolved.user && resolved.organizationId) { - return { - organizationId: resolved.organizationId, - user: resolved.user, - } - } else { - return null - } -} - -/** - * Resolve the provided credentials/identifier, and fetch the authenticating document from the database. - * Note: this requires that the provided UserId comes from an up-to-date location in meteor, it must not be from user input - * @returns The resolved object. If the identifiers were invalid then this object will have no properties - */ -export async function resolveCredentials(cred: Credentials | ResolvedCredentials): Promise { - const span = profiler.startSpan('security.lib.credentials') - - if (isResolvedCredentials(cred)) { - span?.end() - return cred - } - - const resolved = cacheResult( - credCacheName(cred), - async () => { - const resolved: ResolvedCredentials = { - organizationId: null, - } - - if (cred.token && typeof cred.token !== 'string') cred.token = undefined - if (cred.userId && !isProtectedString(cred.userId)) cred.userId = null - - // Lookup user, using userId: - if (cred.userId && isProtectedString(cred.userId)) { - const user = (await Users.findOneAsync(cred.userId, { - fields: { - _id: 1, - organizationId: 1, - superAdmin: 1, - }, - })) as ResolvedUser - if (user) { - resolved.user = user - resolved.organizationId = user.organizationId - } - } - // Lookup device, using token - if (cred.token) { - // TODO - token is not enforced to be unique and can be defined by a connecting gateway. - // This is rather flawed in the current model.. - const device = (await PeripheralDevices.findOneAsync( - { token: cred.token }, - { - fields: { - _id: 1, - organizationId: 1, - token: 1, - studioId: 1, - }, - } - )) as ResolvedPeripheralDevice - if (device) { - resolved.device = device - resolved.organizationId = device.organizationId - } - } - - // TODO: Implement user-token / API-key - // Lookup user, using token - // if (!resolved.user && !resolved.device && cred.token) { - // user = Users.findOne({ token: cred.token}) - // if (user) resolved.user = user - // } - - // // Make sure the organizationId is valid - // if (resolved.organizationId) { - // const org = (await Organizations.findOneAsync(resolved.organizationId, { - // fields: { _id: 1 }, - // })) as Pick | undefined - // if (org) { - // resolved.organizationId = null - // } - // } - - return resolved - }, - LIMIT_CACHE_TIME - ) - - span?.end() - return resolved -} -/** To be called whenever a user is changed */ -export function resetCredentials(cred: Credentials): void { - clearCacheResult(credCacheName(cred)) -} -function credCacheName(cred: Credentials) { - return `resolveCredentials_${cred.userId}_${cred.token}` -} -export function isResolvedCredentials(cred: Credentials | ResolvedCredentials): cred is ResolvedCredentials { - const c = cred as ResolvedCredentials - return !!(c.user || c.organizationId || c.device) -} diff --git a/meteor/server/security/lib/security.ts b/meteor/server/security/lib/security.ts deleted file mode 100644 index ed27ed1846..0000000000 --- a/meteor/server/security/lib/security.ts +++ /dev/null @@ -1,349 +0,0 @@ -import * as _ from 'underscore' -import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' -import { Settings } from '../../Settings' -import { resolveCredentials, ResolvedCredentials, Credentials, isResolvedCredentials } from './credentials' -import { allAccess, noAccess, combineAccess, Access } from './access' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { isProtectedString } from '../../lib/tempLib' -import { DBOrganization } from '@sofie-automation/meteor-lib/dist/collections/Organization' -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { profiler } from '../../api/profiler' -import { fetchShowStyleBasesLight, fetchStudioLight, ShowStyleBaseLight } from '../../optimizations' -import { Organizations, PeripheralDevices, RundownPlaylists, Rundowns, ShowStyleVariants } from '../../collections' -import { - OrganizationId, - PeripheralDeviceId, - RundownId, - RundownPlaylistId, - ShowStyleBaseId, - ShowStyleVariantId, - StudioId, - UserId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' - -export const LIMIT_CACHE_TIME = 1000 * 60 * 15 // 15 minutes - -// TODO: add caching - -/** - * Grant access to everything if security is disabled - * @returns Access granting access to everything - */ -export function allowAccessToAnythingWhenSecurityDisabled(): Access { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - else return noAccess('Security is enabled') -} - -/** - * Check if access is allowed to the coreSystem collection - * @param cred0 Credentials to check - */ -export async function allowAccessToCoreSystem(cred: ResolvedCredentials): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - - return AccessRules.accessCoreSystem(cred) -} - -/** - * Check if access is allowed to a User, and that user is the current User - * @param cred0 Credentials to check - */ -export async function allowAccessToCurrentUser( - cred0: Credentials | ResolvedCredentials, - userId: UserId | null -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!userId) return noAccess('userId missing') - if (!isProtectedString(userId)) return noAccess('userId is not a string') - - return { - ...(await AccessRules.accessCurrentUser(cred0, userId)), - insert: false, // only allowed through methods - update: false, // only allowed through methods - remove: false, // only allowed through methods - } -} - -/** - * Check if access is allowed to the systemStatus collection - * @param cred0 Credentials to check - */ -export async function allowAccessToSystemStatus(cred0: Credentials | ResolvedCredentials): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - - return { - ...AccessRules.accessSystemStatus(cred0), - insert: false, // only allowed through methods - update: false, // only allowed through methods - remove: false, // only allowed through methods - } -} - -export async function allowAccessToOrganization( - cred0: Credentials | ResolvedCredentials, - organizationId: OrganizationId | null -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!organizationId) return noAccess('organizationId not set') - if (!isProtectedString(organizationId)) return noAccess('organizationId is not a string') - const cred = await resolveCredentials(cred0) - - const organization = await Organizations.findOneAsync(organizationId) - if (!organization) return noAccess('Organization not found') - - return { - ...AccessRules.accessOrganization(organization, cred), - insert: false, // only allowed through methods - remove: false, // only allowed through methods - } -} -export async function allowAccessToShowStyleBase( - cred0: Credentials | ResolvedCredentials, - showStyleBaseId: MongoQueryKey -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!showStyleBaseId) return noAccess('showStyleBaseId not set') - const cred = await resolveCredentials(cred0) - - const showStyleBases = await fetchShowStyleBasesLight({ - _id: showStyleBaseId, - }) - let access: Access = allAccess(null) - for (const showStyleBase of showStyleBases) { - access = combineAccess(access, AccessRules.accessShowStyleBase(showStyleBase, cred)) - } - return { - ...access, - insert: false, // only allowed through methods - remove: false, // only allowed through methods - } -} -export async function allowAccessToShowStyleVariant( - cred0: Credentials | ResolvedCredentials, - showStyleVariantId: MongoQueryKey -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!showStyleVariantId) return noAccess('showStyleVariantId not set') - const cred = await resolveCredentials(cred0) - - const showStyleVariants = await ShowStyleVariants.findFetchAsync({ - _id: showStyleVariantId, - }) - const showStyleBaseIds = _.uniq(_.map(showStyleVariants, (v) => v.showStyleBaseId)) - const showStyleBases = await fetchShowStyleBasesLight({ - _id: { $in: showStyleBaseIds }, - }) - let access: Access = allAccess(null) - for (const showStyleBase of showStyleBases) { - access = combineAccess(access, AccessRules.accessShowStyleBase(showStyleBase, cred)) - } - return { ...access, document: _.last(showStyleVariants) || null } -} -export async function allowAccessToStudio( - cred0: Credentials | ResolvedCredentials, - studioId: StudioId -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!studioId) return noAccess('studioId not set') - if (!isProtectedString(studioId)) return noAccess('studioId is not a string') - const cred = await resolveCredentials(cred0) - - const studio = await fetchStudioLight(studioId) - if (!studio) return noAccess('Studio not found') - - return { - ...AccessRules.accessStudio(studio, cred), - insert: false, // only allowed through methods - remove: false, // only allowed through methods - } -} -export async function allowAccessToRundownPlaylist( - cred0: Credentials | ResolvedCredentials, - playlistId: RundownPlaylistId -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!playlistId) return noAccess('playlistId not set') - const cred = await resolveCredentials(cred0) - - const playlist = await RundownPlaylists.findOneAsync(playlistId) - if (playlist) { - return AccessRules.accessRundownPlaylist(playlist, cred) - } else { - return allAccess(null) - } -} -export async function allowAccessToRundown( - cred0: Credentials | ResolvedCredentials, - rundownId: MongoQueryKey -): Promise> { - const access = await allowAccessToRundownContent(cred0, rundownId) - return { - ...access, - insert: false, // only allowed through methods - update: false, // only allowed through methods - remove: false, // only allowed through methods - } -} -export async function allowAccessToRundownContent( - cred0: Credentials | ResolvedCredentials, - rundownId: MongoQueryKey -): Promise> { - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - if (!rundownId) return noAccess('rundownId missing') - const cred = await resolveCredentials(cred0) - - const rundowns = await Rundowns.findFetchAsync({ _id: rundownId }) - let access: Access = allAccess(null) - for (const rundown of rundowns) { - // TODO - this is reeally inefficient on db queries - access = combineAccess(access, await AccessRules.accessRundown(rundown, cred)) - } - return access -} -export async function allowAccessToPeripheralDevice( - cred0: Credentials | ResolvedCredentials, - deviceId: PeripheralDeviceId -): Promise> { - if (!deviceId) return noAccess('deviceId missing') - if (!isProtectedString(deviceId)) return noAccess('deviceId is not a string') - - const device = await PeripheralDevices.findOneAsync(deviceId) - if (!device) return noAccess('Device not found') - - const access = await allowAccessToPeripheralDeviceContent(cred0, device) - return { - ...access, - insert: false, // only allowed through methods - remove: false, // only allowed through methods - } -} - -export async function allowAccessToPeripheralDeviceContent( - cred0: Credentials | ResolvedCredentials, - device: PeripheralDevice -): Promise> { - const span = profiler.startSpan('security.lib.security.allowAccessToPeripheralDeviceContent') - if (!Settings.enableUserAccounts) return allAccess(null, 'No security') - const cred = await resolveCredentials(cred0) - - const access = AccessRules.accessPeripheralDevice(device, cred) - - span?.end() - return access -} - -namespace AccessRules { - /** - * Check if access is allowed to the coreSystem collection - * @param cred0 Credentials to check - */ - export function accessCoreSystem(cred: ResolvedCredentials): Access { - if (cred.user && cred.user.superAdmin) { - return { - ...allAccess(null), - insert: false, // only allowed through methods - remove: false, // only allowed through methods - } - } else { - return { - ...noAccess('User is not superAdmin'), - read: true, - } - } - } - - /** - * Check the allowed access to a user (and verify that user is the current user) - * @param cred0 Credentials to check - * @param userId User to check access to - */ - export async function accessCurrentUser( - cred0: Credentials | ResolvedCredentials, - userId: UserId - ): Promise> { - let credUserId: UserId | undefined = undefined - if (isResolvedCredentials(cred0) && cred0.user) { - credUserId = cred0.user._id - } else if (!isResolvedCredentials(cred0) && cred0.userId) { - credUserId = cred0.userId - } else { - const cred = await resolveCredentials(cred0) - if (!cred.user) return noAccess('User in cred not found') - credUserId = cred.user._id - } - - if (credUserId) { - if (credUserId === userId) { - // TODO: user role access - return allAccess(null) - } else return noAccess('Not accessing current user') - } else return noAccess('Requested user not found') - } - - export function accessSystemStatus(_cred0: Credentials | ResolvedCredentials): Access { - // No restrictions on systemStatus - return allAccess(null) - } - // export function accessUser (cred: ResolvedCredentials, user: User): Access { - // if (!cred.organizationId) return noAccess('No organization in credentials') - // if (user.organizationId === cred.organizationId) { - // // TODO: user role access - // return allAccess() - // } else return noAccess('User is not in the same organization as requested user') - // } - export function accessOrganization( - organization: DBOrganization, - cred: ResolvedCredentials - ): Access { - if (!cred.organizationId) return noAccess('No organization in credentials') - if (organization._id === cred.organizationId) { - // TODO: user role access - return allAccess(organization) - } else return noAccess(`User is not in the organization "${organization._id}"`) - } - export function accessShowStyleBase( - showStyleBase: ShowStyleBaseLight, - cred: ResolvedCredentials - ): Access { - if (!showStyleBase.organizationId) return noAccess('ShowStyleBase has no organization') - if (!cred.organizationId) return noAccess('No organization in credentials') - if (showStyleBase.organizationId === cred.organizationId) { - // TODO: user role access - return allAccess(showStyleBase) - } else return noAccess(`User is not in the same organization as the showStyleBase "${showStyleBase._id}"`) - } - export function accessStudio(studio: StudioLight, cred: ResolvedCredentials): Access { - if (!studio.organizationId) return noAccess('Studio has no organization') - if (!cred.organizationId) return noAccess('No organization in credentials') - if (studio.organizationId === cred.organizationId) { - // TODO: user role access - return allAccess(studio) - } else return noAccess(`User is not in the same organization as the studio ${studio._id}`) - } - export async function accessRundownPlaylist( - playlist: DBRundownPlaylist, - cred: ResolvedCredentials - ): Promise> { - const studio = await fetchStudioLight(playlist.studioId) - if (!studio) return noAccess(`Studio of playlist "${playlist._id}" not found`) - return { ...accessStudio(studio, cred), document: playlist } - } - export async function accessRundown(rundown: Rundown, cred: ResolvedCredentials): Promise> { - const playlist = await RundownPlaylists.findOneAsync(rundown.playlistId) - if (!playlist) return noAccess(`Rundown playlist of rundown "${rundown._id}" not found`) - return { ...(await accessRundownPlaylist(playlist, cred)), document: rundown } - } - export function accessPeripheralDevice( - device: PeripheralDevice, - cred: ResolvedCredentials - ): Access { - if (!cred.organizationId) return noAccess('No organization in credentials') - if (!device.organizationId) return noAccess('Device has no organizationId') - if (device.organizationId === cred.organizationId) { - return allAccess(device) - } else return noAccess(`Device "${device._id}" is not in the same organization as user`) - } -} diff --git a/meteor/server/security/noSecurity.ts b/meteor/server/security/noSecurity.ts deleted file mode 100644 index 73236204eb..0000000000 --- a/meteor/server/security/noSecurity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { allowAccessToAnythingWhenSecurityDisabled } from './lib/security' - -export namespace NoSecurityReadAccess { - /** - * Grant read access if security is disabled - */ - export function any(): boolean { - const access = allowAccessToAnythingWhenSecurityDisabled() - if (!access.read) return false // don't even log anything - return true - } -} diff --git a/meteor/server/security/organization.ts b/meteor/server/security/organization.ts deleted file mode 100644 index 8fd686c20a..0000000000 --- a/meteor/server/security/organization.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { SnapshotItem } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' -import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { logNotAllowed } from './lib/lib' -import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' -import { allowAccessToOrganization } from './lib/security' -import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' -import { Settings } from '../Settings' -import { MethodContext } from '../api/methodContext' -import { triggerWriteAccess } from './lib/securityVerify' -import { isProtectedString } from '../lib/tempLib' -import { fetchShowStyleBaseLight, fetchStudioLight, ShowStyleBaseLight } from '../optimizations' -import { - BlueprintId, - OrganizationId, - ShowStyleBaseId, - SnapshotId, - StudioId, - UserId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Blueprints, Snapshots } from '../collections' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' - -export type BasicAccessContext = { organizationId: OrganizationId | null; userId: UserId | null } - -export interface OrganizationContentAccess { - userId: UserId | null - organizationId: OrganizationId | null - cred: ResolvedCredentials | Credentials -} - -export namespace OrganizationReadAccess { - export async function organization( - organizationId: MongoQueryKey, - cred: Credentials | ResolvedCredentials - ): Promise { - return organizationContent(organizationId, cred) - } - /** Handles read access for all organization content (UserActions, Evaluations etc..) */ - export async function organizationContent( - organizationId: MongoQueryKey | undefined, - cred: Credentials | ResolvedCredentials - ): Promise { - if (!Settings.enableUserAccounts) return true - if (!organizationId || !isProtectedString(organizationId)) - throw new Meteor.Error(400, 'selector must contain organizationId') - - const access = await allowAccessToOrganization(cred, organizationId) - if (!access.read) return logNotAllowed('Organization content', access.reason) - - return true - } - export async function adminUsers( - organizationId: MongoQueryKey | undefined, - cred: Credentials | ResolvedCredentials - ): Promise { - // TODO: User roles - return organizationContent(organizationId, cred) - } -} -export namespace OrganizationContentWriteAccess { - // These functions throws if access is not allowed. - - export async function organization( - cred0: Credentials, - organizationId: OrganizationId - ): Promise { - return anyContent(cred0, { organizationId }) - } - - export async function studio( - cred0: Credentials, - existingStudio?: StudioLight | StudioId - ): Promise { - triggerWriteAccess() - if (existingStudio && isProtectedString(existingStudio)) { - const studioId = existingStudio - existingStudio = await fetchStudioLight(studioId) - if (!existingStudio) throw new Meteor.Error(404, `Studio "${studioId}" not found!`) - } - return { ...(await anyContent(cred0, existingStudio)), studio: existingStudio } - } - export async function evaluation(cred0: Credentials): Promise { - return anyContent(cred0) - } - export async function mediaWorkFlows(cred0: Credentials): Promise { - // "All mediaWOrkflows in all devices of an organization" - return anyContent(cred0) - } - export async function blueprint( - cred0: Credentials, - existingBlueprint?: Blueprint | BlueprintId, - allowMissing?: boolean - ): Promise { - triggerWriteAccess() - if (existingBlueprint && isProtectedString(existingBlueprint)) { - const blueprintId = existingBlueprint - existingBlueprint = await Blueprints.findOneAsync(blueprintId) - if (!existingBlueprint && !allowMissing) - throw new Meteor.Error(404, `Blueprint "${blueprintId}" not found!`) - } - return { ...(await anyContent(cred0, existingBlueprint)), blueprint: existingBlueprint } - } - export async function snapshot( - cred0: Credentials, - existingSnapshot?: SnapshotItem | SnapshotId - ): Promise { - triggerWriteAccess() - if (existingSnapshot && isProtectedString(existingSnapshot)) { - const snapshotId = existingSnapshot - existingSnapshot = await Snapshots.findOneAsync(snapshotId) - if (!existingSnapshot) throw new Meteor.Error(404, `Snapshot "${snapshotId}" not found!`) - } - return { ...(await anyContent(cred0, existingSnapshot)), snapshot: existingSnapshot } - } - export async function dataFromSnapshot( - cred0: Credentials, - organizationId: OrganizationId - ): Promise { - return anyContent(cred0, { organizationId: organizationId }) - } - export async function translationBundle( - cred0: Credentials, - existingObj?: { organizationId: OrganizationId | null } - ): Promise { - return anyContent(cred0, existingObj) - } - export async function showStyleBase( - cred0: Credentials, - existingShowStyleBase?: ShowStyleBaseLight | ShowStyleBaseId - ): Promise { - triggerWriteAccess() - if (existingShowStyleBase && isProtectedString(existingShowStyleBase)) { - const showStyleBaseId = existingShowStyleBase - existingShowStyleBase = await fetchShowStyleBaseLight(showStyleBaseId) - if (!existingShowStyleBase) throw new Meteor.Error(404, `ShowStyleBase "${showStyleBaseId}" not found!`) - } - return { ...(await anyContent(cred0, existingShowStyleBase)), showStyleBase: existingShowStyleBase } - } - /** Return credentials if writing is allowed, throw otherwise */ - async function anyContent( - cred0: Credentials | MethodContext, - existingObj?: { organizationId: OrganizationId | null } - ): Promise { - triggerWriteAccess() - if (!Settings.enableUserAccounts) { - return { userId: null, organizationId: null, cred: cred0 } - } - const cred = await resolveCredentials(cred0) - if (!cred.user) throw new Meteor.Error(403, `Not logged in`) - if (!cred.organizationId) throw new Meteor.Error(500, `User has no organization`) - - const access = await allowAccessToOrganization( - cred, - existingObj ? existingObj.organizationId : cred.organizationId - ) - if (!access.update) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - - return { - userId: cred.user._id, - organizationId: cred.organizationId, - cred: cred, - } - } -} diff --git a/meteor/server/security/peripheralDevice.ts b/meteor/server/security/peripheralDevice.ts deleted file mode 100644 index a773b199d1..0000000000 --- a/meteor/server/security/peripheralDevice.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { check } from '../lib/check' -import { PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { isProtectedString } from '../lib/tempLib' -import { logNotAllowed } from './lib/lib' -import { MediaWorkFlow } from '@sofie-automation/shared-lib/dist/core/model/MediaWorkFlows' -import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' -import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' -import { allowAccessToPeripheralDevice, allowAccessToPeripheralDeviceContent } from './lib/security' -import { Settings } from '../Settings' -import { triggerWriteAccess } from './lib/securityVerify' -import { profiler } from '../api/profiler' -import { StudioContentWriteAccess } from './studio' -import { - MediaWorkFlowId, - OrganizationId, - PeripheralDeviceId, - StudioId, - UserId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { MediaWorkFlows, PeripheralDevices } from '../collections' - -export namespace PeripheralDeviceReadAccess { - /** Check for read access for a peripheral device */ - export async function peripheralDevice( - deviceId: MongoQueryKey, - cred: Credentials | ResolvedCredentials - ): Promise { - return peripheralDeviceContent(deviceId, cred) - } - /** Check for read access for all peripheraldevice content (commands, mediaWorkFlows, etc..) */ - export async function peripheralDeviceContent( - deviceId: MongoQueryKey | undefined, - cred: Credentials | ResolvedCredentials - ): Promise { - if (!Settings.enableUserAccounts) return true - if (!deviceId || !isProtectedString(deviceId)) throw new Meteor.Error(400, 'selector must contain deviceId') - - const access = await allowAccessToPeripheralDevice(cred, deviceId) - if (!access.read) return logNotAllowed('PeripheralDevice content', access.reason) - - return true - } -} -export interface MediaWorkFlowContentAccess extends PeripheralDeviceContentWriteAccess.ContentAccess { - mediaWorkFlow: MediaWorkFlow -} - -export namespace PeripheralDeviceContentWriteAccess { - export interface ContentAccess { - userId: UserId | null - organizationId: OrganizationId | null - deviceId: PeripheralDeviceId - device: PeripheralDevice - cred: ResolvedCredentials | Credentials - } - - // These functions throws if access is not allowed. - - /** - * Check if a user is allowed to execute a PeripheralDevice function in a Studio - */ - export async function executeFunction(cred0: Credentials, deviceId: PeripheralDeviceId): Promise { - triggerWriteAccess() - const device = await PeripheralDevices.findOneAsync(deviceId) - if (!device) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) - - let studioId: StudioId - if (device.studioId) { - studioId = device.studioId - } else if (device.parentDeviceId) { - // Child devices aren't assigned to the studio themselves, instead look up the parent device and use it's studioId: - const parentDevice = await PeripheralDevices.findOneAsync(device.parentDeviceId) - if (!parentDevice) - throw new Meteor.Error( - 404, - `Parent PeripheralDevice "${device.parentDeviceId}" of "${deviceId}" not found!` - ) - if (!parentDevice.studioId) - throw new Meteor.Error( - 404, - `Parent PeripheralDevice "${device.parentDeviceId}" of "${deviceId}" doesn't have any studioId set` - ) - studioId = parentDevice.studioId - } else { - throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" doesn't have any studioId set`) - } - - const access = await StudioContentWriteAccess.executeFunction(cred0, studioId) - - const access2 = await allowAccessToPeripheralDeviceContent(access.cred, device) - if (!access2.playout) throw new Meteor.Error(403, `Not allowed: ${access2.reason}`) - - return { - ...access, - deviceId: device._id, - device, - } - } - - /** Check for permission to modify a peripheralDevice */ - export async function peripheralDevice(cred0: Credentials, deviceId: PeripheralDeviceId): Promise { - await backwardsCompatibilityfix(cred0, deviceId) - return anyContent(cred0, deviceId) - } - - /** Check for permission to modify a mediaWorkFlow */ - export async function mediaWorkFlow( - cred0: Credentials, - existingWorkFlow: MediaWorkFlow | MediaWorkFlowId - ): Promise { - triggerWriteAccess() - if (existingWorkFlow && isProtectedString(existingWorkFlow)) { - const workFlowId = existingWorkFlow - const m = await MediaWorkFlows.findOneAsync(workFlowId) - if (!m) throw new Meteor.Error(404, `MediaWorkFlow "${workFlowId}" not found!`) - existingWorkFlow = m - } - await backwardsCompatibilityfix(cred0, existingWorkFlow.deviceId) - return { ...(await anyContent(cred0, existingWorkFlow.deviceId)), mediaWorkFlow: existingWorkFlow } - } - - /** Return credentials if writing is allowed, throw otherwise */ - async function anyContent(cred0: Credentials, deviceId: PeripheralDeviceId): Promise { - const span = profiler.startSpan('PeripheralDeviceContentWriteAccess.anyContent') - triggerWriteAccess() - check(deviceId, String) - const device = await PeripheralDevices.findOneAsync(deviceId) - if (!device) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) - - // If the device has a parent, use that for access control: - const parentDevice = device.parentDeviceId - ? await PeripheralDevices.findOneAsync(device.parentDeviceId) - : device - if (!parentDevice) - throw new Meteor.Error(404, `PeripheralDevice parentDevice "${device.parentDeviceId}" not found`) - - if (!Settings.enableUserAccounts) { - // Note: this is kind of a hack to keep backwards compatibility.. - if (!device.parentDeviceId && parentDevice.token !== cred0.token) { - throw new Meteor.Error(401, `Not allowed access to peripheralDevice`) - } - - span?.end() - return { - userId: null, - organizationId: null, - deviceId: deviceId, - device: device, - cred: cred0, - } - } else { - if (!cred0.userId && parentDevice.token !== cred0.token) { - throw new Meteor.Error(401, `Not allowed access to peripheralDevice`) - } - const cred = await resolveCredentials(cred0) - const access = await allowAccessToPeripheralDeviceContent(cred, parentDevice) - if (!access.update) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - if (!access.document) throw new Meteor.Error(500, `Internal error: access.document not set`) - - span?.end() - return { - userId: cred.user ? cred.user._id : null, - organizationId: cred.organizationId, - deviceId: deviceId, - device: device, - cred: cred, - } - } - } -} -async function backwardsCompatibilityfix(cred0: Credentials, deviceId: PeripheralDeviceId) { - if (!Settings.enableUserAccounts) { - // Note: This is a temporary hack to keep backwards compatibility: - const device = (await PeripheralDevices.findOneAsync(deviceId, { fields: { token: 1 } })) as - | Pick - | undefined - if (device) cred0.token = device.token - } -} diff --git a/meteor/server/security/rundown.ts b/meteor/server/security/rundown.ts deleted file mode 100644 index 8f4bf30ba9..0000000000 --- a/meteor/server/security/rundown.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { check } from '../lib/check' -import * as _ from 'underscore' -import { Credentials, ResolvedCredentials } from './lib/credentials' -import { logNotAllowed } from './lib/lib' -import { allowAccessToRundown } from './lib/security' -import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { ExpectedMediaItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedMediaItem' -import { PeripheralDeviceType, PeripheralDevice } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' -import { ExpectedPlayoutItem } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' -import { Settings } from '../Settings' -import { RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { PeripheralDevices, Segments } from '../collections' -import { getStudioIdFromDevice } from '../api/studio/lib' -import { MongoQuery, MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' - -export namespace RundownReadAccess { - /** Check for read access to the rundown collection */ - export async function rundown( - rundownId: MongoQueryKey, - cred: Credentials | ResolvedCredentials - ): Promise { - return rundownContent(rundownId, cred) - } - /** Check for read access for all rundown content (segments, parts, pieces etc..) */ - export async function rundownContent( - rundownId: MongoQueryKey | undefined, - cred: Credentials | ResolvedCredentials - ): Promise { - if (!Settings.enableUserAccounts) return true - if (!rundownId) throw new Meteor.Error(400, 'selector must contain rundownId') - - const access = await allowAccessToRundown(cred, rundownId) - if (!access.read) return logNotAllowed('Rundown content', access.reason) - - return true - } - /** Check for read access for segments in a rundown */ - export async function segments(segmentId: MongoQueryKey, cred: Credentials): Promise { - if (!Settings.enableUserAccounts) return true - if (!segmentId) throw new Meteor.Error(400, 'selector must contain _id') - - const segments = (await Segments.findFetchAsync(segmentId, { - fields: { - _id: 1, - rundownId: 1, - }, - })) as Array> - const rundownIds = _.uniq(_.map(segments, (s) => s.rundownId)) - - const access = await allowAccessToRundown(cred, { $in: rundownIds }) - if (!access.read) return logNotAllowed('Segments', access.reason) - - return true - } - /** Check for read access for pieces in a rundown */ - export async function pieces(rundownId: MongoQueryKey, cred: Credentials): Promise { - if (!Settings.enableUserAccounts) return true - if (!rundownId) throw new Meteor.Error(400, 'selector must contain rundownId') - - const access = await allowAccessToRundown(cred, rundownId) - if (!access.read) return logNotAllowed('Piece', access.reason) - - return true - } - /** Check for read access for exoected media items in a rundown */ - export async function expectedMediaItems( - selector: MongoQuery | any, - cred: Credentials - ): Promise { - check(selector, Object) - if (selector.mediaFlowId) { - check(selector.mediaFlowId, Object) - check(selector.mediaFlowId.$in, Array) - } - if (!(await rundownContent(selector.rundownId, cred))) return null - - const mediaManagerDevice = await PeripheralDevices.findOneAsync({ - type: PeripheralDeviceType.MEDIA_MANAGER, - token: cred.token, - }) - - if (!mediaManagerDevice) return false - - mediaManagerDevice.studioId = await getStudioIdFromDevice(mediaManagerDevice) - - if (mediaManagerDevice && cred.token) { - // mediaManagerDevice.settings - - return mediaManagerDevice - } else { - // TODO: implement access logic here - // use context.userId - - // just returning true for now - return true - } - } - - /** Check for read access to expectedPlayoutItems */ - export async function expectedPlayoutItems( - selector: MongoQuery | any, - cred: Credentials - ): Promise { - check(selector, Object) - check(selector.studioId, String) - - if (!(await rundownContent(selector.rundownId, cred))) return null - - const playoutDevice = await PeripheralDevices.findOneAsync({ - type: PeripheralDeviceType.PLAYOUT, - token: cred.token, - }) - if (!playoutDevice) return false - - playoutDevice.studioId = await getStudioIdFromDevice(playoutDevice) - - if (playoutDevice && cred.token) { - return playoutDevice - } else { - // TODO: implement access logic here - // just returning true for now - return true - } - } -} diff --git a/meteor/server/security/rundownPlaylist.ts b/meteor/server/security/rundownPlaylist.ts deleted file mode 100644 index 4666e718f8..0000000000 --- a/meteor/server/security/rundownPlaylist.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { check } from '../lib/check' -import { logNotAllowed } from './lib/lib' -import { allowAccessToRundownPlaylist } from './lib/security' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' -import { triggerWriteAccess } from './lib/securityVerify' -import { isProtectedString } from '../lib/tempLib' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { Settings } from '../Settings' -import { - OrganizationId, - RundownId, - RundownPlaylistId, - StudioId, - UserId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RundownPlaylists, Rundowns } from '../collections' - -export namespace RundownPlaylistReadAccess { - /** Handles read access for all playlist document */ - export async function rundownPlaylist( - id: RundownPlaylistId, - cred: Credentials | ResolvedCredentials - ): Promise { - return rundownPlaylistContent(id, cred) - } - /** Handles read access for all playlist content (segments, parts, pieces etc..) */ - export async function rundownPlaylistContent( - id: RundownPlaylistId, - cred: Credentials | ResolvedCredentials - ): Promise { - triggerWriteAccess() - check(id, String) - if (!Settings.enableUserAccounts) return true - if (!id) throw new Meteor.Error(400, 'selector must contain playlistId') - - const access = await allowAccessToRundownPlaylist(cred, id) - if (!access.read) return logNotAllowed('RundownPlaylist content', access.reason) - - return true - } -} - -/** - * This is returned from a check of access to a playlist. - * Fields will be populated about the user, and the playlist if they have permission - */ -export interface RundownPlaylistContentAccess { - userId: UserId | null - organizationId: OrganizationId | null - studioId: StudioId | null - playlist: DBRundownPlaylist | null - cred: ResolvedCredentials | Credentials -} - -/** - * This is returned from a check of access to a rundown. - * Fields will be populated about the user, and the rundown if they have permission - */ -export interface RundownContentAccess { - userId: UserId | null - organizationId: OrganizationId | null - studioId: StudioId | null - rundown: Rundown | null - cred: ResolvedCredentials | Credentials -} - -export namespace RundownPlaylistContentWriteAccess { - /** Access to playout for a playlist, from a rundown. ie the playlist and everything inside it. */ - export async function rundown( - cred0: Credentials, - existingRundown: Rundown | RundownId - ): Promise { - triggerWriteAccess() - if (existingRundown && isProtectedString(existingRundown)) { - const rundownId = existingRundown - const m = await Rundowns.findOneAsync(rundownId) - if (!m) throw new Meteor.Error(404, `Rundown "${rundownId}" not found!`) - existingRundown = m - } - - const access = await anyContent(cred0, existingRundown.playlistId) - return { ...access, rundown: existingRundown } - } - /** Access to playout for a playlist. ie the playlist and everything inside it. */ - export async function playout( - cred0: Credentials, - playlistId: RundownPlaylistId - ): Promise { - return anyContent(cred0, playlistId) - } - /** - * We don't have user levels, so we can use a simple check for all cases - * Return credentials if writing is allowed, throw otherwise - */ - async function anyContent( - cred0: Credentials, - playlistId: RundownPlaylistId - ): Promise { - triggerWriteAccess() - if (!Settings.enableUserAccounts) { - const playlist = await RundownPlaylists.findOneAsync(playlistId) - return { - userId: null, - organizationId: null, - studioId: playlist?.studioId || null, - playlist: playlist || null, - cred: cred0, - } - } - const cred = await resolveCredentials(cred0) - if (!cred.user) throw new Meteor.Error(403, `Not logged in`) - if (!cred.organizationId) throw new Meteor.Error(500, `User has no organization`) - const access = await allowAccessToRundownPlaylist(cred, playlistId) - if (!access.update) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - - return { - userId: cred.user._id, - organizationId: cred.organizationId, - studioId: access.document?.studioId || null, - playlist: access.document, - cred: cred, - } - } -} diff --git a/meteor/server/security/lib/securityVerify.ts b/meteor/server/security/securityVerify.ts similarity index 99% rename from meteor/server/security/lib/securityVerify.ts rename to meteor/server/security/securityVerify.ts index edde48cb35..e7edc63cfc 100644 --- a/meteor/server/security/lib/securityVerify.ts +++ b/meteor/server/security/securityVerify.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor' -import { AllMeteorMethods, suppressExtraErrorLogging } from '../../methods' -import { disableChecks, enableChecks as restoreChecks } from '../../lib/check' +import { AllMeteorMethods, suppressExtraErrorLogging } from '../methods' +import { disableChecks, enableChecks as restoreChecks } from '../lib/check' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' /** These function are used to verify that all methods defined are using security functions */ diff --git a/meteor/server/security/showStyle.ts b/meteor/server/security/showStyle.ts deleted file mode 100644 index bd3e83811c..0000000000 --- a/meteor/server/security/showStyle.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { check } from '../lib/check' -import { logNotAllowed } from './lib/lib' -import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { RundownLayoutBase } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { MongoQuery, MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' -import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' -import { allowAccessToShowStyleBase, allowAccessToShowStyleVariant } from './lib/security' -import { triggerWriteAccess } from './lib/securityVerify' -import { Settings } from '../Settings' -import { isProtectedString } from '../lib/tempLib' -import { TriggeredActionsObj } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' -import { SystemWriteAccess } from './system' -import { fetchShowStyleBaseLight, ShowStyleBaseLight } from '../optimizations' -import { - OrganizationId, - RundownLayoutId, - ShowStyleBaseId, - ShowStyleVariantId, - TriggeredActionId, - UserId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { RundownLayouts, ShowStyleVariants, TriggeredActions } from '../collections' - -export interface ShowStyleContentAccess { - userId: UserId | null - organizationId: OrganizationId | null - showStyleBaseId: ShowStyleBaseId | null - showStyleBase: ShowStyleBaseLight | null - cred: ResolvedCredentials | Credentials -} - -export namespace ShowStyleReadAccess { - /** Handles read access for all showstyle document */ - export async function showStyleBase( - showStyleBaseId: MongoQueryKey, - cred: Credentials | ResolvedCredentials - ): Promise { - return showStyleBaseContent({ showStyleBaseId }, cred) - } - - /** Handles read access for all showstyle content */ - export async function showStyleBaseContent( - selector: MongoQuery, - cred: Credentials | ResolvedCredentials - ): Promise { - check(selector, Object) - if (!Settings.enableUserAccounts) return true - if (!selector.showStyleBaseId || !isProtectedString(selector.showStyleBaseId)) - throw new Meteor.Error(400, 'selector must contain showStyleBaseId') - - const access = await allowAccessToShowStyleBase(cred, selector.showStyleBaseId) - if (!access.read) return logNotAllowed('ShowStyleBase content', access.reason) - - return true - } - - /** Check for read access to the showstyle variants */ - export async function showStyleVariant( - showStyleVariantId: MongoQueryKey, - cred: Credentials | ResolvedCredentials - ): Promise { - if (!Settings.enableUserAccounts) return true - if (!showStyleVariantId) throw new Meteor.Error(400, 'selector must contain _id') - - const access = await allowAccessToShowStyleVariant(cred, showStyleVariantId) - if (!access.read) return logNotAllowed('ShowStyleVariant', access.reason) - - return true - } -} -export namespace ShowStyleContentWriteAccess { - // These functions throws if access is not allowed. - - /** Check permissions for write access to a showStyleVariant */ - export async function showStyleVariant( - cred0: Credentials, - existingVariant: DBShowStyleVariant | ShowStyleVariantId - ): Promise { - triggerWriteAccess() - if (existingVariant && isProtectedString(existingVariant)) { - const variantId = existingVariant - const m = await ShowStyleVariants.findOneAsync(variantId) - if (!m) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found!`) - existingVariant = m - } - return { ...(await anyContent(cred0, existingVariant.showStyleBaseId)), showStyleVariant: existingVariant } - } - /** Check permissions for write access to a rundownLayout */ - export async function rundownLayout( - cred0: Credentials, - existingLayout: RundownLayoutBase | RundownLayoutId - ): Promise { - triggerWriteAccess() - if (existingLayout && isProtectedString(existingLayout)) { - const layoutId = existingLayout - const m = await RundownLayouts.findOneAsync(layoutId) - if (!m) throw new Meteor.Error(404, `RundownLayout "${layoutId}" not found!`) - existingLayout = m - } - return { ...(await anyContent(cred0, existingLayout.showStyleBaseId)), rundownLayout: existingLayout } - } - /** Check permissions for write access to a triggeredAction */ - export async function triggeredActions( - cred0: Credentials, - existingTriggeredAction: TriggeredActionsObj | TriggeredActionId - ): Promise<(ShowStyleContentAccess & { triggeredActions: TriggeredActionsObj }) | boolean> { - triggerWriteAccess() - if (existingTriggeredAction && isProtectedString(existingTriggeredAction)) { - const layoutId = existingTriggeredAction - const m = await TriggeredActions.findOneAsync(layoutId) - if (!m) throw new Meteor.Error(404, `RundownLayout "${layoutId}" not found!`) - existingTriggeredAction = m - } - if (existingTriggeredAction.showStyleBaseId) { - return { - ...(await anyContent(cred0, existingTriggeredAction.showStyleBaseId)), - triggeredActions: existingTriggeredAction, - } - } else { - return SystemWriteAccess.coreSystem(cred0) - } - } - /** Return credentials if writing is allowed, throw otherwise */ - export async function anyContent( - cred0: Credentials, - showStyleBaseId: ShowStyleBaseId - ): Promise { - triggerWriteAccess() - if (!Settings.enableUserAccounts) { - return { - userId: null, - organizationId: null, - showStyleBaseId: showStyleBaseId, - showStyleBase: (await fetchShowStyleBaseLight(showStyleBaseId)) || null, - cred: cred0, - } - } - const cred = await resolveCredentials(cred0) - if (!cred.user) throw new Meteor.Error(403, `Not logged in`) - if (!cred.organizationId) throw new Meteor.Error(500, `User has no organization`) - - const access = await allowAccessToShowStyleBase(cred, showStyleBaseId) - if (!access.update) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - - return { - userId: cred.user._id, - organizationId: cred.organizationId, - showStyleBaseId: showStyleBaseId, - showStyleBase: access.document, - cred: cred, - } - } -} diff --git a/meteor/server/security/studio.ts b/meteor/server/security/studio.ts deleted file mode 100644 index 3b52624f84..0000000000 --- a/meteor/server/security/studio.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { allowAccessToStudio } from './lib/security' -import { MongoQueryKey } from '@sofie-automation/corelib/dist/mongo' -import { logNotAllowed } from './lib/lib' -import { ExternalMessageQueueObj } from '@sofie-automation/corelib/dist/dataModel/ExternalMessageQueue' -import { Credentials, ResolvedCredentials, resolveCredentials } from './lib/credentials' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Settings } from '../Settings' -import { triggerWriteAccess } from './lib/securityVerify' -import { isProtectedString } from '../lib/tempLib' -import { fetchStudioLight } from '../optimizations' -import { - ExternalMessageQueueObjId, - OrganizationId, - RundownPlaylistId, - StudioId, - UserId, -} from '@sofie-automation/corelib/dist/dataModel/Ids' -import { ExternalMessageQueue, RundownPlaylists } from '../collections' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' - -export namespace StudioReadAccess { - /** Handles read access for all studio document */ - export async function studio( - studioId: MongoQueryKey, - cred: Credentials | ResolvedCredentials - ): Promise { - return studioContent(studioId, cred) - } - /** Handles read access for all studioId content */ - export async function studioContent( - studioId: MongoQueryKey | undefined, - cred: Credentials | ResolvedCredentials - ): Promise { - if (!Settings.enableUserAccounts) return true - if (!studioId || !isProtectedString(studioId)) throw new Meteor.Error(400, 'selector must contain studioId') - - const access = await allowAccessToStudio(cred, studioId) - if (!access.read) return logNotAllowed('Studio content', access.reason) - - return true - } -} - -/** - * This is returned from a check of access to a studio. - * Fields will be populated about the user, and the studio if they have permission - */ -export interface StudioContentAccess { - userId: UserId | null - organizationId: OrganizationId | null - studioId: StudioId - studio: StudioLight - cred: ResolvedCredentials | Credentials -} - -export interface ExternalMessageContentAccess extends StudioContentAccess { - message: ExternalMessageQueueObj -} - -export namespace StudioContentWriteAccess { - // These functions throws if access is not allowed. - - export async function rundownPlaylist( - cred0: Credentials, - existingPlaylist: DBRundownPlaylist | RundownPlaylistId - ): Promise { - triggerWriteAccess() - if (existingPlaylist && isProtectedString(existingPlaylist)) { - const playlistId = existingPlaylist - const m = await RundownPlaylists.findOneAsync(playlistId) - if (!m) throw new Meteor.Error(404, `RundownPlaylist "${playlistId}" not found!`) - existingPlaylist = m - } - return { ...(await anyContent(cred0, existingPlaylist.studioId)), playlist: existingPlaylist } - } - - /** Check for permission to restore snapshots into the studio */ - export async function dataFromSnapshot(cred0: Credentials, studioId: StudioId): Promise { - return anyContent(cred0, studioId) - } - - /** Check for permission to select active routesets in the studio */ - export async function routeSet(cred0: Credentials, studioId: StudioId): Promise { - return anyContent(cred0, studioId) - } - - export async function timelineDatastore(cred0: Credentials, studioId: StudioId): Promise { - return anyContent(cred0, studioId) - } - /** Check for permission to update the studio baseline */ - export async function baseline(cred0: Credentials, studioId: StudioId): Promise { - return anyContent(cred0, studioId) - } - - /** Check for permission to modify a bucket or its contents belonging to the studio */ - export async function bucket(cred0: Credentials, studioId: StudioId): Promise { - return anyContent(cred0, studioId) - } - - /** Check for permission to execute a function on a PeripheralDevice in the studio */ - export async function executeFunction(cred0: Credentials, studioId: StudioId): Promise { - return anyContent(cred0, studioId) - } - - /** Check for permission to modify an ExternalMessageQueueObj */ - export async function externalMessage( - cred0: Credentials, - existingMessage: ExternalMessageQueueObj | ExternalMessageQueueObjId - ): Promise { - triggerWriteAccess() - if (existingMessage && isProtectedString(existingMessage)) { - const messageId = existingMessage - const m = await ExternalMessageQueue.findOneAsync(messageId) - if (!m) throw new Meteor.Error(404, `ExternalMessage "${messageId}" not found!`) - existingMessage = m - } - return { ...(await anyContent(cred0, existingMessage.studioId)), message: existingMessage } - } - - /** - * We don't have user levels, so we can use a simple check for all cases - * Return credentials if writing is allowed, throw otherwise - */ - async function anyContent(cred0: Credentials, studioId: StudioId): Promise { - triggerWriteAccess() - if (!Settings.enableUserAccounts) { - const studio = await fetchStudioLight(studioId) - if (!studio) throw new Meteor.Error(404, `Studio "${studioId}" not found`) - - return { - userId: null, - organizationId: null, - studioId: studioId, - studio: studio, - cred: cred0, - } - } - const cred = await resolveCredentials(cred0) - if (!cred.user) throw new Meteor.Error(403, `Not logged in`) - if (!cred.organizationId) throw new Meteor.Error(500, `User has no organization`) - - const access = await allowAccessToStudio(cred, studioId) - if (!access.update) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - if (!access.document) throw new Meteor.Error(404, `Studio "${studioId}" not found`) - - return { - userId: cred.user._id, - organizationId: cred.organizationId, - studioId: studioId, - studio: access.document, - cred: cred, - } - } -} diff --git a/meteor/server/security/system.ts b/meteor/server/security/system.ts deleted file mode 100644 index d7d13b760e..0000000000 --- a/meteor/server/security/system.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { Credentials, resolveAuthenticatedUser, resolveCredentials } from './lib/credentials' -import { logNotAllowed } from './lib/lib' -import { allowAccessToCoreSystem, allowAccessToCurrentUser, allowAccessToSystemStatus } from './lib/security' -import { Settings } from '../Settings' -import { triggerWriteAccess } from './lib/securityVerify' -import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' - -export namespace SystemReadAccess { - /** Handles read access for all organization content (segments, parts, pieces etc..) */ - export async function coreSystem(cred0: Credentials): Promise { - const cred = await resolveCredentials(cred0) - - const access = await allowAccessToCoreSystem(cred) - if (!access.read) return logNotAllowed('CoreSystem', access.reason) - - return true - } - /** Check if access is allowed to read a User, and that user is the current User */ - export async function currentUser(userId: UserId, cred: Credentials): Promise { - const access = await allowAccessToCurrentUser(cred, userId) - if (!access.read) return logNotAllowed('Current user', access.reason) - - return true - } - /** Check permissions to get the system status */ - export async function systemStatus(cred0: Credentials): Promise { - // For reading only - triggerWriteAccess() - const access = await allowAccessToSystemStatus(cred0) - if (!access.read) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - - return true - } -} -export namespace SystemWriteAccess { - // These functions throws if access is not allowed. - - export async function coreSystem(cred0: Credentials): Promise { - triggerWriteAccess() - if (!Settings.enableUserAccounts) return true - const cred = await resolveAuthenticatedUser(cred0) - if (!cred) throw new Meteor.Error(403, `Not logged in`) - - const access = await allowAccessToCoreSystem(cred) - if (!access.configure) throw new Meteor.Error(403, `Not allowed: ${access.reason}`) - - return true - } - /** Check if access is allowed to modify a User, and that user is the current User */ - export async function currentUser(userId: UserId | null, cred: Credentials): Promise { - const access = await allowAccessToCurrentUser(cred, userId) - if (!access.update) return logNotAllowed('Current user', access.reason) - - return true - } - /** Check permissions to run migrations of all types */ - export async function migrations(cred0: Credentials): Promise { - return coreSystem(cred0) - } - /** Check permissions to perform a system-level action */ - export async function systemActions(cred0: Credentials): Promise { - return coreSystem(cred0) - } -} diff --git a/meteor/server/security/translationsBundles.ts b/meteor/server/security/translationsBundles.ts deleted file mode 100644 index b7733f1517..0000000000 --- a/meteor/server/security/translationsBundles.ts +++ /dev/null @@ -1,8 +0,0 @@ -export namespace TranslationsBundlesSecurity { - export function allowReadAccess(_selector: object, _token: string | undefined, _context: unknown): boolean { - return true - } - export function allowWriteAccess(): boolean { - return false - } -} diff --git a/meteor/server/systemStatus/api.ts b/meteor/server/systemStatus/api.ts index d81b351114..6a95a37388 100644 --- a/meteor/server/systemStatus/api.ts +++ b/meteor/server/systemStatus/api.ts @@ -6,7 +6,6 @@ import { } from '@sofie-automation/meteor-lib/dist/api/systemStatus' import { getDebugStates, getSystemStatus } from './systemStatus' import { protectString } from '../lib/tempLib' -import { Settings } from '../Settings' import { MethodContextAPI } from '../api/methodContext' import { profiler } from '../api/profiler' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -22,53 +21,38 @@ const apmNamespace = 'http' export const metricsRouter = new KoaRouter() export const healthRouter = new KoaRouter() -if (!Settings.enableUserAccounts) { - // For backwards compatibility: +metricsRouter.get('/', async (ctx) => { + const transaction = profiler.startTransaction('metrics', apmNamespace) + try { + ctx.response.type = PrometheusHTTPContentType - metricsRouter.get('/', async (ctx) => { - const transaction = profiler.startTransaction('metrics', apmNamespace) - try { - ctx.response.type = PrometheusHTTPContentType + const [meteorMetrics, workerMetrics] = await Promise.all([ + getPrometheusMetricsString(), + collectWorkerPrometheusMetrics(), + ]) - const [meteorMetrics, workerMetrics] = await Promise.all([ - getPrometheusMetricsString(), - collectWorkerPrometheusMetrics(), - ]) - - ctx.body = [meteorMetrics, ...workerMetrics].join('\n\n') - } catch (ex) { - ctx.response.status = 500 - ctx.body = ex + '' - } - transaction?.end() - }) - - healthRouter.get('/', async (ctx) => { - const transaction = profiler.startTransaction('health', apmNamespace) - const status = await getSystemStatus({ userId: null }) - health(status, ctx) - transaction?.end() - }) + ctx.body = [meteorMetrics, ...workerMetrics].join('\n\n') + } catch (ex) { + ctx.response.status = 500 + ctx.body = ex + '' + } + transaction?.end() +}) - healthRouter.get('/:studioId', async (ctx) => { - const transaction = profiler.startTransaction('health', apmNamespace) - const status = await getSystemStatus({ userId: null }, protectString(ctx.params.studioId)) - health(status, ctx) - transaction?.end() - }) -} -healthRouter.get('/:token', async (ctx) => { +healthRouter.get('/', async (ctx) => { const transaction = profiler.startTransaction('health', apmNamespace) - const status = await getSystemStatus({ userId: null, token: ctx.params.token }) + const status = await getSystemStatus(ctx) health(status, ctx) transaction?.end() }) -healthRouter.get('/:token/:studioId', async (ctx) => { + +healthRouter.get('/:studioId', async (ctx) => { const transaction = profiler.startTransaction('health', apmNamespace) - const status = await getSystemStatus({ userId: null, token: ctx.params.token }, protectString(ctx.params.studioId)) + const status = await getSystemStatus(ctx, protectString(ctx.params.studioId)) health(status, ctx) transaction?.end() }) + function health(status: StatusResponse, ctx: Koa.ParameterizedContext) { ctx.response.type = 'application/json' @@ -79,7 +63,7 @@ function health(status: StatusResponse, ctx: Koa.ParameterizedContext) { class ServerSystemStatusAPI extends MethodContextAPI implements NewSystemStatusAPI { async getSystemStatus() { - return getSystemStatus(this) + return getSystemStatus(this.connection) } async getDebugStates(peripheralDeviceId: PeripheralDeviceId) { diff --git a/meteor/server/systemStatus/systemStatus.ts b/meteor/server/systemStatus/systemStatus.ts index 9e48106430..34ae34ce49 100644 --- a/meteor/server/systemStatus/systemStatus.ts +++ b/meteor/server/systemStatus/systemStatus.ts @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor' import { PeripheralDevice, PERIPHERAL_SUBTYPE_PROCESS } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { Time, getRandomId, literal } from '../lib/tempLib' import { getCurrentTime } from '../lib/lib' @@ -18,19 +17,15 @@ import { Component, } from '@sofie-automation/meteor-lib/dist/api/systemStatus' import { RelevantSystemVersions } from '../coreSystem' -import { Settings } from '../Settings' -import { StudioReadAccess } from '../security/studio' -import { OrganizationReadAccess } from '../security/organization' -import { resolveCredentials, Credentials } from '../security/lib/credentials' -import { SystemReadAccess } from '../security/system' import { StatusCode } from '@sofie-automation/blueprints-integration' import { PeripheralDevices, Workers, WorkerThreadStatuses } from '../collections' import { PeripheralDeviceId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ServerPeripheralDeviceAPI } from '../api/peripheralDevice' -import { PeripheralDeviceContentWriteAccess } from '../security/peripheralDevice' import { MethodContext } from '../api/methodContext' import { getBlueprintVersions } from './blueprintVersions' import { getUpgradeSystemStatusMessages } from './blueprintUpgradeStatus' +import { triggerWriteAccessBecauseNoCheckNecessary } from '../security/securityVerify' +import { assertConnectionHasOneOfPermissions, RequestCredentials } from '../security/auth' const PackageInfo = require('../../package.json') const integrationVersionRange = parseCoreIntegrationCompatabilityRange(PackageInfo.version) @@ -166,10 +161,12 @@ function getSystemStatusForDevice(device: PeripheralDevice): StatusResponse { * Returns system status * @param studioId (Optional) If provided, limits the status to what's affecting the studio */ -export async function getSystemStatus(cred0: Credentials, studioId?: StudioId): Promise { - const checks: Array = [] +export async function getSystemStatus(_cred: RequestCredentials | null, studioId?: StudioId): Promise { + // Future: this should consider the studioId + // For now, all users should have access to all statuses + triggerWriteAccessBecauseNoCheckNecessary() - await SystemReadAccess.systemStatus(cred0) + const checks: Array = [] // Check systemStatuses: for (const [key, status] of Object.entries(systemStatuses)) { @@ -251,25 +248,11 @@ export async function getSystemStatus(cred0: Credentials, studioId?: StudioId): if (studioId) { // Check status for a certain studio: - if (!(await StudioReadAccess.studioContent(studioId, cred0))) { - throw new Meteor.Error(403, `Not allowed`) - } devices = await PeripheralDevices.findFetchAsync({ studioId: studioId }) } else { - if (Settings.enableUserAccounts) { - // Check status for the user's studios: + // Check status for all studios: - const cred = await resolveCredentials(cred0) - if (!cred.organizationId) throw new Meteor.Error(500, 'user has no organization') - if (!(await OrganizationReadAccess.organizationContent(cred.organizationId, cred))) { - throw new Meteor.Error(403, `Not allowed`) - } - devices = await PeripheralDevices.findFetchAsync({ organizationId: cred.organizationId }) - } else { - // Check status for all studios: - - devices = await PeripheralDevices.findFetchAsync({}) - } + devices = await PeripheralDevices.findFetchAsync({}) } for (const device of devices) { const so = getSystemStatusForDevice(device) @@ -405,6 +388,7 @@ export async function getDebugStates( methodContext: MethodContext, peripheralDeviceId: PeripheralDeviceId ): Promise { - const access = await PeripheralDeviceContentWriteAccess.peripheralDevice(methodContext, peripheralDeviceId) - return ServerPeripheralDeviceAPI.getDebugStates(access) + assertConnectionHasOneOfPermissions(methodContext.connection, 'developer') + + return ServerPeripheralDeviceAPI.getDebugStates(peripheralDeviceId) } diff --git a/meteor/server/worker/worker.ts b/meteor/server/worker/worker.ts index 6a4b8651cf..cdc1bbbb6c 100644 --- a/meteor/server/worker/worker.ts +++ b/meteor/server/worker/worker.ts @@ -21,6 +21,7 @@ import { initializeWorkerStatus, setWorkerStatus } from './workerStatus' import { MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { UserActionsLog } from '../collections' import { MetricsCounter } from '@sofie-automation/corelib/dist/prometheus' +import { isInTestWrite } from '../security/securityVerify' const FREEZE_LIMIT = 1000 // how long to wait for a response to a Ping const RESTART_TIMEOUT = 30000 // how long to wait for a restart to complete before throwing an error @@ -459,6 +460,7 @@ export async function QueueStudioJob( studioId: StudioId, jobParameters: Parameters[0] ): Promise>> { + if (isInTestWrite()) throw new Meteor.Error(404, 'Should not be reachable during startup tests') if (!studioId) throw new Meteor.Error(500, 'Missing studioId') const now = getCurrentTime() diff --git a/packages/corelib/src/dataModel/Collections.ts b/packages/corelib/src/dataModel/Collections.ts index 670bdfcd44..0032e85b0c 100644 --- a/packages/corelib/src/dataModel/Collections.ts +++ b/packages/corelib/src/dataModel/Collections.ts @@ -44,7 +44,6 @@ export enum CollectionName { TriggeredActions = 'triggeredActions', TranslationsBundles = 'translationsBundles', UserActionsLog = 'userActionsLog', - Users = 'Users', Workers = 'workers', WorkerThreads = 'workersThreads', } diff --git a/packages/meteor-lib/src/Settings.ts b/packages/meteor-lib/src/Settings.ts index 82965e4aa9..347fd04f84 100644 --- a/packages/meteor-lib/src/Settings.ts +++ b/packages/meteor-lib/src/Settings.ts @@ -15,8 +15,8 @@ export interface ISettings { defaultTimeScale: number // Allow grabbing the entire timeline allowGrabbingTimeline: boolean - /** If true, enables security measures, access control and user accounts. */ - enableUserAccounts: boolean + /** If true, enable http header based security measures */ + enableHeaderAuth: boolean /** Default duration to use to render parts when no duration is provided */ defaultDisplayDuration: number /** How many segments of history to show when scrolling back in time (0 = show current segment only) */ @@ -59,7 +59,7 @@ export const DEFAULT_SETTINGS = Object.freeze({ disableBlurBorder: false, defaultTimeScale: 1, allowGrabbingTimeline: true, - enableUserAccounts: false, + enableHeaderAuth: false, defaultDisplayDuration: 3000, poisonKey: 'Escape', followOnAirSegmentsHistory: 0, diff --git a/packages/meteor-lib/src/api/pubsub.ts b/packages/meteor-lib/src/api/pubsub.ts index 9ef1d945e6..ca8522ccce 100644 --- a/packages/meteor-lib/src/api/pubsub.ts +++ b/packages/meteor-lib/src/api/pubsub.ts @@ -19,7 +19,6 @@ import { SnapshotItem } from '../collections/Snapshots' import { TranslationsBundle } from '../collections/TranslationsBundles' import { DBTriggeredActions, UITriggeredActionsObj } from '../collections/TriggeredActions' import { UserActionsLogItem } from '../collections/UserActionsLog' -import { DBUser } from '../collections/Users' import { UIBucketContentStatus, UIPieceContentStatus, UISegmentPartNote } from './rundownNotifications' import { UIShowStyleBase } from './showStyles' import { UIStudio } from './studios' @@ -208,8 +207,6 @@ export interface MeteorPubSubTypes { showStyleBaseIds: ShowStyleBaseId[] | null, token?: string ) => CollectionName.RundownLayouts - [MeteorPubSub.loggedInUser]: (token?: string) => CollectionName.Users - [MeteorPubSub.usersInOrganization]: (organizationId: OrganizationId, token?: string) => CollectionName.Users [MeteorPubSub.organization]: (organizationId: OrganizationId | null, token?: string) => CollectionName.Organizations [MeteorPubSub.buckets]: (studioId: StudioId, bucketId: BucketId | null, token?: string) => CollectionName.Buckets [MeteorPubSub.translationsBundles]: (token?: string) => CollectionName.TranslationsBundles @@ -282,7 +279,6 @@ export type MeteorPubSubCollections = { [CollectionName.Organizations]: DBOrganization [CollectionName.Buckets]: Bucket [CollectionName.TranslationsBundles]: TranslationsBundle - [CollectionName.Users]: DBUser [CollectionName.ExpectedPlayoutItems]: ExpectedPlayoutItem [CollectionName.MediaWorkFlows]: MediaWorkFlow diff --git a/packages/meteor-lib/src/api/user.ts b/packages/meteor-lib/src/api/user.ts index f1f737f271..83881d2819 100644 --- a/packages/meteor-lib/src/api/user.ts +++ b/packages/meteor-lib/src/api/user.ts @@ -1,33 +1,8 @@ -import { UserProfile } from '../collections/Users' -import { UserId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { UserPermissions } from '../userPermissions' export interface NewUserAPI { - enrollUser(email: string, name: string): Promise - requestPasswordReset(email: string): Promise - removeUser(): Promise + getUserPermissions(): Promise } export enum UserAPIMethods { - 'enrollUser' = 'user.enrollUser', - 'requestPasswordReset' = 'user.requestPasswordReset', - 'removeUser' = 'user.removeUser', -} - -export interface CreateNewUserData { - email: string - profile: UserProfile - password?: string - createOrganization?: { - name: string - applications: string[] - broadcastMediums: string[] - } -} -export async function createUser(_newUser: CreateNewUserData): Promise { - // This is available both client-side and server side. - // The reason for that is that the client-side should use Accounts.createUser right away - // so that the password aren't sent in "plaintext" to the server. - - // const userId = await Accounts.createUserAsync(newUser) - // return protectString(userId) - throw new Error('Not implemented') + 'getUserPermissions' = 'user.getUserPermissions', } diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index 91f521b617..01db5ba8fe 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -212,16 +212,19 @@ export interface NewUserActionAPI { mediaRestartWorkflow( userEvent: string, eventTime: Time, + deviceId: PeripheralDeviceId, workflowId: MediaWorkFlowId ): Promise> mediaAbortWorkflow( userEvent: string, eventTime: Time, + deviceId: PeripheralDeviceId, workflowId: MediaWorkFlowId ): Promise> mediaPrioritizeWorkflow( userEvent: string, eventTime: Time, + deviceId: PeripheralDeviceId, workflowId: MediaWorkFlowId ): Promise> mediaRestartAllWorkflows(userEvent: string, eventTime: Time): Promise> diff --git a/packages/meteor-lib/src/collections/Users.ts b/packages/meteor-lib/src/collections/Users.ts deleted file mode 100644 index 04d5b6b887..0000000000 --- a/packages/meteor-lib/src/collections/Users.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { UserId, OrganizationId } from '@sofie-automation/corelib/dist/dataModel/Ids' - -export interface UserProfile { - name: string -} - -export interface DBUser { - // Note: This interface is partly defined by the dataset from the Meteor.users collection - - _id: UserId - createdAt: string - services: { - password: { - bcrypt: string - } - } - username: string - emails: [ - { - address: string - verified: boolean - } - ] - profile: UserProfile - organizationId: OrganizationId - superAdmin?: boolean -} - -export type User = DBUser // to be replaced by a class somet ime later? diff --git a/packages/meteor-lib/src/userPermissions.ts b/packages/meteor-lib/src/userPermissions.ts new file mode 100644 index 0000000000..2d0f1246f3 --- /dev/null +++ b/packages/meteor-lib/src/userPermissions.ts @@ -0,0 +1,58 @@ +/** + * The header to use for user permissions + * This is currently limited to a small set that sockjs supports: https://github.com/sockjs/sockjs-node/blob/46d2f846653a91822a02794b852886c7f137378c/lib/session.js#L137-L150 + * Any other headers are not exposed in a way we can access, no matter how deep we look into meteor internals. + */ +export const USER_PERMISSIONS_HEADER = 'dnt' + +export interface UserPermissions { + studio: boolean + configure: boolean + developer: boolean + testing: boolean + service: boolean + gateway: boolean +} +const allowedPermissions = new Set([ + 'studio', + 'configure', + 'developer', + 'testing', + 'service', + 'gateway', +]) + +export function parseUserPermissions(encodedPermissions: string | undefined): UserPermissions { + if (encodedPermissions === 'admin') { + return { + studio: true, + configure: true, + developer: true, + testing: true, + service: true, + gateway: true, + } + } + + const result: UserPermissions = { + studio: false, + configure: false, + developer: false, + testing: false, + service: false, + gateway: false, + } + + if (encodedPermissions && typeof encodedPermissions === 'string') { + const parts = encodedPermissions.split(',') + + for (const part of parts) { + const part2 = part.trim() as keyof UserPermissions + if (allowedPermissions.has(part2)) { + result[part2] = true + } + } + } + + return result +} diff --git a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts index 80dd4dd366..1f8a142f7b 100644 --- a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts +++ b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts @@ -116,7 +116,7 @@ export interface NewPeripheralDeviceAPI { timelineTriggerTime(deviceId: PeripheralDeviceId, deviceToken: string, r: TimelineTriggerTimeResult): Promise requestUserAuthToken(deviceId: PeripheralDeviceId, deviceToken: string, authUrl: string): Promise storeAccessToken(deviceId: PeripheralDeviceId, deviceToken: string, authToken: string): Promise - removePeripheralDevice(deviceId: PeripheralDeviceId): Promise + removePeripheralDevice(deviceId: PeripheralDeviceId, deviceToken?: string): Promise reportResolveDone( deviceId: PeripheralDeviceId, deviceToken: string, diff --git a/packages/webui/src/__mocks__/meteor.ts b/packages/webui/src/__mocks__/meteor.ts index f11ac9b999..170ed3d5dd 100644 --- a/packages/webui/src/__mocks__/meteor.ts +++ b/packages/webui/src/__mocks__/meteor.ts @@ -1,5 +1,4 @@ import * as _ from 'underscore' -import { MongoMock } from './mongo' import type { DDP } from 'meteor/ddp' let controllableDefer = false @@ -11,7 +10,7 @@ export function useNextTickDefer(): void { controllableDefer = false } -namespace Meteor { +export namespace Meteor { export interface Settings { public: { [id: string]: any @@ -19,19 +18,6 @@ namespace Meteor { [id: string]: any } - export interface UserEmail { - address: string - verified: boolean - } - export interface User { - _id?: string - username?: string - emails?: UserEmail[] - createdAt?: number - profile?: any - services?: any - } - export interface ErrorStatic { new (error: string | number, reason?: string, details?: string): Error } @@ -89,7 +75,6 @@ export namespace MeteorMock { export const settings: any = {} export const mockMethods: { [name: string]: Function } = {} - export let mockUser: Meteor.User | undefined = undefined export const mockStartupFunctions: Function[] = [] export function status(): DDP.DDPStatus { @@ -100,15 +85,8 @@ export namespace MeteorMock { } } - export function user(): Meteor.User | undefined { - return mockUser - } - export function userId(): string | undefined { - return mockUser ? mockUser._id : undefined - } function getMethodContext() { return { - userId: mockUser ? mockUser._id : undefined, connection: { clientAddress: '1.1.1.1', }, @@ -223,7 +201,6 @@ export namespace MeteorMock { export function bindEnvironment(_fcn: Function): any { throw new Error(500, 'bindEnvironment not supported on client') } - export let users: MongoMock.Collection | undefined = undefined // -- Mock functions: -------------------------- /** @@ -236,12 +213,6 @@ export namespace MeteorMock { await waitTimeNoFakeTimers(10) // So that any observers or defers has had time to run. } - export function mockLoginUser(newUser: Meteor.User): void { - mockUser = newUser - } - export function mockSetUsersCollection(usersCollection: MongoMock.Collection): void { - users = usersCollection - } /** Wait for time to pass ( unaffected by jest.useFakeTimers() ) */ export async function sleepNoFakeTimers(time: number): Promise { diff --git a/packages/webui/src/__mocks__/mongo.ts b/packages/webui/src/__mocks__/mongo.ts index 2f31d6400b..7a0d8566cb 100644 --- a/packages/webui/src/__mocks__/mongo.ts +++ b/packages/webui/src/__mocks__/mongo.ts @@ -349,5 +349,3 @@ export function setup(): any { Mongo: MongoMock, } } - -MeteorMock.mockSetUsersCollection(new MongoMock.Collection('Meteor.users')) diff --git a/packages/webui/src/client/ui/App.tsx b/packages/webui/src/client/ui/App.tsx index 77c4d7afa7..73d5ad43d0 100644 --- a/packages/webui/src/client/ui/App.tsx +++ b/packages/webui/src/client/ui/App.tsx @@ -52,7 +52,7 @@ export const App: React.FC = function App() { const [lastStart] = useState(Date.now()) - const roles = useUserPermissions() + const [roles, _rolesReady] = useUserPermissions() const featureFlags = useFeatureFlags() useEffect(() => { diff --git a/packages/webui/src/client/ui/Status/MediaManager.tsx b/packages/webui/src/client/ui/Status/MediaManager.tsx index 3471ce6b0b..d7401b0e88 100644 --- a/packages/webui/src/client/ui/Status/MediaManager.tsx +++ b/packages/webui/src/client/ui/Status/MediaManager.tsx @@ -364,7 +364,7 @@ export function MediaManagerStatus(): JSX.Element { const actionRestart = useCallback( (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { doUserAction(t, event, UserAction.RESTART_MEDIA_WORKFLOW, (e, ts) => - MeteorCall.userAction.mediaRestartWorkflow(e, ts, workflow._id) + MeteorCall.userAction.mediaRestartWorkflow(e, ts, workflow.deviceId, workflow._id) ) }, [t] @@ -372,7 +372,7 @@ export function MediaManagerStatus(): JSX.Element { const actionAbort = useCallback( (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { doUserAction(t, event, UserAction.ABORT_MEDIA_WORKFLOW, (e, ts) => - MeteorCall.userAction.mediaAbortWorkflow(e, ts, workflow._id) + MeteorCall.userAction.mediaAbortWorkflow(e, ts, workflow.deviceId, workflow._id) ) }, [t] @@ -380,7 +380,7 @@ export function MediaManagerStatus(): JSX.Element { const actionPrioritize = useCallback( (event: React.MouseEvent, workflow: MediaWorkFlowUi) => { doUserAction(t, event, UserAction.PRIORITIZE_MEDIA_WORKFLOW, (e, ts) => - MeteorCall.userAction.mediaPrioritizeWorkflow(e, ts, workflow._id) + MeteorCall.userAction.mediaPrioritizeWorkflow(e, ts, workflow.deviceId, workflow._id) ) }, [t] diff --git a/packages/webui/src/client/ui/Status/SystemStatus/SystemStatus.tsx b/packages/webui/src/client/ui/Status/SystemStatus/SystemStatus.tsx index bc047ccc19..346d19f6a9 100644 --- a/packages/webui/src/client/ui/Status/SystemStatus/SystemStatus.tsx +++ b/packages/webui/src/client/ui/Status/SystemStatus/SystemStatus.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useContext, useEffect, useMemo, useState } from 'react' import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/react-meteor-data' import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/PeripheralDevice' import { useTranslation } from 'react-i18next' @@ -13,10 +13,13 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { CoreItem } from './CoreItem' import { DeviceItem } from './DeviceItem' +import { UserPermissions, UserPermissionsContext } from '../../UserPermissions' export function SystemStatus(): JSX.Element { const { t } = useTranslation() + const userPermissions = useContext(UserPermissionsContext) + // Subscribe to data: useSubscription(CorelibPubSub.peripheralDevices, null) @@ -24,7 +27,7 @@ export function SystemStatus(): JSX.Element { const devices = useTracker(() => PeripheralDevices.find({}, { sort: { lastConnected: -1 } }).fetch(), [], []) const systemStatus = useSystemStatus() - const playoutDebugStates = usePlayoutDebugStates(devices) + const playoutDebugStates = usePlayoutDebugStates(devices, userPermissions) const devicesHierarchy = convertDevicesIntoHeirarchy(devices) @@ -98,7 +101,10 @@ function useSystemStatus(): StatusResponse | undefined { return sytemStatus } -function usePlayoutDebugStates(devices: PeripheralDevice[]): Map { +function usePlayoutDebugStates( + devices: PeripheralDevice[], + userPermissions: UserPermissions +): Map { const { t } = useTranslation() const [playoutDebugStates, setPlayoutDebugStates] = useState>(new Map()) @@ -117,6 +123,11 @@ function usePlayoutDebugStates(devices: PeripheralDevice[]): Map { + if (!userPermissions.developer) { + setPlayoutDebugStates(new Map()) + return + } + let destroyed = false const refreshDebugStates = () => { @@ -145,7 +156,7 @@ function usePlayoutDebugStates(devices: PeripheralDevice[]): Map>({ +const NO_PERMISSIONS: UserPermissions = Object.freeze({ studio: false, configure: false, developer: false, testing: false, service: false, + gateway: false, }) -export function useUserPermissions(): UserPermissions { +export const UserPermissionsContext = React.createContext>(NO_PERMISSIONS) + +export function useUserPermissions(): [roles: UserPermissions, ready: boolean] { const location = window.location - const [permissions, setPermissions] = useState({ - studio: getLocalAllowStudio(), - configure: getLocalAllowConfigure(), - developer: getLocalAllowDeveloper(), - testing: getLocalAllowTesting(), - service: getLocalAllowService(), - }) + const [ready, setReady] = useState(!Settings.enableHeaderAuth) + + const [permissions, setPermissions] = useState( + Settings.enableHeaderAuth + ? NO_PERMISSIONS + : { + studio: getLocalAllowStudio(), + configure: getLocalAllowConfigure(), + developer: getLocalAllowDeveloper(), + testing: getLocalAllowTesting(), + service: getLocalAllowService(), + gateway: false, + } + ) + + const isConnected = useTracker(() => Meteor.status().connected, [], false) + + useEffect(() => { + if (!Settings.enableHeaderAuth) return + + // Do nothing when not connected. Persist the previous values. + if (!isConnected) return + + const checkPermissions = () => { + MeteorCall.user + .getUserPermissions() + .then((v) => { + setPermissions(v || NO_PERMISSIONS) + setReady(true) + }) + .catch((e) => { + console.error('Failed to set level', e) + setPermissions(NO_PERMISSIONS) + }) + } + + const interval = setInterval(checkPermissions, 30000) // Arbitrary poll interval + + // Initial check now + checkPermissions() + + return () => { + clearInterval(interval) + } + }, [Settings.enableHeaderAuth, isConnected]) useEffect(() => { + if (Settings.enableHeaderAuth) return + if (!location.search) return const params = queryStringParse(location.search) @@ -66,9 +108,10 @@ export function useUserPermissions(): UserPermissions { developer: getLocalAllowDeveloper(), testing: getLocalAllowTesting(), service: getLocalAllowService(), + gateway: false, }) - }, [location.search]) + }, [location.search, Settings.enableHeaderAuth]) // A naive memoizing of the value, to avoid reactions when the value is identical - return useMemo(() => permissions, [JSON.stringify(permissions)]) + return [useMemo(() => permissions, [JSON.stringify(permissions)]), ready] } diff --git a/scripts/run.mjs b/scripts/run.mjs index 344814f7ba..5302a5e2f5 100644 --- a/scripts/run.mjs +++ b/scripts/run.mjs @@ -1,15 +1,22 @@ import process from "process"; +import fs from "fs"; import concurrently from "concurrently"; import { EXTRA_PACKAGES, config } from "./lib.js"; +function joinCommand(...parts) { + return parts.filter((part) => !!part).join(" "); +} + function watchPackages() { return [ { - command: config.uiOnly - ? `yarn watch ${EXTRA_PACKAGES.map((pkg) => `--ignore ${pkg}`).join( - " " - )}` - : "yarn watch", + command: joinCommand('yarn watch', + config.uiOnly + ? EXTRA_PACKAGES.map((pkg) => `--ignore ${pkg}`).join( + " " + ) + : "", + ), cwd: "packages", name: "PACKAGES-TSC", prefixColor: "red", @@ -29,6 +36,13 @@ function watchWorker() { } function watchMeteor() { + const settingsFileExists = fs.existsSync("meteor-settings.json"); + if (settingsFileExists) { + console.log('Found meteor-settings.json') + } else { + console.log('No meteor-settings.json') + } + return [ { command: "yarn watch-types --preserveWatchOutput", @@ -37,9 +51,12 @@ function watchMeteor() { prefixColor: "blue", }, { - command: `yarn debug${config.inspectMeteor ? " --inspect" : ""}${ - config.verbose ? " --verbose" : "" - }`, + command: joinCommand( + 'yarn debug', + config.inspectMeteor ? " --inspect" : "", + config.verbose ? " --verbose" : "", + settingsFileExists ? " --settings ../meteor-settings.json" : "" + ), cwd: "meteor", name: "METEOR", prefixColor: "cyan", From 4918e040737fac91b16c4e6cc8f786d0934df4d1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 26 Nov 2024 14:01:22 +0000 Subject: [PATCH 07/18] chore: update documentation --- .../configuration/sofie-core-settings.md | 3 +- .../docs/user-guide/features/access-levels.md | 58 ++++++++++++------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/packages/documentation/docs/user-guide/configuration/sofie-core-settings.md b/packages/documentation/docs/user-guide/configuration/sofie-core-settings.md index 2c3599a7fe..d26b3628c4 100644 --- a/packages/documentation/docs/user-guide/configuration/sofie-core-settings.md +++ b/packages/documentation/docs/user-guide/configuration/sofie-core-settings.md @@ -87,12 +87,11 @@ There are various settings you can set for an installation. See the list below: | **Field name** | Use | Default value | | :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | -| `defaultToCollapsedSegments` | Should all segments be collapsed by default, until the user expands them | `false` | | `autoRewindLeavingSegment` | Should segments be automatically rewound after they stop playing | `false` | | `disableBlurBorder` | Should a border be displayed around the Rundown View when it's not in focus and studio mode is enabled | `false` | | `defaultTimeScale` | An arbitrary number, defining the default zoom factor of the Timelines | `1` | | `allowGrabbingTimeline` | Can Segment Timelines be grabbed to scroll them? | `true` | -| `enableUserAccounts` | Enables User Accounts and Authentication. If disabled, all user stations will be treated as a single, anonymous user | `false` | +| `enableHeaderAuth` | If true, enable http header based security measures. See [here](../features/access-levels) for details on using this | `false` | | `defaultDisplayDuration` | The fallback duration of a Part, when it's expectedDuration is 0. \_\_In milliseconds | `3000` | | `allowMultiplePlaylistsInGUI` | If true, allows creation of new playlists in the Lobby Gui (rundown list). If false; only pre-existing playlists are allowed. | `false` | | `followOnAirSegmentsHistory` | How many segments of history to show when scrolling back in time (0 = show current segment only) | `0` | diff --git a/packages/documentation/docs/user-guide/features/access-levels.md b/packages/documentation/docs/user-guide/features/access-levels.md index 50307f970e..49277d5299 100644 --- a/packages/documentation/docs/user-guide/features/access-levels.md +++ b/packages/documentation/docs/user-guide/features/access-levels.md @@ -1,46 +1,60 @@ --- sidebar_position: 3 --- + # Access Levels -A variety of access levels can be set via the URL. By default, a user cannot edit settings, nor play out anything. Some of the access levels provide additional administrative pages or helpful tool tips for new users. These modes are persistent between sessions and will need to be manually disabled by replacing the _1_ with a _0_ in the URL. Below is a quick reference to the modes and what they have access to. +## Permissions -If user accounts are enabled \(`enableUserAccounts` in [_Sofie Core_ settings](../configuration/sofie-core-settings#settings-file)\), the access levels are set under the user settings. If no user accounts are set, the access level for a browser is set by adding `?theaccessmode=1` to the URL as described below. +There are a few different access levels that users can be assigned. They are not heirarchical, you will often need to enable multiple for each user. +Any client that can access sofie always has at least view-only access to the rundowns, and system status pages. -The access level is persisted in browser's Local Storage. To disable, visit`?theaccessmode=0`. +| Level | Summary | +| :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| **studio** | Grants access to operate a studio for playout of a rundown. | +| **configure** | Grants access to the settings pages of Sofie, and other abilities to configure the system. | +| **developer** | Grants access to some tools useful to developers. This also changes some ui behaviours to be less agressive in what is shown in the rundown view | +| **testing** | Enables the page Test Tools, which contains various tools useful for testing the system during development | +| **service** | Grants access to the external message status page, and some additional rundown management options that are not commonly needed | +| **gateway** | Grants access to various APIs intended for use by the various gateways that connect Sofie to other systems. | -| Access area | Basic Mode | Configuration Mode | Studio Mode | Admin Mode | -| :--- | :--- | :--- | :--- | :--- | -| **Rundowns** | View Only | View Only | Yes, playout | Yes, playout | -| **Settings** | No | Yes | No | Yes | +## Authentication providers +There are two ways to define the access for each user, which to use depends on your security requirements. -### Basic mode +### Browser based -Without enabling any additional modes in Sofie, the browser will have minimal access to the system. It will be able to view a rundown but, will not have the ability to manipulate it. This includes activating, deactivating, or resetting the rundown as well as taking the next part, adlib, etc. +:::info -### Studio mode +This is a simple mode that relies on being able to trust every client that can connect to Sofie -Studio Mode gives the current browser full control of the studio and all information associated to it. This includes allowing actions like activating and deactivating rundowns, taking parts, adlibbing, etc. This mode is accessed by adding a `?studio=1` to the end of the URL. +::: -### Configuration mode +In this mode, a variety of access levels can be set via the URL. The access level is persisted in browser's Local Storage. -Configuration mode gives the user full control over the Settings pages and allows full access to the system including the ability to modify _Blueprints_, _Studios_, or _Show Styles_, creating and restoring _Snapshots_, as well as modifying attached devices. +By default, a user cannot edit settings, nor play out anything. Some of the access levels provide additional administrative pages or helpful tool tips for new users. These modes are persistent between sessions and will need to be manually enabled or disabled by appending a suffix to the url. +Each of the modes listed in the levels table above can be used here, such as by navigating to `https://my-sofie/?studio=1` to enable studio mode, or `https://my-sofie/?studio=0` to disable studio mode. -### Help Mode +There are some additional url parameters that can be used to simplify the granting of permissions: -Enables some tooltips that might be useful to new users. This mode is accessed by adding `?help=1` to the end of the URL. +- `?help=1` will enable some tooltips that might be useful to new users. +- `?admin=1` will give the user the same access as the _Configuration_ and _Studio_ modes as well as having access to a set of _Test Tools_ and a _Manual Control_ section on the Rundown page. -### Admin Mode +### Header based -This mode will give the user the same access as the _Configuration_ and _Studio_ modes as well as having access to a set of _Test Tools_ and a _Manual Control_ section on the Rundown page. +:::danger -This mode is enabled when `?admin=1` is added the end of the URL. +This mode is very new and could have some undiscovered holes. +It is known that secrets can be leaked to all clients who can connect to Sofie, which is not desirable. -### Testing Mode +::: -Enables the page Test Tools, which contains various tools useful for testing the system during development. This mode is enabled when `?testing=1` is added the end of the URL. +In this mode, we rely on Sofie being run behind a reverse-proxy which will inform Sofie of the permissions of each connection. This allows you to use your organisations preferred auth provider, and translate that into something that Sofie can understand. +To enable this mode, you need to enable the `enableHeaderAuth` property in the [settings file](../configuration/sofie-core-settings.md) -### Developer Mode +Sofie expects that for each DDP connection or http request, the `dnt` header will be set containing a comma separated list of the levels from the above table. If the header is not defined or is empty, the connection will have view-only access to Sofie. +This header can also contain simply `admin` to grant the connection permission to everything. +We are using the `dnt` header due to limitations imposed by Meteor, but intend this to become a proper header name in a future release. -This mode will enable the browsers default right click menu to appear and can be accessed by adding `?develop=1` to the URL. It will also reveal the Manual Control section on the Rundown page. +When in this mode, you should make sure that Sofie can only be accessed through the reverse proxy, and that the reverse-proxy will always override any value sent by a client. +Because the value is defined in the http headers, it is not possible to revoke permissions for a user who currently has the ui open. If this is necessary to do, you can force the connection to be dropped by the reverse-proxy. From 6b02621550201cdab33b2ba9a799e84991de8637 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 26 Nov 2024 14:36:29 +0000 Subject: [PATCH 08/18] chore: remove unused css --- packages/webui/src/client/styles/main.scss | 1 - packages/webui/src/client/styles/users.scss | 71 --------------------- 2 files changed, 72 deletions(-) delete mode 100644 packages/webui/src/client/styles/users.scss diff --git a/packages/webui/src/client/styles/main.scss b/packages/webui/src/client/styles/main.scss index 7b50ba2a90..a1d77edf9c 100644 --- a/packages/webui/src/client/styles/main.scss +++ b/packages/webui/src/client/styles/main.scss @@ -42,7 +42,6 @@ input { @import 'systemStatus'; @import 'testtools'; @import 'tooltips'; -@import 'users'; @import 'utils'; @import 'countdown/overlay'; diff --git a/packages/webui/src/client/styles/users.scss b/packages/webui/src/client/styles/users.scss deleted file mode 100644 index ef2c573098..0000000000 --- a/packages/webui/src/client/styles/users.scss +++ /dev/null @@ -1,71 +0,0 @@ - -.center-page { - align-items: center; - display: flex; - flex-direction: column; - position: fixed; - top: 0; - bottom: 0; - justify-content: center; - width: 100vw; - - .header .badge .sofie-logo { - margin: 0 auto; - height: 8em; - width: 8em; - } - .page { - min-width: 25vw; - padding: 0 45px; - } - .container { - min-width: 30vw; - text-align: center; - } - .error-msg { - text-align: center; - opacity: 0; - transition: opacity 1s; - } - .error-msg-active { - opacity: 1; - } - a, - a:visited { - color: #000; - } - p { - margin: 0; - } - li { - list-style: none; - } - button { - /* Rule to add space between buttons on smaller screens */ - display: block; - margin: 10px auto; - } - .content-left { - text-align: left; - } -} - -@media screen and (max-height: 750px) and (min-width: 1400px) { - .center-page { - position: unset; - } -} - -@media screen and (max-height: 1000px) and (max-width: 1400px) { - .center-page { - position: unset; - } -} - -@media screen and (max-width: 1400px) { - .center-page { - > .frow { - flex-direction: column; - } - } -} From 1e5bbc669fe67fedf48027524232ad6696fc8909 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 9 Sep 2024 12:26:50 +0200 Subject: [PATCH 09/18] feat: support for http header in packagemanager --- .../server-core-integration/src/lib/coreConnection.ts | 2 +- packages/server-core-integration/src/lib/ddpClient.ts | 9 ++++++++- packages/server-core-integration/src/lib/ddpConnector.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/server-core-integration/src/lib/coreConnection.ts b/packages/server-core-integration/src/lib/coreConnection.ts index 0b7185ea49..ec03973683 100644 --- a/packages/server-core-integration/src/lib/coreConnection.ts +++ b/packages/server-core-integration/src/lib/coreConnection.ts @@ -126,7 +126,7 @@ export class CoreConnection< } }) - const ddpOptions = ddpOptions0 || { + const ddpOptions: DDPConnectorOptions = ddpOptions0 || { host: '127.0.0.1', port: 3000, } diff --git a/packages/server-core-integration/src/lib/ddpClient.ts b/packages/server-core-integration/src/lib/ddpClient.ts index 73782ea5ae..427f7b6f2e 100644 --- a/packages/server-core-integration/src/lib/ddpClient.ts +++ b/packages/server-core-integration/src/lib/ddpClient.ts @@ -34,6 +34,7 @@ export interface TLSOpts { export interface DDPConnectorOptions { host: string port: number + headers?: { [header: string]: string } path?: string ssl?: boolean debug?: boolean @@ -343,6 +344,10 @@ export class DDPClient extends EventEmitter { public get port(): number { return this.portInt } + private headersInt: { [header: string]: string } = {} + public get headers(): { [header: string]: string } { + return this.headersInt + } private pathInt?: string public get path(): string | undefined { return this.pathInt @@ -410,6 +415,7 @@ export class DDPClient extends EventEmitter { // console.log(opts) this.hostInt = opts.host || '127.0.0.1' this.portInt = opts.port || 3000 + this.headersInt = opts.headers || {} this.pathInt = opts.path this.sslInt = opts.ssl || this.port === 443 this.tlsOpts = opts.tlsOpts || {} @@ -722,6 +728,7 @@ export class DDPClient extends EventEmitter { try { const response = await got(url, { + headers: this.headers, https: { certificateAuthority: this.tlsOpts.ca, key: this.tlsOpts.key, @@ -762,7 +769,7 @@ export class DDPClient extends EventEmitter { private makeWebSocketConnection(url: string): void { // console.log('About to create WebSocket client') - this.socket = new WebSocket.Client(url, null, { tls: this.tlsOpts }) + this.socket = new WebSocket.Client(url, null, { tls: this.tlsOpts, headers: this.headers }) this.socket.on('open', () => { // just go ahead and open the connection on connect diff --git a/packages/server-core-integration/src/lib/ddpConnector.ts b/packages/server-core-integration/src/lib/ddpConnector.ts index a1f1fd17bb..5905863169 100644 --- a/packages/server-core-integration/src/lib/ddpConnector.ts +++ b/packages/server-core-integration/src/lib/ddpConnector.ts @@ -31,6 +31,7 @@ export class DDPConnector extends EventEmitter { const o: DDPConnectorOptions = { host: this._options.host, port: this._options.port, + headers: this._options.headers, path: this._options.path || '', ssl: this._options.ssl || false, tlsOpts: this._options.tlsOpts || {}, From c7bdfe6787b76a5d23eb841764abf7b0cc799028 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 27 Nov 2024 10:13:07 +0000 Subject: [PATCH 10/18] feat: always add `dnt=gateway` header for gateway connections --- packages/server-core-integration/src/lib/ddpClient.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server-core-integration/src/lib/ddpClient.ts b/packages/server-core-integration/src/lib/ddpClient.ts index 427f7b6f2e..08a7a06704 100644 --- a/packages/server-core-integration/src/lib/ddpClient.ts +++ b/packages/server-core-integration/src/lib/ddpClient.ts @@ -719,6 +719,13 @@ export class DDPClient extends EventEmitter { }) } + private getHeadersWithDefaults(): { [header: string]: string } { + return { + dnt: 'gateway', // Provide the header needed for the header based auth to work when not connected through a reverse proxy + ...this.headers, + } + } + private async makeSockJSConnection(): Promise { const protocol = this.ssl ? 'https://' : 'http://' if (this.path && !this.path?.endsWith('/')) { @@ -728,7 +735,7 @@ export class DDPClient extends EventEmitter { try { const response = await got(url, { - headers: this.headers, + headers: this.getHeadersWithDefaults(), https: { certificateAuthority: this.tlsOpts.ca, key: this.tlsOpts.key, @@ -769,7 +776,7 @@ export class DDPClient extends EventEmitter { private makeWebSocketConnection(url: string): void { // console.log('About to create WebSocket client') - this.socket = new WebSocket.Client(url, null, { tls: this.tlsOpts, headers: this.headers }) + this.socket = new WebSocket.Client(url, null, { tls: this.tlsOpts, headers: this.getHeadersWithDefaults() }) this.socket.on('open', () => { // just go ahead and open the connection on connect From 3d559f177b80168661a52c7b66f80e8122891b91 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 3 Dec 2024 12:17:34 +0000 Subject: [PATCH 11/18] fix: unable to remove peripheralDevice --- .../api/__tests__/peripheralDevice.test.ts | 2 +- meteor/server/api/peripheralDevice.ts | 16 ++++++++-------- .../src/peripheralDevice/methodsAPI.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 4a3b69fe5a..30e78c928d 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -618,7 +618,7 @@ describe('test peripheralDevice general API methods', () => { const deviceObj = await PeripheralDevices.findOneAsync(device?._id) expect(deviceObj).toBeDefined() - await MeteorCall.peripheralDevice.removePeripheralDevice(device._id, device.token) + await MeteorCall.peripheralDevice.removePeripheralDevice(device._id) } { diff --git a/meteor/server/api/peripheralDevice.ts b/meteor/server/api/peripheralDevice.ts index 90604fb61a..b6b29b218f 100644 --- a/meteor/server/api/peripheralDevice.ts +++ b/meteor/server/api/peripheralDevice.ts @@ -67,6 +67,7 @@ import { convertPeripheralDeviceForGateway } from '../publications/peripheralDev import { executePeripheralDeviceFunction } from './peripheralDevice/executeFunction' import KoaRouter from '@koa/router' import bodyParser from 'koa-bodyparser' +import { assertConnectionHasOneOfPermissions } from '../security/auth' const apmNamespace = 'peripheralDevice' export namespace ServerPeripheralDeviceAPI { @@ -513,12 +514,11 @@ export namespace ServerPeripheralDeviceAPI { }, }) } - export async function removePeripheralDevice( - context: MethodContext, - deviceId: PeripheralDeviceId, - token?: string - ): Promise { - const peripheralDevice = await checkAccessAndGetPeripheralDevice(deviceId, token, context) + export async function removePeripheralDevice(context: MethodContext, deviceId: PeripheralDeviceId): Promise { + assertConnectionHasOneOfPermissions(context.connection, 'configure') + + const peripheralDevice = await PeripheralDevices.findOneAsync(deviceId) + if (!peripheralDevice) throw new Meteor.Error(404, `PeripheralDevice "${deviceId}" not found`) logger.info(`Removing PeripheralDevice ${peripheralDevice._id}`) @@ -850,8 +850,8 @@ class ServerPeripheralDeviceAPIClass extends MethodContextAPI implements NewPeri async testMethod(deviceId: PeripheralDeviceId, deviceToken: string, returnValue: string, throwError?: boolean) { return ServerPeripheralDeviceAPI.testMethod(this, deviceId, deviceToken, returnValue, throwError) } - async removePeripheralDevice(deviceId: PeripheralDeviceId, token?: string) { - return ServerPeripheralDeviceAPI.removePeripheralDevice(this, deviceId, token) + async removePeripheralDevice(deviceId: PeripheralDeviceId) { + return ServerPeripheralDeviceAPI.removePeripheralDevice(this, deviceId) } // ------ Playout Gateway -------- diff --git a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts index 1f8a142f7b..80dd4dd366 100644 --- a/packages/shared-lib/src/peripheralDevice/methodsAPI.ts +++ b/packages/shared-lib/src/peripheralDevice/methodsAPI.ts @@ -116,7 +116,7 @@ export interface NewPeripheralDeviceAPI { timelineTriggerTime(deviceId: PeripheralDeviceId, deviceToken: string, r: TimelineTriggerTimeResult): Promise requestUserAuthToken(deviceId: PeripheralDeviceId, deviceToken: string, authUrl: string): Promise storeAccessToken(deviceId: PeripheralDeviceId, deviceToken: string, authToken: string): Promise - removePeripheralDevice(deviceId: PeripheralDeviceId, deviceToken?: string): Promise + removePeripheralDevice(deviceId: PeripheralDeviceId): Promise reportResolveDone( deviceId: PeripheralDeviceId, deviceToken: string, From 4c7a3377fff3188c5de3cbb165145db579ae244b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Dec 2024 15:42:58 +0000 Subject: [PATCH 12/18] chore: fix missed security check --- meteor/server/api/userActions.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 0816952ca1..43dc0a46c6 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -45,10 +45,10 @@ import { NrcsIngestCacheType } from '@sofie-automation/corelib/dist/dataModel/Nr import { verifyHashedToken } from './singleUseTokens' import { QuickLoopMarker } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { runIngestOperation } from './ingest/lib' -import { RundownPlaylistContentWriteAccess } from '../security/rundownPlaylist' import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' import { assertConnectionHasOneOfPermissions } from '../security/auth' +import { checkAccessToRundown } from '../security/check' const PERMISSIONS_FOR_PLAYOUT_USERACTION: Array = ['studio'] const PERMISSIONS_FOR_BUCKET_MODIFICATION: Array = ['studio'] @@ -1315,11 +1315,10 @@ class ServerUserActionAPI 'executeUserChangeOperation', { operationTarget, operation }, async () => { - const access = await RundownPlaylistContentWriteAccess.rundown(this, rundownId) - if (!access.rundown) throw new Error(`Rundown "${rundownId}" not found`) + const rundown = await checkAccessToRundown(this.connection, rundownId) - await runIngestOperation(access.rundown.studioId, IngestJobs.UserExecuteChangeOperation, { - rundownExternalId: access.rundown.externalId, + await runIngestOperation(rundown.studioId, IngestJobs.UserExecuteChangeOperation, { + rundownExternalId: rundown.externalId, operationTarget, operation, }) From f54bd28532ef075799ff184759543a53c8287f11 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 11 Dec 2024 09:29:05 +0000 Subject: [PATCH 13/18] chore: add deprecation note --- DEVELOPER.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DEVELOPER.md b/DEVELOPER.md index df3d084cb0..fc6612306a 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -147,6 +147,11 @@ However, one usage by AdlibActions for their userDataManifest remains as this is In R49, a replacement flow was added consisting of `validateConfig` and `applyConfig`. It is no longer recommended to use the old migrations flow for showstyle and studio blueprints. +## Blueprint Migrations + +In R52, the replacement flow of `validateConfig` and `applyConfig` was extended to the system blueprint +It is no longer recommended to use the old migrations flow for system blueprints. + ### ExpectedMediaItems These are used for Media-manager which is no longer being developed. From 5a58f71ec158f879c8dbd4aab96c5b79ea9d026c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 11 Dec 2024 09:21:12 +0000 Subject: [PATCH 14/18] feat!: remove showstyle and studio blueprint migrations --- DEVELOPER.md | 5 - meteor/__mocks__/helpers/database.ts | 2 - .../api/blueprints/__tests__/api.test.ts | 3 - meteor/server/api/blueprints/__tests__/lib.ts | 3 - .../__tests__/migrationContext.test.ts | 1458 ----------------- meteor/server/api/blueprints/api.ts | 6 - .../server/api/blueprints/migrationContext.ts | 614 ------- .../coreSystem/checkDatabaseVersions.ts | 64 +- .../migration/__tests__/migrations.test.ts | 169 +- meteor/server/migration/databaseMigration.ts | 189 +-- .../upgrades/__tests__/showStyleBase.test.ts | 1 - .../src/api/showStyle.ts | 6 +- .../blueprints-integration/src/api/studio.ts | 5 - .../blueprints-integration/src/migrations.ts | 85 +- packages/corelib/src/dataModel/Blueprint.ts | 6 - packages/job-worker/src/__mocks__/context.ts | 2 - .../src/blueprints/__tests__/lib.ts | 3 - .../src/blueprints/defaults/studio.ts | 1 - 18 files changed, 18 insertions(+), 2604 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 140ac0712a..e20be03db2 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -140,11 +140,6 @@ Then submit this as a PR. The ConfigManifests for Blueprints and Gateways was replaced with JSONSchema in R50. However, one usage by AdlibActions for their userDataManifest remains as this is not something we are actively using. -## Blueprint Migrations - -In R49, a replacement flow was added consisting of `validateConfig` and `applyConfig`. -It is no longer recommended to use the old migrations flow for showstyle and studio blueprints. - ### ExpectedMediaItems These are used for Media-manager which is no longer being developed. diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 6abd5a60bf..a96546a92f 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -368,7 +368,6 @@ export async function setupMockStudioBlueprint( }, studioConfigSchema: '{}' as any, - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -425,7 +424,6 @@ export async function setupMockShowStyleBlueprint( }, showStyleConfigSchema: '{}' as any, - showStyleMigrations: [], getShowStyleVariantId: (): string | null => { return SHOW_STYLE_VARIANT_ID }, diff --git a/meteor/server/api/blueprints/__tests__/api.test.ts b/meteor/server/api/blueprints/__tests__/api.test.ts index b92bf0a0ac..b1fb4939fe 100644 --- a/meteor/server/api/blueprints/__tests__/api.test.ts +++ b/meteor/server/api/blueprints/__tests__/api.test.ts @@ -61,8 +61,6 @@ describe('Test blueprint management api', () => { showStyleConfigSchema: JSONBlobStringify({}), databaseVersion: { - showStyle: {}, - studio: {}, system: undefined, }, @@ -244,7 +242,6 @@ describe('Test blueprint management api', () => { TSRVersion: '0.0.0', // studioConfigManifest: [], - // studioMigrations: [], // getBaseline: (context: IStudioContext): TSRTimelineObjBase[] => { // return [] // }, diff --git a/meteor/server/api/blueprints/__tests__/lib.ts b/meteor/server/api/blueprints/__tests__/lib.ts index 1b443d9715..e50bf8b59e 100644 --- a/meteor/server/api/blueprints/__tests__/lib.ts +++ b/meteor/server/api/blueprints/__tests__/lib.ts @@ -17,7 +17,6 @@ export function generateFakeBlueprint( integrationVersion: '0.0.0', TSRVersion: '0.0.0', studioConfigManifest: [], - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -43,8 +42,6 @@ export function generateFakeBlueprint( showStyleConfigSchema: JSONBlobStringify({}), databaseVersion: { - showStyle: {}, - studio: {}, system: undefined, }, diff --git a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts index 419a13b9a7..2c44af3b17 100644 --- a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts +++ b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts @@ -16,1464 +16,6 @@ describe('Test blueprint migrationContext', () => { await setupDefaultStudioEnvironment() }) - // eslint-disable-next-line jest/no-commented-out-tests - /* - describe('MigrationContextStudio', () => { - async function getContext() { - const studio = (await Studios.findOneAsync({})) as DBStudio - expect(studio).toBeTruthy() - return new MigrationContextStudio(studio) - } - function getStudio(context: MigrationContextStudio): DBStudio { - const studio = (context as any).studio - expect(studio).toBeTruthy() - return studio - } - describe('mappings', () => { - async function getMappingFromDb(studio: DBStudio, mappingId: string): Promise { - const studio2 = (await Studios.findOneAsync(studio._id)) as DBStudio - expect(studio2).toBeTruthy() - return studio2.mappingsWithOverrides.defaults[mappingId] - } - - test('getMapping: no id', async () => { - const ctx = await getContext() - const mapping = ctx.getMapping('') - expect(mapping).toBeFalsy() - }) - test('getMapping: missing', async () => { - const ctx = await getContext() - const mapping = ctx.getMapping('fake_mapping') - expect(mapping).toBeFalsy() - }) - test('getMapping: good', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const rawMapping: MappingExt = { - device: TSR.DeviceType.ABSTRACT, - deviceId: protectString('dev1'), - lookahead: LookaheadMode.NONE, - options: {}, - } - studio.mappingsWithOverrides.defaults['mapping1'] = rawMapping - - const mapping = ctx.getMapping('mapping1') as BlueprintMapping - expect(mapping).toEqual(rawMapping) - - // Ensure it is a copy - mapping.deviceId = 'changed' - expect(mapping).not.toEqual(studio.mappingsWithOverrides.defaults['mapping1']) - }) - - test('insertMapping: good', async () => { - const ctx = await getContext() - - const rawMapping: BlueprintMapping = { - device: TSR.DeviceType.ABSTRACT, - deviceId: 'dev1', - lookahead: LookaheadMode.NONE, - options: {}, - } - - const mappingId = ctx.insertMapping('mapping2', rawMapping) - expect(mappingId).toEqual('mapping2') - - // get should return the same - const mapping = ctx.getMapping('mapping2') - expect(mapping).toEqual(rawMapping) - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') - expect(dbMapping).toEqual(rawMapping) - }) - test('insertMapping: no id', async () => { - const ctx = await getContext() - - const rawMapping: BlueprintMapping = { - device: TSR.DeviceType.ABSTRACT, - deviceId: 'dev1', - lookahead: LookaheadMode.NONE, - options: {}, - } - - expect(() => ctx.insertMapping('', rawMapping)).toThrow(`[500] Mapping id "" is invalid`) - - // get should return the same - const mapping = ctx.getMapping('') - expect(mapping).toBeFalsy() - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), '') - expect(dbMapping).toBeFalsy() - }) - test('insertMapping: existing', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('mapping2') - expect(existingMapping).toBeTruthy() - - const rawMapping: BlueprintMapping = { - device: TSR.DeviceType.ATEM, - deviceId: 'dev2', - lookahead: LookaheadMode.PRELOAD, - options: {}, - } - expect(rawMapping).not.toEqual(existingMapping) - - expect(() => ctx.insertMapping('mapping2', rawMapping)).toThrow( - `[404] Mapping "mapping2" cannot be inserted as it already exists` - ) - - // get should return the same - const mapping = ctx.getMapping('mapping2') - expect(mapping).toEqual(existingMapping) - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') - expect(dbMapping).toEqual(existingMapping) - }) - - test('updateMapping: good', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('mapping2') as BlueprintMapping - expect(existingMapping).toBeTruthy() - - const rawMapping = { - device: TSR.DeviceType.HYPERDECK, - deviceId: 'hyper0', - } - ctx.updateMapping('mapping2', rawMapping) - - const expectedMapping = { - ...existingMapping, - ...rawMapping, - } - - // get should return the same - const mapping = ctx.getMapping('mapping2') - expect(mapping).toEqual(expectedMapping) - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping2') - expect(dbMapping).toEqual(expectedMapping) - }) - test('updateMapping: no props', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('mapping2') as BlueprintMapping - expect(existingMapping).toBeTruthy() - - // Should not error - ctx.updateMapping('mapping2', {}) - }) - test('updateMapping: no id', async () => { - const ctx = await getContext() - const existingMapping = ctx.getMapping('') as BlueprintMapping - expect(existingMapping).toBeFalsy() - - expect(() => ctx.updateMapping('', { device: TSR.DeviceType.HYPERDECK })).toThrow( - `[404] Mapping "" cannot be updated as it does not exist` - ) - }) - test('updateMapping: missing', async () => { - const ctx = await getContext() - expect(ctx.getMapping('mapping1')).toBeFalsy() - - const rawMapping = { - device: TSR.DeviceType.HYPERDECK, - deviceId: 'hyper0', - } - - expect(() => ctx.updateMapping('mapping1', rawMapping)).toThrow( - `[404] Mapping "mapping1" cannot be updated as it does not exist` - ) - - // get should return the same - const mapping = ctx.getMapping('mapping1') - expect(mapping).toBeFalsy() - - // check db is the same - const dbMapping = await getMappingFromDb(getStudio(ctx), 'mapping1') - expect(dbMapping).toBeFalsy() - }) - - test('removeMapping: missing', async () => { - const ctx = await getContext() - expect(ctx.getMapping('mapping1')).toBeFalsy() - - // Should not error - ctx.removeMapping('mapping1') - }) - test('removeMapping: no id', async () => { - const ctx = await getContext() - expect(ctx.getMapping('')).toBeFalsy() - expect(ctx.getMapping('mapping2')).toBeTruthy() - - // Should not error - ctx.removeMapping('') - - // ensure other mappings still exist - expect(await getMappingFromDb(getStudio(ctx), 'mapping2')).toBeTruthy() - }) - test('removeMapping: good', async () => { - const ctx = await getContext() - expect(ctx.getMapping('mapping2')).toBeTruthy() - - ctx.removeMapping('mapping2') - - // check was removed - expect(ctx.getMapping('mapping2')).toBeFalsy() - expect(await getMappingFromDb(getStudio(ctx), 'mapping2')).toBeFalsy() - }) - }) - - describe('config', () => { - async function getAllConfigFromDb(studio: DBStudio): Promise { - const studio2 = (await Studios.findOneAsync(studio._id)) as DBStudio - expect(studio2).toBeTruthy() - return studio2.blueprintConfigWithOverrides.defaults - } - - test('getConfig: no id', async () => { - const ctx = await getContext() - - expect(ctx.getConfig('')).toBeFalsy() - }) - test('getConfig: missing', async () => { - const ctx = await getContext() - - expect(ctx.getConfig('conf1')).toBeFalsy() - }) - test('getConfig: good', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - - studio.blueprintConfigWithOverrides.defaults['conf1'] = 5 - expect(ctx.getConfig('conf1')).toEqual(5) - - studio.blueprintConfigWithOverrides.defaults['conf2'] = ' af ' - expect(ctx.getConfig('conf2')).toEqual('af') - }) - - test('setConfig: no id', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - - expect(() => ctx.setConfig('', 34)).toThrow(`[500] Config id "" is invalid`) - - // Config should not have changed - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('setConfig: insert', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeFalsy() - - ctx.setConfig('conf1', 34) - - const expectedItem = { - _id: 'conf1', - value: 34, - } - expect(ctx.getConfig('conf1')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('setConfig: insert undefined', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('confUndef')).toBeFalsy() - - ctx.setConfig('confUndef', undefined as any) - - const expectedItem = { - _id: 'confUndef', - value: undefined as any, - } - expect(ctx.getConfig('confUndef')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - - test('setConfig: update', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - ctx.setConfig('conf1', 'hello') - - const expectedItem = { - _id: 'conf1', - value: 'hello', - } - expect(ctx.getConfig('conf1')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('setConfig: update undefined', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - ctx.setConfig('conf1', undefined as any) - - const expectedItem = { - _id: 'conf1', - value: undefined as any, - } - expect(ctx.getConfig('conf1')).toEqual(expectedItem.value) - - // Config should have changed - initialConfig[expectedItem._id] = expectedItem.value - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - - test('removeConfig: no id', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - ctx.setConfig('conf1', true) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeConfig('') - - // Config should not have changed - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('removeConfig: missing', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - expect(ctx.getConfig('fake_conf')).toBeFalsy() - - // Should not error - ctx.removeConfig('fake_conf') - - // Config should not have changed - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - test('removeConfig: good', async () => { - const ctx = await getContext() - const studio = getStudio(ctx) - const initialConfig = _.clone(studio.blueprintConfigWithOverrides.defaults) - expect(ctx.getConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeConfig('conf1') - - // Config should have changed - delete initialConfig['conf1'] - expect(studio.blueprintConfigWithOverrides.defaults).toEqual(initialConfig) - expect(await getAllConfigFromDb(studio)).toEqual(initialConfig) - }) - }) - - describe('devices', () => { - async function getStudio(context: MigrationContextStudio): Promise { - const studioId = (context as any).studio._id - const studio = (await Studios.findOneAsync(studioId)) as DBStudio - expect(studio).toBeTruthy() - return studio - } - async function createPlayoutDevice(studio: DBStudio) { - const peripheralDeviceId = getRandomId() - studio.peripheralDeviceSettings.playoutDevices.defaults = { - device01: { - peripheralDeviceId: peripheralDeviceId, - options: { - type: TSR.DeviceType.ABSTRACT, - options: {}, - }, - }, - } - - await Studios.updateAsync(studio._id, studio) - return PeripheralDevices.insertAsync({ - _id: peripheralDeviceId, - name: 'Fake parent device', - organizationId: null, - type: PeripheralDeviceType.PLAYOUT, - category: PeripheralDeviceCategory.PLAYOUT, - subType: PERIPHERAL_SUBTYPE_PROCESS, - deviceName: 'Playout Gateway', - studioId: studio._id, - created: 0, - lastConnected: 0, - lastSeen: 0, - status: { - statusCode: 0, - }, - connected: false, - connectionId: null, - token: '', - settings: {}, - configManifest: { - deviceConfigSchema: JSONBlobStringify({}), // can be empty as it's only useful for UI. - subdeviceManifest: {}, - }, - }) - } - async function getPlayoutDevice(studio: DBStudio): Promise { - const device = await PeripheralDevices.findOneAsync({ - studioId: studio._id, - type: PeripheralDeviceType.PLAYOUT, - category: PeripheralDeviceCategory.PLAYOUT, - subType: PERIPHERAL_SUBTYPE_PROCESS, - }) - expect(device).toBeTruthy() - return device as PeripheralDevice - } - - test('getDevice: no id', async () => { - const ctx = await getContext() - const device = ctx.getDevice('') - expect(device).toBeFalsy() - }) - test('getDevice: missing', async () => { - const ctx = await getContext() - const device = ctx.getDevice('fake_device') - expect(device).toBeFalsy() - }) - test('getDevice: missing with parent', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const playoutId = await createPlayoutDevice(studio) - expect(playoutId).toBeTruthy() - - const device = ctx.getDevice('fake_device') - expect(device).toBeFalsy() - }) - test('getDevice: good', async () => { - const ctx = await getContext() - const peripheral = getPlayoutDevice(await getStudio(ctx)) - expect(peripheral).toBeTruthy() - - const device = ctx.getDevice('device01') - expect(device).toBeTruthy() - - // Ensure bad id doesnt match it - const device2 = ctx.getDevice('fake_device') - expect(device2).toBeFalsy() - }) - - test('insertDevice: no id', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('')).toBeFalsy() - - expect(() => ctx.insertDevice('', { type: TSR.DeviceType.ABSTRACT } as any)).toThrow( - `[500] Device id "" is invalid` - ) - - expect(ctx.getDevice('')).toBeFalsy() - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('insertDevice: already exists', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device01')).toBeTruthy() - - expect(() => ctx.insertDevice('device01', { type: TSR.DeviceType.CASPARCG } as any)).toThrow( - `[404] Device "device01" cannot be inserted as it already exists` - ) - - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('insertDevice: ok', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device11')).toBeFalsy() - - const rawDevice: any = { type: TSR.DeviceType.CASPARCG } - - const deviceId = ctx.insertDevice('device11', rawDevice) - expect(deviceId).toEqual('device11') - initialSettings.defaults[deviceId] = { - peripheralDeviceId: (await getPlayoutDevice(studio))._id, - options: rawDevice, - } - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - - const device = ctx.getDevice(deviceId) - expect(device).toEqual(rawDevice) - }) - - test('updateDevice: no id', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('')).toBeFalsy() - - expect(() => ctx.updateDevice('', { type: TSR.DeviceType.ABSTRACT })).toThrow( - `[500] Device id "" is invalid` - ) - - expect(ctx.getDevice('')).toBeFalsy() - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('updateDevice: missing', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device22')).toBeFalsy() - - expect(() => ctx.updateDevice('device22', { type: TSR.DeviceType.ATEM })).toThrow( - `[404] Device "device22" cannot be updated as it does not exist` - ) - - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('Device: good', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device01')).toBeTruthy() - - const rawDevice: any = { - type: TSR.DeviceType.HYPERDECK, - } - const expectedDevice = { - ...initialSettings.defaults['device01'].options, - ...rawDevice, - } - - ctx.updateDevice('device01', rawDevice) - - expect(ctx.getDevice('device01')).toEqual(expectedDevice) - - initialSettings.defaults['device01'].options = expectedDevice - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - - test('removeDevice: no id', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('')).toBeFalsy() - - expect(() => ctx.removeDevice('')).toThrow(`[500] Device id "" is invalid`) - - expect(ctx.getDevice('')).toBeFalsy() - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('removeDevice: missing', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device22')).toBeFalsy() - - // Should not error - ctx.removeDevice('device22') - - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - test('removeDevice: good', async () => { - const ctx = await getContext() - const studio = await getStudio(ctx) - const initialSettings = studio.peripheralDeviceSettings.playoutDevices - expect(ctx.getDevice('device01')).toBeTruthy() - - // Should not error - ctx.removeDevice('device01') - - expect(ctx.getDevice('device01')).toBeFalsy() - delete initialSettings.defaults['device01'] - expect((await getStudio(ctx)).peripheralDeviceSettings.playoutDevices).toEqual(initialSettings) - }) - }) - }) - - describe('MigrationContextShowStyle', () => { - async function getContext() { - const showStyle = (await ShowStyleBases.findOneAsync({})) as DBShowStyleBase - expect(showStyle).toBeTruthy() - return new MigrationContextShowStyle(showStyle) - } - function getShowStyle(context: MigrationContextShowStyle): DBShowStyleBase { - const showStyleBase = (context as any).showStyleBase - expect(showStyleBase).toBeTruthy() - return showStyleBase - } - async function createVariant(ctx: MigrationContextShowStyle, id: string, config?: IBlueprintConfig) { - const showStyle = getShowStyle(ctx) - - const rawVariant = literal({ - _id: protectString(ctx.getVariantId(id)), - name: 'test', - showStyleBaseId: showStyle._id, - blueprintConfigWithOverrides: wrapDefaultObject(config || {}), - _rundownVersionHash: '', - _rank: 0, - }) - await ShowStyleVariants.insertAsync(rawVariant) - - return rawVariant - } - - describe('variants', () => { - test('getAllVariants: good', async () => { - const ctx = await getContext() - const variants = ctx.getAllVariants() - expect(variants).toHaveLength(1) - }) - test('getAllVariants: missing base', () => { - const ctx = new MigrationContextShowStyle({ _id: 'fakeStyle' } as any) - const variants = ctx.getAllVariants() - expect(variants).toHaveLength(0) - }) - - test('getVariantId: consistent', async () => { - const ctx = await getContext() - - const id1 = ctx.getVariantId('variant1') - const id2 = ctx.getVariantId('variant1') - expect(id2).toEqual(id1) - - const id3 = ctx.getVariantId('variant2') - expect(id3).not.toEqual(id1) - }) - test('getVariantId: different base', async () => { - const ctx = await getContext() - const ctx2 = new MigrationContextShowStyle({ _id: 'fakeStyle' } as any) - - const id1 = ctx.getVariantId('variant1') - const id2 = ctx2.getVariantId('variant1') - expect(id2).not.toEqual(id1) - }) - - test('getVariant: good', async () => { - const ctx = await getContext() - const rawVariant = await createVariant(ctx, 'variant1') - - const variant = ctx.getVariant('variant1') - expect(variant).toBeTruthy() - expect(variant).toEqual(rawVariant) - }) - test('getVariant: no id', async () => { - const ctx = await getContext() - - expect(() => ctx.getVariant('')).toThrow(`[500] Variant id "" is invalid`) - }) - test('getVariant: missing', async () => { - const ctx = await getContext() - - const variant = ctx.getVariant('fake_variant') - expect(variant).toBeFalsy() - }) - - test('insertVariant: no id', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - - expect(() => - ctx.insertVariant('', { - name: 'test2', - }) - ).toThrow(`[500] Variant id "" is invalid`) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('insertVariant: already exists', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant1')).toBeTruthy() - - expect(() => - ctx.insertVariant('variant1', { - name: 'test2', - }) - ).toThrow(/*`[500] Variant id "variant1" already exists`* /) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('insertVariant: good', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant2')).toBeFalsy() - - const variantId = ctx.insertVariant('variant2', { - name: 'test2', - }) - expect(variantId).toBeTruthy() - expect(variantId).toEqual(ctx.getVariantId('variant2')) - - initialVariants.push( - literal({ - _id: protectString(variantId), - showStyleBaseId: getShowStyle(ctx)._id, - name: 'test2', - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - _rank: 0, - }) as any as IBlueprintShowStyleVariant - ) - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - - test('updateVariant: no id', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - - expect(() => - ctx.updateVariant('', { - name: 'test12', - }) - ).toThrow(`[500] Variant id "" is invalid`) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('updateVariant: missing', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant11')).toBeFalsy() - - expect(() => - ctx.updateVariant('variant11', { - name: 'test2', - }) - ).toThrow(/*`[404] Variant id "variant1" does not exist`* /) - // TODO - tidy up the error type - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('updateVariant: good', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant1')).toBeTruthy() - - ctx.updateVariant('variant1', { - name: 'newname', - }) - - _.each(initialVariants, (variant) => { - if (variant._id === ctx.getVariantId('variant1')) { - variant.name = 'newname' - } - }) - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - - test('removeVariant: no id', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - - expect(() => ctx.removeVariant('')).toThrow(`[500] Variant id "" is invalid`) - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('removeVariant: missing', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant11')).toBeFalsy() - - // Should not error - ctx.removeVariant('variant11') - - expect(ctx.getAllVariants()).toEqual(initialVariants) - }) - test('removeVariant: good', async () => { - const ctx = await getContext() - const initialVariants = _.clone(ctx.getAllVariants()) - expect(ctx.getVariant('variant1')).toBeTruthy() - - // Should not error - ctx.removeVariant('variant1') - - const expectedVariants = _.filter( - initialVariants, - (variant) => variant._id !== ctx.getVariantId('variant1') - ) - expect(ctx.getAllVariants()).toEqual(expectedVariants) - }) - }) - - describe('sourcelayer', () => { - async function getAllSourceLayersFromDb(showStyle: DBShowStyleBase): Promise { - const showStyle2 = (await ShowStyleBases.findOneAsync(showStyle._id)) as DBShowStyleBase - expect(showStyle2).toBeTruthy() - return showStyle2.sourceLayersWithOverrides.defaults - } - - test('getSourceLayer: no id', async () => { - const ctx = await getContext() - - expect(() => ctx.getSourceLayer('')).toThrow(`[500] SourceLayer id "" is invalid`) - }) - test('getSourceLayer: missing', async () => { - const ctx = await getContext() - - const layer = ctx.getSourceLayer('fake_source_layer') - expect(layer).toBeFalsy() - }) - test('getSourceLayer: good', async () => { - const ctx = await getContext() - - const layer = ctx.getSourceLayer('cam0') as ISourceLayer - expect(layer).toBeTruthy() - expect(layer._id).toEqual('cam0') - - const layer2 = ctx.getSourceLayer('vt0') as ISourceLayer - expect(layer2).toBeTruthy() - expect(layer2._id).toEqual('vt0') - }) - - test('insertSourceLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.insertSourceLayer('', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[500] SourceLayer id "" is invalid`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('insertSourceLayer: existing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.insertSourceLayer('vt0', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[500] SourceLayer "vt0" already exists`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('insertSourceLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - const rawLayer = { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - } - - ctx.insertSourceLayer('lay1', rawLayer) - - initialSourceLayers['lay1'] = { - ...rawLayer, - _id: 'lay1', - } - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - - test('updateSourceLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.updateSourceLayer('', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[500] SourceLayer id "" is invalid`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('updateSourceLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => - ctx.updateSourceLayer('fake99', { - name: 'test', - _rank: 10, - type: SourceLayerType.UNKNOWN, - }) - ).toThrow(`[404] SourceLayer "fake99" cannot be updated as it does not exist`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('updateSourceLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - expect(ctx.getSourceLayer('lay1')).toBeTruthy() - - const rawLayer = { - name: 'test98', - type: SourceLayerType.VT, - } - - ctx.updateSourceLayer('lay1', rawLayer) - - initialSourceLayers['lay1'] = { - ...initialSourceLayers['lay1']!, - ...rawLayer, - } - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - - test('removeSourceLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - - expect(() => ctx.removeSourceLayer('')).toThrow(`[500] SourceLayer id "" is invalid`) - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('removeSourceLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - expect(ctx.getSourceLayer('fake99')).toBeFalsy() - - // Should not error - ctx.removeSourceLayer('fake99') - - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - test('removeSourceLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialSourceLayers = _.clone(showStyle.sourceLayersWithOverrides.defaults) - expect(ctx.getSourceLayer('lay1')).toBeTruthy() - - // Should not error - ctx.removeSourceLayer('lay1') - - delete initialSourceLayers['lay1'] - expect(getShowStyle(ctx).sourceLayersWithOverrides.defaults).toEqual(initialSourceLayers) - expect(await getAllSourceLayersFromDb(showStyle)).toEqual(initialSourceLayers) - }) - }) - - describe('outputlayer', () => { - async function getAllOutputLayersFromDb( - showStyle: DBShowStyleBase - ): Promise> { - const showStyle2 = (await ShowStyleBases.findOneAsync(showStyle._id)) as DBShowStyleBase - expect(showStyle2).toBeTruthy() - return showStyle2.outputLayersWithOverrides.defaults - } - - test('getOutputLayer: no id', async () => { - const ctx = await getContext() - - expect(() => ctx.getOutputLayer('')).toThrow(`[500] OutputLayer id "" is invalid`) - }) - test('getOutputLayer: missing', async () => { - const ctx = await getContext() - - const layer = ctx.getOutputLayer('fake_source_layer') - expect(layer).toBeFalsy() - }) - test('getOutputLayer: good', async () => { - const ctx = await getContext() - - const layer = ctx.getOutputLayer('pgm') as IOutputLayer - expect(layer).toBeTruthy() - expect(layer._id).toEqual('pgm') - }) - - test('insertOutputLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.insertOutputLayer('', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[500] OutputLayer id "" is invalid`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('insertOutputLayer: existing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.insertOutputLayer('pgm', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[500] OutputLayer "pgm" already exists`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('insertOutputLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - const rawLayer = { - name: 'test', - _rank: 10, - isPGM: true, - } - - ctx.insertOutputLayer('lay1', rawLayer) - - initialOutputLayers['lay1'] = { - ...rawLayer, - _id: 'lay1', - } - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - - test('updateOutputLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.updateOutputLayer('', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[500] OutputLayer id "" is invalid`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('updateOutputLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => - ctx.updateOutputLayer('fake99', { - name: 'test', - _rank: 10, - isPGM: true, - }) - ).toThrow(`[404] OutputLayer "fake99" cannot be updated as it does not exist`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('updateOutputLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - expect(ctx.getOutputLayer('lay1')).toBeTruthy() - - const rawLayer = { - name: 'test98', - } - - ctx.updateOutputLayer('lay1', rawLayer) - - initialOutputLayers['lay1'] = { - ...initialOutputLayers['lay1']!, - ...rawLayer, - } - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - - test('removeOutputLayer: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - - expect(() => ctx.removeOutputLayer('')).toThrow(`[500] OutputLayer id "" is invalid`) - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('removeOutputLayer: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - expect(ctx.getOutputLayer('fake99')).toBeFalsy() - - // Should not error - ctx.removeOutputLayer('fake99') - - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - test('removeOutputLayer: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialOutputLayers = _.clone(showStyle.outputLayersWithOverrides.defaults) - expect(ctx.getOutputLayer('lay1')).toBeTruthy() - - // Should not error - ctx.removeOutputLayer('lay1') - - delete initialOutputLayers['lay1'] - expect(getShowStyle(ctx).outputLayersWithOverrides.defaults).toEqual(initialOutputLayers) - expect(await getAllOutputLayersFromDb(showStyle)).toEqual(initialOutputLayers) - }) - }) - - describe('base-config', () => { - async function getAllBaseConfigFromDb(showStyle: DBShowStyleBase): Promise { - const showStyle2 = (await ShowStyleBases.findOneAsync(showStyle._id)) as DBShowStyleBase - expect(showStyle2).toBeTruthy() - return showStyle2.blueprintConfigWithOverrides.defaults - } - - test('getBaseConfig: no id', async () => { - const ctx = await getContext() - - expect(ctx.getBaseConfig('')).toBeFalsy() - }) - test('getBaseConfig: missing', async () => { - const ctx = await getContext() - - expect(ctx.getBaseConfig('conf1')).toBeFalsy() - }) - test('getBaseConfig: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - - showStyle.blueprintConfigWithOverrides.defaults['conf1'] = 5 - expect(ctx.getBaseConfig('conf1')).toEqual(5) - - showStyle.blueprintConfigWithOverrides.defaults['conf2'] = ' af ' - expect(ctx.getBaseConfig('conf2')).toEqual('af') - }) - - test('setBaseConfig: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - - expect(() => ctx.setBaseConfig('', 34)).toThrow(`[500] Config id "" is invalid`) - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('setBaseConfig: insert', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeFalsy() - - ctx.setBaseConfig('conf1', 34) - - const expectedItem = { - _id: 'conf1', - value: 34, - } - expect(ctx.getBaseConfig('conf1')).toEqual(expectedItem.value) - - // BaseConfig should have changed - initialBaseConfig[expectedItem._id] = expectedItem.value - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('setBaseConfig: insert undefined', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('confUndef')).toBeFalsy() - - expect(() => ctx.setBaseConfig('confUndef', undefined as any)).toThrow( - `[400] setBaseConfig "confUndef": value is undefined` - ) - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - - test('setBaseConfig: update', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - ctx.setBaseConfig('conf1', 'hello') - - const expectedItem = { - _id: 'conf1', - value: 'hello', - } - expect(ctx.getBaseConfig('conf1')).toEqual(expectedItem.value) - - // BaseConfig should have changed - initialBaseConfig[expectedItem._id] = expectedItem.value - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('setBaseConfig: update undefined', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - expect(() => ctx.setBaseConfig('conf1', undefined as any)).toThrow( - `[400] setBaseConfig "conf1": value is undefined` - ) - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - - test('removeBaseConfig: no id', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - ctx.setBaseConfig('conf1', true) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeBaseConfig('') - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('removeBaseConfig: missing', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - expect(ctx.getBaseConfig('fake_conf')).toBeFalsy() - - // Should not error - ctx.removeBaseConfig('fake_conf') - - // BaseConfig should not have changed - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - test('removeBaseConfig: good', async () => { - const ctx = await getContext() - const showStyle = getShowStyle(ctx) - const initialBaseConfig = _.clone(showStyle.blueprintConfigWithOverrides.defaults) - expect(ctx.getBaseConfig('conf1')).toBeTruthy() - - // Should not error - ctx.removeBaseConfig('conf1') - - // BaseConfig should have changed - delete initialBaseConfig['conf1'] - expect(showStyle.blueprintConfigWithOverrides.defaults).toEqual(initialBaseConfig) - expect(await getAllBaseConfigFromDb(showStyle)).toEqual(initialBaseConfig) - }) - }) - describe('variant-config', () => { - async function getAllVariantConfigFromDb( - ctx: MigrationContextShowStyle, - variantId: string - ): Promise { - const variant = (await ShowStyleVariants.findOneAsync( - protectString(ctx.getVariantId(variantId)) - )) as DBShowStyleVariant - expect(variant).toBeTruthy() - return variant.blueprintConfigWithOverrides.defaults - } - - test('getVariantConfig: no variant id', async () => { - const ctx = await getContext() - - expect(() => ctx.getVariantConfig('', 'conf1')).toThrow(`[404] ShowStyleVariant "" not found`) - }) - test('getVariantConfig: missing variant', async () => { - const ctx = await getContext() - - expect(() => ctx.getVariantConfig('fake_variant', 'conf1')).toThrow( - `[404] ShowStyleVariant "fake_variant" not found` - ) - }) - test('getVariantConfig: missing', async () => { - const ctx = await getContext() - await createVariant(ctx, 'configVariant', { conf1: 5, conf2: ' af ' }) - - expect(ctx.getVariantConfig('configVariant', 'conf11')).toBeFalsy() - }) - test('getVariantConfig: good', async () => { - const ctx = await getContext() - expect(ctx.getVariant('configVariant')).toBeTruthy() - - expect(ctx.getVariantConfig('configVariant', 'conf1')).toEqual(5) - expect(ctx.getVariantConfig('configVariant', 'conf2')).toEqual('af') - }) - - test('setVariantConfig: no variant id', async () => { - const ctx = await getContext() - - expect(() => ctx.setVariantConfig('', 'conf1', 5)).toThrow(`[404] ShowStyleVariant "" not found`) - }) - test('setVariantConfig: missing variant', async () => { - const ctx = await getContext() - - expect(() => ctx.setVariantConfig('fake_variant', 'conf1', 5)).toThrow( - `[404] ShowStyleVariant "fake_variant" not found` - ) - }) - test('setVariantConfig: no id', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariant('configVariant')).toBeTruthy() - - expect(() => ctx.setVariantConfig('configVariant', '', 34)).toThrow(`[500] Config id "" is invalid`) - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('setVariantConfig: insert', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf19')).toBeFalsy() - - ctx.setVariantConfig('configVariant', 'conf19', 34) - - const expectedItem = { - _id: 'conf19', - value: 34, - } - expect(ctx.getVariantConfig('configVariant', 'conf19')).toEqual(expectedItem.value) - - // VariantConfig should have changed - initialVariantConfig[expectedItem._id] = expectedItem.value - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('setVariantConfig: insert undefined', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'confUndef')).toBeFalsy() - - expect(() => ctx.setVariantConfig('configVariant', 'confUndef', undefined as any)).toThrow( - `[400] setVariantConfig "configVariant", "confUndef": value is undefined` - ) - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - - test('setVariantConfig: update', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - ctx.setVariantConfig('configVariant', 'conf1', 'hello') - - const expectedItem = { - _id: 'conf1', - value: 'hello', - } - expect(ctx.getVariantConfig('configVariant', 'conf1')).toEqual(expectedItem.value) - - // VariantConfig should have changed - initialVariantConfig[expectedItem._id] = expectedItem.value - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('setVariantConfig: update undefined', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - expect(() => ctx.setVariantConfig('configVariant', 'conf1', undefined as any)).toThrow( - `[400] setVariantConfig "configVariant", "conf1": value is undefined` - ) - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - - test('removeVariantConfig: no variant id', async () => { - const ctx = await getContext() - - expect(() => ctx.removeVariantConfig('', 'conf1')).toThrow(`[404] ShowStyleVariant "" not found`) - }) - test('removeVariantConfig: missing variant', async () => { - const ctx = await getContext() - - expect(() => ctx.removeVariantConfig('fake_variant', 'conf1')).toThrow( - `[404] ShowStyleVariant "fake_variant" not found` - ) - }) - test('removeVariantConfig: no id', async () => { - const ctx = await getContext() - ctx.setVariantConfig('configVariant', 'conf1', true) - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - // Should not error - ctx.removeVariantConfig('configVariant', '') - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('removeVariantConfig: missing', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - expect(ctx.getVariantConfig('configVariant', 'fake_conf')).toBeFalsy() - - // Should not error - ctx.removeVariantConfig('configVariant', 'fake_conf') - - // VariantConfig should not have changed - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - test('removeVariantConfig: good', async () => { - const ctx = await getContext() - const initialVariantConfig = _.clone(await getAllVariantConfigFromDb(ctx, 'configVariant')) - expect(ctx.getVariantConfig('configVariant', 'conf1')).toBeTruthy() - - // Should not error - ctx.removeVariantConfig('configVariant', 'conf1') - - // VariantConfig should have changed - delete initialVariantConfig['conf1'] - expect(await getAllVariantConfigFromDb(ctx, 'configVariant')).toEqual(initialVariantConfig) - }) - }) - }) - */ - describe('MigrationContextSystem', () => { async function getContext() { const coreSystem = await CoreSystem.findOneAsync({}) diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index e2f2ec7bc5..c20d256ced 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -57,8 +57,6 @@ export async function insertBlueprint( blueprintType: type, databaseVersion: { - studio: {}, - showStyle: {}, system: undefined, }, @@ -155,8 +153,6 @@ async function innerUploadBlueprint( databaseVersion: blueprint ? blueprint.databaseVersion : { - studio: {}, - showStyle: {}, system: undefined, }, blueprintId: '', @@ -206,8 +202,6 @@ async function innerUploadBlueprint( // Force reset migrations newBlueprint.databaseVersion = { - showStyle: {}, - studio: {}, system: undefined, } } else { diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts index a273f24bd1..c5ab3c15c1 100644 --- a/meteor/server/api/blueprints/migrationContext.ts +++ b/meteor/server/api/blueprints/migrationContext.ts @@ -11,11 +11,6 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { TriggeredActions } from '../../collections' -// function trimIfString(value: T): T | string { -// if (_.isString(value)) return value.trim() -// return value -// } - function convertTriggeredActionToBlueprints(triggeredAction: TriggeredActionsObj): IBlueprintTriggeredActions { const obj: Complete = { _id: unprotectString(triggeredAction._id), @@ -125,612 +120,3 @@ class AbstractMigrationContextWithTriggeredActions { export class MigrationContextSystem extends AbstractMigrationContextWithTriggeredActions implements IMigrationContextSystem {} - -/* -export class MigrationContextStudio implements IMigrationContextStudio { - private studio: DBStudio - - constructor(studio: DBStudio) { - this.studio = studio - } - - getMapping(mappingId: string): BlueprintMapping | undefined { - check(mappingId, String) - const mapping = this.studio.mappingsWithOverrides.defaults[mappingId] - if (mapping) { - return clone({ - ...mapping, - deviceId: unprotectString(mapping.deviceId), - }) - } - } - insertMapping(mappingId: string, mapping: OmitId): string { - check(mappingId, String) - if (this.studio.mappingsWithOverrides.defaults[mappingId]) { - throw new Meteor.Error(404, `Mapping "${mappingId}" cannot be inserted as it already exists`) - } - if (!mappingId) { - throw new Meteor.Error(500, `Mapping id "${mappingId}" is invalid`) - } - - const m: any = {} - m['mappingsWithOverrides.defaults.' + mappingId] = mapping - waitForPromise(Studios.updateAsync(this.studio._id, { $set: m })) - this.studio.mappingsWithOverrides.defaults[mappingId] = m['mappingsWithOverrides.defaults.' + mappingId] // Update local - return mappingId - } - updateMapping(mappingId: string, mapping: Partial): void { - check(mappingId, String) - if (!this.studio.mappingsWithOverrides.defaults[mappingId]) { - throw new Meteor.Error(404, `Mapping "${mappingId}" cannot be updated as it does not exist`) - } - - if (mappingId) { - const m: any = {} - m['mappingsWithOverrides.defaults.' + mappingId] = _.extend( - this.studio.mappingsWithOverrides.defaults[mappingId], - mapping - ) - waitForPromise(Studios.updateAsync(this.studio._id, { $set: m })) - this.studio.mappingsWithOverrides.defaults[mappingId] = m['mappingsWithOverrides.defaults.' + mappingId] // Update local - } - } - removeMapping(mappingId: string): void { - check(mappingId, String) - if (mappingId) { - const m: any = {} - m['mappingsWithOverrides.defaults.' + mappingId] = 1 - waitForPromise(Studios.updateAsync(this.studio._id, { $unset: m })) - delete this.studio.mappingsWithOverrides.defaults[mappingId] // Update local - } - } - - getConfig(configId: string): ConfigItemValue | undefined { - check(configId, String) - if (configId === '') return undefined - const configItem = objectPathGet(this.studio.blueprintConfigWithOverrides.defaults, configId) - return trimIfString(configItem) - } - setConfig(configId: string, value: ConfigItemValue): void { - check(configId, String) - if (!configId) { - throw new Meteor.Error(500, `Config id "${configId}" is invalid`) - } - - value = trimIfString(value) - - let modifier: MongoModifier = {} - if (value === undefined) { - modifier = { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - objectPathDelete(this.studio.blueprintConfigWithOverrides.defaults, configId) // Update local - } else { - modifier = { - $set: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: value, - }, - } - objectPathSet(this.studio.blueprintConfigWithOverrides.defaults, configId, value) // Update local - } - waitForPromise( - Studios.updateAsync( - { - _id: this.studio._id, - }, - modifier - ) - ) - } - removeConfig(configId: string): void { - check(configId, String) - - if (configId) { - waitForPromise( - Studios.updateAsync( - { - _id: this.studio._id, - }, - { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - ) - ) - // Update local: - objectPathDelete(this.studio.blueprintConfigWithOverrides.defaults, configId) - } - } - - getDevice(deviceId: string): TSR.DeviceOptionsAny | undefined { - check(deviceId, String) - - const studio = waitForPromise(Studios.findOneAsync(this.studio._id)) - if (!studio || !studio.peripheralDeviceSettings.playoutDevices) return undefined - - const playoutDevices = studio.peripheralDeviceSettings.playoutDevices.defaults - - return playoutDevices[deviceId]?.options - } - insertDevice(deviceId: string, device: TSR.DeviceOptionsAny): string { - check(deviceId, String) - - if (!deviceId) { - throw new Meteor.Error(500, `Device id "${deviceId}" is invalid`) - } - - const studio = waitForPromise(Studios.findOneAsync(this.studio._id)) - if (!studio || !studio.peripheralDeviceSettings.playoutDevices) - throw new Meteor.Error(500, `Studio was not found`) - - const playoutDevices = studio.peripheralDeviceSettings.playoutDevices.defaults - - if (playoutDevices && playoutDevices[deviceId]) { - throw new Meteor.Error(404, `Device "${deviceId}" cannot be inserted as it already exists`) - } - - const parentDevice = waitForPromise( - PeripheralDevices.findOneAsync( - { - type: PeripheralDeviceType.PLAYOUT, - subType: PERIPHERAL_SUBTYPE_PROCESS, - studioId: this.studio._id, - }, - { - sort: { - created: 1, - }, - } - ) - ) - if (!parentDevice) { - throw new Meteor.Error(404, `Device "${deviceId}" cannot be updated as it does not exist`) - } - - waitForPromise( - Studios.updateAsync(this.studio._id, { - $set: { - [`peripheralDeviceSettings.playoutDevices.defaults.${deviceId}`]: literal({ - peripheralDeviceId: parentDevice._id, - options: device, - }), - }, - }) - ) - - return deviceId - } - updateDevice(deviceId: string, device: Partial): void { - check(deviceId, String) - - if (!deviceId) { - throw new Meteor.Error(500, `Device id "${deviceId}" is invalid`) - } - - const studio = waitForPromise(Studios.findOneAsync(this.studio._id)) - if (!studio || !studio.peripheralDeviceSettings.playoutDevices) - throw new Meteor.Error(500, `Studio was not found`) - - const playoutDevices = studio.peripheralDeviceSettings.playoutDevices.defaults - - if (!playoutDevices || !playoutDevices[deviceId]) { - throw new Meteor.Error(404, `Device "${deviceId}" cannot be updated as it does not exist`) - } - - const newOptions = _.extend(playoutDevices[deviceId].options, device) - - waitForPromise( - Studios.updateAsync(this.studio._id, { - $set: { - [`peripheralDeviceSettings.playoutDevices.defaults.${deviceId}.options`]: newOptions, - }, - }) - ) - } - removeDevice(deviceId: string): void { - check(deviceId, String) - - if (!deviceId) { - throw new Meteor.Error(500, `Device id "${deviceId}" is invalid`) - } - - waitForPromise( - Studios.updateAsync(this.studio._id, { - $unset: { - [`peripheralDeviceSettings.playoutDevices.defaults.${deviceId}`]: 1, - }, - }) - ) - } -} - -export class MigrationContextShowStyle - extends AbstractMigrationContextWithTriggeredActions - implements IMigrationContextShowStyle -{ - private showStyleBase: DBShowStyleBase - constructor(showStyleBase: DBShowStyleBase) { - super() - this.showStyleBaseId = showStyleBase._id - this.showStyleBase = showStyleBase - } - - getAllVariants(): IBlueprintShowStyleVariant[] { - return waitForPromise( - ShowStyleVariants.findFetchAsync({ - showStyleBaseId: this.showStyleBase._id, - }) - ).map((variant) => unprotectObject(variant)) as any - } - getVariantId(variantId: string): string { - return getHash(this.showStyleBase._id + '_' + variantId) - } - private getProtectedVariantId(variantId: string): ShowStyleVariantId { - return protectString(this.getVariantId(variantId)) - } - private getVariantFromDb(variantId: string): DBShowStyleVariant | undefined { - const variant = waitForPromise( - ShowStyleVariants.findOneAsync({ - showStyleBaseId: this.showStyleBase._id, - _id: this.getProtectedVariantId(variantId), - }) - ) - if (variant) return variant - - // Assume we were given the full id - return waitForPromise( - ShowStyleVariants.findOneAsync({ - showStyleBaseId: this.showStyleBase._id, - _id: protectString(variantId), - }) - ) - } - getVariant(variantId: string): IBlueprintShowStyleVariant | undefined { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - - return unprotectObject(this.getVariantFromDb(variantId)) as any - } - insertVariant(variantId: string, variant: OmitId): string { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - - return unprotectString( - waitForPromise( - ShowStyleVariants.insertAsync({ - ...variant, - _id: this.getProtectedVariantId(variantId), - showStyleBaseId: this.showStyleBase._id, - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - _rank: 0, - }) - ) - ) - } - updateVariant(variantId: string, newVariant: Partial): void { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `Variant "${variantId}" not found`) - - waitForPromise(ShowStyleVariants.updateAsync(variant._id, { $set: newVariant })) - } - removeVariant(variantId: string): void { - check(variantId, String) - if (!variantId) { - throw new Meteor.Error(500, `Variant id "${variantId}" is invalid`) - } - - waitForPromise( - ShowStyleVariants.removeAsync({ - _id: this.getProtectedVariantId(variantId), - showStyleBaseId: this.showStyleBase._id, - }) - ) - } - getSourceLayer(sourceLayerId: string): ISourceLayer | undefined { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - return this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - } - insertSourceLayer(sourceLayerId: string, layer: OmitId): string { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - if (oldLayer) { - throw new Meteor.Error(500, `SourceLayer "${sourceLayerId}" already exists`) - } - - const fullLayer: ISourceLayer = { - ...layer, - _id: sourceLayerId, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $set: { - [`sourceLayersWithOverrides.defaults.${sourceLayerId}`]: fullLayer, - }, - } - ) - ) - this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] = fullLayer // Update local - return fullLayer._id - } - updateSourceLayer(sourceLayerId: string, layer: Partial): void { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - if (!oldLayer) { - throw new Meteor.Error(404, `SourceLayer "${sourceLayerId}" cannot be updated as it does not exist`) - } - - const fullLayer = { - ...oldLayer, - ...layer, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - 'sourceLayers._id': sourceLayerId, - }, - { - $set: { - [`sourceLayersWithOverrides.defaults.${sourceLayerId}`]: fullLayer, - }, - }, - { multi: false } - ) - ) - this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] = fullLayer // Update local - } - removeSourceLayer(sourceLayerId: string): void { - check(sourceLayerId, String) - if (!sourceLayerId) { - throw new Meteor.Error(500, `SourceLayer id "${sourceLayerId}" is invalid`) - } - - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $unset: { - [`sourceLayersWithOverrides.defaults.${sourceLayerId}`]: 1, - }, - } - ) - ) - // Update local: - delete this.showStyleBase.sourceLayersWithOverrides.defaults[sourceLayerId] - } - getOutputLayer(outputLayerId: string): IOutputLayer | undefined { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - return this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - } - insertOutputLayer(outputLayerId: string, layer: OmitId): string { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - if (oldLayer) { - throw new Meteor.Error(500, `OutputLayer "${outputLayerId}" already exists`) - } - - const fullLayer: IOutputLayer = { - ...layer, - _id: outputLayerId, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $set: { - [`outputLayersWithOverrides.defaults.${outputLayerId}`]: fullLayer, - }, - } - ) - ) - - this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] = fullLayer // Update local - return fullLayer._id - } - updateOutputLayer(outputLayerId: string, layer: Partial): void { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - const oldLayer = this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - if (!oldLayer) { - throw new Meteor.Error(404, `OutputLayer "${outputLayerId}" cannot be updated as it does not exist`) - } - - const fullLayer = { - ...oldLayer, - ...layer, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $set: { - [`outputLayersWithOverrides.defaults.${outputLayerId}`]: fullLayer, - }, - } - ) - ) - this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] = fullLayer // Update local - } - removeOutputLayer(outputLayerId: string): void { - check(outputLayerId, String) - if (!outputLayerId) { - throw new Meteor.Error(500, `OutputLayer id "${outputLayerId}" is invalid`) - } - - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $unset: { - [`outputLayersWithOverrides.defaults.${outputLayerId}`]: 1, - }, - } - ) - ) - // Update local: - delete this.showStyleBase.outputLayersWithOverrides.defaults[outputLayerId] - } - getBaseConfig(configId: string): ConfigItemValue | undefined { - check(configId, String) - if (configId === '') return undefined - const configItem = objectPathGet(this.showStyleBase.blueprintConfigWithOverrides.defaults, configId) - return trimIfString(configItem) - } - setBaseConfig(configId: string, value: ConfigItemValue): void { - check(configId, String) - if (!configId) { - throw new Meteor.Error(500, `Config id "${configId}" is invalid`) - } - - if (_.isUndefined(value)) throw new Meteor.Error(400, `setBaseConfig "${configId}": value is undefined`) - - value = trimIfString(value) - - const modifier: MongoModifier = { - $set: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: value, - }, - } - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - modifier - ) - ) - objectPathSet(this.showStyleBase.blueprintConfigWithOverrides.defaults, configId, value) // Update local - } - removeBaseConfig(configId: string): void { - check(configId, String) - if (configId) { - waitForPromise( - ShowStyleBases.updateAsync( - { - _id: this.showStyleBase._id, - }, - { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - ) - ) - // Update local: - objectPathDelete(this.showStyleBase.blueprintConfigWithOverrides.defaults, configId) - } - } - getVariantConfig(variantId: string, configId: string): ConfigItemValue | undefined { - check(variantId, String) - check(configId, String) - if (configId === '') return undefined - - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found`) - - const configItem = objectPathGet(variant.blueprintConfigWithOverrides.defaults, configId) - return trimIfString(configItem) - } - setVariantConfig(variantId: string, configId: string, value: ConfigItemValue): void { - check(variantId, String) - check(configId, String) - if (!configId) { - throw new Meteor.Error(500, `Config id "${configId}" is invalid`) - } - - value = trimIfString(value) - - if (_.isUndefined(value)) - throw new Meteor.Error(400, `setVariantConfig "${variantId}", "${configId}": value is undefined`) - - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found`) - - const modifier: MongoModifier = { - $set: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: value, - }, - } - waitForPromise( - ShowStyleVariants.updateAsync( - { - _id: variant._id, - }, - modifier - ) - ) - objectPathSet(variant.blueprintConfigWithOverrides.defaults, configId, value) // Update local - } - removeVariantConfig(variantId: string, configId: string): void { - check(variantId, String) - check(configId, String) - - if (configId) { - const variant = this.getVariantFromDb(variantId) - if (!variant) throw new Meteor.Error(404, `ShowStyleVariant "${variantId}" not found`) - - waitForPromise( - ShowStyleVariants.updateAsync( - { - _id: variant._id, - }, - { - $unset: { - [`blueprintConfigWithOverrides.defaults.${configId}`]: 1, - }, - } - ) - ) - // Update local: - objectPathDelete(variant.blueprintConfigWithOverrides.defaults, configId) - } - } -} -*/ diff --git a/meteor/server/coreSystem/checkDatabaseVersions.ts b/meteor/server/coreSystem/checkDatabaseVersions.ts index 8fdd141b56..469b3e6914 100644 --- a/meteor/server/coreSystem/checkDatabaseVersions.ts +++ b/meteor/server/coreSystem/checkDatabaseVersions.ts @@ -1,8 +1,7 @@ import { StatusCode } from '@sofie-automation/blueprints-integration' -import { BlueprintId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' -import { Blueprints, ShowStyleBases, Studios } from '../collections' +import { Blueprints } from '../collections' import { parseVersion, compareSemverVersions, @@ -10,8 +9,6 @@ import { isPrerelease, parseCoreIntegrationCompatabilityRange, } from '../systemStatus/semverUtils' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { lazyIgnore } from '../lib/lib' import { logger } from '../logging' import { CURRENT_SYSTEM_VERSION } from '../migration/currentSystemVersion' @@ -89,62 +86,7 @@ export function checkDatabaseVersions(): void { blueprintIds.add(blueprint._id) if (!blueprint.databaseVersion || typeof blueprint.databaseVersion === 'string') - blueprint.databaseVersion = { showStyle: {}, studio: {}, system: undefined } - if (!blueprint.databaseVersion.showStyle) blueprint.databaseVersion.showStyle = {} - if (!blueprint.databaseVersion.studio) blueprint.databaseVersion.studio = {} - - let o: { - statusCode: StatusCode - messages: string[] - } = { - statusCode: StatusCode.BAD, - messages: [], - } - - const checkedStudioIds = new Set() - - const showStylesForBlueprint = (await ShowStyleBases.findFetchAsync( - { blueprintId: blueprint._id }, - { - fields: { _id: 1 }, - } - )) as Array> - for (const showStyleBase of showStylesForBlueprint) { - if (o.statusCode === StatusCode.GOOD) { - o = compareSemverVersions( - parseVersion(blueprint.blueprintVersion), - parseRange(blueprint.databaseVersion.showStyle[unprotectString(showStyleBase._id)]), - false, - 'to fix, run migration', - 'blueprint version', - `showStyle "${showStyleBase._id}" migrations` - ) - } - - const studiosForShowStyleBase = (await Studios.findFetchAsync( - { supportedShowStyleBase: showStyleBase._id }, - { - fields: { _id: 1 }, - } - )) as Array> - for (const studio of studiosForShowStyleBase) { - if (!checkedStudioIds.has(studio._id)) { - // only run once per blueprint and studio - checkedStudioIds.add(studio._id) - - if (o.statusCode === StatusCode.GOOD) { - o = compareSemverVersions( - parseVersion(blueprint.blueprintVersion), - parseRange(blueprint.databaseVersion.studio[unprotectString(studio._id)]), - false, - 'to fix, run migration', - 'blueprint version', - `studio "${studio._id}]" migrations` - ) - } - } - } - } + blueprint.databaseVersion = { system: undefined } checkBlueprintCompability(blueprint) } diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 788fdaf33a..4994be533b 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -5,21 +5,11 @@ import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigra import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion' import { RunMigrationResult, GetMigrationStatusResult } from '@sofie-automation/meteor-lib/dist/api/migration' import { literal, protectString } from '../../lib/tempLib' -import { - MigrationStepInputResult, - BlueprintManifestType, - MigrationContextStudio, - MigrationContextShowStyle, - PlaylistTimingType, - PlaylistTimingNone, - ShowStyleBlueprintManifest, - StudioBlueprintManifest, -} from '@sofie-automation/blueprints-integration' +import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { generateFakeBlueprint } from '../../api/blueprints/__tests__/lib' import { MeteorCall } from '../../api/methods' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { Blueprints, ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' +import { ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' import { getCoreSystemAsync } from '../../coreSystem/collection' import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' import fs from 'fs' @@ -247,160 +237,6 @@ describe('Migrations', () => { const studio = (await Studios.findOneAsync({})) as DBStudio expect(studio).toBeTruthy() - const studioManifest = (): StudioBlueprintManifest => ({ - blueprintType: 'studio' as BlueprintManifestType.STUDIO, - blueprintVersion: '1.0.0', - integrationVersion: '0.0.0', - TSRVersion: '0.0.0', - - configPresets: { - main: { - name: 'Main', - config: {}, - }, - }, - - studioConfigSchema: '{}' as any, - studioMigrations: [ - { - version: '0.2.0', - id: 'myStudioMockStep2', - validate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest2')) return `mocktest2 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest2')) { - context.setConfig('mocktest2', true) - } - }, - }, - { - version: '0.3.0', - id: 'myStudioMockStep3', - validate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest3')) return `mocktest3 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest3')) { - context.setConfig('mocktest3', true) - } - }, - }, - { - version: '0.1.0', - id: 'myStudioMockStep1', - validate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest1')) return `mocktest1 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextStudio) => { - if (!context.getConfig('mocktest1')) { - context.setConfig('mocktest1', true) - } - }, - }, - ], - getBaseline: () => { - return { - timelineObjects: [], - } - }, - getShowStyleId: () => null, - }) - - const showStyleManifest = (): ShowStyleBlueprintManifest => ({ - blueprintType: 'showstyle' as BlueprintManifestType.SHOWSTYLE, - blueprintVersion: '1.0.0', - integrationVersion: '0.0.0', - TSRVersion: '0.0.0', - - configPresets: { - main: { - name: 'Main', - config: {}, - - variants: { - main: { - name: 'Default', - config: {}, - }, - }, - }, - }, - - showStyleConfigSchema: '{}' as any, - showStyleMigrations: [ - { - version: '0.2.0', - id: 'myShowStyleMockStep2', - validate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest2')) return `mocktest2 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest2')) { - context.setBaseConfig('mocktest2', true) - } - }, - }, - { - version: '0.3.0', - id: 'myShowStyleMockStep3', - validate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest3')) return `mocktest3 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest3')) { - context.setBaseConfig('mocktest3', true) - } - }, - }, - { - version: '0.1.0', - id: 'myShowStyleMockStep1', - validate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest1')) return `mocktest1 config not set` - return false - }, - canBeRunAutomatically: true, - migrate: (context: MigrationContextShowStyle) => { - if (!context.getBaseConfig('mocktest1')) { - context.setBaseConfig('mocktest1', true) - } - }, - }, - ], - getShowStyleVariantId: () => null, - getRundown: () => ({ - rundown: { - externalId: '', - name: '', - timing: literal({ - type: PlaylistTimingType.None, - }), - }, - globalAdLibPieces: [], - globalActions: [], - baseline: { timelineObjects: [] }, - }), - getSegment: () => ({ - segment: { name: '' }, - parts: [], - }), - }) - - await Blueprints.insertAsync( - generateFakeBlueprint('showStyle0', BlueprintManifestType.SHOWSTYLE, showStyleManifest) - ) - await ShowStyleBases.insertAsync({ _id: protectString('showStyle0'), name: '', @@ -424,7 +260,6 @@ describe('Migrations', () => { _rank: 0, }) - await Blueprints.insertAsync(generateFakeBlueprint('studio0', BlueprintManifestType.STUDIO, studioManifest)) await Studios.updateAsync(studio._id, { $set: { blueprintId: protectString('studio0'), diff --git a/meteor/server/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index 42b0d76b1e..bebc1edaab 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -4,25 +4,15 @@ import { BlueprintManifestType, InputFunctionCore, InputFunctionSystem, - InputFunctionShowStyle, - InputFunctionStudio, MigrateFunctionCore, - MigrateFunctionShowStyle, - MigrateFunctionStudio, MigrationContextSystem as IMigrationContextSystem, - MigrationContextShowStyle as IMigrationContextShowStyle, - MigrationContextStudio as IMigrationContextStudio, MigrationStep, MigrationStepInput, MigrationStepInputFilteredResult, MigrationStepInputResult, - ShowStyleBlueprintManifest, - StudioBlueprintManifest, SystemBlueprintManifest, ValidateFunctionCore, ValidateFunctionSystem, - ValidateFunctionShowStyle, - ValidateFunctionStudio, MigrateFunctionSystem, ValidateFunction, MigrateFunction, @@ -40,13 +30,13 @@ import { logger } from '../logging' import { internalStoreSystemSnapshot } from '../api/snapshot' import { parseVersion, Version } from '../systemStatus/semverUtils' import { GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { clone, getHash, omit, protectString, unprotectString } from '../lib/tempLib' +import { clone, getHash, omit, protectString } from '../lib/tempLib' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { evalBlueprint } from '../api/blueprints/cache' import { MigrationContextSystem } from '../api/blueprints/migrationContext' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { SnapshotId, ShowStyleBaseId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Blueprints, CoreSystem, ShowStyleBases, Studios } from '../collections' +import { SnapshotId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Blueprints, CoreSystem } from '../collections' import { getSystemStorePath } from '../coreSystem' import { getCoreSystemAsync, setCoreSystemVersion } from '../coreSystem/collection' @@ -158,101 +148,19 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { - const chunk: MigrationChunk = { - sourceType: MigrationStepType.SHOWSTYLE, - sourceName: 'Blueprint ' + blueprint.name + ' for showStyle ' + showStyleBase.name, - blueprintId: blueprint._id, - sourceId: showStyleBase._id, - _dbVersion: parseVersion( - blueprint.databaseVersion.showStyle[unprotectString(showStyleBase._id)] || '0.0.0' - ), - _targetVersion: parseVersion(bp.blueprintVersion), - _steps: [], - } - migrationChunks.push(chunk) - // Add show-style migration steps from blueprint: - for (const step of bp.showStyleMigrations) { - allMigrationSteps.push( - prefixIdsOnStep('blueprint_' + blueprint._id + '_showStyle_' + showStyleBase._id + '_', { - id: step.id, - overrideSteps: step.overrideSteps, - validate: step.validate, - canBeRunAutomatically: step.canBeRunAutomatically, - migrate: step.migrate, - input: step.input, - dependOnResultFrom: step.dependOnResultFrom, - version: step.version, - _version: parseVersion(step.version), - _validateResult: false, // to be set later - _rank: rank++, - chunk: chunk, - }) - ) - } - }) - } else if (blueprint.blueprintType === BlueprintManifestType.STUDIO) { - const bp = blueprintManifest as StudioBlueprintManifest - - // If blueprint uses the new flow, don't attempt migrations - if (typeof bp.applyConfig === 'function') continue - - // Find all studios that use this blueprint - const studios = await Studios.findFetchAsync({ blueprintId: blueprint._id }) - studios.forEach((studio) => { - const chunk: MigrationChunk = { - sourceType: MigrationStepType.STUDIO, - sourceName: 'Blueprint ' + blueprint.name + ' for studio ' + studio.name, - blueprintId: blueprint._id, - sourceId: studio._id, - _dbVersion: parseVersion( - blueprint.databaseVersion.studio[unprotectString(studio._id)] || '0.0.0' - ), - _targetVersion: parseVersion(bp.blueprintVersion), - _steps: [], - } - migrationChunks.push(chunk) - // Add studio migration steps from blueprint: - for (const step of bp.studioMigrations) { - allMigrationSteps.push( - prefixIdsOnStep('blueprint_' + blueprint._id + '_studio_' + studio._id + '_', { - id: step.id, - overrideSteps: step.overrideSteps, - validate: step.validate, - canBeRunAutomatically: step.canBeRunAutomatically, - migrate: step.migrate, - input: step.input, - dependOnResultFrom: step.dependOnResultFrom, - version: step.version, - _version: parseVersion(step.version), - _validateResult: false, // to be set later - _rank: rank++, - chunk: chunk, - }) - ) - } - }) - } else if (blueprint.blueprintType === BlueprintManifestType.SYSTEM) { + if (blueprint.blueprintType === BlueprintManifestType.SYSTEM) { const bp = blueprintManifest as SystemBlueprintManifest // Check if the coreSystem uses this blueprint const coreSystems = await CoreSystem.findFetchAsync({ @@ -307,15 +215,6 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise) { for (const chunk of chunks) { if (chunk.sourceType === MigrationStepType.CORE) { await setCoreSystemVersion(chunk._targetVersion) - } else if ( - chunk.sourceType === MigrationStepType.STUDIO || - chunk.sourceType === MigrationStepType.SHOWSTYLE || - chunk.sourceType === MigrationStepType.SYSTEM - ) { + } else if (chunk.sourceType === MigrationStepType.SYSTEM) { if (!chunk.blueprintId) throw new Meteor.Error(500, `chunk.blueprintId missing!`) if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing!`) @@ -708,20 +579,6 @@ async function completeMigration(chunks: Array) { `Updating Blueprint "${chunk.sourceName}" version, from "${blueprint.databaseVersion.system}" to "${chunk._targetVersion}".` ) m[`databaseVersion.system`] = chunk._targetVersion - } else if (chunk.sourceType === MigrationStepType.STUDIO && chunk.sourceId !== 'system') { - logger.info( - `Updating Blueprint "${chunk.sourceName}" version, from "${ - blueprint.databaseVersion.studio[unprotectString(chunk.sourceId)] - }" to "${chunk._targetVersion}".` - ) - m[`databaseVersion.studio.${chunk.sourceId}`] = chunk._targetVersion - } else if (chunk.sourceType === MigrationStepType.SHOWSTYLE && chunk.sourceId !== 'system') { - logger.info( - `Updating Blueprint "${chunk.sourceName}" version, from "${ - blueprint.databaseVersion.showStyle[unprotectString(chunk.sourceId)] - }" to "${chunk._targetVersion}".` - ) - m[`databaseVersion.showStyle.${chunk.sourceId}`] = chunk._targetVersion } else throw new Meteor.Error(500, `Bad chunk.sourcetype: "${chunk.sourceType}"`) await Blueprints.updateAsync(chunk.blueprintId, { $set: m }) @@ -777,8 +634,6 @@ export async function resetDatabaseVersions(): Promise { { $set: { databaseVersion: { - studio: {}, - showStyle: {}, system: '', }, }, @@ -794,29 +649,3 @@ function getMigrationSystemContext(chunk: MigrationChunk): IMigrationContextSyst return new MigrationContextSystem() } -async function getMigrationStudioContext(chunk: MigrationChunk): Promise { - if (chunk.sourceType !== MigrationStepType.STUDIO) - throw new Meteor.Error(500, `wrong chunk.sourceType "${chunk.sourceType}", expected STUDIO`) - if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing`) - if (chunk.sourceId === 'system') - throw new Meteor.Error(500, `cunk.sourceId invalid in this context: ${chunk.sourceId}`) - - const studio = await Studios.findOneAsync(chunk.sourceId as StudioId) - if (!studio) throw new Meteor.Error(404, `Studio "${chunk.sourceId}" not found`) - - // return new MigrationContextStudio(studio) - throw new Meteor.Error(500, 'Studio migrations not supported!') -} -async function getMigrationShowStyleContext(chunk: MigrationChunk): Promise { - if (chunk.sourceType !== MigrationStepType.SHOWSTYLE) - throw new Meteor.Error(500, `wrong chunk.sourceType "${chunk.sourceType}", expected SHOWSTYLE`) - if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing`) - if (chunk.sourceId === 'system') - throw new Meteor.Error(500, `cunk.sourceId invalid in this context: ${chunk.sourceId}`) - - const showStyleBase = await ShowStyleBases.findOneAsync(chunk.sourceId as ShowStyleBaseId) - if (!showStyleBase) throw new Meteor.Error(404, `ShowStyleBase "${chunk.sourceId}" not found`) - - // return new MigrationContextShowStyle(showStyleBase) - throw new Meteor.Error(500, 'ShowStyle migrations not supported!') -} diff --git a/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts b/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts index 645f78e989..9ac3aff7ca 100644 --- a/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts +++ b/meteor/server/migration/upgrades/__tests__/showStyleBase.test.ts @@ -49,7 +49,6 @@ describe('ShowStyleBase upgrades', () => { }, showStyleConfigSchema: JSONBlobStringify({}), - showStyleMigrations: [], getShowStyleVariantId: (): string | null => { return null }, diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 9a4958ea28..c318ab1cf1 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -21,7 +21,7 @@ import type { } from '../context' import type { IngestAdlib, ExtendedIngestRundown, IngestRundown } from '../ingest' import type { IBlueprintExternalMessageQueueObj } from '../message' -import type { MigrationStepShowStyle } from '../migrations' +import type {} from '../migrations' import type { IBlueprintAdLibPiece, IBlueprintResolvedPieceInstance, @@ -56,10 +56,6 @@ export interface ShowStyleBlueprintManifest - /** A list of Migration steps related to a ShowStyle - * @deprecated This has been replaced with `validateConfig` and `applyConfig` - */ - showStyleMigrations: MigrationStepShowStyle[] /** The config presets exposed by this blueprint */ configPresets: Record> diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index fd0c49c5d2..27eeb4074f 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -3,7 +3,6 @@ import type { ReadonlyDeep } from 'type-fest' import type { BlueprintConfigCoreConfig, BlueprintManifestBase, BlueprintManifestType, IConfigMessage } from './base' import type { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import type { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob' -import type { MigrationStepStudio } from '../migrations' import type { ICommonContext, IFixUpConfigContext, @@ -35,10 +34,6 @@ export interface StudioBlueprintManifest - /** A list of Migration steps related to a Studio - * @deprecated This has been replaced with `validateConfig` and `applyConfig` - */ - studioMigrations: MigrationStepStudio[] /** The config presets exposed by this blueprint */ configPresets: Record> diff --git a/packages/blueprints-integration/src/migrations.ts b/packages/blueprints-integration/src/migrations.ts index 6309e8cc90..62d3d4913f 100644 --- a/packages/blueprints-integration/src/migrations.ts +++ b/packages/blueprints-integration/src/migrations.ts @@ -1,9 +1,4 @@ -import { ConfigItemValue } from './common' -import { OmitId } from './lib' -import { IBlueprintShowStyleVariant, IOutputLayer, ISourceLayer } from './showStyle' import { IBlueprintTriggeredActions } from './triggers' -import { BlueprintMapping } from './studio' -import { TSR } from './timeline' export interface MigrationStepInput { stepId?: string // automatically filled in later @@ -28,59 +23,18 @@ export type ValidateFunctionSystem = ( context: MigrationContextSystem, afterMigration: boolean ) => Promise -export type ValidateFunctionStudio = (context: MigrationContextStudio, afterMigration: boolean) => boolean | string -export type ValidateFunctionShowStyle = ( - context: MigrationContextShowStyle, - afterMigration: boolean -) => boolean | string -export type ValidateFunction = - | ValidateFunctionStudio - | ValidateFunctionShowStyle - | ValidateFunctionSystem - | ValidateFunctionCore +export type ValidateFunction = ValidateFunctionSystem | ValidateFunctionCore export type MigrateFunctionCore = (input: MigrationStepInputFilteredResult) => Promise export type MigrateFunctionSystem = ( context: MigrationContextSystem, input: MigrationStepInputFilteredResult ) => Promise -export type MigrateFunctionStudio = (context: MigrationContextStudio, input: MigrationStepInputFilteredResult) => void -export type MigrateFunctionShowStyle = ( - context: MigrationContextShowStyle, - input: MigrationStepInputFilteredResult -) => void -export type MigrateFunction = - | MigrateFunctionStudio - | MigrateFunctionShowStyle - | MigrateFunctionSystem - | MigrateFunctionCore +export type MigrateFunction = MigrateFunctionSystem | MigrateFunctionCore export type InputFunctionCore = () => MigrationStepInput[] export type InputFunctionSystem = (context: MigrationContextSystem) => MigrationStepInput[] -export type InputFunctionStudio = (context: MigrationContextStudio) => MigrationStepInput[] -export type InputFunctionShowStyle = (context: MigrationContextShowStyle) => MigrationStepInput[] -export type InputFunction = InputFunctionStudio | InputFunctionShowStyle | InputFunctionSystem | InputFunctionCore - -export interface MigrationContextStudio { - getMapping: (mappingId: string) => BlueprintMapping | undefined - insertMapping: (mappingId: string, mapping: OmitId) => string - updateMapping: (mappingId: string, mapping: Partial) => void - removeMapping: (mappingId: string) => void - - getConfig: (configId: string) => ConfigItemValue | undefined - setConfig: (configId: string, value: ConfigItemValue) => void - removeConfig: (configId: string) => void - - getDevice: (deviceId: string) => TSR.DeviceOptionsAny | undefined - insertDevice: (deviceId: string, device: TSR.DeviceOptionsAny) => string | null - updateDevice: (deviceId: string, device: Partial) => void - removeDevice: (deviceId: string) => void -} - -export interface ShowStyleVariantPart { - // Note: if more props are added it may make sense to use Omit<> to build this type - name: string -} +export type InputFunction = InputFunctionSystem | InputFunctionCore interface MigrationContextWithTriggeredActions { getAllTriggeredActions: () => Promise @@ -90,33 +44,6 @@ interface MigrationContextWithTriggeredActions { removeTriggeredAction: (triggeredActionId: string) => Promise } -export interface MigrationContextShowStyle extends MigrationContextWithTriggeredActions { - getAllVariants: () => IBlueprintShowStyleVariant[] - getVariantId: (variantId: string) => string - getVariant: (variantId: string) => IBlueprintShowStyleVariant | undefined - insertVariant: (variantId: string, variant: OmitId) => string - updateVariant: (variantId: string, variant: Partial) => void - removeVariant: (variantId: string) => void - - getSourceLayer: (sourceLayerId: string) => ISourceLayer | undefined - insertSourceLayer: (sourceLayerId: string, layer: OmitId) => string - updateSourceLayer: (sourceLayerId: string, layer: Partial) => void - removeSourceLayer: (sourceLayerId: string) => void - - getOutputLayer: (outputLayerId: string) => IOutputLayer | undefined - insertOutputLayer: (outputLayerId: string, layer: OmitId) => string - updateOutputLayer: (outputLayerId: string, layer: Partial) => void - removeOutputLayer: (outputLayerId: string) => void - - getBaseConfig: (configId: string) => ConfigItemValue | undefined - setBaseConfig: (configId: string, value: ConfigItemValue) => void - removeBaseConfig: (configId: string) => void - - getVariantConfig: (variantId: string, configId: string) => ConfigItemValue | undefined - setVariantConfig: (variantId: string, configId: string, value: ConfigItemValue) => void - removeVariantConfig: (variantId: string, configId: string) => void -} - export type MigrationContextSystem = MigrationContextWithTriggeredActions export interface MigrationStepBase< @@ -163,9 +90,3 @@ export interface MigrationStep< export type MigrationStepCore = MigrationStep export type MigrationStepSystem = MigrationStep -export type MigrationStepStudio = MigrationStep -export type MigrationStepShowStyle = MigrationStep< - ValidateFunctionShowStyle, - MigrateFunctionShowStyle, - InputFunctionShowStyle -> diff --git a/packages/corelib/src/dataModel/Blueprint.ts b/packages/corelib/src/dataModel/Blueprint.ts index 99e025bfdf..8a80fa586a 100644 --- a/packages/corelib/src/dataModel/Blueprint.ts +++ b/packages/corelib/src/dataModel/Blueprint.ts @@ -38,12 +38,6 @@ export interface Blueprint { showStyleConfigPresets?: Record databaseVersion: { - showStyle: { - [showStyleBaseId: string]: string - } - studio: { - [studioId: string]: string - } system: string | undefined } diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index 57a861bb9f..df92565e29 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -288,7 +288,6 @@ const MockStudioBlueprint: () => StudioBlueprintManifest = () => ({ }, studioConfigSchema: JSONBlobStringify({}), - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -320,7 +319,6 @@ const MockShowStyleBlueprint: () => ShowStyleBlueprintManifest = () => ({ }, showStyleConfigSchema: JSONBlobStringify({}), - showStyleMigrations: [], getShowStyleVariantId: (_context, variants): string | null => { return variants[0]._id }, diff --git a/packages/job-worker/src/blueprints/__tests__/lib.ts b/packages/job-worker/src/blueprints/__tests__/lib.ts index cd34200204..29b54f4aac 100644 --- a/packages/job-worker/src/blueprints/__tests__/lib.ts +++ b/packages/job-worker/src/blueprints/__tests__/lib.ts @@ -18,7 +18,6 @@ export function generateFakeBlueprint( integrationVersion: '0.0.0', TSRVersion: '0.0.0', studioConfigManifest: [], - studioMigrations: [], getBaseline: () => { return { timelineObjects: [], @@ -45,8 +44,6 @@ export function generateFakeBlueprint( databaseVersion: { system: undefined, - showStyle: {}, - studio: {}, }, blueprintVersion: '', diff --git a/packages/job-worker/src/blueprints/defaults/studio.ts b/packages/job-worker/src/blueprints/defaults/studio.ts index ff8a899cf8..43949c6384 100644 --- a/packages/job-worker/src/blueprints/defaults/studio.ts +++ b/packages/job-worker/src/blueprints/defaults/studio.ts @@ -26,7 +26,6 @@ export const DefaultStudioBlueprint: ReadonlyDeep = dee blueprintType: BlueprintManifestType.STUDIO, studioConfigSchema: JSONBlobStringify({}), - studioMigrations: [], configPresets: { 0: { From 11b7493f322cecd14063c4ad763d27ee569fe0bf Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 11 Dec 2024 13:33:28 +0100 Subject: [PATCH 15/18] WIP --- packages/job-worker/src/jobs/index.ts | 1 + packages/job-worker/src/workers/caches.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/job-worker/src/jobs/index.ts b/packages/job-worker/src/jobs/index.ts index 41103a4c23..15473ed085 100644 --- a/packages/job-worker/src/jobs/index.ts +++ b/packages/job-worker/src/jobs/index.ts @@ -109,6 +109,7 @@ export interface StudioCacheContext { readonly studio: ReadonlyDeep /** + * // nocommit: so whats the difference between studio and rawStudio? * The Studio the job belongs to */ readonly rawStudio: ReadonlyDeep diff --git a/packages/job-worker/src/workers/caches.ts b/packages/job-worker/src/workers/caches.ts index 6fee8f1aed..1c70ac4c30 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -101,6 +101,7 @@ export class WorkerDataCacheWrapperImpl implements WorkerDataCacheWrapper { */ export interface WorkerDataCache { rawStudio: ReadonlyDeep + // nocommit: maybe add description of what this is, why is it different from rawStudio jobStudio: ReadonlyDeep studioBlueprint: ReadonlyDeep studioBlueprintConfig: ProcessedStudioConfig | undefined From 20952103627d7597c6c057d594c1dd2926902aea Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 11 Dec 2024 13:26:41 +0100 Subject: [PATCH 16/18] chore: minor docs after code review --- meteor/server/api/rest/koa.ts | 3 ++- meteor/server/api/system.ts | 4 ++-- .../server/collections/implementations/asyncCollection.ts | 8 ++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index 6fc91e8706..673e9c3174 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -46,13 +46,14 @@ Meteor.startup(() => { ) // Expose the API at the url - WebApp.rawConnectHandlers.use((req, res) => { + WebApp.rawHandlers.use((req, res) => { const transaction = profiler.startTransaction(`${req.method}:${req.url}`, 'http.incoming') if (transaction) { transaction.setLabel('url', `${req.url}`) transaction.setLabel('method', `${req.method}`) res.on('finish', () => { + // When the end of the request is sent to the client, submit the apm transaction let route = req.originalUrl if (req.originalUrl && req.url && req.originalUrl.endsWith(req.url.slice(1)) && req.url.length > 1) { route = req.originalUrl.slice(0, -1 * (req.url.length - 1)) diff --git a/meteor/server/api/system.ts b/meteor/server/api/system.ts index 1668f76720..78419e28f0 100644 --- a/meteor/server/api/system.ts +++ b/meteor/server/api/system.ts @@ -73,7 +73,7 @@ async function setupIndexes(removeOldIndexes = false): Promise { // Ensure indexes are created on startup: - ensureIndexes() + createIndexes() }) async function cleanupIndexes( diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index db05a469ee..52bb47eca6 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -18,15 +18,19 @@ import { profiler } from '../../api/profiler' import { PromisifyCallbacks } from '@sofie-automation/shared-lib/dist/lib/types' import { AsyncOnlyMongoCollection } from '../collection' +/** + * A stripped down version of Meteor's Mongo.Cursor, with only the async methods + */ export type MinimalMongoCursor }> = Pick< MongoCursor, 'fetchAsync' | 'observeChangesAsync' | 'observeAsync' | 'countAsync' // | 'forEach' | 'map' | > - +/** + * A stripped down version of Meteor's Mongo.Collection, with only the async methods + */ export type MinimalMeteorMongoCollection }> = Pick< Mongo.Collection, - // | 'find' 'insertAsync' | 'removeAsync' | 'updateAsync' | 'upsertAsync' | 'rawCollection' | 'rawDatabase' | 'createIndex' > & { find: (...args: Parameters['find']>) => MinimalMongoCursor From 8ac7b39a9490a79ec89c06926b0060f8d07a61e0 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 11 Dec 2024 14:24:48 +0100 Subject: [PATCH 17/18] chore: minor docs after code review --- packages/job-worker/src/jobs/index.ts | 5 +++-- packages/job-worker/src/workers/caches.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/job-worker/src/jobs/index.ts b/packages/job-worker/src/jobs/index.ts index 15473ed085..586688a1f2 100644 --- a/packages/job-worker/src/jobs/index.ts +++ b/packages/job-worker/src/jobs/index.ts @@ -104,13 +104,14 @@ export interface StudioCacheContext { */ readonly studioId: StudioId /** - * The Studio the job belongs to + * The Studio the job belongs to. + * This has any ObjectWithOverrides in their computed/flattened form */ readonly studio: ReadonlyDeep /** - * // nocommit: so whats the difference between studio and rawStudio? * The Studio the job belongs to + * This has any ObjectWithOverrides in their original form */ readonly rawStudio: ReadonlyDeep diff --git a/packages/job-worker/src/workers/caches.ts b/packages/job-worker/src/workers/caches.ts index 1c70ac4c30..80d7eec6f1 100644 --- a/packages/job-worker/src/workers/caches.ts +++ b/packages/job-worker/src/workers/caches.ts @@ -100,8 +100,15 @@ export class WorkerDataCacheWrapperImpl implements WorkerDataCacheWrapper { * This is a reusable cache of these properties */ export interface WorkerDataCache { + /** + * The Studio the cache belongs to + * This has any ObjectWithOverrides in their original form + */ rawStudio: ReadonlyDeep - // nocommit: maybe add description of what this is, why is it different from rawStudio + /** + * The Studio the cache belongs to. + * This has any ObjectWithOverrides in their computed/flattened form + */ jobStudio: ReadonlyDeep studioBlueprint: ReadonlyDeep studioBlueprintConfig: ProcessedStudioConfig | undefined From dd5010a147e5d17db940430ae96fce73262c2523 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 11 Dec 2024 14:40:00 +0100 Subject: [PATCH 18/18] chore: fix unit test --- meteor/__mocks__/webapp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/__mocks__/webapp.ts b/meteor/__mocks__/webapp.ts index 4e0b5a4a93..28bb8d218e 100644 --- a/meteor/__mocks__/webapp.ts +++ b/meteor/__mocks__/webapp.ts @@ -1,5 +1,5 @@ export const WebAppMock = { - rawConnectHandlers: { + rawHandlers: { use: (): void => { // No web server to setup },