galbe 0.12.2 → 0.13.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.
|
@@ -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.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",
|
|
@@ -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/types.ts
CHANGED
|
@@ -269,7 +269,7 @@ export class RequestError {
|
|
|
269
269
|
payload?: any
|
|
270
270
|
headers?: Record<string, string>
|
|
271
271
|
constructor(options: { status?: number; payload?: any; headers?: Record<string, string> }) {
|
|
272
|
-
this.status = options.status ??
|
|
272
|
+
this.status = options.status ?? 400
|
|
273
273
|
this.payload = options.payload
|
|
274
274
|
this.headers = options.headers
|
|
275
275
|
}
|