Skip to content

Commit 5cb8566

Browse files
committed
fix(training): make the canonical/rendered video lists exhaustive both ways
`satisfies readonly CanonicalTrainingVideo[]` only proved that every rendered video is canonical. The reverse — a canonical ID with no rendered video — still compiled, and that is the dangerous direction: completion requires a row for every canonical ID, so adding one without a matching video would leave employees permanently unable to finish training. Key the data by the canonical union instead. `Record<GeneralTrainingVideoId, ...>` is exhaustive by construction, so both drifts now fail the build: a missing key (TS2741) and an unknown key (TS2353). Verified by introducing each drift and compiling. The `Exclude<GeneralTrainingVideoId, (typeof trainingVideos)[number]['id']>` form would not have worked here — the `readonly TrainingVideo[]` annotation widens `id` to `string`, so the Exclude resolves to `never` and the guard passes vacuously.
1 parent 335a918 commit 5cb8566

2 files changed

Lines changed: 46 additions & 38 deletions

File tree

‎apps/app/src/lib/data/training-videos.ts‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,52 @@ export interface TrainingVideo {
1818
}
1919

2020
/**
21-
* A rendered video whose ID is one the training-completion paths actually
22-
* accept. `@trycompai/company` owns the canonical ID list, shared with the
23-
* NestJS training service and the portal's complete-training route.
21+
* Videos keyed by the canonical IDs that `@trycompai/company` owns, shared
22+
* with the NestJS training service and the portal's complete-training route.
23+
*
24+
* Keying by the canonical union makes the lists exhaustive in BOTH directions:
25+
* a canonical ID with no entry here fails to compile, and an entry that isn't
26+
* canonical fails too. Both matter — completion requires a row for every
27+
* canonical ID, so a video we never render would leave employees permanently
28+
* unable to finish training, while a rendered video the API rejects would 400.
29+
*
30+
* The import is type-only on purpose: client components and a Trigger.dev task
31+
* import this file, and `@trycompai/company` requires `@trycompai/db` at
32+
* runtime.
2433
*/
25-
type CanonicalTrainingVideo = Omit<TrainingVideo, 'id'> & {
26-
id: GeneralTrainingVideoId;
27-
};
28-
29-
export const trainingVideos: readonly TrainingVideo[] = [
30-
{
31-
id: 'sat-1',
34+
const trainingVideosById: Record<GeneralTrainingVideoId, Omit<TrainingVideo, 'id'>> = {
35+
'sat-1': {
3236
title: 'Security Awareness Training - Part 1',
3337
description: 'Security Awareness Training - Part 1',
3438
youtubeId: 'N-sBS3uCWB4',
3539
url: 'https://www.youtube.com/watch?v=N-sBS3uCWB4',
3640
},
37-
{
38-
id: 'sat-2',
41+
'sat-2': {
3942
title: 'Security Awareness Training - Part 2',
4043
description: 'Security Awareness Training - Part 2',
4144
youtubeId: 'JwQNwhDyXig',
4245
url: 'https://www.youtube.com/watch?v=JwQNwhDyXig',
4346
},
44-
{
45-
id: 'sat-3',
47+
'sat-3': {
4648
title: 'Security Awareness Training - Part 3',
4749
description: 'Security Awareness Training - Part 3',
4850
youtubeId: 'fzMNw_-KEGE',
4951
url: 'https://www.youtube.com/watch?v=fzMNw_-KEGE',
5052
},
51-
{
52-
id: 'sat-4',
53+
'sat-4': {
5354
title: 'Security Awareness Training - Part 4',
5455
description: 'Security Awareness Training - Part 4',
5556
youtubeId: 'WbpqjH9kI2Y',
5657
url: 'https://www.youtube.com/watch?v=WbpqjH9kI2Y',
5758
},
58-
{
59-
id: 'sat-5',
59+
'sat-5': {
6060
title: 'Security Awareness Training - Part 5',
6161
description: 'Security Awareness Training - Part 5',
6262
youtubeId: 'Clvfkm6azDs',
6363
url: 'https://www.youtube.com/watch?v=Clvfkm6azDs',
6464
},
65-
] as const satisfies readonly CanonicalTrainingVideo[];
65+
};
66+
67+
export const trainingVideos: readonly TrainingVideo[] = Object.entries(trainingVideosById).map(
68+
([id, video]) => ({ id, ...video }),
69+
);

‎apps/portal/src/lib/data/training-videos.ts‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,52 @@ export interface TrainingVideo {
1818
}
1919

2020
/**
21-
* A rendered video whose ID is one the training-completion paths actually
22-
* accept. `@trycompai/company` owns the canonical ID list, shared with the
23-
* NestJS training service and the portal's complete-training route.
21+
* Videos keyed by the canonical IDs that `@trycompai/company` owns, shared
22+
* with the NestJS training service and the portal's complete-training route.
23+
*
24+
* Keying by the canonical union makes the lists exhaustive in BOTH directions:
25+
* a canonical ID with no entry here fails to compile, and an entry that isn't
26+
* canonical fails too. Both matter — completion requires a row for every
27+
* canonical ID, so a video we never render would leave employees permanently
28+
* unable to finish training, while a rendered video the API rejects would 400.
29+
*
30+
* The import is type-only on purpose: client components and a Trigger.dev task
31+
* import this file, and `@trycompai/company` requires `@trycompai/db` at
32+
* runtime.
2433
*/
25-
type CanonicalTrainingVideo = Omit<TrainingVideo, 'id'> & {
26-
id: GeneralTrainingVideoId;
27-
};
28-
29-
export const trainingVideos: readonly TrainingVideo[] = [
30-
{
31-
id: 'sat-1',
34+
const trainingVideosById: Record<GeneralTrainingVideoId, Omit<TrainingVideo, 'id'>> = {
35+
'sat-1': {
3236
title: 'Security Awareness Training - Part 1',
3337
description: 'Security Awareness Training - Part 1',
3438
youtubeId: 'N-sBS3uCWB4',
3539
url: 'https://www.youtube.com/watch?v=N-sBS3uCWB4',
3640
},
37-
{
38-
id: 'sat-2',
41+
'sat-2': {
3942
title: 'Security Awareness Training - Part 2',
4043
description: 'Security Awareness Training - Part 2',
4144
youtubeId: 'JwQNwhDyXig',
4245
url: 'https://www.youtube.com/watch?v=JwQNwhDyXig',
4346
},
44-
{
45-
id: 'sat-3',
47+
'sat-3': {
4648
title: 'Security Awareness Training - Part 3',
4749
description: 'Security Awareness Training - Part 3',
4850
youtubeId: 'fzMNw_-KEGE',
4951
url: 'https://www.youtube.com/watch?v=fzMNw_-KEGE',
5052
},
51-
{
52-
id: 'sat-4',
53+
'sat-4': {
5354
title: 'Security Awareness Training - Part 4',
5455
description: 'Security Awareness Training - Part 4',
5556
youtubeId: 'WbpqjH9kI2Y',
5657
url: 'https://www.youtube.com/watch?v=WbpqjH9kI2Y',
5758
},
58-
{
59-
id: 'sat-5',
59+
'sat-5': {
6060
title: 'Security Awareness Training - Part 5',
6161
description: 'Security Awareness Training - Part 5',
6262
youtubeId: 'Clvfkm6azDs',
6363
url: 'https://www.youtube.com/watch?v=Clvfkm6azDs',
6464
},
65-
] as const satisfies readonly CanonicalTrainingVideo[];
65+
};
66+
67+
export const trainingVideos: readonly TrainingVideo[] = Object.entries(trainingVideosById).map(
68+
([id, video]) => ({ id, ...video }),
69+
);

0 commit comments

Comments
 (0)