galbe 0.6.2 → 0.7.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.
@@ -108,16 +108,22 @@ const parseOapiSchema = (
108
108
  title: os.title,
109
109
  description: os.description
110
110
  }
111
+ let resp = ''
111
112
  let hasOptions = Object.values(options).some(v => !!v)
112
113
  let optArg = hasOptions ? `, ${JSON.stringify(options)}` : ''
113
114
  let anyOf = os.oneOf || os.anyOf || os.allOf
115
+ let required = os.required
116
+ let nullable = os.nullable
117
+
114
118
  if (anyOf?.length) {
115
- return `$T.union([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}], ${JSON.stringify(
116
- options
117
- )})`
118
- }
119
- if (os.type === 'boolean') return `$T.boolean(${hasOptions ? JSON.stringify(options) : ''})`
120
- if (os.type === 'number') {
119
+ if (anyOf.length === 1) resp = parseOapiSchema(anyOf[0] as OpenAPIV3.SchemaObject, details, extra)
120
+ else {
121
+ resp = `$T.union([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}], ${JSON.stringify(
122
+ options
123
+ )})`
124
+ }
125
+ } else if (os.type === 'boolean') resp = `$T.boolean(${hasOptions ? JSON.stringify(options) : ''})`
126
+ else if (os.type === 'number') {
121
127
  let { max, min, exclusiveMax, exclusiveMin } = {
122
128
  max: os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined,
123
129
  min: os.minimum !== undefined && !os.exclusiveMinimum ? os.minimum : undefined,
@@ -126,49 +132,52 @@ const parseOapiSchema = (
126
132
  }
127
133
  options = { ...options, min, max, exclusiveMax, exclusiveMin }
128
134
  hasOptions = Object.values(options).some(v => !!v)
129
- return `$T.number(${hasOptions ? JSON.stringify(options) : ''})`
130
- }
131
- if (os.type === 'integer') {
135
+ resp = `$T.number(${hasOptions ? JSON.stringify(options) : ''})`
136
+ } else if (os.type === 'integer') {
132
137
  let max = os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined
133
138
  let min = os.minimum !== undefined && !os.exclusiveMinimum ? os.minimum : undefined
134
139
  let exclusiveMax = os.maximum !== undefined && os.exclusiveMaximum ? os.maximum : undefined
135
140
  let exclusiveMin = os.minimum !== undefined && os.exclusiveMinimum ? os.minimum : undefined
136
141
  options = { ...options, min, max, exclusiveMax, exclusiveMin }
137
142
  hasOptions = Object.values(options).some(v => !!v)
138
- return `$T.integer(${hasOptions ? JSON.stringify(options) : ''})`
139
- }
140
- if (os.type === 'string') {
141
- if (os.format === 'binary') return `$T.byteArray(${hasOptions ? JSON.stringify(options) : ''})`
142
- let minLength = os.minLength
143
- let maxLength = os.maxLength
144
- let pattern = os.pattern
145
- options = { ...options, minLength, maxLength, pattern }
146
- hasOptions = Object.values(options).some(v => !!v)
147
- return `$T.string(${hasOptions ? JSON.stringify(options) : ''})`
148
- }
149
- if (os.type === 'array') {
143
+ resp = `$T.integer(${hasOptions ? JSON.stringify(options) : ''})`
144
+ } else if (os.type === 'string') {
145
+ if (os.format === 'binary') resp = `$T.byteArray(${hasOptions ? JSON.stringify(options) : ''})`
146
+ else {
147
+ let minLength = os.minLength
148
+ let maxLength = os.maxLength
149
+ let pattern = os.pattern
150
+ options = { ...options, minLength, maxLength, pattern }
151
+ hasOptions = Object.values(options).some(v => !!v)
152
+ resp = `$T.string(${hasOptions ? JSON.stringify(options) : ''})`
153
+ }
154
+ } else if (os.type === 'array') {
150
155
  let minItems = os.minItems
151
156
  let maxItems = os.maxItems
152
157
  let unique = os.uniqueItems
153
158
  options = { ...options, minItems, maxItems, unique }
154
- return `$T.array(${parseOapiSchema(os?.items)}${optArg})`
155
- }
156
- if (os.type === 'object') {
159
+ resp = `$T.array(${parseOapiSchema(os?.items)}${optArg})`
160
+ } else if (os.type === 'object') {
157
161
  if (extra?.media === 'multipart/form-data') {
158
- return `$T.multipartForm({${Object.entries(os?.properties || {})
162
+ resp = `$T.multipartForm({${Object.entries(os?.properties || {})
159
163
  .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
160
164
  .join(',')}}${optArg})`
161
- }
162
- if (extra?.media === 'application/x-www-form-urlencoded') {
163
- return `$T.urlForm({${Object.entries(os?.properties || {})
165
+ } else if (extra?.media === 'application/x-www-form-urlencoded') {
166
+ resp = `$T.urlForm({${Object.entries(os?.properties || {})
167
+ .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
168
+ .join(',')}}${optArg})`
169
+ } else {
170
+ resp = `$T.object({${Object.entries(os?.properties || {})
164
171
  .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
165
172
  .join(',')}}${optArg})`
166
173
  }
167
- return `$T.object({${Object.entries(os?.properties || {})
168
- .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
169
- .join(',')}}${optArg})`
170
- }
171
- throw new Error(`Unknown schema type ${os.type}`)
174
+ } else throw new Error(`Unknown schema type ${os.type}`)
175
+
176
+ if (!required && nullable) resp = `$T.nullish(${resp})`
177
+ else if (!required) resp = `$T.optional(${resp})`
178
+ else if (nullable) resp = `$T.nullable(${resp})`
179
+
180
+ return resp
172
181
  }
173
182
 
174
183
  const buildSchemaIndex = (def: OpenAPIV3.Document) => {
@@ -217,7 +226,7 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
217
226
  let imports = {}
218
227
  let p = path.replaceAll(/\{([^\}]*)\}/g, ':$1')
219
228
  let description = def.summary || def.description
220
- let pathName = path.replaceAll(/\/\{[^\}]*\}/g, 'X').replaceAll(/[^$\w\d-_]([$\w\d-_])/g, (_, $1) => $1.toUpperCase())
229
+ let pathName = path.replaceAll(/\/\{[^\}]*\}/g, 'X').replaceAll(/[^$\w\d_]+([$\w\d_])/g, (_, $1) => $1.toUpperCase())
221
230
  let schemaName = `${method}${pathName}`.replace(/^\w/, c => c.toUpperCase())
