Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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