Skip to content

Commit 61a59ce

Browse files
committed
refactor: extract out typegen templating
1 parent 7241c07 commit 61a59ce

File tree

2 files changed

+172
-153
lines changed

2 files changed

+172
-153
lines changed
Lines changed: 8 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { FastifyInstance } from 'fastify'
2-
import prettier from 'prettier'
3-
import { PostgresMeta, PostgresType } from '../../../lib'
1+
import type { FastifyInstance } from 'fastify'
2+
import { PostgresMeta } from '../../../lib'
43
import { DEFAULT_POOL_CONFIG } from '../../constants'
54
import { extractRequestForLogging } from '../../utils'
5+
import { apply as applyTypescriptTemplate } from '../../templates/typescript'
66

77
export default async (fastify: FastifyInstance) => {
88
fastify.get<{
@@ -45,156 +45,11 @@ export default async (fastify: FastifyInstance) => {
4545
return { error: typesError.message }
4646
}
4747

48-
let output = `
49-
export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
50-
51-
export interface Database {
52-
${schemas
53-
.filter(({ name }) => !excludedSchemas.includes(name))
54-
.map(
55-
(schema) =>
56-
`${JSON.stringify(schema.name)}: {
57-
Tables: {
58-
${tables
59-
.filter((table) => table.schema === schema.name)
60-
.map(
61-
(table) => `${JSON.stringify(table.name)}: {
62-
Row: {
63-
${table.columns.map(
64-
(column) =>
65-
`${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, types)} ${
66-
column.is_nullable ? '| null' : ''
67-
}`
68-
)}
69-
}
70-
Insert: {
71-
${table.columns.map((column) => {
72-
let output = JSON.stringify(column.name)
73-
74-
if (column.identity_generation === 'ALWAYS') {
75-
return `${output}?: never`
76-
}
77-
78-
if (
79-
column.is_nullable ||
80-
column.is_identity ||
81-
column.default_value !== null
82-
) {
83-
output += '?:'
84-
} else {
85-
output += ':'
86-
}
87-
88-
output += pgTypeToTsType(column.format, types)
89-
90-
if (column.is_nullable) {
91-
output += '| null'
92-
}
93-
94-
return output
95-
})}
96-
}
97-
Update: {
98-
${table.columns.map((column) => {
99-
let output = JSON.stringify(column.name)
100-
101-
if (column.identity_generation === 'ALWAYS') {
102-
return `${output}?: never`
103-
}
104-
105-
output += `?: ${pgTypeToTsType(column.format, types)}`
106-
107-
if (column.is_nullable) {
108-
output += '| null'
109-
}
110-
111-
return output
112-
})}
113-
}
114-
}`
115-
)}
116-
}
117-
Functions: {
118-
${functions
119-
.filter(
120-
(function_) =>
121-
function_.schema === schema.name && function_.return_type !== 'trigger'
122-
)
123-
.map(
124-
(function_) => `${JSON.stringify(function_.name)}: {
125-
Args: ${(() => {
126-
if (function_.argument_types === '') {
127-
return 'Record<PropertyKey, never>'
128-
}
129-
130-
const splitArgs = function_.argument_types.split(',').map((arg) => arg.trim())
131-
if (splitArgs.some((arg) => arg.includes('"') || !arg.includes(' '))) {
132-
return 'Record<string, unknown>'
133-
}
134-
135-
const argsNameAndType = splitArgs.map((arg) => {
136-
const [name, ...rest] = arg.split(' ')
137-
const type = types.find((_type) => _type.format === rest.join(' '))
138-
if (!type) {
139-
return { name, type: 'unknown' }
140-
}
141-
return { name, type: pgTypeToTsType(type.name, types) }
142-
})
143-
144-
return `{ ${argsNameAndType.map(
145-
({ name, type }) => `${JSON.stringify(name)}: ${type}`
146-
)} }`
147-
})()}
148-
Returns: ${pgTypeToTsType(function_.return_type, types)}
149-
}`
150-
)}
151-
}
152-
}`
153-
)}
154-
}`
155-
156-
output = prettier.format(output, {
157-
parser: 'typescript',
48+
return applyTypescriptTemplate({
49+
schemas: schemas.filter(({ name }) => !excludedSchemas.includes(name)),
50+
tables,
51+
functions,
52+
types,
15853
})
159-
return output
16054
})
16155
}
162-
163-
// TODO: Make this more robust. Currently doesn't handle composite types - returns them as unknown.
164-
const pgTypeToTsType = (pgType: string, types: PostgresType[]): string => {
165-
if (pgType === 'bool') {
166-
return 'boolean'
167-
} else if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric'].includes(pgType)) {
168-
return 'number'
169-
} else if (
170-
[
171-
'bytea',
172-
'bpchar',
173-
'varchar',
174-
'date',
175-
'text',
176-
'time',
177-
'timetz',
178-
'timestamp',
179-
'timestamptz',
180-
'uuid',
181-
].includes(pgType)
182-
) {
183-
return 'string'
184-
} else if (['json', 'jsonb'].includes(pgType)) {
185-
return 'Json'
186-
} else if (pgType === 'void') {
187-
return 'undefined'
188-
} else if (pgType === 'record') {
189-
return 'Record<string, unknown>[]'
190-
} else if (pgType.startsWith('_')) {
191-
return pgTypeToTsType(pgType.substring(1), types) + '[]'
192-
} else {
193-
const type = types.find((type) => type.name === pgType && type.enums.length > 0)
194-
if (type) {
195-
return type.enums.map((variant) => JSON.stringify(variant)).join('|')
196-
}
197-
198-
return 'unknown'
199-
}
200-
}

