Skip to content

Commit 8992658

Browse files
adiologydevAdam Gough
authored andcommitted
fix(blocks): workflow handler not working outside gui (#609)
* fix: key to call api internally for workflow block * feat: use jwt for internal auth to avoid a static key * chore: formatter
1 parent 487f032 commit 8992658

File tree

1 file changed

+360
-0
lines changed
  • apps/sim/app/api/marketplace/workflows

1 file changed

+360
-0
lines changed
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
import { desc, eq, sql } from 'drizzle-orm'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { createLogger } from '@/lib/logs/console-logger'
4+
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
5+
import { CATEGORIES } from '@/app/workspace/[workspaceId]/marketplace/constants/categories'
6+
import { db } from '@/db'
7+
import * as schema from '@/db/schema'
8+
9+
const logger = createLogger('MarketplaceWorkflowsAPI')
10+
11+
// Cache for 1 minute but can be revalidated on-demand
12+
export const revalidate = 60
13+
14+
/**
15+
* Consolidated API endpoint for marketplace workflows
16+
*
17+
* Supports:
18+
* - Getting featured/popular/recent workflows
19+
* - Getting workflows by category
20+
* - Getting workflow state
21+
* - Getting workflow details
22+
* - Incrementing view counts
23+
*
24+
* Query parameters:
25+
* - section: 'popular', 'recent', 'byCategory', or specific category name
26+
* - limit: Maximum number of items to return per section (default: 6)
27+
* - includeState: Whether to include workflow state in the response (default: false)
28+
* - workflowId: Specific workflow ID to fetch details for
29+
* - marketplaceId: Specific marketplace entry ID to fetch details for
30+
*/
31+
export async function GET(request: NextRequest) {
32+
const requestId = crypto.randomUUID().slice(0, 8)
33+
34+
try {
35+
// Parse query parameters
36+
const url = new URL(request.url)
37+
const sectionParam = url.searchParams.get('section')
38+
const categoryParam = url.searchParams.get('category')
39+
const limitParam = url.searchParams.get('limit') || '6'
40+
const limit = Number.parseInt(limitParam, 10)
41+
const includeState = url.searchParams.get('includeState') === 'true'
42+
const workflowId = url.searchParams.get('workflowId')
43+
const marketplaceId = url.searchParams.get('marketplaceId')
44+
45+
// Handle single workflow request first (by workflow ID)
46+
if (workflowId) {
47+
let marketplaceEntry
48+
49+
if (includeState) {
50+
// Query with state included
51+
marketplaceEntry = await db
52+
.select({
53+
id: schema.marketplace.id,
54+
workflowId: schema.marketplace.workflowId,
55+
name: schema.marketplace.name,
56+
description: schema.marketplace.description,
57+
authorId: schema.marketplace.authorId,
58+
authorName: schema.marketplace.authorName,
59+
state: schema.marketplace.state,
60+
views: schema.marketplace.views,
61+
category: schema.marketplace.category,
62+
createdAt: schema.marketplace.createdAt,
63+
updatedAt: schema.marketplace.updatedAt,
64+
})
65+
.from(schema.marketplace)
66+
.where(eq(schema.marketplace.workflowId, workflowId))
67+
.limit(1)
68+
.then((rows) => rows[0])
69+
} else {
70+
// Query without state
71+
marketplaceEntry = await db
72+
.select({
73+
id: schema.marketplace.id,
74+
workflowId: schema.marketplace.workflowId,
75+
name: schema.marketplace.name,
76+
description: schema.marketplace.description,
77+
authorId: schema.marketplace.authorId,
78+
authorName: schema.marketplace.authorName,
79+
views: schema.marketplace.views,
80+
category: schema.marketplace.category,
81+
createdAt: schema.marketplace.createdAt,
82+
updatedAt: schema.marketplace.updatedAt,
83+
})
84+
.from(schema.marketplace)
85+
.where(eq(schema.marketplace.workflowId, workflowId))
86+
.limit(1)
87+
.then((rows) => rows[0])
88+
}
89+
90+
if (!marketplaceEntry) {
91+
logger.warn(`[${requestId}] No marketplace entry found for workflow: ${workflowId}`)
92+
return createErrorResponse('Workflow not found in marketplace', 404)
93+
}
94+
95+
// Transform response if state was requested
96+
const responseData =
97+
includeState && 'state' in marketplaceEntry
98+
? {
99+
...marketplaceEntry,
100+
workflowState: marketplaceEntry.state,
101+
state: undefined,
102+
}
103+
: marketplaceEntry
104+
105+
logger.info(`[${requestId}] Retrieved marketplace data for workflow: ${workflowId}`)
106+
return createSuccessResponse(responseData)
107+
}
108+
109+
// Handle single marketplace entry request (by marketplace ID)
110+
if (marketplaceId) {
111+
let marketplaceEntry
112+
113+
if (includeState) {
114+
// Query with state included
115+
marketplaceEntry = await db
116+
.select({
117+
id: schema.marketplace.id,
118+
workflowId: schema.marketplace.workflowId,
119+
name: schema.marketplace.name,
120+
description: schema.marketplace.description,
121+
authorId: schema.marketplace.authorId,
122+
authorName: schema.marketplace.authorName,
123+
state: schema.marketplace.state,
124+
views: schema.marketplace.views,
125+
category: schema.marketplace.category,
126+
createdAt: schema.marketplace.createdAt,
127+
updatedAt: schema.marketplace.updatedAt,
128+
})
129+
.from(schema.marketplace)
130+
.where(eq(schema.marketplace.id, marketplaceId))
131+
.limit(1)
132+
.then((rows) => rows[0])
133+
} else {
134+
// Query without state
135+
marketplaceEntry = await db
136+
.select({
137+
id: schema.marketplace.id,
138+
workflowId: schema.marketplace.workflowId,
139+
name: schema.marketplace.name,
140+
description: schema.marketplace.description,
141+
authorId: schema.marketplace.authorId,
142+
authorName: schema.marketplace.authorName,
143+
views: schema.marketplace.views,
144+
category: schema.marketplace.category,
145+
createdAt: schema.marketplace.createdAt,
146+
updatedAt: schema.marketplace.updatedAt,
147+
})
148+
.from(schema.marketplace)
149+
.where(eq(schema.marketplace.id, marketplaceId))
150+
.limit(1)
151+
.then((rows) => rows[0])
152+
}
153+
154+
if (!marketplaceEntry) {
155+
logger.warn(`[${requestId}] No marketplace entry found with ID: ${marketplaceId}`)
156+
return createErrorResponse('Marketplace entry not found', 404)
157+
}
158+
159+
// Transform response if state was requested
160+
const responseData =
161+
includeState && 'state' in marketplaceEntry
162+
? {
163+
...marketplaceEntry,
164+
workflowState: marketplaceEntry.state,
165+
state: undefined,
166+
}
167+
: marketplaceEntry
168+
169+
logger.info(`[${requestId}] Retrieved marketplace entry: ${marketplaceId}`)
170+
return createSuccessResponse(responseData)
171+
}
172+
173+
// Handle featured/collection requests
174+
const result: {
175+
popular: any[]
176+
recent: any[]
177+
byCategory: Record<string, any[]>
178+
} = {
179+
popular: [],
180+
recent: [],
181+
byCategory: {},
182+
}
183+
184+
// Define common fields to select
185+
const baseFields = {
186+
id: schema.marketplace.id,
187+
workflowId: schema.marketplace.workflowId,
188+
name: schema.marketplace.name,
189+
description: schema.marketplace.description,
190+
authorName: schema.marketplace.authorName,
191+
views: schema.marketplace.views,
192+
category: schema.marketplace.category,
193+
createdAt: schema.marketplace.createdAt,
194+
updatedAt: schema.marketplace.updatedAt,
195+
}
196+
197+
// Add state if requested
198+
const selectFields = includeState
199+
? { ...baseFields, state: schema.marketplace.state }
200+
: baseFields
201+
202+
// Determine which sections to fetch
203+
const sections = sectionParam ? sectionParam.split(',') : ['popular', 'recent', 'byCategory']
204+
205+
// Get popular items if requested
206+
if (sections.includes('popular')) {
207+
result.popular = await db
208+
.select(selectFields)
209+
.from(schema.marketplace)
210+
.orderBy(desc(schema.marketplace.views))
211+
.limit(limit)
212+
}
213+
214+
// Get recent items if requested
215+
if (sections.includes('recent')) {
216+
result.recent = await db
217+
.select(selectFields)
218+
.from(schema.marketplace)
219+
.orderBy(desc(schema.marketplace.createdAt))
220+
.limit(limit)
221+
}
222+
223+
// Get categories if requested
224+
if (
225+
sections.includes('byCategory') ||
226+
categoryParam ||
227+
sections.some((s) => CATEGORIES.some((c) => c.value === s))
228+
) {
229+
// Identify all requested categories
230+
const requestedCategories = new Set<string>()
231+
232+
// Add explicitly requested category
233+
if (categoryParam) {
234+
requestedCategories.add(categoryParam)
235+
}
236+
237+
// Add categories from sections parameter
238+
sections.forEach((section) => {
239+
if (CATEGORIES.some((c) => c.value === section)) {
240+
requestedCategories.add(section)
241+
}
242+
})
243+
244+
// Include byCategory section contents if requested
245+
if (sections.includes('byCategory')) {
246+
CATEGORIES.forEach((c) => requestedCategories.add(c.value))
247+
}
248+
249+
// Log what we're fetching
250+
const categoriesToFetch = Array.from(requestedCategories)
251+
logger.info(`[${requestId}] Fetching specific categories: ${categoriesToFetch.join(', ')}`)
252+
253+
// Process each requested category
254+
await Promise.all(
255+
categoriesToFetch.map(async (categoryValue) => {
256+
const categoryItems = await db
257+
.select(selectFields)
258+
.from(schema.marketplace)
259+
.where(eq(schema.marketplace.category, categoryValue))
260+
.orderBy(desc(schema.marketplace.views))
261+
.limit(limit)
262+
263+
// Always add the category to the result, even if empty
264+
result.byCategory[categoryValue] = categoryItems
265+
logger.info(
266+
`[${requestId}] Category ${categoryValue}: found ${categoryItems.length} items`
267+
)
268+
})
269+
)
270+
}
271+
272+
// Transform the data if state was included to match the expected format
273+
if (includeState) {
274+
const transformSection = (section: any[]) => {
275+
return section.map((item) => {
276+
if ('state' in item) {
277+
// Create a new object without the state field, but with workflowState
278+
const { state, ...rest } = item
279+
return {
280+
...rest,
281+
workflowState: state,
282+
}
283+
}
284+
return item
285+
})
286+
}
287+
288+
if (result.popular.length > 0) {
289+
result.popular = transformSection(result.popular)
290+
}
291+
292+
if (result.recent.length > 0) {
293+
result.recent = transformSection(result.recent)
294+
}
295+
296+
Object.keys(result.byCategory).forEach((category) => {
297+
if (result.byCategory[category].length > 0) {
298+
result.byCategory[category] = transformSection(result.byCategory[category])
299+
}
300+
})
301+
}
302+
303+
logger.info(`[${requestId}] Fetched marketplace items${includeState ? ' with state' : ''}`)
304+
return NextResponse.json(result)
305+
} catch (error: any) {
306+
logger.error(`[${requestId}] Error fetching marketplace items`, error)
307+
return NextResponse.json({ error: error.message }, { status: 500 })
308+
}
309+
}
310+
311+
/**
312+
* POST handler for incrementing view counts
313+
*
314+
* Request body:
315+
* - id: Marketplace entry ID to increment view count for
316+
*/
317+
export async function POST(request: NextRequest) {
318+
const requestId = crypto.randomUUID().slice(0, 8)
319+
320+
try {
321+
const body = await request.json()
322+
const { id } = body
323+
324+
if (!id) {
325+
return createErrorResponse('Marketplace ID is required', 400)
326+
}
327+
328+
// Find the marketplace entry
329+
const marketplaceEntry = await db
330+
.select({
331+
id: schema.marketplace.id,
332+
})
333+
.from(schema.marketplace)
334+
.where(eq(schema.marketplace.id, id))
335+
.limit(1)
336+
.then((rows) => rows[0])
337+
338+
if (!marketplaceEntry) {
339+
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
340+
return createErrorResponse('Marketplace entry not found', 404)
341+
}
342+
343+
// Increment the view count
344+
await db
345+
.update(schema.marketplace)
346+
.set({
347+
views: sql`${schema.marketplace.views} + 1`,
348+
})
349+
.where(eq(schema.marketplace.id, id))
350+
351+
logger.info(`[${requestId}] Incremented view count for marketplace entry: ${id}`)
352+
353+
return createSuccessResponse({
354+
success: true,
355+
})
356+
} catch (error: any) {
357+
logger.error(`[${requestId}] Error incrementing view count`, error)
358+
return createErrorResponse(`Failed to track view: ${error.message}`, 500)
359+
}
360+
}

0 commit comments

Comments
 (0)