Skip to content
60 changes: 24 additions & 36 deletions apps/sim/app/api/schedules/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
*
* @vitest-environment node
*/

import { createFeatureFlagsMock, createMockRequest } from '@sim/testing'
import { drizzleOrmMock } from '@sim/testing/mocks'
import type { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
mockVerifyCronAuth,
mockExecuteScheduleJob,
mockExecuteJobInline,
mockFeatureFlags,
mockDbReturning,
mockDbUpdate,
mockEnqueue,
Expand All @@ -33,12 +35,6 @@ const {
mockVerifyCronAuth: vi.fn().mockReturnValue(null),
mockExecuteScheduleJob: vi.fn().mockResolvedValue(undefined),
mockExecuteJobInline: vi.fn().mockResolvedValue(undefined),
mockFeatureFlags: {
isTriggerDevEnabled: false,
isHosted: false,
isProd: false,
isDev: true,
},
mockDbReturning,
mockDbUpdate,
mockEnqueue,
Expand All @@ -49,6 +45,13 @@ const {
}
})

const mockFeatureFlags = createFeatureFlagsMock({
isTriggerDevEnabled: false,
isHosted: false,
isProd: false,
isDev: true,
})

vi.mock('@/lib/auth/internal', () => ({
verifyCronAuth: mockVerifyCronAuth,
}))
Expand Down Expand Up @@ -91,17 +94,7 @@ vi.mock('@/lib/workflows/utils', () => ({
}),
}))

vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
ne: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'ne' })),
lte: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'lte' })),
lt: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'lt' })),
not: vi.fn((condition: unknown) => ({ type: 'not', condition })),
isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })),
or: vi.fn((...conditions: unknown[]) => ({ type: 'or', conditions })),
sql: vi.fn((strings: unknown, ...values: unknown[]) => ({ type: 'sql', strings, values })),
}))
vi.mock('drizzle-orm', () => drizzleOrmMock)

