Episode Data Model
References
Section titled “References”Linear issues:
Source decisions:
- Data Access Matrix (10/02/26) — defines visibility and editability per field
- D2D-2 comments: genre storage design (Dan, 14/02/26), genre inheritance rules (Ben, 03/03/26)
- CDN x TEP weekly catch-up (13/04/26): auto-generated episode IDs
- Field Reference v5.4: episode and publication field definitions
Working docs (engineering): 01-data-model-redesign.md, 06-genre-handling.md, 09-id-strategy-and-scenarios.md
Dependencies: XSD v1.1 support — project genre must be optional in validation before genre inheritance can work.
Note: This feature includes the full genre inheritance logic (first episode → project, effectiveGenre, projectGenre denormalization, commissioner genre negation) which was separated from XSD v1.1 support because it depends on the new episode document structure.
Overview
Section titled “Overview”The current system stores episode and post-transmission data in sub-documents on the screenProject (companyData and sharedData). This conflates storage location with access control and makes per-episode reporting queries inefficient.
This feature restructures where data is stored:
- Episode-level data (both pre-TX metadata and post-TX publication events) moves to the existing
episodescollection - Project-level data stays on the screenProject document in
formData - The
companyDataandsharedDataconcepts on screenProject are removed, replaced byformDatawith field-level metadata for access control (see Feature 4: Field Locking)
What This Means for Broadcasters
Section titled “What This Means for Broadcasters”Broadcasters provide two types of data: production metadata (pre-TX) and transmission records (post-TX). Both types of episode-level data are now stored against individual episodes, enabling per-episode reporting. For example, a broadcaster can query “show me all episodes broadcast on BBC One in Q3 2026” directly, without needing to go through the project first.
What This Means for Production Companies
Section titled “What This Means for Production Companies”Production companies continue to manage their projects and episodes through the same TEP interface. The underlying storage change is transparent — episodes are already a separate concept in the UI. Contributors remain linked to episodes as before.
Genre Handling
Section titled “Genre Handling”Genre data uses a three-field design on episodes to support efficient reporting while maintaining the inheritance rules agreed with CDN:
- Episode genre — only present when explicitly set (from XML or manual entry)
- Project genre (denormalized) — always present on episodes, kept in sync with the project
- Effective genre — always populated (from episode genre if set, else from project genre). This is the field used for all genre-based reporting queries.
Post-TX Data
Section titled “Post-TX Data”Post-transmission data (publication events) is stored as an array on the episode document. Each publication event records when and where an episode was broadcast or made available on-demand. This data is used for reporting filters only — it is not visible in the project management interface. Only broadcasters provide this data; production companies do not edit it.
Acceptance Criteria
Section titled “Acceptance Criteria”Episode Document Structure
Section titled “Episode Document Structure”- Episodes store pre-TX data: name, number, slotLength, releaseDate, genre, tags
- Episodes store post-TX data: publications array with publicationDateTime, channelPlatforms, availabilityMode, windowClosureDateTime, isPrimary, isRepeat, publicationId
- Episodes store external episode IDs:
externalEpisodeIdsarray with funderCompanyId + episodeId - Episodes store maker IDs:
makerIdsarray with funderCompanyId + makerId - Episodes store
genre,projectGenre, andeffectiveGenrefields correctly
Genre Three-Field Design
Section titled “Genre Three-Field Design”-
genrefield is only set when the episode has its own explicitly-set genre -
projectGenrefield is always present, reflecting the current project genre -
effectiveGenrefield is always populated (fromgenreif set, else fromprojectGenre) - When project genre changes,
projectGenreandeffectiveGenreare bulk-updated on all episodes without their owngenre - Episodes with their own
genreare unaffected by project genre changes (theireffectiveGenrestays as their own genre) - Genre reporting queries use
effectiveGenreonly (no$orpattern needed)
Project Document Changes
Section titled “Project Document Changes”-
formDatacaptures all project-level attributes — existing fields (projectName,numberOfEpisodes,deliveryDate,seriesIdentifier) continue to work; new fields (medium,broadcasterProjectType,greenlightDate,genre,commissioningDepartment,tags) are added -
companyDatasub-document is removed from screenProject -
sharedDatasub-document is removed from screenProject -
companyDataandsharedDatafields are removed from the screenProject schema (no data migration needed — these fields are not in use)
Post-TX Storage
Section titled “Post-TX Storage”- Publication events are stored in the episode’s
publicationsarray - Multiple publication events per episode are supported (broadcast + on-demand, repeats)
- Publication events include
funderCompanyIdto identify which broadcaster provided the data -
publicationIdenables corrections via resubmission (matching by publicationId updates the existing event)
Indexes
Section titled “Indexes”-
{ projectId: 1 }on episodes — basic project lookup -
{ companyId: 1 }on episodes — scope by production company -
{ "effectiveGenre.ofcom": 1 }on episodes — genre reporting filter -
{ "publications.publicationDateTime": 1 }on episodes — TX date reporting filter -
{ "publications.availabilityMode": 1 }on episodes — broadcast vs on-demand filter -
{ "tags.name": 1, "tags.value": 1 }on episodes — tag reporting filter -
{ releaseDate: 1 }on episodes — release date filter -
{ "makerIds.funderCompanyId": 1, "makerIds.makerId": 1 }on episodes — maker ID reporting scope -
{ "formData.genre.ofcom": 1 }on screenProjects — project genre reporting filter (new field) -
{ "formData.greenlightDate": 1 }on screenProjects — greenlight date filter (new field) -
{ "formData.deliveryDate": 1 }on screenProjects — delivery date filter (existing field, new index) -
{ "formData.medium": 1 }on screenProjects — medium filter (new field) -
{ "formData.broadcasterProjectType": 1 }on screenProjects — broadcaster project type filter (new field) -
{ "formData.tags.name": 1, "formData.tags.value": 1 }on screenProjects — project tag filter (new field)
Technical Specification
Section titled “Technical Specification”Episode Document Structure
Section titled “Episode Document Structure”{ "_id": "ObjectId", "projectId": "ObjectId", "companyId": "ObjectId",
"externalEpisodeIds": [ { "funderCompanyId": "<funderCompanyId>", "episodeId": "episode789" } ], "makerIds": [ { "funderCompanyId": "<funderCompanyId>", "makerId": "maker123" } ],
"name": "Episode 1", "number": "1", "slotLength": "PT1H", "releaseDate": "2024-09-15",
"genre": { "ofcom": "Drama", "ofcomSuper": "Entertainment", "commissioner": "Police Drama" }, "effectiveGenre": { "ofcom": "Drama", "ofcomSuper": "Entertainment", "commissioner": "Police Drama" }, "projectGenre": { "ofcom": "Drama", "ofcomSuper": "Entertainment" },
"tags": [ { "name": "series name", "value": "Line of Duty" }, { "name": "tx season", "value": "Autumn 2024" } ],
"publications": [ { "publicationId": "pub-001", "funderCompanyId": "<funderCompanyId>", "availabilityMode": "broadcast", "publicationDateTime": "2024-10-05T21:00:00Z", "windowClosureDateTime": null, "channelPlatforms": [ { "label": "BBC One", "id": "bbc-one", "subChannels": [ { "label": "HD", "id": "hd" } ] } ], "isPrimary": true, "isRepeat": false } ],
"fieldMetadata": { "name": { "source": "broadcaster", "channel": "xml", "setBy": "<funderCompanyId>", "lockedAt": "2026-..." }, "genre": { "source": "broadcaster", "channel": "xml", "setBy": "<funderCompanyId>", "lockedAt": "2026-..." }, "slotLength": { "source": "broadcaster", "channel": "xml", "setBy": "<funderCompanyId>", "lockedAt": "2026-..." } }}ScreenProject Changes
Section titled “ScreenProject Changes”Removed:
companyData— per-funder keyed data areasharedData— cross-funder shared data area
Modified:
-
formData— expanded to capture CDN XML project-level attributes. Mapping to existing vs new fields:Existing fields (no changes needed):
projectName← XML@namenumberOfEpisodes← XML@numberOfEpisodesdeliveryDate← XMLDeliveryDateseriesIdentifier← XML@number(series number, e.g., “6”)
New fields to add:
medium← XML@medium(enum:audio|video)broadcasterProjectType← XML@type(enum:series|special|feature film). Note: stored separately from existingcontentTypefield — whether these should be merged needs Adam’s input.greenlightDate← XMLGreenlightDate(date, inferred as first XML submission date if omitted)genre← XMLGenres(object:{ ofcom, ofcomSuper, commissioner }). This is a structured object separate from the existingcontentGenrestring field (see resolved decision below).commissioningDepartment← XML<Programmes>block@commissioningDepartmentattributetags← XMLTags(array of{ name, value })
Open questions for Adam:
- Should CDN’s
@type(series | special | feature film) map to the existingcontentTypefield, or stay as a separatebroadcasterProjectType? The existing values may use different terminology.
Resolved decisions:
contentGenrevsformData.genre(decided 2026-05-01): These remain separate concepts.contentGenreis a flat string field used by TEP/Omni clients for their own content classification.formData.genreis a structured{ ofcom, ofcomSuper }object sourced from CDN’s XML genre system (Ofcom regulatory genres) and used for episode genre inheritance. The CDN project form showsformData.genre.ofcomandformData.genre.ofcomSuperinstead ofcontentGenre. Commissioner genre is stored separately inprivateCompanyData.commissionerGenre(per TEP-452).- Ofcom genre validation list: Not yet available. Genre fields accept free-text strings for now. A validated Ofcom genre list (and the Ofcom → OfcomSuper derivation mapping) should be sourced and enforced in a future iteration.
- Genre field access:
formData.genre.ofcomandformData.genre.ofcomSuperare readable by all roles (shared data) but only editable by funders and system admins. Producers (owners) have read-only access. This is enforced via themyProjectAccessquery’s access matrix.
Added:
fieldMetadata— inline locking metadata (see Feature 4)externalIds— array-based external ID structure (see Feature 3)
XML Ingestion Data Store Changes
Section titled “XML Ingestion Data Store Changes”The current three data stores need restructuring:
mapping/stores/ProjectDataStore.ts:
- Update to write project-level fields to
formData(expanded scope)
mapping/stores/FunderDataStore.ts:
- Remove or repurpose —
companyDatano longer exists - Funder-specific metadata (commissioningDepartment, tags) now goes to
formDatawith appropriatefieldMetadata
mapping/stores/SharedDataStore.ts:
- Remove —
sharedDatano longer exists
New: mapping/stores/EpisodeDataStore.ts:
- Writes pre-TX episode data to the episodes collection
- Handles genre inheritance logic (
effectiveGenrepopulation) - Handles
projectGenredenormalization - Writes post-TX publication events to the episode’s
publicationsarray (viaupsertPublicationmethod) - Upsert by
publicationId+funderCompanyIdfor corrections; if nopublicationId, append as new publication event
Genre Inheritance Implementation
Section titled “Genre Inheritance Implementation”On episode write:
- If episode has its own
<Genres>in XML → setgenrefield, seteffectiveGenre=genre - If episode has no
<Genres>→ do NOT setgenrefield, seteffectiveGenre= current project genre - Always set
projectGenre= current project genre (denormalized copy)
On project genre change:
db.episodes.updateMany( { projectId: projectId, genre: { $exists: false } }, { $set: { projectGenre: newProjectGenre, effectiveGenre: newProjectGenre }})// Also update projectGenre on episodes WITH their own genre (for reference, though effectiveGenre is unchanged)db.episodes.updateMany( { projectId: projectId, genre: { $exists: true } }, { $set: { projectGenre: newProjectGenre } })GraphQL Schema Changes
Section titled “GraphQL Schema Changes”Removals (companyData/sharedData):
| Component | Action | Location |
|---|---|---|
| ScreenProject type | Remove sharedData and companyData fields | schema.graphql |
| Mutations | Remove updateScreenProjectSharedData | schema.graphql |
| Mutations | Remove updateScreenProjectFunderPrivateData | schema.graphql |
| Input types | Remove UpdateScreenProjectSharedDataInput | schema.graphql |
| Input types | Remove UpdateScreenProjectFunderPrivateDataInput | schema.graphql |
| Payload types | Remove UpdateScreenProjectSharedDataPayload | schema.graphql |
| Payload types | Remove UpdateScreenProjectFunderPrivateDataPayload | schema.graphql |
| Mutation resolvers | Delete updateScreenProjectSharedData.ts | resolvers/Mutations/ |
| Mutation resolvers | Delete updateScreenProjectFunderPrivateData.ts | resolvers/Mutations/ |
| Mutations index | Remove imports for both mutations | resolvers/Mutations/index.ts |
| Field resolvers | Remove sharedData and companyData resolvers | resolvers/Queries/fields.ts |
| Service layer | Delete ProjectSharedDataUpdater.ts | serviceLogic/ |
| Service layer | Delete ProjectFunderPrivateDataUpdater.ts | serviceLogic/ |
| Factory | Remove sharedDataUpdater and funderPrivateDataUpdater methods | serviceLogic/ProjectServiceLogicFactory.ts |
| Validation | Remove UpdateScreenProjectSharedDataSchema and UpdateScreenProjectFunderPrivateDataSchema | validation/funderDataSchemas.ts |
Additions (new formData fields):
The ScreenProjectFormData type, UpdateScreenProjectFormDataInput, and CreateScreenProjectInput need new fields:
# Add to ScreenProjectFormData typemedium: String # "audio" | "video"broadcasterProjectType: String # "series" | "special" | "feature film"greenlightDate: Dategenre: GenreData # new structured typecommissioningDepartment: Stringtags: [TagData!]
# New typestype GenreData { ofcom: String ofcomSuper: String commissioner: String}
type TagData { name: String! value: String!}The corresponding input types, Zod validation schemas, TypeScript interfaces, and MongoDB validation schema all need updating to match.
Episode type additions:
The existing Episode type needs new fields for publication data and genre:
# Add to Episode typegenre: GenreData # episode-specific genre (if set)effectiveGenre: GenreData # always populated (own genre or inherited)projectGenre: GenreData # denormalized project genreslotLength: String # ISO 8601 durationreleaseDate: Datetags: [TagData!]publications: [PublicationEvent!] # post-TX data
# New typetype PublicationEvent { publicationId: String funderCompanyId: ID! availabilityMode: String! # "broadcast" | "onDemand" publicationDateTime: DateTime! windowClosureDateTime: DateTime channelPlatforms: [ChannelPlatform!]! isPrimary: Boolean isRepeat: Boolean}
type ChannelPlatform { label: String! id: String subChannels: [SubChannel!]}
type SubChannel { label: String! id: String}Note: Publication events are read-only via GraphQL — they are only written by the XML ingestion pipeline or Ben’s upload tool. No create/update/delete mutations needed for publications at this stage.
Episode mutation updates:
The existing episode mutations (createScreenProjectEpisode, updateScreenProjectEpisode, deleteScreenProjectEpisode) need their input types expanded:
# Update CreateScreenProjectEpisodeInputinput CreateScreenProjectEpisodeInput { id: ID! number: Int! title: String # existing startDate: Date # existing deliveryDate: Date # existing slotLength: String # new releaseDate: Date # new genre: GenreInput # new tags: [TagInput!] # new}Resolver Changes
Section titled “Resolver Changes”Field resolvers (resolvers/Queries/fields.ts):
- Remove
sharedDataresolver - Remove
companyDataresolver - Add/update Episode field resolvers for
genre,effectiveGenre,projectGenre,publications,tags,slotLength,releaseDate
Episode resolvers:
- Update episode creation service to handle new fields
- Update episode update service to handle new fields and trigger genre inheritance logic (if genre changes on project, bulk-update episodes)
Service Layer Changes
Section titled “Service Layer Changes”Delete:
serviceLogic/ProjectSharedDataUpdater.tsserviceLogic/ProjectFunderPrivateDataUpdater.ts
Update:
serviceLogic/ProjectFormUpdater.ts— handle new formData fields (medium, broadcasterProjectType, greenlightDate, genre, commissioningDepartment, tags)- Episode creation/update services — handle new episode fields, genre inheritance, effectiveGenre population
Database Schema Changes
Section titled “Database Schema Changes”ScreenProject MongoDB validation schema (validationSchema.ts):
- Remove
companyDataandsharedDatafrom schema - Add new fields to
formData:medium,broadcasterProjectType,greenlightDate,genre(object),commissioningDepartment,tags(array)
Episode MongoDB validation schema:
- Add fields:
genre(object),effectiveGenre(object),projectGenre(object),slotLength,releaseDate,tags(array),publications(array),externalEpisodeIds(array),makerIds(array),fieldMetadata(object)
Migration
Section titled “Migration”No data migration is needed — companyData and sharedData are not currently in use. The changes are:
- Remove
companyDataandsharedDatafrom the screenProject MongoDB validation schema - Add new fields to the screenProject and episode MongoDB validation schemas
- Create new indexes on the episodes and screenProject collections
- Remove code references to
companyData/sharedData(data stores, resolvers, service logic, validation)
Testing Requirements
Section titled “Testing Requirements”- Unit tests for genre three-field logic (all scenarios from doc 06)
- Unit tests for publication upsert by publicationId
- Integration tests for the full XML → episode storage pipeline
- Test that
effectiveGenreis always correct after various sequences of project/episode genre updates - Test that
companyData/sharedDataremoval doesn’t break existing queries (verify no other code references them) - Test that new formData fields can be read and written via GraphQL
- Test that episode genre/tags/publications are correctly exposed via GraphQL
- Test that publication events are read-only (no mutations available)
- E2E tests via test harness for pre-TX and post-TX ingestion writing to episodes