chore: add lint rule and format (#367)

* chore: add lint rule and format

* fix ci

* add typescript

* yarn install

* add `@cloudflare/workers-types`
pull/373/head
Yusuke Wada 2024-01-29 22:53:43 +09:00 committed by GitHub
parent fce4d55b9a
commit e8b494b207
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 5164 additions and 189 deletions

View File

@ -21,8 +21,5 @@ jobs:
with:
node-version: 18.x
- run: yarn install --frozen-lockfile
- name: Build zod-validator in root directory
run: yarn build:zod-validator
working-directory: .
- run: yarn build
- run: yarn test

View File

@ -13,7 +13,7 @@ export class ClientSessionError extends AuthError {}
export interface AuthClientConfig {
baseUrl: string
basePath: string
credentials?: RequestCredentials,
credentials?: RequestCredentials
/** Stores last session response */
_session?: Session | null | undefined
/** Used for timestamp since last sycned (in seconds) */
@ -33,9 +33,7 @@ export interface UseSessionOptions<R extends boolean> {
// Util type that matches some strings literally, but allows any other string as well.
// @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611
export type LiteralUnion<T extends U, U = string> =
| T
| (U & Record<never, never>)
export type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>)
export interface ClientSafeProvider {
id: LiteralUnion<BuiltInProviderType>
@ -137,7 +135,9 @@ export async function fetchData<T = any>(
const res = await fetch(url, options)
const data = await res.json()
if (!res.ok) throw data
if (!res.ok) {
throw data
}
return data as T
} catch (error) {
logger.error(new ClientFetchError((error as Error).message, error as any))
@ -211,4 +211,4 @@ export function parseUrl(url?: string): {
base,
toString: () => base,
}
}
}

View File

@ -68,8 +68,7 @@ export async function getAuthUser(c: Context): Promise<AuthUser | null> {
...config.callbacks,
async session(...args) {
authUser = args[0]
const session =
(await config.callbacks?.session?.(...args)) ?? args[0].session
const session = (await config.callbacks?.session?.(...args)) ?? args[0].session
const user = args[0].user ?? args[0].token
return { user, ...session } satisfies Session
},
@ -90,7 +89,9 @@ export function verifyAuth(): MiddlewareHandler {
status: 401,
})
throw new HTTPException(401, { res })
} else c.set('authUser', authUser)
} else {
c.set('authUser', authUser)
}
await next()
}

View File

