From 57cae247410522a56def0551026893a8ecaecb1d Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Thu, 21 Mar 2024 10:07:53 +0000 Subject: [PATCH 1/2] fix(typegen): deprecate type in favour of --- src/server/templates/typescript.ts | 547 +++++++++++++++-------------- test/server/typegen.ts | 42 +-- 2 files changed, 295 insertions(+), 294 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 563a2ce4..2fd1b54b 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -31,357 +31,368 @@ export const apply = async ({ .forEach((c) => columnsByTableId[c.table_id].push(c)) let output = ` -export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] +/** @deprecated Use \`unknown\` instead. \`Json\` will be removed in a future release. */ +export type Json = unknown export type 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, - tables, - views, - })} ${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, tables, views }) - } - return `${JSON.stringify(fn.name)}: ${tsType} | null` - }), - ]} + ...columnsByTableId[table.id].map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} ${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, tables, views }) + } + 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, tables, views }) + output += pgTypeToTsType(column.format, { types, schemas, tables, views }) - 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, tables, views })}` + output += `?: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })}` - 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, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} ${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, tables, views })} | null` + output += `?: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} | 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, tables, views })} | null` + output += `?: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} | 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 }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) - } - return { name, type: tsType, has_default } - }) + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) + } + return { name, type: tsType, 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 }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) - } - return { name, type: tsType } - }) - - return `{ + // Case 1: `returns table`. + const tableArgs = args.filter(({ mode }) => mode === 'table') + if (tableArgs.length > 0) { + const argsNameAndType = tableArgs.map(({ name, type_id }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) + } + return { name, type: tsType } + }) + + 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, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} ${column.is_nullable ? '| null' : ''}` + )} }` - } + } - // Case 3: returns base/array/composite/enum type. - const type = types.find(({ id }) => id === return_type_id) - if (type) { - return pgTypeToTsType(type.name, { types, schemas, tables, views }) - } + // Case 3: returns base/array/composite/enum type. + const type = types.find(({ id }) => id === return_type_id) + if (type) { + return pgTypeToTsType(type.name, { types, schemas, tables, views }) + } - 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) - let tsType = 'unknown' - if (type) { - tsType = `${pgTypeToTsType(type.name, { types, schemas, tables, views })} | null` - } - return `${JSON.stringify(name)}: ${tsType}` - })} + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = `${pgTypeToTsType(type.name, { + types, + schemas, + tables, + views, + })} | null` + } + return `${JSON.stringify(name)}: ${tsType}` + })} }` - ) - } + ) + } } }` - })} + })} } type PublicSchema = Database[Extract] @@ -509,7 +520,7 @@ const pgTypeToTsType = ( ) { return 'string' } else if (['json', 'jsonb'].includes(pgType)) { - return 'Json' + return 'unknown' } else if (pgType === 'void') { return 'undefined' } else if (pgType === 'record') { diff --git a/test/server/typegen.ts b/test/server/typegen.ts index ddf31801..2db304d6 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -4,13 +4,8 @@ import { app } from './utils' test('typegen', async () => { const { body } = await app.inject({ method: 'GET', path: '/generators/typescript' }) expect(body).toMatchInlineSnapshot(` - "export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json | undefined } - | Json[] + "/** @deprecated Use \`unknown\` instead. \`Json\` will be removed in a future release. */ + export type Json = unknown export type Database = { public: { @@ -41,7 +36,7 @@ test('typegen', async () => { category: number | null created_at: string id: number - metadata: Json | null + metadata: unknown | null name: string status: Database["public"]["Enums"]["meme_status"] | null } @@ -49,7 +44,7 @@ test('typegen', async () => { category?: number | null created_at: string id?: number - metadata?: Json | null + metadata?: unknown | null name: string status?: Database["public"]["Enums"]["meme_status"] | null } @@ -57,7 +52,7 @@ test('typegen', async () => { category?: number | null created_at?: string id?: number - metadata?: Json | null + metadata?: unknown | null name?: string status?: Database["public"]["Enums"]["meme_status"] | null } @@ -183,19 +178,19 @@ test('typegen', async () => { Row: { created_at: string | null id: number - previous_value: Json | null + previous_value: unknown | null user_id: number | null } Insert: { created_at?: string | null id?: number - previous_value?: Json | null + previous_value?: unknown | null user_id?: number | null } Update: { created_at?: string | null id?: number - previous_value?: Json | null + previous_value?: unknown | null user_id?: number | null } Relationships: [] @@ -487,13 +482,8 @@ test('typegen w/ one-to-one relationships', async () => { query: { detect_one_to_one_relationships: 'true' }, }) expect(body).toMatchInlineSnapshot(` - "export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json | undefined } - | Json[] + "/** @deprecated Use \`unknown\` instead. \`Json\` will be removed in a future release. */ + export type Json = unknown export type Database = { public: { @@ -524,7 +514,7 @@ test('typegen w/ one-to-one relationships', async () => { category: number | null created_at: string id: number - metadata: Json | null + metadata: unknown | null name: string status: Database["public"]["Enums"]["meme_status"] | null } @@ -532,7 +522,7 @@ test('typegen w/ one-to-one relationships', async () => { category?: number | null created_at: string id?: number - metadata?: Json | null + metadata?: unknown | null name: string status?: Database["public"]["Enums"]["meme_status"] | null } @@ -540,7 +530,7 @@ test('typegen w/ one-to-one relationships', async () => { category?: number | null created_at?: string id?: number - metadata?: Json | null + metadata?: unknown | null name?: string status?: Database["public"]["Enums"]["meme_status"] | null } @@ -673,19 +663,19 @@ test('typegen w/ one-to-one relationships', async () => { Row: { created_at: string | null id: number - previous_value: Json | null + previous_value: unknown | null user_id: number | null } Insert: { created_at?: string | null id?: number - previous_value?: Json | null + previous_value?: unknown | null user_id?: number | null } Update: { created_at?: string | null id?: number - previous_value?: Json | null + previous_value?: unknown | null user_id?: number | null } Relationships: [] From 35488c6e7daa39c1bda3445e330c5bc047739454 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Thu, 21 Mar 2024 10:10:09 +0000 Subject: [PATCH 2/2] fix(typegen): format --- src/server/templates/typescript.ts | 562 +++++++++++++++-------------- 1 file changed, 286 insertions(+), 276 deletions(-) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 2fd1b54b..1d5fa925 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -36,363 +36,373 @@ export type Json = unknown export type 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, - tables, - views, - })} ${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, tables, views }) - } - return `${JSON.stringify(fn.name)}: ${tsType} | null` - }), - ]} + ...columnsByTableId[table.id].map( + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} ${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, tables, views }) + } + 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, tables, views }) + output += pgTypeToTsType(column.format, { types, schemas, tables, views }) - 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, - tables, - views, - })}` + output += `?: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })}` - 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, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} ${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, - tables, - views, - })} | null` + output += `?: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} | 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, - tables, - views, - })} | null` + output += `?: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} | 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 }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) - } - return { name, type: tsType, has_default } - }) + const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) + } + return { name, type: tsType, 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 }) => { - const type = types.find(({ id }) => id === type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) - } - return { name, type: tsType } - }) - - return `{ + // Case 1: `returns table`. + const tableArgs = args.filter(({ mode }) => mode === 'table') + if (tableArgs.length > 0) { + const argsNameAndType = tableArgs.map(({ name, type_id }) => { + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = pgTypeToTsType(type.name, { types, schemas, tables, views }) + } + return { name, type: tsType } + }) + + 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, - tables, - views, - })} ${column.is_nullable ? '| null' : ''}` - )} + (column) => + `${JSON.stringify(column.name)}: ${pgTypeToTsType(column.format, { + types, + schemas, + tables, + views, + })} ${column.is_nullable ? '| null' : ''}` + )} }` - } + } - // Case 3: returns base/array/composite/enum type. - const type = types.find(({ id }) => id === return_type_id) - if (type) { - return pgTypeToTsType(type.name, { types, schemas, tables, views }) - } + // Case 3: returns base/array/composite/enum type. + const type = types.find(({ id }) => id === return_type_id) + if (type) { + return pgTypeToTsType(type.name, { types, schemas, tables, views }) + } - 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) - let tsType = 'unknown' - if (type) { - tsType = `${pgTypeToTsType(type.name, { - types, - schemas, - tables, - views, - })} | null` - } - return `${JSON.stringify(name)}: ${tsType}` - })} + const type = types.find(({ id }) => id === type_id) + let tsType = 'unknown' + if (type) { + tsType = `${pgTypeToTsType(type.name, { + types, + schemas, + tables, + views, + })} | null` + } + return `${JSON.stringify(name)}: ${tsType}` + })} }` - ) - } + ) + } } }` - })} + })} } type PublicSchema = Database[Extract]