galbe 0.12.2 → 0.13.1
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/generate/code/openapi.parser.ts +18 -11
- package/bin/commands/generate/index.ts +3 -1
- package/bin/commands/generate/model.ts +153 -0
- package/bin/commands/generate/spec.ts +7 -5
- package/bin/util.ts +9 -0
- package/package.json +3 -4
- package/src/parser.ts +19 -1
- package/src/types.ts +3 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { semver } from 'bun'
|
|
2
2
|
import { transformSync } from '@swc/core'
|
|
3
3
|
import { resolve, relative, dirname } from 'path'
|
|
4
|
-
import { load as ymlLoad } from 'js-yaml'
|
|
5
4
|
import { OpenAPIV3 } from 'openapi-types'
|
|
6
5
|
import { inferBodyType } from '../../../../src/util'
|
|
7
6
|
|
|
@@ -125,8 +124,6 @@ const parseOapiSchema = (
|
|
|
125
124
|
let optArg = hasOptions ? serialize(options) : ''
|
|
126
125
|
let anyOf = os.oneOf || os.anyOf
|
|
127
126
|
let allOf = os.allOf
|
|
128
|
-
let required = os.required
|
|
129
|
-
let nullable = os.nullable
|
|
130
127
|
|
|
131
128
|
if (os.discriminator) {
|
|
132
129
|
const propName = os.discriminator.propertyName
|
|
@@ -176,7 +173,11 @@ const parseOapiSchema = (
|
|
|
176
173
|
resp = `$T.integer(${hasOptions ? serialize(options) : ''})`
|
|
177
174
|
} else if (os.type === 'string') {
|
|
178
175
|
if (os.format === 'binary') resp = `$T.byteArray(${hasOptions ? serialize(options) : ''})`
|
|
179
|
-
else {
|
|
176
|
+
else if (os.enum?.length === 1) {
|
|
177
|
+
resp = `$T.literal("${os.enum[0]}")`
|
|
178
|
+
} else if (os.enum?.length) {
|
|
179
|
+
resp = `$T.union([${os.enum.map(v => `$T.literal("${v}")`).join(', ')}])`
|
|
180
|
+
} else {
|
|
180
181
|
let minLength = os.minLength
|
|
181
182
|
let maxLength = os.maxLength
|
|
182
183
|
let pattern = os.pattern
|
|
@@ -192,7 +193,16 @@ const parseOapiSchema = (
|
|
|
192
193
|
resp = `$T.array(${parseOapiSchema(os?.items)}, ${optArg})`
|
|
193
194
|
} else if (os.type === 'object') {
|
|
194
195
|
let props = Object.entries(os?.properties || {})
|
|
195
|
-
.map(([k, v]) =>
|
|
196
|
+
.map(([k, v]) => {
|
|
197
|
+
v = v as OpenAPIV3.SchemaObject
|
|
198
|
+
const w = (s: string) => {
|
|
199
|
+
if (!v.required && v.nullable) return `$T.nullish(${s})`
|
|
200
|
+
else if (!v.required) return `$T.optional(${s})`
|
|
201
|
+
else if (v.nullable) return `$T.nullable(${s})`
|
|
202
|
+
return s
|
|
203
|
+
}
|
|
204
|
+
return `"${k}":${w(parseOapiSchema(v))}`
|
|
205
|
+
})
|
|
196
206
|
.join(',')
|
|
197
207
|
if (extra?.media === 'multipart/form-data') {
|
|
198
208
|
resp = `$T.multipartForm({${props}}${optArg ? `, ${optArg}` : ''})`
|
|
@@ -203,10 +213,6 @@ const parseOapiSchema = (
|
|
|
203
213
|
}
|
|
204
214
|
} else throw new Error(`Unknown schema type ${JSON.stringify(os)}`)
|
|
205
215
|
|
|
206
|
-
if (!required && nullable) resp = `$T.nullish(${resp})`
|
|
207
|
-
else if (!required) resp = `$T.optional(${resp})`
|
|
208
|
-
else if (nullable) resp = `$T.nullable(${resp})`
|
|
209
|
-
|
|
210
216
|
return resp
|
|
211
217
|
}
|
|
212
218
|
|
|
@@ -452,7 +458,7 @@ const writeFiles = async (
|
|
|
452
458
|
imports[depOrig].push(depName)
|
|
453
459
|
}
|
|
454
460
|
}
|
|
455
|
-
decl.push(`export const ${s.key} = ${s.schema}\nexport type
|
|
461
|
+
decl.push(`export const ${s.key} = ${s.schema}\nexport type ${s.key} = Static<typeof ${s.key}>\n`)
|
|
456
462
|
})
|
|
457
463
|
if (decl.length === 0) return ''
|
|
458
464
|
return `import type { Static } from 'galbe/schema'\nimport { $T } from 'galbe'\n${Object.entries(imports)
|
|
@@ -539,7 +545,8 @@ export const generateFromOapi = async (
|
|
|
539
545
|
out: string,
|
|
540
546
|
{ version, ext, target }: { version: string; ext: 'json' | 'yaml'; target: 'js' | 'ts' }
|
|
541
547
|
) => {
|
|
542
|
-
let def: OpenAPIV3.Document =
|
|
548
|
+
let def: OpenAPIV3.Document =
|
|
549
|
+
ext === 'json' ? await Bun.file(input).json() : Bun.YAML.parse(await Bun.file(input).text())
|
|
543
550
|
|
|
544
551
|
let v = def?.openapi
|
|
545
552
|
if (!v || !semver.satisfies(v, version)) throw new Error('Invalid openapi version')
|
|
@@ -3,10 +3,12 @@ import { Command } from 'commander'
|
|
|
3
3
|
import client from './client'
|
|
4
4
|
import spec from './spec'
|
|
5
5
|
import code from './code'
|
|
6
|
+
import model from './model'
|
|
6
7
|
|
|
7
8
|
export default (cmd: Command) => {
|
|
8
9
|
cmd.description('generate util')
|
|
9
|
-
client(cmd.command('client'))
|
|
10
10
|
spec(cmd.command('spec'))
|
|
11
|
+
client(cmd.command('client'))
|
|
11
12
|
code(cmd.command('code'))
|
|
13
|
+
model(cmd.command('model'))
|
|
12
14
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Command, Option } from 'commander'
|
|
2
|
+
import { fmtVal, toPascalCase } from '../../util'
|
|
3
|
+
import { SQL } from 'bun'
|
|
4
|
+
import { mkdirSync } from 'fs'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
|
|
7
|
+
const TYPE_MAP: Record<string, string> = {
|
|
8
|
+
// --- Postgres + MySQL numerics ---
|
|
9
|
+
smallint: 'number',
|
|
10
|
+
integer: 'number',
|
|
11
|
+
int: 'number',
|
|
12
|
+
int2: 'number',
|
|
13
|
+
int4: 'number',
|
|
14
|
+
int8: 'number',
|
|
15
|
+
bigint: 'bigint', // or 'number' ?
|
|
16
|
+
decimal: 'number',
|
|
17
|
+
numeric: 'number',
|
|
18
|
+
real: 'number',
|
|
19
|
+
float: 'number',
|
|
20
|
+
float4: 'number',
|
|
21
|
+
float8: 'number',
|
|
22
|
+
double: 'number',
|
|
23
|
+
'double precision': 'number',
|
|
24
|
+
serial: 'number',
|
|
25
|
+
bigserial: 'number',
|
|
26
|
+
smallserial: 'number',
|
|
27
|
+
tinyint: 'number',
|
|
28
|
+
mediumint: 'number',
|
|
29
|
+
bit: 'number',
|
|
30
|
+
|
|
31
|
+
// --- Strings ---
|
|
32
|
+
text: 'string',
|
|
33
|
+
'character varying': 'string',
|
|
34
|
+
varchar: 'string',
|
|
35
|
+
character: 'string',
|
|
36
|
+
char: 'string',
|
|
37
|
+
citext: 'string',
|
|
38
|
+
enum: 'string',
|
|
39
|
+
set: 'string',
|
|
40
|
+
tinytext: 'string',
|
|
41
|
+
mediumtext: 'string',
|
|
42
|
+
longtext: 'string',
|
|
43
|
+
|
|
44
|
+
// --- Binary ---
|
|
45
|
+
bytea: 'Buffer',
|
|
46
|
+
blob: 'Buffer',
|
|
47
|
+
tinyblob: 'Buffer',
|
|
48
|
+
mediumblob: 'Buffer',
|
|
49
|
+
longblob: 'Buffer',
|
|
50
|
+
binary: 'Buffer',
|
|
51
|
+
varbinary: 'Buffer',
|
|
52
|
+
|
|
53
|
+
// --- Booleans ---
|
|
54
|
+
boolean: 'boolean',
|
|
55
|
+
|
|
56
|
+
// --- Date/Time ---
|
|
57
|
+
date: 'Date | string',
|
|
58
|
+
datetime: 'Date | string',
|
|
59
|
+
timestamp: 'Date | string',
|
|
60
|
+
'timestamp without time zone': 'Date | string',
|
|
61
|
+
'timestamp with time zone': 'Date | string',
|
|
62
|
+
time: 'string',
|
|
63
|
+
'time without time zone': 'string',
|
|
64
|
+
'time with time zone': 'string',
|
|
65
|
+
interval: 'string',
|
|
66
|
+
year: 'number',
|
|
67
|
+
|
|
68
|
+
// --- JSON ---
|
|
69
|
+
json: 'any',
|
|
70
|
+
jsonb: 'any',
|
|
71
|
+
|
|
72
|
+
// --- UUID ---
|
|
73
|
+
uuid: 'string',
|
|
74
|
+
|
|
75
|
+
// --- Spatial / geometric ---
|
|
76
|
+
point: 'any',
|
|
77
|
+
line: 'any',
|
|
78
|
+
lseg: 'any',
|
|
79
|
+
box: 'any',
|
|
80
|
+
path: 'any',
|
|
81
|
+
polygon: 'any',
|
|
82
|
+
circle: 'any',
|
|
83
|
+
geometry: 'any',
|
|
84
|
+
linestring: 'any',
|
|
85
|
+
multipoint: 'any',
|
|
86
|
+
multilinestring: 'any',
|
|
87
|
+
multipolygon: 'any',
|
|
88
|
+
geometrycollection: 'any',
|
|
89
|
+
|
|
90
|
+
// --- Other ---
|
|
91
|
+
xml: 'string',
|
|
92
|
+
money: 'string',
|
|
93
|
+
tsvector: 'string',
|
|
94
|
+
tsquery: 'string',
|
|
95
|
+
inet: 'string',
|
|
96
|
+
cidr: 'string',
|
|
97
|
+
macaddr: 'string',
|
|
98
|
+
macaddr8: 'string',
|
|
99
|
+
array: 'any[]',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default (cmd: Command) => {
|
|
103
|
+
cmd
|
|
104
|
+
.description('generate TypeScript types from a database')
|
|
105
|
+
.addOption(
|
|
106
|
+
new Option(
|
|
107
|
+
'-u, --url <connection_url>',
|
|
108
|
+
`database connection url (ex. ${fmtVal('postgres://postgres:secret@localhost:5432')})`
|
|
109
|
+
).makeOptionMandatory()
|
|
110
|
+
)
|
|
111
|
+
.addOption(new Option('-t, --table <table_name>', `table name (ex. users or public.users)`))
|
|
112
|
+
.addOption(new Option('-s, --schema <schema_name>', `schema name (ex. public)`).default('public', fmtVal('public')))
|
|
113
|
+
.addOption(new Option('-o, --out <dir|file>', 'output dir or file').default('.', fmtVal('.')))
|
|
114
|
+
.addOption(new Option('-F, --force', 'force overriding output'))
|
|
115
|
+
.action(async props => {
|
|
116
|
+
const { url, table, schema, out, force } = props
|
|
117
|
+
|
|
118
|
+
const db = new SQL({ url })
|
|
119
|
+
let tables: string[] = []
|
|
120
|
+
let types: Record<string, string> = {}
|
|
121
|
+
|
|
122
|
+
if (table) tables = [table]
|
|
123
|
+
else {
|
|
124
|
+
const r = await db.unsafe(`SELECT table_name
|
|
125
|
+
FROM information_schema.tables
|
|
126
|
+
WHERE table_schema = '${schema}'
|
|
127
|
+
AND table_type = 'BASE TABLE'`)
|
|
128
|
+
tables = r.map(r => r.table_name)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const tableName of tables) {
|
|
132
|
+
const t = await db.unsafe(`SELECT column_name, data_type, is_nullable
|
|
133
|
+
FROM information_schema.columns
|
|
134
|
+
WHERE table_schema = '${schema}' AND table_name = '${tableName}'`)
|
|
135
|
+
types[tableName] = `type ${toPascalCase(tableName)} = {\n${t
|
|
136
|
+
.map(r => ` ${r.column_name}: ${TYPE_MAP?.[r.data_type] ?? 'any'}${r.is_nullable ? ' | null' : ''}`)
|
|
137
|
+
.join(';\n')}\n}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
mkdirSync(path.dirname(out), { recursive: true })
|
|
141
|
+
|
|
142
|
+
if (path.extname(out) === '.ts') {
|
|
143
|
+
await Bun.write(
|
|
144
|
+
out,
|
|
145
|
+
Object.values(types).map(type => `export ${type}\n`)
|
|
146
|
+
)
|
|
147
|
+
} else {
|
|
148
|
+
for (const [tableName, type] of Object.entries(types)) {
|
|
149
|
+
await Bun.write(`${out}/${tableName}.ts`, `export ${type}\n`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Command, Option } from 'commander'
|
|
2
2
|
import { resolve, relative, extname } from 'path'
|
|
3
|
-
import { dump as ymlDump, load as ymlLoad } from 'js-yaml'
|
|
4
3
|
import { CWD, fmtList, instanciateRoutes, silentExec } from '../../util'
|
|
5
4
|
import { Galbe } from '../../../src'
|
|
6
5
|
import { OpenAPISerializer } from '../../../src/extras'
|
|
@@ -47,7 +46,7 @@ export default (cmd: Command) => {
|
|
|
47
46
|
if (base) {
|
|
48
47
|
let ext = extname(base)
|
|
49
48
|
let rd = (str: string) =>
|
|
50
|
-
ext === '.json' ? JSON.parse(str) : ['.yml', '.yaml'].includes(ext) ?
|
|
49
|
+
ext === '.json' ? JSON.parse(str) : ['.yml', '.yaml'].includes(ext) ? Bun.YAML.parse(str) : null
|
|
51
50
|
baseSpec = rd(await Bun.file(relative(CWD, base)).text())
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -84,11 +83,14 @@ export default (cmd: Command) => {
|
|
|
84
83
|
description: pckg?.description,
|
|
85
84
|
contact: parseAuthor(pckg.author),
|
|
86
85
|
//license: TODO
|
|
87
|
-
version: pckg?.version || '0.1.0'
|
|
88
|
-
}
|
|
86
|
+
version: pckg?.version || '0.1.0',
|
|
87
|
+
},
|
|
89
88
|
}
|
|
90
89
|
openapiSpec = softMerge(openapiSpec, baseSpec) as OpenAPIV3.Document
|
|
91
|
-
Bun.write(
|
|
90
|
+
Bun.write(
|
|
91
|
+
resolve(CWD, out),
|
|
92
|
+
tFormat === 'json' ? JSON.stringify(openapiSpec, null, 2) : Bun.YAML.stringify(openapiSpec, null, 2)
|
|
93
|
+
)
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
package/bin/util.ts
CHANGED
|
@@ -194,3 +194,12 @@ export const HttpStatus = {
|
|
|
194
194
|
507: 'Insufficient Storage',
|
|
195
195
|
511: 'Network Authentication Required',
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
export const toPascalCase = (input: string) =>
|
|
199
|
+
input
|
|
200
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
201
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
202
|
+
.trim()
|
|
203
|
+
.split(/\s+/)
|
|
204
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
205
|
+
.join('')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "galbe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"release": "release-it"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/bun": "^1.
|
|
42
|
+
"@types/bun": "^1.2.22",
|
|
43
43
|
"openapi-types": "^12.1.3",
|
|
44
44
|
"release-it": "^17.1.1"
|
|
45
45
|
},
|
|
@@ -52,8 +52,7 @@
|
|
|
52
52
|
"acorn": "^8.11.2",
|
|
53
53
|
"acorn-walk": "^8.3.0",
|
|
54
54
|
"chokidar": "^3.6.0",
|
|
55
|
-
"commander": "^11.1.0"
|
|
56
|
-
"js-yaml": "^4.1.0"
|
|
55
|
+
"commander": "^11.1.0"
|
|
57
56
|
},
|
|
58
57
|
"release-it": {
|
|
59
58
|
"git": {
|
package/src/parser.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
STNull,
|
|
13
13
|
STPropsValue,
|
|
14
14
|
STUnion,
|
|
15
|
+
STIntersection,
|
|
15
16
|
} from './schema'
|
|
16
17
|
|
|
17
18
|
import { readableStreamToArrayBuffer } from 'bun'
|
|
@@ -112,13 +113,20 @@ export const requestBodyParser = async (
|
|
|
112
113
|
}
|
|
113
114
|
return await streamToString(body, schema as STBodyValue)
|
|
114
115
|
} else if (contentType === 'json') {
|
|
115
|
-
if (
|
|
116
|
+
if (
|
|
117
|
+
!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'union', 'intersection'].includes(kind)
|
|
118
|
+
)
|
|
116
119
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
117
120
|
if (kind === 'union') {
|
|
118
121
|
let str = body === null ? 'null' : await streamToString(body)
|
|
119
122
|
let json = JSON.parse(str)
|
|
120
123
|
return unionize(json, schema)
|
|
121
124
|
}
|
|
125
|
+
if (kind === 'intersection') {
|
|
126
|
+
let str = body === null ? 'null' : await streamToString(body)
|
|
127
|
+
let json = JSON.parse(str)
|
|
128
|
+
return intersectionize(json, schema)
|
|
129
|
+
}
|
|
122
130
|
const str = body === null ? 'null' : await streamToString(body)
|
|
123
131
|
let json
|
|
124
132
|
try {
|
|
@@ -753,3 +761,13 @@ const unionize = (b: any, schema: STUnion) => {
|
|
|
753
761
|
else if (error) throw new RequestError({ status: 400, payload: { body: error } })
|
|
754
762
|
else throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
|
|
755
763
|
}
|
|
764
|
+
|
|
765
|
+
const intersectionize = (b: any, schema: STIntersection) => {
|
|
766
|
+
let res
|
|
767
|
+
try {
|
|
768
|
+
for (let s of schema.allOf) res = validate(b, s, { parse: true })
|
|
769
|
+
return res
|
|
770
|
+
} catch (e) {
|
|
771
|
+
throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
|
|
772
|
+
}
|
|
773
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
STBoolean,
|
|
6
6
|
STByteArray,
|
|
7
7
|
STInteger,
|
|
8
|
+
STIntersection,
|
|
8
9
|
STJson,
|
|
9
10
|
STLiteral,
|
|
10
11
|
STMultipartForm,
|
|
@@ -40,7 +41,7 @@ export type STBody =
|
|
|
40
41
|
| Partial<{
|
|
41
42
|
byteArray?: STByteArray | STStream
|
|
42
43
|
text?: STString | STLiteral | STBoolean | STNumber | STInteger | STUnion | STStream
|
|
43
|
-
json?: STJson | STObject | STBoolean | STInteger | STNumber | STString | STArray | STUnion
|
|
44
|
+
json?: STJson | STObject | STBoolean | STInteger | STNumber | STString | STArray | STUnion | STIntersection
|
|
44
45
|
urlForm?: STObject | STStream | STUnion
|
|
45
46
|
multipart?: STMultipartForm | STStream | STUnion
|
|
46
47
|
default?: STString | STByteArray | STStream | STAny
|
|
@@ -269,7 +270,7 @@ export class RequestError {
|
|
|
269
270
|
payload?: any
|
|
270
271
|
headers?: Record<string, string>
|
|
271
272
|
constructor(options: { status?: number; payload?: any; headers?: Record<string, string> }) {
|
|
272
|
-
this.status = options.status ??
|
|
273
|
+
this.status = options.status ?? 400
|
|
273
274
|
this.payload = options.payload
|
|
274
275
|
this.headers = options.headers
|
|
275
276
|
}
|