@ -33,7 +33,7 @@ class AuthConfigManager {
_config: AuthClientConfig = {
baseUrl: parseUrl(window.location.origin).origin,
basePath: parseUrl(window.location.origin).path,
credentials:'same-origin',
credentials: 'same-origin',
_lastSync: 0,
_session: undefined,
_getSession: () => {},
@ -112,8 +112,11 @@ export function useSession<R extends boolean>(
error: 'SessionRequired',
callbackUrl: window.location.href,
})}`
if (onUnauthenticated) onUnauthenticated()
else window.location.href = url
if (onUnauthenticated) {
onUnauthenticated()
} else {
window.location.href = url
}
}
}, [requiredAndNotLoading, onUnauthenticated])
@ -153,7 +156,11 @@ export async function getSession(params?: GetSessionParams) {
* @internal
*/
export async function getCsrfToken() {
const response = await fetchData<{ csrfToken: string }>('csrf', authConfigManager.getConfig(), logger)
const response = await fetchData<{ csrfToken: string }>(
'csrf',
authConfigManager.getConfig(),
logger
)
return response?.csrfToken ?? ''
}
@ -215,7 +222,9 @@ export async function signIn<P extends RedirectableProviderType | undefined = un
const url = (data as any).url ?? callbackUrl
window.location.href = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
if (url.includes('#')) {
window.location.reload()
}
return
}
@ -261,7 +270,9 @@ export async function signOut<R extends boolean = true>(
const url = (data as any).url ?? callbackUrl
window.location.href = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
if (url.includes('#')) {
window.location.reload()
}
// @ts-expect-error TODO: Fix this
return
}
@ -285,18 +296,19 @@ export function SessionProvider(props: SessionProviderProps) {
__AUTHJS._lastSync = hasInitialSession ? now() : 0
const [session, setSession] = React.useState(() => {
if (hasInitialSession) __AUTHJS._session = props.session
if (hasInitialSession) {
__AUTHJS._session = props.session
}
return props.session
})
const [loading, setLoading] = React.useState(!hasInitialSession)
React.useEffect(() => {
__AUTHJS._getSession = async ({ event } = {}) => {
try {
const storageEvent = event === 'storage'
if (storageEvent || __AUTHJS._session === undefined) {
__AUTHJS._lastSync = now()
__AUTHJS._session = await getSession({
@ -363,8 +375,9 @@ export function SessionProvider(props: SessionProviderProps) {
// and makes our tab visible again, re-fetch the session, but only if
// this feature is not disabled.
const visibilityHandler = () => {
if (refetchOnWindowFocus && document.visibilityState === 'visible')
if (refetchOnWindowFocus && document.visibilityState === 'visible') {
__AUTHJS._getSession({ event: 'visibilitychange' })
}
}
document.addEventListener('visibilitychange', visibilityHandler, false)
return () => document.removeEventListener('visibilitychange', visibilityHandler, false)
@ -390,7 +403,9 @@ export function SessionProvider(props: SessionProviderProps) {
data: session,
status: loading ? 'loading' : session ? 'authenticated' : 'unauthenticated',
async update(data: any) {
if (loading || !session) return
if (loading || !session) {
return
}
setLoading(true)
const newSession = await fetchData<Session>(
'session',
@ -415,4 +430,4 @@ export function SessionProvider(props: SessionProviderProps) {
)
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
}
}

View File

@ -1,12 +1,11 @@
import {webcrypto} from 'node:crypto'
import { webcrypto } from 'node:crypto'
import { Hono } from 'hono'
import { describe, expect, it } from 'vitest'
import { authHandler, verifyAuth,initAuthConfig} from '../src'
import { authHandler, verifyAuth, initAuthConfig } from '../src'
// @ts-expect-error - global crypto
//needed for node 18 and below but should work in node 20 and above
global.crypto = webcrypto
global.crypto = webcrypto
describe('Auth.js Adapter Middleware', () => {
it('Should return 500 if AUTH_SECRET is missing', async () => {
@ -37,7 +36,7 @@ describe('Auth.js Adapter Middleware', () => {
const app = new Hono()
app.use('/*', (c, next) => {
c.env = {'AUTH_SECRET':'secret'}
c.env = { AUTH_SECRET: 'secret' }
return next()
})
@ -60,7 +59,7 @@ describe('Auth.js Adapter Middleware', () => {
const app = new Hono()
app.use('/*', (c, next) => {
c.env = {'AUTH_SECRET':'secret'}
c.env = { AUTH_SECRET: 'secret' }
return next()
})
@ -77,7 +76,7 @@ describe('Auth.js Adapter Middleware', () => {
app.use('/api/auth/*', authHandler())
app.get('/api/protected', (c)=> c.text('protected'))
app.get('/api/protected', (c) => c.text('protected'))
const req = new Request('http://localhost/api/protected')
const res = await app.request(req)
expect(res.status).toBe(401)

View File

@ -39,5 +39,8 @@
"hono": "^3.11.7",
"tsup": "^8.0.1",
"vitest": "^1.0.4"
},
"engines": {
"node": ">=18.14.1"
}
}

View File

@ -23,7 +23,9 @@ export const bunTranspiler = (options?: BunTranspilerOptions) => {
const extensions = options?.extensions ?? defaultOptions.extensions
const headers = options?.headers ?? defaultOptions.headers
if (extensions?.every((ext) => !url.pathname.endsWith(ext))) return
if (extensions?.every((ext) => !url.pathname.endsWith(ext))) {
return
}
try {
const loader = url.pathname.split('.').pop() as Bun.TranspilerOptions['loader']

View File

@ -25,7 +25,9 @@ export const esbuildTranspiler = (options?: EsbuildTranspilerOptions) => {
const url = new URL(c.req.url)
const extensions = options?.extensions ?? ['.ts', '.tsx']
if (extensions.every((ext) => !url.pathname.endsWith(ext))) return
if (extensions.every((ext) => !url.pathname.endsWith(ext))) {
return
}
const headers = { 'content-type': options?.contentType ?? 'text/javascript' }
const script = await c.res.text()

View File

@ -85,6 +85,8 @@ const setFirebaseToken = (c: Context, idToken: FirebaseIdToken) => c.set(idToken
export const getFirebaseToken = (c: Context): FirebaseIdToken | null => {
const idToken = c.get(idTokenContextKey)
if (!idToken) return null
if (!idToken) {
return null
}
return idToken
}

View File

@ -96,7 +96,9 @@ const TestSchema = new GraphQLSchema({
const urlString = (query?: Record<string, string>): string => {
const base = 'http://localhost/graphql'
if (!query) return base
if (!query) {
return base
}
const queryString = new URLSearchParams(query).toString()
return `${base}?${queryString}`
}

View File

@ -159,7 +159,9 @@ const TestSchema = new GraphQLSchema({
const urlString = (query?: Record<string, string>): string => {
const base = 'http://localhost/graphql'
if (!query) return base
if (!query) {
return base
}
const queryString = new URLSearchParams(query).toString()
return `${base}?${queryString}`
}

View File

@ -84,10 +84,15 @@ export class AuthFlow {
body: parsedOptions,
}).then((res) => res.json())) as DiscordTokenResponse | DiscordErrorResponse
if ('error_description' in response)
if ('error_description' in response) {
throw new HTTPException(400, { message: response.error_description })
if ('error' in response) throw new HTTPException(400, { message: response.error })
if ('message' in response) throw new HTTPException(400, { message: response.message })
}
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('message' in response) {
throw new HTTPException(400, { message: response.message })
}
if ('access_token' in response) {
this.token = {
@ -103,7 +108,9 @@ export class AuthFlow {
}
}
if ('scope' in response) this.granted_scopes = response.scope.split(' ')
if ('scope' in response) {
this.granted_scopes = response.scope.split(' ')
}
}
async getUserData() {
@ -114,11 +121,18 @@ export class AuthFlow {
},
}).then((res) => res.json())) as DiscordMeResponse | DiscordErrorResponse
if ('error_description' in response)
if ('error_description' in response) {
throw new HTTPException(400, { message: response.error_description })
if ('error' in response) throw new HTTPException(400, { message: response.error })
if ('message' in response) throw new HTTPException(400, { message: response.message })
}
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
if ('message' in response) {
throw new HTTPException(400, { message: response.message })
}
if ('user' in response) this.user = response.user
if ('user' in response) {
this.user = response.user
}
}
}

View File

@ -22,7 +22,9 @@ export async function refreshToken(
body: params,
}).then((res) => res.json())) as DiscordTokenResponse | { error: string }
if ('error' in response) throw new HTTPException(400, { message: response.error })
if ('error' in response) {
throw new HTTPException(400, { message: response.error })
}
return response
}

View File

@ -21,7 +21,9 @@ export async function revokeToken(
body: params,
})
if (response.status !== 200) throw new HTTPException(400, { message: 'Something went wrong' })
if (response.status !== 200) {
throw new HTTPException(400, { message: 'Something went wrong' })
}
return true
}

View File

@ -78,7 +78,9 @@ export class AuthFlow {
| FacebookTokenResponse
| FacebookErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
if ('error' in response) {
throw new HTTPException(400, { message: response.error?.message })
}
if ('access_token' in response) {
this.token = {
@ -93,9 +95,13 @@ export class AuthFlow {
`https://graph.facebook.com/v18.0/me?access_token=${this.token?.token}`
).then((res) => res.json())) as FacebookMeResponse | FacebookErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
if ('error' in response) {
throw new HTTPException(400, { message: response.error?.message })
}
if ('id' in response) this.user = response
if ('id' in response) {
this.user = response
}
}
async getUserData() {
@ -107,8 +113,12 @@ export class AuthFlow {
`https://graph.facebook.com/${this.user?.id}?fields=${parsedFields}&access_token=${this.token?.token}`
).then((res) => res.json())) as FacebookUser | FacebookErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
if ('error' in response) {
throw new HTTPException(400, { message: response.error?.message })
}
if ('id' in response) this.user = response
if ('id' in response) {
this.user = response
}
}
}