vi.mock('@sim/db', () => ({
db: {
Expand Down Expand Up @@ -177,18 +170,13 @@ const SINGLE_JOB = [
},
]

function createMockRequest(): NextRequest {
const mockHeaders = new Map([
['authorization', 'Bearer test-cron-secret'],
['content-type', 'application/json'],
])

return {
headers: {
get: (key: string) => mockHeaders.get(key.toLowerCase()) || null,
},
url: 'http://localhost:3000/api/schedules/execute',
} as NextRequest
function createCronRequest() {
return createMockRequest(
'GET',
undefined,
{ Authorization: 'Bearer test-cron-secret' },
'http://localhost:3000/api/schedules/execute'
)
}

describe('Scheduled Workflow Execution API Route', () => {
Expand All @@ -204,7 +192,7 @@ describe('Scheduled Workflow Execution API Route', () => {
it('should execute scheduled workflows with Trigger.dev disabled', async () => {
mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([])

const response = await GET(createMockRequest())
const response = await GET(createCronRequest() as unknown as NextRequest)

expect(response).toBeDefined()
expect(response.status).toBe(200)
Expand All @@ -217,7 +205,7 @@ describe('Scheduled Workflow Execution API Route', () => {
mockFeatureFlags.isTriggerDevEnabled = true
mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([])

const response = await GET(createMockRequest())
const response = await GET(createCronRequest() as unknown as NextRequest)

expect(response).toBeDefined()
expect(response.status).toBe(200)
Expand All @@ -228,7 +216,7 @@ describe('Scheduled Workflow Execution API Route', () => {
it('should handle case with no due schedules', async () => {
mockDbReturning.mockReturnValueOnce([]).mockReturnValueOnce([])

const response = await GET(createMockRequest())
const response = await GET(createCronRequest() as unknown as NextRequest)

expect(response.status).toBe(200)
const data = await response.json()
Expand All @@ -239,7 +227,7 @@ describe('Scheduled Workflow Execution API Route', () => {
it('should execute multiple schedules in parallel', async () => {
mockDbReturning.mockReturnValueOnce(MULTIPLE_SCHEDULES).mockReturnValueOnce([])

const response = await GET(createMockRequest())
const response = await GET(createCronRequest() as unknown as NextRequest)

expect(response.status).toBe(200)
const data = await response.json()
Expand All @@ -249,7 +237,7 @@ describe('Scheduled Workflow Execution API Route', () => {
it('should queue mothership jobs to BullMQ when available', async () => {
mockDbReturning.mockReturnValueOnce([]).mockReturnValueOnce(SINGLE_JOB)

const response = await GET(createMockRequest())
const response = await GET(createCronRequest() as unknown as NextRequest)

expect(response.status).toBe(200)
expect(mockEnqueueWorkspaceDispatch).toHaveBeenCalledWith(
Expand All @@ -274,7 +262,7 @@ describe('Scheduled Workflow Execution API Route', () => {
it('should enqueue preassigned correlation metadata for schedules', async () => {
mockDbReturning.mockReturnValue(SINGLE_SCHEDULE)

const response = await GET(createMockRequest())
const response = await GET(createCronRequest() as unknown as NextRequest)

expect(response.status).toBe(200)
expect(mockEnqueueWorkspaceDispatch).toHaveBeenCalledWith(
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/v1/admin/folders/[id]/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { exportFolderToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
import { exportFolderToZip } from '@/lib/workflows/operations/import-export'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/v1/admin/workflows/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { createLogger } from '@sim/logger'
import { inArray } from 'drizzle-orm'
import JSZip from 'jszip'
import { NextResponse } from 'next/server'
import { sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
import {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { workflow, workflowFolder, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { exportWorkspaceToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export'
import { sanitizePathSegment } from '@/lib/core/utils/file-download'
import { exportWorkspaceToZip } from '@/lib/workflows/operations/import-export'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
DropdownMenuTrigger,
} from '@/components/emcn'
import { ArrowDown, ArrowUp, Duplicate, Pencil, Trash } from '@/components/emcn/icons'
import type { ContextMenuState } from '../../types'
import type { ContextMenuState } from '@/app/workspace/[workspaceId]/tables/[tableId]/types'

interface ContextMenuProps {
contextMenu: ContextMenuState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import {
Textarea,
} from '@/components/emcn'
import type { ColumnDefinition, TableInfo, TableRow } from '@/lib/table'
import {
cleanCellValue,
formatValueForInput,
} from '@/app/workspace/[workspaceId]/tables/[tableId]/utils'
import {
useCreateTableRow,
useDeleteTableRow,
useDeleteTableRows,
useUpdateTableRow,
} from '@/hooks/queries/tables'
import { cleanCellValue, formatValueForInput } from '../../utils'
import { useTableUndoStore } from '@/stores/table/store'

const logger = createLogger('RowModal')

Expand All @@ -39,13 +43,9 @@ export interface RowModalProps {

function createInitialRowData(columns: ColumnDefinition[]): Record<string, unknown> {
const initial: Record<string, unknown> = {}
columns.forEach((col) => {
if (col.type === 'boolean') {
initial[col.name] = false
} else {
initial[col.name] = ''
}
})
for (const col of columns) {
initial[col.name] = col.type === 'boolean' ? false : ''
}
return initial
}

Expand All @@ -54,16 +54,13 @@ function cleanRowData(
rowData: Record<string, unknown>
): Record<string, unknown> {
const cleanData: Record<string, unknown> = {}

columns.forEach((col) => {
const value = rowData[col.name]
for (const col of columns) {
try {
cleanData[col.name] = cleanCellValue(value, col)
cleanData[col.name] = cleanCellValue(rowData[col.name], col)
} catch {
throw new Error(`Invalid JSON for field: ${col.name}`)
}
})

}
return cleanData
}

Expand All @@ -86,8 +83,7 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
const workspaceId = params.workspaceId as string
const tableId = table.id

const schema = table?.schema
const columns = schema?.columns || []
const columns = table.schema?.columns || []

const [rowData, setRowData] = useState<Record<string, unknown>>(() =>
getInitialRowData(mode, columns, row)
Expand All @@ -97,6 +93,7 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
const updateRowMutation = useUpdateTableRow({ workspaceId, tableId })
const deleteRowMutation = useDeleteTableRow({ workspaceId, tableId })
const deleteRowsMutation = useDeleteTableRows({ workspaceId, tableId })
const pushToUndoStack = useTableUndoStore((s) => s.push)
const isSubmitting =
createRowMutation.isPending ||
updateRowMutation.isPending ||
Expand All @@ -111,9 +108,24 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
const cleanData = cleanRowData(columns, rowData)

if (mode === 'add') {
await createRowMutation.mutateAsync({ data: cleanData })
const response = await createRowMutation.mutateAsync({ data: cleanData })
const createdRow = (response as { data?: { row?: { id?: string; position?: number } } })
?.data?.row
if (createdRow?.id) {
pushToUndoStack(tableId, {
type: 'create-row',
rowId: createdRow.id,
position: createdRow.position ?? 0,
data: cleanData,
})
}
} else if (mode === 'edit' && row) {
const oldData = row.data as Record<string, unknown>
await updateRowMutation.mutateAsync({ rowId: row.id, data: cleanData })
pushToUndoStack(tableId, {
type: 'update-cells',
cells: [{ rowId: row.id, oldData, newData: cleanData }],
})
}

onSuccess()
Expand All @@ -129,8 +141,14 @@ export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess
const idsToDelete = rowIds ?? (row ? [row.id] : [])

try {
if (idsToDelete.length === 1) {
if (idsToDelete.length === 1 && row) {
await deleteRowMutation.mutateAsync(idsToDelete[0])
pushToUndoStack(tableId, {
type: 'delete-rows',
rows: [
{ rowId: row.id, data: row.data as Record<string, unknown>, position: row.position },
],
})
} else {
await deleteRowsMutation.mutateAsync(idsToDelete)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type { TableFilterHandle } from './table-filter'
export { TableFilter } from './table-filter'
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
'use client'

import { memo, useCallback, useMemo, useRef, useState } from 'react'
import {
forwardRef,
memo,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { X } from 'lucide-react'
import { nanoid } from 'nanoid'
import {
Expand All @@ -19,22 +27,42 @@ const OPERATOR_LABELS = Object.fromEntries(
COMPARISON_OPERATORS.map((op) => [op.value, op.label])
) as Record<string, string>

export interface TableFilterHandle {
addColumnRule: (columnName: string) => void
}

interface TableFilterProps {
columns: Array<{ name: string; type: string }>
filter: Filter | null
onApply: (filter: Filter | null) => void
onClose: () => void
initialColumn?: string | null
}

export function TableFilter({ columns, filter, onApply, onClose }: TableFilterProps) {
export const TableFilter = forwardRef<TableFilterHandle, TableFilterProps>(function TableFilter(
{ columns, filter, onApply, onClose, initialColumn },
ref
) {
const [rules, setRules] = useState<FilterRule[]>(() => {
const fromFilter = filterToRules(filter)
return fromFilter.length > 0 ? fromFilter : [createRule(columns)]
if (fromFilter.length > 0) return fromFilter
const rule = createRule(columns)
return [initialColumn ? { ...rule, column: initialColumn } : rule]
})

const rulesRef = useRef(rules)
rulesRef.current = rules

useImperativeHandle(
ref,
() => ({
addColumnRule: (columnName: string) => {
setRules((prev) => [...prev, { ...createRule(columns), column: columnName }])
},
}),
[columns]
)

const columnOptions = useMemo(
() => columns.map((col) => ({ value: col.name, label: col.name })),
[columns]
Expand Down Expand Up @@ -125,7 +153,7 @@ export function TableFilter({ columns, filter, onApply, onClose }: TableFilterPr
</div>
</div>
)
}
})

interface FilterRuleRowProps {
rule: FilterRule
Expand Down
Loading
Loading