src/server/templates/typescript.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import prettier from 'prettier'
2+
import type { PostgresFunction, PostgresSchema, PostgresTable, PostgresType } from '../../lib'
3+
4+
export const apply = ({
5+
schemas,
6+
tables,
7+
functions,
8+
types,
9+
}: {
10+
schemas: PostgresSchema[]
11+
tables: PostgresTable[]
12+
functions: PostgresFunction[]
13+
types: PostgresType[]
14+
}): string => {
15+
let output = `
16+
export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
17+
18+
export interface Database {
19+
${schemas.map(
20+
(schema) =>
21+
`${JSON.stringify(schema.name)}: {
22+
Tables: {
23+
${tables
24+
.filter((table) => table.schema === schema.name)
25+
.map(
26+
(table) => `${JSON.stringify(table.name)}: {
27+
Row: {
28+
${table.columns.map(
29+
(column) =>
30+
`${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, types)} ${
31+
column.is_nullable ? '| null' : ''
32+
}`
33+
)}
34+
}
35+
Insert: {
36+
${table.columns.map((column) => {
37+
let output = JSON.stringify(column.name)
38+
39+
if (column.identity_generation === 'ALWAYS') {
40+
return `${output}?: never`
41+
}
42+
43+
if (
44+
column.is_nullable ||
45+
column.is_identity ||
46+
column.default_value !== null
47+
) {
48+
output += '?:'
49+
} else {
50+
output += ':'
51+
}
52+
53+
output += pgTypeToTsType(column.format, types)
54+
55+
if (column.is_nullable) {
56+
output += '| null'
57+
}
58+
59+
return output
60+
})}
61+
}
62+
Update: {
63+
${table.columns.map((column) => {
64+
let output = JSON.stringify(column.name)
65+
66+
if (column.identity_generation === 'ALWAYS') {
67+
return `${output}?: never`
68+
}
69+
70+
output += `?: ${pgTypeToTsType(column.format, types)}`
71+
72+
if (column.is_nullable) {
73+
output += '| null'
74+
}
75+
76+
return output
77+
})}
78+
}
79+
}`
80+
)}
81+
}
82+
Functions: {
83+
${functions
84+
.filter(
85+
(function_) =>
86+
function_.schema === schema.name && function_.return_type !== 'trigger'
87+
)
88+
.map(
89+
(function_) => `${JSON.stringify(function_.name)}: {
90+
Args: ${(() => {
91+
if (function_.argument_types === '') {
92+
return 'Record<PropertyKey, never>'
93+
}
94+
95+
const splitArgs = function_.argument_types.split(',').map((arg) => arg.trim())
96+
if (splitArgs.some((arg) => arg.includes('"') || !arg.includes(' '))) {
97+
return 'Record<string, unknown>'
98+
}
99+
100+
const argsNameAndType = splitArgs.map((arg) => {
101+
const [name, ...rest] = arg.split(' ')
102+
const type = types.find((_type) => _type.format === rest.join(' '))
103+
if (!type) {
104+
return { name, type: 'unknown' }
105+
}
106+
return { name, type: pgTypeToTsType(type.name, types) }
107+
})
108+
109+
return `{ ${argsNameAndType.map(
110+
({ name, type }) => `${JSON.stringify(name)}: ${type}`
111+
)} }`
112+
})()}
113+
Returns: ${pgTypeToTsType(function_.return_type, types)}
114+
}`
115+
)}
116+
}
117+
}`
118+
)}
119+
}`
120+
121+
output = prettier.format(output, {
122+
parser: 'typescript',
123+
})
124+
return output
125+
}
126+
127+
// TODO: Make this more robust. Currently doesn't handle composite types - returns them as unknown.
128+
const pgTypeToTsType = (pgType: string, types: PostgresType[]): string => {
129+
if (pgType === 'bool') {
130+
return 'boolean'
131+
} else if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric'].includes(pgType)) {
132+
return 'number'
133+
} else if (
134+
[
135+
'bytea',
136+
'bpchar',
137+
'varchar',
138+
'date',
139+
'text',
140+
'time',
141+
'timetz',
142+
'timestamp',
143+
'timestamptz',
144+
'uuid',
145+
].includes(pgType)
146+
) {
147+
return 'string'
148+
} else if (['json', 'jsonb'].includes(pgType)) {
149+
return 'Json'
150+
} else if (pgType === 'void') {
151+
return 'undefined'
152+
} else if (pgType === 'record') {
153+
return 'Record<string, unknown>[]'
154+
} else if (pgType.startsWith('_')) {
155+
return pgTypeToTsType(pgType.substring(1), types) + '[]'
156+
} else {
157+
const type = types.find((type) => type.name === pgType && type.enums.length > 0)
158+
if (type) {
159+
return type.enums.map((variant) => JSON.stringify(variant)).join('|')
160+
}
161+
162+
return 'unknown'
163+
}
164+
}

0 commit comments

Comments
 (0)