galbe 0.7.0 → 0.9.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/bin/commands/build.ts +42 -25
- package/bin/commands/dev.ts +8 -6
- package/bin/commands/generate/client.ts +26 -15
- package/bin/commands/generate/code/openapi.parser.ts +92 -57
- package/bin/commands/generate/code.ts +14 -13
- package/bin/commands/generate/spec.ts +2 -2
- package/bin/res/cli.template.js +13 -13
- package/bin/res/client.template.ts +26 -16
- package/bin/util.ts +11 -8
- package/docs/context.md +5 -1
- package/docs/error-handler.md +24 -13
- package/docs/routes.md +3 -0
- package/package.json +1 -1
- package/src/extras/spec/openapi.serializer.ts +25 -20
- package/src/index.ts +44 -4
- package/src/parser.ts +17 -12
- package/src/routes.ts +99 -91
- package/src/server.ts +7 -5
- package/src/types.ts +41 -21
- package/src/util.ts +21 -7
- package/src/validator.ts +16 -16
- package/test/parser.test.ts +38 -38
- package/test/requests.test.ts +8 -5
- package/test/router.test.ts +2 -2
package/bin/commands/build.ts
CHANGED
|
@@ -3,34 +3,42 @@ import { $ } from 'bun'
|
|
|
3
3
|
import { Command, Option } from 'commander'
|
|
4
4
|
import { resolve, relative, dirname } from 'path'
|
|
5
5
|
import { tmpdir } from 'os'
|
|
6
|
-
import { mkdir, rm } from 'fs/promises'
|
|
6
|
+
import { mkdir, rm, exists } from 'fs/promises'
|
|
7
7
|
|
|
8
8
|
import { CWD, fmtVal, silentExec } from '../util'
|
|
9
9
|
import { Galbe } from '../../src'
|
|
10
|
-
import { defineRoutes } from '../../src/routes'
|
|
10
|
+
import { defineRoutes, GalbeProxy } from '../../src/routes'
|
|
11
11
|
import { BuildConfig } from 'bun'
|
|
12
12
|
|
|
13
|
-
const createBuildIndex = async (indexPath: string, g: Galbe) => {
|
|
14
|
-
const buildId = crypto.randomUUID()
|
|
13
|
+
const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string) => {
|
|
15
14
|
const buildPath = resolve(tmpdir(), buildId)
|
|
16
|
-
const routes = new
|
|
15
|
+
const routes = new Map<string, { filepath: string, static?: { path: string, root: string } }>()
|
|
17
16
|
let errors: any[] = []
|
|
18
|
-
|
|
17
|
+
// Create GalbeProxy here
|
|
18
|
+
// use it to define routes
|
|
19
|
+
const proxy = new GalbeProxy(g, ({ type, error, filepath, route }) => {
|
|
19
20
|
if (!filepath) return
|
|
20
|
-
routes.
|
|
21
|
+
routes.set(filepath, { filepath, static: route?.static })
|
|
21
22
|
if (type === 'error') errors.push(error)
|
|
22
23
|
})
|
|
24
|
+
await defineRoutes({ routes: g?.config?.routes }, proxy)
|
|
25
|
+
// call init on it for plugin initialization
|
|
26
|
+
await proxy.init()
|
|
27
|
+
|
|
23
28
|
if (errors.length) throw errors
|
|
24
29
|
await mkdir(buildPath, { recursive: true })
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`import galbe from '${relative(buildPath, indexPath)}'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
galbe.
|
|
32
|
-
`
|
|
33
|
-
|
|
30
|
+
|
|
31
|
+
let buildIndex =
|
|
32
|
+
`import galbe from '${relative(buildPath, indexPath)}';\n` +
|
|
33
|
+
`${[...routes.values()].map((r, idx) => `import _${idx} from '${relative(buildPath, r.filepath)}'`).join(';\n')}\n` +
|
|
34
|
+
`Bun.env.BUN_ENV = 'production';\n` +
|
|
35
|
+
`Bun.env.GALBE_BUILD = '${buildId}';\n` +
|
|
36
|
+
`galbe.meta = ${JSON.stringify(g.meta)};\n` +
|
|
37
|
+
`${[...routes].map((_, idx) => `_${idx}(galbe)`).join(';\n')};\n` +
|
|
38
|
+
`galbe.listen();\n`
|
|
39
|
+
|
|
40
|
+
await Bun.write(resolve(buildPath, 'index.ts'), buildIndex)
|
|
41
|
+
|
|
34
42
|
return resolve(buildPath, 'index.ts')
|
|
35
43
|
}
|
|
36
44
|
|
|
@@ -44,10 +52,18 @@ export default (cmd: Command) => {
|
|
|
44
52
|
.action(async (index, props) => {
|
|
45
53
|
const { out, compile, config } = props
|
|
46
54
|
|
|
55
|
+
const buildID = crypto.randomUUID()
|
|
56
|
+
const outPath = resolve(CWD, out)
|
|
57
|
+
|
|
58
|
+
Bun.env.GALBE_BUILD = buildID
|
|
59
|
+
Bun.env.GALBE_BUILD_OUT = outPath
|
|
60
|
+
|
|
47
61
|
const bunfig = config ? (await import(resolve(CWD, config)))?.default || {} : {}
|
|
48
62
|
|
|
63
|
+
if(await exists(outPath)) await rm(outPath, { recursive: true })
|
|
64
|
+
|
|
49
65
|
let error = null
|
|
50
|
-
|
|
66
|
+
Bun.write(Bun.stdout, '📦 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m app\x1b[0m')
|
|
51
67
|
let g: Galbe = await silentExec(async () => {
|
|
52
68
|
try {
|
|
53
69
|
const g = (await import(resolve(CWD, index))).default
|
|
@@ -63,7 +79,7 @@ export default (cmd: Command) => {
|
|
|
63
79
|
}
|
|
64
80
|
let buildIndex: string = ''
|
|
65
81
|
try {
|
|
66
|
-
buildIndex = await createBuildIndex(index, g)
|
|
82
|
+
buildIndex = await createBuildIndex(index, g, buildID)
|
|
67
83
|
} catch (errors) {
|
|
68
84
|
console.log(`\nerror: build errors`)
|
|
69
85
|
for (let error of errors) console.log(error)
|
|
@@ -75,24 +91,25 @@ export default (cmd: Command) => {
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
const buildConfig: BuildConfig = {
|
|
78
|
-
publicPath: `${
|
|
94
|
+
publicPath: `${outPath}/`,
|
|
79
95
|
...Object.fromEntries(Object.entries(bunfig).filter(([k, v]) => v)),
|
|
80
96
|
entrypoints: [buildIndex],
|
|
81
|
-
outdir:
|
|
97
|
+
outdir: outPath,
|
|
82
98
|
sourcemap: 'external',
|
|
83
|
-
target: 'bun'
|
|
99
|
+
target: 'bun',
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
await rm(resolve(CWD, out), { recursive: true })
|
|
87
|
-
|
|
88
102
|
let bo = await Bun.build(buildConfig)
|
|
89
|
-
if (bo.success)
|
|
103
|
+
if (bo.success) Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
90
104
|
else {
|
|
91
105
|
console.log(`\nerror: build errors`)
|
|
92
106
|
console.log(...bo.logs)
|
|
93
107
|
}
|
|
94
|
-
if (compile)
|
|
108
|
+
if (compile) {
|
|
109
|
+
await $`bun build --compile ${resolve(CWD, out, 'index.js')} --outfile ${outPath}/bin`
|
|
110
|
+
}
|
|
95
111
|
|
|
96
112
|
await rm(dirname(buildIndex), { recursive: true })
|
|
113
|
+
process.exit(0)
|
|
97
114
|
})
|
|
98
115
|
}
|
package/bin/commands/dev.ts
CHANGED
|
@@ -20,7 +20,8 @@ export default (cmd: Command) => {
|
|
|
20
20
|
})
|
|
21
21
|
.default(null, fmtVal(defaultPort))
|
|
22
22
|
)
|
|
23
|
-
.addOption(new Option('-w, --watch', 'watch file changes').default(false, fmtVal(false)))
|
|
23
|
+
.addOption(new Option('-w, --watch <dir>', 'watch file changes').default(false, fmtVal(false)))
|
|
24
|
+
.addOption(new Option('-wi, --watchignore <regexp>', 'ignore file changes').default(false, fmtVal(false)))
|
|
24
25
|
.addOption(new Option('-nc, --noclear', "don't clear on file changes").default(false, fmtVal(false)))
|
|
25
26
|
.addOption(
|
|
26
27
|
new Option('-f, --force', 'kills any process running on defined port before strating the server').default(
|
|
@@ -29,7 +30,8 @@ export default (cmd: Command) => {
|
|
|
29
30
|
)
|
|
30
31
|
)
|
|
31
32
|
.action(async (index, props) => {
|
|
32
|
-
const { port, watch, noclear, force } = props
|
|
33
|
+
const { port, watch, watchignore, noclear, force } = props
|
|
34
|
+
let watch_dir = typeof watch === 'string' ? watch : CWD
|
|
33
35
|
const clear = !noclear
|
|
34
36
|
const indexPath = resolve(CWD, index)
|
|
35
37
|
let g: Galbe
|
|
@@ -38,9 +40,9 @@ export default (cmd: Command) => {
|
|
|
38
40
|
|
|
39
41
|
if (force) await killPort(port || 3000)
|
|
40
42
|
|
|
41
|
-
if (watch) {
|
|
43
|
+
if (!!watch) {
|
|
42
44
|
await watchDir(
|
|
43
|
-
|
|
45
|
+
watch_dir,
|
|
44
46
|
async () => {
|
|
45
47
|
g.stop()
|
|
46
48
|
if (clear) await $`clear`
|
|
@@ -49,11 +51,11 @@ export default (cmd: Command) => {
|
|
|
49
51
|
await instanciateRoutes(g)
|
|
50
52
|
await g.listen(port)
|
|
51
53
|
},
|
|
52
|
-
{ ignore: /node_modules/ }
|
|
54
|
+
{ ignore: watchignore ? new RegExp(watchignore) : /node_modules/ }
|
|
53
55
|
)
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
if (watch && clear) await $`clear`
|
|
58
|
+
if (!!watch && clear) await $`clear`
|
|
57
59
|
g = (await import(indexPath)).default
|
|
58
60
|
await instanciateRoutes(g)
|
|
59
61
|
await g.listen(port)
|
|
@@ -6,7 +6,7 @@ import { resolve, extname } from 'path'
|
|
|
6
6
|
import { rm } from 'fs/promises'
|
|
7
7
|
import { transformSync } from '@swc/core'
|
|
8
8
|
import { CWD, fmtList, instanciateRoutes, silentExec } from '../../util'
|
|
9
|
-
import { $T, Galbe, GalbeCLICommand, Method, Route } from '../../../src'
|
|
9
|
+
import { $T, Galbe, GalbeCLICommand, Method, Route, STResponse } from '../../../src'
|
|
10
10
|
import { walkRoutes } from '../../../src/util'
|
|
11
11
|
import { schemaToTypeStr, Optional, STSchema } from '../../../src/schema'
|
|
12
12
|
|
|
@@ -36,10 +36,10 @@ export default (cmd: Command) => {
|
|
|
36
36
|
let pckg: any = {}
|
|
37
37
|
try {
|
|
38
38
|
pckg = await Bun.file(resolve(CWD, 'package.json')).json()
|
|
39
|
-
} catch (e) {}
|
|
39
|
+
} catch (e) { }
|
|
40
40
|
|
|
41
41
|
let error = null
|
|
42
|
-
|
|
42
|
+
Bun.write(Bun.stdout, '💻 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m client\x1b[0m')
|
|
43
43
|
let g: Galbe = await silentExec(async () => {
|
|
44
44
|
try {
|
|
45
45
|
const g = (await import(resolve(CWD, index))).default
|
|
@@ -65,6 +65,7 @@ export default (cmd: Command) => {
|
|
|
65
65
|
options: [],
|
|
66
66
|
head: []
|
|
67
67
|
}
|
|
68
|
+
const types: Record<string, STResponse> = {}
|
|
68
69
|
let commands: GalbeCLICommand[] = []
|
|
69
70
|
const metaRoutes = g.meta?.reduce(
|
|
70
71
|
(routes, c) => ({ ...routes, ...c.routes }),
|
|
@@ -73,9 +74,13 @@ export default (cmd: Command) => {
|
|
|
73
74
|
|
|
74
75
|
walkRoutes(g.router.routes, r => {
|
|
75
76
|
let meta = metaRoutes?.[r.path]?.[r.method]
|
|
77
|
+
let [_, summary, description] = meta?.head?.match(/^([^\n]*)\n\n(.*)/) || []
|
|
78
|
+
if(!summary) description = meta?.head
|
|
76
79
|
let route = {
|
|
77
80
|
...r,
|
|
78
81
|
...(meta?.operationId ? { alias: meta?.operationId } : {}),
|
|
82
|
+
...(summary ? { summary } : {}),
|
|
83
|
+
...(description ? { description } : {}),
|
|
79
84
|
pathT: r.path.replaceAll(/:([^\/]+)/g, '${$1}'),
|
|
80
85
|
params:
|
|
81
86
|
Object.fromEntries(
|
|
@@ -84,11 +89,11 @@ export default (cmd: Command) => {
|
|
|
84
89
|
{
|
|
85
90
|
...(r.schema?.params?.[m?.[1]]
|
|
86
91
|
? {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
type: schemaToTypeStr(r.schema.params[m[1]]),
|
|
93
|
+
...(r.schema.params[m[1]]?.description
|
|
94
|
+
? { description: r.schema.params[m[1]].description as string }
|
|
95
|
+
: {})
|
|
96
|
+
}
|
|
92
97
|
: { type: 'string' })
|
|
93
98
|
}
|
|
94
99
|
])
|
|
@@ -99,18 +104,22 @@ export default (cmd: Command) => {
|
|
|
99
104
|
...(r.schema.body ? { body: schemaToTypeStr(r.schema.body) } : {}),
|
|
100
105
|
...(r.schema.response
|
|
101
106
|
? {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
response: Object.fromEntries(
|
|
108
|
+
Object.entries(r.schema.response).map(([k, v]) => [k === 'default' ? '"default"' : k, schemaToTypeStr(v as STSchema)])
|
|
109
|
+
)
|
|
110
|
+
}
|
|
106
111
|
: {})
|
|
107
112
|
}
|
|
108
113
|
}
|
|
114
|
+
Object.values(r.schema.response || {}).filter(s => s?.id).forEach(s => {
|
|
115
|
+
//@ts-ignore
|
|
116
|
+
types[s.id] = schemaToTypeStr(s)
|
|
117
|
+
})
|
|
109
118
|
routes[r.method.toLocaleLowerCase()].push(route)
|
|
110
119
|
if (target === 'cli' && meta?.operationId)
|
|
111
120
|
commands.push({
|
|
112
121
|
name: meta.operationId,
|
|
113
|
-
description:
|
|
122
|
+
description: route.summary || route.description,
|
|
114
123
|
route,
|
|
115
124
|
arguments:
|
|
116
125
|
Object.entries((route?.params || {}) as Record<string, { type: string; description?: string }>)?.map(
|
|
@@ -144,7 +153,8 @@ export default (cmd: Command) => {
|
|
|
144
153
|
const sandbox = {
|
|
145
154
|
console,
|
|
146
155
|
version: pckg?.version || '0.1.0',
|
|
147
|
-
routes
|
|
156
|
+
routes,
|
|
157
|
+
types,
|
|
148
158
|
}
|
|
149
159
|
createContext(sandbox)
|
|
150
160
|
let res = script.runInNewContext(sandbox)
|
|
@@ -195,6 +205,7 @@ export default (cmd: Command) => {
|
|
|
195
205
|
await rm(resolve(CWD, '.galbe'), { recursive: true })
|
|
196
206
|
}
|
|
197
207
|
|
|
198
|
-
|
|
208
|
+
Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
209
|
+
process.exit(0)
|
|
199
210
|
})
|
|
200
211
|
}
|
|
@@ -68,6 +68,13 @@ const orderDeps = (deps: Record<string, SchemaEntry>) => {
|
|
|
68
68
|
}
|
|
69
69
|
return Object.fromEntries([...l].map(k => [k, deps[k]]))
|
|
70
70
|
}
|
|
71
|
+
const serialize = (obj: any) => {
|
|
72
|
+
return JSON.stringify(obj, (k, value) => {
|
|
73
|
+
if (k === 'pattern' && value) return `/${value}/`
|
|
74
|
+
return value
|
|
75
|
+
}).replace(/"\/(.*)\/([gimsuy]*)"/g, '/$1/$2');
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
const writeCodeFile = async (path: string, content: string, target: 'js' | 'ts') => {
|
|
72
79
|
if (target === 'js') {
|
|
73
80
|
content = transformSync(content, {
|
|
@@ -88,7 +95,9 @@ const parseOapiSchema = (
|
|
|
88
95
|
details: { id?: string; title?: string; description?: string } = {},
|
|
89
96
|
extra?: { media?: string }
|
|
90
97
|
): string => {
|
|
91
|
-
if (!os)
|
|
98
|
+
if (!os) {
|
|
99
|
+
return `$T.any(${details && Object.keys(details).length ? JSON.stringify(details) : ''})`
|
|
100
|
+
}
|
|
92
101
|
//@ts-ignore
|
|
93
102
|
if (os?.$ref) return `%ref:${os.$ref}%`
|
|
94
103
|
os = os as OpenAPIV3.SchemaObject
|
|
@@ -106,11 +115,12 @@ const parseOapiSchema = (
|
|
|
106
115
|
} = {
|
|
107
116
|
...details,
|
|
108
117
|
title: os.title,
|
|
109
|
-
description: os.description
|
|
118
|
+
description: details.description || os.description
|
|
110
119
|
}
|
|
120
|
+
if(!os?.type) return `$T.any(${details && Object.keys(details).length ? JSON.stringify(details) : ''})`
|
|
111
121
|
let resp = ''
|
|
112
122
|
let hasOptions = Object.values(options).some(v => !!v)
|
|
113
|
-
let optArg = hasOptions ?
|
|
123
|
+
let optArg = hasOptions ? serialize(options) : ''
|
|
114
124
|
let anyOf = os.oneOf || os.anyOf || os.allOf
|
|
115
125
|
let required = os.required
|
|
116
126
|
let nullable = os.nullable
|
|
@@ -118,11 +128,11 @@ const parseOapiSchema = (
|
|
|
118
128
|
if (anyOf?.length) {
|
|
119
129
|
if (anyOf.length === 1) resp = parseOapiSchema(anyOf[0] as OpenAPIV3.SchemaObject, details, extra)
|
|
120
130
|
else {
|
|
121
|
-
resp = `$T.union([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}], ${
|
|
131
|
+
resp = `$T.union([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}], ${serialize(
|
|
122
132
|
options
|
|
123
133
|
)})`
|
|
124
134
|
}
|
|
125
|
-
} else if (os.type === 'boolean') resp = `$T.boolean(${hasOptions ?
|
|
135
|
+
} else if (os.type === 'boolean') resp = `$T.boolean(${hasOptions ? serialize(options) : ''})`
|
|
126
136
|
else if (os.type === 'number') {
|
|
127
137
|
let { max, min, exclusiveMax, exclusiveMin } = {
|
|
128
138
|
max: os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined,
|
|
@@ -132,7 +142,7 @@ const parseOapiSchema = (
|
|
|
132
142
|
}
|
|
133
143
|
options = { ...options, min, max, exclusiveMax, exclusiveMin }
|
|
134
144
|
hasOptions = Object.values(options).some(v => !!v)
|
|
135
|
-
resp = `$T.number(${hasOptions ?
|
|
145
|
+
resp = `$T.number(${hasOptions ? serialize(options) : ''})`
|
|
136
146
|
} else if (os.type === 'integer') {
|
|
137
147
|
let max = os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined
|
|
138
148
|
let min = os.minimum !== undefined && !os.exclusiveMinimum ? os.minimum : undefined
|
|
@@ -140,38 +150,35 @@ const parseOapiSchema = (
|
|
|
140
150
|
let exclusiveMin = os.minimum !== undefined && os.exclusiveMinimum ? os.minimum : undefined
|
|
141
151
|
options = { ...options, min, max, exclusiveMax, exclusiveMin }
|
|
142
152
|
hasOptions = Object.values(options).some(v => !!v)
|
|
143
|
-
resp = `$T.integer(${hasOptions ?
|
|
153
|
+
resp = `$T.integer(${hasOptions ? serialize(options) : ''})`
|
|
144
154
|
} else if (os.type === 'string') {
|
|
145
|
-
if (os.format === 'binary') resp = `$T.byteArray(${hasOptions ?
|
|
155
|
+
if (os.format === 'binary') resp = `$T.byteArray(${hasOptions ? serialize(options) : ''})`
|
|
146
156
|
else {
|
|
147
157
|
let minLength = os.minLength
|
|
148
158
|
let maxLength = os.maxLength
|
|
149
159
|
let pattern = os.pattern
|
|
150
160
|
options = { ...options, minLength, maxLength, pattern }
|
|
151
161
|
hasOptions = Object.values(options).some(v => !!v)
|
|
152
|
-
resp = `$T.string(${hasOptions ?
|
|
162
|
+
resp = `$T.string(${hasOptions ? serialize(options) : ''})`
|
|
153
163
|
}
|
|
154
164
|
} else if (os.type === 'array') {
|
|
155
165
|
let minItems = os.minItems
|
|
156
166
|
let maxItems = os.maxItems
|
|
157
167
|
let unique = os.uniqueItems
|
|
158
168
|
options = { ...options, minItems, maxItems, unique }
|
|
159
|
-
resp = `$T.array(${parseOapiSchema(os?.items)}${optArg})`
|
|
169
|
+
resp = `$T.array(${parseOapiSchema(os?.items)}, ${optArg})`
|
|
160
170
|
} else if (os.type === 'object') {
|
|
171
|
+
let props = Object.entries(os?.properties || {})
|
|
172
|
+
.map(([k, v]) => `"${k}":${parseOapiSchema(v)}`)
|
|
173
|
+
.join(',')
|
|
161
174
|
if (extra?.media === 'multipart/form-data') {
|
|
162
|
-
resp = `$T.multipartForm({${
|
|
163
|
-
.map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
|
|
164
|
-
.join(',')}}${optArg})`
|
|
175
|
+
resp = `$T.multipartForm({${props}}${optArg ? `, ${optArg}`:''})`
|
|
165
176
|
} else if (extra?.media === 'application/x-www-form-urlencoded') {
|
|
166
|
-
resp = `$T.urlForm({${
|
|
167
|
-
.map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
|
|
168
|
-
.join(',')}}${optArg})`
|
|
177
|
+
resp = `$T.urlForm({${props}}${optArg ? `, ${optArg}`:''})`
|
|
169
178
|
} else {
|
|
170
|
-
resp = `$T.object({${
|
|
171
|
-
.map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
|
|
172
|
-
.join(',')}}${optArg})`
|
|
179
|
+
resp = `$T.object({${props}}${optArg ? `, ${optArg}`:''})`
|
|
173
180
|
}
|
|
174
|
-
} else throw new Error(`Unknown schema type ${os
|
|
181
|
+
} else throw new Error(`Unknown schema type ${JSON.stringify(os)}`)
|
|
175
182
|
|
|
176
183
|
if (!required && nullable) resp = `$T.nullish(${resp})`
|
|
177
184
|
else if (!required) resp = `$T.optional(${resp})`
|
|
@@ -188,13 +195,21 @@ const buildSchemaIndex = (def: OpenAPIV3.Document) => {
|
|
|
188
195
|
let dependsOn = new Set<string>()
|
|
189
196
|
if (kind === 'schemas') schema = parseOapiSchema(s, { id: k })
|
|
190
197
|
else if (kind === 'requestBodies' || kind === 'responses') {
|
|
191
|
-
let schemas
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
let schemas=[] as string[]
|
|
199
|
+
if(!!s.content){
|
|
200
|
+
|
|
201
|
+
schemas = [
|
|
202
|
+
...new Set(
|
|
203
|
+
Object.entries((s as OpenAPIV3.RequestBodyObject)?.content || {null:{}}).map(([media, v]) => {
|
|
204
|
+
return parseOapiSchema(v.schema, { id: k }, { media })
|
|
205
|
+
})
|
|
195
206
|
)
|
|
196
|
-
|
|
197
|
-
|
|
207
|
+
]
|
|
208
|
+
} else {
|
|
209
|
+
schemas = [
|
|
210
|
+
parseOapiSchema(undefined, {id: k, ...s})
|
|
211
|
+
]
|
|
212
|
+
}
|
|
198
213
|
schema = schemas.length <= 0 ? '' : schemas.length === 1 ? schemas[0] : `$T.union([${schemas.join(',')}])`
|
|
199
214
|
}
|
|
200
215
|
schema = unref(schema, m => {
|
|
@@ -225,26 +240,33 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
225
240
|
if (!def) return {}
|
|
226
241
|
let imports = {}
|
|
227
242
|
let p = path.replaceAll(/\{([^\}]*)\}/g, ':$1')
|
|
228
|
-
let description = def.summary || def.description
|
|
243
|
+
// let description = def.summary || def.description
|
|
229
244
|
let pathName = path.replaceAll(/\/\{[^\}]*\}/g, 'X').replaceAll(/[^$\w\d_]+([$\w\d_])/g, (_, $1) => $1.toUpperCase())
|
|
230
245
|
let schemaName = `${method}${pathName}`.replace(/^\w/, c => c.toUpperCase())
|
|
231
246
|
|
|
232
247
|
let meta = '/**\n'
|
|
233
|
-
if (
|
|
248
|
+
if (def.summary) meta += ` * ${def.summary}\n *\n`
|
|
249
|
+
if (def.description) meta += ` * ${def.description.replace('\n', '\n * ')}\n`
|
|
234
250
|
if (def.operationId) meta += ` * @operationId ${def.operationId}\n`
|
|
235
251
|
if (def.externalDocs) meta += ` * @externalDocs ${def.externalDocs}\n`
|
|
236
252
|
if (def.tags) meta += ` * @tags ${def.tags.join(' ')}\n`
|
|
237
253
|
if (def.deprecated) meta == ' * @deprecated\n'
|
|
238
254
|
meta += ' */'
|
|
239
|
-
let endpoint = `${method}("${p}", ${schemaName}, ctx => {\n throw new
|
|
255
|
+
let endpoint = `${method}("${p}", ${schemaName}, ctx => {\n throw new NotImplementedError()\n})`
|
|
240
256
|
|
|
241
257
|
let sp = { path: {}, query: {}, header: {}, body: {}, formData: {} } // TODO handle body and formData cases
|
|
242
258
|
for (let _p of def?.parameters || []) {
|
|
243
259
|
// @ts-ignore: TODO handle refs cases
|
|
244
260
|
if (_p.$ref) continue
|
|
245
261
|
let p = _p as OpenAPIV3.ParameterObject
|
|
246
|
-
let o = (s: string) => (p.in !== 'path' && !p.required ? `$T.optional(${s})` : s)
|
|
247
|
-
sp[p.in][p.name] = o(
|
|
262
|
+
let o = (s: string) => (p.in !== 'path' && !p.required && !/^\$T.optional\(.*\)$/.test(s) ? `$T.optional(${s})` : s)
|
|
263
|
+
sp[p.in][p.name] = o(
|
|
264
|
+
unref(parseOapiSchema(p.schema, {description: p.description }), m => {
|
|
265
|
+
let l = m.split('/')
|
|
266
|
+
imports[l[l.length - 1]] = m
|
|
267
|
+
return l[l.length - 1]
|
|
268
|
+
})
|
|
269
|
+
)
|
|
248
270
|
}
|
|
249
271
|
|
|
250
272
|
let [schemaParams, schemaQuery, schemaHeaders] = [
|
|
@@ -254,34 +276,36 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
254
276
|
].map(({ g, o }) =>
|
|
255
277
|
Object.keys(sp[o]).length
|
|
256
278
|
? ` ${g}: {${Object.entries(sp[o])
|
|
257
|
-
.map(([k, v]) =>
|
|
279
|
+
.map(([k, v]) => `"${k}":${v}`)
|
|
258
280
|
.join(',')}}`
|
|
259
281
|
: ''
|
|
260
282
|
)
|
|
261
283
|
|
|
262
284
|
let body = ''
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
if(!['get', 'options', 'head'].includes(method)){
|
|
286
|
+
let _rb = def?.requestBody as OpenAPIV3.ReferenceObject
|
|
287
|
+
if (_rb?.$ref) {
|
|
288
|
+
body = unref(` body: %ref:${_rb.$ref}%`, m => {
|
|
289
|
+
let l = m.split('/')
|
|
290
|
+
imports[l[l.length - 1]] = m
|
|
291
|
+
return l[l.length - 1]
|
|
292
|
+
})
|
|
293
|
+
} else {
|
|
294
|
+
let rb = def?.requestBody as OpenAPIV3.RequestBodyObject
|
|
295
|
+
let o = (s: string) => (!rb?.required ? `$T.optional(${s})` : s)
|
|
296
|
+
let bs = [
|
|
297
|
+
...new Set(
|
|
298
|
+
Object.entries(rb?.content || {null:{}}).map(([media, v]) =>
|
|
299
|
+
unref(parseOapiSchema(v.schema, undefined, { media }), m => {
|
|
300
|
+
let l = m.split('/')
|
|
301
|
+
imports[l[l.length - 1]] = m
|
|
302
|
+
return l[l.length - 1]
|
|
303
|
+
})
|
|
304
|
+
)
|
|
281
305
|
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
306
|
+
]
|
|
307
|
+
body = bs.length === 1 ? ` body: ${o(bs[0])}` : bs.length > 1 ? ` body: ${o(`$T.union([${bs.join(',')}])`)}` : ''
|
|
308
|
+
}
|
|
285
309
|
}
|
|
286
310
|
|
|
287
311
|
let resp = ''
|
|
@@ -289,7 +313,18 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
289
313
|
let rs = Object.fromEntries(
|
|
290
314
|
Object.entries(r || {}).map(([status, sv]) => {
|
|
291
315
|
let entries: string[] = []
|
|
292
|
-
let s: string = Number.isInteger(Number(status)) ? status : '
|
|
316
|
+
let s: string = Number.isInteger(Number(status)) ? status : 'default'
|
|
317
|
+
|
|
318
|
+
//@ts-ignore
|
|
319
|
+
let rootRef = sv?.$ref ? unref(parseOapiSchema(sv), m => {
|
|
320
|
+
let l = m.split('/')
|
|
321
|
+
imports[l[l.length - 1]] = m
|
|
322
|
+
return l[l.length - 1]
|
|
323
|
+
}) : null
|
|
324
|
+
if (rootRef) {
|
|
325
|
+
return [s, [rootRef]]
|
|
326
|
+
}
|
|
327
|
+
|
|
293
328
|
for (let [_type, tv] of Object.entries((sv as OpenAPIV3.ResponseObject)?.content || {})) {
|
|
294
329
|
entries.push(
|
|
295
330
|
unref(parseOapiSchema(tv.schema), m => {
|
|
@@ -306,7 +341,7 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
306
341
|
if (Object.keys(rs).length) {
|
|
307
342
|
resp = ` response: {${Object.entries(rs)
|
|
308
343
|
.filter(([_, v]) => v.length)
|
|
309
|
-
.map(([s, v]) => `${s}: ${v.length === 1 ? v[0] : v.length > 1 ? `$T.union(${v.join(',')})` : ''}`)
|
|
344
|
+
.map(([s, v]) => `${s}: ${v.length === 1 ? v[0] : v.length > 1 ? `$T.union([${v.join(',')}])` : ''}`)
|
|
310
345
|
.join(',')}}`
|
|
311
346
|
} else resp = ''
|
|
312
347
|
|
|
@@ -394,7 +429,7 @@ const writeFiles = async (
|
|
|
394
429
|
})
|
|
395
430
|
if (decl.length === 0) return ''
|
|
396
431
|
return `import type { Static } from 'galbe/schema'\nimport { $T } from 'galbe'\n${Object.entries(imports)
|
|
397
|
-
.map(([k, v]) => `import { ${v.join(', ')} } from './${typeMap[k]}.schema'\n`)
|
|
432
|
+
.map(([k, v]) => `import { ${[...new Set(v)].join(', ')} } from './${typeMap[k]}.schema'\n`)
|
|
398
433
|
.join('\n')}\n${decl.join('\n')}\n`
|
|
399
434
|
}
|
|
400
435
|
|
|
@@ -461,7 +496,7 @@ const writeFiles = async (
|
|
|
461
496
|
`${sDecl.map(d => d).join('\n\n')}\n`
|
|
462
497
|
|
|
463
498
|
let routeFile =
|
|
464
|
-
`import type
|
|
499
|
+
`import { NotImplementedError, type Galbe } from 'galbe'\n` +
|
|
465
500
|
`import { ${[...rImports].join(', ')} } from '../schemas${scopeKey}.schema'\n\n` +
|
|
466
501
|
`export default (g: Galbe) => {\n` +
|
|
467
502
|
rDecl.map(d => d.replaceAll('\n', '\n ')).join('\n\n') +
|
|
@@ -43,20 +43,21 @@ export default (cmd: Command) => {
|
|
|
43
43
|
|
|
44
44
|
if (!format) format = `openapi:3.0:${inputExt.slice(1)}`
|
|
45
45
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
if (await exists(resolve(CWD, out))) {
|
|
47
|
+
if(!force){
|
|
48
|
+
console.log(
|
|
49
|
+
`error: output directory ${fmtVal(
|
|
50
|
+
out
|
|
51
|
+
)} already exists. If you're sure you want to override its content, please remove it before or use the ${fmtVal(
|
|
52
|
+
'-F --force'
|
|
53
|
+
)} option`
|
|
54
|
+
)
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
await rm(resolve(CWD, out), { recursive: true })
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
process.stdout.write('💻 \x1b[1;30mGenerating \x1b[36mGalbe\x1b[0m\x1b[1;30m sources\x1b[0m')
|
|
60
|
+
Bun.write(Bun.stdout, '💻 \x1b[1;30mGenerating \x1b[36mGalbe\x1b[0m\x1b[1;30m sources\x1b[0m')
|
|
60
61
|
try {
|
|
61
62
|
let match = format.match(/^([^:]*):([^:]*):(.*)$/)
|
|
62
63
|
if (!match) throw new Error(`error: invalid format ${format}`)
|
|
@@ -70,6 +71,6 @@ export default (cmd: Command) => {
|
|
|
70
71
|
console.log(`error: ${err.message}`)
|
|
71
72
|
process.exit(1)
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
74
75
|
})
|
|
75
76
|
}
|
|
@@ -54,7 +54,7 @@ export default (cmd: Command) => {
|
|
|
54
54
|
pckg = await Bun.file(resolve(CWD, 'package.json')).json()
|
|
55
55
|
} catch (e) {}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Bun.write(Bun.stdout, `📖 \x1b[1;30mGenerating ${target.split(':')?.[0]} spec\x1b[0m`)
|
|
58
58
|
|
|
59
59
|
let error = null
|
|
60
60
|
let g: Galbe = await silentExec(async () => {
|
|
@@ -89,6 +89,6 @@ export default (cmd: Command) => {
|
|
|
89
89
|
Bun.write(resolve(CWD, out), tFormat === 'json' ? JSON.stringify(openapiSpec, null, 2) : ymlDump(openapiSpec))
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
93
93
|
})
|
|
94
94
|
}
|