From 9b6a6af50e705d274e8ede6b5a63ede573429cc7 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Fri, 24 May 2024 11:40:17 +0100 Subject: [PATCH] feat(typegen): Go Type Gen (#687) * feat(typegen): add route for go * feat(typegen): cleanup server.ts and set up local typegen for go * feat(typegen): create go table structs * feat(typegen): create go view/matview structs * feat(typegen): create go composite type structs * feat(typegen): go: handle range types * feat(typegen): go: test * feat(typegen): go: format * feat(typegen): separate go types by operation and add nullable types * feat(typegen): go: format * feat(typegen): go: handle empty tables * chore: update snapshot --------- Co-authored-by: Bobbie Soedirgo --- README.md | 9 +- package.json | 1 + src/server/routes/generators/go.ts | 35 ++ src/server/routes/index.ts | 6 +- src/server/server.ts | 38 ++ src/server/templates/go.ts | 321 +++++++++++++ test/server/typegen.ts | 692 ++++++++++++++++++++++++++++- 7 files changed, 1097 insertions(+), 5 deletions(-) create mode 100644 src/server/routes/generators/go.ts create mode 100644 src/server/templates/go.ts diff --git a/README.md b/README.md index 5db98912..e49bd68d 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,14 @@ To start developing, run `npm run dev`. It will set up the database with Docker If you are fixing a bug, you should create a new test case. To test your changes, add the `-u` flag to `vitest` on the `test:run` script, run `npm run test`, and then review the git diff of the snapshots. Depending on your change, you may see `id` fields being changed - this is expected and you are free to commit it, as long as it passes the CI. Don't forget to remove the `-u` flag when committing. -To make changes to the TypeScript type generation, run `npm run gen:types:typescript` while you have `npm run dev` running. +To make changes to the type generation, run `npm run gen:types:` while you have `npm run dev` running, +where `` is one of: + +- `typescript` +- `go` + To use your own database connection string instead of the provided test database, run: -`PG_META_DB_URL=postgresql://postgres:postgres@localhost:5432/postgres npm run gen:types:typescript` +`PG_META_DB_URL=postgresql://postgres:postgres@localhost:5432/postgres npm run gen:types:` ## Licence diff --git a/package.json b/package.json index cee9928b..365de941 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "build": "tsc -p tsconfig.json && cpy 'src/lib/sql/*.sql' dist/lib/sql", "docs:export": "PG_META_EXPORT_DOCS=true node --loader ts-node/esm src/server/server.ts > openapi.json", "gen:types:typescript": "PG_META_GENERATE_TYPES=typescript node --loader ts-node/esm src/server/server.ts", + "gen:types:go": "PG_META_GENERATE_TYPES=go node --loader ts-node/esm src/server/server.ts", "start": "node dist/server/server.js", "dev": "trap 'npm run db:clean' INT && run-s db:clean db:run && nodemon --exec node --loader ts-node/esm src/server/server.ts | pino-pretty --colorize", "test": "run-s db:clean db:run test:run db:clean", diff --git a/src/server/routes/generators/go.ts b/src/server/routes/generators/go.ts new file mode 100644 index 00000000..aaf1bb07 --- /dev/null +++ b/src/server/routes/generators/go.ts @@ -0,0 +1,35 @@ +import type { FastifyInstance } from 'fastify' +import { PostgresMeta } from '../../../lib/index.js' +import { DEFAULT_POOL_CONFIG } from '../../constants.js' +import { extractRequestForLogging } from '../../utils.js' +import { apply as applyGoTemplate } from '../../templates/go.js' +import { getGeneratorMetadata } from '../../../lib/generators.js' + +export default async (fastify: FastifyInstance) => { + fastify.get<{ + Headers: { pg: string } + Querystring: { + excluded_schemas?: string + included_schemas?: string + } + }>('/', async (request, reply) => { + const connectionString = request.headers.pg + const excludedSchemas = + request.query.excluded_schemas?.split(',').map((schema) => schema.trim()) ?? [] + const includedSchemas = + request.query.included_schemas?.split(',').map((schema) => schema.trim()) ?? [] + + const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + 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 applyGoTemplate(generatorMeta) + }) +} diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index a4282db8..99678515 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -18,7 +18,8 @@ import TablesRoute from './tables.js' import TriggersRoute from './triggers.js' import TypesRoute from './types.js' import ViewsRoute from './views.js' -import TypeGenRoute from './generators/typescript.js' +import TypeScriptTypeGenRoute from './generators/typescript.js' +import GoTypeGenRoute from './generators/go.js' import { PG_CONNECTION, CRYPTO_KEY } from '../constants.js' export default async (fastify: FastifyInstance) => { @@ -62,5 +63,6 @@ export default async (fastify: FastifyInstance) => { fastify.register(TriggersRoute, { prefix: '/triggers' }) fastify.register(TypesRoute, { prefix: '/types' }) fastify.register(ViewsRoute, { prefix: '/views' }) - fastify.register(TypeGenRoute, { prefix: '/generators/typescript' }) + fastify.register(TypeScriptTypeGenRoute, { prefix: '/generators/typescript' }) + fastify.register(GoTypeGenRoute, { prefix: '/generators/go' }) } diff --git a/src/server/server.ts b/src/server/server.ts index a4d715f1..b09644bc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -14,6 +14,8 @@ import { PG_META_PORT, } from './constants.js' import { apply as applyTypescriptTemplate } from './templates/typescript.js' +import { apply as applyGoTemplate } from './templates/go.js' +import { getGeneratorMetadata } from '../lib/generators.js' const logger = pino({ formatters: { @@ -27,6 +29,37 @@ const logger = pino({ const app = buildApp({ logger }) const adminApp = buildAdminApp({ logger }) +async function getTypeOutput(): Promise { + const pgMeta: PostgresMeta = new PostgresMeta({ + ...DEFAULT_POOL_CONFIG, + connectionString: PG_CONNECTION, + }) + const { data: generatorMetadata, error: generatorMetadataError } = await getGeneratorMetadata( + pgMeta, + { + includedSchemas: GENERATE_TYPES_INCLUDED_SCHEMAS, + } + ) + if (generatorMetadataError) { + throw new Error(generatorMetadataError.message) + } + + let output: string | null = null + switch (GENERATE_TYPES) { + case 'typescript': + output = await applyTypescriptTemplate({ + ...generatorMetadata, + detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, + }) + break + case 'go': + output = applyGoTemplate(generatorMetadata) + break + } + + return output +} + if (EXPORT_DOCS) { // TODO: Move to a separate script. await app.ready() @@ -154,3 +187,8 @@ if (EXPORT_DOCS) { adminApp.listen({ port: adminPort, host: PG_META_HOST }) }) } + +app.listen({ port: PG_META_PORT, host: PG_META_HOST }, () => { + const adminPort = PG_META_PORT + 1 + adminApp.listen({ port: adminPort, host: PG_META_HOST }) +}) diff --git a/src/server/templates/go.ts b/src/server/templates/go.ts new file mode 100644 index 00000000..b7495723 --- /dev/null +++ b/src/server/templates/go.ts @@ -0,0 +1,321 @@ +import type { + PostgresColumn, + PostgresMaterializedView, + PostgresSchema, + PostgresTable, + PostgresType, + PostgresView, +} from '../../lib/index.js' +import type { GeneratorMetadata } from '../../lib/generators.js' + +type Operation = 'Select' | 'Insert' | 'Update' + +export const apply = ({ + schemas, + tables, + views, + materializedViews, + columns, + types, +}: GeneratorMetadata): string => { + const columnsByTableId = columns + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + .reduce( + (acc, curr) => { + acc[curr.table_id] ??= [] + acc[curr.table_id].push(curr) + return acc + }, + {} as Record + ) + + const compositeTypes = types.filter((type) => type.attributes.length > 0) + + let output = ` +package database + +import "database/sql" + +${tables + .flatMap((table) => + generateTableStructsForOperations( + schemas.find((schema) => schema.name === table.schema)!, + table, + columnsByTableId[table.id], + types, + ['Select', 'Insert', 'Update'] + ) + ) + .join('\n\n')} + +${views + .flatMap((view) => + generateTableStructsForOperations( + schemas.find((schema) => schema.name === view.schema)!, + view, + columnsByTableId[view.id], + types, + ['Select'] + ) + ) + .join('\n\n')} + +${materializedViews + .flatMap((materializedView) => + generateTableStructsForOperations( + schemas.find((schema) => schema.name === materializedView.schema)!, + materializedView, + columnsByTableId[materializedView.id], + types, + ['Select'] + ) + ) + .join('\n\n')} + +${compositeTypes + .map((compositeType) => + generateCompositeTypeStruct( + schemas.find((schema) => schema.name === compositeType.schema)!, + compositeType, + types + ) + ) + .join('\n\n')} +`.trim() + + return output +} + +/** + * Converts a Postgres name to PascalCase. + * + * @example + * ```ts + * formatForGoTypeName('pokedex') // Pokedex + * formatForGoTypeName('pokemon_center') // PokemonCenter + * formatForGoTypeName('victory-road') // VictoryRoad + * formatForGoTypeName('pokemon league') // PokemonLeague + * ``` + */ +function formatForGoTypeName(name: string): string { + return name + .split(/[^a-zA-Z0-9]/) + .map((word) => `${word[0].toUpperCase()}${word.slice(1)}`) + .join('') +} + +function generateTableStruct( + schema: PostgresSchema, + table: PostgresTable | PostgresView | PostgresMaterializedView, + columns: PostgresColumn[] | undefined, + types: PostgresType[], + operation: Operation +): string { + // Storing columns as a tuple of [formattedName, type, name] rather than creating the string + // representation of the line allows us to pre-format the entries. Go formats + // struct fields to be aligned, e.g.: + // ```go + // type Pokemon struct { + // id int `json:"id"` + // name string `json:"name"` + // } + const columnEntries: [string, string, string][] = + columns?.map((column) => { + let nullable: boolean + if (operation === 'Insert') { + nullable = + column.is_nullable || column.is_identity || column.is_generated || !!column.default_value + } else if (operation === 'Update') { + nullable = true + } else { + nullable = column.is_nullable + } + return [ + formatForGoTypeName(column.name), + pgTypeToGoType(column.format, nullable, types), + column.name, + ] + }) ?? [] + + const [maxFormattedNameLength, maxTypeLength] = columnEntries.reduce( + ([maxFormattedName, maxType], [formattedName, type]) => { + return [Math.max(maxFormattedName, formattedName.length), Math.max(maxType, type.length)] + }, + [0, 0] + ) + + // Pad the formatted name and type to align the struct fields, then join + // create the final string representation of the struct fields. + const formattedColumnEntries = columnEntries.map(([formattedName, type, name]) => { + return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd( + maxTypeLength + )} \`json:"${name}"\`` + }) + + return ` +type ${formatForGoTypeName(schema.name)}${formatForGoTypeName(table.name)}${operation} struct { +${formattedColumnEntries.join('\n')} +} +`.trim() +} + +function generateTableStructsForOperations( + schema: PostgresSchema, + table: PostgresTable | PostgresView | PostgresMaterializedView, + columns: PostgresColumn[] | undefined, + types: PostgresType[], + operations: Operation[] +): string[] { + return operations.map((operation) => + generateTableStruct(schema, table, columns, types, operation) + ) +} + +function generateCompositeTypeStruct( + schema: PostgresSchema, + type: PostgresType, + types: PostgresType[] +): string { + // Use the type_id of the attributes to find the types of the attributes + const typeWithRetrievedAttributes = { + ...type, + attributes: type.attributes.map((attribute) => { + const type = types.find((type) => type.id === attribute.type_id) + return { + ...attribute, + type, + } + }), + } + const attributeEntries: [string, string, string][] = typeWithRetrievedAttributes.attributes.map( + (attribute) => [ + formatForGoTypeName(attribute.name), + pgTypeToGoType(attribute.type!.format, false), + attribute.name, + ] + ) + + const [maxFormattedNameLength, maxTypeLength] = attributeEntries.reduce( + ([maxFormattedName, maxType], [formattedName, type]) => { + return [Math.max(maxFormattedName, formattedName.length), Math.max(maxType, type.length)] + }, + [0, 0] + ) + + // Pad the formatted name and type to align the struct fields, then join + // create the final string representation of the struct fields. + const formattedAttributeEntries = attributeEntries.map(([formattedName, type, name]) => { + return ` ${formattedName.padEnd(maxFormattedNameLength)} ${type.padEnd( + maxTypeLength + )} \`json:"${name}"\`` + }) + + return ` +type ${formatForGoTypeName(schema.name)}${formatForGoTypeName(type.name)} struct { +${formattedAttributeEntries.join('\n')} +} +`.trim() +} + +// Note: the type map uses `interface{ } `, not `any`, to remain compatible with +// older versions of Go. +const GO_TYPE_MAP = { + // Bool + bool: 'bool', + + // Numbers + int2: 'int16', + int4: 'int32', + int8: 'int64', + float4: 'float32', + float8: 'float64', + numeric: 'float64', + + // Strings + bytea: '[]byte', + bpchar: 'string', + varchar: 'string', + date: 'string', + text: 'string', + citext: 'string', + time: 'string', + timetz: 'string', + timestamp: 'string', + timestamptz: 'string', + uuid: 'string', + vector: 'string', + + // JSON + json: 'interface{}', + jsonb: 'interface{}', + + // Range + int4range: 'string', + int4multirange: 'string', + int8range: 'string', + int8multirange: 'string', + numrange: 'string', + nummultirange: 'string', + tsrange: 'string', + tsmultirange: 'string', + tstzrange: 'string', + tstzmultirange: 'string', + daterange: 'string', + datemultirange: 'string', + + // Misc + void: 'interface{}', + record: 'map[string]interface{}', +} as const + +type GoType = (typeof GO_TYPE_MAP)[keyof typeof GO_TYPE_MAP] + +const GO_NULLABLE_TYPE_MAP: Record = { + string: 'sql.NullString', + bool: 'sql.NullBool', + int16: 'sql.NullInt32', + int32: 'sql.NullInt32', + int64: 'sql.NullInt64', + float32: 'sql.NullFloat64', + float64: 'sql.NullFloat64', + '[]byte': '[]byte', + 'interface{}': 'interface{}', + 'map[string]interface{}': 'map[string]interface{}', +} + +function pgTypeToGoType(pgType: string, nullable: boolean, types: PostgresType[] = []): string { + let goType: GoType | undefined = undefined + if (pgType in GO_TYPE_MAP) { + goType = GO_TYPE_MAP[pgType as keyof typeof GO_TYPE_MAP] + } + + // Enums + const enumType = types.find((type) => type.name === pgType && type.enums.length > 0) + if (enumType) { + goType = 'string' + } + + if (goType) { + if (nullable) { + return GO_NULLABLE_TYPE_MAP[goType] + } + return goType + } + + // Composite types + const compositeType = types.find((type) => type.name === pgType && type.attributes.length > 0) + if (compositeType) { + // TODO: generate composite types + // return formatForGoTypeName(pgType) + return 'map[string]interface{}' + } + + // Arrays + if (pgType.startsWith('_')) { + const innerType = pgTypeToGoType(pgType.slice(1), nullable) + return `[]${innerType} ` + } + + // Fallback + return 'interface{}' +} diff --git a/test/server/typegen.ts b/test/server/typegen.ts index eaf786ec..4614d434 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest' import { app } from './utils' -test('typegen', async () => { +test('typegen: typescript', async () => { const { body } = await app.inject({ method: 'GET', path: '/generators/typescript' }) expect(body).toMatchInlineSnapshot(` "export type Json = @@ -1011,3 +1011,693 @@ test('typegen w/ one-to-one relationships', async () => { " `) }) + +test('typegen: typescript w/ one-to-one relationships', async () => { + const { body } = await app.inject({ + method: 'GET', + path: '/generators/typescript', + query: { detect_one_to_one_relationships: 'true' }, + }) + expect(body).toMatchInlineSnapshot(` + "export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + + export type Database = { + public: { + Tables: { + category: { + Row: { + id: number + name: string + } + Insert: { + id?: number + name: string + } + Update: { + id?: number + name?: string + } + Relationships: [] + } + empty: { + Row: {} + Insert: {} + Update: {} + Relationships: [] + } + foreign_table: { + Row: { + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + Insert: { + id: number + name?: string | null + status?: Database["public"]["Enums"]["user_status"] | null + } + Update: { + id?: number + name?: string | null + status?: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } + memes: { + Row: { + category: number | null + created_at: string + id: number + metadata: Json | null + name: string + status: Database["public"]["Enums"]["meme_status"] | null + } + Insert: { + category?: number | null + created_at: string + id?: number + metadata?: Json | null + name: string + status?: Database["public"]["Enums"]["meme_status"] | null + } + Update: { + category?: number | null + created_at?: string + id?: number + metadata?: Json | null + name?: string + status?: Database["public"]["Enums"]["meme_status"] | null + } + Relationships: [ + { + foreignKeyName: "memes_category_fkey" + columns: ["category"] + isOneToOne: false + referencedRelation: "category" + referencedColumns: ["id"] + }, + ] + } + table_with_other_tables_row_type: { + Row: { + col1: Database["public"]["Tables"]["user_details"]["Row"] | null + col2: Database["public"]["Views"]["a_view"]["Row"] | null + } + Insert: { + col1?: Database["public"]["Tables"]["user_details"]["Row"] | null + col2?: Database["public"]["Views"]["a_view"]["Row"] | null + } + Update: { + col1?: Database["public"]["Tables"]["user_details"]["Row"] | null + col2?: Database["public"]["Views"]["a_view"]["Row"] | null + } + Relationships: [] + } + todos: { + Row: { + details: string | null + id: number + "user-id": number + blurb: string | null + blurb_varchar: string | null + details_is_long: boolean | null + details_length: number | null + details_words: string[] | null + } + Insert: { + details?: string | null + id?: number + "user-id": number + } + Update: { + details?: string | null + id?: number + "user-id"?: number + } + Relationships: [ + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "a_view" + referencedColumns: ["id"] + }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "users_view" + referencedColumns: ["id"] + }, + ] + } + user_details: { + Row: { + details: string | null + user_id: number + } + Insert: { + details?: string | null + user_id: number + } + Update: { + details?: string | null + user_id?: number + } + Relationships: [ + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "a_view" + referencedColumns: ["id"] + }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "user_details_user_id_fkey" + columns: ["user_id"] + isOneToOne: true + referencedRelation: "users_view" + referencedColumns: ["id"] + }, + ] + } + users: { + Row: { + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + Insert: { + id?: number + name?: string | null + status?: Database["public"]["Enums"]["user_status"] | null + } + Update: { + id?: number + name?: string | null + status?: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } + users_audit: { + Row: { + created_at: string | null + id: number + previous_value: Json | null + user_id: number | null + } + Insert: { + created_at?: string | null + id?: number + previous_value?: Json | null + user_id?: number | null + } + Update: { + created_at?: string | null + id?: number + previous_value?: Json | null + user_id?: number | null + } + Relationships: [] + } + } + Views: { + a_view: { + Row: { + id: number | null + } + Insert: { + id?: number | null + } + Update: { + id?: number | null + } + Relationships: [] + } + todos_matview: { + Row: { + details: string | null + id: number | null + "user-id": number | null + } + Relationships: [ + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "a_view" + referencedColumns: ["id"] + }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "users_view" + referencedColumns: ["id"] + }, + ] + } + todos_view: { + Row: { + details: string | null + id: number | null + "user-id": number | null + } + Insert: { + details?: string | null + id?: number | null + "user-id"?: number | null + } + Update: { + details?: string | null + id?: number | null + "user-id"?: number | null + } + Relationships: [ + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "a_view" + referencedColumns: ["id"] + }, + { + foreignKeyName: "todos_user-id_fkey" + columns: ["user-id"] + isOneToOne: false + referencedRelation: "users_view" + referencedColumns: ["id"] + }, + ] + } + users_view: { + Row: { + id: number | null + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + Insert: { + id?: number | null + name?: string | null + status?: Database["public"]["Enums"]["user_status"] | null + } + Update: { + id?: number | null + name?: string | null + status?: Database["public"]["Enums"]["user_status"] | null + } + Relationships: [] + } + } + Functions: { + blurb: { + Args: { + "": unknown + } + Returns: string + } + blurb_varchar: { + Args: { + "": unknown + } + Returns: string + } + details_is_long: { + Args: { + "": unknown + } + Returns: boolean + } + details_length: { + Args: { + "": unknown + } + Returns: number + } + details_words: { + Args: { + "": unknown + } + Returns: string[] + } + function_returning_row: { + Args: Record + Returns: { + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + } + } + function_returning_set_of_rows: { + Args: Record + Returns: { + id: number + name: string | null + status: Database["public"]["Enums"]["user_status"] | null + }[] + } + function_returning_table: { + Args: Record + Returns: { + id: number + name: string + }[] + } + polymorphic_function: + | { + Args: { + "": boolean + } + Returns: undefined + } + | { + Args: { + "": string + } + Returns: undefined + } + postgres_fdw_disconnect: { + Args: { + "": string + } + Returns: boolean + } + postgres_fdw_disconnect_all: { + Args: Record + Returns: boolean + } + postgres_fdw_get_connections: { + Args: Record + Returns: Record[] + } + postgres_fdw_handler: { + Args: Record + Returns: unknown + } + } + Enums: { + meme_status: "new" | "old" | "retired" + user_status: "ACTIVE" | "INACTIVE" + } + CompositeTypes: { + composite_type_with_array_attribute: { + my_text_array: string[] | null + } + } + } + } + + type PublicSchema = Database[Extract] + + export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) + : never = never, + > = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & + PublicSchema["Views"]) + ? (PublicSchema["Tables"] & + PublicSchema["Views"])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + + export type TablesInsert< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, + > = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + + export type TablesUpdate< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, + > = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + + export type Enums< + PublicEnumNameOrOptions extends + | keyof PublicSchema["Enums"] + | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + : never = never, + > = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] + ? PublicSchema["Enums"][PublicEnumNameOrOptions] + : never + " + `) +}) + +test('typegen: go', async () => { + const { body } = await app.inject({ method: 'GET', path: '/generators/go' }) + expect(body).toMatchInlineSnapshot(` + "package database + +import "database/sql" + +type PublicUsersSelect struct { + Id int64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicUsersInsert struct { + Id sql.NullInt64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicUsersUpdate struct { + Id sql.NullInt64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicTodosSelect struct { + Details sql.NullString \`json:"details"\` + Id int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` +} + +type PublicTodosInsert struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId int64 \`json:"user-id"\` +} + +type PublicTodosUpdate struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId sql.NullInt64 \`json:"user-id"\` +} + +type PublicUsersAuditSelect struct { + CreatedAt sql.NullString \`json:"created_at"\` + Id int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId sql.NullInt64 \`json:"user_id"\` +} + +type PublicUsersAuditInsert struct { + CreatedAt sql.NullString \`json:"created_at"\` + Id sql.NullInt64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId sql.NullInt64 \`json:"user_id"\` +} + +type PublicUsersAuditUpdate struct { + CreatedAt sql.NullString \`json:"created_at"\` + Id sql.NullInt64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId sql.NullInt64 \`json:"user_id"\` +} + +type PublicUserDetailsSelect struct { + Details sql.NullString \`json:"details"\` + UserId int64 \`json:"user_id"\` +} + +type PublicUserDetailsInsert struct { + Details sql.NullString \`json:"details"\` + UserId int64 \`json:"user_id"\` +} + +type PublicUserDetailsUpdate struct { + Details sql.NullString \`json:"details"\` + UserId sql.NullInt64 \`json:"user_id"\` +} + +type PublicEmptySelect struct { + +} + +type PublicEmptyInsert struct { + +} + +type PublicEmptyUpdate struct { + +} + +type PublicTableWithOtherTablesRowTypeSelect struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` +} + +type PublicTableWithOtherTablesRowTypeInsert struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` +} + +type PublicTableWithOtherTablesRowTypeUpdate struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` +} + +type PublicCategorySelect struct { + Id int32 \`json:"id"\` + Name string \`json:"name"\` +} + +type PublicCategoryInsert struct { + Id sql.NullInt32 \`json:"id"\` + Name string \`json:"name"\` +} + +type PublicCategoryUpdate struct { + Id sql.NullInt32 \`json:"id"\` + Name sql.NullString \`json:"name"\` +} + +type PublicMemesSelect struct { + Category sql.NullInt32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicMemesInsert struct { + Category sql.NullInt32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id sql.NullInt32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicMemesUpdate struct { + Category sql.NullInt32 \`json:"category"\` + CreatedAt sql.NullString \`json:"created_at"\` + Id sql.NullInt32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicTodosViewSelect struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId sql.NullInt64 \`json:"user-id"\` +} + +type PublicUsersViewSelect struct { + Id sql.NullInt64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` +} + +type PublicAViewSelect struct { + Id sql.NullInt64 \`json:"id"\` +} + +type PublicTodosMatviewSelect struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId sql.NullInt64 \`json:"user-id"\` +} + +type PublicCompositeTypeWithArrayAttribute struct { + MyTextArray interface{} \`json:"my_text_array"\` +}" + `) +})