From 3cff801a5ea9e1044c7251a99d9d9022bf57f16f Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 3 Jan 2024 13:57:53 +0000 Subject: [PATCH 1/4] chore(typegen): migrate metadata collection to function --- src/lib/generators.ts | 102 +++++++++++++++++++++ src/server/routes/generators/typescript.ts | 102 +++------------------ 2 files changed, 115 insertions(+), 89 deletions(-) create mode 100644 src/lib/generators.ts diff --git a/src/lib/generators.ts b/src/lib/generators.ts new file mode 100644 index 00000000..03b1bd08 --- /dev/null +++ b/src/lib/generators.ts @@ -0,0 +1,102 @@ +import PostgresMeta from "./PostgresMeta.js"; +import { PostgresColumn, PostgresFunction, PostgresMaterializedView, PostgresRelationship, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "./types.js"; +import { PostgresMetaResult } from "./types.js"; + +type GeneratorMetadata = { + schemas: PostgresSchema[]; + tables: PostgresTable[]; + views: PostgresView[]; + materializedViews: PostgresMaterializedView[]; + columns: PostgresColumn[]; + relationships: PostgresRelationship[]; + functions: PostgresFunction[]; + types: PostgresType[]; + arrayTypes: PostgresType[]; +} + +export async function getGeneratorMetadata(pgMeta: PostgresMeta, filters: { includedSchemas?: string[]; excludedSchemas?: string[] } = { includedSchemas: [], excludedSchemas: [] }): Promise> { + const includedSchemas = filters.includedSchemas ?? [] + const excludedSchemas = filters.excludedSchemas ?? [] + + const { data: schemas, error: schemasError } = await pgMeta.schemas.list() + if (schemasError) { + return { data: null, error: schemasError } + } + + const { data: tables, error: tablesError } = await pgMeta.tables.list({ + includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, + excludedSchemas, + includeColumns: false, + }) + if (tablesError) { + return { data: null, error: tablesError } + } + + const { data: views, error: viewsError } = await pgMeta.views.list({ + includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, + excludedSchemas, + includeColumns: false, + }) + if (viewsError) { + return { data: null, error: viewsError } + } + + const { data: materializedViews, error: materializedViewsError } = + await pgMeta.materializedViews.list({ + includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, + excludedSchemas, + includeColumns: false, + }) + if (materializedViewsError) { + return { data: null, error: materializedViewsError } + } + + const { data: columns, error: columnsError } = await pgMeta.columns.list({ + includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, + excludedSchemas, + }) + if (columnsError) { + return { data: null, error: columnsError } + } + + const { data: relationships, error: relationshipsError } = await pgMeta.relationships.list() + if (relationshipsError) { + return { data: null, error: relationshipsError } + } + + const { data: functions, error: functionsError } = await pgMeta.functions.list({ + includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, + excludedSchemas, + }) + if (functionsError) { + return { data: null, error: functionsError } + } + + const { data: types, error: typesError } = await pgMeta.types.list({ + includeArrayTypes: true, + includeSystemSchemas: true, + }) + if (typesError) { + return { data: null, error: typesError } + } + + const nonArrayTypes = types.filter(({ name }) => name[0] !== '_') + const arrayTypes = types.filter(({ name }) => name[0] === '_') + + await pgMeta.end() + + return { + data: { + schemas, + tables, + views, + materializedViews, + columns, + relationships, + functions, + types: nonArrayTypes, + arrayTypes, + }, + error: null, + } +} diff --git a/src/server/routes/generators/typescript.ts b/src/server/routes/generators/typescript.ts index e5c69b26..e67a3971 100644 --- a/src/server/routes/generators/typescript.ts +++ b/src/server/routes/generators/typescript.ts @@ -3,6 +3,7 @@ import { PostgresMeta } from '../../../lib/index.js' import { DEFAULT_POOL_CONFIG } from '../../constants.js' import { extractRequestForLogging } from '../../utils.js' import { apply as applyTypescriptTemplate } from '../../templates/typescript.js' +import { getGeneratorMetadata } from '../../../lib/generators.js' export default async (fastify: FastifyInstance) => { fastify.get<{ @@ -21,98 +22,21 @@ export default async (fastify: FastifyInstance) => { const detectOneToOneRelationships = request.query.detect_one_to_one_relationships === 'true' const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data: schemas, error: schemasError } = await pgMeta.schemas.list() - const { data: tables, error: tablesError } = await pgMeta.tables.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas, - includeColumns: false, - }) - const { data: views, error: viewsError } = await pgMeta.views.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas, - includeColumns: false, - }) - const { data: materializedViews, error: materializedViewsError } = - await pgMeta.materializedViews.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas, - includeColumns: false, - }) - const { data: columns, error: columnsError } = await pgMeta.columns.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas, - }) - const { data: relationships, error: relationshipsError } = await pgMeta.relationships.list() - const { data: functions, error: functionsError } = await pgMeta.functions.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas, - }) - const { data: types, error: typesError } = await pgMeta.types.list({ - includeArrayTypes: true, - includeSystemSchemas: true, - }) - await pgMeta.end() - - if (schemasError) { - request.log.error({ error: schemasError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: schemasError.message } - } - if (tablesError) { - request.log.error({ error: tablesError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: tablesError.message } - } - if (viewsError) { - request.log.error({ error: viewsError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: viewsError.message } - } - if (materializedViewsError) { - request.log.error({ - error: materializedViewsError, - request: extractRequestForLogging(request), - }) - reply.code(500) - return { error: materializedViewsError.message } - } - if (columnsError) { - request.log.error({ error: columnsError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: columnsError.message } - } - if (relationshipsError) { - request.log.error({ error: relationshipsError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: relationshipsError.message } - } - if (functionsError) { - request.log.error({ error: functionsError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: functionsError.message } - } - if (typesError) { - request.log.error({ error: typesError, request: extractRequestForLogging(request) }) - reply.code(500) - return { error: typesError.message } + const { data: generatorMeta, error: generatorMetaError } = await getGeneratorMetadata( + pgMeta, + { + includedSchemas, + excludedSchemas + }, + ) + if (generatorMetaError) { + request.log.error({ error: generatorMetaError, request: extractRequestForLogging(request) }) + reply.code(500) + return { error: generatorMetaError.message } } return applyTypescriptTemplate({ - schemas: schemas.filter( - ({ name }) => - !excludedSchemas.includes(name) && - (includedSchemas.length === 0 || includedSchemas.includes(name)) - ), - tables, - views, - materializedViews, - columns, - relationships, - functions: functions.filter( - ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) - ), - types: types.filter(({ name }) => name[0] !== '_'), - arrayTypes: types.filter(({ name }) => name[0] === '_'), + ...generatorMeta, detectOneToOneRelationships, }) }) From c08dec49f1fbca93410441ffa9f2fa88b4a9ddad Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 3 Jan 2024 14:03:23 +0000 Subject: [PATCH 2/4] chore(typegen): fix metadata collection function filtering --- src/lib/generators.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lib/generators.ts b/src/lib/generators.ts index 03b1bd08..1648b6d3 100644 --- a/src/lib/generators.ts +++ b/src/lib/generators.ts @@ -87,13 +87,20 @@ export async function getGeneratorMetadata(pgMeta: PostgresMeta, filters: { incl return { data: { - schemas, + schemas: schemas.filter( + ({ name }) => + !excludedSchemas.includes(name) && + (includedSchemas.length === 0 || includedSchemas.includes(name) + ) + ), tables, views, materializedViews, columns, relationships, - functions, + functions: functions.filter( + ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) + ), types: nonArrayTypes, arrayTypes, }, From 5e5f558f322b29d0c14ce39d479fbbe9d80dc5cb Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 3 Jan 2024 14:08:15 +0000 Subject: [PATCH 3/4] chore(typegen): fix, export and use GeneratorMetadata type --- src/lib/generators.ts | 8 +- src/server/templates/typescript.ts | 573 ++++++++++++++--------------- 2 files changed, 283 insertions(+), 298 deletions(-) diff --git a/src/lib/generators.ts b/src/lib/generators.ts index 1648b6d3..c9a1b79a 100644 --- a/src/lib/generators.ts +++ b/src/lib/generators.ts @@ -2,11 +2,11 @@ import PostgresMeta from "./PostgresMeta.js"; import { PostgresColumn, PostgresFunction, PostgresMaterializedView, PostgresRelationship, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "./types.js"; import { PostgresMetaResult } from "./types.js"; -type GeneratorMetadata = { +export type GeneratorMetadata = { schemas: PostgresSchema[]; - tables: PostgresTable[]; - views: PostgresView[]; - materializedViews: PostgresMaterializedView[]; + tables: Omit[]; + views: Omit[]; + materializedViews: Omit[]; columns: PostgresColumn[]; relationships: PostgresRelationship[]; functions: PostgresFunction[]; diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 78c4ee8d..aa303425 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -9,6 +9,7 @@ import type { PostgresType, PostgresView, } from '../../lib/index.js' +import type { GeneratorMetadata } from '../../lib/generators.js' export const apply = ({ schemas, @@ -21,16 +22,7 @@ export const apply = ({ types, arrayTypes, detectOneToOneRelationships, -}: { - schemas: PostgresSchema[] - tables: Omit[] - views: Omit[] - materializedViews: Omit[] - columns: PostgresColumn[] - relationships: PostgresRelationship[] - functions: PostgresFunction[] - types: PostgresType[] - arrayTypes: PostgresType[] +}: GeneratorMetadata & { detectOneToOneRelationships: boolean }): string => { const columnsByTableId = columns @@ -46,371 +38,364 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un export interface Database { ${schemas - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .map((schema) => { - const schemaTables = tables - .filter((table) => table.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaViews = [...views, ...materializedViews] - .filter((view) => view.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaFunctions = functions - .filter((func) => { - if (func.schema !== schema.name) { - return false - } + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + .map((schema) => { + const schemaTables = tables + .filter((table) => table.schema === schema.name) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaViews = [...views, ...materializedViews] + .filter((view) => view.schema === schema.name) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaFunctions = functions + .filter((func) => { + if (func.schema !== schema.name) { + return false + } - // Either: - // 1. All input args are be named, or - // 2. There is only one input arg which is unnamed - const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) + // Either: + // 1. All input args are be named, or + // 2. There is only one input arg which is unnamed + const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) - if (!inArgs.some(({ name }) => name === '')) { - return true - } + if (!inArgs.some(({ name }) => name === '')) { + return true + } - if (inArgs.length === 1) { - return true - } + if (inArgs.length === 1) { + return true + } - return false - }) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaCompositeTypes = types - .filter((type) => type.schema === schema.name && type.attributes.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - return `${JSON.stringify(schema.name)}: { + return false + }) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaEnums = types + .filter((type) => type.schema === schema.name && type.enums.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaCompositeTypes = types + .filter((type) => type.schema === schema.name && type.attributes.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + return `${JSON.stringify(schema.name)}: { Tables: { - ${ - schemaTables.length === 0 - ? '[_ in never]: never' - : schemaTables.map( - (table) => `${JSON.stringify(table.name)}: { + ${schemaTables.length === 0 + ? '[_ in never]: never' + : schemaTables.map( + (table) => `${JSON.stringify(table.name)}: { Row: { ${[ - ...columnsByTableId[table.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - column.format, - types, - schemas - )} ${column.is_nullable ? '| null' : ''}` - ), - ...schemaFunctions - .filter((fn) => fn.argument_types === table.name) - .map((fn) => { - const type = types.find(({ id }) => id === fn.return_type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(type.name, types, schemas) - } - return `${JSON.stringify(fn.name)}: ${tsType} | null` - }), - ]} + ...columnsByTableId[table.id].map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType( + column.format, + types, + schemas + )} ${column.is_nullable ? '| null' : ''}` + ), + ...schemaFunctions + .filter((fn) => fn.argument_types === table.name) + .map((fn) => { + const type = types.find(({ id }) => id === fn.return_type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(type.name, types, schemas) + } + return `${JSON.stringify(fn.name)}: ${tsType} | null` + }), + ]} } Insert: { ${columnsByTableId[table.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (column.identity_generation === 'ALWAYS') { - return `${output}?: never` - } + if (column.identity_generation === 'ALWAYS') { + return `${output}?: never` + } - if ( - column.is_nullable || - column.is_identity || - column.default_value !== null - ) { - output += '?:' - } else { - output += ':' - } + if ( + column.is_nullable || + column.is_identity || + column.default_value !== null + ) { + output += '?:' + } else { + output += ':' + } - output += pgTypeToTsType(column.format, types, schemas) + output += pgTypeToTsType(column.format, types, schemas) - if (column.is_nullable) { - output += '| null' - } + if (column.is_nullable) { + output += '| null' + } - return output - })} + return output + })} } Update: { ${columnsByTableId[table.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (column.identity_generation === 'ALWAYS') { - return `${output}?: never` - } + if (column.identity_generation === 'ALWAYS') { + return `${output}?: never` + } - output += `?: ${pgTypeToTsType(column.format, types, schemas)}` + output += `?: ${pgTypeToTsType(column.format, types, schemas)}` - if (column.is_nullable) { - output += '| null' - } + if (column.is_nullable) { + output += '| null' + } - return output - })} + return output + })} } Relationships: [ ${relationships - .filter( - (relationship) => - relationship.schema === table.schema && - relationship.relation === table.name - ) - .sort( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) - ) - .map( - (relationship) => `{ + .filter( + (relationship) => + relationship.schema === table.schema && + relationship.relation === table.name + ) + .sort( + (a, b) => + a.foreign_key_name.localeCompare(b.foreign_key_name) || + a.referenced_relation.localeCompare(b.referenced_relation) + ) + .map( + (relationship) => `{ foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} columns: ${JSON.stringify(relationship.columns)} - ${ - detectOneToOneRelationships - ? `isOneToOne: ${relationship.is_one_to_one};` - : '' - }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} + ${detectOneToOneRelationships + ? `isOneToOne: ${relationship.is_one_to_one};` + : '' + }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} referencedColumns: ${JSON.stringify(relationship.referenced_columns)} }` - )} + )} ] }` - ) - } + ) + } } Views: { - ${ - schemaViews.length === 0 - ? '[_ in never]: never' - : schemaViews.map( - (view) => `${JSON.stringify(view.name)}: { + ${schemaViews.length === 0 + ? '[_ in never]: never' + : schemaViews.map( + (view) => `${JSON.stringify(view.name)}: { Row: { ${columnsByTableId[view.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - column.format, - types, - schemas - )} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType( + column.format, + types, + schemas + )} ${column.is_nullable ? '| null' : ''}` + )} } - ${ - 'is_updatable' in view && view.is_updatable - ? `Insert: { + ${'is_updatable' in view && view.is_updatable + ? `Insert: { ${columnsByTableId[view.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (!column.is_updatable) { - return `${output}?: never` - } + if (!column.is_updatable) { + return `${output}?: never` + } - output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` + output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` - return output - })} + return output + })} } Update: { ${columnsByTableId[view.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (!column.is_updatable) { - return `${output}?: never` - } + if (!column.is_updatable) { + return `${output}?: never` + } - output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` + output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` - return output - })} + return output + })} } ` - : '' - }Relationships: [ + : '' + }Relationships: [ ${relationships - .filter( - (relationship) => - relationship.schema === view.schema && relationship.relation === view.name - ) - .sort(({ foreign_key_name: a }, { foreign_key_name: b }) => - a.localeCompare(b) - ) - .map( - (relationship) => `{ + .filter( + (relationship) => + relationship.schema === view.schema && relationship.relation === view.name + ) + .sort(({ foreign_key_name: a }, { foreign_key_name: b }) => + a.localeCompare(b) + ) + .map( + (relationship) => `{ foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} columns: ${JSON.stringify(relationship.columns)} - ${ - detectOneToOneRelationships - ? `isOneToOne: ${relationship.is_one_to_one};` - : '' - }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} + ${detectOneToOneRelationships + ? `isOneToOne: ${relationship.is_one_to_one};` + : '' + }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} referencedColumns: ${JSON.stringify(relationship.referenced_columns)} }` - )} + )} ] }` - ) - } + ) + } } Functions: { ${(() => { - if (schemaFunctions.length === 0) { - return '[_ in never]: never' - } - - const schemaFunctionsGroupedByName = schemaFunctions.reduce((acc, curr) => { - acc[curr.name] ??= [] - acc[curr.name].push(curr) - return acc - }, {} as Record) - - return Object.entries(schemaFunctionsGroupedByName).map( - ([fnName, fns]) => - `${JSON.stringify(fnName)}: ${fns - .map( - ({ - args, - return_type_id, - return_type_relation_id, - is_set_returning_function, - }) => `{ - Args: ${(() => { - const inArgs = args.filter(({ mode }) => mode === 'in') + if (schemaFunctions.length === 0) { + return '[_ in never]: never' + } - if (inArgs.length === 0) { - return 'Record' - } + const schemaFunctionsGroupedByName = schemaFunctions.reduce((acc, curr) => { + acc[curr.name] ??= [] + acc[curr.name].push(curr) + return acc + }, {} as Record) + + return Object.entries(schemaFunctionsGroupedByName).map( + ([fnName, fns]) => + `${JSON.stringify(fnName)}: ${fns + .map( + ({ + args, + return_type_id, + return_type_relation_id, + is_set_returning_function, + }) => `{ + Args: ${(() => { + const inArgs = args.filter(({ mode }) => mode === 'in') - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - let type = arrayTypes.find(({ id }) => id === type_id) - if (type) { - // If it's an array type, the name looks like `_int8`. - const elementTypeName = type.name.substring(1) - return { - name, - type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, - has_default, - } - } - type = types.find(({ id }) => id === type_id) - if (type) { - return { - name, - type: pgTypeToTsType(type.name, types, schemas), - has_default, + if (inArgs.length === 0) { + return 'Record' } - } - return { name, type: 'unknown', has_default } - }) - return `{ + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + let type = arrayTypes.find(({ id }) => id === type_id) + if (type) { + // If it's an array type, the name looks like `_int8`. + const elementTypeName = type.name.substring(1) + return { + name, + type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, + has_default, + } + } + type = types.find(({ id }) => id === type_id) + if (type) { + return { + name, + type: pgTypeToTsType(type.name, types, schemas), + has_default, + } + } + return { name, type: 'unknown', has_default } + }) + + return `{ ${argsNameAndType.map( - ({ name, type, has_default }) => - `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}` - )} + ({ name, type, has_default }) => + `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}` + )} }` - })()} + })()} Returns: (${(() => { - // Case 1: `returns table`. - const tableArgs = args.filter(({ mode }) => mode === 'table') - if (tableArgs.length > 0) { - const argsNameAndType = tableArgs.map(({ name, type_id }) => { - let type = arrayTypes.find(({ id }) => id === type_id) - if (type) { - // If it's an array type, the name looks like `_int8`. - const elementTypeName = type.name.substring(1) - return { - name, - type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, - } - } - type = types.find(({ id }) => id === type_id) - if (type) { - return { name, type: pgTypeToTsType(type.name, types, schemas) } - } - return { name, type: 'unknown' } - }) - - return `{ + // Case 1: `returns table`. + const tableArgs = args.filter(({ mode }) => mode === 'table') + if (tableArgs.length > 0) { + const argsNameAndType = tableArgs.map(({ name, type_id }) => { + let type = arrayTypes.find(({ id }) => id === type_id) + if (type) { + // If it's an array type, the name looks like `_int8`. + const elementTypeName = type.name.substring(1) + return { + name, + type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, + } + } + type = types.find(({ id }) => id === type_id) + if (type) { + return { name, type: pgTypeToTsType(type.name, types, schemas) } + } + return { name, type: 'unknown' } + }) + + return `{ ${argsNameAndType.map( - ({ name, type }) => `${JSON.stringify(name)}: ${type}` - )} + ({ name, type }) => `${JSON.stringify(name)}: ${type}` + )} }` - } + } - // Case 2: returns a relation's row type. - const relation = [...tables, ...views].find( - ({ id }) => id === return_type_relation_id - ) - if (relation) { - return `{ + // Case 2: returns a relation's row type. + const relation = [...tables, ...views].find( + ({ id }) => id === return_type_relation_id + ) + if (relation) { + return `{ ${columnsByTableId[relation.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - column.format, - types, - schemas - )} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType( + column.format, + types, + schemas + )} ${column.is_nullable ? '| null' : ''}` + )} }` - } + } - // Case 3: returns base/composite/enum type. - const type = types.find(({ id }) => id === return_type_id) - if (type) { - return pgTypeToTsType(type.name, types, schemas) - } + // Case 3: returns base/composite/enum type. + const type = types.find(({ id }) => id === return_type_id) + if (type) { + return pgTypeToTsType(type.name, types, schemas) + } - return 'unknown' - })()})${is_set_returning_function ? '[]' : ''} + return 'unknown' + })()})${is_set_returning_function ? '[]' : ''} }` - ) - // We only sorted by name on schemaFunctions - here we sort by arg names, arg types, and return type. - .sort() - .join('|')}` - ) - })()} + ) + // We only sorted by name on schemaFunctions - here we sort by arg names, arg types, and return type. + .sort() + .join('|')}` + ) + })()} } Enums: { - ${ - schemaEnums.length === 0 - ? '[_ in never]: never' - : schemaEnums.map( - (enum_) => - `${JSON.stringify(enum_.name)}: ${enum_.enums - .map((variant) => JSON.stringify(variant)) - .join('|')}` - ) - } + ${schemaEnums.length === 0 + ? '[_ in never]: never' + : schemaEnums.map( + (enum_) => + `${JSON.stringify(enum_.name)}: ${enum_.enums + .map((variant) => JSON.stringify(variant)) + .join('|')}` + ) + } } CompositeTypes: { - ${ - schemaCompositeTypes.length === 0 - ? '[_ in never]: never' - : schemaCompositeTypes.map( - ({ name, attributes }) => - `${JSON.stringify(name)}: { + ${schemaCompositeTypes.length === 0 + ? '[_ in never]: never' + : schemaCompositeTypes.map( + ({ name, attributes }) => + `${JSON.stringify(name)}: { ${attributes.map(({ name, type_id }) => { - const type = types.find(({ id }) => id === type_id) - if (type) { - return `${JSON.stringify(name)}: ${pgTypeToTsType( - type.name, - types, - schemas - )}` - } - return `${JSON.stringify(name)}: unknown` - })} + const type = types.find(({ id }) => id === type_id) + if (type) { + return `${JSON.stringify(name)}: ${pgTypeToTsType( + type.name, + types, + schemas + )}` + } + return `${JSON.stringify(name)}: unknown` + })} }` - ) - } + ) + } } }` - })} + })} } export type Tables< From 0cbadf6e0fdd853b844725dd4ecd3b4413b5128b Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Wed, 3 Jan 2024 14:08:44 +0000 Subject: [PATCH 4/4] chore(typegen): format --- src/lib/generators.ts | 44 +- src/server/routes/generators/typescript.ts | 11 +- src/server/templates/typescript.ts | 561 +++++++++++---------- 3 files changed, 317 insertions(+), 299 deletions(-) diff --git a/src/lib/generators.ts b/src/lib/generators.ts index c9a1b79a..47972b19 100644 --- a/src/lib/generators.ts +++ b/src/lib/generators.ts @@ -1,20 +1,35 @@ -import PostgresMeta from "./PostgresMeta.js"; -import { PostgresColumn, PostgresFunction, PostgresMaterializedView, PostgresRelationship, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "./types.js"; -import { PostgresMetaResult } from "./types.js"; +import PostgresMeta from './PostgresMeta.js' +import { + PostgresColumn, + PostgresFunction, + PostgresMaterializedView, + PostgresRelationship, + PostgresSchema, + PostgresTable, + PostgresType, + PostgresView, +} from './types.js' +import { PostgresMetaResult } from './types.js' export type GeneratorMetadata = { - schemas: PostgresSchema[]; - tables: Omit[]; - views: Omit[]; - materializedViews: Omit[]; - columns: PostgresColumn[]; - relationships: PostgresRelationship[]; - functions: PostgresFunction[]; - types: PostgresType[]; - arrayTypes: PostgresType[]; + schemas: PostgresSchema[] + tables: Omit[] + views: Omit[] + materializedViews: Omit[] + columns: PostgresColumn[] + relationships: PostgresRelationship[] + functions: PostgresFunction[] + types: PostgresType[] + arrayTypes: PostgresType[] } -export async function getGeneratorMetadata(pgMeta: PostgresMeta, filters: { includedSchemas?: string[]; excludedSchemas?: string[] } = { includedSchemas: [], excludedSchemas: [] }): Promise> { +export async function getGeneratorMetadata( + pgMeta: PostgresMeta, + filters: { includedSchemas?: string[]; excludedSchemas?: string[] } = { + includedSchemas: [], + excludedSchemas: [], + } +): Promise> { const includedSchemas = filters.includedSchemas ?? [] const excludedSchemas = filters.excludedSchemas ?? [] @@ -90,8 +105,7 @@ export async function getGeneratorMetadata(pgMeta: PostgresMeta, filters: { incl schemas: schemas.filter( ({ name }) => !excludedSchemas.includes(name) && - (includedSchemas.length === 0 || includedSchemas.includes(name) - ) + (includedSchemas.length === 0 || includedSchemas.includes(name)) ), tables, views, diff --git a/src/server/routes/generators/typescript.ts b/src/server/routes/generators/typescript.ts index e67a3971..b7a75248 100644 --- a/src/server/routes/generators/typescript.ts +++ b/src/server/routes/generators/typescript.ts @@ -22,13 +22,10 @@ export default async (fastify: FastifyInstance) => { const detectOneToOneRelationships = request.query.detect_one_to_one_relationships === 'true' const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) - const { data: generatorMeta, error: generatorMetaError } = await getGeneratorMetadata( - pgMeta, - { - includedSchemas, - excludedSchemas - }, - ) + const { data: generatorMeta, error: generatorMetaError } = await getGeneratorMetadata(pgMeta, { + includedSchemas, + excludedSchemas, + }) if (generatorMetaError) { request.log.error({ error: generatorMetaError, request: extractRequestForLogging(request) }) reply.code(500) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index aa303425..cdcc52d5 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -38,364 +38,371 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un export interface Database { ${schemas - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .map((schema) => { - const schemaTables = tables - .filter((table) => table.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaViews = [...views, ...materializedViews] - .filter((view) => view.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaFunctions = functions - .filter((func) => { - if (func.schema !== schema.name) { - return false - } + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + .map((schema) => { + const schemaTables = tables + .filter((table) => table.schema === schema.name) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaViews = [...views, ...materializedViews] + .filter((view) => view.schema === schema.name) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaFunctions = functions + .filter((func) => { + if (func.schema !== schema.name) { + return false + } - // Either: - // 1. All input args are be named, or - // 2. There is only one input arg which is unnamed - const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) + // Either: + // 1. All input args are be named, or + // 2. There is only one input arg which is unnamed + const inArgs = func.args.filter(({ mode }) => ['in', 'inout', 'variadic'].includes(mode)) - if (!inArgs.some(({ name }) => name === '')) { - return true - } + if (!inArgs.some(({ name }) => name === '')) { + return true + } - if (inArgs.length === 1) { - return true - } + if (inArgs.length === 1) { + return true + } - return false - }) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaEnums = types - .filter((type) => type.schema === schema.name && type.enums.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - const schemaCompositeTypes = types - .filter((type) => type.schema === schema.name && type.attributes.length > 0) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - return `${JSON.stringify(schema.name)}: { + return false + }) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaEnums = types + .filter((type) => type.schema === schema.name && type.enums.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + const schemaCompositeTypes = types + .filter((type) => type.schema === schema.name && type.attributes.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + return `${JSON.stringify(schema.name)}: { Tables: { - ${schemaTables.length === 0 - ? '[_ in never]: never' - : schemaTables.map( - (table) => `${JSON.stringify(table.name)}: { + ${ + schemaTables.length === 0 + ? '[_ in never]: never' + : schemaTables.map( + (table) => `${JSON.stringify(table.name)}: { Row: { ${[ - ...columnsByTableId[table.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - column.format, - types, - schemas - )} ${column.is_nullable ? '| null' : ''}` - ), - ...schemaFunctions - .filter((fn) => fn.argument_types === table.name) - .map((fn) => { - const type = types.find(({ id }) => id === fn.return_type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(type.name, types, schemas) - } - return `${JSON.stringify(fn.name)}: ${tsType} | null` - }), - ]} + ...columnsByTableId[table.id].map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType( + column.format, + types, + schemas + )} ${column.is_nullable ? '| null' : ''}` + ), + ...schemaFunctions + .filter((fn) => fn.argument_types === table.name) + .map((fn) => { + const type = types.find(({ id }) => id === fn.return_type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(type.name, types, schemas) + } + return `${JSON.stringify(fn.name)}: ${tsType} | null` + }), + ]} } Insert: { ${columnsByTableId[table.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (column.identity_generation === 'ALWAYS') { - return `${output}?: never` - } + if (column.identity_generation === 'ALWAYS') { + return `${output}?: never` + } - if ( - column.is_nullable || - column.is_identity || - column.default_value !== null - ) { - output += '?:' - } else { - output += ':' - } + if ( + column.is_nullable || + column.is_identity || + column.default_value !== null + ) { + output += '?:' + } else { + output += ':' + } - output += pgTypeToTsType(column.format, types, schemas) + output += pgTypeToTsType(column.format, types, schemas) - if (column.is_nullable) { - output += '| null' - } + if (column.is_nullable) { + output += '| null' + } - return output - })} + return output + })} } Update: { ${columnsByTableId[table.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (column.identity_generation === 'ALWAYS') { - return `${output}?: never` - } + if (column.identity_generation === 'ALWAYS') { + return `${output}?: never` + } - output += `?: ${pgTypeToTsType(column.format, types, schemas)}` + output += `?: ${pgTypeToTsType(column.format, types, schemas)}` - if (column.is_nullable) { - output += '| null' - } + if (column.is_nullable) { + output += '| null' + } - return output - })} + return output + })} } Relationships: [ ${relationships - .filter( - (relationship) => - relationship.schema === table.schema && - relationship.relation === table.name - ) - .sort( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) - ) - .map( - (relationship) => `{ + .filter( + (relationship) => + relationship.schema === table.schema && + relationship.relation === table.name + ) + .sort( + (a, b) => + a.foreign_key_name.localeCompare(b.foreign_key_name) || + a.referenced_relation.localeCompare(b.referenced_relation) + ) + .map( + (relationship) => `{ foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} columns: ${JSON.stringify(relationship.columns)} - ${detectOneToOneRelationships - ? `isOneToOne: ${relationship.is_one_to_one};` - : '' - }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} + ${ + detectOneToOneRelationships + ? `isOneToOne: ${relationship.is_one_to_one};` + : '' + }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} referencedColumns: ${JSON.stringify(relationship.referenced_columns)} }` - )} + )} ] }` - ) - } + ) + } } Views: { - ${schemaViews.length === 0 - ? '[_ in never]: never' - : schemaViews.map( - (view) => `${JSON.stringify(view.name)}: { + ${ + schemaViews.length === 0 + ? '[_ in never]: never' + : schemaViews.map( + (view) => `${JSON.stringify(view.name)}: { Row: { ${columnsByTableId[view.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - column.format, - types, - schemas - )} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType( + column.format, + types, + schemas + )} ${column.is_nullable ? '| null' : ''}` + )} } - ${'is_updatable' in view && view.is_updatable - ? `Insert: { + ${ + 'is_updatable' in view && view.is_updatable + ? `Insert: { ${columnsByTableId[view.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (!column.is_updatable) { - return `${output}?: never` - } + if (!column.is_updatable) { + return `${output}?: never` + } - output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` + output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` - return output - })} + return output + })} } Update: { ${columnsByTableId[view.id].map((column) => { - let output = JSON.stringify(column.name) + let output = JSON.stringify(column.name) - if (!column.is_updatable) { - return `${output}?: never` - } + if (!column.is_updatable) { + return `${output}?: never` + } - output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` + output += `?: ${pgTypeToTsType(column.format, types, schemas)} | null` - return output - })} + return output + })} } ` - : '' - }Relationships: [ + : '' + }Relationships: [ ${relationships - .filter( - (relationship) => - relationship.schema === view.schema && relationship.relation === view.name - ) - .sort(({ foreign_key_name: a }, { foreign_key_name: b }) => - a.localeCompare(b) - ) - .map( - (relationship) => `{ + .filter( + (relationship) => + relationship.schema === view.schema && relationship.relation === view.name + ) + .sort(({ foreign_key_name: a }, { foreign_key_name: b }) => + a.localeCompare(b) + ) + .map( + (relationship) => `{ foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} columns: ${JSON.stringify(relationship.columns)} - ${detectOneToOneRelationships - ? `isOneToOne: ${relationship.is_one_to_one};` - : '' - }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} + ${ + detectOneToOneRelationships + ? `isOneToOne: ${relationship.is_one_to_one};` + : '' + }referencedRelation: ${JSON.stringify(relationship.referenced_relation)} referencedColumns: ${JSON.stringify(relationship.referenced_columns)} }` - )} + )} ] }` - ) - } + ) + } } Functions: { ${(() => { - if (schemaFunctions.length === 0) { - return '[_ in never]: never' - } - - const schemaFunctionsGroupedByName = schemaFunctions.reduce((acc, curr) => { - acc[curr.name] ??= [] - acc[curr.name].push(curr) - return acc - }, {} as Record) - - return Object.entries(schemaFunctionsGroupedByName).map( - ([fnName, fns]) => - `${JSON.stringify(fnName)}: ${fns - .map( - ({ - args, - return_type_id, - return_type_relation_id, - is_set_returning_function, - }) => `{ + if (schemaFunctions.length === 0) { + return '[_ in never]: never' + } + + const schemaFunctionsGroupedByName = schemaFunctions.reduce((acc, curr) => { + acc[curr.name] ??= [] + acc[curr.name].push(curr) + return acc + }, {} as Record) + + return Object.entries(schemaFunctionsGroupedByName).map( + ([fnName, fns]) => + `${JSON.stringify(fnName)}: ${fns + .map( + ({ + args, + return_type_id, + return_type_relation_id, + is_set_returning_function, + }) => `{ Args: ${(() => { - const inArgs = args.filter(({ mode }) => mode === 'in') + const inArgs = args.filter(({ mode }) => mode === 'in') - if (inArgs.length === 0) { - return 'Record' - } + if (inArgs.length === 0) { + return 'Record' + } - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - let type = arrayTypes.find(({ id }) => id === type_id) - if (type) { - // If it's an array type, the name looks like `_int8`. - const elementTypeName = type.name.substring(1) - return { - name, - type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, - has_default, - } - } - type = types.find(({ id }) => id === type_id) - if (type) { - return { - name, - type: pgTypeToTsType(type.name, types, schemas), - has_default, - } - } - return { name, type: 'unknown', has_default } - }) + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + let type = arrayTypes.find(({ id }) => id === type_id) + if (type) { + // If it's an array type, the name looks like `_int8`. + const elementTypeName = type.name.substring(1) + return { + name, + type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, + has_default, + } + } + type = types.find(({ id }) => id === type_id) + if (type) { + return { + name, + type: pgTypeToTsType(type.name, types, schemas), + has_default, + } + } + return { name, type: 'unknown', has_default } + }) - return `{ + return `{ ${argsNameAndType.map( - ({ name, type, has_default }) => - `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}` - )} + ({ name, type, has_default }) => + `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}` + )} }` - })()} + })()} Returns: (${(() => { - // Case 1: `returns table`. - const tableArgs = args.filter(({ mode }) => mode === 'table') - if (tableArgs.length > 0) { - const argsNameAndType = tableArgs.map(({ name, type_id }) => { - let type = arrayTypes.find(({ id }) => id === type_id) - if (type) { - // If it's an array type, the name looks like `_int8`. - const elementTypeName = type.name.substring(1) - return { - name, - type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, - } - } - type = types.find(({ id }) => id === type_id) - if (type) { - return { name, type: pgTypeToTsType(type.name, types, schemas) } - } - return { name, type: 'unknown' } - }) - - return `{ + // Case 1: `returns table`. + const tableArgs = args.filter(({ mode }) => mode === 'table') + if (tableArgs.length > 0) { + const argsNameAndType = tableArgs.map(({ name, type_id }) => { + let type = arrayTypes.find(({ id }) => id === type_id) + if (type) { + // If it's an array type, the name looks like `_int8`. + const elementTypeName = type.name.substring(1) + return { + name, + type: `(${pgTypeToTsType(elementTypeName, types, schemas)})[]`, + } + } + type = types.find(({ id }) => id === type_id) + if (type) { + return { name, type: pgTypeToTsType(type.name, types, schemas) } + } + return { name, type: 'unknown' } + }) + + return `{ ${argsNameAndType.map( - ({ name, type }) => `${JSON.stringify(name)}: ${type}` - )} + ({ name, type }) => `${JSON.stringify(name)}: ${type}` + )} }` - } + } - // Case 2: returns a relation's row type. - const relation = [...tables, ...views].find( - ({ id }) => id === return_type_relation_id - ) - if (relation) { - return `{ + // Case 2: returns a relation's row type. + const relation = [...tables, ...views].find( + ({ id }) => id === return_type_relation_id + ) + if (relation) { + return `{ ${columnsByTableId[relation.id].map( - (column) => - `${JSON.stringify(column.name)}: ${pgTypeToTsType( - column.format, - types, - schemas - )} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType( + column.format, + types, + schemas + )} ${column.is_nullable ? '| null' : ''}` + )} }` - } + } - // Case 3: returns base/composite/enum type. - const type = types.find(({ id }) => id === return_type_id) - if (type) { - return pgTypeToTsType(type.name, types, schemas) - } + // Case 3: returns base/composite/enum type. + const type = types.find(({ id }) => id === return_type_id) + if (type) { + return pgTypeToTsType(type.name, types, schemas) + } - return 'unknown' - })()})${is_set_returning_function ? '[]' : ''} + return 'unknown' + })()})${is_set_returning_function ? '[]' : ''} }` - ) - // We only sorted by name on schemaFunctions - here we sort by arg names, arg types, and return type. - .sort() - .join('|')}` - ) - })()} + ) + // We only sorted by name on schemaFunctions - here we sort by arg names, arg types, and return type. + .sort() + .join('|')}` + ) + })()} } Enums: { - ${schemaEnums.length === 0 - ? '[_ in never]: never' - : schemaEnums.map( - (enum_) => - `${JSON.stringify(enum_.name)}: ${enum_.enums - .map((variant) => JSON.stringify(variant)) - .join('|')}` - ) - } + ${ + schemaEnums.length === 0 + ? '[_ in never]: never' + : schemaEnums.map( + (enum_) => + `${JSON.stringify(enum_.name)}: ${enum_.enums + .map((variant) => JSON.stringify(variant)) + .join('|')}` + ) + } } CompositeTypes: { - ${schemaCompositeTypes.length === 0 - ? '[_ in never]: never' - : schemaCompositeTypes.map( - ({ name, attributes }) => - `${JSON.stringify(name)}: { + ${ + schemaCompositeTypes.length === 0 + ? '[_ in never]: never' + : schemaCompositeTypes.map( + ({ name, attributes }) => + `${JSON.stringify(name)}: { ${attributes.map(({ name, type_id }) => { - const type = types.find(({ id }) => id === type_id) - if (type) { - return `${JSON.stringify(name)}: ${pgTypeToTsType( - type.name, - types, - schemas - )}` - } - return `${JSON.stringify(name)}: unknown` - })} + const type = types.find(({ id }) => id === type_id) + if (type) { + return `${JSON.stringify(name)}: ${pgTypeToTsType( + type.name, + types, + schemas + )}` + } + return `${JSON.stringify(name)}: unknown` + })} }` - ) - } + ) + } } }` - })} + })} } export type Tables<