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/.github/workflows/build_test.yml +19 -0
- package/.github/workflows/deploy_website.yml +20 -0
- package/.prettierrc +10 -0
- package/README.md +39 -0
- package/bin/cli.ts +114 -0
- package/bun.lockb +0 -0
- package/docs/Getting Started/basic.md +1 -0
- package/docs/Guides/routes.md +1 -0
- package/docs/getting-started.md +5 -0
- package/package.json +49 -0
- package/scripts/build.ts +14 -0
- package/scripts/hooks/prepare-commit-msg +10 -0
- package/scripts/postinstall.ts +9 -0
- package/src/index.ts +353 -0
- package/src/parser.ts +656 -0
- package/src/router.ts +100 -0
- package/src/routes.ts +157 -0
- package/src/server.ts +174 -0
- package/src/types.ts +279 -0
- package/src/util.ts +12 -0
- package/src/validator.ts +117 -0
- package/test/hooks.test.ts +177 -0
- package/test/parser.test.ts +1198 -0
- package/test/plugins.test.ts +239 -0
- package/test/requests.test.ts +909 -0
- package/test/resources/image.png +0 -0
- package/test/resources/object.badSyntax.json +8 -0
- package/test/resources/object.json +8 -0
- package/test/resources/object.missing.json +6 -0
- package/test/resources/test.route.comment.ts +38 -0
- package/test/resources/test.route.empty.ts +9 -0
- package/test/responses.test.ts +109 -0
- package/test/routeFiles.test.ts +209 -0
- package/test/router.test.ts +207 -0
- package/test/test.utils.ts +102 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Build & Test
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
pull_request:
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
name: Build & Test
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: oven-sh/setup-bun@v1
|
|
12
|
+
with:
|
|
13
|
+
bun-version: latest
|
|
14
|
+
- name: Install
|
|
15
|
+
run: bun install
|
|
16
|
+
- name: Build
|
|
17
|
+
run: bun run build
|
|
18
|
+
- name: Test
|
|
19
|
+
run: bun test
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Redeploy website
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
paths:
|
|
6
|
+
- 'docs/**'
|
|
7
|
+
jobs:
|
|
8
|
+
redeploy_website:
|
|
9
|
+
name: Update website with new doc
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Trigger website deployment
|
|
13
|
+
run: |
|
|
14
|
+
curl -L \
|
|
15
|
+
-X POST \
|
|
16
|
+
-H "Accept: application/vnd.github+json" \
|
|
17
|
+
-H "Authorization: Bearer ${{ secrets.GH_WEBSITE_TOKEN }}" \
|
|
18
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
19
|
+
https://api.github.com/repos/pierre-cm/galbe-website/actions/workflows/ci.yml/dispatches \
|
|
20
|
+
-d '{"ref":"main"}'
|
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Todos
|
|
2
|
+
|
|
3
|
+
- [x] Params and QueryParams as {} instead of Type.Object({})
|
|
4
|
+
- [x] Body validator (/ parser?)
|
|
5
|
+
- [x] Hooks
|
|
6
|
+
- [x] Export limited version of Typebox
|
|
7
|
+
- [x] Headers
|
|
8
|
+
- [x] Details
|
|
9
|
+
- [x] Build
|
|
10
|
+
|
|
11
|
+
- Own Framework
|
|
12
|
+
|
|
13
|
+
- [x] Types
|
|
14
|
+
- [x] errorHandler
|
|
15
|
+
- [x] plugin use()
|
|
16
|
+
- [ ] jsdoc
|
|
17
|
+
- [x] on the fly body request validation (REST handle parsing of Multipart arrays + Multipart static )
|
|
18
|
+
|
|
19
|
+
- [ ] Unit tests
|
|
20
|
+
|
|
21
|
+
- Todo api example
|
|
22
|
+
|
|
23
|
+
- [x] Create
|
|
24
|
+
- [x] GetList
|
|
25
|
+
- [x] Get
|
|
26
|
+
- [x] Update
|
|
27
|
+
- [x] Delete
|
|
28
|
+
- [ ] Bulk create from file
|
|
29
|
+
- [ ] Dl as file
|
|
30
|
+
|
|
31
|
+
- Hooks
|
|
32
|
+
|
|
33
|
+
- [ ] Headers authentication pos/update/delete
|
|
34
|
+
|
|
35
|
+
- [ ] Documentation
|
|
36
|
+
|
|
37
|
+
- [ ] create-kadre
|
|
38
|
+
|
|
39
|
+
- [ ] Website
|
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import type { RouteMeta } from '../src/routes'
|
|
5
|
+
import { program } from 'commander'
|
|
6
|
+
import { relative, resolve } from 'path'
|
|
7
|
+
import { mkdir, readdir, rm } from 'fs/promises'
|
|
8
|
+
import { metaAnalysis } from '../src/routes'
|
|
9
|
+
import { randomUUID } from 'crypto'
|
|
10
|
+
import { glob } from 'glob'
|
|
11
|
+
import { Galbe } from '../src'
|
|
12
|
+
|
|
13
|
+
const ROOT = process.cwd()
|
|
14
|
+
const BUILD_ID = randomUUID()
|
|
15
|
+
|
|
16
|
+
Bun.env.FORCE_COLOR = '1'
|
|
17
|
+
|
|
18
|
+
const parseRoutes = async (routes?: string | string[]): Promise<{ path: string; meta: RouteMeta }[]> => {
|
|
19
|
+
if (!routes) return []
|
|
20
|
+
let files: { path: string; meta: RouteMeta }[] = []
|
|
21
|
+
if (typeof routes === 'string') {
|
|
22
|
+
let filePaths = await glob(routes, { cwd: ROOT, withFileTypes: true, ignore: 'node_modules/**' })
|
|
23
|
+
for (const file of filePaths.map(f => ({ name: f.name, path: f.path, type: f.getType() }))) {
|
|
24
|
+
const path = `${file.path}/${file.name}`
|
|
25
|
+
if (file.type === 'File') files.push({ path, meta: await metaAnalysis(path) })
|
|
26
|
+
else if (file.type === 'Directory') {
|
|
27
|
+
files = files.concat(
|
|
28
|
+
await Promise.all(
|
|
29
|
+
(
|
|
30
|
+
await readdir(`${file.path}/${file.name}`)
|
|
31
|
+
).map(async f => ({
|
|
32
|
+
path: `${file.path}/${f}`,
|
|
33
|
+
meta: await metaAnalysis(`${file.path}/${f}`)
|
|
34
|
+
}))
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(routes)) for (const r of routes) files = files.concat(await parseRoutes(r))
|
|
41
|
+
return files
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const createBuildIndex = async (indexPath: string, routes: { path: string; meta: RouteMeta }[]) => {
|
|
45
|
+
const buildPath = resolve(ROOT, '.galbe', 'build', BUILD_ID)
|
|
46
|
+
await mkdir(buildPath, { recursive: true })
|
|
47
|
+
await Bun.write(
|
|
48
|
+
resolve(buildPath, 'index.ts'),
|
|
49
|
+
`import galbe from '${relative(buildPath, indexPath)}';
|
|
50
|
+
${routes.map((r, idx) => `import _${idx} from '${relative(buildPath, r.path)}'`).join(';\n')}
|
|
51
|
+
${routes
|
|
52
|
+
.map(
|
|
53
|
+
(r, idx) => `galbe.routesMetadata = {...${JSON.stringify(r.meta)}}
|
|
54
|
+
_${idx}(galbe)`
|
|
55
|
+
)
|
|
56
|
+
.join(';\n')}
|
|
57
|
+
galbe.listen();
|
|
58
|
+
`
|
|
59
|
+
)
|
|
60
|
+
return resolve(buildPath, 'index.ts')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
program.name('galbe').description('CLI to execute galbe utilities').version('0.1.0')
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('dev')
|
|
67
|
+
.description('Run a dev server running your galbe API')
|
|
68
|
+
.argument('<string>', 'filename')
|
|
69
|
+
.option('-p, --port <number>', 'port number', '')
|
|
70
|
+
.action(async (fileName, props) => {
|
|
71
|
+
const { port } = props
|
|
72
|
+
const devRoot = resolve(ROOT, '.galbe', 'dev')
|
|
73
|
+
await mkdir(devRoot, { recursive: true })
|
|
74
|
+
await Bun.write(
|
|
75
|
+
resolve(devRoot, 'index.ts'),
|
|
76
|
+
`import galbe from '${relative(devRoot, fileName)}';galbe.listen(${port});`
|
|
77
|
+
)
|
|
78
|
+
process.on('SIGINT', async () => {
|
|
79
|
+
await rm(resolve(ROOT, '.galbe', 'dev'), { recursive: true })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await $`BUN_ENV=development bun run --watch ${resolve(devRoot, 'index.ts')}`.cwd(ROOT)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
program
|
|
86
|
+
.command('build')
|
|
87
|
+
.description('Build your galbe API')
|
|
88
|
+
.argument('<string>', 'filename')
|
|
89
|
+
.option('-o, --out <string>', 'output file', '')
|
|
90
|
+
.option('-c, --compile', 'standalone executable', false)
|
|
91
|
+
.action(async (fileName, props) => {
|
|
92
|
+
const { out, compile } = props
|
|
93
|
+
const g: Galbe = (await import(resolve(ROOT, fileName))).default
|
|
94
|
+
const routes = await parseRoutes(g?.config?.routes)
|
|
95
|
+
const buildIndex = await createBuildIndex(fileName, routes)
|
|
96
|
+
|
|
97
|
+
const cmds = [
|
|
98
|
+
'bun',
|
|
99
|
+
'build',
|
|
100
|
+
buildIndex,
|
|
101
|
+
'--target',
|
|
102
|
+
'bun',
|
|
103
|
+
...(compile ? ['--compile', '--outfile', out ? out : 'api'] : ['--outdir', out ? out : 'dist'])
|
|
104
|
+
].filter(c => c)
|
|
105
|
+
Bun.spawn(cmds, {
|
|
106
|
+
cwd: ROOT,
|
|
107
|
+
stdout: 'inherit',
|
|
108
|
+
async onExit() {
|
|
109
|
+
await rm(resolve(ROOT, '.galbe', 'build', BUILD_ID), { recursive: true })
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
program.parse()
|
package/bun.lockb
ADDED
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TODO
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TODO: Routes documentation
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "galbe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
|
|
5
|
+
"author": "Pierre Caillaud M (https://github.com/pierre-cm)",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": "./bin/cli.ts",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./*": "./dist/*.js"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/pierre-cm/galbe"
|
|
17
|
+
},
|
|
18
|
+
"bugs": "https://github.com/pierre-cm/galbe/issues",
|
|
19
|
+
"homepage": "https://galbe.dev",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"bun",
|
|
22
|
+
"http",
|
|
23
|
+
"web",
|
|
24
|
+
"framework",
|
|
25
|
+
"server",
|
|
26
|
+
"api"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "bun ./scripts/build.ts",
|
|
31
|
+
"clean": "rm -rf dist",
|
|
32
|
+
"test": "bun test",
|
|
33
|
+
"postinstall": "bun run ./scripts/postinstall.ts"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/bun": "^1.0.4"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"typescript": "^5.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@sinclair/typebox": "^0.31.28",
|
|
43
|
+
"@swc/core": "^1.3.107",
|
|
44
|
+
"@swc/wasm": "^1.4.0",
|
|
45
|
+
"acorn": "^8.11.2",
|
|
46
|
+
"acorn-walk": "^8.3.0",
|
|
47
|
+
"commander": "^11.1.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const COMMIT_MSG = await Bun.file(Bun.argv[2]).text()
|
|
4
|
+
const COMMIT_EMOJI = { feat: 'āØ', fix: 'š§', doc: 'š', chore: 'š§¹' }
|
|
5
|
+
|
|
6
|
+
const newCommitMsg = COMMIT_MSG.replace(/^(feat|fix|doc|chore):(.*)/, (_, type, msg) => {
|
|
7
|
+
return `${COMMIT_EMOJI[type]} ${type}:${msg}`
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
await Bun.write(Bun.argv[2], newCommitMsg)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
if (existsSync('.git')) {
|
|
5
|
+
console.log('Setting up dev environment')
|
|
6
|
+
Bun.write('.git/hooks/prepare-commit-msg', Bun.file('scripts/hooks/prepare-commit-msg'))
|
|
7
|
+
await $`chmod +x .git/hooks/prepare-commit-msg`
|
|
8
|
+
console.log('done')
|
|
9
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import type { Server } from 'bun'
|
|
2
|
+
import type {
|
|
3
|
+
ArrayOptions,
|
|
4
|
+
NumericOptions,
|
|
5
|
+
ObjectOptions,
|
|
6
|
+
SchemaOptions,
|
|
7
|
+
Static,
|
|
8
|
+
StringOptions,
|
|
9
|
+
TAny,
|
|
10
|
+
TArray,
|
|
11
|
+
TBoolean,
|
|
12
|
+
TInteger,
|
|
13
|
+
TLiteral,
|
|
14
|
+
TLiteralValue,
|
|
15
|
+
TNever,
|
|
16
|
+
TNumber,
|
|
17
|
+
TObject,
|
|
18
|
+
TOptional,
|
|
19
|
+
TProperties,
|
|
20
|
+
TSchema,
|
|
21
|
+
TString,
|
|
22
|
+
TUnion
|
|
23
|
+
} from '@sinclair/typebox'
|
|
24
|
+
import type { RouteFileMeta } from './routes'
|
|
25
|
+
import type {
|
|
26
|
+
GalbeConfig,
|
|
27
|
+
Method,
|
|
28
|
+
Schema,
|
|
29
|
+
Hook,
|
|
30
|
+
Handler,
|
|
31
|
+
Endpoint,
|
|
32
|
+
Context,
|
|
33
|
+
ErrorHandler,
|
|
34
|
+
GalbePlugin,
|
|
35
|
+
TStream,
|
|
36
|
+
TMultipartProperties,
|
|
37
|
+
TMultipartForm,
|
|
38
|
+
TUrlFormProperties,
|
|
39
|
+
TUrlForm,
|
|
40
|
+
TBody,
|
|
41
|
+
TByteArray,
|
|
42
|
+
TStreamable,
|
|
43
|
+
MultipartFormData
|
|
44
|
+
} from './types'
|
|
45
|
+
|
|
46
|
+
import { TypeClone, Kind, TypeBuilder, Optional, TypeGuard } from '@sinclair/typebox'
|
|
47
|
+
import server from './server'
|
|
48
|
+
import { GalbeRouter } from './router'
|
|
49
|
+
import { defineRoutes } from './routes'
|
|
50
|
+
import { Stream } from './types'
|
|
51
|
+
import { logRoute } from './util'
|
|
52
|
+
|
|
53
|
+
const overloadDiscriminer = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
54
|
+
galbe: Galbe,
|
|
55
|
+
method: Method,
|
|
56
|
+
path: string,
|
|
57
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
58
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
59
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
60
|
+
) => {
|
|
61
|
+
const defaultSchema = {}
|
|
62
|
+
if (typeof arg2 === 'function') {
|
|
63
|
+
return galbeMethod(galbe, method, path, defaultSchema, undefined, arg2)
|
|
64
|
+
} else {
|
|
65
|
+
if (Array.isArray(arg2)) {
|
|
66
|
+
if (typeof arg3 === 'function') return galbeMethod(galbe, method, path, defaultSchema, arg2, arg3)
|
|
67
|
+
} else {
|
|
68
|
+
if (Array.isArray(arg3) && arg4) return galbeMethod(galbe, method, path, arg2, arg3, arg4)
|
|
69
|
+
else if (typeof arg3 === 'function') return galbeMethod(galbe, method, path, arg2, undefined, arg3)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error('Undefined endpoint signature')
|
|
73
|
+
}
|
|
74
|
+
const galbeMethod = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
75
|
+
_galbe: Galbe,
|
|
76
|
+
method: Method,
|
|
77
|
+
path: string,
|
|
78
|
+
schema: Schema<H, P, Q, B> | undefined,
|
|
79
|
+
hooks: Hook<Schema<H, P, Q, B>>[] | undefined,
|
|
80
|
+
handler: Handler<Schema<H, P, Q, B>>
|
|
81
|
+
) => {
|
|
82
|
+
schema = schema ?? {}
|
|
83
|
+
hooks = hooks || []
|
|
84
|
+
const context: Context<typeof schema> = {
|
|
85
|
+
headers: {} as Static<TObject<Exclude<(typeof schema)['headers'], undefined>>>,
|
|
86
|
+
params: {} as Static<TObject<Exclude<(typeof schema)['params'], undefined>>>,
|
|
87
|
+
query: {} as Static<TObject<Exclude<(typeof schema)['query'], undefined>>>,
|
|
88
|
+
body: {} as Static<Exclude<(typeof schema)['body'], undefined>>,
|
|
89
|
+
request: {} as Request,
|
|
90
|
+
state: {},
|
|
91
|
+
set: {} as {
|
|
92
|
+
headers: {
|
|
93
|
+
[header: string]: string
|
|
94
|
+
}
|
|
95
|
+
status?: number
|
|
96
|
+
redirect?: string
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//@ts-ignore
|
|
100
|
+
return {
|
|
101
|
+
method,
|
|
102
|
+
path,
|
|
103
|
+
schema,
|
|
104
|
+
context,
|
|
105
|
+
hooks,
|
|
106
|
+
handler
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class TypeboxTypeBuilder extends TypeBuilder {
|
|
111
|
+
/** `[Json]` Creates an Optional property */
|
|
112
|
+
public Optional<T extends TSchema>(schema: T): TOptional<T> {
|
|
113
|
+
return { ...TypeClone.Type(schema), [Optional]: 'Optional' }
|
|
114
|
+
}
|
|
115
|
+
/** `[Json]` Creates an Any type */
|
|
116
|
+
public Any(options: SchemaOptions = {}): TAny {
|
|
117
|
+
return this.Create({ ...options, [Kind]: 'Any' })
|
|
118
|
+
}
|
|
119
|
+
/** `[Json]` Creates an Array type */
|
|
120
|
+
public Array<T extends TSchema>(schema: T, options: ArrayOptions = {}): TArray<T> {
|
|
121
|
+
return this.Create({ ...options, [Kind]: 'Array', type: 'array', items: TypeClone.Type(schema) })
|
|
122
|
+
}
|
|
123
|
+
/** `[Json]` Creates a Boolean type */
|
|
124
|
+
public Boolean(options: SchemaOptions = {}): TBoolean {
|
|
125
|
+
return this.Create({ ...options, [Kind]: 'Boolean', type: 'boolean' })
|
|
126
|
+
}
|
|
127
|
+
/** `[Json]` Creates an Integer type */
|
|
128
|
+
public Integer(options: NumericOptions<number> = {}): TInteger {
|
|
129
|
+
return this.Create({ ...options, [Kind]: 'Integer', type: 'integer' })
|
|
130
|
+
}
|
|
131
|
+
/** `[Json]` Creates a Literal type */
|
|
132
|
+
public Literal<T extends TLiteralValue>(value: T, options: SchemaOptions = {}): TLiteral<T> {
|
|
133
|
+
return this.Create({
|
|
134
|
+
...options,
|
|
135
|
+
[Kind]: 'Literal',
|
|
136
|
+
const: value,
|
|
137
|
+
type: typeof value as 'string' | 'number' | 'boolean'
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
/** `[Json]` Creates a Number type */
|
|
141
|
+
public Number(options: NumericOptions<number> = {}): TNumber {
|
|
142
|
+
return this.Create({ ...options, [Kind]: 'Number', type: 'number' })
|
|
143
|
+
}
|
|
144
|
+
/** `[Json]` Creates an Object type */
|
|
145
|
+
public Object<T extends TProperties>(properties: T, options: ObjectOptions = {}): TObject<T> {
|
|
146
|
+
const propertyKeys = Object.getOwnPropertyNames(properties)
|
|
147
|
+
const optionalKeys = propertyKeys.filter(key => TypeGuard.TOptional(properties[key]))
|
|
148
|
+
const requiredKeys = propertyKeys.filter(name => !optionalKeys.includes(name))
|
|
149
|
+
const clonedAdditionalProperties = TypeGuard.TSchema(options.additionalProperties)
|
|
150
|
+
? { additionalProperties: TypeClone.Type(options.additionalProperties) }
|
|
151
|
+
: {}
|
|
152
|
+
const clonedProperties = propertyKeys.reduce(
|
|
153
|
+
(acc, key) => ({ ...acc, [key]: TypeClone.Type(properties[key]) }),
|
|
154
|
+
{} as TProperties
|
|
155
|
+
)
|
|
156
|
+
return requiredKeys.length > 0
|
|
157
|
+
? this.Create({
|
|
158
|
+
...options,
|
|
159
|
+
...clonedAdditionalProperties,
|
|
160
|
+
[Kind]: 'Object',
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: clonedProperties,
|
|
163
|
+
required: requiredKeys
|
|
164
|
+
})
|
|
165
|
+
: this.Create({
|
|
166
|
+
...options,
|
|
167
|
+
...clonedAdditionalProperties,
|
|
168
|
+
[Kind]: 'Object',
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: clonedProperties
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
/** `[Json]` Creates a String type */
|
|
174
|
+
public String(options: StringOptions = {}): TString {
|
|
175
|
+
return this.Create({ ...options, [Kind]: 'String', type: 'string' })
|
|
176
|
+
}
|
|
177
|
+
/** `[Json]` Creates a Union type */
|
|
178
|
+
public Union(anyOf: [], options?: SchemaOptions): TNever
|
|
179
|
+
/** `[Json]` Creates a Union type */
|
|
180
|
+
public Union<T extends [TSchema]>(anyOf: [...T], options?: SchemaOptions): T[0]
|
|
181
|
+
/** `[Json]` Creates a Union type */
|
|
182
|
+
public Union<T extends TSchema[]>(anyOf: [...T], options?: SchemaOptions): TUnion<T>
|
|
183
|
+
/** `[Json]` Creates a Union type */
|
|
184
|
+
public Union(union: TSchema[], options: SchemaOptions = {}) {
|
|
185
|
+
// prettier-ignore
|
|
186
|
+
return (() => {
|
|
187
|
+
const anyOf = union
|
|
188
|
+
if (anyOf.length === 0) throw new Error("Union type must decalre at least one schema")
|
|
189
|
+
if (anyOf.length === 1) return this.Create(TypeClone.Type(anyOf[0], options))
|
|
190
|
+
const clonedAnyOf = TypeClone.Rest(anyOf)
|
|
191
|
+
return this.Create({ ...options, [Kind]: 'Union', anyOf: clonedAnyOf })
|
|
192
|
+
})()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class GalbeTypeBuilder extends TypeboxTypeBuilder {
|
|
197
|
+
/** `[Galbe]` Creates an Stream type */
|
|
198
|
+
public Stream<T extends TUrlForm>(
|
|
199
|
+
schema: T
|
|
200
|
+
): Omit<TStream<T>, 'static'> & { static: AsyncGenerator<[string, string | number | boolean]>; params: unknown[] }
|
|
201
|
+
public Stream<T extends TMultipartForm>(
|
|
202
|
+
schema: T
|
|
203
|
+
): Omit<TStream<T>, 'static'> & { static: AsyncGenerator<MultipartFormData, void, unknown>; params: unknown[] }
|
|
204
|
+
public Stream<T extends TByteArray>(
|
|
205
|
+
schema: T
|
|
206
|
+
): Omit<TStream<T>, 'static'> & { static: AsyncGenerator<Uint8Array>; params: unknown[] }
|
|
207
|
+
public Stream<T extends TString>(
|
|
208
|
+
schema: T
|
|
209
|
+
): Omit<TStream<T>, 'static'> & { static: AsyncGenerator<string>; params: unknown[] }
|
|
210
|
+
public Stream<T extends TStreamable>(schema: T): TStream<T> {
|
|
211
|
+
return {
|
|
212
|
+
...TypeClone.Type(schema),
|
|
213
|
+
[Stream]: 'Stream'
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** `[Galbe]` Creates an ByteArray type */
|
|
217
|
+
public ByteArray(): TByteArray {
|
|
218
|
+
return this.Create({ [Kind]: 'ByteArray', type: 'byteArray', params: {} })
|
|
219
|
+
}
|
|
220
|
+
/** `[Galbe]` Creates an MultipartForm type */
|
|
221
|
+
public MultipartForm<T extends TMultipartProperties>(properties?: T): TMultipartForm {
|
|
222
|
+
if (!properties) return this.Create({ [Kind]: 'MultipartForm', type: 'multipartForm' })
|
|
223
|
+
const propertyKeys = Object.getOwnPropertyNames(properties)
|
|
224
|
+
const clonedProperties = propertyKeys.reduce(
|
|
225
|
+
//@ts-ignore
|
|
226
|
+
(acc, key) => ({ ...acc, [key]: TypeClone.Type(properties[key]) }),
|
|
227
|
+
{} as TProperties
|
|
228
|
+
)
|
|
229
|
+
return this.Create({
|
|
230
|
+
[Kind]: 'MultipartForm',
|
|
231
|
+
type: 'multipartForm',
|
|
232
|
+
properties: clonedProperties
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
/** `[Galbe]` Creates an UrlForm type */
|
|
236
|
+
public UrlForm<T extends TUrlFormProperties>(properties?: T): TUrlForm {
|
|
237
|
+
if (!properties) return this.Create({ [Kind]: 'UrlForm', type: 'urlForm' })
|
|
238
|
+
const propertyKeys = Object.getOwnPropertyNames(properties)
|
|
239
|
+
const clonedProperties = propertyKeys.reduce(
|
|
240
|
+
//@ts-ignore
|
|
241
|
+
(acc, key) => ({ ...acc, [key]: TypeClone.Type(properties[key]) }),
|
|
242
|
+
{} as TProperties
|
|
243
|
+
)
|
|
244
|
+
return this.Create({ [Kind]: 'UrlForm', type: 'urlForm', properties: clonedProperties })
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const T = new GalbeTypeBuilder()
|
|
249
|
+
|
|
250
|
+
export { RequestError } from './types'
|
|
251
|
+
|
|
252
|
+
const indexRoutes: { method: string; path: string }[] = []
|
|
253
|
+
/**
|
|
254
|
+
* #### Galbe Server
|
|
255
|
+
* Instanciate a Galbe web server
|
|
256
|
+
*
|
|
257
|
+
* ---
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* import { Galbe } from 'galbe'
|
|
261
|
+
* import config from "./galbe.config"
|
|
262
|
+
*
|
|
263
|
+
* export default new Galbe(config)
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export class Galbe {
|
|
267
|
+
config: GalbeConfig
|
|
268
|
+
meta?: Array<RouteFileMeta> = []
|
|
269
|
+
router: GalbeRouter
|
|
270
|
+
errorHandler?: ErrorHandler
|
|
271
|
+
listening: boolean = false
|
|
272
|
+
#prepare: boolean = false
|
|
273
|
+
server?: Server
|
|
274
|
+
plugins: GalbePlugin[] = []
|
|
275
|
+
constructor(config?: GalbeConfig) {
|
|
276
|
+
this.config = config ?? {}
|
|
277
|
+
this.router = new GalbeRouter(this.config?.basePath || '')
|
|
278
|
+
}
|
|
279
|
+
private add(route: any) {
|
|
280
|
+
this.router.add(route)
|
|
281
|
+
if (Bun.env.BUN_ENV === 'development') {
|
|
282
|
+
if (!this.#prepare) indexRoutes.push({ method: route.method, path: route.path })
|
|
283
|
+
else logRoute(route)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async use(plugin: GalbePlugin) {
|
|
287
|
+
this.plugins.push(plugin)
|
|
288
|
+
}
|
|
289
|
+
async listen(port?: number) {
|
|
290
|
+
port = port || this.config?.port || 3000
|
|
291
|
+
this.config.port = port
|
|
292
|
+
if (this.listening) this.stop()
|
|
293
|
+
if (Bun.env.BUN_ENV === 'development') {
|
|
294
|
+
this.#prepare = true
|
|
295
|
+
console.log('šļø \x1b[1;30mConstructing routes\x1b[0m')
|
|
296
|
+
for (const r of indexRoutes) logRoute(r)
|
|
297
|
+
await defineRoutes(this.config || {}, this)
|
|
298
|
+
console.log('\nā
\x1b[1;30mdone\x1b[0m')
|
|
299
|
+
this.server = await server(this, port)
|
|
300
|
+
const url = `http://localhost:${port}${this.config?.basePath || ''}`
|
|
301
|
+
console.log(`\n\x1b[1;30mš API running at\x1b[0m \x1b[4;34m${url}\x1b[0m`)
|
|
302
|
+
} else {
|
|
303
|
+
this.server = await server(this, port)
|
|
304
|
+
}
|
|
305
|
+
this.listening = true
|
|
306
|
+
this.#prepare = false
|
|
307
|
+
return this.server
|
|
308
|
+
}
|
|
309
|
+
stop() {
|
|
310
|
+
this.server?.stop(true)
|
|
311
|
+
}
|
|
312
|
+
onError(handler: ErrorHandler) {
|
|
313
|
+
this.errorHandler = handler
|
|
314
|
+
}
|
|
315
|
+
get: Endpoint = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
316
|
+
path: string,
|
|
317
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
318
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
319
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
320
|
+
) => this.add(overloadDiscriminer(this, 'get', path, arg2, arg3, arg4))
|
|
321
|
+
post: Endpoint = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
322
|
+
path: string,
|
|
323
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
324
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
325
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
326
|
+
) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
|
|
327
|
+
put: Endpoint = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
328
|
+
path: string,
|
|
329
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
330
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
331
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
332
|
+
) => this.add(overloadDiscriminer(this, 'put', path, arg2, arg3, arg4))
|
|
333
|
+
patch: Endpoint = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
334
|
+
path: string,
|
|
335
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
336
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
337
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
338
|
+
) => this.add(overloadDiscriminer(this, 'patch', path, arg2, arg3, arg4))
|
|
339
|
+
delete: Endpoint = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
340
|
+
path: string,
|
|
341
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
342
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
343
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
344
|
+
) => this.add(overloadDiscriminer(this, 'delete', path, arg2, arg3, arg4))
|
|
345
|
+
options: Endpoint = <H extends TProperties, P extends TProperties, Q extends TProperties, B extends TBody>(
|
|
346
|
+
path: string,
|
|
347
|
+
arg2: Schema<H, P, Q, B> | Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
348
|
+
arg3?: Hook<Schema<H, P, Q, B>>[] | Handler<Schema<H, P, Q, B>>,
|
|
349
|
+
arg4?: Handler<Schema<H, P, Q, B>>
|
|
350
|
+
) => this.add(overloadDiscriminer(this, 'options', path, arg2, arg3, arg4))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export * from './types'
|