222
231
 
223
232
  let meta = '/**\n'
package/docs/schemas.md CHANGED
@@ -44,6 +44,14 @@ Schema Type matching integer `number` values.
44
44
  const intSchema = $T.integer(options)
45
45
  ```
46
46
 
47
+ #### Null
48
+
49
+ Schema Type matching `null` values.
50
+
51
+ ```ts
52
+ const nullSchema = $T.null(options)
53
+ ```
54
+
47
55
  #### Any
48
56
 
49
57
  Schema Type matching `any` of the previous Schema Types.
@@ -68,6 +76,22 @@ Makes any type optional. This allows for `undefined` values.
68
76
  const optionalSchema = $T.optional($T.string())
69
77
  ```
70
78
 
79
+ #### Nullable
80
+
81
+ Makes any type nullable. This allows for `null` values.
82
+
83
+ ```ts
84
+ const nullableSchema = $T.nullable($T.string())
85
+ ```
86
+
87
+ #### Nullish
88
+
89
+ Makes any type nullish. This allows for `undefined` and `null` values.
90
+
91
+ ```ts
92
+ const nullishSchema = $T.nullish($T.string())
93
+ ```
94
+
71
95
  #### Union
72
96
 
73
97
  Creates an union of Schema Types.
@@ -155,7 +179,7 @@ const schema = {
155
179
  body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMulripartForm | STUrlForm | STStream
156
180
  ```
157
181
 
158
- #### Json
182
+ #### Object
159
183
 
160
184
  To define an `application/json` request body, use `STObject` Schema Type. Example:
161
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
@@ -51,8 +51,12 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
51
51
  //@ts-ignore
52
52
  return { schema: { $ref: `#/components/schemas/${schema.id}` } }
53
53
  }
54
- // TODO add constraints min, max etc.
55
- if (kind === 'boolean') s = { type: 'boolean' }
54
+
55
+ if (kind === 'null') {
56
+ s = {
57
+ anyOf: ['null']
58
+ }
59
+ } else if (kind === 'boolean') s = { type: 'boolean' }
56
60
  else if (kind === 'byteArray') s = { type: 'string', format: 'byte' }
57
61
  else if (kind === 'number')
58
62
  s = {
@@ -118,10 +122,23 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
118
122
  }
119
123
  } else if (kind === 'union') {
120
124
  let anyOf = (schema as STUnion).anyOf
121
- s = {
122
- anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
125
+ let nullable = anyOf.some(s => s[Kind] === 'null')
126
+ anyOf = anyOf.filter(s => s[Kind] !== 'null')
127
+
128
+ if (anyOf.length === 0) {
129
+ s = {}
130
+ } else if (anyOf.length === 1) {
131
+ s = schemaToOpenapi(anyOf[0]).schema
132
+ } else if (anyOf.length > 1) {
133
+ s = {
134
+ anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
135
+ }
123
136
  }
137
+
138
+ //@ts-ignore
139
+ if (nullable) s.nullable = nullable
124
140
  }
141
+
125
142
  s = { title: schema.title, description: schema.description, ...s }
126
143
  if (components.schemas && schema.id) {
127
144
  components.schemas[schema.id] = s
package/src/parser.ts CHANGED
@@ -56,7 +56,7 @@ export const requestBodyParser = async (
56
56
  }
57
57
  })
58
58
  } else return new Uint8Array()
59
- } else if (schema?.[Optional]) {
59
+ } else if (schema?.[Optional] || kind === 'null') {
60
60
  return null
61
61
  } else {
62
62
  throw new RequestError({ status: 400, payload: { body: `Not a valid ${kind}` } })
@@ -75,7 +75,8 @@ export const requestBodyParser = async (
75
75
  } else return null
76
76
  }
77
77
  } else {
78
- if (kind === 'byteArray') {
78
+ if (kind === 'null') throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
79
+ else if (kind === 'byteArray') {
79
80
  if (isStream) return rsToAsyncIterator(body)
80
81
  const bytes = await readableStreamToArrayBuffer(body)
81
82
  return new Uint8Array(bytes)
@@ -435,7 +436,7 @@ const parseMultipartContent = (
435
436
  } else {
436
437
  throw new RequestError({
437
438
  status: 400,
438
- payload: { body: { [headers.name]: `Expect ${schema?.props[headers.name][Kind]} found json` } }
439
+ payload: { body: { [headers.name]: `Expected ${schema?.props[headers.name][Kind]} found json` } }
439
440
  })
440
441
  }
441
442
  }
package/src/router.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Method, Route, RouteNode } from './types'
2
2
  import { MethodNotAllowedError, NotFoundError } from './types'
3
3
 
4
- const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d][\w-]+[\w\d]))*\/?$/
4
+ const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d.][\w-.]+[\w\d]))*\/?$/
5
5
 
6
6
  const walk = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
7
7
  if (path.length < 1) throw new NotFoundError()
