Skip to content

Commit 1a6ad87

Browse files
Merge remote-tracking branch 'origin/improvement/v2-endpoints' into codex/v2-application-tables
# Conflicts: # apps/sim/lib/api/server/routes/v2-json-route.ts # apps/sim/lib/folders/orchestration.ts # apps/sim/lib/uploads/upload-session/service.ts
2 parents 910f133 + d7ccbec commit 1a6ad87

249 files changed

Lines changed: 21151 additions & 10697 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { NextRequest } from 'next/server'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const mocks = vi.hoisted(() => ({
8+
getSession: vi.fn(),
9+
execute: vi.fn(),
10+
}))
11+
12+
vi.mock('@/lib/auth', () => ({ getSession: mocks.getSession }))
13+
14+
vi.mock('@/lib/audit-logs/application/list-audit-logs', () => ({
15+
listAuditLogs: { operation: { id: 'audit_logs.list' }, execute: mocks.execute },
16+
}))
17+
18+
import { OrchestrationError } from '@/lib/core/orchestration/types'
19+
import { GET } from '@/app/api/audit-logs/route'
20+
21+
const log = {
22+
id: 'audit-1',
23+
workspaceId: 'workspace-1',
24+
actorId: 'admin-1',
25+
actorName: 'Ada',
26+
actorEmail: 'ada@example.com',
27+
action: 'workspace.updated',
28+
resourceType: 'workspace',
29+
resourceId: 'workspace-1',
30+
resourceName: 'Engineering',
31+
description: null,
32+
metadata: {},
33+
createdAt: new Date('2026-08-01T00:00:00Z'),
34+
}
35+
36+
describe('GET /api/audit-logs', () => {
37+
beforeEach(() => {
38+
vi.clearAllMocks()
39+
mocks.getSession.mockResolvedValue({
40+
user: { id: 'admin-1' },
41+
session: { id: 'session-1' },
42+
})
43+
mocks.execute.mockResolvedValue({ data: [log], nextCursor: 'next-1' })
44+
})
45+
46+
it('authenticates before parsing the organization query', async () => {
47+
const response = await GET(new NextRequest('http://localhost:3000/api/audit-logs'))
48+
49+
expect(response.status).toBe(400)
50+
expect(mocks.getSession).toHaveBeenCalled()
51+
expect(mocks.execute).not.toHaveBeenCalled()
52+
})
53+
54+
it('keeps the internal envelope while sharing the application operation', async () => {
55+
const request = new NextRequest(
56+
'http://localhost:3000/api/audit-logs?organizationId=organization-1'
57+
)
58+
const response = await GET(request)
59+
60+
expect(response.status).toBe(200)
61+
expect(await response.json()).toMatchObject({
62+
success: true,
63+
data: [{ id: 'audit-1', actorId: 'admin-1' }],
64+
nextCursor: 'next-1',
65+
})
66+
expect(mocks.execute).toHaveBeenCalledWith({
67+
principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' },
68+
input: expect.objectContaining({ organizationId: 'organization-1' }),
69+
request,
70+
})
71+
})
72+
73+
it('preserves internal typed error presentation', async () => {
74+
mocks.execute.mockRejectedValueOnce(new OrchestrationError('forbidden', 'Admin required'))
75+
76+
const response = await GET(
77+
new NextRequest('http://localhost:3000/api/audit-logs?organizationId=organization-1')
78+
)
79+
80+
expect(response.status).toBe(403)
81+
expect(await response.json()).toEqual({ error: 'Admin required' })
82+
})
83+
})
Lines changed: 36 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,42 @@
1-
import { createLogger } from '@sim/logger'
2-
import { getErrorMessage } from '@sim/utils/errors'
3-
import { type NextRequest, NextResponse } from 'next/server'
41
import { listAuditLogsContract } from '@/lib/api/contracts/audit-logs'
5-
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
6-
import { getSession } from '@/lib/auth'
7-
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
9-
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
102
import {
11-
buildFilterConditions,
12-
buildOrgScopeCondition,
13-
getOrgWorkspaceIds,
14-
queryAuditLogs,
15-
} from '@/app/api/v1/audit-logs/query'
16-
17-
const logger = createLogger('AuditLogsAPI')
3+
defineInternalJsonRoute,
4+
internalPlainOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { listAuditLogs } from '@/lib/audit-logs/application/list-audit-logs'
9+
import { auditLogOperations } from '@/lib/audit-logs/application/operations'
10+
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
1811

1912
export const dynamic = 'force-dynamic'
2013

