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]) => `"${k}":${parseOapiSchema(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 T${s.key} = Static<typeof ${s.key}>\n`)
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 = ext === 'json' ? await Bun.file(input).json() : ymlLoad(await Bun.file(input).text())
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) ? ymlLoad(str) : null
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(resolve(CWD, out), tFormat === 'json' ? JSON.stringify(openapiSpec, null, 2) : ymlDump(openapiSpec))
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.12.2",
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.0.4",
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 ?? 500
272
+ this.status = options.status ?? 400
273
273
  this.payload = options.payload
274
274
  this.headers = options.headers
275
275
  }