fix(studio): validate auth conn percentage input between 1 and 100 (#45061)

## Summary

- Closes
[FE-3039](https://linear.app/supabase/issue/FE-3039/validate-conn-percentage-input-between-1-and-100).
- Adds a `superRefine`d `DatabaseFormSchema` in
`PerformanceSettingsForm.tsx` that blocks submission when
`DB_MAX_POOL_SIZE_UNIT === 'percent'` and the value falls outside 1-100.
Connections (fixed pool size) path keeps the existing `min(1)` behavior
per the ticket.
- Updates the number input's `min`/`max` attributes to match: percent is
now `1`-`100` (was `3`-`80`); connections unchanged
(`3`-`Math.floor(maxConnectionLimit * 0.8)`).

## Test plan

- [x] Unit tests added in `PerformanceSettingsForm.test.ts` covering
bounds (1, 100), out-of-range (0, 101, -5, 150), mid-range, string
coercion, connections path (no upper zod bound), and enum rejection. All
12 pass.
- [x] Manually verify in Auth → Performance settings that entering 0 or
101 in percent mode shows a validation error and blocks save, and that
switching to absolute connections still uses the existing bounds.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Refined database pool-size validation and input constraints;
percentage unit enforces values between 1–100 and rejects zero,
negative, and out-of-range entries. String inputs that represent numbers
are coerced where appropriate; unknown unit values are rejected.
* **Tests**
* Added comprehensive tests for boundary, invalid, string, and
unknown-unit cases and for validation error messages.
* **Refactor**
* Validation moved to a centralized schema and HTML min/max input
attributes were removed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ali Waseem authored and GitHub committed 2026-04-21 07:59:35 -06:00
1 parent 2e3ddc3a52
commit e8e0842be3
2 files changed
+129 -12

No files matched your search

@@ -0,0 +1,111 @@
import { describe, expect, it } from 'vitest'
import { DatabaseFormSchema } from './PerformanceSettingsForm'
describe('DatabaseFormSchema', () => {
describe('percent unit', () => {
it('accepts 1 (lower bound)', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 1,
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(true)
})
it('accepts 100 (upper bound)', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 100,
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(true)
})
it('accepts a mid-range value', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 50,
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(true)
})
it('rejects 0', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 0,
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(false)
})
it('rejects 101', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 101,
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(false)
if (!result.success) {
const issue = result.error.issues.find((i) => i.path[0] === 'DB_MAX_POOL_SIZE')
expect(issue?.message).toBe('Percentage must be between 1 and 100')
}
})
it('rejects negative values', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: -5,
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(false)
})
it('coerces string input', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: '75',
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(true)
})
it('rejects out-of-range string input', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: '150',
DB_MAX_POOL_SIZE_UNIT: 'percent',
})
expect(result.success).toBe(false)
})
})
describe('connections unit', () => {
it('accepts 1 (min)', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 1,
DB_MAX_POOL_SIZE_UNIT: 'connections',
})
expect(result.success).toBe(true)
})
it('accepts values above 100 (no upper schema bound for absolute)', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 500,
DB_MAX_POOL_SIZE_UNIT: 'connections',
})
expect(result.success).toBe(true)
})
it('rejects 0', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 0,
DB_MAX_POOL_SIZE_UNIT: 'connections',
})
expect(result.success).toBe(false)
})
})
describe('unit enum', () => {
it('rejects unknown unit values', () => {
const result = DatabaseFormSchema.safeParse({
DB_MAX_POOL_SIZE: 10,
DB_MAX_POOL_SIZE_UNIT: 'bytes',
})
expect(result.success).toBe(false)
})
})
})
@@ -46,6 +46,23 @@ const FormSchema = z.object({
DB_MAX_POOL_SIZE_UNIT: z.enum(['percent', 'connections']),
})
export const DatabaseFormSchema = z
.object({
DB_MAX_POOL_SIZE: z.coerce.number().min(1),
DB_MAX_POOL_SIZE_UNIT: z.enum(['percent', 'connections']),
})
.superRefine((data, ctx) => {
if (data.DB_MAX_POOL_SIZE_UNIT === 'percent') {
if (data.DB_MAX_POOL_SIZE < 1 || data.DB_MAX_POOL_SIZE > 100) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['DB_MAX_POOL_SIZE'],
message: 'Percentage must be between 1 and 100',
})
}
}
})
export const PerformanceSettingsForm = () => {
const { data: project } = useSelectedProjectQuery()
const { hasAccess: hasAccessToPerformance, isLoading: isLoadingEntitlement } =
@@ -87,12 +104,7 @@ export const PerformanceSettingsForm = () => {
})
const databaseForm = useForm({
resolver: zodResolver(
z.object({
DB_MAX_POOL_SIZE: FormSchema.shape.DB_MAX_POOL_SIZE,
DB_MAX_POOL_SIZE_UNIT: FormSchema.shape.DB_MAX_POOL_SIZE_UNIT,
})
),
resolver: zodResolver(DatabaseFormSchema),
defaultValues: {
DB_MAX_POOL_SIZE: 10,
DB_MAX_POOL_SIZE_UNIT: 'connections',
@@ -375,12 +387,6 @@ export const PerformanceSettingsForm = () => {
<FormInputGroupInput
type="number"
{...field}
min={3}
max={
chosenUnit === 'percent'
? 80
: Math.floor(maxConnectionLimit * 0.8)
}
disabled={!canUpdateConfig || promptUpgrade}
/>
<InputGroupAddon align="inline-end">