Skip to content

feat: ingest API: add the ability to create rundowns without specifying a playlist#1691

Merged
justandras merged 5 commits intoSofie-Automation:mainfrom
SuperFlyTV:fix/post-rundown-existing-rundown-check
Apr 1, 2026
Merged

feat: ingest API: add the ability to create rundowns without specifying a playlist#1691
justandras merged 5 commits intoSofie-Automation:mainfrom
SuperFlyTV:fix/post-rundown-existing-rundown-check

Conversation

@justandras
Copy link
Copy Markdown
Member

About the Contributor

This pull request is posted on behalf of the CBC.

Type of Contribution

This is a: Feature / Code improvement

Current Behavior

Previously, the Ingest REST API only supported creating a rundown via the endpoint:

POST /ingest/{studioId}/playlists/{playlistId}/rundowns

This forced external systems to provide a playlistId in the URL, treating the playlist as the "owner" of the rundown.

As discussed in #1686, this created limitations for workflows where the external system might not know the playlist mapping beforehand or where rundowns need to be ingested more flexibly.

New Behavior

This PR introduces a new studio-scoped endpoint:

POST /ingest/{studioId}/rundowns

(while keeping the already existing endpoint)

Benefits:

  • Studio-scoped Ingest:
    • Allows creating/updating a rundown without specifying a playlistId in the URL path.
    • If a playlistExternalId is provided within the rundown's data body, it is respected; otherwise, the system falls back to default behavior (allowing blueprints or core logic to handle playlist grouping/creation).
  • Reusing the same code path:
    • The postRundown method in the Ingest API now treats playlistId as optional (string | undefined).
    • The new endpoint is an alias that passes undefined as the playlistId.

Testing

  • I have added one or more unit tests for this PR
  • I have updated the relevant unit tests
  • No unit test changes are needed for this PR

Affected areas

  • Ingest API: Specifically the REST routing and the IngestServerAPI implementation.
  • OpenAPI Definitions: Updated actions.yaml and definitions/ingest.yaml to reflect the new endpoint.

Time Frame

Not urgent, but we would like to get this merged into the in-development release.

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

@justandras justandras requested review from imaretic and nytamin March 18, 2026 15:55
@justandras justandras self-assigned this Mar 18, 2026
@justandras justandras added the Contribution from CBC/Radio-Canada Contributions sponsored by CBC/Radio-Canada (cbc.radio-canada.ca) label Mar 18, 2026
@justandras justandras linked an issue Mar 18, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0b4f6944-8042-440b-971b-059b72913cdc

📥 Commits

Reviewing files that changed from the base of the PR and between d62c0f8 and 4633db2.

📒 Files selected for processing (2)
  • .github/workflows/node.yaml
  • .github/workflows/trivy.yml
💤 Files with no reviewable changes (2)
  • .github/workflows/trivy.yml
  • .github/workflows/node.yaml

Walkthrough

Makes playlistId optional for ingest POSTs, adds a studio-scoped POST /ingest/:studioId/rundowns route, preserves existing playlist-scoped route, updates payload handling/validation to accept an optional playlistExternalId, and extends OpenAPI and tests for the new endpoint and schema field.

Changes

