| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- import { keyword, literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
- import { toString as CronToString } from 'cronstrue'
- import { Column } from 'react-data-grid'
- import { cn } from 'ui'
- import { CronJobType } from './CreateCronJobSheet/CreateCronJobSheet.constants'
- import { CRON_TABLE_COLUMNS, HTTPHeader, secondsPattern } from './CronJobs.constants'
- import { CronJobTableCell } from './CronJobTableCell'
- import { CronJob } from '@/data/database-cron-jobs/database-cron-jobs-infinite-query'
- const unescapeSqlLiteral = (value = '', isEscapeString = false) => {
- const unescaped = value.replaceAll("''", "'")
- return isEscapeString ? unescaped.replaceAll('\\\\', '\\') : unescaped
- }
- export function buildCronQuery(name: string, schedule: string, command: string): SafeSqlFragment {
- return safeSql`select cron.schedule(${literal(name)}, ${literal(schedule)}, ${literal(command)});`
- }
- export const buildHttpRequestCommand = (
- method: 'GET' | 'POST',
- url: string,
- headers: HTTPHeader[] = [],
- body: string | undefined,
- timeout: number
- ): SafeSqlFragment => {
- const funcName = keyword(method === 'GET' ? 'http_get' : 'http_post')
- const headersJson = JSON.stringify(
- Object.fromEntries(headers.filter((v) => v.name && v.value).map((v) => [v.name, v.value]))
- )
- const bodyPart = method === 'POST' && body ? safeSql`\n body:=${literal(body)},` : safeSql``
- return safeSql`
- select
- net.${funcName}(
- url:=${literal(url)},
- headers:=${literal(headersJson)}::jsonb, ${bodyPart}
- timeout_milliseconds:=${literal(timeout)}
- );`
- }
- const DEFAULT_CRONJOB_COMMAND = {
- type: 'sql_snippet',
- snippet: '',
- // add default values for the other command types. Even though they don't exist in sql_snippet, they'll still work as default values.
- method: 'POST',
- timeoutMs: 1000,
- httpBody: '',
- } as const
- export const parseCronJobCommand = (originalCommand: string, projectRef: string): CronJobType => {
- const command = originalCommand.replaceAll('$$', ' ').replaceAll(/\n/g, ' ').trim()
- if (command.toLocaleLowerCase().match(/^select\s+net\./)) {
- const methodMatch = command.match(/select\s+net\.([^']+)\(\s*url:=/i)
- const method = methodMatch?.[1] || ''
- const urlMatch = command.match(/url:=(E)?'((?:''|[^'])*)'/i)
- const url = unescapeSqlLiteral(urlMatch?.[2], Boolean(urlMatch?.[1]))
- const bodyMatch = command.match(/body:=(E)?'((?:''|[^'])*)'/i)
- const body = unescapeSqlLiteral(bodyMatch?.[2], Boolean(bodyMatch?.[1]))
- const timeoutMatch = command.match(/timeout_milliseconds:=(\d+)/i)
- const timeout = timeoutMatch?.[1] || ''
- const headersJsonBuildObjectMatch = command.match(/headers:=jsonb_build_object\(([^)]*)/i)
- const headersJsonBuildObject = headersJsonBuildObjectMatch?.[1] || ''
- let headersObjs: { name: string; value: string }[] = []
- if (headersJsonBuildObject) {
- const headers = headersJsonBuildObject
- .split(',')
- .map((s) => unescapeSqlLiteral(s.trim().replace(/^'|'$/g, '')))
- for (let i = 0; i < headers.length; i += 2) {
- if (headers[i] && headers[i].length > 0) {
- headersObjs.push({ name: headers[i], value: headers[i + 1] })
- }
- }
- } else {
- const headersStringMatch = command.match(/headers:=(E)?'((?:''|[^'])*)'/i)
- const headersString =
- unescapeSqlLiteral(headersStringMatch?.[2], Boolean(headersStringMatch?.[1])) || '{}'
- try {
- const parsedHeaders = JSON.parse(headersString)
- headersObjs = Object.entries(parsedHeaders).map(([name, value]) => ({
- name,
- value: value as string,
- }))
- } catch (error) {
- console.error('Error parsing headers:', error)
- }
- }
- // If there's a search param or hash in the edge function URL, let it be handled by the HTTP Request case.
- // Otherwise, the params/hash may be lost during editing of the cron job.
- let searchParams = ''
- let urlHash = ''
- try {
- const urlObject = new URL(url)
- searchParams = urlObject.search
- urlHash = urlObject.hash
- } catch {}
- if (
- url.includes(`${projectRef}.briven.`) &&
- url.includes('/functions/v1/') &&
- searchParams.length === 0 &&
- urlHash.length === 0
- ) {
- return {
- type: 'edge_function',
- method: method === 'http_get' ? 'GET' : 'POST',
- edgeFunctionName: url,
- httpHeaders: headersObjs,
- httpBody: body,
- timeoutMs: Number(timeout ?? 1000),
- snippet: originalCommand,
- }
- }
- if (url !== '') {
- return {
- type: 'http_request',
- method: method === 'http_get' ? 'GET' : 'POST',
- endpoint: url,
- httpHeaders: headersObjs,
- httpBody: body,
- timeoutMs: Number(timeout ?? 1000),
- snippet: originalCommand,
- }
- }
- }
- const regexDBFunction = /select\s+[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+\s*\(\)/g
- if (command.toLocaleLowerCase().match(regexDBFunction)) {
- const [schemaName, functionName] = command
- .replace(/^select\s+/i, '')
- .replace(/\(.*\);*/, '')
- .trim()
- .split('.')
- return {
- type: 'sql_function',
- schema: schemaName,
- functionName: functionName,
- snippet: originalCommand,
- }
- }
- if (command.length > 0) {
- return {
- type: 'sql_snippet',
- snippet: originalCommand,
- }
- }
- return DEFAULT_CRONJOB_COMMAND
- }
- export function calculateDuration(start: string, end: string): string {
- const startTime = new Date(start).getTime()
- const endTime = new Date(end).getTime()
- const duration = endTime - startTime
- if (isNaN(duration)) return 'Invalid Date'
- if (duration < 1000) return `${duration}ms`
- if (duration < 60000) return `${(duration / 1000).toFixed(1)}s`
- return `${(duration / 60000).toFixed(1)}m`
- }
- export function formatDate(dateString: string): string {
- const date = new Date(dateString)
- if (isNaN(date.getTime())) {
- return 'Invalid Date'
- }
- const options: Intl.DateTimeFormatOptions = {
- year: 'numeric',
- month: 'short', // Use 'long' for full month name
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false, // Use 12-hour format if preferred
- timeZoneName: 'short', // Optional: to include timezone
- }
- return date.toLocaleString(undefined, options)
- }
- export function isSecondsFormat(schedule: string): boolean {
- return secondsPattern.test(schedule.trim().toLocaleLowerCase())
- }
- export function getScheduleMessage(scheduleString: string) {
- if (!scheduleString) {
- return 'Enter a valid cron expression above'
- }
- // if the schedule is in seconds format, scheduleString is same as the schedule
- if (secondsPattern.test(scheduleString)) {
- return `The cron will run every ${scheduleString}`
- }
- if (scheduleString.includes('Invalid cron expression')) {
- return scheduleString
- }
- const readableSchedule = scheduleString
- .split(' ')
- .map((s, i) => (i === 0 ? s.toLowerCase() : s))
- .join(' ')
- return `The cron will run ${readableSchedule}.`
- }
- export const formatScheduleString = (value: string) => {
- try {
- if (secondsPattern.test(value)) {
- return value
- } else {
- return CronToString(value)
- }
- } catch (error) {
- return ''
- }
- }
- export const formatCronJobColumns = ({
- onSelectEdit,
- onSelectDelete,
- }: {
- onSelectEdit: (job: CronJob) => void
- onSelectDelete: (job: CronJob) => void
- }): Array<Column<CronJob>> => {
- return CRON_TABLE_COLUMNS.map((col) => {
- const res: Column<CronJob> = {
- key: col.id,
- name: col.name,
- minWidth: col.minWidth ?? 100,
- maxWidth: col.maxWidth,
- width: col.width,
- resizable: col.resizable ?? false,
- sortable: false,
- draggable: false,
- headerCellClass: undefined,
- renderHeaderCell: () => {
- return (
- <div
- className={cn(
- 'flex items-center justify-between font-normal text-xs w-full',
- col.id === 'jobname' && 'ml-8'
- )}
- >
- <p className="text-foreground!">{col.name}</p>
- </div>
- )
- },
- renderCell: ({ row }) => (
- <CronJobTableCell
- row={row}
- col={col}
- onSelectEdit={onSelectEdit}
- onSelectDelete={onSelectDelete}
- />
- ),
- }
- return res
- })
- }
|