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.
@@ -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.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.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/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 (!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'union'].includes(kind))
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 ?? 500
273
+ this.status = options.status ?? 400
273
274
  this.payload = options.payload
274
275
  this.headers = options.headers
275
276
  }