Cohort / File(s) Summary
Server API Implementation
meteor/server/api/rest/v1/ingest.ts, meteor/server/lib/rest/v1/ingest.ts
postRundown signature now accepts `playlistId: string
OpenAPI: paths & schemas
packages/openapi/api/actions.yaml, packages/openapi/api/definitions/ingest.yaml
Added /ingest/{studioId}/rundowns operation (postRundownInStudio) and added optional playlistExternalId to components.schemas.rundown.
Tests
packages/openapi/src/__tests__/ingest.spec.ts
Added test "Can create rundown (studio-scoped)" exercising the new studio-scoped endpoint.
CI workflows
.github/workflows/node.yaml, .github/workflows/trivy.yml
Removed Trivy scanning steps from node.yaml and deleted the scheduled Trivy workflow (trivy.yml).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant REST as REST API (ingest route)
    participant API as IngestServerAPI.postRundown
    participant DB as Playlists DB
    participant Worker as Ingest Worker

    Client->>REST: POST /ingest/:studioId/rundowns (rundown payload)
    REST->>API: postRundown(connection, event, studioId, undefined, rawIngestRundown)
    API-->>DB: (optional) lookup playlist by playlistId/externalId
    DB-->>API: playlist (or null)
    API->>Worker: runIngestOperation(UpdateRundown, ingestRundown, resyncUrl)
    Worker-->>API: job accepted / queued
    API-->>REST: 202 Accepted
    REST-->>Client: 202 Accepted
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested labels

Contribution

Suggested reviewers

  • nytamin
  • imaretic
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: adding a studio-scoped rundown creation endpoint without requiring a playlist specification.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, new behavior, and affected areas.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 0% with 52 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
meteor/server/api/rest/v1/ingest.ts 0.00% 50 Missing ⚠️
meteor/server/lib/rest/v1/ingest.ts 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
meteor/server/lib/rest/v1/ingest.ts (1)

59-65: Keep the REST rundown type aligned with the new studio-scoped API.

postRundown can now be invoked without a path playlistId, but RestApiIngestRundown still omits playlistExternalId. That means the only caller-supplied playlist hint for the new route is currently untyped and unvalidated on the server side.

🧩 Suggested type/validation sync
-export type RestApiIngestRundown = Omit<IngestRundown, 'playlistExternalId'> & {
+export type RestApiIngestRundown = IngestRundown & {
 	resyncUrl: string
 }
 	private validateRundown(ingestRundown: RestApiIngestRundown) {
 		check(ingestRundown, Object)
 		check(ingestRundown.externalId, String)
 		check(ingestRundown.name, String)
 		check(ingestRundown.type, String)
 		check(ingestRundown.segments, Array)
 		check(ingestRundown.resyncUrl, String)
+		if (ingestRundown.playlistExternalId !== undefined) check(ingestRundown.playlistExternalId, String)
 
 		ingestRundown.segments.forEach((ingestSegment) => this.validateSegment(ingestSegment))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@meteor/server/lib/rest/v1/ingest.ts` around lines 59 - 65, The REST handler
postRundown accepts routes without a path playlistId but RestApiIngestRundown
still omits playlistExternalId, so add an optional playlistExternalId field to
the RestApiIngestRundown type and validate it in postRundown: when playlistId is
undefined, use/resolve ingestRundown.playlistExternalId (ensure it is present
and well-formed) and fallback behavior is explicit; update any validation/error
messages and places that derive a playlist from the request to prefer the path
playlistId then ingestRundown.playlistExternalId so server-side typing and
runtime checks stay in sync.
packages/openapi/src/__tests__/ingest.spec.ts (1)

146-150: Add a case that omits playlistExternalId.

This new test still passes a playlist hint, so it only verifies the alias route. It won't catch regressions in the actual “create rundown without specifying a playlist” path that motivated this change.

🧪 Suggested extra coverage
 test('Can create rundown (studio-scoped)', async () => {
 	const result = await ingestApi.postRundownInStudio({
 		studioId,
 		rundown: { ...rundown, externalId: 'newRundownInStudio', playlistExternalId: playlistIds[0] },
 	})
 	expect(result).toBe(undefined)
 })
+
+test('Can create rundown in studio without playlistExternalId', async () => {
+	const result = await ingestApi.postRundownInStudio({
+		studioId,
+		rundown: { ...rundown, externalId: 'newRundownInStudioWithoutPlaylist' },
+	})
+	expect(result).toBe(undefined)
+})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/openapi/src/__tests__/ingest.spec.ts` around lines 146 - 150, Add a
new test that exercises the "create rundown without specifying a playlist" path
by calling ingestApi.postRundownInStudio without the playlistExternalId field;
duplicate the existing Can create rundown (studio-scoped) test scenario but
remove playlistExternalId from the rundown payload and assert the expected
success behavior (same assertions as the existing test). Locate the existing
test named "Can create rundown (studio-scoped)" and create a sibling test (e.g.,
"Can create rundown in studio without playlistExternalId") that calls
postRundownInStudio({ studioId, rundown: { ...rundown, externalId:
'newRundownNoPlaylist' } }) to ensure the route works when no playlist hint is
provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@meteor/server/api/rest/v1/ingest.ts`:
- Around line 489-492: The code is blindly assigning the raw playlistId path
segment to ingestRundown.playlistExternalId when calling runIngestOperation
(IngestJobs.UpdateRundown), which causes internal playlist _id values to be
stored as external IDs; resolve/normalize playlistId the same way
putRundowns/putRundown do before passing it through: if playlistId refers to an
existing internal playlist _id, load that Playlist (by _id) and use its
externalId, otherwise accept the provided value as a new externalId, then pass
ingestRundown with playlistExternalId set to the resolved external ID.

