mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? - Minor issues here, the validation for creating names is there but users can create crons with empty names through SQL - When they edit the name in the Cron editor, since we use names as the where clause it treats it as a new create - So a duplicate cron is created - Since creating requires a name, the validation is moved to the component rather than zod and disabled when editing mode is on! <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Cron jobs can now be created without requiring a name field. * Improved handling to properly distinguish between creating new cron jobs and editing existing ones. * **Bug Fixes** * Fixed issue where editing unnamed cron jobs would create duplicate entries instead of updating the existing job in place. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46486?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
150 lines
4.6 KiB
TypeScript
150 lines
4.6 KiB
TypeScript
import { toString as CronToString } from 'cronstrue'
|
|
import { getKeyValueFieldArrayValidationIssues } from 'ui-patterns/form/KeyValueFieldArray/validation'
|
|
import z from 'zod'
|
|
|
|
import { cronPattern, secondsPattern } from '../CronJobs.constants'
|
|
import { httpEndpointUrlSchema } from '@/lib/validation/http-url'
|
|
|
|
const convertCronToString = (schedule: string) => {
|
|
// pg_cron can also use "30 seconds" format for schedule. Cronstrue doesn't understand that format so just use the
|
|
// original schedule when cronstrue throws.
|
|
// pg_cron uses '$' for "last day of month"; cronstrue uses 'L' — normalize before parsing.
|
|
try {
|
|
return CronToString(schedule.replace(/\$/g, 'L'))
|
|
} catch (error) {
|
|
return schedule
|
|
}
|
|
}
|
|
|
|
const httpHeadersSchema = z.array(z.object({ name: z.string().trim(), value: z.string().trim() }))
|
|
|
|
const addHttpHeaderIssues = (
|
|
rows: z.infer<typeof httpHeadersSchema>,
|
|
ctx: z.RefinementCtx,
|
|
pathPrefix: string[]
|
|
) => {
|
|
getKeyValueFieldArrayValidationIssues({
|
|
rows,
|
|
keyFieldName: 'name',
|
|
valueFieldName: 'value',
|
|
keyRequiredMessage: 'Header name is required',
|
|
valueRequiredMessage: 'Header value is required',
|
|
}).forEach((issue) => {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: issue.message,
|
|
path: [...pathPrefix, ...issue.path],
|
|
})
|
|
})
|
|
}
|
|
|
|
const edgeFunctionSchema = z.object({
|
|
type: z.literal('edge_function'),
|
|
method: z.enum(['GET', 'POST']),
|
|
edgeFunctionName: z.string().trim().min(1, 'Please select one of the listed Edge Functions'),
|
|
timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000),
|
|
httpHeaders: httpHeadersSchema,
|
|
httpBody: z
|
|
.string()
|
|
.trim()
|
|
.optional()
|
|
.refine((value) => {
|
|
if (!value) return true
|
|
try {
|
|
JSON.parse(value)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}, 'Input must be valid JSON'),
|
|
// When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it
|
|
snippet: z.string().trim(),
|
|
})
|
|
|
|
const httpRequestSchema = z.object({
|
|
type: z.literal('http_request'),
|
|
method: z.enum(['GET', 'POST']),
|
|
endpoint: httpEndpointUrlSchema({
|
|
requiredMessage: 'Please provide a URL',
|
|
invalidMessage: 'Please provide a valid URL',
|
|
prefixMessage: 'Please prefix your URL with http:// or https://',
|
|
}),
|
|
timeoutMs: z.coerce.number().int().gte(1000).lte(5000).default(1000),
|
|
httpHeaders: httpHeadersSchema,
|
|
httpBody: z
|
|
.string()
|
|
.trim()
|
|
.optional()
|
|
.refine((value) => {
|
|
if (!value) return true
|
|
try {
|
|
JSON.parse(value)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}, 'Input must be valid JSON'),
|
|
// When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it
|
|
snippet: z.string().trim(),
|
|
})
|
|
|
|
const sqlFunctionSchema = z.object({
|
|
type: z.literal('sql_function'),
|
|
schema: z.string().trim().min(1, 'Please select one of the listed database schemas'),
|
|
functionName: z.string().trim().min(1, 'Please select one of the listed database functions'),
|
|
// When editing a cron job, we want to keep the original command as a snippet in case the user wants to manually edit it
|
|
snippet: z.string().trim(),
|
|
})
|
|
|
|
const sqlSnippetSchema = z.object({
|
|
type: z.literal('sql_snippet'),
|
|
snippet: z.string().trim().min(1),
|
|
})
|
|
|
|
export const FormSchema = z
|
|
.object({
|
|
name: z.string().trim(),
|
|
supportsSeconds: z.boolean(),
|
|
schedule: z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.refine((value) => {
|
|
if (cronPattern.test(value)) {
|
|
try {
|
|
convertCronToString(value)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
} else if (secondsPattern.test(value)) {
|
|
return true
|
|
}
|
|
return false
|
|
}, 'Invalid Cron format'),
|
|
values: z.discriminatedUnion('type', [
|
|
edgeFunctionSchema,
|
|
httpRequestSchema,
|
|
sqlFunctionSchema,
|
|
sqlSnippetSchema,
|
|
]),
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
if (!cronPattern.test(data.schedule)) {
|
|
if (!(data.supportsSeconds && secondsPattern.test(data.schedule))) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Seconds are supported only in pg_cron v1.5.0+. Please use a valid Cron format.',
|
|
path: ['schedule'],
|
|
})
|
|
}
|
|
}
|
|
|
|
if (data.values.type === 'edge_function' || data.values.type === 'http_request') {
|
|
addHttpHeaderIssues(data.values.httpHeaders, ctx, ['values', 'httpHeaders'])
|
|
}
|
|
})
|
|
|
|
export type CreateCronJobForm = z.infer<typeof FormSchema>
|
|
export type CronJobType = CreateCronJobForm['values']
|