galbe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { Route, RouteNode, RouteTree } from './types'
2
+ import { NotFoundError } from './types'
3
+
4
+ const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d][\w-]+[\w\d]))*\/?$/
5
+
6
+ const walkRoutes = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
7
+ if (path.length < 1) throw new NotFoundError()
8
+ if (path.length === 1 && node.route) return node
9
+
10
+ if (node.children?.['*']) alts.push(node.children['*'])
11
+ if (node.param) alts.push(node.param)
12
+
13
+ if (node.children && path[1] in node.children) {
14
+ path.shift()
15
+ return walkRoutes(path, node.children[path[0]], alts)
16
+ }
17
+ if (node.param) {
18
+ path.shift()
19
+ alts.pop()
20
+ try {
21
+ return walkRoutes(path, node.param, alts)
22
+ } catch (error) {
23
+ if (error instanceof NotFoundError) console.log(error)
24
+ else throw error
25
+ }
26
+ }
27
+ if (alts.length > 1) {
28
+ return walkRoutes(path, alts.pop() as RouteNode, alts)
29
+ }
30
+ if (alts.length === 1) {
31
+ let lastAlt = alts.pop() as RouteNode
32
+ try {
33
+ return walkRoutes(path, lastAlt, alts)
34
+ } catch (error) {
35
+ if (error instanceof NotFoundError) {
36
+ if (lastAlt?.route) return lastAlt
37
+ } else throw error
38
+ }
39
+ }
40
+
41
+ throw new NotFoundError()
42
+ }
43
+
44
+ export class GalbeRouter {
45
+ routes: RouteTree
46
+ prefix: string
47
+ staticRoutes: Map<string, Route>
48
+ constructor(prefix?: string) {
49
+ this.routes = { GET: {}, POST: {}, PUT: {}, PATCH: {}, DELETE: {}, OPTIONS: {} }
50
+ prefix = prefix || ''
51
+ if (prefix && !prefix.match(/^\//)) prefix = `/${prefix}`
52
+ this.prefix = prefix
53
+ this.staticRoutes = new Map()
54
+ }
55
+ add(route: Route) {
56
+ route.path = route?.path?.[0] === '/' ? route.path : `/${route.path}`
57
+ if (!route.path.match(ROUTE_REGEX)) throw new SyntaxError(`${route.path} is not a valid route path.`)
58
+ const isStatic = !route.path.match(/(:[\w\d-]+|\*)/)
59
+ if (isStatic) this.staticRoutes.set(`[${route.method.toUpperCase()}]${route.path}`, route)
60
+ route.path = `${this.prefix || ''}${route.path}`
61
+ let path = route.path.replace(/^\/$(.*)\/?$/, '$1').split('/')
62
+ path.shift()
63
+ let r = this.routes[route.method.toUpperCase()]
64
+ if (!path.length) {
65
+ r.route = route
66
+ } else {
67
+ while (path.length) {
68
+ let p = path.shift()
69
+ if (p === undefined) break
70
+ if (!path.length) {
71
+ if (p.match(/^:/)) r.param = { route }
72
+ else {
73
+ if (!r.children) r.children = {}
74
+ r.children[p] = { ...r.children[p], route }
75
+ }
76
+ } else {
77
+ if (p.match(/^:/)) {
78
+ if (!r.param) r.param = {}
79
+ r = r.param
80
+ } else {
81
+ if (!r.children) r.children = {}
82
+ if (!(p in r.children)) r.children[p] = {}
83
+ r = r.children[p]
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ find(method: string, path: string) {
90
+ const staticRoute = this.staticRoutes.get(`[${method}]${path}`)
91
+ if (staticRoute !== undefined) return staticRoute
92
+ let parts = path
93
+ .replace(/\/+/g, '/')
94
+ .replace(/^\/$(.*)\/?$/, '$1')
95
+ .split('/')
96
+ const route = walkRoutes(parts, this.routes[method]).route
97
+ if (!route) throw new NotFoundError()
98
+ return route
99
+ }
100
+ }
package/src/routes.ts ADDED
@@ -0,0 +1,157 @@
1
+ import type { GalbeConfig } from './types'
2
+
3
+ import { readdir, lstat } from 'fs/promises'
4
+ import { extname } from 'path'
5
+ import { parse } from 'acorn'
6
+ import { simple } from 'acorn-walk'
7
+ import { Galbe } from './index'
8
+ import { transformSync } from '@swc/core'
9
+ import { Glob } from 'bun'
10
+
11
+ export type RouteMeta = {
12
+ header: Record<string, boolean | string | string[]>
13
+ routes: Record<string, Record<string, Record<string, boolean | string | string[]>>>
14
+ }
15
+
16
+ export type RouteFileMeta = {
17
+ file: string
18
+ } & RouteMeta
19
+
20
+ const parseComment = (comment: string): Record<string, string | string[]> => {
21
+ const head =
22
+ comment
23
+ .match(/^([^@]*)/)?.[1]
24
+ .replace(/^ *\* */gm, '')
25
+ .trim() || ''
26
+ const refs = {
27
+ ...(head ? { head } : {}),
28
+ ...[...comment.matchAll(new RegExp(`^\\s*\\*\\s*@([a-zA-Z_][0-9a-zA-Z_]*)(?:$|\\s+([^\\n]*)\\s*$)`, 'gm'))].reduce(
29
+ (acc, n) => {
30
+ return {
31
+ ...acc,
32
+ [n[1]]:
33
+ n[1] in acc ? [...(typeof acc[n[1]] === 'string' ? [acc[n[1]]] : acc[n[1]]), n[2] ?? true] : n[2] ?? true
34
+ }
35
+ },
36
+ {} as Record<string, any>
37
+ )
38
+ }
39
+ return refs
40
+ }
41
+ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
42
+ const file = Bun.file(filePath)
43
+ const fileExt = extname(filePath)
44
+ let content = await file.text()
45
+ let meta: RouteMeta = { header: {}, routes: {} }
46
+
47
+ if (fileExt === '.ts') {
48
+ //// Much faster but doesn't include comments. See https://github.com/oven-sh/bun/pull/7055
49
+ // content = new Bun.Transpiler({
50
+ // loader: 'ts',
51
+ // target: 'bun',
52
+ // tsconfig: {
53
+ // compilerOptions: {
54
+ // // @ts-ignore https://github.com/oven-sh/bun/pull/7055
55
+ // removeComments: false
56
+ // }
57
+ // }
58
+ // }).transformSync(content)
59
+ content = transformSync(content, {
60
+ jsc: {
61
+ parser: {
62
+ syntax: 'typescript'
63
+ },
64
+ preserveAllComments: true,
65
+ target: 'esnext'
66
+ }
67
+ }).code
68
+ }
69
+
70
+ const comments: Record<number, string> = {}
71
+
72
+ const ast = parse(content, {
73
+ ecmaVersion: 'latest',
74
+ sourceType: 'module',
75
+ locations: true,
76
+ onComment: (isBlock, text, _start, _end, _locStart, locEnd) => {
77
+ if (isBlock && locEnd?.line) comments[locEnd?.line] = text
78
+ }
79
+ })
80
+ simple(ast, {
81
+ ExportDefaultDeclaration(node) {
82
+ // @ts-ignore
83
+ let galbeIdentifier = node.declaration.params[0].name
84
+
85
+ const headerLine = node.loc?.start.line
86
+ const headerCom = headerLine !== undefined && comments?.[headerLine] ? comments[headerLine] : ''
87
+ const headerRef = parseComment(headerCom)
88
+ meta.header = headerRef
89
+ // @ts-ignore
90
+ simple(node.declaration.body, {
91
+ CallExpression(node) {
92
+ // @ts-ignore
93
+ if (node?.callee?.object?.name === galbeIdentifier) {
94
+ // @ts-ignore
95
+ const path = node.arguments[0].value
96
+ // @ts-ignore
97
+ const method = node.callee.property.name
98
+ const line = node.loc?.start.line
99
+ const com = line !== undefined && comments?.[line] ? comments[line] : ''
100
+
101
+ const routeRefs = parseComment(com)
102
+
103
+ if (!(path in meta.routes)) meta.routes[path] = {}
104
+ if (!(method in meta.routes[path])) meta.routes[path][method] = routeRefs
105
+ }
106
+ }
107
+ })
108
+ }
109
+ })
110
+ return meta
111
+ }
112
+
113
+ const importRoutes = async (filePath: string, galbe: Galbe) => {
114
+ const routes = (await import(filePath)).default
115
+ routes(galbe)
116
+ }
117
+
118
+ export const defineRoutes = async (options: GalbeConfig, galbe: Galbe) => {
119
+ const routes = options?.routes
120
+ if (!routes) {
121
+ console.log(`\x1b\[38;5;245m No route file defined\x1b[0m`)
122
+ return
123
+ }
124
+ const root = process.cwd()
125
+ if (typeof routes === 'string') {
126
+ let noRouteFound = true
127
+ for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false })) {
128
+ noRouteFound = false
129
+ const isDir = (await lstat(path)).isDirectory()
130
+
131
+ let files: string[] = []
132
+ if (!isDir) files.push(path)
133
+ else files = files.concat((await readdir(path)).map(f => `${path}/${f}`))
134
+ if (files.length === 0) console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
135
+ for (const f of files) {
136
+ try {
137
+ const metadata = await metaAnalysis(f)
138
+ galbe.meta?.push({ file: path, ...metadata })
139
+ console.log(`\n\x1b\[0;36m ${f}\x1b[0m`)
140
+ await importRoutes(f, galbe)
141
+ } catch (err) {
142
+ // console.log(`\x1b\[0;31m ${f}\x1b[0m`)
143
+ throw err
144
+ }
145
+ }
146
+ }
147
+ if (noRouteFound) {
148
+ process.stdout.write('\r\x1b[K')
149
+ console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
150
+ return
151
+ }
152
+ } else if (Array.isArray(routes)) {
153
+ for (const r of routes) {
154
+ await defineRoutes({ routes: r }, galbe)
155
+ }
156
+ }
157
+ }
package/src/server.ts ADDED
@@ -0,0 +1,174 @@
1
+ import type { Context, Route } from './types'
2
+
3
+ import { NotFoundError, RequestError } from './types'
4
+ import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
+ import { Galbe } from './index'
6
+
7
+ const handleInternalError = (error: any) => {
8
+ console.error(error)
9
+ return new RequestError({ status: 500, error: 'Internal Server Error' })
10
+ }
11
+
12
+ const setupPluginCallbacks = (galbe: Galbe) => ({
13
+ init: galbe.plugins.reduce((l: { name: string; cb: Function }[], p) => {
14
+ if (p.init) l.push({ name: p.name, cb: p.init })
15
+ return l
16
+ }, []),
17
+ onFetch: galbe.plugins.reduce((l: Function[], p) => {
18
+ if (p.onFetch) l.push(p.onFetch)
19
+ return l
20
+ }, []),
21
+ onRoute: galbe.plugins.reduce((l: Function[], p) => {
22
+ if (p.onRoute) l.push(p.onRoute)
23
+ return l
24
+ }, []),
25
+ beforeHandle: galbe.plugins.reduce((l: Function[], p) => {
26
+ if (p.beforeHandle) l.push(p.beforeHandle)
27
+ return l
28
+ }, []),
29
+ afterHandle: galbe.plugins.reduce((l: Function[], p) => {
30
+ if (p.afterHandle) l.push(p.afterHandle)
31
+ return l
32
+ }, [])
33
+ })
34
+
35
+ export default async (galbe: Galbe, port?: number) => {
36
+ const router = galbe.router
37
+ if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
38
+ galbe.config.basePath = `/${galbe?.config?.basePath}`
39
+ let pluginsCb = setupPluginCallbacks(galbe)
40
+ for (const { name, cb } of pluginsCb.init) await cb(galbe?.config?.plugin?.[name], galbe)
41
+
42
+ return Bun.serve({
43
+ port: port || galbe.config?.port || 3000,
44
+ async fetch(req) {
45
+ const context: Context = {
46
+ request: req,
47
+ set: { headers: {} },
48
+ headers: {},
49
+ params: {},
50
+ query: {},
51
+ body: {},
52
+ state: {}
53
+ }
54
+ for (const cb of pluginsCb.onFetch) {
55
+ const r = await cb(req)
56
+ if (r) return r
57
+ }
58
+ const url = new URL(req.url)
59
+ let route: Route
60
+ let response: any = ''
61
+ try {
62
+ // find route
63
+ if (!url.pathname.match(new RegExp(`^${galbe.config?.basePath || ''}`))) throw new NotFoundError()
64
+ try {
65
+ route = router.find(req.method, url.pathname)
66
+ } catch (error) {
67
+ if (error instanceof RequestError) throw error
68
+ else throw handleInternalError(error)
69
+ }
70
+
71
+ for (const cb of pluginsCb.onRoute) {
72
+ const r = await cb(route)
73
+ if (r) return r
74
+ }
75
+
76
+ // parse request
77
+ const schema = route.schema
78
+ let inHeaders = Object.fromEntries(req.headers.entries())
79
+ let inQuery = Object.fromEntries(url.searchParams.entries())
80
+ let inParams = requestPathParser(url.pathname, route.path)
81
+
82
+ context.body = await requestBodyParser(req.body, inHeaders, schema.body)
83
+ context.headers = { ...context.headers, ...inHeaders }
84
+ context.query = inQuery
85
+ context.params = inParams
86
+
87
+ // request validation
88
+ let errors: RequestError[] = []
89
+ try {
90
+ if (schema?.headers)
91
+ context.headers = {
92
+ ...context.headers,
93
+ ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
94
+ }
95
+ } catch (error) {
96
+ if (error instanceof RequestError) errors.push(error)
97
+ else throw handleInternalError(error)
98
+ }
99
+ try {
100
+ if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
101
+ } catch (error) {
102
+ if (error instanceof RequestError) errors.push(error)
103
+ else throw handleInternalError(error)
104
+ }
105
+ try {
106
+ if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
107
+ } catch (error) {
108
+ if (error instanceof RequestError) errors.push(error)
109
+ else throw handleInternalError(error)
110
+ }
111
+ if (errors.length) {
112
+ throw new RequestError({ status: 400, error: errors.reduce((acc, c) => ({ ...acc, ...c.error }), {}) })
113
+ }
114
+
115
+ for (const cb of pluginsCb.beforeHandle) {
116
+ const r = await cb(context)
117
+ if (r) return r
118
+ }
119
+
120
+ // call chain
121
+ let handlerCalled = false
122
+ const handlerWrapper = async (context: Context) => {
123
+ handlerCalled = true
124
+ return route.handler(context)
125
+ }
126
+ const callChain = route.hooks.map((hook, idx) => ({
127
+ call: async () => {
128
+ let nextCalled = false
129
+ let next = async () => {
130
+ await callChain[idx + 1].call()
131
+ }
132
+ await hook(context, next)
133
+ if (!nextCalled && !handlerCalled) await next()
134
+ }
135
+ }))
136
+ callChain.push({
137
+ call: async () => {
138
+ response = await handlerWrapper(context)
139
+ context.set.status = response instanceof Response ? response.status : 200
140
+ }
141
+ })
142
+ if (callChain.length > 1) await callChain[0].call()
143
+ else response = await handlerWrapper(context)
144
+ const parsedResponse = responseParser(response, context)
145
+
146
+ for (const cb of pluginsCb.afterHandle) {
147
+ const r = await cb(parsedResponse)
148
+ if (r) return r
149
+ }
150
+
151
+ return parsedResponse
152
+ } catch (error) {
153
+ context.set.status = error instanceof RequestError ? error.status : 500
154
+ if (galbe.errorHandler) return galbe.errorHandler(error, context)
155
+ if (error instanceof RequestError) {
156
+ return new Response(JSON.stringify(error.error), {
157
+ status: error.status,
158
+ headers: { 'Content-Type': 'application/json' }
159
+ })
160
+ }
161
+ return new Response('Internal Server Error', { status: 500 })
162
+ }
163
+ },
164
+ error(error) {
165
+ console.error(error)
166
+ return new Response('Internal Server Error', {
167
+ status: 500,
168
+ headers: {
169
+ 'Content-Type': 'text/plain'
170
+ }
171
+ })
172
+ }
173
+ })
174
+ }
package/src/types.ts ADDED
@@ -0,0 +1,279 @@
1
+ import type {
2
+ TSchema,
3
+ TBoolean,
4
+ TNumber,
5
+ TInteger,
6
+ TString,
7
+ TLiteral,
8
+ TArray,
9
+ TObject,
10
+ TProperties,
11
+ TUnion,
12
+ Static,
13
+ OptionalPropertyKeys,
14
+ ReadonlyOptionalPropertyKeys,
15
+ ReadonlyPropertyKeys,
16
+ RequiredPropertyKeys,
17
+ TAny
18
+ } from '@sinclair/typebox'
19
+
20
+ import { Kind } from '@sinclair/typebox'
21
+ import { Galbe } from './index'
22
+
23
+ export const Stream = Symbol.for('Galbe.Stream')
24
+ export type TStream<T extends TSchema = TSchema> = T & {
25
+ [Stream]: 'Stream'
26
+ }
27
+
28
+ export type TBody =
29
+ | TByteArray
30
+ | TString
31
+ | TBoolean
32
+ | TNumber
33
+ | TInteger
34
+ | TObject
35
+ | TArray
36
+ | TUrlForm
37
+ | TMultipartForm
38
+ | TUnion
39
+ | TStream
40
+ export type TStreamable = TByteArray | TString | TUrlForm | TMultipartForm
41
+ export type TUrlFormParam = TString | TByteArray | TBoolean | TNumber | TInteger | TLiteral | TArray | TAny | TUnion
42
+ export type TMultipartFormParam = TByteArray | TUrlFormParam | TObject | TArray
43
+ export type MaybeArray<T> = T | T[]
44
+
45
+ export interface MultipartFormData<
46
+ K extends string | number | symbol = string,
47
+ V extends Static<TMultipartFormParam> = any
48
+ > {
49
+ headers: { type?: string; name: K; filename?: string }
50
+ content: V
51
+ }
52
+
53
+ export interface TByteArray extends TSchema {
54
+ [Kind]: 'ByteArray'
55
+ static: Uint8Array
56
+ type: 'byteArray'
57
+ }
58
+ export interface TMultipartForm<T extends TMultipartProperties = TMultipartProperties> extends TSchema {
59
+ [Kind]: 'MultipartForm'
60
+ static: MultipartPropertiesReduce<T, this['params']>
61
+ type: 'multipartForm'
62
+ }
63
+ export interface TUrlForm<T extends TUrlFormProperties = TUrlFormProperties> extends TSchema {
64
+ [Kind]: 'UrlForm'
65
+ static: UrlFormPropertiesReduce<T, this['params']>
66
+ type: 'urlForm'
67
+ }
68
+
69
+ export type TMultipartProperties<V = TMultipartFormParam> = Record<string, V>
70
+ export type MultipartPropertiesReduce<T extends TMultipartProperties, P extends unknown[]> = MultipartPropertiesReducer<
71
+ T,
72
+ {
73
+ [K in keyof T]: Static<T[K], P>
74
+ }
75
+ >
76
+ export type MultipartPropertiesReducer<
77
+ T extends TMultipartProperties,
78
+ R extends Record<keyof any, unknown>
79
+ > = MultipartEvaluate<
80
+ Readonly<Partial<Pick<R, ReadonlyOptionalPropertyKeys<T>>>> &
81
+ Readonly<Pick<R, ReadonlyPropertyKeys<T>>> &
82
+ Partial<Pick<R, OptionalPropertyKeys<T>>> &
83
+ Required<Pick<R, RequiredPropertyKeys<T>>>
84
+ >
85
+ export type MultipartEvaluate<T> = T extends infer O
86
+ ? {
87
+ [K in keyof O]: O[K] extends Static<TMultipartFormParam> ? MultipartFormData<K, O[K]> : never
88
+ }
89
+ : never
90
+
91
+ export type TUrlFormProperties<V = TUrlFormParam> = Record<string, V>
92
+ export type UrlFormPropertiesReduce<T extends TUrlFormProperties, P extends unknown[]> = UrlFormPropertiesReducer<
93
+ T,
94
+ {
95
+ [K in keyof T]: Static<T[K], P>
96
+ }
97
+ >
98
+ export type UrlFormPropertiesReducer<
99
+ T extends TUrlFormProperties,
100
+ R extends Record<keyof any, unknown>
101
+ > = UrlFormEvaluate<
102
+ Readonly<Partial<Pick<R, ReadonlyOptionalPropertyKeys<T>>>> &
103
+ Readonly<Pick<R, ReadonlyPropertyKeys<T>>> &
104
+ Partial<Pick<R, OptionalPropertyKeys<T>>> &
105
+ Required<Pick<R, RequiredPropertyKeys<T>>>
106
+ >
107
+ export type UrlFormEvaluate<T> = T extends infer O
108
+ ? {
109
+ [K in keyof O]: O[K] extends Static<TUrlFormParam> ? O[K] : never
110
+ }
111
+ : never
112
+
113
+ export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'
114
+ type MaybePromise<T> = T | Promise<T>
115
+
116
+ /**
117
+ * #### GalbeConfig
118
+ * Instanciate a Galbe web server
119
+ *
120
+ * ---
121
+ * @example
122
+ * ```typescript
123
+ * import { Galbe } from 'galbe'
124
+ * const config : GalbeConfig = {
125
+ * port: 8080,
126
+ * basePath: "/v1",
127
+ * routes: "src/**­/*.route.ts"
128
+ * }
129
+ *
130
+ * export default new Galbe(config)
131
+ * ```
132
+ */
133
+ export type GalbeConfig = {
134
+ port?: number
135
+ basePath?: string
136
+ routes?: string | string[]
137
+ plugin?: Record<string, any>
138
+ }
139
+ /**
140
+ * #### Schema
141
+ * Define a request Schema with constraint upon
142
+ *
143
+ * ---
144
+ * @example
145
+ * ```typescript
146
+ * import { T, Schema } from 'galbe'
147
+ * const MyRequestSchema = {
148
+ * params: {
149
+ * id: T.Number(),
150
+ * },
151
+ * body: T.Object({
152
+ * name: T.String()
153
+ * age: T.Optional(T.Number({minimum: 0})),
154
+ * })
155
+ * }
156
+ * ```
157
+ */
158
+ export type Schema<
159
+ H extends TProperties = TProperties,
160
+ P extends TProperties = TProperties,
161
+ Q extends TProperties = TProperties,
162
+ B extends TBody = TBody
163
+ > = {
164
+ headers?: H
165
+ params?: P
166
+ query?: Q
167
+ body?: B
168
+ }
169
+ export type Context<S extends Schema = any> = {
170
+ headers: Static<TObject<Exclude<S['headers'], undefined>>>
171
+ params: Static<TObject<Exclude<S['params'], undefined>>>
172
+ query: Static<TObject<Exclude<S['query'], undefined>>>
173
+ body: Static<Exclude<S['body'], undefined>>
174
+ request: Request
175
+ state: Record<string, any>
176
+ set: {
177
+ headers: {
178
+ [header: string]: string
179
+ }
180
+ status?: number
181
+ redirect?: string
182
+ }
183
+ }
184
+ export type Next = () => void | Promise<void>
185
+ export type Hook<S extends Schema = Schema> = (ctx: Context<S>, next: Next) => any | Promise<any>
186
+ export type Handler<S extends Schema = Schema> = (ctx: Context<S>) => any
187
+ export type Endpoint = {
188
+ <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody = TObject>(
189
+ path: string,
190
+ schema: Schema<H, P, Q, B>,
191
+ hooks: Hook<Schema<H, P, Q, B>>[],
192
+ handler: Handler<Schema<H, P, Q, B>>
193
+ ): void
194
+ <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody = TObject>(
195
+ path: string,
196
+ schema: Schema<H, P, Q, B>,
197
+ handler: Handler<Schema<H, P, Q, B>>
198
+ ): void
199
+ <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody = TObject>(
200
+ path: string,
201
+ hooks: Hook<Schema<H, P, Q, B>>[],
202
+ handler: Handler<Schema<H, P, Q, B>>
203
+ ): void
204
+ <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody = TObject>(
205
+ path: string,
206
+ handler: Handler<Schema<H, P, Q, B>>
207
+ ): void
208
+ }
209
+
210
+ export class RequestError {
211
+ status: number
212
+ error: any
213
+ constructor(options: { status?: number; error?: any }) {
214
+ this.status = options.status ?? 500
215
+ this.error = options.error ?? 'Internal server error'
216
+ }
217
+ }
218
+
219
+ export type ErrorHandler = (error: any, context: Context) => any
220
+
221
+ export type RouteNode = {
222
+ route?: Route
223
+ param?: RouteNode
224
+ children?: Record<string, RouteNode>
225
+ }
226
+
227
+ export type Route<
228
+ H extends TProperties = TProperties,
229
+ P extends TProperties = TProperties,
230
+ Q extends TProperties = TProperties,
231
+ B extends TBody = TBody
232
+ > = {
233
+ method: Method
234
+ path: string
235
+ schema: Schema<H, P, Q, B>
236
+ context: Context<Schema<H, P, Q, B>>
237
+ hooks: Hook[]
238
+ handler: Handler<Schema<H, P, Q, B>>
239
+ }
240
+
241
+ export type RouteTree = {
242
+ [key: string]: RouteNode
243
+ }
244
+
245
+ export class NotFoundError extends RequestError {
246
+ constructor(message?: string) {
247
+ super({ status: 404, error: message ?? 'Not found' })
248
+ }
249
+ }
250
+
251
+ /**
252
+ * #### GalbePlugin
253
+ * Define a plugin for a Galbe server
254
+ *
255
+ * ---
256
+ * @example
257
+ * ```typescript
258
+ * import { GalbePlugin } from 'galbe'
259
+ * const MyPlugin : GalbePlugin = {
260
+ * name: 'com.example.plugin.name',
261
+ * init: (config, galbe) => {
262
+ * console.log('Plugin initialization')
263
+ * },
264
+ * onRoute: (route) => {
265
+ * if(route.path === '/myPlugin') {
266
+ * return new Response('Hello Mom!')
267
+ * }
268
+ * }
269
+ * }
270
+ * ```
271
+ */
272
+ export type GalbePlugin = {
273
+ name: string
274
+ init?: (config: any, galbe: Galbe) => MaybePromise<void>
275
+ onFetch?: (request: Request) => MaybePromise<Response | void>
276
+ onRoute?: (route: Route) => MaybePromise<Response | void>
277
+ beforeHandle?: (context: Context) => MaybePromise<Response | void>
278
+ afterHandle?: (response: Response) => MaybePromise<Response | void>
279
+ }