View File

@ -65,8 +65,9 @@ export class AuthFlow {
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
}).then((res) => res.json())) as GitHubTokenResponse | GitHubErrorResponse
if ('error_description' in response)
if ('error_description' in response) {
throw new HTTPException(400, { message: response.error_description })
}
if ('access_token' in response) {
this.token = {
@ -85,7 +86,9 @@ export class AuthFlow {
}
async getUserData() {
if (!this.token?.token) await this.getTokenFromCode()
if (!this.token?.token) {
await this.getTokenFromCode()
}
const response = (await fetch('https://api.github.com/user', {
headers: {
@ -96,7 +99,9 @@ export class AuthFlow {
},
}).then((res) => res.json())) as GitHubUser | GitHubErrorResponse
if ('message' in response) throw new HTTPException(400, { message: response.message })
if ('message' in response) {
throw new HTTPException(400, { message: response.message })
}
if ('id' in response) {
this.user = response

View File

@ -91,7 +91,9 @@ export class AuthFlow {
}),
}).then((res) => res.json())) as GoogleTokenResponse | GoogleErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
if ('error' in response) {
throw new HTTPException(400, { message: response.error_description })
}
if ('access_token' in response) {
this.token = {
@ -112,8 +114,12 @@ export class AuthFlow {
},
}).then((res) => res.json())) as GoogleUser | GoogleErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error?.message })
if ('error' in response) {
throw new HTTPException(400, { message: response.error?.message })
}
if ('id' in response) this.user = response
if ('id' in response) {
this.user = response
}
}
}

