Skip to content

Commit 9d4a431

Browse files
CCM-8579: Alternate gen of union lists
1 parent caa3608 commit 9d4a431

File tree

13 files changed

+190
-139
lines changed

13 files changed

+190
-139
lines changed

frontend/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
"dependencies": {
2121
"@aws-amplify/adapter-nextjs": "^1.4.3",
2222
"@aws-amplify/ui-react": "^6.9.1",
23-
"@aws-sdk/client-ses": "^3.637.0",
24-
"aws-amplify": "^6.12.3",
2523
"clsx": "^2.1.1",
24+
"aws-amplify": "^6.12.3",
2625
"date-fns": "^4.1.0",
2726
"jsonwebtoken": "^9.0.2",
2827
"jwt-decode": "^4.0.0",
2928
"markdown-it": "^13.0.1",
3029
"mimetext": "^3.0.24",
31-
"next": "^15.1.7",
30+
"next": "^15.2.1",
3231
"next-client-cookies": "^2.0.1",
3332
"nhs-notify-backend-client": "*",
3433
"nhs-notify-web-template-management-utils": "*",

frontend/src/components/forms/ChooseTemplate/server-action.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import {
66
} from 'nhs-notify-web-template-management-utils';
77
import { z } from 'zod';
88

9-
const [firstTemplateType, ...remainingTemplateTypes] = TEMPLATE_TYPE_LIST;
10-
119
const $ChooseTemplate = z.object({
12-
templateType: z.enum([firstTemplateType, ...remainingTemplateTypes], {
10+
templateType: z.enum(TEMPLATE_TYPE_LIST, {
1311
message: 'Select a template type',
1412
}),
1513
});

frontend/src/components/forms/CopyTemplate/server-action.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import {
1111
ValidatedTemplateDto,
1212
} from 'nhs-notify-backend-client';
1313

14-
const [firstType, ...remainingTypes] = TEMPLATE_TYPE_LIST;
15-
1614
const $CopyTemplate = z.object({
17-
templateType: z.enum([firstType, ...remainingTypes], {
15+
templateType: z.enum(TEMPLATE_TYPE_LIST, {
1816
message: 'Select a template type',
1917
}),
2018
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
arrayOfAll,
3+
LANGUAGE_LIST,
4+
LETTER_TYPE_LIST,
5+
TEMPLATE_STATUS_LIST,
6+
TEMPLATE_TYPE_LIST,
7+
VIRUS_SCAN_STATUS_LIST,
8+
} from '../../schemas/union-lists';
9+
10+
describe('arrayOfAll', () => {
11+
type Union = 'a' | 'b' | 'c';
12+
13+
test('compiles when all cases of the union are in the array, order is ignored', () => {
14+
expect(arrayOfAll<Union>()(['c', 'b', 'a'])).toEqual(['c', 'b', 'a']);
15+
});
16+
17+
test('does not compile if a case is missing', () => {
18+
const a: Union[] = ['a', 'b', 'c'];
19+
20+
// @ts-expect-error `Type 'Union[]' is not assignable to type '"Invalid"'.`
21+
expect(arrayOfAll<Union | 'd'>()(a)).toEqual(a);
22+
});
23+
24+
test('does not compile if extra cases are present in the array', () => {
25+
const a: Array<Union | 'd'> = ['a', 'b', 'c', 'd'];
26+
27+
// @ts-expect-error `Type '"d"' is not assignable to type 'Union'.`
28+
expect(arrayOfAll<Union>()(a)).toEqual(a);
29+
});
30+
});
31+
32+
describe('Union lists', () => {
33+
test.each([
34+
{ name: 'TemplateType', list: TEMPLATE_TYPE_LIST },
35+
{ name: 'TemplateStatus', list: TEMPLATE_STATUS_LIST },
36+
{ name: 'Language', list: LANGUAGE_LIST },
37+
{ name: 'LetterType', list: LETTER_TYPE_LIST },
38+
{ name: 'VirusScanStatus', list: VIRUS_SCAN_STATUS_LIST },
39+
])('$name list contains no duplicates', ({ list }) => {
40+
expect(list.length).toEqual(new Set(list).size);
41+
});
42+
});

lambdas/backend-client/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ export * from './types/generated';
33
export * from './types/result';
44
export * from './types/template-client';
55
export * from './schemas/template-schema';
6-
export * from './types/union-lists';
6+
export * from './schemas/union-lists';

lambdas/backend-client/src/schemas/template-schema.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
TEMPLATE_STATUS_LIST,
2525
TEMPLATE_TYPE_LIST,
2626
VIRUS_SCAN_STATUS_LIST,
27-
} from '../types/union-lists';
27+
} from './union-lists';
2828

2929
export type EmailPropertiesWithType = EmailProperties & {
3030
templateType: 'EMAIL';
@@ -59,21 +59,11 @@ export type ValidatedTemplateDto = TemplateDto &
5959
| LetterPropertiesWithType
6060
);
6161

62-
const [firstTemplateType, ...remainingTemplateTypes] = TEMPLATE_TYPE_LIST;
63-
const [firstTemplateStatus, ...remainingTemplateStatuses] =
64-
TEMPLATE_STATUS_LIST;
65-
const [firstLanguage, ...remainingLanguages] = LANGUAGE_LIST;
66-
const [firstLetterType, ...remainingLetterTypes] = LETTER_TYPE_LIST;
67-
const [firstVirusScanStatus, ...remainingVirusScanStatus] =
68-
VIRUS_SCAN_STATUS_LIST;
69-
7062
const $FileDetails = schemaFor<FileDetails>()(
7163
z.object({
7264
fileName: z.string().trim().min(1),
7365
currentVersion: z.string().optional(),
74-
virusScanStatus: z
75-
.enum([firstVirusScanStatus, ...remainingVirusScanStatus])
76-
.optional(),
66+
virusScanStatus: z.enum(VIRUS_SCAN_STATUS_LIST).optional(),
7767
})
7868
);
7969

@@ -114,16 +104,16 @@ const $SmsProperties = schemaFor<SmsProperties>()(
114104

115105
const $LetterProperties = schemaFor<LetterProperties>()(
116106
z.object({
117-
letterType: z.enum([firstLetterType, ...remainingLetterTypes]),
118-
language: z.enum([firstLanguage, ...remainingLanguages]),
107+
letterType: z.enum(LETTER_TYPE_LIST),
108+
language: z.enum(LANGUAGE_LIST),
119109
files: $LetterFiles,
120110
})
121111
);
122112

123113
export const $BaseTemplateSchema = schemaFor<BaseTemplate>()(
124114
z.object({
125115
name: z.string().trim().min(1),
126-
templateType: z.enum([firstTemplateType, ...remainingTemplateTypes]),
116+
templateType: z.enum(TEMPLATE_TYPE_LIST),
127117
})
128118
);
129119

@@ -145,16 +135,16 @@ export const $CreateTemplateSchema = schemaFor<
145135
ValidatedCreateTemplate
146136
>()(
147137
z.discriminatedUnion('templateType', [
148-
$BaseTemplateSchema.merge($EmailPropertiesWithType),
149138
$BaseTemplateSchema.merge($NhsAppPropertiesWithType),
139+
$BaseTemplateSchema.merge($EmailPropertiesWithType),
150140
$BaseTemplateSchema.merge($SmsPropertiesWithType),
151141
$BaseTemplateSchema.merge($LetterPropertiesWithType),
152142
])
153143
);
154144

155145
const $UpdateTemplateFields = z
156146
.object({
157-
templateStatus: z.enum([firstTemplateStatus, ...remainingTemplateStatuses]),
147+
templateStatus: z.enum(TEMPLATE_STATUS_LIST),
158148
})
159149
.merge($BaseTemplateSchema);
160150

@@ -163,8 +153,8 @@ export const $UpdateTemplateSchema = schemaFor<
163153
ValidatedUpdateTemplate
164154
>()(
165155
z.discriminatedUnion('templateType', [
166-
$UpdateTemplateFields.merge($EmailPropertiesWithType),
167156
$UpdateTemplateFields.merge($NhsAppPropertiesWithType),
157+
$UpdateTemplateFields.merge($EmailPropertiesWithType),
168158
$UpdateTemplateFields.merge($SmsPropertiesWithType),
169159
$UpdateTemplateFields.merge($LetterPropertiesWithType),
170160
])
@@ -183,8 +173,8 @@ export const $TemplateDtoSchema = schemaFor<
183173
ValidatedTemplateDto
184174
>()(
185175
z.discriminatedUnion('templateType', [
186-
$TemplateDtoFields.merge($EmailPropertiesWithType),
187176
$TemplateDtoFields.merge($NhsAppPropertiesWithType),
177+
$TemplateDtoFields.merge($EmailPropertiesWithType),
188178
$TemplateDtoFields.merge($SmsPropertiesWithType),
189179
$TemplateDtoFields.merge($LetterPropertiesWithType),
190180
])
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Lists of generated string literal unions, for iteration and zod validation via z.enum.
3+
* Ideally the generator would create the unions off an as const array and
4+
* this wouldn't be needed, but that's not currently available.
5+
* Note that there is a unit test against these to guard against duplicates
6+
*/
7+
import {
8+
Language,
9+
LetterType,
10+
TemplateStatus,
11+
TemplateType,
12+
VirusScanStatus,
13+
} from '../types/generated';
14+
15+
/**
16+
* Returns an identity function which will fail to compile if 'array'
17+
* doesn't contain all the cases of 'Union'
18+
*
19+
* @example
20+
* const arrayOfFooBarBaz = arrayOfAll<'foo' | 'bar' | 'baz'>();
21+
*
22+
* const a = arrayOfFooBarBaz(['foo', 'bar']); // does not compile
23+
* const b = arrayOfFooBarBaz(['foo', 'bar', 'baz']); // compiles
24+
*/
25+
export function arrayOfAll<Union>() {
26+
return <T extends [Union, ...Union[]]>(
27+
array: T & ([Union] extends [T[number]] ? unknown : 'Invalid')
28+
) => array;
29+
}
30+
31+
export const TEMPLATE_TYPE_LIST = arrayOfAll<TemplateType>()([
32+
'NHS_APP',
33+
'EMAIL',
34+
'SMS',
35+
'LETTER',
36+
]);
37+
38+
export const TEMPLATE_STATUS_LIST = arrayOfAll<TemplateStatus>()([
39+
'NOT_YET_SUBMITTED',
40+
'SUBMITTED',
41+
'DELETED',
42+
]);
43+
44+
export const LANGUAGE_LIST = arrayOfAll<Language>()([
45+
'ar',
46+
'bg',
47+
'bn',
48+
'de',
49+
'el',
50+
'en',
51+
'es',
52+
'fa',
53+
'fr',
54+
'gu',
55+
'hi',
56+
'hu',
57+
'it',
58+
'ku',
59+
'lt',
60+
'lv',
61+
'ne',
62+
'pa',
63+
'pl',
64+
'pt',
65+
'ro',
66+
'ru',
67+
'sk',
68+
'so',
69+
'sq',
70+
'ta',
71+
'tr',
72+
'ur',
73+
'zh',
74+
]);
75+
76+
export const LETTER_TYPE_LIST = arrayOfAll<LetterType>()([
77+
'x3',
78+
'q1',
79+
'q4',
80+
'x0',
81+
'x1',
82+
]);
83+
84+
export const VIRUS_SCAN_STATUS_LIST = arrayOfAll<VirusScanStatus>()([
85+
'PENDING',
86+
'FAILED',
87+
'PASSED',
88+
]);

lambdas/backend-client/src/types/union-lists.ts

Lines changed: 0 additions & 66 deletions
This file was deleted.

tests/test-team/template-mgmt-api-tests/create-template.api.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test.describe('POST /v1/template', () => {
4848
technicalMessage: 'Request failed validation',
4949
details: {
5050
templateType:
51-
"Invalid discriminator value. Expected 'SMS' | 'NHS_APP' | 'EMAIL' | 'LETTER'",
51+
"Invalid discriminator value. Expected 'NHS_APP' | 'EMAIL' | 'SMS' | 'LETTER'",
5252
},
5353
});
5454
});
@@ -75,7 +75,7 @@ test.describe('POST /v1/template', () => {
7575
technicalMessage: 'Request failed validation',
7676
details: {
7777
templateType:
78-
"Invalid discriminator value. Expected 'SMS' | 'NHS_APP' | 'EMAIL' | 'LETTER'",
78+
"Invalid discriminator value. Expected 'NHS_APP' | 'EMAIL' | 'SMS' | 'LETTER'",
7979
},
8080
});
8181
});

tests/test-team/template-mgmt-api-tests/update-template.api.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ test.describe('POST /v1/template/:templateId', () => {
128128
technicalMessage: 'Request failed validation',
129129
details: {
130130
templateType:
131-
"Invalid discriminator value. Expected 'SMS' | 'NHS_APP' | 'EMAIL' | 'LETTER'",
131+
"Invalid discriminator value. Expected 'NHS_APP' | 'EMAIL' | 'SMS' | 'LETTER'",
132132
},
133133
});
134134
});
@@ -171,7 +171,7 @@ test.describe('POST /v1/template/:templateId', () => {
171171
technicalMessage: 'Request failed validation',
172172
details: {
173173
templateType:
174-
"Invalid discriminator value. Expected 'SMS' | 'NHS_APP' | 'EMAIL' | 'LETTER'",
174+
"Invalid discriminator value. Expected 'NHS_APP' | 'EMAIL' | 'SMS' | 'LETTER'",
175175
},
176176
});
177177
});

0 commit comments

Comments
 (0)