Skip to content

Commit cb9b3d6

Browse files
committed
fix: Properly validate optionality with the new valid domain restrictions
1 parent 36b6a8d commit cb9b3d6

File tree

7 files changed

+40
-31
lines changed

7 files changed

+40
-31
lines changed

src/features/clusters/upsert/ClusterDetails.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Suspense, useEffect, useMemo } from 'react';
1414
import { UseFormReturn, useFormState } from 'react-hook-form';
1515
import { ClusterRegions } from './ClusterRegions';
1616
import { ClusterInstances } from './components/ClusterInstances';
17-
import { UpsertClusterSchema, UpsertClusterSchemaType } from './upsertClusterSchema';
17+
import { specifiedAbbreviatedName, UpsertClusterSchema, UpsertClusterSchemaType } from './upsertClusterSchema';
1818

1919
interface ClusterDetailsProps {
2020
calculatedNames: { suggestedAbbreviatedName: string; fullHostName: string };
@@ -245,7 +245,7 @@ export function ClusterDetails({
245245
<Input
246246
{...field}
247247
type="text"
248-
maxLength={UpsertClusterSchema.shape.abbreviatedName.unwrap().maxLength!}
248+
maxLength={specifiedAbbreviatedName.maxLength!}
249249
autoCapitalize="none"
250250
autoComplete="off"
251251
autoCorrect="off"

src/features/clusters/upsert/ClusterForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { ClusterDetails } from './ClusterDetails';
2727
import { calculateInstanceFQDN } from './lib/calculateInstanceFQDN';
2828
import { pickDefaultDeploymentPerformanceAndRegionPlans } from './lib/pickDefaultDeploymentPerformanceAndRegionPlans';
2929
import { PriceDisplay } from './PriceDisplay';
30-
import { UpsertClusterSchema, UpsertClusterSchemaType } from './upsertClusterSchema';
30+
import { specifiedAbbreviatedName, UpsertClusterSchema, UpsertClusterSchemaType } from './upsertClusterSchema';
3131

3232
interface ClusterFormProps {
3333
alreadyUsingFree: boolean;
@@ -225,7 +225,7 @@ export function ClusterForm({
225225
const calculatedNames = useMemo(() => {
226226
const suggestedAbbreviatedName = collapseKebabsToMaxLength(
227227
toKebabCase(clusterName),
228-
UpsertClusterSchema.shape.abbreviatedName.unwrap().maxLength!,
228+
specifiedAbbreviatedName.maxLength!,
229229
);
230230
return {
231231
suggestedAbbreviatedName,

src/features/clusters/upsert/upsertClusterSchema.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ describe('UpsertClusterSchema', () => {
9292
expect(result.success).toBe(true);
9393
});
9494

95-
it('invalidates empty string (must match regex if provided)', () => {
95+
it('allows it to be optional (empty string)', () => {
9696
const result = validateAbbreviated('');
97-
expect(result.success).toBe(false);
97+
expect(result.success).toBe(true);
9898
});
9999
});
100100

src/features/clusters/upsert/upsertClusterSchema.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ import { hostNameRegex } from '@/lib/string/regex/hostNameRegex';
22
import { maxPortNumber, minPortNumber } from '@/lib/types/portNumbers';
33
import { z } from 'zod';
44

5+
export const specifiedAbbreviatedName = z
6+
.string()
7+
.max(20, 'Must be at most 20 characters long.')
8+
.regex(
9+
/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/,
10+
'Can only contain lowercase letters, numbers and dashes. Must not start or end with a dash.',
11+
);
12+
513
export const UpsertClusterSchema = z.object({
614
clusterName: z.string()
715
.nonempty('Please enter a cluster name.')
816
.max(255, 'Cluster name cannot be longer than 255 characters long.'),
9-
abbreviatedName: z
10-
.string()
11-
.max(20, 'Must be at most 20 characters long.')
12-
.regex(
13-
/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/,
14-
'Can only contain lowercase letters, numbers and dashes. Must not start or end with a dash.',
15-
)
16-
.optional(),
17+
abbreviatedName: z.union([
18+
z.literal(''),
19+
z.undefined(),
20+
specifiedAbbreviatedName,
21+
]),
1722
fqdn: z
1823
.string()
1924
.regex(hostNameRegex, 'Please enter a valid host name without the port or any path.')

src/features/organizations/components/NewOrgForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Input } from '@/components/ui/input';
1010
import { currentUserQueryKey } from '@/features/auth/queries/getCurrentUser';
1111
import { authStore, OverallAppSignIn } from '@/features/auth/store/authStore';
1212
import { useCreateNewOrganizationMutation } from '@/features/organizations/hooks/useCreateNewOrganization';
13-
import { NewOrganizationSchema } from '@/features/organizations/mutations/newOrganizationSchema';
13+
import { NewOrganizationSchema, specifiedSubdomain } from '@/features/organizations/mutations/newOrganizationSchema';
1414
import { useCloudAuth } from '@/hooks/useAuth';
1515
import { collapseKebabsToMaxLength } from '@/lib/string/collapseKebabsToMaxLength';
1616
import { toKebabCase } from '@/lib/string/to-kebab-case';
@@ -45,7 +45,7 @@ export function NewOrgForm() {
4545
const calculatedNames = useMemo(() => {
4646
const suggestedSubdomain = collapseKebabsToMaxLength(
4747
toKebabCase(name),
48-
NewOrganizationSchema.shape.subdomain.maxLength!,
48+
specifiedSubdomain.maxLength!,
4949
);
5050
return {
5151
suggestedSubdomain,
@@ -102,7 +102,7 @@ export function NewOrgForm() {
102102
<FormControl>
103103
<Input
104104
type="text"
105-
maxLength={NewOrganizationSchema.shape.subdomain.maxLength!}
105+
maxLength={specifiedSubdomain.maxLength!}
106106
autoCapitalize="none"
107107
placeholder={calculatedNames.suggestedSubdomain}
108108
{...field}

src/features/organizations/mutations/newOrganizationSchema.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,18 @@ describe('NewOrganizationSchema', () => {
7575
const result = validateSubdomain('a'.repeat(63));
7676
expect(result.success).toBe(false);
7777
if (!result.success) {
78-
expect(result.error.issues[0].message).toBe('The subdomain cannot be longer than 62 characters.');
78+
expect(result.error.issues[0].message).toBe('Must be at most 62 characters long.');
7979
}
8080
});
8181

82-
it('is required', () => {
82+
it('can be left blank (undefined)', () => {
8383
const result = validateSubdomain(undefined);
84-
expect(result.success).toBe(false);
84+
expect(result.success).toBe(true);
8585
});
8686

87-
it('invalidates empty string (must match regex)', () => {
87+
it('can be left blank (empty string)', () => {
8888
const result = validateSubdomain('');
89-
expect(result.success).toBe(false);
89+
expect(result.success).toBe(true);
9090
});
9191
});
9292
});
Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { z } from 'zod';
22

3+
export const specifiedSubdomain = z
4+
.string()
5+
.max(62, { error: 'Must be at most 62 characters long.' })
6+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
7+
error:
8+
'Please only use lowercase letters, digits and dashes (-) in the subdomain. Must not start or end with a dash.',
9+
});
10+
311
export const NewOrganizationSchema = z.object({
412
name: z
513
.string()
614
.max(255, {
715
error: 'Name cannot be longer than 255 characters.',
816
}),
9-
subdomain: z
10-
.string()
11-
.max(62, {
12-
error: 'The subdomain cannot be longer than 62 characters.',
13-
})
14-
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
15-
error:
16-
'Please only use lowercase letters, digits and dashes (-) in the subdomain. Must not start or end with a dash.',
17-
}),
17+
subdomain: z.union([
18+
z.literal(''),
19+
z.undefined(),
20+
specifiedSubdomain,
21+
]),
1822
});

0 commit comments

Comments
 (0)