Skip to content

Commit 8713c67

Browse files
committed
Safely return null in functions where applicable.
Add support for domains and take their nullability into account. Centralize logic in `pgTypeToTsType`.
1 parent 3cf0bff commit 8713c67

File tree

8 files changed

+266
-99
lines changed

8 files changed

+266
-99
lines changed

src/lib/sql/types.sql

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ select
33
t.typname as name,
44
n.nspname as schema,
55
format_type (t.oid, null) as format,
6+
nullif(t.typbasetype, 0) as base_type_id,
7+
not (t.typnotnull) as is_nullable,
68
coalesce(t_enums.enums, '[]') as enums,
79
coalesce(t_attributes.attributes, '[]') as attributes,
810
obj_description (t.oid, 'pg_type') as comment

src/lib/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ export const postgresTypeSchema = Type.Object({
357357
name: Type.String(),
358358
schema: Type.String(),
359359
format: Type.String(),
360+
base_type_id: Type.Optional(Type.Integer()),
361+
is_nullable: Type.Boolean(),
360362
enums: Type.Array(Type.String()),
361363
attributes: Type.Array(
362364
Type.Object({

src/server/routes/generators/typescript.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ export default async (fastify: FastifyInstance) => {
7777
functions: functions.filter(
7878
({ return_type }) => !['trigger', 'event_trigger'].includes(return_type)
7979
),
80-
types: types.filter(({ name }) => name[0] !== '_'),
81-
arrayTypes: types.filter(({ name }) => name[0] === '_'),
80+
types,
8281
})
8382
})
8483
}

src/server/server.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ if (EXPORT_DOCS) {
8080
functions: functions.filter(
8181
({ return_type }) => !['trigger', 'event_trigger'].includes(return_type)
8282
),
83-
types: types.filter(({ name }) => name[0] !== '_'),
84-
arrayTypes: types.filter(({ name }) => name[0] === '_'),
83+
types,
8584
})
8685
)
8786
} else {

src/server/templates/typescript.ts

+110-87
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ export const apply = ({
1515
materializedViews,
1616
functions,
1717
types,
18-
arrayTypes,
1918
}: {
2019
schemas: PostgresSchema[]
2120
tables: (PostgresTable & { columns: unknown[] })[]
2221
views: (PostgresView & { columns: unknown[] })[]
2322
materializedViews: (PostgresMaterializedView & { columns: unknown[] })[]
2423
functions: PostgresFunction[]
2524
types: PostgresType[]
26-
arrayTypes: PostgresType[]
2725
}): string => {
2826
let output = `
2927
export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]
@@ -63,6 +61,15 @@ export interface Database {
6361
const schemaEnums = types
6462
.filter((type) => type.schema === schema.name && type.enums.length > 0)
6563
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
64+
const schemaDomainTypes = types
65+
.flatMap((type) => {
66+
const baseType =
67+
type.schema === schema.name &&
68+
type.base_type_id &&
69+
types.find(({ id }) => id === type.base_type_id)
70+
return baseType ? [{ type, baseType }] : []
71+
})
72+
.sort(({ type: { name: a } }, { type: { name: b } }) => a.localeCompare(b))
6673
const schemaCompositeTypes = types
6774
.filter((type) => type.schema === schema.name && type.attributes.length > 0)
6875
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
@@ -82,8 +89,9 @@ export interface Database {
8289
`${JSON.stringify(column.name)}: ${pgTypeToTsType(
8390
column.format,
8491
types,
85-
schemas
86-
)} ${column.is_nullable ? '| null' : ''}`
92+
schemas,
93+
{ nullable: column.is_nullable }
94+
)}`
8795
),
8896
...schemaFunctions
8997
.filter((fn) => fn.argument_types === table.name)
@@ -93,7 +101,7 @@ export interface Database {
93101
fn.return_type,
94102
types,
95103
schemas
96-
)} | null`
104+
)}`
97105
),
98106
]}
99107
}
@@ -117,11 +125,9 @@ export interface Database {
117125
output += ':'
118126
}
119127
120-
output += pgTypeToTsType(column.format, types, schemas)
121-
122-
if (column.is_nullable) {
123-
output += '| null'
124-
}
128+
output += pgTypeToTsType(column.format, types, schemas, {
129+
nullable: column.is_nullable,
130+
})
125131
126132
return output
127133
})}
@@ -136,11 +142,9 @@ export interface Database {
136142
return `${output}?: never`
137143
}
138144
139-
output += `?: ${pgTypeToTsType(column.format, types, schemas)}`
140-
141-
if (column.is_nullable) {
142-
output += '| null'
143-
}
145+
output += `?: ${pgTypeToTsType(column.format, types, schemas, {
146+
nullable: column.is_nullable,
147+
})}`
144148
145149
return output
146150
})}
@@ -163,8 +167,9 @@ export interface Database {
163167
`${JSON.stringify(column.name)}: ${pgTypeToTsType(
164168
column.format,
165169
types,
166-
schemas
167-
)} ${column.is_nullable ? '| null' : ''}`
170+
schemas,
171+
{ nullable: column.is_nullable }
172+
)}`
168173
)}
169174
}
170175
${
@@ -179,7 +184,9 @@ export interface Database {
179184
return `${output}?: never`
180185
}
181186
182-
output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null`
187+
output += `?: ${pgTypeToTsType(column.format, types, schemas, {
188+
nullable: true,
189+
})}`
183190
184191
return output
185192
})}
@@ -198,7 +205,9 @@ export interface Database {
198205
return `${output}?: never`
199206
}
200207
201-
output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null`
208+
output += `?: ${pgTypeToTsType(column.format, types, schemas, {
209+
nullable: true,
210+
})}`
202211
203212
return output
204213
})}
@@ -239,17 +248,7 @@ export interface Database {
239248
}
240249
241250
const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => {
242-
let type = arrayTypes.find(({ id }) => id === type_id)
243-
if (type) {
244-
// If it's an array type, the name looks like `_int8`.
245-
const elementTypeName = type.name.substring(1)
246-
return {
247-
name,
248-
type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`,
249-
has_default,
250-
}
251-
}
252-
type = types.find(({ id }) => id === type_id)
251+
const type = types.find(({ id }) => id === type_id)
253252
if (type) {
254253
return {
255254
name,
@@ -272,19 +271,13 @@ export interface Database {
272271
const tableArgs = args.filter(({ mode }) => mode === 'table')
273272
if (tableArgs.length > 0) {
274273
const argsNameAndType = tableArgs.map(({ name, type_id }) => {
275-
let type = arrayTypes.find(({ id }) => id === type_id)
274+
const type = types.find(({ id }) => id === type_id)
276275
if (type) {
277-
// If it's an array type, the name looks like `_int8`.
278-
const elementTypeName = type.name.substring(1)
279276
return {
280277
name,
281-
type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`,
278+
type: pgTypeToTsType(type.name, types, schemas),
282279
}
283280
}
284-
type = types.find(({ id }) => id === type_id)
285-
if (type) {
286-
return { name, type: pgTypeToTsType(type.name, types, schemas) }
287-
}
288281
return { name, type: 'unknown' }
289282
})
290283
@@ -308,8 +301,9 @@ export interface Database {
308301
`${JSON.stringify(column.name)}: ${pgTypeToTsType(
309302
column.format,
310303
types,
311-
schemas
312-
)} ${column.is_nullable ? '| null' : ''}`
304+
schemas,
305+
{ nullable: column.is_nullable }
306+
)}`
313307
)}
314308
}`
315309
}
@@ -340,6 +334,21 @@ export interface Database {
340334
)
341335
}
342336
}
337+
DomainTypes: {
338+
${
339+
schemaDomainTypes.length === 0
340+
? '[_ in never]: never'
341+
: schemaDomainTypes.map(
342+
({ type: domain_, baseType }) =>
343+
`${JSON.stringify(domain_.name)}: ${pgTypeToTsType(
344+
baseType.name,
345+
types,
346+
schemas,
347+
{ nullable: domain_.is_nullable }
348+
)}`
349+
)
350+
}
351+
}
343352
CompositeTypes: {
344353
${
345354
schemaCompositeTypes.length === 0
@@ -377,58 +386,72 @@ export interface Database {
377386
const pgTypeToTsType = (
378387
pgType: string,
379388
types: PostgresType[],
380-
schemas: PostgresSchema[]
389+
schemas: PostgresSchema[],
390+
opts: { nullable?: boolean } = {}
381391
): string => {
382-
if (pgType === 'bool') {
383-
return 'boolean'
384-
} else if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric'].includes(pgType)) {
385-
return 'number'
386-
} else if (
387-
[
388-
'bytea',
389-
'bpchar',
390-
'varchar',
391-
'date',
392-
'text',
393-
'citext',
394-
'time',
395-
'timetz',
396-
'timestamp',
397-
'timestamptz',
398-
'uuid',
399-
'vector',
400-
].includes(pgType)
401-
) {
402-
return 'string'
403-
} else if (['json', 'jsonb'].includes(pgType)) {
404-
return 'Json'
405-
} else if (pgType === 'void') {
406-
return 'undefined'
407-
} else if (pgType === 'record') {
408-
return 'Record<string, unknown>'
409-
} else if (pgType.startsWith('_')) {
410-
return `(${pgTypeToTsType(pgType.substring(1), types, schemas)})[]`
411-
} else {
412-
const enumType = types.find((type) => type.name === pgType && type.enums.length > 0)
413-
if (enumType) {
414-
if (schemas.some(({ name }) => name === enumType.schema)) {
415-
return `Database[${JSON.stringify(enumType.schema)}]['Enums'][${JSON.stringify(
416-
enumType.name
417-
)}]`
392+
const type = types.find((type) => type.name === pgType)
393+
const strictTsType = pgTypeToStrictTsType()
394+
return strictTsType
395+
? `${strictTsType}${opts.nullable ?? type?.is_nullable ? ' | null' : ''}`
396+
: 'unknown'
397+
398+
function pgTypeToStrictTsType() {
399+
if (pgType === 'bool') {
400+
return 'boolean'
401+
} else if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric'].includes(pgType)) {
402+
return 'number'
403+
} else if (
404+
[
405+
'bytea',
406+
'bpchar',
407+
'varchar',
408+
'date',
409+
'text',
410+
'citext',
411+
'time',
412+
'timetz',
413+
'timestamp',
414+
'timestamptz',
415+
'uuid',
416+
'vector',
417+
].includes(pgType)
418+
) {
419+
return 'string'
420+
} else if (['json', 'jsonb'].includes(pgType)) {
421+
return 'Json'
422+
} else if (pgType === 'void') {
423+
return 'undefined'
424+
} else if (pgType === 'record') {
425+
return 'Record<string, unknown>'
426+
} else if (pgType.startsWith('_')) {
427+
return `(${pgTypeToTsType(pgType.substring(1), types, schemas)})[]`
428+
} else if (type != null) {
429+
if (type.base_type_id != null) {
430+
if (schemas.some(({ name }) => name === type.schema)) {
431+
return `Database[${JSON.stringify(type.schema)}]['DomainTypes'][${JSON.stringify(
432+
type.name
433+
)}]`
434+
}
435+
return undefined
436+
}
437+
438+
if (type.enums.length > 0) {
439+
if (schemas.some(({ name }) => name === type.schema)) {
440+
return `Database[${JSON.stringify(type.schema)}]['Enums'][${JSON.stringify(type.name)}]`
441+
}
442+
return type.enums.map((variant) => JSON.stringify(variant)).join('|')
418443
}
419-
return enumType.enums.map((variant) => JSON.stringify(variant)).join('|')
420-
}
421444

422-
const compositeType = types.find((type) => type.name === pgType && type.attributes.length > 0)
423-
if (compositeType) {
424-
if (schemas.some(({ name }) => name === compositeType.schema)) {
425-
return `Database[${JSON.stringify(
426-
compositeType.schema
427-
)}]['CompositeTypes'][${JSON.stringify(compositeType.name)}]`
445+
if (type.attributes.length > 0) {
446+
if (schemas.some(({ name }) => name === type.schema)) {
447+
return `Database[${JSON.stringify(type.schema)}]['CompositeTypes'][${JSON.stringify(
448+
type.name
449+
)}]`
450+
}
451+
return undefined
428452
}
429-
return 'unknown'
430453
}
431454

432-
return 'unknown'
455+
return undefined
433456
}
434457
}

0 commit comments

Comments
 (0)