Skip to content

Commit 5315002

Browse files
v0.5.3: docs, sheets, slack, custom tools fixes and templates contexts improvements
2 parents 66c8fa2 + 4e5b834 commit 5315002

File tree

42 files changed

+1382
-1401
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1382
-1401
lines changed

apps/docs/app/[lang]/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Navbar } from '@/components/navbar/navbar'
1313
import { i18n } from '@/lib/i18n'
1414
import { source } from '@/lib/source'
1515
import '../global.css'
16-
import { Analytics } from '@vercel/analytics/next'
1716

1817
const inter = Inter({
1918
subsets: ['latin'],
@@ -94,6 +93,8 @@ export default async function Layout({ children, params }: LayoutProps) {
9493
type='application/ld+json'
9594
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
9695
/>
96+
{/* OneDollarStats Analytics - CDN script handles everything automatically */}
97+
<script defer src='https://assets.onedollarstats.com/stonks.js' />
9798
</head>
9899
<body className='flex min-h-screen flex-col font-sans'>
99100
<RootProvider i18n={provider(lang)}>
@@ -132,7 +133,6 @@ export default async function Layout({ children, params }: LayoutProps) {
132133
>
133134
{children}
134135
</DocsLayout>
135-
<Analytics />
136136
</RootProvider>
137137
</body>
138138
</html>

apps/docs/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
},
1313
"dependencies": {
1414
"@tabler/icons-react": "^3.31.0",
15-
"@vercel/analytics": "1.5.0",
1615
"@vercel/og": "^0.6.5",
1716
"clsx": "^2.1.1",
1817
"fumadocs-core": "15.8.2",

apps/sim/app/api/tools/custom/route.test.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ describe('Custom Tools API Routes', () => {
7070
const mockSelect = vi.fn()
7171
const mockFrom = vi.fn()
7272
const mockWhere = vi.fn()
73+
const mockOrderBy = vi.fn()
7374
const mockInsert = vi.fn()
7475
const mockValues = vi.fn()
7576
const mockUpdate = vi.fn()
@@ -84,10 +85,23 @@ describe('Custom Tools API Routes', () => {
8485
// Reset all mock implementations
8586
mockSelect.mockReturnValue({ from: mockFrom })
8687
mockFrom.mockReturnValue({ where: mockWhere })
87-
// where() can be called with limit() or directly awaited
88-
// Create a mock query builder that supports both patterns
88+
// where() can be called with orderBy(), limit(), or directly awaited
89+
// Create a mock query builder that supports all patterns
8990
mockWhere.mockImplementation((condition) => {
90-
// Return an object that is both awaitable and has a limit() method
91+
// Return an object that is both awaitable and has orderBy() and limit() methods
92+
const queryBuilder = {
93+
orderBy: mockOrderBy,
94+
limit: mockLimit,
95+
then: (resolve: (value: typeof sampleTools) => void) => {
96+
resolve(sampleTools)
97+
return queryBuilder
98+
},
99+
catch: (reject: (error: Error) => void) => queryBuilder,
100+
}
101+
return queryBuilder
102+
})
103+
mockOrderBy.mockImplementation(() => {
104+
// orderBy returns an awaitable query builder
91105
const queryBuilder = {
92106
limit: mockLimit,
93107
then: (resolve: (value: typeof sampleTools) => void) => {
@@ -120,9 +134,22 @@ describe('Custom Tools API Routes', () => {
120134
const txMockUpdate = vi.fn().mockReturnValue({ set: mockSet })
121135
const txMockDelete = vi.fn().mockReturnValue({ where: mockWhere })
122136

123-
// Transaction where() should also support the query builder pattern
137+
// Transaction where() should also support the query builder pattern with orderBy
138+
const txMockOrderBy = vi.fn().mockImplementation(() => {
139+
const queryBuilder = {
140+
limit: mockLimit,
141+
then: (resolve: (value: typeof sampleTools) => void) => {
142+
resolve(sampleTools)
143+
return queryBuilder
144+
},
145+
catch: (reject: (error: Error) => void) => queryBuilder,
146+
}
147+
return queryBuilder
148+
})
149+
124150
const txMockWhere = vi.fn().mockImplementation((condition) => {
125151
const queryBuilder = {
152+
orderBy: txMockOrderBy,
126153
limit: mockLimit,
127154
then: (resolve: (value: typeof sampleTools) => void) => {
128155
resolve(sampleTools)
@@ -201,13 +228,19 @@ describe('Custom Tools API Routes', () => {
201228
or: vi.fn().mockImplementation((...conditions) => ({ operator: 'or', conditions })),
202229
isNull: vi.fn().mockImplementation((field) => ({ field, operator: 'isNull' })),
203230
ne: vi.fn().mockImplementation((field, value) => ({ field, value, operator: 'ne' })),
231+
desc: vi.fn().mockImplementation((field) => ({ field, operator: 'desc' })),
204232
}
205233
})
206234

207235
// Mock utils
208236
vi.doMock('@/lib/utils', () => ({
209237
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
210238
}))
239+
240+
// Mock custom tools operations
241+
vi.doMock('@/lib/custom-tools/operations', () => ({
242+
upsertCustomTools: vi.fn().mockResolvedValue(sampleTools),
243+
}))
211244
})
212245

213246
afterEach(() => {
@@ -224,8 +257,10 @@ describe('Custom Tools API Routes', () => {
224257
'http://localhost:3000/api/tools/custom?workspaceId=workspace-123'
225258
)
226259

227-
// Simulate DB returning tools
228-
mockWhere.mockReturnValueOnce(Promise.resolve(sampleTools))
260+
// Simulate DB returning tools with orderBy chain
261+
mockWhere.mockReturnValueOnce({
262+
orderBy: mockOrderBy.mockReturnValueOnce(Promise.resolve(sampleTools)),
263+
})
229264

230265
// Import handler after mocks are set up
231266
const { GET } = await import('@/app/api/tools/custom/route')
@@ -243,6 +278,7 @@ describe('Custom Tools API Routes', () => {
243278
expect(mockSelect).toHaveBeenCalled()
244279
expect(mockFrom).toHaveBeenCalled()
245280
expect(mockWhere).toHaveBeenCalled()
281+
expect(mockOrderBy).toHaveBeenCalled()
246282
})
247283

248284
it('should handle unauthorized access', async () => {

apps/sim/app/api/tools/custom/route.ts

Lines changed: 11 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { db } from '@sim/db'
22
import { customTools, workflow } from '@sim/db/schema'
3-
import { and, eq, isNull, ne, or } from 'drizzle-orm'
3+
import { and, desc, eq, isNull, or } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
66
import { checkHybridAuth } from '@/lib/auth/hybrid'
7+
import { upsertCustomTools } from '@/lib/custom-tools/operations'
78
import { createLogger } from '@/lib/logs/console/logger'
89
import { getUserEntityPermissions } from '@/lib/permissions/utils'
910
import { generateRequestId } from '@/lib/utils'
@@ -101,6 +102,7 @@ export async function GET(request: NextRequest) {
101102
.select()
102103
.from(customTools)
103104
.where(or(...conditions))
105+
.orderBy(desc(customTools.createdAt))
104106

105107
return NextResponse.json({ data: result }, { status: 200 })
106108
} catch (error) {
@@ -150,96 +152,15 @@ export async function POST(req: NextRequest) {
150152
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
151153
}
152154

153-
// Use a transaction for multi-step database operations
154-
return await db.transaction(async (tx) => {
155-
// Process each tool: either update existing or create new
156-
for (const tool of tools) {
157-
const nowTime = new Date()
158-
159-
if (tool.id) {
160-
// Check if tool exists and belongs to the workspace
161-
const existingTool = await tx
162-
.select()
163-
.from(customTools)
164-
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
165-
.limit(1)
166-
167-
if (existingTool.length > 0) {
168-
// Tool exists - check if name changed and if new name conflicts
169-
if (existingTool[0].title !== tool.title) {
170-
// Check for duplicate name in workspace (excluding current tool)
171-
const duplicateTool = await tx
172-
.select()
173-
.from(customTools)
174-
.where(
175-
and(
176-
eq(customTools.workspaceId, workspaceId),
177-
eq(customTools.title, tool.title),
178-
ne(customTools.id, tool.id)
179-
)
180-
)
181-
.limit(1)
182-
183-
if (duplicateTool.length > 0) {
184-
return NextResponse.json(
185-
{
186-
error: `A tool with the name "${tool.title}" already exists in this workspace`,
187-
},
188-
{ status: 400 }
189-
)
190-
}
191-
}
192-
193-
// Update existing tool
194-
await tx
195-
.update(customTools)
196-
.set({
197-
title: tool.title,
198-
schema: tool.schema,
199-
code: tool.code,
200-
updatedAt: nowTime,
201-
})
202-
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
203-
continue
204-
}
205-
}
206-
207-
// Creating new tool - check for duplicate names in workspace
208-
const duplicateTool = await tx
209-
.select()
210-
.from(customTools)
211-
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.title, tool.title)))
212-
.limit(1)
213-
214-
if (duplicateTool.length > 0) {
215-
return NextResponse.json(
216-
{ error: `A tool with the name "${tool.title}" already exists in this workspace` },
217-
{ status: 400 }
218-
)
219-
}
220-
221-
// Create new tool
222-
const newToolId = tool.id || crypto.randomUUID()
223-
await tx.insert(customTools).values({
224-
id: newToolId,
225-
workspaceId,
226-
userId,
227-
title: tool.title,
228-
schema: tool.schema,
229-
code: tool.code,
230-
createdAt: nowTime,
231-
updatedAt: nowTime,
232-
})
233-
}
234-
235-
// Fetch and return the created/updated tools
236-
const resultTools = await tx
237-
.select()
238-
.from(customTools)
239-
.where(eq(customTools.workspaceId, workspaceId))
240-
241-
return NextResponse.json({ success: true, data: resultTools })
155+
// Use the extracted upsert function
156+
const resultTools = await upsertCustomTools({
157+
tools,
158+
workspaceId,
159+
userId,
160+
requestId,
242161
})
162+
163+
return NextResponse.json({ success: true, data: resultTools })
243164
} catch (validationError) {
244165
if (validationError instanceof z.ZodError) {
245166
logger.warn(`[${requestId}] Invalid custom tools data`, {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { db } from '@sim/db'
2+
import { permissions, workspace } from '@sim/db/schema'
3+
import { and, desc, eq } from 'drizzle-orm'
4+
import { redirect } from 'next/navigation'
5+
import { getSession } from '@/lib/auth'
6+
7+
export const dynamic = 'force-dynamic'
8+
export const revalidate = 0
9+
10+
interface TemplateLayoutProps {
11+
children: React.ReactNode
12+
params: Promise<{
13+
id: string
14+
}>
15+
}
16+
17+
/**
18+
* Template detail layout (public scope).
19+
* - If user is authenticated, redirect to workspace-scoped template detail.
20+
* - Otherwise render the public template detail children.
21+
*/
22+
export default async function TemplateDetailLayout({ children, params }: TemplateLayoutProps) {
23+
const { id } = await params
24+
const session = await getSession()
25+
26+
if (session?.user?.id) {
27+
const userWorkspaces = await db
28+
.select({
29+
workspace: workspace,
30+
})
31+
.from(permissions)
32+
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
33+
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
34+
.orderBy(desc(workspace.createdAt))
35+
.limit(1)
36+
37+
if (userWorkspaces.length > 0) {
38+
const firstWorkspace = userWorkspaces[0].workspace
39+
redirect(`/workspace/${firstWorkspace.id}/templates/${id}`)
40+
}
41+
}
42+
43+
return children
44+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import TemplateDetails from './template'
22

3+
/**
4+
* Public template detail page for unauthenticated users.
5+
* Authenticated-user redirect is handled in templates/[id]/layout.tsx.
6+
*/
37
export default function TemplatePage() {
48
return <TemplateDetails />
59
}

0 commit comments

Comments
 (0)