View File

@ -79,7 +79,9 @@ export class AuthFlow {
},
}).then((res) => res.json())) as LinkedInTokenResponse | LinkedInErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
if ('error' in response) {
throw new HTTPException(400, { message: response.error_description })
}
if ('access_token' in response) {
this.token = {
@ -106,9 +108,13 @@ export class AuthFlow {
},
}).then((res) => res.json())) as LinkedInUser | LinkedInErrorResponse
if ('message' in response) throw new HTTPException(400, { message: response.message })
if ('message' in response) {
throw new HTTPException(400, { message: response.message })
}
if ('sub' in response) this.user = response
if ('sub' in response) {
this.user = response
}
}
async getAppToken() {
@ -122,7 +128,9 @@ export class AuthFlow {
(res) => res.json()
)) as LinkedInTokenResponse | LinkedInErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
if ('error' in response) {
throw new HTTPException(400, { message: response.error_description })
}
if ('access_token' in response) {
this.token = {

View File

@ -93,7 +93,9 @@ export class AuthFlow {
},
}).then((res) => res.json())) as XTokenResponse | XErrorResponse
if ('error' in response) throw new HTTPException(400, { message: response.error_description })
if ('error' in response) {
throw new HTTPException(400, { message: response.error_description })
}
if ('access_token' in response) {
this.token = {
@ -122,9 +124,12 @@ export class AuthFlow {
},
}).then((res) => res.json())) as XMeResponse | XErrorResponse
if ('error_description' in response)
if ('error_description' in response) {
throw new HTTPException(400, { message: response.error_description })
}
if ('data' in response) this.user = response.data
if ('data' in response) {
this.user = response.data
}
}
}

View File

@ -21,8 +21,9 @@ export async function refreshToken(
},
}).then((res) => res.json())) as XTokenResponse | XErrorResponse
if ('error_description' in response)
if ('error_description' in response) {
throw new HTTPException(400, { message: response.error_description })
}
return response
}

View File

@ -21,8 +21,9 @@ export async function revokeToken(
},
}).then((res) => res.json())) as XRevokeResponse | XErrorResponse
if ('error_description' in response)
if ('error_description' in response) {
throw new HTTPException(400, { message: response.error_description })
}
return response.revoked
}

View File

