d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
179 lines
5.2 KiB
TypeScript
179 lines
5.2 KiB
TypeScript
import { db } from '@sim/db'
|
|
import { workflow } from '@sim/db/schema'
|
|
import { createLogger } from '@sim/logger'
|
|
import { getErrorMessage } from '@sim/utils/errors'
|
|
import { generateId } from '@sim/utils/id'
|
|
import { and, asc, eq, gt, isNull, or } from 'drizzle-orm'
|
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
import { v1ListWorkflowsContract } from '@/lib/api/contracts/v1/workflows'
|
|
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
|
|
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
|
import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
|
|
import {
|
|
checkRateLimit,
|
|
createRateLimitResponse,
|
|
validateWorkspaceAccess,
|
|
} from '@/app/api/v1/middleware'
|
|
|
|
const logger = createLogger('V1WorkflowsAPI')
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
export const revalidate = 0
|
|
|
|
interface CursorData {
|
|
sortOrder: number
|
|
createdAt: string
|
|
id: string
|
|
}
|
|
|
|
function encodeCursor(data: CursorData): string {
|
|
return Buffer.from(JSON.stringify(data)).toString('base64')
|
|
}
|
|
|
|
function decodeCursor(cursor: string): CursorData | null {
|
|
try {
|
|
return JSON.parse(Buffer.from(cursor, 'base64').toString())
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const GET = withRouteHandler(async (request: NextRequest) => {
|
|
const requestId = generateId().slice(0, 8)
|
|
|
|
try {
|
|
const rateLimit = await checkRateLimit(request, 'workflows')
|
|
if (!rateLimit.allowed) {
|
|
return createRateLimitResponse(rateLimit)
|
|
}
|
|
|
|
const userId = rateLimit.userId!
|
|
const parsed = await parseRequest(
|
|
v1ListWorkflowsContract,
|
|
request,
|
|
{},
|
|
{
|
|
validationErrorResponse: (error) =>
|
|
NextResponse.json(
|
|
{
|
|
error: getValidationErrorMessage(error, 'Invalid parameters'),
|
|
details: error.issues,
|
|
},
|
|
{ status: 400 }
|
|
),
|
|
}
|
|
)
|
|
if (!parsed.success) return parsed.response
|
|
|
|
const params = parsed.data.query
|
|
|
|
logger.info(`[${requestId}] Fetching workflows for workspace ${params.workspaceId}`, {
|
|
userId,
|
|
filters: {
|
|
folderId: params.folderId,
|
|
deployedOnly: params.deployedOnly,
|
|
},
|
|
})
|
|
|
|
const accessError = await validateWorkspaceAccess(rateLimit, userId, params.workspaceId)
|
|
if (accessError) return accessError
|
|
|
|
const conditions = [eq(workflow.workspaceId, params.workspaceId), isNull(workflow.archivedAt)]
|
|
|
|
if (params.folderId) {
|
|
conditions.push(eq(workflow.folderId, params.folderId))
|
|
}
|
|
|
|
if (params.deployedOnly) {
|
|
conditions.push(eq(workflow.isDeployed, true))
|
|
}
|
|
|
|
if (params.cursor) {
|
|
const cursorData = decodeCursor(params.cursor)
|
|
if (cursorData) {
|
|
const cursorCondition = or(
|
|
gt(workflow.sortOrder, cursorData.sortOrder),
|
|
and(
|
|
eq(workflow.sortOrder, cursorData.sortOrder),
|
|
gt(workflow.createdAt, new Date(cursorData.createdAt))
|
|
),
|
|
and(
|
|
eq(workflow.sortOrder, cursorData.sortOrder),
|
|
eq(workflow.createdAt, new Date(cursorData.createdAt)),
|
|
gt(workflow.id, cursorData.id)
|
|
)
|
|
)
|
|
if (cursorCondition) {
|
|
conditions.push(cursorCondition)
|
|
}
|
|
}
|
|
}
|
|
|
|
const orderByClause = [asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id)]
|
|
|
|
const rows = await db
|
|
.select({
|
|
id: workflow.id,
|
|
name: workflow.name,
|
|
description: workflow.description,
|
|
folderId: workflow.folderId,
|
|
workspaceId: workflow.workspaceId,
|
|
isDeployed: workflow.isDeployed,
|
|
deployedAt: workflow.deployedAt,
|
|
runCount: workflow.runCount,
|
|
lastRunAt: workflow.lastRunAt,
|
|
sortOrder: workflow.sortOrder,
|
|
createdAt: workflow.createdAt,
|
|
updatedAt: workflow.updatedAt,
|
|
})
|
|
.from(workflow)
|
|
.where(and(...conditions))
|
|
.orderBy(...orderByClause)
|
|
.limit(params.limit + 1)
|
|
|
|
const hasMore = rows.length > params.limit
|
|
const data = rows.slice(0, params.limit)
|
|
|
|
let nextCursor: string | undefined
|
|
if (hasMore && data.length > 0) {
|
|
const lastWorkflow = data[data.length - 1]
|
|
nextCursor = encodeCursor({
|
|
sortOrder: lastWorkflow.sortOrder,
|
|
createdAt: lastWorkflow.createdAt.toISOString(),
|
|
id: lastWorkflow.id,
|
|
})
|
|
}
|
|
|
|
const formattedWorkflows = data.map((w) => ({
|
|
id: w.id,
|
|
name: w.name,
|
|
description: w.description,
|
|
folderId: w.folderId,
|
|
workspaceId: w.workspaceId,
|
|
isDeployed: w.isDeployed,
|
|
deployedAt: w.deployedAt?.toISOString() || null,
|
|
runCount: w.runCount,
|
|
lastRunAt: w.lastRunAt?.toISOString() || null,
|
|
createdAt: w.createdAt.toISOString(),
|
|
updatedAt: w.updatedAt.toISOString(),
|
|
}))
|
|
|
|
const limits = await getUserLimits(userId)
|
|
|
|
const response = createApiResponse(
|
|
{
|
|
data: formattedWorkflows,
|
|
nextCursor,
|
|
},
|
|
limits,
|
|
rateLimit
|
|
)
|
|
|
|
return NextResponse.json(response.body, { headers: response.headers })
|
|
} catch (error: unknown) {
|
|
const message = getErrorMessage(error, 'Unknown error')
|
|
logger.error(`[${requestId}] Workflows fetch error`, { error: message })
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
}
|
|
})
|