mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-11 07:58:06 -05:00
Feature/api (#82)
* my test changes for branch protection * feat(api): introduced 'deploy as an API' button and updated workflows db to include status of deployment * feat(api): added 'trigger' column for logs table to indicate source of workflow run, persist logs from API executions, removed session validation in favor of API key * fix(bug): cleanup old reference to JSX element in favor of ReactElement * feat(api): added persistent notification for one-click deployment with copy boxes for url, keys, & ex curl * fix(ui/notifications): cleaned up deploy with one-click button ui
This commit is contained in:
@@ -290,6 +290,7 @@ export async function GET(req: NextRequest) {
|
||||
log.error || `Completed successfully`
|
||||
}`,
|
||||
duration: log.success ? `${log.durationMs}ms` : 'NA',
|
||||
trigger: 'schedule',
|
||||
createdAt: new Date(log.endedAt || log.startedAt),
|
||||
})
|
||||
}
|
||||
@@ -309,6 +310,7 @@ export async function GET(req: NextRequest) {
|
||||
? 'Scheduled workflow executed successfully'
|
||||
: `Scheduled workflow execution failed: ${result.error}`,
|
||||
duration: result.success ? `${totalDuration}ms` : 'NA',
|
||||
trigger: 'schedule',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
@@ -353,6 +355,7 @@ export async function GET(req: NextRequest) {
|
||||
level: 'error',
|
||||
message: error.message || 'Unknown error during scheduled workflow execution',
|
||||
createdAt: new Date(),
|
||||
trigger: 'schedule',
|
||||
})
|
||||
|
||||
// On error, increment next_run_at by a small delay to prevent immediate retries
|
||||
|
||||
40
app/api/workflow/[id]/deploy/route.ts
Normal file
40
app/api/workflow/[id]/deploy/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { db } from '@/db'
|
||||
import { workflow } from '@/db/schema'
|
||||
import { validateWorkflowAccess } from '../../middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Generate a new API key
|
||||
const apiKey = `wf_${uuidv4().replace(/-/g, '')}`
|
||||
|
||||
// Update the workflow with the API key and deployment status
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({
|
||||
apiKey,
|
||||
isDeployed: true,
|
||||
deployedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflow.id, id))
|
||||
|
||||
return createSuccessResponse({ apiKey })
|
||||
} catch (error: any) {
|
||||
console.error('Error deploying workflow:', error)
|
||||
return createErrorResponse(error.message || 'Failed to deploy workflow', 500)
|
||||
}
|
||||
}
|
||||
214
app/api/workflow/[id]/execute/route.ts
Normal file
214
app/api/workflow/[id]/execute/route.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { persistLog } from '@/lib/logging'
|
||||
import { decryptSecret } from '@/lib/utils'
|
||||
import { WorkflowState } from '@/stores/workflow/types'
|
||||
import { mergeSubblockState } from '@/stores/workflow/utils'
|
||||
import { db } from '@/db'
|
||||
import { environment } from '@/db/schema'
|
||||
import { Executor } from '@/executor'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { validateWorkflowAccess } from '../../middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '../../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
// Define the schema for environment variables
|
||||
const EnvVarsSchema = z.record(z.string())
|
||||
|
||||
// Keep track of running executions to prevent overlap
|
||||
const runningExecutions = new Set<string>()
|
||||
|
||||
async function executeWorkflow(workflow: any, input?: any) {
|
||||
const workflowId = workflow.id
|
||||
const executionId = uuidv4()
|
||||
|
||||
// Skip if this workflow is already running
|
||||
if (runningExecutions.has(workflowId)) {
|
||||
throw new Error('Workflow is already running')
|
||||
}
|
||||
|
||||
try {
|
||||
runningExecutions.add(workflowId)
|
||||
|
||||
// Get the workflow state
|
||||
const state = workflow.state as WorkflowState
|
||||
const { blocks, edges, loops } = state
|
||||
|
||||
// Use the same execution flow as in scheduled executions
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
|
||||
// Retrieve environment variables for this user
|
||||
const [userEnv] = await db
|
||||
.select()
|
||||
.from(environment)
|
||||
.where(eq(environment.userId, workflow.userId))
|
||||
.limit(1)
|
||||
|
||||
if (!userEnv) {
|
||||
throw new Error('No environment variables found for this user')
|
||||
}
|
||||
|
||||
// Parse and validate environment variables
|
||||
const variables = EnvVarsSchema.parse(userEnv.variables)
|
||||
|
||||
// Replace environment variables in the block states
|
||||
const currentBlockStates = await Object.entries(mergedStates).reduce(
|
||||
async (accPromise, [id, block]) => {
|
||||
const acc = await accPromise
|
||||
acc[id] = await Object.entries(block.subBlocks).reduce(
|
||||
async (subAccPromise, [key, subBlock]) => {
|
||||
const subAcc = await subAccPromise
|
||||
let value = subBlock.value
|
||||
|
||||
// If the value is a string and contains environment variable syntax
|
||||
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
||||
const matches = value.match(/{{([^}]+)}}/g)
|
||||
if (matches) {
|
||||
// Process all matches sequentially
|
||||
for (const match of matches) {
|
||||
const varName = match.slice(2, -2) // Remove {{ and }}
|
||||
const encryptedValue = variables[varName]
|
||||
if (!encryptedValue) {
|
||||
throw new Error(`Environment variable "${varName}" was not found`)
|
||||
}
|
||||
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(encryptedValue)
|
||||
value = (value as string).replace(match, decrypted)
|
||||
} catch (error: any) {
|
||||
console.error('Error decrypting value:', error)
|
||||
throw new Error(
|
||||
`Failed to decrypt environment variable "${varName}": ${error.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subAcc[key] = value
|
||||
return subAcc
|
||||
},
|
||||
Promise.resolve({} as Record<string, any>)
|
||||
)
|
||||
return acc
|
||||
},
|
||||
Promise.resolve({} as Record<string, Record<string, any>>)
|
||||
)
|
||||
|
||||
// Create a map of decrypted environment variables
|
||||
const decryptedEnvVars: Record<string, string> = {}
|
||||
for (const [key, encryptedValue] of Object.entries(variables)) {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(encryptedValue)
|
||||
decryptedEnvVars[key] = decrypted
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to decrypt ${key}:`, error)
|
||||
throw new Error(`Failed to decrypt environment variable "${key}": ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize and execute the workflow
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
|
||||
const executor = new Executor(serializedWorkflow, currentBlockStates, decryptedEnvVars)
|
||||
const result = await executor.execute(workflowId)
|
||||
|
||||
// Log each execution step
|
||||
for (const log of result.logs || []) {
|
||||
await persistLog({
|
||||
id: uuidv4(),
|
||||
workflowId,
|
||||
executionId,
|
||||
level: log.success ? 'info' : 'error',
|
||||
message: `Block ${log.blockName || log.blockId} (${log.blockType}): ${
|
||||
log.error || 'Completed successfully'
|
||||
}`,
|
||||
duration: log.success ? `${log.durationMs}ms` : 'NA',
|
||||
trigger: 'api',
|
||||
createdAt: new Date(log.endedAt || log.startedAt),
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate total duration from successful block logs
|
||||
const totalDuration = (result.logs || [])
|
||||
.filter((log) => log.success)
|
||||
.reduce((sum, log) => sum + log.durationMs, 0)
|
||||
|
||||
// Log the final execution result
|
||||
await persistLog({
|
||||
id: uuidv4(),
|
||||
workflowId,
|
||||
executionId,
|
||||
level: result.success ? 'info' : 'error',
|
||||
message: result.success
|
||||
? 'API workflow executed successfully'
|
||||
: `API workflow execution failed: ${result.error}`,
|
||||
duration: result.success ? `${totalDuration}ms` : 'NA',
|
||||
trigger: 'api',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
// Log the error
|
||||
await persistLog({
|
||||
id: uuidv4(),
|
||||
workflowId,
|
||||
executionId,
|
||||
level: 'error',
|
||||
message: `API workflow execution failed: ${error.message}`,
|
||||
duration: 'NA',
|
||||
trigger: 'api',
|
||||
createdAt: new Date(),
|
||||
})
|
||||
throw error
|
||||
} finally {
|
||||
runningExecutions.delete(workflowId)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const validation = await validateWorkflowAccess(request, id)
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
const result = await executeWorkflow(validation.workflow)
|
||||
return createSuccessResponse(result)
|
||||
} catch (error: any) {
|
||||
console.error('Error executing workflow:', error)
|
||||
return createErrorResponse(
|
||||
error.message || 'Failed to execute workflow',
|
||||
500,
|
||||
'EXECUTION_ERROR'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
const validation = await validateWorkflowAccess(request, id)
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const result = await executeWorkflow(validation.workflow, body)
|
||||
return createSuccessResponse(result)
|
||||
} catch (error: any) {
|
||||
console.error('Error executing workflow:', error)
|
||||
return createErrorResponse(
|
||||
error.message || 'Failed to execute workflow',
|
||||
500,
|
||||
'EXECUTION_ERROR'
|
||||
)
|
||||
}
|
||||
}
|
||||
54
app/api/workflow/[id]/route.ts
Normal file
54
app/api/workflow/[id]/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { Executor } from '@/executor'
|
||||
import { SerializedWorkflow } from '@/serializer/types'
|
||||
import { validateWorkflowAccess } from '../middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
async function executeWorkflow(workflow: any, input?: any) {
|
||||
try {
|
||||
const executor = new Executor(workflow.state as SerializedWorkflow, input)
|
||||
const result = await executor.execute(workflow.id)
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error('Workflow execution failed:', { workflowId: workflow.id, error })
|
||||
throw new Error(`Execution failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
const validation = await validateWorkflowAccess(request, id)
|
||||
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
const result = await executeWorkflow(validation.workflow)
|
||||
return createSuccessResponse(result)
|
||||
} catch (error: any) {
|
||||
console.error('Error executing workflow:', error)
|
||||
return createErrorResponse('Failed to execute workflow', 500, 'EXECUTION_ERROR')
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const validation = await validateWorkflowAccess(request, id)
|
||||
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const result = await executeWorkflow(validation.workflow, body)
|
||||
return createSuccessResponse(result)
|
||||
} catch (error: any) {
|
||||
console.error('Error executing workflow:', error)
|
||||
return createErrorResponse('Failed to execute workflow', 500, 'EXECUTION_ERROR')
|
||||
}
|
||||
}
|
||||
20
app/api/workflow/[id]/status/route.ts
Normal file
20
app/api/workflow/[id]/status/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { validateWorkflowAccess } from '../../middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '../../utils'
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
if (validation.error) {
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
return createSuccessResponse({
|
||||
isDeployed: validation.workflow.isDeployed,
|
||||
deployedAt: validation.workflow.deployedAt,
|
||||
})
|
||||
} catch (error) {
|
||||
return createErrorResponse('Failed to get status', 500)
|
||||
}
|
||||
}
|
||||
56
app/api/workflow/middleware.ts
Normal file
56
app/api/workflow/middleware.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { getWorkflowById } from '@/lib/workflows'
|
||||
|
||||
export interface ValidationResult {
|
||||
error?: { message: string; status: number }
|
||||
workflow?: any
|
||||
}
|
||||
|
||||
export async function validateWorkflowAccess(
|
||||
request: NextRequest,
|
||||
workflowId: string,
|
||||
requireDeployment = true
|
||||
): Promise<ValidationResult> {
|
||||
try {
|
||||
const workflow = await getWorkflowById(workflowId)
|
||||
if (!workflow) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Workflow not found',
|
||||
status: 404,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (requireDeployment) {
|
||||
if (!workflow.isDeployed) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Workflow is not deployed',
|
||||
status: 403,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// API key authentication
|
||||
const apiKey = request.headers.get('x-api-key')
|
||||
if (!apiKey || !workflow.apiKey || apiKey !== workflow.apiKey) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Unauthorized',
|
||||
status: 401,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return { workflow }
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error)
|
||||
return {
|
||||
error: {
|
||||
message: 'Internal server error',
|
||||
status: 500,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
15
app/api/workflow/utils.ts
Normal file
15
app/api/workflow/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export function createErrorResponse(error: string, status: number, code?: string) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error,
|
||||
code: code || error.toUpperCase().replace(/\s+/g, '_'),
|
||||
},
|
||||
{ status }
|
||||
)
|
||||
}
|
||||
|
||||
export function createSuccessResponse(data: any) {
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import Landing from './(landing)/landing'
|
||||
|
||||
export default Landing
|
||||
|
||||
// adding test
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import { highlight, languages } from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/themes/prism.css'
|
||||
@@ -148,7 +149,7 @@ export function Code({
|
||||
|
||||
// Render helpers
|
||||
const renderLineNumbers = () => {
|
||||
const numbers: JSX.Element[] = []
|
||||
const numbers: ReactElement[] = []
|
||||
let lineNumber = 1
|
||||
|
||||
visualLineHeights.forEach((height) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import { ChevronDown, ChevronUp, Plus, Trash } from 'lucide-react'
|
||||
import { highlight, languages } from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
@@ -220,7 +221,7 @@ export function ConditionInput({ blockId, subBlockId, isConnecting }: ConditionI
|
||||
|
||||
// Modify the line numbers rendering to be block-specific
|
||||
const renderLineNumbers = (blockId: string) => {
|
||||
const numbers: JSX.Element[] = []
|
||||
const numbers: ReactElement[] = []
|
||||
let lineNumber = 1
|
||||
const blockHeights = visualLineHeights[blockId] || []
|
||||
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { AlertCircle, Terminal } from 'lucide-react'
|
||||
import { AlertCircle, Copy, Key, Terminal, X } from 'lucide-react'
|
||||
import { ErrorIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { NotificationStore, NotificationType } from '@/stores/notifications/types'
|
||||
import {
|
||||
NotificationOptions,
|
||||
NotificationStore,
|
||||
NotificationType,
|
||||
} from '@/stores/notifications/types'
|
||||
|
||||
interface NotificationDropdownItemProps {
|
||||
id: string
|
||||
type: NotificationType
|
||||
message: string
|
||||
timestamp: number
|
||||
options?: NotificationOptions
|
||||
}
|
||||
|
||||
const NotificationIcon = {
|
||||
error: ErrorIcon,
|
||||
console: Terminal,
|
||||
api: Key,
|
||||
}
|
||||
|
||||
const NotificationColors = {
|
||||
error: 'text-destructive',
|
||||
console: 'text-foreground',
|
||||
api: 'text-green-500',
|
||||
}
|
||||
|
||||
export function NotificationDropdownItem({
|
||||
@@ -29,10 +37,12 @@ export function NotificationDropdownItem({
|
||||
type,
|
||||
message,
|
||||
timestamp,
|
||||
options,
|
||||
}: NotificationDropdownItemProps) {
|
||||
const { showNotification } = useNotificationStore()
|
||||
const { removeNotification } = useNotificationStore()
|
||||
const Icon = NotificationIcon[type]
|
||||
const [, forceUpdate] = useState({})
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// Update the time display every minute
|
||||
useEffect(() => {
|
||||
@@ -42,21 +52,58 @@ export function NotificationDropdownItem({
|
||||
|
||||
const timeAgo = formatDistanceToNow(timestamp, { addSuffix: true })
|
||||
|
||||
// Truncate message if it's too long
|
||||
const truncatedMessage = message.length > 50 ? `${message.slice(0, 50)}...` : message
|
||||
const handleCopy = async () => {
|
||||
if (options?.copyableContent) {
|
||||
await navigator.clipboard.writeText(options.copyableContent)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className="flex items-start gap-2 p-3 cursor-pointer"
|
||||
onClick={() => showNotification(id)}
|
||||
className="flex items-start gap-2 p-3 cursor-default"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<Icon className={cn('h-4 w-4', NotificationColors[type])} />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">{type === 'error' ? 'Error' : 'Console'}</span>
|
||||
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
||||
<Icon className={cn('h-4 w-4 mt-1', NotificationColors[type])} />
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium">
|
||||
{type === 'error' ? 'Error' : type === 'api' ? 'API' : 'Console'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{timeAgo}</span>
|
||||
</div>
|
||||
{options?.isPersistent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 hover:bg-transparent hover:text-destructive"
|
||||
onClick={() => removeNotification(id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-foreground">{truncatedMessage}</p>
|
||||
<p className="text-sm text-foreground whitespace-pre-wrap">{message}</p>
|
||||
{options?.copyableContent && (
|
||||
<div className="mt-2 relative">
|
||||
<pre className="bg-muted rounded-md p-2 text-xs font-mono">
|
||||
{options.copyableContent}
|
||||
</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 h-6 w-6 hover:bg-muted-foreground/20"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
{copied && (
|
||||
<span className="absolute top-2 right-9 text-xs text-muted-foreground">Copied!</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { Bell, History, MessageSquare, Play, Trash2 } from 'lucide-react'
|
||||
import { Bell, History, Play, Rocket, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflow/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflow/store'
|
||||
@@ -31,10 +32,12 @@ import { HistoryDropdownItem } from './components/history-dropdown-item'
|
||||
import { NotificationDropdownItem } from './components/notification-dropdown-item'
|
||||
|
||||
export function ControlBar() {
|
||||
const { notifications, getWorkflowNotifications } = useNotificationStore()
|
||||
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
|
||||
const { history, undo, redo, revertToHistoryState, lastSaved } = useWorkflowStore()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedName, setEditedName] = useState('')
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [notificationsOpen, setNotificationsOpen] = useState(false)
|
||||
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
|
||||
const [, forceUpdate] = useState({})
|
||||
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
|
||||
@@ -103,6 +106,66 @@ export function ControlBar() {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the deployment state and handlers
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isDeployed, setIsDeployed] = useState(false)
|
||||
|
||||
// Check deployment status on mount
|
||||
useEffect(() => {
|
||||
async function checkStatus() {
|
||||
if (!activeWorkflowId) return
|
||||
try {
|
||||
const response = await fetch(`/api/workflow/${activeWorkflowId}/status`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setIsDeployed(data.isDeployed)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check deployment status:', error)
|
||||
}
|
||||
}
|
||||
checkStatus()
|
||||
}, [activeWorkflowId])
|
||||
|
||||
const handleDeploy = async () => {
|
||||
if (!activeWorkflowId) return
|
||||
try {
|
||||
setIsDeploying(true)
|
||||
const response = await fetch(`/api/workflow/${activeWorkflowId}/deploy`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Failed to deploy workflow')
|
||||
|
||||
const { apiKey } = await response.json()
|
||||
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflow/${activeWorkflowId}/execute`
|
||||
|
||||
addNotification('api', 'Workflow successfully deployed', activeWorkflowId, {
|
||||
isPersistent: true,
|
||||
sections: [
|
||||
{
|
||||
label: 'API Endpoint',
|
||||
content: endpoint,
|
||||
},
|
||||
{
|
||||
label: 'API Key',
|
||||
content: apiKey,
|
||||
},
|
||||
{
|
||||
label: 'Example curl command',
|
||||
content: `curl -X POST -H "X-API-Key: ${apiKey}" -H "Content-Type: application/json" ${endpoint}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
setIsDeployed(true)
|
||||
} catch (error) {
|
||||
addNotification('error', 'Failed to deploy workflow. Please try again.', activeWorkflowId)
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-16 w-full items-center justify-between bg-background px-6 border-b transition-all duration-300">
|
||||
{/* Left Section - Workflow Info */}
|
||||
@@ -177,13 +240,39 @@ export function ControlBar() {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<History />
|
||||
<span className="sr-only">Version History</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDeploy}
|
||||
disabled={isDeploying}
|
||||
className={cn(
|
||||
'hover:text-foreground',
|
||||
isDeployed && 'text-green-500 hover:text-green-500'
|
||||
)}
|
||||
>
|
||||
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
|
||||
<span className="sr-only">Deploy API</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenu open={historyOpen} onOpenChange={setHistoryOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<History />
|
||||
<span className="sr-only">Version History</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
{!historyOpen && <TooltipContent>History</TooltipContent>}
|
||||
</Tooltip>
|
||||
|
||||
{history.past.length === 0 && history.future.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
@@ -227,13 +316,18 @@ export function ControlBar() {
|
||||
)}
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell />
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Bell />
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
|
||||
</Tooltip>
|
||||
|
||||
{workflowNotifications.length === 0 ? (
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
@@ -246,7 +340,14 @@ export function ControlBar() {
|
||||
{[...workflowNotifications]
|
||||
.sort((a, b) => b.timestamp - a.timestamp)
|
||||
.map((notification) => (
|
||||
<NotificationDropdownItem key={notification.id} {...notification} />
|
||||
<NotificationDropdownItem
|
||||
key={notification.id}
|
||||
id={notification.id}
|
||||
type={notification.type}
|
||||
message={notification.message}
|
||||
timestamp={notification.timestamp}
|
||||
options={notification.options}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertCircle, Terminal } from 'lucide-react'
|
||||
import { AlertCircle, Copy, Key, Terminal, X } from 'lucide-react'
|
||||
import { ErrorIcon } from '@/components/icons'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
|
||||
@@ -11,6 +12,7 @@ const FADE_DURATION = 300
|
||||
const NotificationIcon = {
|
||||
error: ErrorIcon,
|
||||
console: Terminal,
|
||||
api: Key,
|
||||
}
|
||||
|
||||
const NotificationColors = {
|
||||
@@ -18,18 +20,19 @@ const NotificationColors = {
|
||||
'border-red-500 bg-red-50 text-destructive dark:border-border dark:text-foreground dark:bg-background',
|
||||
console:
|
||||
'border-border bg-background text-foreground dark:border-border dark:text-foreground dark:bg-background',
|
||||
api: 'border-green-500 bg-green-50 text-green-700 dark:border-border dark:text-green-500 dark:bg-background',
|
||||
}
|
||||
|
||||
export function NotificationList() {
|
||||
const { notifications, hideNotification } = useNotificationStore()
|
||||
const [fadingNotifications, setFadingNotifications] = useState<Set<string>>(new Set())
|
||||
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Only show visible notifications in the display
|
||||
const visibleNotifications = notifications.filter((n) => n.isVisible)
|
||||
|
||||
useEffect(() => {
|
||||
notifications.forEach((notification) => {
|
||||
if (!notification.isVisible) return
|
||||
if (!notification.isVisible || notification.options?.isPersistent) return
|
||||
|
||||
// Start fade out
|
||||
const fadeTimer = setTimeout(() => {
|
||||
@@ -53,6 +56,14 @@ export function NotificationList() {
|
||||
})
|
||||
}, [notifications, hideNotification])
|
||||
|
||||
const handleCopy = async (id: string, sectionIndex: number, content: string) => {
|
||||
await navigator.clipboard.writeText(content)
|
||||
setCopiedMap((prev) => ({ ...prev, [`${id}-${sectionIndex}`]: true }))
|
||||
setTimeout(() => {
|
||||
setCopiedMap((prev) => ({ ...prev, [`${id}-${sectionIndex}`]: false }))
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
if (visibleNotifications.length === 0) return null
|
||||
|
||||
return (
|
||||
@@ -76,16 +87,55 @@ export function NotificationList() {
|
||||
NotificationColors[notification.type]
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn('h-4 w-4', {
|
||||
'!text-red-500': notification.type === 'error',
|
||||
'text-foreground': notification.type === 'console',
|
||||
})}
|
||||
/>
|
||||
<AlertTitle className="ml-2">
|
||||
{notification.type === 'error' ? 'Error' : 'Console'}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="ml-2">{notification.message}</AlertDescription>
|
||||
<div className="flex items-start gap-2 w-full">
|
||||
<Icon
|
||||
className={cn('h-4 w-4 mt-1', {
|
||||
'!text-red-500': notification.type === 'error',
|
||||
'text-foreground': notification.type === 'console',
|
||||
'!text-green-500': notification.type === 'api',
|
||||
})}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<AlertTitle className="flex items-center justify-between">
|
||||
<span>
|
||||
{notification.type === 'error'
|
||||
? 'Error'
|
||||
: notification.type === 'api'
|
||||
? 'API'
|
||||
: 'Console'}
|
||||
</span>
|
||||
{notification.options?.isPersistent && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-4 w-4 hover:bg-transparent hover:text-destructive -mt-1"
|
||||
onClick={() => hideNotification(notification.id)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p className="mb-4">{notification.message}</p>
|
||||
{notification.options?.sections?.map((section, index) => (
|
||||
<div key={index} className="mt-4">
|
||||
<div className="text-xs font-medium mb-2">{section.label}</div>
|
||||
<div
|
||||
className="relative group cursor-pointer"
|
||||
onClick={() => handleCopy(notification.id, index, section.content)}
|
||||
>
|
||||
<pre className="bg-muted rounded-md p-2 pr-20 text-xs font-mono whitespace-pre-wrap transition-colors hover:bg-muted/80">
|
||||
{section.content}
|
||||
</pre>
|
||||
<div className="absolute top-2 right-2 text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{copiedMap[`${notification.id}-${index}`] ? 'Copied!' : 'Click to copy'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)
|
||||
})}
|
||||
|
||||
3
db/migrations/0009_cynical_bullseye.sql
Normal file
3
db/migrations/0009_cynical_bullseye.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "workflow" ADD COLUMN "is_deployed" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "workflow" ADD COLUMN "deployed_at" timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "workflow" ADD COLUMN "api_key" text;
|
||||
1
db/migrations/0010_flashy_nebula.sql
Normal file
1
db/migrations/0010_flashy_nebula.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "workflow_logs" ADD COLUMN "trigger" text NOT NULL;
|
||||
1
db/migrations/0011_youthful_iron_lad.sql
Normal file
1
db/migrations/0011_youthful_iron_lad.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "workflow_logs" ALTER COLUMN "trigger" DROP NOT NULL;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "b90e880d-8074-44c6-808a-fc57568d7bd3",
|
||||
"prevId": "1e6e54f4-6927-4625-983d-b4a1b1fad36d",
|
||||
"id": "7e51973f-f82e-4094-86b3-34866e50f43e",
|
||||
"prevId": "b90e880d-8074-44c6-808a-fc57568d7bd3",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -503,6 +503,25 @@
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_deployed": {
|
||||
"name": "is_deployed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"deployed_at": {
|
||||
"name": "deployed_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"api_key": {
|
||||
"name": "api_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
|
||||
708
db/migrations/meta/0010_snapshot.json
Normal file
708
db/migrations/meta/0010_snapshot.json
Normal file
@@ -0,0 +1,708 @@
|
||||
{
|
||||
"id": "141235ed-23e4-420b-8e3c-d502c64a4c28",
|
||||
"prevId": "7e51973f-f82e-4094-86b3-34866e50f43e",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.environment": {
|
||||
"name": "environment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"variables": {
|
||||
"name": "variables",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"environment_user_id_user_id_fk": {
|
||||
"name": "environment_user_id_user_id_fk",
|
||||
"tableFrom": "environment",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"environment_user_id_unique": {
|
||||
"name": "environment_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["user_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["token"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.settings": {
|
||||
"name": "settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"general": {
|
||||
"name": "general",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"settings_user_id_user_id_fk": {
|
||||
"name": "settings_user_id_user_id_fk",
|
||||
"tableFrom": "settings",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"settings_user_id_unique": {
|
||||
"name": "settings_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["user_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.waitlist": {
|
||||
"name": "waitlist",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"waitlist_email_unique": {
|
||||
"name": "waitlist_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow": {
|
||||
"name": "workflow",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_synced": {
|
||||
"name": "last_synced",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_deployed": {
|
||||
"name": "is_deployed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"deployed_at": {
|
||||
"name": "deployed_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"api_key": {
|
||||
"name": "api_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_user_id_user_id_fk": {
|
||||
"name": "workflow_user_id_user_id_fk",
|
||||
"tableFrom": "workflow",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow_logs": {
|
||||
"name": "workflow_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"execution_id": {
|
||||
"name": "execution_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"level": {
|
||||
"name": "level",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trigger": {
|
||||
"name": "trigger",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_logs_workflow_id_workflow_id_fk": {
|
||||
"name": "workflow_logs_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "workflow_logs",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow_schedule": {
|
||||
"name": "workflow_schedule",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_run_at": {
|
||||
"name": "next_run_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_ran_at": {
|
||||
"name": "last_ran_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trigger_type": {
|
||||
"name": "trigger_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_schedule_workflow_id_workflow_id_fk": {
|
||||
"name": "workflow_schedule_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "workflow_schedule",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"workflow_schedule_workflow_id_unique": {
|
||||
"name": "workflow_schedule_workflow_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["workflow_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
708
db/migrations/meta/0011_snapshot.json
Normal file
708
db/migrations/meta/0011_snapshot.json
Normal file
@@ -0,0 +1,708 @@
|
||||
{
|
||||
"id": "87a9f389-6dcc-441d-8000-a447c9c25522",
|
||||
"prevId": "141235ed-23e4-420b-8e3c-d502c64a4c28",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.environment": {
|
||||
"name": "environment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"variables": {
|
||||
"name": "variables",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"environment_user_id_user_id_fk": {
|
||||
"name": "environment_user_id_user_id_fk",
|
||||
"tableFrom": "environment",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"environment_user_id_unique": {
|
||||
"name": "environment_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["user_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["token"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.settings": {
|
||||
"name": "settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"general": {
|
||||
"name": "general",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"settings_user_id_user_id_fk": {
|
||||
"name": "settings_user_id_user_id_fk",
|
||||
"tableFrom": "settings",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"settings_user_id_unique": {
|
||||
"name": "settings_user_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["user_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.waitlist": {
|
||||
"name": "waitlist",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"waitlist_email_unique": {
|
||||
"name": "waitlist_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow": {
|
||||
"name": "workflow",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_synced": {
|
||||
"name": "last_synced",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_deployed": {
|
||||
"name": "is_deployed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"deployed_at": {
|
||||
"name": "deployed_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"api_key": {
|
||||
"name": "api_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_user_id_user_id_fk": {
|
||||
"name": "workflow_user_id_user_id_fk",
|
||||
"tableFrom": "workflow",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow_logs": {
|
||||
"name": "workflow_logs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"execution_id": {
|
||||
"name": "execution_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"level": {
|
||||
"name": "level",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trigger": {
|
||||
"name": "trigger",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_logs_workflow_id_workflow_id_fk": {
|
||||
"name": "workflow_logs_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "workflow_logs",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workflow_schedule": {
|
||||
"name": "workflow_schedule",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"workflow_id": {
|
||||
"name": "workflow_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_run_at": {
|
||||
"name": "next_run_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_ran_at": {
|
||||
"name": "last_ran_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"trigger_type": {
|
||||
"name": "trigger_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"workflow_schedule_workflow_id_workflow_id_fk": {
|
||||
"name": "workflow_schedule_workflow_id_workflow_id_fk",
|
||||
"tableFrom": "workflow_schedule",
|
||||
"tableTo": "workflow",
|
||||
"columnsFrom": ["workflow_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"workflow_schedule_workflow_id_unique": {
|
||||
"name": "workflow_schedule_workflow_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["workflow_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,27 @@
|
||||
"when": 1740004147676,
|
||||
"tag": "0008_quick_paladin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1740336159745,
|
||||
"tag": "0009_cynical_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1740340257207,
|
||||
"tag": "0010_flashy_nebula",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "7",
|
||||
"when": 1740340299261,
|
||||
"tag": "0011_youthful_iron_lad",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ export const workflow = pgTable('workflow', {
|
||||
lastSynced: timestamp('last_synced').notNull(),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
isDeployed: boolean('is_deployed').notNull().default(false),
|
||||
deployedAt: timestamp('deployed_at'),
|
||||
apiKey: text('api_key'),
|
||||
})
|
||||
|
||||
export const waitlist = pgTable('waitlist', {
|
||||
@@ -80,6 +83,7 @@ export const workflowLogs = pgTable('workflow_logs', {
|
||||
level: text('level').notNull(), // e.g. "info", "error", etc.
|
||||
message: text('message').notNull(),
|
||||
duration: text('duration'), // Store as text to allow 'NA' for errors
|
||||
trigger: text('trigger'), // e.g. "api", "schedule", "manual"
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
})
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface LogEntry {
|
||||
message: string
|
||||
createdAt: Date
|
||||
duration?: string
|
||||
trigger?: string
|
||||
}
|
||||
|
||||
export async function persistLog(log: LogEntry) {
|
||||
|
||||
16
lib/utils.ts
16
lib/utils.ts
@@ -1,5 +1,5 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
@@ -117,3 +117,17 @@ export function convertScheduleOptionsToCron(
|
||||
throw new Error('Unsupported schedule type')
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateApiKey(): Promise<string> {
|
||||
const buffer = randomBytes(32)
|
||||
const hash = createHash('sha256').update(buffer).digest('hex')
|
||||
return `wf_${hash}`
|
||||
}
|
||||
|
||||
export async function validateApiKey(
|
||||
apiKey: string | null,
|
||||
storedApiKey: string | null
|
||||
): Promise<boolean> {
|
||||
if (!apiKey || !storedApiKey) return false
|
||||
return apiKey === storedApiKey
|
||||
}
|
||||
|
||||
30
lib/workflows.ts
Normal file
30
lib/workflows.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db } from '@/db'
|
||||
import { workflow as workflowTable } from '@/db/schema'
|
||||
|
||||
export async function getWorkflowById(id: string) {
|
||||
const workflows = await db.select().from(workflowTable).where(eq(workflowTable.id, id)).limit(1)
|
||||
|
||||
return workflows[0]
|
||||
}
|
||||
|
||||
export async function updateWorkflowDeploymentStatus(
|
||||
id: string,
|
||||
isDeployed: boolean,
|
||||
apiKey?: string
|
||||
) {
|
||||
return db
|
||||
.update(workflowTable)
|
||||
.set({
|
||||
isDeployed,
|
||||
deployedAt: isDeployed ? new Date() : null,
|
||||
updatedAt: new Date(),
|
||||
apiKey: apiKey || null,
|
||||
})
|
||||
.where(eq(workflowTable.id, id))
|
||||
}
|
||||
|
||||
export function getWorkflowEndpoint(id: string) {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
||||
return `${baseUrl}/api/workflow/${id}`
|
||||
}
|
||||
1428
package-lock.json
generated
1428
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import { Notification, NotificationStore, NotificationType } from './types'
|
||||
import { Notification, NotificationOptions, NotificationStore, NotificationType } from './types'
|
||||
|
||||
const STORAGE_KEY = 'workflow-notifications'
|
||||
// Maximum number of notifications to keep across all workflows
|
||||
@@ -24,7 +24,7 @@ export const useNotificationStore = create<NotificationStore>()(
|
||||
(set, get) => ({
|
||||
notifications: loadPersistedNotifications(),
|
||||
|
||||
addNotification: (type, message, workflowId) => {
|
||||
addNotification: (type, message, workflowId, options: NotificationOptions = {}) => {
|
||||
// Only create notifications on the client side
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
@@ -32,9 +32,10 @@ export const useNotificationStore = create<NotificationStore>()(
|
||||
id: crypto.randomUUID(),
|
||||
type,
|
||||
message,
|
||||
timestamp: Date.now(), // Simplified timestamp handling
|
||||
timestamp: Date.now(),
|
||||
isVisible: true,
|
||||
workflowId,
|
||||
options,
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type NotificationType = 'error' | 'console'
|
||||
export type NotificationType = 'error' | 'console' | 'api'
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
@@ -7,11 +7,28 @@ export interface Notification {
|
||||
timestamp: number
|
||||
isVisible: boolean
|
||||
workflowId: string | null
|
||||
options?: NotificationOptions
|
||||
}
|
||||
|
||||
export interface NotificationSection {
|
||||
label: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface NotificationOptions {
|
||||
copyableContent?: string
|
||||
isPersistent?: boolean
|
||||
sections?: NotificationSection[]
|
||||
}
|
||||
|
||||
export interface NotificationStore {
|
||||
notifications: Notification[]
|
||||
addNotification: (type: NotificationType, message: string, workflowId: string | null) => void
|
||||
addNotification: (
|
||||
type: NotificationType,
|
||||
message: string,
|
||||
workflowId: string | null,
|
||||
options?: NotificationOptions
|
||||
) => void
|
||||
hideNotification: (id: string) => void
|
||||
showNotification: (id: string) => void
|
||||
removeNotification: (id: string) => void
|
||||
|
||||
Reference in New Issue
Block a user