@ -1,6 +1,7 @@
import type { DefaultBodyType, StrictResponse } from 'msw'
import { HttpResponse, http } from 'msw'
import type { DiscordErrorResponse, DiscordTokenResponse } from '../src/providers/discord'
import type {
FacebookErrorResponse,
FacebookTokenResponse,
@ -14,7 +15,6 @@ import type {
} from '../src/providers/google/types'
import type { LinkedInErrorResponse, LinkedInTokenResponse } from '../src/providers/linkedin'
import type { XErrorResponse, XRevokeResponse, XTokenResponse } from '../src/providers/x'
import { DiscordErrorResponse, DiscordTokenResponse } from '../src/providers/discord'
export const handlers = [
// Google

View File

@ -1,5 +1,11 @@
import { Hono } from 'hono'
import { setupServer } from 'msw/node'
import type { DiscordUser } from '../src/providers/discord'
import {
refreshToken as discordRefresh,
revokeToken as discordRevoke,
discordAuth,
} from '../src/providers/discord'
import { facebookAuth } from '../src/providers/facebook'
import type { FacebookUser } from '../src/providers/facebook'
import { githubAuth } from '../src/providers/github'
@ -38,13 +44,6 @@ import {
discordRefreshTokenError,
} from './handlers'
import {
refreshToken as discordRefresh,
revokeToken as discordRevoke,
discordAuth,
DiscordUser,
} from '../src/providers/discord'
const server = setupServer(...handlers)
server.listen()

View File

@ -1,5 +1,5 @@
import { Hono } from 'hono'
import type { Histogram} from 'prom-client'
import type { Histogram } from 'prom-client'
import { Registry } from 'prom-client'
import { prometheus } from './index'
@ -7,9 +7,12 @@ describe('Prometheus middleware', () => {
const app = new Hono()
const registry = new Registry()
app.use('*', prometheus({
registry,
}).registerMetrics)
app.use(
'*',
prometheus({
registry,
}).registerMetrics
)
app.get('/', (c) => c.text('hello'))
app.get('/user/:id', (c) => c.text(c.req.param('id')))
@ -21,10 +24,13 @@ describe('Prometheus middleware', () => {
const app = new Hono()
const registry = new Registry()
app.use('*', prometheus({
registry,
prefix: 'myprefix_',
}).registerMetrics)
app.use(
'*',
prometheus({
registry,
prefix: 'myprefix_',
}).registerMetrics
)
expect(await registry.metrics()).toMatchInlineSnapshot(`
"# HELP myprefix_http_request_duration_seconds Duration of HTTP requests in seconds
@ -40,17 +46,20 @@ describe('Prometheus middleware', () => {
const app = new Hono()
const registry = new Registry()
app.use('*', prometheus({
registry,
metricOptions: {
requestsTotal: {
customLabels: {
id: (c) => c.req.query('id') ?? 'unknown',
contentType: (c) => c.res.headers.get('content-type') ?? 'unknown',
}
app.use(
'*',
prometheus({
registry,
metricOptions: {
requestsTotal: {
customLabels: {
id: (c) => c.req.query('id') ?? 'unknown',
contentType: (c) => c.res.headers.get('content-type') ?? 'unknown',
},
},
},
}
}).registerMetrics)
}).registerMetrics
)
app.get('/', (c) => c.text('hello'))
@ -68,9 +77,9 @@ describe('Prometheus middleware', () => {
describe('http_requests_total', () => {
it('increments the http_requests_total metric with the correct labels on successful responses', async () => {
await app.request('http://localhost/')
const { values } = await registry.getSingleMetric('http_requests_total')!.get()!
expect(values).toEqual([
{
labels: {
@ -80,15 +89,15 @@ describe('Prometheus middleware', () => {
ok: 'true',
},
value: 1,
}
},
])
})
it('increments the http_requests_total metric with the correct labels on errors', async () => {
await app.request('http://localhost/notfound')
const { values } = await registry.getSingleMetric('http_requests_total')!.get()!
expect(values).toEqual([
{
labels: {
@ -98,19 +107,22 @@ describe('Prometheus middleware', () => {
ok: 'false',
},
value: 1,
}
},
])
})
})
describe('http_requests_duration', () => {
it('updates the http_requests_duration metric with the correct labels on successful responses', async () => {
await app.request('http://localhost/')
const { values } = await (registry.getSingleMetric('http_request_duration_seconds') as Histogram)!.get()!
const { values } = await (registry.getSingleMetric(
'http_request_duration_seconds'
) as Histogram)!.get()!
const countMetric = values.find(
(v) => v.metricName === 'http_request_duration_seconds_count' &&
(v) =>
v.metricName === 'http_request_duration_seconds_count' &&
v.labels.method === 'GET' &&
v.labels.route === '/' &&
v.labels.status === '200'
@ -118,19 +130,22 @@ describe('Prometheus middleware', () => {
expect(countMetric?.value).toBe(1)
})
it('updates the http_requests_duration metric with the correct labels on errors', async () => {
await app.request('http://localhost/notfound')
const { values } = await (registry.getSingleMetric('http_request_duration_seconds') as Histogram)!.get()!
const { values } = await (registry.getSingleMetric(
'http_request_duration_seconds'
) as Histogram)!.get()!
const countMetric = values.find(
(v) => v.metricName === 'http_request_duration_seconds_count' &&
(v) =>
v.metricName === 'http_request_duration_seconds_count' &&
v.labels.method === 'GET' &&
v.labels.route === '/*' &&
v.labels.status === '404'
)
expect(countMetric?.value).toBe(1)
})
})
@ -146,8 +161,8 @@ describe('Prometheus middleware', () => {
metricOptions: {
requestDuration: {
disabled: true, // Disable duration metrics to make the test result more predictable
}
}
},
},
})
app.use('*', registerMetrics)

View File

@ -2,19 +2,20 @@ import type { Context } from 'hono'
import { createMiddleware } from 'hono/factory'
import type { DefaultMetricsCollectorConfiguration, RegistryContentType } from 'prom-client'
import { Registry, collectDefaultMetrics as promCollectDefaultMetrics } from 'prom-client'
import { type MetricOptions, type CustomMetricsOptions, createStandardMetrics } from './standardMetrics'
import {
type MetricOptions,
type CustomMetricsOptions,
createStandardMetrics,
} from './standardMetrics'
interface PrometheusOptions {
registry?: Registry;
collectDefaultMetrics?: boolean | DefaultMetricsCollectorConfiguration<RegistryContentType>;
prefix?: string;
metricOptions?: Omit<CustomMetricsOptions, 'prefix' | 'register'>;
registry?: Registry
collectDefaultMetrics?: boolean | DefaultMetricsCollectorConfiguration<RegistryContentType>
prefix?: string
metricOptions?: Omit<CustomMetricsOptions, 'prefix' | 'register'>
}
const evaluateCustomLabels = (
customLabels: MetricOptions['customLabels'],
context: Context,
) => {
const evaluateCustomLabels = (customLabels: MetricOptions['customLabels'], context: Context) => {
const labels: Record<string, string> = {}
for (const [key, fn] of Object.entries(customLabels ?? {})) {
@ -36,7 +37,7 @@ export const prometheus = (options?: PrometheusOptions) => {
promCollectDefaultMetrics({
prefix,
register: registry,
...(typeof collectDefaultMetrics === 'object' && collectDefaultMetrics)
...(typeof collectDefaultMetrics === 'object' && collectDefaultMetrics),
})
}
@ -50,10 +51,9 @@ export const prometheus = (options?: PrometheusOptions) => {
printMetrics: async (c: Context) => c.text(await registry.metrics()),
registerMetrics: createMiddleware(async (c, next) => {
const timer = metrics.requestDuration?.startTimer()
try {
await next()
} finally {
const commonLabels = {
method: c.req.method,
@ -64,14 +64,14 @@ export const prometheus = (options?: PrometheusOptions) => {
timer?.({
...commonLabels,
...evaluateCustomLabels(metricOptions?.requestDuration?.customLabels, c)
...evaluateCustomLabels(metricOptions?.requestDuration?.customLabels, c),
})
metrics.requestsTotal?.inc({
...commonLabels,
...evaluateCustomLabels(metricOptions?.requestsTotal?.customLabels, c)
...evaluateCustomLabels(metricOptions?.requestsTotal?.customLabels, c),
})
}
})
}),
}
}

View File

@ -3,11 +3,11 @@ import type { CounterConfiguration, HistogramConfiguration, Metric } from 'prom-
import { Counter, Histogram, type Registry } from 'prom-client'
export type MetricOptions = {
disabled?: boolean;
customLabels?: Record<string, (c: Context) => string>;
disabled?: boolean
customLabels?: Record<string, (c: Context) => string>
} & (
({ type: 'counter' } & CounterConfiguration<string>) |
({ type: 'histogram' } & HistogramConfiguration<string>)
| ({ type: 'counter' } & CounterConfiguration<string>)
| ({ type: 'histogram' } & HistogramConfiguration<string>)
)
const standardMetrics = {
@ -18,7 +18,7 @@ const standardMetrics = {
labelNames: ['method', 'status', 'ok', 'route'],
// OpenTelemetry recommendation for histogram buckets of http request duration:
// https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
buckets: [ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 ],
buckets: [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10],
},
requestsTotal: {
type: 'counter',
@ -35,22 +35,25 @@ export type CustomMetricsOptions = {
}
type CreatedMetrics = {
[Name in MetricName]: (typeof standardMetrics)[Name]['type'] extends 'counter' ? Counter<string> : Histogram<string>
[Name in MetricName]: (typeof standardMetrics)[Name]['type'] extends 'counter'
? Counter<string>
: Histogram<string>
}
const getMetricConstructor = (type: MetricOptions['type']) => ({
counter: Counter,
histogram: Histogram,
})[type]
const getMetricConstructor = (type: MetricOptions['type']) =>
({
counter: Counter,
histogram: Histogram,
}[type])
export const createStandardMetrics = ({
registry,
prefix = '',
customOptions,
} : {
registry: Registry;
prefix?: string;
customOptions?: CustomMetricsOptions;
}: {
registry: Registry
prefix?: string
customOptions?: CustomMetricsOptions
}) => {
const createdMetrics: Record<string, Metric> = {}
@ -70,11 +73,12 @@ export const createStandardMetrics = ({
...(opts as object),
name: `${prefix}${opts.name}`,
help: opts.help,
registers: [...opts.registers ?? [], registry],
labelNames: [...opts.labelNames ?? [], ...Object.keys(opts.customLabels ?? {})],
...(opts.type === 'histogram' && opts.buckets && {
buckets: opts.buckets,
}),
registers: [...(opts.registers ?? []), registry],
labelNames: [...(opts.labelNames ?? []), ...Object.keys(opts.customLabels ?? {})],
...(opts.type === 'histogram' &&
opts.buckets && {
buckets: opts.buckets,
}),
})
}

View File

@ -42,7 +42,9 @@ export const sentry = (
...options,
})
c.set('sentry', sentry)
if (callback) callback(sentry)
if (callback) {
callback(sentry)
}
await next()
if (c.error) {

View File

@ -58,7 +58,9 @@ export const renderSwaggerUIOptions = (options: DistSwaggerUIOptions) => {
return `${key}: '${v}'`
}
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.STRING_ARRAY) {
if (!Array.isArray(v)) return ''
if (!Array.isArray(v)) {
return ''
}
return `${key}: [${v.map((ve) => `${ve}`).join(',')}]`
}
if (RENDER_TYPE_MAP[key] === RENDER_TYPE.JSON_STRING) {

View File

@ -1,39 +1,37 @@
/*eslint quotes: ["off", "single"]*/
import { renderSwaggerUIOptions } from '../src/swagger/renderer'
describe('SwaggerUIOption Rendering', () => {
it('renders correctly with configUrl', () => {
it('renders correctly with configUrl', () =>
expect(
renderSwaggerUIOptions({
configUrl: 'https://petstore3.swagger.io/api/v3/openapi.json',
})
).toEqual('configUrl: \'https://petstore3.swagger.io/api/v3/openapi.json\'')
})
).toEqual("configUrl: 'https://petstore3.swagger.io/api/v3/openapi.json'"))
it('renders correctly with presets', () => {
it('renders correctly with presets', () =>
expect(
renderSwaggerUIOptions({
presets: ['SwaggerUIBundle.presets.apis', 'SwaggerUIStandalonePreset'],
})
).toEqual('presets: [SwaggerUIBundle.presets.apis,SwaggerUIStandalonePreset]')
})
).toEqual('presets: [SwaggerUIBundle.presets.apis,SwaggerUIStandalonePreset]'))
it('renders correctly with plugins', () => {
it('renders correctly with plugins', () =>
expect(
renderSwaggerUIOptions({
plugins: ['SwaggerUIBundle.plugins.DownloadUrl'],
})
).toEqual('plugins: [SwaggerUIBundle.plugins.DownloadUrl]')
})
).toEqual('plugins: [SwaggerUIBundle.plugins.DownloadUrl]'))
it('renders correctly with deepLinking', () => {
it('renders correctly with deepLinking', () =>
expect(
renderSwaggerUIOptions({
deepLinking: true,
})
).toEqual('deepLinking: true')
})
).toEqual('deepLinking: true'))
it('renders correctly with spec', () => {
it('renders correctly with spec', () =>
expect(
renderSwaggerUIOptions({
spec: {
@ -51,15 +49,14 @@ describe('SwaggerUIOption Rendering', () => {
})
).toEqual(
'spec: {"openapi":"3.0.0","info":{"title":"Swagger Petstore","version":"1.0.0"},"servers":[{"url":"https://petstore3.swagger.io/api/v3"}]}'
)
})
))
it('renders correctly with url', () => {
expect(
renderSwaggerUIOptions({
url: 'https://petstore3.swagger.io/api/v3/openapi.json',
})
).toEqual('url: \'https://petstore3.swagger.io/api/v3/openapi.json\'')
).toEqual("url: 'https://petstore3.swagger.io/api/v3/openapi.json'")
})
it('renders correctly with urls', () => {
@ -77,59 +74,52 @@ describe('SwaggerUIOption Rendering', () => {
)
})
it('renders correctly with layout', () => {
it('renders correctly with layout', () =>
expect(
renderSwaggerUIOptions({
layout: 'StandaloneLayout',
})
).toEqual('layout: \'StandaloneLayout\'')
})
).toEqual("layout: 'StandaloneLayout'"))
it('renders correctly with docExpansion', () => {
it('renders correctly with docExpansion', () =>
expect(
renderSwaggerUIOptions({
docExpansion: 'list',
})
).toEqual('docExpansion: \'list\'')
})
).toEqual("docExpansion: 'list'"))
it('renders correctly with maxDisplayedTags', () => {
it('renders correctly with maxDisplayedTags', () =>
expect(
renderSwaggerUIOptions({
maxDisplayedTags: 5,
})
).toEqual('maxDisplayedTags: 5')
})
).toEqual('maxDisplayedTags: 5'))
it('renders correctly with operationsSorter', () => {
it('renders correctly with operationsSorter', () =>
expect(
renderSwaggerUIOptions({
operationsSorter: '(a, b) => a.path.localeCompare(b.path)',
})
).toEqual('operationsSorter: (a, b) => a.path.localeCompare(b.path)')
})
).toEqual('operationsSorter: (a, b) => a.path.localeCompare(b.path)'))
it('renders correctly with requestInterceptor', () => {
it('renders correctly with requestInterceptor', () =>
expect(
renderSwaggerUIOptions({
requestInterceptor: '(req) => req',
})
).toEqual('requestInterceptor: (req) => req')
})
).toEqual('requestInterceptor: (req) => req'))
it('renders correctly with responseInterceptor', () => {
it('renders correctly with responseInterceptor', () =>
expect(
renderSwaggerUIOptions({
responseInterceptor: '(res) => res',
})
).toEqual('responseInterceptor: (res) => res')
})
).toEqual('responseInterceptor: (res) => res'))
it('renders correctly with persistAuthorization', () => {
it('renders correctly with persistAuthorization', () =>
expect(
renderSwaggerUIOptions({
persistAuthorization: true,
})
).toEqual('persistAuthorization: true')
})
).toEqual('persistAuthorization: true'))
})

Binary file not shown.

View File

@ -41,16 +41,18 @@
"zod": "3.*"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240117.0",
"hono": "^3.11.7",
"jest": "^29.7.0",
"openapi3-ts": "^4.1.2",
"tsup": "^8.0.1",
"typescript": "^5.3.3",
"vitest": "^1.0.1",
"zod": "^3.22.1"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^5.5.0",
"@hono/zod-validator": "^0.1.11"
"@hono/zod-validator": "0.1.11"
},
"engines": {
"node": ">=16.0.0"

File diff suppressed because it is too large Load Diff

View File

@ -876,6 +876,13 @@ __metadata:
languageName: node
linkType: hard
"@cloudflare/workers-types@npm:^4.20240117.0":
version: 4.20240117.0
resolution: "@cloudflare/workers-types@npm:4.20240117.0"
checksum: 900b796af2ae97257e1f6171b9c37d718c5f8ae064cea8f8d1c48e52ccde01492c5cfa61716fc703d05a6bd92e50a55f7d9e525d2c91919db76e42458c3e8e76
languageName: node
linkType: hard
"@colors/colors@npm:1.5.0":
version: 1.5.0
resolution: "@colors/colors@npm:1.5.0"
@ -1688,11 +1695,13 @@ __metadata:
resolution: "@hono/zod-openapi@workspace:packages/zod-openapi"
dependencies:
"@asteasolutions/zod-to-openapi": "npm:^5.5.0"
"@hono/zod-validator": "npm:^0.1.11"
"@cloudflare/workers-types": "npm:^4.20240117.0"
"@hono/zod-validator": "npm:0.1.11"
hono: "npm:^3.11.7"
jest: "npm:^29.7.0"
openapi3-ts: "npm:^4.1.2"
tsup: "npm:^8.0.1"
typescript: "npm:^5.3.3"
vitest: "npm:^1.0.1"
zod: "npm:^3.22.1"
peerDependencies:
@ -1701,7 +1710,7 @@ __metadata:
languageName: unknown
linkType: soft
"@hono/zod-validator@npm:^0.1.11, @hono/zod-validator@workspace:packages/zod-validator":
"@hono/zod-validator@npm:0.1.11, @hono/zod-validator@workspace:packages/zod-validator":
version: 0.0.0-use.local
resolution: "@hono/zod-validator@workspace:packages/zod-validator"
dependencies: