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/util.ts ADDED
@@ -0,0 +1,12 @@
1
+ const METHOD_COLOR: Record<string, string> = {
2
+ get: '\x1b[32m',
3
+ post: '\x1b[34m',
4
+ put: '\x1b[36m',
5
+ patch: '\x1b[33m',
6
+ delete: '\x1b[31m',
7
+ options: ''
8
+ }
9
+ export const logRoute = (r: { method: string; path: string }) => {
10
+ let color = METHOD_COLOR?.[r.method] || ''
11
+ console.log(` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path}`)
12
+ }
@@ -0,0 +1,117 @@
1
+ import type { TProperties, TSchema } from '@sinclair/typebox'
2
+
3
+ import { Kind, Optional } from '@sinclair/typebox'
4
+
5
+ export const validate = (elt: any, schema: TSchema, parse = false): any => {
6
+ type ValidationError = string | string[] | { [key: string]: ValidationError }
7
+ const errors: ValidationError[] = []
8
+ const iElt = elt
9
+
10
+ if (schema[Kind] === 'Boolean') {
11
+ if (parse && typeof elt === 'string') elt = elt === 'true' ? true : elt === 'false' ? false : null
12
+ if (elt !== true && elt !== false) throw `${iElt} is not a valid boolean. Should be 'true' or 'false'`
13
+ } else if (schema[Kind] === 'Integer') {
14
+ if (parse && typeof elt === 'string') elt = Number(elt)
15
+ if (!Number.isInteger(elt)) throw `${iElt} is not a valid integer`
16
+ schemaValidation(elt, schema)
17
+ } else if (schema[Kind] === 'Number') {
18
+ if (parse && typeof elt === 'string') elt = Number(elt)
19
+ if (!Number.isFinite(elt)) throw `${iElt} is not a valid number`
20
+ schemaValidation(elt, schema)
21
+ } else if (schema[Kind] === 'String') {
22
+ if (!(typeof elt === 'string')) throw `${iElt} is not a valid string`
23
+ schemaValidation(elt, schema)
24
+ } else if (schema[Kind] === 'Literal') {
25
+ if (elt !== schema.const) throw `${iElt} is not a valid value`
26
+ } else if (schema[Kind] === 'Object') {
27
+ if (parse && typeof elt === 'string') {
28
+ try {
29
+ elt = JSON.parse(elt)
30
+ } catch {
31
+ throw `${iElt} is not a valid object`
32
+ }
33
+ }
34
+ if (typeof elt !== 'object') throw `${iElt} is not a valid object`
35
+ if (Array.isArray(elt)) throw `Expected an object, not an array`
36
+ const err: ValidationError = {}
37
+ Object.entries(schema.properties as TProperties).forEach(([k, s]) => {
38
+ if (!(k in elt)) {
39
+ if (!s[Optional]) err[k] = 'Required'
40
+ return
41
+ }
42
+ try {
43
+ validate(elt[k], s, parse)
44
+ } catch (e) {
45
+ err[k] = e as ValidationError
46
+ }
47
+ })
48
+ if (Object.keys(err).length) errors.push(err)
49
+ } else if (schema[Kind] === 'Array') {
50
+ if (parse && typeof elt === 'string') {
51
+ try {
52
+ elt = JSON.parse(elt)
53
+ } catch (error) {
54
+ throw 'Not a valid array'
55
+ }
56
+ }
57
+ if (!Array.isArray(elt)) throw 'Not a valid array'
58
+ schemaValidation(elt, schema)
59
+ } else if (schema[Kind] === 'ByteArray') {
60
+ if (parse && typeof elt === 'string') elt = Uint8Array.from(elt, c => c.charCodeAt(0))
61
+ else if (parse && Array.isArray(elt)) elt = new Uint8Array(elt)
62
+ if (!(elt instanceof Uint8Array)) throw 'Not a valid ByteArray'
63
+ } else if (schema[Kind] === 'Union') {
64
+ const union = Object.values(schema.anyOf)
65
+ let valid = false
66
+ for (const u of union) {
67
+ try {
68
+ elt = validate(elt, u as TSchema, parse)
69
+ valid = true
70
+ break
71
+ } catch (err) {
72
+ continue
73
+ }
74
+ }
75
+ // @ts-ignore
76
+ if (!valid) throw `${elt} could not be parsed to any of: ${union.map(u => u?.const ?? u[Kind]).join(', ')}`
77
+ } else if (schema[Kind] === 'Any') {
78
+ } else {
79
+ throw `Unsupported schema type ${schema[Kind]}`
80
+ }
81
+
82
+ if (Object.keys(errors).length === 1) throw errors[0]
83
+ if (Object.keys(errors).length > 1) throw errors
84
+
85
+ return elt
86
+ }
87
+
88
+ const schemaValidation = (value: any, schema: TSchema) => {
89
+ const errors = []
90
+ if (schema[Kind] === 'Integer' || schema[Kind] === 'Number') {
91
+ if (schema.exclusiveMinimum !== undefined)
92
+ if ((value as number) <= schema.exclusiveMinimum)
93
+ errors.push(`${value} is less or equal to ${schema.exclusiveMinimum}`)
94
+ if (schema.exclusiveMaximum !== undefined)
95
+ if ((value as number) >= schema.exclusiveMaximum)
96
+ errors.push(`${value} is greater or equal to ${schema.exclusiveMaximum}`)
97
+ if (schema.minimum !== undefined)
98
+ if ((value as number) < schema.minimum) errors.push(`${value} is less than ${schema.minimum}`)
99
+ if (schema.maximum !== undefined)
100
+ if ((value as number) > schema.maximum) errors.push(`${value} is greater than ${schema.maximum}`)
101
+ } else if (schema[Kind] === 'String') {
102
+ if (schema.minLength !== undefined && (value as string).length < schema.minLength)
103
+ errors.push(`${value} length is too small (${schema.minLength} char min)`)
104
+ if (schema.maxLength !== undefined && (value as string).length > schema.maxLength)
105
+ errors.push(`${value} length is too large (${schema.maxLength} char max)`)
106
+ if (schema.pattern !== undefined && !(value as string).match(new RegExp(schema.pattern)))
107
+ errors.push(`${value} does not match pattern ${schema.pattern}`)
108
+ } else if (schema[Kind] === 'Array') {
109
+ if (schema.minItems !== undefined && (value as any[]).length < schema.minItems)
110
+ errors.push(`Must contain at least ${schema.minItems} item${schema.minItems > 1 ? 's' : ''}`)
111
+ if (schema.maxItems !== undefined && (value as any[]).length > schema.maxItems)
112
+ errors.push(`Must contain at most (${schema.maxItems} item${schema.maxItems > 1 ? 's' : ''}`)
113
+ if (schema.uniqueItems === true && new Set(value as any[]).size !== (value as any[]).length)
114
+ errors.push(`Has duplicate values`)
115
+ }
116
+ if (errors.length) throw Array.isArray(errors) && errors.length === 1 ? errors[0] : errors
117
+ }
@@ -0,0 +1,177 @@
1
+ import { expect, test, describe } from 'bun:test'
2
+ import { Galbe } from '../src'
3
+
4
+ const port = 7360
5
+
6
+ describe('hooks', async () => {
7
+ const galbe = new Galbe()
8
+ await galbe.listen(port)
9
+
10
+ test('hooks, empty', async () => {
11
+ galbe.get('/hooks/empty', [], () => 'handled')
12
+
13
+ let resp = await fetch(`http://localhost:${port}/hooks/empty`, {
14
+ method: 'GET'
15
+ })
16
+ expect(resp.status).toBe(200)
17
+ expect(await resp?.text()).toBe('handled')
18
+ })
19
+
20
+ test('hooks, void', async () => {
21
+ galbe.get('/hooks/void', [(_ctx, _next) => {}], () => 'handled')
22
+
23
+ let resp = await fetch(`http://localhost:${port}/hooks/void`, {
24
+ method: 'GET'
25
+ })
26
+ expect(resp.status).toBe(200)
27
+ expect(await resp?.text()).toBe('handled')
28
+ })
29
+
30
+ test('hooks, called without next', async () => {
31
+ let hookCalled = 0
32
+ galbe.get(
33
+ '/hooks/called',
34
+ [
35
+ _ => {
36
+ hookCalled++
37
+ }
38
+ ],
39
+ () => 'handled'
40
+ )
41
+ expect(hookCalled).toBe(0)
42
+ let resp = await fetch(`http://localhost:${port}/hooks/called`, {
43
+ method: 'GET'
44
+ })
45
+ expect(hookCalled).toBe(1)
46
+ expect(resp.status).toBe(200)
47
+ expect(await resp?.text()).toBe('handled')
48
+ })
49
+
50
+ test('hooks, called with next', async () => {
51
+ let hookCalled = 0
52
+ galbe.get(
53
+ '/hooks/called',
54
+ [
55
+ async (_, next) => {
56
+ hookCalled++
57
+ await Bun.sleep(10)
58
+ await next()
59
+ }
60
+ ],
61
+ () => 'handled'
62
+ )
63
+ expect(hookCalled).toBe(0)
64
+ let resp = await fetch(`http://localhost:${port}/hooks/called`, {
65
+ method: 'GET'
66
+ })
67
+ expect(hookCalled).toBe(1)
68
+ expect(resp.status).toBe(200)
69
+ expect(await resp?.text()).toBe('handled')
70
+ })
71
+
72
+ test('hooks, wrapper hook', async () => {
73
+ let before = 0
74
+ let after = 0
75
+ galbe.get(
76
+ '/hooks/called',
77
+ [
78
+ async (_, next) => {
79
+ before++
80
+ await Bun.sleep(10)
81
+ await next()
82
+ after++
83
+ }
84
+ ],
85
+ () => {
86
+ expect(before).toBe(1)
87
+ expect(after).toBe(0)
88
+ return 'handled'
89
+ }
90
+ )
91
+ expect(before).toBe(0)
92
+ expect(after).toBe(0)
93
+ let resp = await fetch(`http://localhost:${port}/hooks/called`, {
94
+ method: 'GET'
95
+ })
96
+ expect(before).toBe(1)
97
+ expect(after).toBe(1)
98
+ expect(resp.status).toBe(200)
99
+ expect(await resp?.text()).toBe('handled')
100
+ })
101
+
102
+ test('hooks, nested wrappers', async () => {
103
+ let before1 = 0
104
+ let after1 = 0
105
+ let before2 = 0
106
+ let after2 = 0
107
+ galbe.get(
108
+ '/hooks/called',
109
+ [
110
+ async (_, next) => {
111
+ before1++
112
+ await Bun.sleep(10)
113
+ await next()
114
+ after1++
115
+ },
116
+ async (_, next) => {
117
+ before2++
118
+ await Bun.sleep(10)
119
+ await next()
120
+ after2++
121
+ }
122
+ ],
123
+ () => {
124
+ expect(before1).toBe(1)
125
+ expect(before2).toBe(1)
126
+ expect(after1).toBe(0)
127
+ expect(after2).toBe(0)
128
+ return 'handled'
129
+ }
130
+ )
131
+ expect(before1).toBe(0)
132
+ expect(before2).toBe(0)
133
+ expect(after1).toBe(0)
134
+ expect(after2).toBe(0)
135
+ let resp = await fetch(`http://localhost:${port}/hooks/called`, {
136
+ method: 'GET'
137
+ })
138
+ expect(before1).toBe(1)
139
+ expect(before2).toBe(1)
140
+ expect(after1).toBe(1)
141
+ expect(after2).toBe(1)
142
+ expect(resp.status).toBe(200)
143
+ expect(await resp?.text()).toBe('handled')
144
+ })
145
+
146
+ test('hooks, linear chaining', async () => {
147
+ let hook1 = 0
148
+ let hook2 = 0
149
+ galbe.get(
150
+ '/hooks/called',
151
+ [
152
+ async _ => {
153
+ hook1++
154
+ await Bun.sleep(10)
155
+ },
156
+ async _ => {
157
+ hook2++
158
+ await Bun.sleep(10)
159
+ }
160
+ ],
161
+ () => {
162
+ expect(hook1).toBe(1)
163
+ expect(hook2).toBe(1)
164
+ return 'handled'
165
+ }
166
+ )
167
+ expect(hook1).toBe(0)
168
+ expect(hook2).toBe(0)
169
+ let resp = await fetch(`http://localhost:${port}/hooks/called`, {
170
+ method: 'GET'
171
+ })
172
+ expect(hook1).toBe(1)
173
+ expect(hook2).toBe(1)
174
+ expect(resp.status).toBe(200)
175
+ expect(await resp?.text()).toBe('handled')
176
+ })
177
+ })