package/src/routes.ts CHANGED
@@ -231,7 +231,7 @@ export const defineRoutes = async (
231
231
  if (!routes) return
232
232
  const root = process.cwd()
233
233
  if (typeof routes === 'string') {
234
- for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false })) {
234
+ for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false, dot: true })) {
235
235
  const isDir = (await lstat(path)).isDirectory()
236
236
 
237
237
  let files: string[] = []
package/src/schema.ts CHANGED
@@ -31,6 +31,7 @@ export interface ArrayOptions extends Options {
31
31
  }
32
32
  export interface STSchema extends Options {
33
33
  [Kind]:
34
+ | 'null'
34
35
  | 'boolean'
35
36
  | 'byteArray'
36
37
  | 'number'
@@ -61,6 +62,7 @@ export type STPropsValue =
61
62
  | STObject
62
63
  | STUnion
63
64
  | STAny
65
+ | STNull
64
66
  export type STProps = Record<string | number, STPropsValue>
65
67
 
66
68
  type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
@@ -84,6 +86,17 @@ export type STStream<T extends STSchema = STSchema> = T & {
84
86
  [Stream]: true
85
87
  }
86
88
 
89
+ // Null
90
+ export interface STNull extends STSchema, Options {
91
+ [Kind]: 'null'
92
+ static: null
93
+ }
94
+ export function _Null(options: Options = {}): STNull {
95
+ return {
96
+ ...options,
97
+ [Kind]: 'null'
98
+ } as unknown as STNull
99
+ }
87
100
  // ByteArray
88
101
  export interface STByteArray extends STSchema, ByteArrayOptions {
89
102
  [Kind]: 'byteArray'
@@ -310,12 +323,28 @@ export function _Stream<T extends STStreamable>(schema: T): STStream<T> {
310
323
  [Stream]: true
311
324
  } as unknown as STStream<T>
312
325
  }
326
+ // Nullable
327
+ type STNullable<T extends STSchema> = STUnion<[T, STNull]>
328
+ // Nullish
329
+ type STNullish<T extends STSchema> = STOptional<STNullable<T>>
313
330
 
314
331
  export class SchemaType {
315
332
  /** Creates an Optional Schema Type Wrapper*/
316
333
  public optional<T extends STSchema>(schema: T): STOptional<T> {
317
334
  return { ...schema, [Optional]: true }
318
335
  }
336
+ /** Creates an Nullable Schema Type Wrapper*/
337
+ public nullable<T extends STSchema>(schema: T): STNullable<T> {
338
+ return { ..._Union([schema, _Null()], {}) }
339
+ }
340
+ /** Creates an Nullish Schema Type Wrapper*/
341
+ public nullish<T extends STSchema>(schema: T): STNullish<T> {
342
+ return { ..._Union([schema, _Null()], {}), [Optional]: true }
343
+ }
344
+ /** Creates a Null Schema Type */
345
+ public null(options: Options = {}): STNull {
346
+ return _Null(options)
347
+ }
319
348
  /** Creates a ByteArray Schema Type */
320
349
  public byteArray(options: ByteArrayOptions = {}): STByteArray {
321
350
  return _ByteArray(options)
@@ -419,7 +448,8 @@ export const schemaToTypeStr = (schema: STSchema): string => {
419
448
  let type = 'unknown'
420
449
  let kind = schema[Kind]
421
450
 
422
- if (kind === 'boolean') type = 'boolean'
451
+ if (kind === 'null') type = 'null'
452
+ else if (kind === 'boolean') type = 'boolean'
423
453
  else if (kind === 'byteArray') type = 'Uint8Array'
424
454
  else if (kind === 'number') type = 'number'
425
455
  else if (kind === 'integer') type = 'number'
package/src/server.ts CHANGED
@@ -4,7 +4,6 @@ import { InternalError, 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 { logger } from 'girok'
8
7
 
9
8
  type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
10
9
 
@@ -140,7 +139,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
140
139
  callChain.push({
141
140
  call: async () => {
142
141
  response = await handlerWrapper(context as Context)
143
- context.set.status = response instanceof Response ? response.status : 200
142
+ context.set.status = response instanceof Response ? response.status : context.set.status || 200
144
143
  }
145
144
  })
146
145
  if (callChain.length > 1) {
package/src/validator.ts CHANGED
@@ -8,7 +8,9 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
8
8
  const errors: ValidationError[] = []
9
9
  const iElt = elt
10
10
 
11
- if (schema[Kind] === 'boolean') {
11
+ if (schema[Kind] === 'null') {
12
+ if (elt !== null) throw `Expected null value got ${iElt}`
13
+ } else if (schema[Kind] === 'boolean') {
12
14
  if (typeof elt === 'string') {
13
15
  if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
14
16
  else throw `Expected boolean, got string.`
@@ -324,14 +324,28 @@ describe('parser', () => {
324
324
  bool: false,
325
325
  object: {},
326
326
  array: [],
327
- any: false
327
+ any: false,
328
+ null: null,
329
+ nullable: null,
330
+ nullish: 42
328
331
  }),
329
332
  type,
330
333
  schema,
331
334
  expected: {
332
335
  status: 200,
333
336
  type: 'object',
334
- resp: { ba: '', string: '', number: 0, bool: false, object: {}, array: [], any: false }
337
+ resp: {
338
+ ba: '',
339
+ string: '',
340
+ number: 0,
341
+ bool: false,
342
+ object: {},
343
+ array: [],
344
+ any: false,
345
+ null: null,
346
+ nullable: null,
347
+ nullish: 42
348
+ }
335
349
  }
336
350
  },
337
351
  {
@@ -343,7 +357,10 @@ describe('parser', () => {
343
357
  object: { foo: 'bar' },
344
358
  array: [false, 'one', 2],
345
359
  any: '36',
346
- optional: 'optional'
360
+ optional: 'optional',
361
+ null: null,
362
+ nullable: 'test',
363
+ nullish: null
347
364
  }),
348
365
  type,
349
366
  schema,
@@ -358,7 +375,10 @@ describe('parser', () => {
358
375
  object: { foo: 'bar' },
359
376
  array: [false, 'one', 2],
360
377
  any: '36',
361
- optional: 'optional'
378
+ optional: 'optional',
379
+ null: null,
380
+ nullable: 'test',
381
+ nullish: null
362
382
  }
363
383
  }
364
384
  },
@@ -370,7 +390,8 @@ describe('parser', () => {
370
390
  bool: 'x',
371
391
  object: [],
372
392
  array: {},
373
- any: {}
393
+ any: {},
394
+ null: ''
374
395
  }),
375
396
  type,
376
397
  schema,
@@ -383,7 +404,9 @@ describe('parser', () => {
383
404
  number: 'a is not a valid number',
384
405
  bool: "x is not a valid boolean. Should be 'true' or 'false'",
385
406
  object: 'Expected an object, not an array',
386
- array: 'Not a valid array'
407
+ array: 'Not a valid array',
408
+ null: 'Expected null value got ',
409
+ nullable: 'Required'
387
410
  }
388
411
  }
389
412
  }
@@ -13,18 +13,7 @@ describe('router', () => {
13
13
  test('routes, bad syntax', async () => {
14
14
  const galbe = new Galbe()
15
15
 
16
- const invalidPaths = [
17
- '.',
18
- '/@',
19
- '/-ta',
20
- '/test/-ta',
21
- '/test/my.path',
22
- '/hell@/w0rld',
23
- '/last-',
24
- '../',
25
- './x',
26
- '/hello?'
27
- ]
16
+ const invalidPaths = ['.', '/@', '/-ta', '/test/-ta', '/hell@/w0rld', '/last-', '../', './x', '/hello?']
28
17
 
29
18
  for (const p of invalidPaths) {
30
19
  try {
@@ -32,7 +32,10 @@ export const schema_objectBase = {
32
32
  export const schema_object = {
33
33
  ...schema_objectBase,
34
34
  object: $T.object($T.any()),
35
- array: $T.array()
35
+ array: $T.array(),
36
+ null: $T.null(),
37
+ nullable: $T.nullable($T.string()),
38
+ nullish: $T.nullish($T.number())
36
39
  }
37
40
 
38
41
  export const isAsyncIterator = (obj: any) => {