21-
export const GET = withRouteHandler(async (request: NextRequest) => {
22-
try {
23-
const session = await getSession()
24-
if (!session?.user?.id) {
25-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
26-
}
27-
28-
const parsed = await parseRequest(
29-
listAuditLogsContract,
30-
request,
31-
{},
32-
{
33-
validationErrorResponse: (error) =>
34-
NextResponse.json(
35-
{ error: getValidationErrorMessage(error, 'Invalid query parameters') },
36-
{ status: 400 }
37-
),
38-
}
39-
)
40-
if (!parsed.success) return parsed.response
41-
42-
const authResult = await validateEnterpriseAuditAccess(
43-
session.user.id,
44-
parsed.data.query.organizationId
45-
)
46-
if (!authResult.success) {
47-
return authResult.response
48-
}
49-
50-
const { organizationId, orgMemberIds } = authResult.context
51-
52-
const {
53-
organizationId: _targetOrganizationId,
54-
search,
55-
action,
56-
resourceType,
57-
actorId,
58-
startDate,
59-
endDate,
60-
includeDeparted,
61-
limit,
62-
cursor,
63-
} = parsed.data.query
64-
65-
const orgWorkspaceIds = await getOrgWorkspaceIds(organizationId)
66-
const scopeCondition = buildOrgScopeCondition({
67-
organizationId,
68-
orgWorkspaceIds,
69-
orgMemberIds,
70-
includeDeparted,
71-
})
72-
const filterConditions = buildFilterConditions({
73-
action,
74-
resourceType,
75-
actorId,
76-
search,
77-
startDate,
78-
endDate,
79-
})
80-
81-
const { data, nextCursor } = await queryAuditLogs(
82-
[scopeCondition, ...filterConditions],
83-
limit,
84-
cursor
85-
)
86-
87-
return NextResponse.json({
88-
success: true,
89-
data: data.map(formatAuditLogEntry),
90-
nextCursor,
91-
})
92-
} catch (error: unknown) {
93-
const message = getErrorMessage(error, 'Unknown error')
94-
logger.error('Audit logs fetch error', { error: message })
95-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
96-
}
14+
export const GET = defineInternalJsonRoute({
15+
contract: listAuditLogsContract,
16+
auth: internalSessionAuth,
17+
operation: auditLogOperations.list,
18+
rateLimit: internalRateLimits.none({
19+
reason: 'Existing authenticated audit-log settings read has no request-rate policy',
20+
}),
21+
errorPolicy: internalPlainOrchestrationErrorPolicy,
22+
mapInput: ({ query }) => ({
23+
organizationId: query.organizationId,
24+
includeDeparted: query.includeDeparted,
25+
filters: {
26+
search: query.search,
27+
action: query.action,
28+
resourceType: query.resourceType,
29+
actorId: query.actorId,
30+
startDate: query.startDate,
31+
endDate: query.endDate,
32+
},
33+
limit: query.limit,
34+
cursor: query.cursor,
35+
}),
36+
useCase: listAuditLogs,
37+
present: ({ data, nextCursor }) => ({
38+
success: true,
39+
data: data.map(formatAuditLogEntry),
40+
nextCursor,
41+
}),
9742
})

apps/sim/app/api/knowledge/[id]/documents/uploads/[uploadId]/complete/route.ts

Lines changed: 40 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { completeKnowledgeDocumentUploadContract } from '@/lib/api/contracts/knowledge/upload-sessions'
33
import { parseRequest } from '@/lib/api/server'
4-
import { generateRequestId } from '@/lib/core/utils/request'
4+
import { PlatformEvents } from '@/lib/core/telemetry'
55
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
6-
import { completeUploadSession } from '@/lib/uploads/upload-session/service'
7-
import { uploadSessionErrorResponse } from '@/app/api/files/uploads/utils'
6+
import { completeKnowledgeDocumentUpload } from '@/lib/knowledge/application/upload-sessions'
7+
import { captureServerEvent } from '@/lib/posthog/server'
88
import {
9-
requireKnowledgeDocumentUploadAccess,
9+
knowledgeDocumentUploadErrorResponse,
1010
requireKnowledgeDocumentUploadActor,
11-
resolveKnowledgeDocumentUploadAttribution,
1211
} from '@/app/api/knowledge/[id]/documents/uploads/utils'
13-
import {
14-
finalizeKnowledgeDocumentUpload,
15-
getOwnedKnowledgeDocumentUpload,
16-
toV2KnowledgeDocumentUpload,
17-
} from '@/app/api/v2/knowledge/[id]/documents/uploads/utils'
12+
import { toV2KnowledgeDocumentUpload } from '@/app/api/v2/knowledge/[id]/documents/uploads/utils'
1813

1914
interface KnowledgeDocumentUploadRouteParams {
2015
params: Promise<{ id: string; uploadId: string }>
@@ -28,44 +23,46 @@ export const POST = withRouteHandler(
2823
if (!parsed.success) return parsed.response
2924
const { id: knowledgeBaseId, uploadId } = parsed.data.params
3025
const { workspaceId } = parsed.data.query
31-
const access = await requireKnowledgeDocumentUploadAccess({
32-
knowledgeBaseId,
33-
workspaceId,
34-
userId: actor.id,
35-
})
36-
if (access instanceof NextResponse) return access
37-
const requestId = generateRequestId()
3826
try {
39-
const upload = await getOwnedKnowledgeDocumentUpload({
40-
knowledgeBaseId,
41-
uploadId,
42-
workspaceId,
43-
userId: actor.id,
44-
uploadToken: parsed.data.headers['upload-token'],
45-
})
46-
const completed = await completeUploadSession({
47-
session: upload,
48-
finalize: (claimed) =>
49-
finalizeKnowledgeDocumentUpload({
50-
claimed,
51-
knowledgeBaseId,
52-
knowledgeBaseName: access.knowledgeBase.name,
53-
workspaceId,
54-
userId: actor.id,
55-
resolveAttribution: () =>
56-
resolveKnowledgeDocumentUploadAttribution({ workspaceId, userId: actor.id }),
57-
source: 'ui',
58-
requestId,
59-
request,
60-
actorName: actor.name,
61-
actorEmail: actor.email,
62-
}),
27+
const completed = await completeKnowledgeDocumentUpload.execute({
28+
principal: { kind: 'session', userId: actor.id, sessionId: actor.sessionId },
29+
input: {
30+
knowledgeBaseId,
31+
assertedWorkspaceId: workspaceId,
32+
uploadId,
33+
uploadToken: parsed.data.headers['upload-token'],
34+
source: 'ui',
35+
},
36+
request,
6337
})
38+
if (completed.value.created) {
39+
captureServerEvent(
40+
actor.id,
41+
'knowledge_base_document_uploaded',
42+
{
43+
knowledge_base_id: completed.knowledgeBaseId,
44+
workspace_id: completed.workspaceId,
45+
document_count: 1,
46+
upload_type: 'single',
47+
},
48+
{
49+
groups: { workspace: completed.workspaceId },
50+
setOnce: { first_document_uploaded_at: new Date().toISOString() },
51+
}
52+
)
53+
PlatformEvents.knowledgeBaseDocumentsUploaded({
54+
knowledgeBaseId: completed.knowledgeBaseId,
55+
documentsCount: 1,
56+
uploadType: 'single',
57+
mimeType: completed.value.document.mimeType,
58+
fileSize: completed.value.document.fileSize,
59+
})
60+
}
6461
return NextResponse.json({
65-
data: toV2KnowledgeDocumentUpload(completed.session, completed.value),
62+
data: toV2KnowledgeDocumentUpload(completed.session, completed.value.document),
6663
})
6764
} catch (error) {
68-
const classified = uploadSessionErrorResponse(error)
65+
const classified = knowledgeDocumentUploadErrorResponse(error)
6966
if (classified) return classified
7067
throw error
7168
}

apps/sim/app/api/knowledge/[id]/documents/uploads/[uploadId]/parts/route.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ import { type NextRequest, NextResponse } from 'next/server'
22
import { createKnowledgeDocumentUploadPartUrlsContract } from '@/lib/api/contracts/knowledge/upload-sessions'
33
import { parseRequest } from '@/lib/api/server'
44
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
5-
import { createUploadPartUrls } from '@/lib/uploads/upload-session/service'
6-
import { uploadSessionErrorResponse } from '@/app/api/files/uploads/utils'
5+
import { issueKnowledgeDocumentUploadParts } from '@/lib/knowledge/application/upload-sessions'
76
import {
8-
requireKnowledgeDocumentUploadAccess,
7+
knowledgeDocumentUploadErrorResponse,
98
requireKnowledgeDocumentUploadActor,
109
} from '@/app/api/knowledge/[id]/documents/uploads/utils'
11-
import { getOwnedKnowledgeDocumentUpload } from '@/app/api/v2/knowledge/[id]/documents/uploads/utils'
1210

1311
interface KnowledgeDocumentUploadRouteParams {
1412
params: Promise<{ id: string; uploadId: string }>
@@ -26,28 +24,21 @@ export const POST = withRouteHandler(
2624
if (!parsed.success) return parsed.response
2725
const { id: knowledgeBaseId, uploadId } = parsed.data.params
2826
const { workspaceId } = parsed.data.query
29-
const access = await requireKnowledgeDocumentUploadAccess({
30-
knowledgeBaseId,
31-
workspaceId,
32-
userId: actor.id,
33-
})
34-
if (access instanceof NextResponse) return access
3527
try {
36-
const upload = await getOwnedKnowledgeDocumentUpload({
37-
knowledgeBaseId,
38-
uploadId,
39-
workspaceId,
40-
userId: actor.id,
41-
uploadToken: parsed.data.headers['upload-token'],
42-
})
43-
const parts = await createUploadPartUrls({
44-
session: upload,
45-
partNumbers: parsed.data.body.partNumbers,
46-
localOrigin: request.nextUrl.origin,
28+
const { parts } = await issueKnowledgeDocumentUploadParts.execute({
29+
principal: { kind: 'session', userId: actor.id, sessionId: actor.sessionId },
30+
input: {
31+
knowledgeBaseId,
32+
assertedWorkspaceId: workspaceId,
33+
uploadId,
34+
uploadToken: parsed.data.headers['upload-token'],
35+
partNumbers: parsed.data.body.partNumbers,
36+
},
37+
request,
4738
})
4839
return NextResponse.json({ data: { parts } })
4940
} catch (error) {
50-
const classified = uploadSessionErrorResponse(error)
41+
const classified = knowledgeDocumentUploadErrorResponse(error)
5142
if (classified) return classified
5243
throw error
5344
}

0 commit comments

Comments
 (0)