galbe 0.13.0 → 0.14.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/schema.ts CHANGED
@@ -7,7 +7,24 @@ export interface Options {
7
7
  title?: string
8
8
  description?: string
9
9
  default?: any
10
+ example?: any
11
+ /**
12
+ * On a response schema: a map of named examples (OpenAPI content-level
13
+ * `examples`). On any other schema: a single example value.
14
+ */
10
15
  examples?: any
16
+ /**
17
+ * Response-only: declares response headers. Each value is a Galbe schema
18
+ * describing the header's value type.
19
+ */
20
+ headers?: Record<string, any>
21
+ /** Marks the schema as deprecated. Surfaced by spec generators (e.g. OpenAPI). */
22
+ deprecated?: boolean
23
+ /**
24
+ * Response-only: declares response headers emitted in the OpenAPI `responses` object.
25
+ * Distinct from the request-level `headers` field.
26
+ */
27
+ responseHeaders?: Record<string, any>
11
28
  }
12
29
  export interface ByteArrayOptions extends Options {
13
30
  minLength?: number
@@ -17,6 +34,7 @@ export interface StringOptions extends Options {
17
34
  minLength?: number
18
35
  maxLength?: number
19
36
  pattern?: RegExp
37
+ format?: string
20
38
  }
21
39
  export interface NumberOptions extends Options {
22
40
  min?: number
@@ -44,14 +62,13 @@ export interface STSchema extends Options {
44
62
  | 'urlForm'
45
63
  | 'multipartForm'
46
64
  | 'any'
47
- | 'union'
65
+ | 'anyOf'
66
+ | 'oneOf'
48
67
  | 'intersection'
49
68
  [Optional]?: boolean
50
69
  [Stream]?: boolean
51
70
  params: unknown[]
52
71
  static: unknown
53
- props?: STProps
54
- [key: string]: any
55
72
  }
56
73
  export type STPropsValue =
57
74
  | STBoolean
@@ -128,7 +145,7 @@ export function _Bool(options: Options = {}): STBoolean {
128
145
  } as unknown as STBoolean
129
146
  }
130
147
  // String
131
- export interface STString extends STSchema, NumberOptions {
148
+ export interface STString extends STSchema, StringOptions {
132
149
  [Kind]: 'string'
133
150
  static: string
134
151
  }
@@ -179,7 +196,6 @@ export function _Literal<T extends STLiteralValue>(value: T, options: Options =
179
196
  export interface STAny extends STSchema, Options {
180
197
  [Kind]: 'any'
181
198
  static: any
182
- [key: string]: any
183
199
  }
184
200
  export function _Any(options: Options = {}): STAny {
185
201
  return {
@@ -196,8 +212,8 @@ export interface STObject<T extends STProps = STProps> extends STSchema {
196
212
  }
197
213
  export interface STJson<T extends STBoolean | STNumber | STString | STObject = any> extends STSchema {
198
214
  [Kind]: 'json'
199
- type: 'boolean' | 'number' | 'string' | 'object' | 'unknown'
200
215
  static: Static<T>
216
+ value: T
201
217
  }
202
218
  type ObjectStatic<T extends STProps, P extends unknown[]> = ObjectStaticProps<T, { [K in keyof T]: Static<T[K], P> }>
203
219
  type OptionalPropertyKeys<T extends STProps> = {
@@ -217,46 +233,18 @@ function _Object<T extends STProps>(properties?: T, options: Options = {}): STOb
217
233
  ? { ...options, [Kind]: 'object', props: clonedProperties, required: requiredKeys }
218
234
  : { ...options, [Kind]: 'object', props: clonedProperties }) as unknown as STObject<T>
219
235
  }
220
- function _Json<T extends STBoolean | STNumber | STString | STObject>(value?: T, options: Options = {}): STJson<T> {
221
- if (value?.[Kind] === 'boolean') return { ..._Bool(options), [Kind]: 'json', type: 'boolean' }
222
- if (value?.[Kind] === 'number') return { ..._Number(options), [Kind]: 'json', type: 'number' }
223
- if (value?.[Kind] === 'string') return { ..._String(options), [Kind]: 'json', type: 'string' }
224
- if (value?.[Kind] === 'object') return { ..._Object(value.props, options), [Kind]: 'json', type: 'object' }
225
- throw Error('Invalid Json type definition')
236
+ function _Json<T extends STBoolean | STNumber | STString | STObject>(value: T, options: Options = {}): STJson<T> {
237
+ const k = value?.[Kind]
238
+ if (k !== 'boolean' && k !== 'number' && k !== 'string' && k !== 'object') {
239
+ throw new Error('Invalid Json type definition')
240
+ }
241
+ return {
242
+ ...options,
243
+ [Kind]: 'json',
244
+ value,
245
+ } as unknown as STJson<T>
226
246
  }
227
247
 
228
- // UrlForm
229
- // export type STUrlFormValues =
230
- // | STByteArray
231
- // | STBoolean
232
- // | STNumber
233
- // | STInteger
234
- // | STString
235
- // | STLiteral
236
- // | STObject
237
- // | STUnion
238
- // | STAny
239
- // | STArray
240
- // export type STUrlFormProps = Record<string, STUrlFormValues>
241
- // export interface STUrlForm<T extends STUrlFormProps = STUrlFormProps> extends STSchema {
242
- // [Kind]: 'urlForm'
243
- // static: T extends undefined ? Record<string, any> : ObjectStatic<T, this['params']>
244
- // props: T
245
- // }
246
- // function _UrlForm<T extends STUrlFormProps>(properties?: T, options: Options = {}): STUrlForm<T> {
247
- // if (!properties) return { ...options, [Kind]: 'urlForm' } as unknown as STUrlForm<T>
248
- // const propertyKeys = globalThis.Object.getOwnPropertyNames(properties)
249
- // const optionalKeys = propertyKeys.filter(key => properties[key]?.[Optional])
250
- // const requiredKeys = propertyKeys.filter(name => !optionalKeys.includes(name))
251
- // const clonedProperties = propertyKeys.reduce(
252
- // (acc, key) => ({ ...acc, [key]: { ...properties[key] } }),
253
- // {} as STUrlFormProps
254
- // )
255
- // return (requiredKeys.length > 0
256
- // ? { ...options, [Kind]: 'urlForm', props: clonedProperties, required: requiredKeys }
257
- // : { ...options, [Kind]: 'urlForm', props: clonedProperties }) as unknown as STUrlForm<T>
258
- // }
259
-
260
248
  // MultipartForm
261
249
  export type STMultipartFormValues = STSchema
262
250
  export interface MultipartFormData<K extends string = string, V extends Static<STMultipartForm> = any> {
@@ -293,7 +281,7 @@ function _MultipartForm<T extends STProps>(properties?: T, options: Options = {}
293
281
 
294
282
  // Array
295
283
  type NonEmptyArray<T> = [T, ...T[]]
296
- export interface STArray<T extends STSchema = STSchema> extends STSchema {
284
+ export interface STArray<T extends STSchema = STSchema> extends STSchema, ArrayOptions {
297
285
  [Kind]: 'array'
298
286
  static: Static<T>[]
299
287
  items: T
@@ -311,16 +299,19 @@ type UnionStatic<T extends STSchema[], P extends unknown[]> = {
311
299
  [K in keyof T]: T[K] extends STSchema ? Static<T[K], P> : never
312
300
  }[number]
313
301
  export interface STUnion<T extends NonEmptyArray<STSchema> = NonEmptyArray<STSchema>> extends STSchema {
314
- [Kind]: 'union'
302
+ [Kind]: 'anyOf' | 'oneOf'
315
303
  static: UnionStatic<T, this['params']>
316
- props: T[number]['props']
317
- anyOf: T
304
+ members: T
318
305
  }
319
- export function _Union<T extends NonEmptyArray<STSchema>>(schemas: [...T], options: Options): STUnion<T> {
306
+ export function _Union<T extends NonEmptyArray<STSchema>>(
307
+ kind: 'anyOf' | 'oneOf',
308
+ schemas: [...T],
309
+ options: Options
310
+ ): STUnion<T> {
320
311
  const s = {
321
312
  ...options,
322
- [Kind]: 'union',
323
- anyOf: schemas as T,
313
+ [Kind]: kind,
314
+ members: schemas as T,
324
315
  optional: () => ({ ...s, [Optional]: true }),
325
316
  }
326
317
  return s as unknown as STUnion<T>
@@ -334,15 +325,14 @@ type IntersectionStatic<T extends readonly STSchema[], P extends unknown[]> = T
334
325
  : never
335
326
  : never
336
327
  : unknown
337
- export interface STIntersection<T extends NonEmptyArray<STObject | STUnion | STIntersection> = NonEmptyArray<STObject>>
338
- extends STSchema {
328
+ // type Intersecs = NonEmptyArray<STObject | STUnion | STIntersection>
329
+ export interface STIntersection<T extends NonEmptyArray<STObject | STUnion | STIntersection<any>>> extends STSchema {
339
330
  [Kind]: 'intersection'
340
331
  static: IntersectionStatic<T, this['params']>
341
- props: T[number]['props']
342
332
  allOf: T
343
333
  }
344
334
 
345
- export function _Intersection<T extends NonEmptyArray<STObject | STUnion | STIntersection>>(
335
+ export function _Intersection<T extends NonEmptyArray<STObject | STUnion | STIntersection<any>>>(
346
336
  schemas: [...T],
347
337
  options: Options
348
338
  ): STIntersection<T> {
@@ -350,24 +340,13 @@ export function _Intersection<T extends NonEmptyArray<STObject | STUnion | STInt
350
340
  ...options,
351
341
  [Kind]: 'intersection',
352
342
  allOf: schemas as T,
353
- props: schemas.reduce(
354
- (b, c) => ({
355
- ...b,
356
- ...(c.props || {}),
357
- ...Object.fromEntries(
358
- Object.entries(b).filter(([_, v]) => (v as STSchema)[Kind] === 'literal' || !(v as STSchema)[Optional])
359
- // TODO: handle cases where left != right
360
- ),
361
- }),
362
- {}
363
- ) as STProps,
364
343
  optional: () => ({ ...s, [Optional]: true }),
365
344
  }
366
345
  return s as unknown as STIntersection<T>
367
346
  }
368
347
 
369
348
  // Stream
370
- type STStreamable = STByteArray | STString | STMultipartForm | STObject | STUnion | STIntersection
349
+ type STStreamable = STByteArray | STString | STMultipartForm | STObject | STUnion | STIntersection<any>
371
350
  export function _Stream<T extends STStreamable>(schema: T): STStream<T> {
372
351
  return {
373
352
  ...schema,
@@ -377,7 +356,7 @@ export function _Stream<T extends STStreamable>(schema: T): STStream<T> {
377
356
  // Nullable
378
357
  type STNullable<T extends STSchema> = STUnion<[T, STNull]>
379
358
  // Nullish
380
- type STNullish<T extends STSchema> = (T | STNull) & { [Kind]: 'union'; anyOf: [T, STNull]; [Optional]: true }
359
+ type STNullish<T extends STSchema> = (T | STNull) & { [Kind]: 'anyOf'; members: [T, STNull]; [Optional]: true }
381
360
  export class SchemaType {
382
361
  /** Creates an Optional Schema Type Wrapper */
383
362
  public optional<T extends STSchema>(schema: T): STOptional<T> {
@@ -385,7 +364,7 @@ export class SchemaType {
385
364
  }
386
365
  /** Creates an Nullable Schema Type Wrapper */
387
366
  public nullable<T extends STSchema>(schema: T): STNullable<T> {
388
- return { ..._Union([schema, _Null()], {}) }
367
+ return { ..._Union('anyOf', [schema, _Null()], {}) }
389
368
  }
390
369
  /** Creates an Nullish Schema Type Wrapper */
391
370
  public nullish<T extends STSchema>(schema: T): STOptional<STNullable<T>> {
@@ -434,24 +413,28 @@ export class SchemaType {
434
413
  ): STJson<T> {
435
414
  return _Json(value, options)
436
415
  }
437
- /** Creates an UrlForm Schema Type */
438
- // public urlForm<T extends STUrlFormProps>(properties?: T, options: Options = {}): STUrlForm<T> {
439
- // return _UrlForm(properties, options)
440
- // }
441
416
  /** Creates a MultipartForm Schema Type */
442
417
  public multipartForm<T extends STProps>(properties?: T, options: Options = {}): STMultipartForm<T> {
443
418
  return _MultipartForm(properties, options)
444
419
  }
445
420
  /** Creates an Array Schema Type */
446
- public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T> {
421
+ public array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T> {
447
422
  return _Array(schema, options)
448
423
  }
449
- /** Creates an Union Schema Type */
424
+ /** Creates an anyOf Schema Type (alias: `union`) */
425
+ public anyOf<T extends NonEmptyArray<STSchema>>(schemas: [...T], options: Options = {}): STUnion<T> {
426
+ return _Union('anyOf', schemas, options)
427
+ }
428
+ /** Creates a oneOf Schema Type */
429
+ public oneOf<T extends NonEmptyArray<STSchema>>(schemas: [...T], options: Options = {}): STUnion<T> {
430
+ return _Union('oneOf', schemas, options)
431
+ }
432
+ /** @deprecated Use `anyOf` instead */
450
433
  public union<T extends NonEmptyArray<STSchema>>(schemas: [...T], options: Options = {}): STUnion<T> {
451
- return _Union(schemas, options)
434
+ return _Union('anyOf', schemas, options)
452
435
  }
453
436
  /** Creates an Intersection Schema Type */
454
- public intersection<T extends NonEmptyArray<STObject | STUnion | STIntersection>>(
437
+ public intersection<T extends NonEmptyArray<STObject | STUnion | STIntersection<any>>>(
455
438
  schemas: [...T],
456
439
  options: Options = {}
457
440
  ): STIntersection<T> {
@@ -465,8 +448,8 @@ export class SchemaType {
465
448
  T['props'] extends undefined
466
449
  ? { [k: string]: Static<STPropsValue> }
467
450
  : T['props'] extends STProps
468
- ? { [K in keyof T['props']]: [K, Static<T['props'][K]>] }[keyof T['props']]
469
- : never
451
+ ? { [K in keyof T['props']]: [K, Static<T['props'][K]>] }[keyof T['props']]
452
+ : never
470
453
  >
471
454
  params: unknown[]
472
455
  }
@@ -475,24 +458,27 @@ export class SchemaType {
475
458
  ): Omit<STStream<T>, 'static'> & {
476
459
  static: AsyncGenerator<
477
460
  Entries<{
478
- [P in KeysOfUnion<T['props']> as ValueAt<T['props'], P> extends STSchema ? P : never]: Static<
479
- ValueAt<T['props'], P>
480
- >
461
+ [P in KeysOfUnion<MemberProps<T['members'][number]>> as ValueAt<
462
+ MemberProps<T['members'][number]>,
463
+ P
464
+ > extends STSchema
465
+ ? P
466
+ : never]: Static<ValueAt<MemberProps<T['members'][number]>, P>>
481
467
  }>
482
468
  >
483
469
  params: unknown[]
484
470
  }
485
- public stream<T extends STIntersection>(
471
+ public stream<T extends STIntersection<any>>(
486
472
  schema: T
487
473
  ): Omit<STStream<T>, 'static'> & {
488
474
  static: AsyncGenerator<
489
- T['props'] extends undefined
475
+ MemberProps<T['allOf'][number]> extends undefined
490
476
  ? never
491
- : T['props'] extends STProps
492
- ? Entries<{
493
- [K in keyof Static<T>]: Static<T>[K]
494
- }>
495
- : never
477
+ : MemberProps<T['allOf'][number]> extends STProps
478
+ ? Entries<{
479
+ [K in keyof Static<T>]: Static<T>[K]
480
+ }>
481
+ : never
496
482
  >
497
483
  params: unknown[]
498
484
  }
@@ -530,6 +516,7 @@ export class SchemaType {
530
516
  }
531
517
  type KeysOfUnion<U> = U extends unknown ? keyof U : never
532
518
  type ValueAt<U, K extends PropertyKey> = U extends unknown ? (K extends keyof U ? U[K] : never) : never
519
+ type MemberProps<U> = U extends { props: infer P } ? P : never
533
520
 
534
521
  export const schemaToTypeStr = (schema: STSchema): string => {
535
522
  let type = 'unknown'
@@ -554,13 +541,13 @@ export const schemaToTypeStr = (schema: STSchema): string => {
554
541
  .map(([k, v]) => `${typeof k === 'string' ? `'${k}'` : k}${v?.[Optional] ? '?' : ''}:${schemaToTypeStr(v)}`)
555
542
  .join(';')}}`
556
543
  } else if (kind === 'json') {
557
- type = `Json<${schemaToTypeStr({ ...schema, [Kind]: schema.type })}>`
558
- } else if (kind === 'union') {
559
- let anyOf = (schema as STUnion).anyOf
560
- type = anyOf.map(s => schemaToTypeStr(s)).join('|')
544
+ type = `Json<${schemaToTypeStr((schema as STJson).value)}>`
545
+ } else if (kind === 'anyOf' || kind === 'oneOf') {
546
+ let members = (schema as STUnion).members
547
+ type = members.map(s => schemaToTypeStr(s)).join('|')
561
548
  } else if (kind === 'intersection') {
562
- let allOf = (schema as STIntersection).allOf
563
- type = allOf.map(s => schemaToTypeStr(s)).join('&')
549
+ let allOf = (schema as STIntersection<any>).allOf
550
+ type = allOf.map((s: STSchema) => schemaToTypeStr(s)).join('&')
564
551
  }
565
552
 
566
553
  // if (schema[Optional]) type = `${type}|undefined`
package/src/server.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { Context, Method, Route } from './types'
2
2
 
3
- import { InternalError, RequestError } from './types'
3
+ import { InternalServerError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
6
  import { validateResponse } from './validator'
7
- import { inferBodyType } from './util'
7
+ const normalizeContentType = (ct: string | null): string | undefined =>
8
+ ct ? ct.split(';')[0].trim() || undefined : undefined
9
+ import { readCookies, stringifyCookie } from './cookies'
8
10
 
9
11
  type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
10
12
 
@@ -13,7 +15,7 @@ const EMPTY_BODY_METHODS = ['GET', 'OPTIONS', 'HEAD']
13
15
 
14
16
  const handleInternalError = (error: any) => {
15
17
  console.error(error)
16
- return new InternalError()
18
+ return new InternalServerError()
17
19
  }
18
20
 
19
21
  const setupPluginCallbacks = (galbe: Galbe) => ({
@@ -37,24 +39,29 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
37
39
 
38
40
  async fetch(req) {
39
41
  if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
42
+ const cookies: string[] = []
40
43
  const context = {
41
44
  request: req,
42
45
  contentType: !EMPTY_BODY_METHODS.includes(req.method)
43
- ? inferBodyType(req.headers.get('content-type'))
46
+ ? normalizeContentType(req.headers.get('content-type'))
44
47
  : undefined,
45
48
  remoteAddress: server.requestIP(req),
46
- set: { headers: { 'set-cookie': [] } },
49
+ set: {
50
+ headers: { 'set-cookie': [] },
51
+ cookie: (name, value, opt = { path: '/' }) => cookies.push(stringifyCookie(name, value, opt)),
52
+ },
47
53
  state: {},
54
+ cookies: readCookies(req.headers.get('cookie')),
48
55
  } as MakeOptional<Context, 'headers' | 'params' | 'query' | 'body'>
49
- for (const p of pluginsCb.onFetch) {
50
- //@ts-ignore
51
- const r = await p.onFetch(context)
52
- if (r) return r
53
- }
54
56
  const url = new URL(req.url)
55
57
  let route: Route
56
58
  let response: any = ''
57
59
  try {
60
+ for (const p of pluginsCb.onFetch) {
61
+ //@ts-ignore
62
+ const r = await p.onFetch(context)
63
+ if (r) return r
64
+ }
58
65
  // find route
59
66
  try {
60
67
  route = router.find(req.method.toLowerCase() as Method, url.pathname)
@@ -84,7 +91,12 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
84
91
  }
85
92
  let inParams = requestPathParser(url.pathname, route.path)
86
93
 
87
- context.body = await requestBodyParser(req.body, inHeaders, schema.body, context.contentType)
94
+ context.body = await requestBodyParser(
95
+ req.body,
96
+ inHeaders,
97
+ EMPTY_BODY_METHODS.includes(req.method) ? undefined : schema.body,
98
+ context.contentType
99
+ )
88
100
  context.headers = inHeaders
89
101
  context.query = inQuery
90
102
  context.params = inParams
@@ -152,12 +164,12 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
152
164
  context.set.status = response instanceof Response ? response.status : context.set.status || 200
153
165
  },
154
166
  })
155
- if (callChain.length > 1) {
156
- let r = await callChain[0].call()
157
- if (r) response = r
158
- } else response = await handlerWrapper(context as Context)
167
+ const r = await callChain[0].call()
168
+ if (r) response = r
169
+ if (context.set.status === undefined)
170
+ context.set.status = response instanceof Response ? response.status : 200
159
171
 
160
- const parsedResponse = responseParser(response, context as Context, schema.response)
172
+ const parsedResponse = responseParser(response, context as Context, cookies, schema.response)
161
173
 
162
174
  if (galbe.config?.responseValidator?.enabled !== false && schema.response)
163
175
  validateResponse(response, schema.response, parsedResponse.status || 200)
@@ -172,17 +184,19 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
172
184
  } catch (error) {
173
185
  context.set.status = error instanceof RequestError ? error.status : 500
174
186
  let customError
175
- for (let eh of galbe.errorCb) customError = responseParser(eh(error, context as Context), context as Context)
187
+ for (let eh of galbe.errorCb)
188
+ customError = responseParser(eh(error, context as Context), context as Context, cookies)
176
189
  if (customError) return customError
177
- if (error instanceof InternalError) {
178
- console.log(`Internal Error`, error?.payload || '')
179
- return new Response('Internal Server Error', {
190
+ if (error instanceof InternalServerError) {
191
+ let internalPayload = 'Internal Server Error'
192
+ try { internalPayload = JSON.stringify(error?.payload || internalPayload) } catch {}
193
+ return new Response(internalPayload, {
180
194
  status: error.status,
181
195
  headers: { 'content-type': 'application/json' },
182
196
  })
183
197
  } else if (error instanceof RequestError) {
184
198
  let payload = error.payload
185
- let headers = new Headers(error?.headers || {})
199
+ let headers = new Headers({ ...context.set.headers, ...error?.headers })
186
200
  if (!headers.has('content-type')) {
187
201
  if (typeof error.payload === 'string') headers.set('content-type', 'text/plain')
188
202
  else {