Skip to content

Commit

Permalink
Merge branch 'main' into fix-tooltip-invisible
Browse files Browse the repository at this point in the history
  • Loading branch information
geclos authored Oct 22, 2024
2 parents f74ad1d + db533c2 commit dfeea9a
Show file tree
Hide file tree
Showing 204 changed files with 12,016 additions and 2,083 deletions.
1 change: 0 additions & 1 deletion apps/gateway/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ ARG SENTRY_AUTH_TOKEN
FROM node:20-alpine AS alpine

# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk update
RUN apk add --no-cache libc6-compat curl

FROM alpine as base
Expand Down
3 changes: 2 additions & 1 deletion apps/gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"hono": "^4.5.3",
"jet-paths": "^1.0.6",
"lodash-es": "^4.17.21",
"zod": "^3.23.8"
"zod": "^3.23.8",
"rate-limiter-flexible": "^5.0.3"
},
"devDependencies": {
"@latitude-data/eslint-config": "workspace:^",
Expand Down
67 changes: 34 additions & 33 deletions apps/gateway/src/middlewares/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,43 @@ import {
UnprocessableEntityError,
} from '@latitude-data/core/lib/errors'
import { captureException } from '$/common/sentry'
import { createMiddleware } from 'hono/factory'
import { HTTPException } from 'hono/http-exception'

import HttpStatusCodes from '../common/httpStatusCodes'

const errorHandlerMiddleware = () =>
createMiddleware(async (c, next) => {
const err = c.error!
if (!err) return next()
const errorHandlerMiddleware = (err: Error) => {
if (process.env.NODE_ENV !== 'test') {
captureException(err)
}

if (process.env.NODE_ENV !== 'test') {
captureException(err)
}

if (err instanceof UnprocessableEntityError) {
return Response.json(
{
name: err.name,
message: err.message,
details: err.details,
},
{ status: err.statusCode },
)
} else if (err instanceof LatitudeError) {
return Response.json(
{
message: err.message,
details: err.details,
},
{ status: err.statusCode },
)
} else {
return Response.json(
{ message: err.message },
{ status: HttpStatusCodes.BAD_REQUEST },
)
}
})
if (err instanceof HTTPException) {
return Response.json(
{ message: err.message },
{ status: err.status, headers: err.res?.headers },
)
} else if (err instanceof UnprocessableEntityError) {
return Response.json(
{
name: err.name,
message: err.message,
details: err.details,
},
{ status: err.statusCode },
)
} else if (err instanceof LatitudeError) {
return Response.json(
{
message: err.message,
details: err.details,
},
{ status: err.statusCode, headers: err.headers },
)
} else {
return Response.json(
{ message: err.message },
{ status: HttpStatusCodes.INTERNAL_SERVER_ERROR },
)
}
}

export default errorHandlerMiddleware
54 changes: 54 additions & 0 deletions apps/gateway/src/middlewares/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { cache } from '@latitude-data/core/cache'
import { RateLimitError } from '@latitude-data/core/lib/errors'
import { createMiddleware } from 'hono/factory'
import { RateLimiterRedis, RateLimiterRes } from 'rate-limiter-flexible'

const RATE_LIMIT_POINTS = 2000
const RATE_LIMIT_DURATION = 60

const rateLimiter = new RateLimiterRedis({
storeClient: await cache(),
points: RATE_LIMIT_POINTS,
duration: RATE_LIMIT_DURATION,
})

const rateLimitMiddleware = () =>
createMiddleware(async (c, next) => {
const handleRateLimitHeaders = (result: RateLimiterRes) => {
c.header('Retry-After', (result.msBeforeNext / 1000).toString())
c.header('X-RateLimit-Limit', RATE_LIMIT_POINTS.toString())
c.header('X-RateLimit-Remaining', result.remainingPoints.toString())
c.header(
'X-RateLimit-Reset',
(Date.now() + result.msBeforeNext).toString(),
)
}

let token: string | undefined
try {
try {
const authorization = c.req.header('Authorization')
token = authorization?.split(' ')[1]
} catch (error) {
return await next()
}

const result = await rateLimiter.consume(token as string)
handleRateLimitHeaders(result)
await next()
} catch (error) {
if (error instanceof RateLimiterRes) {
const res = error as RateLimiterRes
throw new RateLimitError(
'Too many requests',
res.msBeforeNext / 1000,
RATE_LIMIT_POINTS,
res.remainingPoints,
Date.now() + res.msBeforeNext,
)
}
throw error
}
})

export default rateLimitMiddleware
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ describe('POST /run', () => {
body = JSON.stringify({
path: document.documentVersion.path,
parameters: {},
customIdentifier: 'miau',
})
headers = {
Authorization: `Bearer ${token}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const factory = new Factory()

const runSchema = z.object({
path: z.string(),
customIdentifier: z.string().optional(),
parameters: z.record(z.any()).optional().default({}),
__internal: z
.object({
Expand All @@ -28,7 +29,8 @@ export const runHandler = factory.createHandlers(
c,
async (stream) => {
const { projectId, versionUuid } = c.req.param()
const { path, parameters, __internal } = c.req.valid('json')
const { path, parameters, customIdentifier, __internal } =
c.req.valid('json')
const workspace = c.get('workspace')
const { document, commit } = await getData({
workspace,
Expand All @@ -41,6 +43,7 @@ export const runHandler = factory.createHandlers(
document,
commit,
parameters,
customIdentifier,
source: __internal?.source ?? LogSources.API,
}).then((r) => r.unwrap())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('POST /run', () => {
)

expect(res.status).toBe(401)
expect(res.headers.get('www-authenticate')).toBe('Bearer realm=""')
})
})

Expand Down Expand Up @@ -109,6 +110,7 @@ describe('POST /run', () => {
body = JSON.stringify({
path: document.documentVersion.path,
parameters: {},
customIdentifier: 'miau',
stream: true,
})
headers = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const factory = new Factory()
const runSchema = z.object({
path: z.string(),
stream: z.boolean().default(false),
customIdentifier: z.string().optional(),
parameters: z.record(z.any()).optional().default({}),
__internal: z
.object({
Expand All @@ -29,7 +30,13 @@ export const runHandler = factory.createHandlers(
zValidator('json', runSchema),
async (c) => {
const { projectId, versionUuid } = c.req.param()
const { path, parameters, stream: useSSE, __internal } = c.req.valid('json')
const {
path,
parameters,
customIdentifier,
stream: useSSE,
__internal,
} = c.req.valid('json')
const workspace = c.get('workspace')
const { document, commit } = await getData({
workspace,
Expand All @@ -43,6 +50,7 @@ export const runHandler = factory.createHandlers(
document,
commit,
parameters,
customIdentifier,
source: __internal?.source ?? LogSources.API,
}).then((r) => r.unwrap())

Expand Down
4 changes: 3 additions & 1 deletion apps/gateway/src/routes/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ROUTES from '$/common/routes'
import authMiddleware from '$/middlewares/auth'
import errorHandlerMiddleware from '$/middlewares/errorHandler'
import rateLimitMiddleware from '$/middlewares/rateLimit'
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import jetPaths from 'jet-paths'
Expand All @@ -21,6 +22,7 @@ app.get('/health', (c) => {
return c.json({ status: 'ok' })
})

app.use(rateLimitMiddleware())
app.use(authMiddleware())

// Routers
Expand All @@ -30,6 +32,6 @@ app.route(jetPaths(ROUTES).Api.V2.Documents.Base, documentsRouterV2)
app.route(jetPaths(ROUTES).Api.V2.Conversations.Base, chatsRouterV2)

// Must be the last one!
app.use(errorHandlerMiddleware())
app.onError(errorHandlerMiddleware)

export default app
1 change: 0 additions & 1 deletion apps/web/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ ARG SENTRY_AUTH_TOKEN
FROM node:20-alpine AS alpine

# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk update
RUN apk add --no-cache libc6-compat curl

FROM alpine as base
Expand Down
14 changes: 10 additions & 4 deletions apps/web/src/actions/commits/deleteDraftCommit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
type User,
type Workspace,
} from '@latitude-data/core/browser'
import { database } from '@latitude-data/core/client'
import * as factories from '@latitude-data/core/factories'
import { DocumentVersionsRepository } from '@latitude-data/core/repositories'
import { deleteDraftCommitAction } from '$/actions/commits/deleteDraftCommitAction'
import { beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down Expand Up @@ -102,11 +102,15 @@ describe('deleteDraftCommitAction', () => {
id: draft.id,
})

expect(data).toEqual(draft)
expect(data?.id).toEqual(draft.id)
})

it('deletes associated documents with draft commit', async () => {
const before = await database.query.documentVersions.findMany()
const documentVersionsScope = new DocumentVersionsRepository(workspace.id)

const before = await documentVersionsScope
.findAll()
.then((r) => r.unwrap())

const { commit: draft } = await factories.createDraft({
project,
Expand Down Expand Up @@ -136,7 +140,9 @@ describe('deleteDraftCommitAction', () => {
id: draft.id,
})

const after = await database.query.documentVersions.findMany()
const after = await documentVersionsScope
.findAll()
.then((r) => r.unwrap())

expect(after.length - before.length).toEqual(1)
})
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/actions/evaluations/destroy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('destroyEvaluationAction', () => {
expect(error!.message).toEqual('Evaluation not found')
})

it('should throw a user-friendly error when trying to delete an evaluation connected to a document', async () => {
it('works even when trying to delete an evaluation connected to a document', async () => {
const { documents } = await factories.createProject({
workspace,
documents: {
Expand All @@ -130,12 +130,10 @@ describe('destroyEvaluationAction', () => {
documentUuid: document.documentUuid,
})

const [_, error] = await destroyEvaluationAction({ id: evaluation.id })
const [data, error] = await destroyEvaluationAction({ id: evaluation.id })

expect(error).not.toBeNull()
expect(error!.message).toEqual(
'Cannot delete evaluation because it is still used in at least one project',
)
expect(error).toBeNull()
expect(data?.id).toEqual(evaluation.id)
})
})
})
11 changes: 11 additions & 0 deletions apps/web/src/app/(private)/_lib/getRunErrorFromErrorable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { RunErrorField } from '@latitude-data/core/repositories'

export function getRunErrorFromErrorable(error: RunErrorField) {
if (!error.code || !error.message) return null

return {
code: error.code!,
message: error.message!,
details: error.details,
}
}
Loading

0 comments on commit dfeea9a

Please sign in to comment.