In `@packages/openapi/api/definitions/ingest.yaml`:
- Around line 228-229: Update the requestBody description in the ingest.yaml
route to stop claiming that playlistExternalId is required: modify the
requestBody.description text so it no longer says "Must include
playlistExternalId" (or rephrase to indicate it's optional or recommended) so
the documented contract matches the shared rundown schema and server behavior;
locate the requestBody description string in
packages/openapi/api/definitions/ingest.yaml and update it accordingly.

---

Nitpick comments:
In `@meteor/server/lib/rest/v1/ingest.ts`:
- Around line 59-65: The REST handler postRundown accepts routes without a path
playlistId but RestApiIngestRundown still omits playlistExternalId, so add an
optional playlistExternalId field to the RestApiIngestRundown type and validate
it in postRundown: when playlistId is undefined, use/resolve
ingestRundown.playlistExternalId (ensure it is present and well-formed) and
fallback behavior is explicit; update any validation/error messages and places
that derive a playlist from the request to prefer the path playlistId then
ingestRundown.playlistExternalId so server-side typing and runtime checks stay
in sync.

In `@packages/openapi/src/__tests__/ingest.spec.ts`:
- Around line 146-150: Add a new test that exercises the "create rundown without
specifying a playlist" path by calling ingestApi.postRundownInStudio without the
playlistExternalId field; duplicate the existing Can create rundown
(studio-scoped) test scenario but remove playlistExternalId from the rundown
payload and assert the expected success behavior (same assertions as the
existing test). Locate the existing test named "Can create rundown
(studio-scoped)" and create a sibling test (e.g., "Can create rundown in studio
without playlistExternalId") that calls postRundownInStudio({ studioId, rundown:
{ ...rundown, externalId: 'newRundownNoPlaylist' } }) to ensure the route works
when no playlist hint is provided.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2319768-2aec-4622-9608-76005476a0d8

📥 Commits

Reviewing files that changed from the base of the PR and between 23b7ecc and a74e19d.

📒 Files selected for processing (5)
  • meteor/server/api/rest/v1/ingest.ts
  • meteor/server/lib/rest/v1/ingest.ts
  • packages/openapi/api/actions.yaml
  • packages/openapi/api/definitions/ingest.yaml
  • packages/openapi/src/__tests__/ingest.spec.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/openapi/api/definitions/ingest.yaml`:
- Around line 1132-1134: The TypeScript type RestApiIngestRundown currently uses
Omit<IngestRundown, 'playlistExternalId'> but the handler reads and processes
playlistExternalId at runtime; update the type so it includes playlistExternalId
(e.g., change RestApiIngestRundown to equal IngestRundown or otherwise include
playlistExternalId as an optional property) to match the OpenAPI schema and the
handler code that accesses playlistExternalId.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e7c9da57-39e3-4887-99b0-66f6e58ccf9e

📥 Commits

Reviewing files that changed from the base of the PR and between a74e19d and 6550898.

📒 Files selected for processing (2)
  • meteor/server/api/rest/v1/ingest.ts
  • packages/openapi/api/definitions/ingest.yaml
✅ Files skipped from review due to trivial changes (1)
  • meteor/server/api/rest/v1/ingest.ts

@justandras justandras merged commit 31e7596 into Sofie-Automation:main Apr 1, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Contribution from CBC/Radio-Canada Contributions sponsored by CBC/Radio-Canada (cbc.radio-canada.ca)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug Report: Rundowns are not correctly scoped under playlists

2 participants