galbe 0.1.13 → 0.3.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/server.ts CHANGED
@@ -3,6 +3,9 @@ import type { Context, Route } from './types'
3
3
  import { InternalError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
+ import { validateResponse } from './validator'
7
+
8
+ const LOADABLE_METHODS = ['POST', 'PUT', 'PATCH']
6
9
 
7
10
  const handleInternalError = (error: any) => {
8
11
  console.error(error)
@@ -10,26 +13,11 @@ const handleInternalError = (error: any) => {
10
13
  }
11
14
 
12
15
  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
- }, [])
16
+ init: galbe.plugins.filter(p => p.init),
17
+ onFetch: galbe.plugins.filter(p => p.onFetch),
18
+ onRoute: galbe.plugins.filter(p => p.onRoute),
19
+ beforeHandle: galbe.plugins.filter(p => p.beforeHandle),
20
+ afterHandle: galbe.plugins.filter(p => p.afterHandle)
33
21
  })
34
22
 
35
23
  export default async (galbe: Galbe, port?: number) => {
@@ -37,7 +25,8 @@ export default async (galbe: Galbe, port?: number) => {
37
25
  if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
38
26
  galbe.config.basePath = `/${galbe?.config?.basePath}`
39
27
  let pluginsCb = setupPluginCallbacks(galbe)
40
- for (const { name, cb } of pluginsCb.init) await cb(galbe?.config?.plugin?.[name], galbe)
28
+ //@ts-ignore
29
+ for (const p of pluginsCb.init) await p.init(galbe?.config?.plugin?.[p.name], galbe)
41
30
 
42
31
  return Bun.serve({
43
32
  port: port || galbe.config?.port || 3000,
@@ -51,8 +40,9 @@ export default async (galbe: Galbe, port?: number) => {
51
40
  body: {},
52
41
  state: {}
53
42
  }
54
- for (const cb of pluginsCb.onFetch) {
55
- const r = await cb(req)
43
+ for (const p of pluginsCb.onFetch) {
44
+ //@ts-ignore
45
+ const r = await p.onFetch(context)
56
46
  if (r) return r
57
47
  }
58
48
  const url = new URL(req.url)
@@ -66,9 +56,11 @@ export default async (galbe: Galbe, port?: number) => {
66
56
  if (error instanceof RequestError) throw error
67
57
  else throw handleInternalError(error)
68
58
  }
59
+ context.route = route
69
60
 
70
- for (const cb of pluginsCb.onRoute) {
71
- const r = await cb(route)
61
+ for (const p of pluginsCb.onRoute) {
62
+ //@ts-ignore
63
+ const r = await p.onRoute(context)
72
64
  if (r) return r
73
65
  }
74
66
 
@@ -80,41 +72,46 @@ export default async (galbe: Galbe, port?: number) => {
80
72
  for (let [k, v] of url.searchParams) inQuery[k] = v
81
73
  let inParams = requestPathParser(url.pathname, route.path)
82
74
 
83
- context.body = await requestBodyParser(req.body, inHeaders, schema.body)
75
+ context.body = LOADABLE_METHODS.includes(req.method)
76
+ ? await requestBodyParser(req.body, inHeaders, schema.body)
77
+ : null
84
78
  context.headers = inHeaders
85
79
  context.query = inQuery
86
80
  context.params = inParams
87
81
 
88
82
  // request validation
89
- let errors: RequestError[] = []
90
- try {
91
- if (schema?.headers)
92
- context.headers = {
93
- ...context.headers,
94
- ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
95
- }
96
- } catch (error) {
97
- if (error instanceof RequestError) errors.push(error)
98
- else throw handleInternalError(error)
99
- }
100
- try {
101
- if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
102
- } catch (error) {
103
- if (error instanceof RequestError) errors.push(error)
104
- else throw handleInternalError(error)
105
- }
106
- try {
107
- if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
108
- } catch (error) {
109
- if (error instanceof RequestError) errors.push(error)
110
- else throw handleInternalError(error)
111
- }
112
- if (errors.length) {
113
- throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
83
+ if (galbe.config?.requestValidator?.enabled) {
84
+ let errors: RequestError[] = []
85
+ try {
86
+ if (schema?.headers)
87
+ context.headers = {
88
+ ...context.headers,
89
+ ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
90
+ }
91
+ } catch (error) {
92
+ if (error instanceof RequestError) errors.push(error)
93
+ else throw handleInternalError(error)
94
+ }
95
+ try {
96
+ if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
97
+ } catch (error) {
98
+ if (error instanceof RequestError) errors.push(error)
99
+ else throw handleInternalError(error)
100
+ }
101
+ try {
102
+ if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
103
+ } catch (error) {
104
+ if (error instanceof RequestError) errors.push(error)
105
+ else throw handleInternalError(error)
106
+ }
107
+ if (errors.length) {
108
+ throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
109
+ }
114
110
  }
115
111
 
116
- for (const cb of pluginsCb.beforeHandle) {
117
- const r = await cb(context)
112
+ for (const p of pluginsCb.beforeHandle) {
113
+ //@ts-ignore
114
+ const r = await p.beforeHandle(context)
118
115
  if (r) return r
119
116
  }
120
117
 
@@ -152,8 +149,12 @@ export default async (galbe: Galbe, port?: number) => {
152
149
 
153
150
  const parsedResponse = responseParser(response, context)
154
151
 
155
- for (const cb of pluginsCb.afterHandle) {
156
- const r = await cb(parsedResponse)
152
+ if (galbe.config?.responseValidator?.enabled && schema.response)
153
+ validateResponse(response, schema.response, context.set.status || 200)
154
+
155
+ for (const p of pluginsCb.afterHandle) {
156
+ //@ts-ignore
157
+ const r = await p.afterHandle(parsedResponse, context)
157
158
  if (r) return r
158
159
  }
159
160
 
@@ -163,8 +164,19 @@ export default async (galbe: Galbe, port?: number) => {
163
164
  let customError
164
165
  if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
165
166
  if (customError) return customError
166
- if (error instanceof RequestError) {
167
- return new Response(JSON.stringify(error.payload), {
167
+ if (error instanceof InternalError) {
168
+ console.log(`Internal Error`, error?.payload || '')
169
+ return new Response('Internal Server Error', {
170
+ status: error.status,
171
+ headers: { 'Content-Type': 'application/json' }
172
+ })
173
+ } else if (error instanceof RequestError) {
174
+ let payload = ''
175
+ if (typeof error.payload === 'string') payload = error.payload
176
+ try {
177
+ payload = JSON.stringify(error.payload)
178
+ } catch (err) {}
179
+ return new Response(payload, {
168
180
  status: error.status,
169
181
  headers: { 'Content-Type': 'application/json' }
170
182
  })
package/src/types.ts CHANGED
@@ -22,12 +22,27 @@ export type STBody =
22
22
  | STBoolean
23
23
  | STNumber
24
24
  | STInteger
25
+ | STLiteral
25
26
  | STObject
26
27
  | STArray
27
28
  | STUrlForm
28
29
  | STMultipartForm
29
30
  | STUnion
30
31
  | STStream
32
+
33
+ export type STResponseValue =
34
+ | STByteArray
35
+ | STString
36
+ | STBoolean
37
+ | STNumber
38
+ | STInteger
39
+ | STLiteral
40
+ | STObject
41
+ | STArray
42
+ | STUnion
43
+ | STStream
44
+ export type STResponse = Record<number, STResponseValue>
45
+
31
46
  export type MaybeArray<T> = T | T[]
32
47
 
33
48
  export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'
@@ -74,6 +89,8 @@ export type GalbeConfig = {
74
89
  routes?: boolean | string | string[]
75
90
  router?: { cacheEnabled: boolean }
76
91
  plugin?: Record<string, any>
92
+ requestValidator?: { enabled: boolean }
93
+ responseValidator?: { enabled: boolean }
77
94
  }
78
95
  /**
79
96
  * #### Schema
@@ -96,15 +113,17 @@ export type GalbeConfig = {
96
113
  */
97
114
  export type RequestSchema<
98
115
  Path extends string = string,
99
- H extends STHeaders = {},
100
- P extends Partial<STParams<Path>> = {},
101
- Q extends STQuery = {},
102
- B extends STBody = STBody
116
+ H extends STHeaders = STHeaders,
117
+ P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
118
+ Q extends STQuery = STQuery,
119
+ B extends STBody = STBody,
120
+ R extends STResponse = STResponse
103
121
  > = {
104
122
  headers?: H
105
123
  params?: P
106
124
  query?: Q
107
125
  body?: B
126
+ response?: R
108
127
  }
109
128
 
110
129
  type OmitNotDefined<S extends RequestSchema> = {
@@ -124,6 +143,7 @@ export type Context<Path extends string = string, S extends RequestSchema = Requ
124
143
  query: Static<STObject<Exclude<S['query'], undefined>>>
125
144
  body: Static<Exclude<S['body'], undefined>>
126
145
  request: Request
146
+ route?: Route
127
147
  state: Record<string, any>
128
148
  set: {
129
149
  headers: {
@@ -146,44 +166,48 @@ export type Endpoint = {
146
166
  H extends STHeaders,
147
167
  P extends Partial<STParams<Path>>,
148
168
  Q extends STQuery,
149
- B extends STBody = any
169
+ B extends STBody = any,
170
+ R extends STResponse = STResponse
150
171
  >(
151
172
  path: Path,
152
- schema: RequestSchema<Path, H, P, Q, B>,
153
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
154
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
173
+ schema: RequestSchema<Path, H, P, Q, B, R>,
174
+ hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[],
175
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
155
176
  ): void
156
177
  <
157
178
  Path extends string,
158
179
  H extends STHeaders,
159
180
  P extends Partial<STParams<Path>>,
160
181
  Q extends STQuery,
161
- B extends STBody = any
182
+ B extends STBody = any,
183
+ R extends STResponse = STResponse
162
184
  >(
163
185
  path: Path,
164
- schema: RequestSchema<Path, H, P, Q, B>,
165
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
186
+ schema: RequestSchema<Path, H, P, Q, B, R>,
187
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
166
188
  ): void
167
189
  <
168
190
  Path extends string,
169
191
  H extends STHeaders,
170
192
  P extends Partial<STParams<Path>>,
171
193
  Q extends STQuery,
172
- B extends STBody = any
194
+ B extends STBody = any,
195
+ R extends STResponse = STResponse
173
196
  >(
174
197
  path: Path,
175
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
176
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
198
+ hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[],
199
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
177
200
  ): void
178
201
  <
179
202
  Path extends string,
180
203
  H extends STHeaders,
181
204
  P extends Partial<STParams<Path>>,
182
205
  Q extends STQuery,
183
- B extends STBody = any
206
+ B extends STBody = any,
207
+ R extends STResponse = STResponse
184
208
  >(
185
209
  path: Path,
186
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
210
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
187
211
  ): void
188
212
  }
189
213
 
@@ -192,7 +216,7 @@ export class RequestError {
192
216
  payload: any
193
217
  constructor(options: { status?: number; payload?: any }) {
194
218
  this.status = options.status ?? 500
195
- this.payload = options.payload ?? 'Internal server error'
219
+ this.payload = options.payload
196
220
  }
197
221
  }
198
222
 
@@ -209,14 +233,15 @@ export type Route<
209
233
  H extends STHeaders = {},
210
234
  P extends Partial<STParams<Path>> = {},
211
235
  Q extends STQuery = {},
212
- B extends STBody = STBody
236
+ B extends STBody = STBody,
237
+ R extends STResponse = STResponse
213
238
  > = {
214
239
  method: Method
215
240
  path: Path
216
- schema: RequestSchema<Path, H, P, Q, B>
217
- context: Context<Path, RequestSchema<Path, H, P, Q, B>>
241
+ schema: RequestSchema<Path, H, P, Q, B, R>
242
+ context: Context<Path, RequestSchema<Path, H, P, Q, B, R>>
218
243
  hooks: Hook[]
219
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
244
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
220
245
  }
221
246
 
222
247
  export type RouteTree = {
@@ -224,13 +249,13 @@ export type RouteTree = {
224
249
  }
225
250
 
226
251
  export class NotFoundError extends RequestError {
227
- constructor(message?: string) {
252
+ constructor(message?: any) {
228
253
  super({ status: 404, payload: message ?? 'Not found' })
229
254
  }
230
255
  }
231
256
 
232
257
  export class InternalError extends RequestError {
233
- constructor(message?: string) {
258
+ constructor(message?: any) {
234
259
  super({ status: 500, payload: message ?? 'Internal Server Error' })
235
260
  }
236
261
  }
@@ -259,8 +284,8 @@ export class InternalError extends RequestError {
259
284
  export type GalbePlugin = {
260
285
  name: string
261
286
  init?: (config: any, galbe: Galbe) => MaybePromise<void>
262
- onFetch?: (request: Request) => MaybePromise<Response | void>
263
- onRoute?: (route: Route) => MaybePromise<Response | void>
287
+ onFetch?: (context: Context) => MaybePromise<Response | void>
288
+ onRoute?: (context: Context) => MaybePromise<Response | void>
264
289
  beforeHandle?: (context: Context) => MaybePromise<Response | void>
265
- afterHandle?: (response: Response) => MaybePromise<Response | void>
290
+ afterHandle?: (response: Response, context: Context) => MaybePromise<Response | void>
266
291
  }
package/src/util.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { RouteFileMeta } from './routes'
2
+
1
3
  const METHOD_COLOR: Record<string, string> = {
2
4
  get: '\x1b[32m',
3
5
  post: '\x1b[34m',
@@ -6,7 +8,21 @@ const METHOD_COLOR: Record<string, string> = {
6
8
  delete: '\x1b[31m',
7
9
  options: ''
8
10
  }
9
- export const logRoute = (r: { method: string; path: string }) => {
11
+
12
+ export const extractMetaRoute = (route: { path: string; method: string }, meta?: Array<RouteFileMeta>) => {
13
+ return meta?.map(e => (route.path in e.routes ? e.routes[route.path]?.[route.method] : null)).filter(e => e)?.[0]
14
+ }
15
+
16
+ export const logRoute = (
17
+ r: { method: string; path: string },
18
+ meta?: Record<string, boolean | string | string[]> | null
19
+ ) => {
10
20
  let color = METHOD_COLOR?.[r.method] || ''
11
- console.log(` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path}`)
21
+ console.log(
22
+ ` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path}${
23
+ meta?.head ? ` - ${meta.head}` : ''
24
+ }`
25
+ )
12
26
  }
27
+
28
+ export const isIterator = (obj: any) => typeof obj?.next === 'function'
package/src/validator.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { InternalError, type STResponse } from './index'
1
2
  import type { STSchema, STProps, STUnion } from './schema'
2
- import { Kind, Optional } from './schema'
3
+ import { Kind, Optional, Stream } from './schema'
4
+ import { isIterator } from './util'
3
5
 
4
6
  export const validate = (elt: any, schema: STSchema, parse = false): any => {
5
7
  type ValidationError = string | string[] | { [key: string]: ValidationError }
@@ -7,7 +9,10 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
7
9
  const iElt = elt
8
10
 
9
11
  if (schema[Kind] === 'boolean') {
10
- if (parse && typeof elt === 'string') elt = elt === 'true' ? true : elt === 'false' ? false : null
12
+ if (typeof elt === 'string') {
13
+ if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
14
+ else throw `Expected boolean, got string.`
15
+ }
11
16
  if (elt !== true && elt !== false) throw `${iElt} is not a valid boolean. Should be 'true' or 'false'`
12
17
  } else if (schema[Kind] === 'integer') {
13
18
  if (parse && typeof elt === 'string') elt = Number(elt)
@@ -84,6 +89,22 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
84
89
  return elt
85
90
  }
86
91
 
92
+ export const validateResponse = (response: any, schema: STResponse, status: number) => {
93
+ if (!(status in schema)) return
94
+ const s = schema[status]
95
+ if (response instanceof ReadableStream) {
96
+ if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got ReadableStream`)
97
+ } else if (isIterator(response)) {
98
+ if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got Iterator`)
99
+ } else {
100
+ try {
101
+ validate(response, s)
102
+ } catch (error) {
103
+ throw new InternalError({ ResponseValidationError: error })
104
+ }
105
+ }
106
+ }
107
+
87
108
  const schemaValidation = (value: any, schema: STSchema) => {
88
109
  const errors = []
89
110
  if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
@@ -92,9 +113,8 @@ const schemaValidation = (value: any, schema: STSchema) => {
92
113
  if (schema.exclusiveMax !== undefined)
93
114
  if ((value as number) >= schema.exclusiveMax)
94
115
  errors.push(`${value} is greater or equal to ${schema.exclusiveMax}`)
95
- if (schema.minimum !== undefined)
96
- if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
97
- if (schema.maximum !== undefined)
116
+ if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
117
+ if (schema.max !== undefined)
98
118
  if ((value as number) > schema.max) errors.push(`${value} is greater than ${schema.max}`)
99
119
  } else if (schema[Kind] === 'string') {
100
120
  if (schema.minLength !== undefined && (value as string).length < schema.minLength)
@@ -39,8 +39,8 @@ describe('plugins', async () => {
39
39
  let request: any = null
40
40
  const plugin: GalbePlugin = {
41
41
  name: 'dev.galbe.test.init',
42
- onFetch: mock(req => {
43
- request = req
42
+ onFetch: mock(ctx => {
43
+ request = ctx.request
44
44
  })
45
45
  }
46
46
 
@@ -86,7 +86,7 @@ describe('plugins', async () => {
86
86
  const plugin: GalbePlugin = {
87
87
  name: 'dev.galbe.test.init',
88
88
  onRoute: mock(r => {
89
- route = r
89
+ route = r.route
90
90
  })
91
91
  }
92
92