diff --git a/package-lock.json b/package-lock.json index 220f2625..0d37b94d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@sentry/node": "^9.12.0", "@sentry/profiling-node": "^9.12.0", "@sinclair/typebox": "^0.31.25", + "@supabase/postgrest-typegen": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/postgrest-typegen@11ff444", "close-with-grace": "^2.1.0", "crypto-js": "^4.0.0", "fastify": "^4.24.3", @@ -63,6 +64,21 @@ "node": ">=6.0.0" } }, + "node_modules/@ark/schema": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz", + "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", + "license": "MIT", + "dependencies": { + "@ark/util": "0.56.0" + } + }, + "node_modules/@ark/util": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz", + "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==", + "license": "MIT" + }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -1875,6 +1891,20 @@ "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", "license": "MIT" }, + "node_modules/@supabase/postgrest-typegen": { + "version": "1.0.0-alpha.1", + "resolved": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/postgrest-typegen@11ff444", + "integrity": "sha512-GaWcxqVKJVBrcNYWZLXjwcd4jMzJwLc3SpdwfinwiSFMtWqgpHwmVv/igIH7jCm1w2bTobMGUMmBx+MDDpzUag==", + "license": "MIT", + "dependencies": { + "arktype": "2.2.1", + "pg-format": "1.0.4", + "prettier": "3.5.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -2271,6 +2301,26 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/arkregex": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.6.tgz", + "integrity": "sha512-9mvuMKQuibfWhBrsNYhsKhNb6k9oEHoAJ/FvDiqe8h+E9Siwe0/cro1WVOGgpajXQ9ZHd24yCOf2k35Q/QqUQw==", + "license": "MIT", + "dependencies": { + "@ark/util": "0.56.0" + } + }, + "node_modules/arktype": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.2.1.tgz", + "integrity": "sha512-CWPJxNoSxrS+NYGB3ufwc/blFonESEW5vBQyYPVS0rf4STu8VWoAWfKJSl5vVVm56h4yxpwbODeYwy6XFKvojA==", + "license": "MIT", + "dependencies": { + "@ark/schema": "0.56.0", + "@ark/util": "0.56.0", + "arkregex": "0.0.6" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", diff --git a/package.json b/package.json index ed9b8ae2..31f6a4bf 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@sentry/node": "^9.12.0", "@sentry/profiling-node": "^9.12.0", "@sinclair/typebox": "^0.31.25", + "@supabase/postgrest-typegen": "https://pkg.pr.new/supabase/pg-toolbelt/@supabase/postgrest-typegen@11ff444", "close-with-grace": "^2.1.0", "crypto-js": "^4.0.0", "fastify": "^4.24.3", diff --git a/src/lib/generators.ts b/src/lib/generators.ts index 6b5f55e5..8030de70 100644 --- a/src/lib/generators.ts +++ b/src/lib/generators.ts @@ -1,29 +1,25 @@ -import PostgresMeta from './PostgresMeta.js' import { - PostgresColumn, - PostgresForeignTable, - PostgresFunction, - PostgresMaterializedView, - PostgresMetaResult, - PostgresRelationship, - PostgresSchema, - PostgresTable, - PostgresType, - PostgresView, -} from './types.js' - -export type GeneratorMetadata = { - schemas: PostgresSchema[] - tables: Omit[] - foreignTables: Omit[] - views: Omit[] - materializedViews: Omit[] - columns: PostgresColumn[] - relationships: PostgresRelationship[] - functions: PostgresFunction[] - types: PostgresType[] -} - + introspect, + sortGeneratorMetadata, + type GeneratorMetadata, + type Queryable, +} from '@supabase/postgrest-typegen' +import PostgresMeta from './PostgresMeta.js' +import { PostgresMetaResult } from './types.js' + +// Re-export so existing consumers can keep importing the type from here. +export type { GeneratorMetadata } + +/** + * Adapter over `@supabase/postgrest-typegen`'s `introspect()`, preserving the + * historical `getGeneratorMetadata` signature and `{ data, error }` contract. + * + * The package is driver-agnostic: it takes a structural `Queryable` whose + * `query()` resolves to `{ rows }` and throws on failure. We wrap `pgMeta.query` + * (which returns `{ data, error }`) into that shape, surface the first query + * error as the result error, and always end the pool — matching the previous + * behavior. + */ export async function getGeneratorMetadata( pgMeta: PostgresMeta, filters: { includedSchemas?: string[]; excludedSchemas?: string[] } = { @@ -31,111 +27,32 @@ export async function getGeneratorMetadata( excludedSchemas: [], } ): Promise> { - const includedSchemas = filters.includedSchemas ?? [] - const excludedSchemas = filters.excludedSchemas ?? [] - - const { data: schemas, error: schemasError } = await pgMeta.schemas.list({ - includeSystemSchemas: false, - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas: excludedSchemas.length > 0 ? excludedSchemas : undefined, - }) - if (schemasError) { - return { data: null, error: schemasError } - } - - const { data: tables, error: tablesError } = await pgMeta.tables.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas: excludedSchemas.length > 0 ? excludedSchemas : undefined, - includeColumns: false, - }) - if (tablesError) { - return { data: null, error: tablesError } - } - - const { data: foreignTables, error: foreignTablesError } = await pgMeta.foreignTables.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas: excludedSchemas.length > 0 ? excludedSchemas : undefined, - includeColumns: false, - }) - if (foreignTablesError) { - return { data: null, error: foreignTablesError } - } - - const { data: views, error: viewsError } = await pgMeta.views.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas: excludedSchemas.length > 0 ? excludedSchemas : undefined, - 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: excludedSchemas.length > 0 ? excludedSchemas : undefined, - 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: excludedSchemas.length > 0 ? excludedSchemas : undefined, - includeSystemSchemas: false, - }) - if (columnsError) { - return { data: null, error: columnsError } - } - - const { data: relationships, error: relationshipsError } = await pgMeta.relationships.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas: excludedSchemas.length > 0 ? excludedSchemas : undefined, - includeSystemSchemas: false, - }) - if (relationshipsError) { - return { data: null, error: relationshipsError } - } - - const { data: functions, error: functionsError } = await pgMeta.functions.list({ - includedSchemas: includedSchemas.length > 0 ? includedSchemas : undefined, - excludedSchemas: excludedSchemas.length > 0 ? excludedSchemas : undefined, - includeSystemSchemas: false, - }) - if (functionsError) { - return { data: null, error: functionsError } - } - - const { data: types, error: typesError } = await pgMeta.types.list({ - includeTableTypes: true, - includeArrayTypes: true, - includeSystemSchemas: true, - }) - if (typesError) { - return { data: null, error: typesError } + const queryable: Queryable = { + query: async (sql: string) => { + const { data, error } = await pgMeta.query(sql) + if (error) { + throw error + } + return { rows: data ?? [] } + }, } - await pgMeta.end() - - return { - data: { - schemas: schemas.filter( - ({ name }) => - !excludedSchemas.includes(name) && - (includedSchemas.length === 0 || includedSchemas.includes(name)) - ), - tables, - foreignTables, - views, - materializedViews, - columns, - relationships, - functions: functions.filter( - ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) - ), - types, - }, - error: null, + try { + // The generators emit objects in metadata order, so apply the package's + // canonical sort pass before returning (and before any generator runs). + const data = sortGeneratorMetadata( + await introspect(queryable, { + includedSchemas: filters.includedSchemas, + excludedSchemas: filters.excludedSchemas, + }) + ) + return { data, error: null } + } catch (error) { + return { + data: null, + error: error as PostgresMetaResult['error'] & { message: string }, + } + } finally { + await pgMeta.end() } } diff --git a/src/server/constants.ts b/src/server/constants.ts index c64b45e6..f4996c3d 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -1,7 +1,7 @@ import crypto from 'crypto' import { PoolConfig } from '../lib/types.js' import { getSecret } from '../lib/secrets.js' -import { AccessControl } from './templates/swift.js' +import type { AccessControl } from '@supabase/postgrest-typegen' import pkg from '#package.json' with { type: 'json' } export const PG_META_HOST = process.env.PG_META_HOST || '0.0.0.0' @@ -51,10 +51,6 @@ export const GENERATE_TYPES_SWIFT_ACCESS_CONTROL = process.env ? (process.env.PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL as AccessControl) : 'internal' -// json/jsonb/text types -export const VALID_UNNAMED_FUNCTION_ARG_TYPES = new Set([114, 3802, 25]) -export const VALID_FUNCTION_ARGS_MODE = new Set(['in', 'inout', 'variadic']) - export const PG_META_MAX_RESULT_SIZE = process.env.PG_META_MAX_RESULT_SIZE_MB ? // Node-postgres get a maximum size in bytes make the conversion from the env variable // from MB to Bytes diff --git a/src/server/routes/generators/go.ts b/src/server/routes/generators/go.ts index fa85fa46..7c27c239 100644 --- a/src/server/routes/generators/go.ts +++ b/src/server/routes/generators/go.ts @@ -1,7 +1,7 @@ import type { FastifyInstance } from 'fastify' import { PostgresMeta } from '../../../lib/index.js' import { createConnectionConfig, extractRequestForLogging } from '../../utils.js' -import { apply as applyGoTemplate } from '../../templates/go.js' +import { generateGo } from '@supabase/postgrest-typegen' import { getGeneratorMetadata } from '../../../lib/generators.js' export default async (fastify: FastifyInstance) => { @@ -29,6 +29,6 @@ export default async (fastify: FastifyInstance) => { return { error: generatorMetaError.message } } - return applyGoTemplate(generatorMeta) + return generateGo(generatorMeta!) }) } diff --git a/src/server/routes/generators/python.ts b/src/server/routes/generators/python.ts index 706d9dd4..9c9010d7 100644 --- a/src/server/routes/generators/python.ts +++ b/src/server/routes/generators/python.ts @@ -1,7 +1,7 @@ import type { FastifyInstance } from 'fastify' import { PostgresMeta } from '../../../lib/index.js' import { createConnectionConfig, extractRequestForLogging } from '../../utils.js' -import { apply as applyPyTemplate } from '../../templates/python.js' +import { generatePython } from '@supabase/postgrest-typegen' import { getGeneratorMetadata } from '../../../lib/generators.js' export default async (fastify: FastifyInstance) => { @@ -28,6 +28,6 @@ export default async (fastify: FastifyInstance) => { return { error: generatorMetaError.message } } - return applyPyTemplate(generatorMeta) + return generatePython(generatorMeta!) }) } diff --git a/src/server/routes/generators/swift.ts b/src/server/routes/generators/swift.ts index e02839fb..34532ddf 100644 --- a/src/server/routes/generators/swift.ts +++ b/src/server/routes/generators/swift.ts @@ -1,7 +1,7 @@ import type { FastifyInstance } from 'fastify' import { PostgresMeta } from '../../../lib/index.js' import { createConnectionConfig, extractRequestForLogging } from '../../utils.js' -import { apply as applySwiftTemplate, AccessControl } from '../../templates/swift.js' +import { type AccessControl, generateSwift } from '@supabase/postgrest-typegen' import { getGeneratorMetadata } from '../../../lib/generators.js' export default async (fastify: FastifyInstance) => { @@ -31,9 +31,6 @@ export default async (fastify: FastifyInstance) => { return { error: generatorMetaError.message } } - return applySwiftTemplate({ - ...generatorMeta, - accessControl, - }) + return generateSwift(generatorMeta!, { accessControl }) }) } diff --git a/src/server/routes/generators/typescript.ts b/src/server/routes/generators/typescript.ts index 259cd141..b2b6f00b 100644 --- a/src/server/routes/generators/typescript.ts +++ b/src/server/routes/generators/typescript.ts @@ -1,7 +1,7 @@ import type { FastifyInstance } from 'fastify' import { PostgresMeta } from '../../../lib/index.js' import { createConnectionConfig, extractRequestForLogging } from '../../utils.js' -import { apply as applyTypescriptTemplate } from '../../templates/typescript.js' +import { generateTypescript } from '@supabase/postgrest-typegen' import { getGeneratorMetadata } from '../../../lib/generators.js' export default async (fastify: FastifyInstance) => { @@ -33,8 +33,7 @@ export default async (fastify: FastifyInstance) => { return { error: generatorMetaError.message } } - return applyTypescriptTemplate({ - ...generatorMeta, + return generateTypescript(generatorMeta!, { detectOneToOneRelationships, postgrestVersion, }) diff --git a/src/server/server.ts b/src/server/server.ts index 68fbb54c..c766026b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,6 +7,7 @@ import { DEFAULT_POOL_CONFIG, EXPORT_DOCS, GENERATE_TYPES, + GENERATE_TYPES_DEFAULT_SCHEMA, GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, GENERATE_TYPES_INCLUDED_SCHEMAS, GENERATE_TYPES_SWIFT_ACCESS_CONTROL, @@ -15,10 +16,13 @@ import { PG_META_PORT, POSTGREST_VERSION, } from './constants.js' -import { apply as applyTypescriptTemplate } from './templates/typescript.js' -import { apply as applyGoTemplate } from './templates/go.js' -import { apply as applySwiftTemplate } from './templates/swift.js' -import { apply as applyPythonTemplate } from './templates/python.js' +import { + generateGo, + generatePython, + generateSwift, + generateTypescript, +} from '@supabase/postgrest-typegen' +import { getGeneratorMetadata } from '../lib/generators.js' const logger = pino({ formatters: { @@ -37,115 +41,31 @@ async function getTypeOutput(): Promise { ...DEFAULT_POOL_CONFIG, connectionString: PG_CONNECTION, }) - const [ - { data: schemas, error: schemasError }, - { data: tables, error: tablesError }, - { data: foreignTables, error: foreignTablesError }, - { data: views, error: viewsError }, - { data: materializedViews, error: materializedViewsError }, - { data: columns, error: columnsError }, - { data: relationships, error: relationshipsError }, - { data: functions, error: functionsError }, - { data: types, error: typesError }, - ] = await Promise.all([ - pgMeta.schemas.list(), - pgMeta.tables.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.foreignTables.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.views.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.materializedViews.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - includeColumns: false, - }), - pgMeta.columns.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - }), - pgMeta.relationships.list(), - pgMeta.functions.list({ - includedSchemas: - GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, - }), - pgMeta.types.list({ - includeTableTypes: true, - includeArrayTypes: true, - includeSystemSchemas: true, - }), - ]) - await pgMeta.end() - - if (schemasError) { - throw new Error(schemasError.message) - } - if (tablesError) { - throw new Error(tablesError.message) - } - if (foreignTablesError) { - throw new Error(foreignTablesError.message) - } - if (viewsError) { - throw new Error(viewsError.message) - } - if (materializedViewsError) { - throw new Error(materializedViewsError.message) - } - if (columnsError) { - throw new Error(columnsError.message) - } - if (relationshipsError) { - throw new Error(relationshipsError.message) - } - if (functionsError) { - throw new Error(functionsError.message) - } - if (typesError) { - throw new Error(typesError.message) - } - - const config = { - schemas: schemas!.filter( - ({ name }) => - GENERATE_TYPES_INCLUDED_SCHEMAS.length === 0 || - GENERATE_TYPES_INCLUDED_SCHEMAS.includes(name) - ), - tables: tables!, - foreignTables: foreignTables!, - views: views!, - materializedViews: materializedViews!, - columns: columns!, - relationships: relationships!, - functions: functions!.filter( - ({ return_type }) => !['trigger', 'event_trigger'].includes(return_type) - ), - types: types!, - detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, - postgrestVersion: POSTGREST_VERSION, + // `getGeneratorMetadata` introspects via @supabase/postgrest-typegen and ends + // the pool. Behavior freeze: the CLI path only supports included schemas. + const { data: generatorMetadata, error } = await getGeneratorMetadata(pgMeta, { + includedSchemas: + GENERATE_TYPES_INCLUDED_SCHEMAS.length > 0 ? GENERATE_TYPES_INCLUDED_SCHEMAS : undefined, + }) + if (error) { + throw new Error(error.message) } switch (GENERATE_TYPES?.toLowerCase()) { case 'typescript': - return await applyTypescriptTemplate(config) + return await generateTypescript(generatorMetadata!, { + detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, + postgrestVersion: POSTGREST_VERSION, + defaultSchema: GENERATE_TYPES_DEFAULT_SCHEMA, + }) case 'swift': - return await applySwiftTemplate({ - ...config, + return generateSwift(generatorMetadata!, { accessControl: GENERATE_TYPES_SWIFT_ACCESS_CONTROL, }) case 'go': - return applyGoTemplate(config) + return generateGo(generatorMetadata!) case 'python': - return applyPythonTemplate(config) + return generatePython(generatorMetadata!) default: throw new Error(`Unsupported language for GENERATE_TYPES: ${GENERATE_TYPES}`) } diff --git a/src/server/templates/go.ts b/src/server/templates/go.ts deleted file mode 100644 index d2cf5b9d..00000000 --- a/src/server/templates/go.ts +++ /dev/null @@ -1,330 +0,0 @@ -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 - -${tables - .filter((table) => schemas.some((schema) => schema.name === table.schema)) - .flatMap((table) => - generateTableStructsForOperations( - schemas.find((schema) => schema.name === table.schema)!, - table, - columnsByTableId[table.id], - types, - ['Select', 'Insert', 'Update'] - ) - ) - .join('\n\n')} - -${views - .filter((view) => schemas.some((schema) => schema.name === view.schema)) - .flatMap((view) => - generateTableStructsForOperations( - schemas.find((schema) => schema.name === view.schema)!, - view, - columnsByTableId[view.id], - types, - ['Select'] - ) - ) - .join('\n\n')} - -${materializedViews - .filter((materializedView) => schemas.some((schema) => schema.name === materializedView.schema)) - .flatMap((materializedView) => - generateTableStructsForOperations( - schemas.find((schema) => schema.name === materializedView.schema)!, - materializedView, - columnsByTableId[materializedView.id], - types, - ['Select'] - ) - ) - .join('\n\n')} - -${compositeTypes - .filter((compositeType) => schemas.some((schema) => schema.name === compositeType.schema)) - .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) => { - if (word) { - return `${word[0].toUpperCase()}${word.slice(1)}` - } else { - return '' - } - }) - .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', - interval: '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: '*string', - bool: '*bool', - int16: '*int16', - int32: '*int32', - int64: '*int64', - float32: '*float32', - float64: '*float64', - '[]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, types) - return `[]${innerType} ` - } - - // Fallback - return 'interface{}' -} diff --git a/src/server/templates/python.ts b/src/server/templates/python.ts deleted file mode 100644 index 0d00f475..00000000 --- a/src/server/templates/python.ts +++ /dev/null @@ -1,416 +0,0 @@ -import type { - PostgresColumn, - PostgresMaterializedView, - PostgresSchema, - PostgresTable, - PostgresType, - PostgresView, -} from '../../lib/index.js' -import type { GeneratorMetadata } from '../../lib/generators.js' - -export const apply = ({ - schemas, - tables, - views, - materializedViews, - columns, - types, -}: GeneratorMetadata): string => { - const ctx = new PythonContext(types, columns, schemas) - // Used for efficient lookup of types by schema name - const schemasNames = new Set(schemas.map((schema) => schema.name)) - const py_tables = tables.flatMap((table) => { - const py_class_and_methods = ctx.tableToClass(table) - return py_class_and_methods - }) - const composite_types = types - // We always include system schemas, so we need to filter out types that are not in the included schemas - .filter((type) => type.attributes.length > 0 && schemasNames.has(type.schema)) - .map((type) => ctx.typeToClass(type)) - const py_views = views.map((view) => ctx.viewToClass(view)) - const py_matviews = materializedViews.map((matview) => ctx.matViewToClass(matview)) - - let output = ` -from __future__ import annotations - -import datetime -import uuid -from typing import ( - Annotated, - Any, - List, - Literal, - NotRequired, - Optional, - TypeAlias, - TypedDict, -) - -from pydantic import BaseModel, Field, Json - -${concatLines(Object.values(ctx.user_enums))} - -${concatLines(py_tables)} - -${concatLines(py_views)} - -${concatLines(py_matviews)} - -${concatLines(composite_types)} - -`.trim() - - return output -} - -interface Serializable { - serialize(): string -} - -class PythonContext { - types: { [k: string]: PostgresType } - user_enums: { [k: string]: PythonEnum } - columns: Record - schemas: { [k: string]: PostgresSchema } - - constructor(types: PostgresType[], columns: PostgresColumn[], schemas: PostgresSchema[]) { - this.schemas = Object.fromEntries(schemas.map((schema) => [schema.name, schema])) - this.types = Object.fromEntries(types.map((type) => [type.name, type])) - this.columns = 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 - ) - this.user_enums = Object.fromEntries( - types.filter((type) => type.enums.length > 0).map((type) => [type.name, new PythonEnum(type)]) - ) - } - - resolveTypeName(name: string): string { - if (name in this.user_enums) { - return this.user_enums[name].name - } - if (name in PY_TYPE_MAP) { - return PY_TYPE_MAP[name] - } - if (name in this.types) { - const type = this.types[name] - const schema = type!.schema - return `${formatForPyClassName(schema)}${formatForPyClassName(type.name)}` - } - return 'Any' - } - - parsePgType(pg_type: string): PythonType { - if (pg_type.startsWith('_')) { - const inner_str = pg_type.slice(1) - const inner = this.parsePgType(inner_str) - return new PythonListType(inner) - } else { - const type_name = this.resolveTypeName(pg_type) - return new PythonSimpleType(type_name) - } - } - - typeToClass(type: PostgresType): PythonBaseModel { - const types = Object.values(this.types) - const attributes = type.attributes.map((attribute) => { - const type = types.find((type) => type.id === attribute.type_id) - return { - ...attribute, - type, - } - }) - const attributeEntries: PythonBaseModelAttr[] = attributes.map((attribute) => { - const type = this.parsePgType(attribute.type!.name) - return new PythonBaseModelAttr(attribute.name, type, false) - }) - - const schema = this.schemas[type.schema] - return new PythonBaseModel(type.name, schema, attributeEntries) - } - - columnsToClassAttrs(table_id: number): PythonBaseModelAttr[] { - const attrs = this.columns[table_id] ?? [] - return attrs.map((col) => { - const type = this.parsePgType(col.format) - return new PythonBaseModelAttr(col.name, type, col.is_nullable) - }) - } - - columnsToDictAttrs(table_id: number, not_required: boolean): PythonTypedDictAttr[] { - const attrs = this.columns[table_id] ?? [] - return attrs.map((col) => { - const type = this.parsePgType(col.format) - return new PythonTypedDictAttr( - col.name, - type, - col.is_nullable, - not_required || col.is_nullable || col.is_identity || col.default_value !== null - ) - }) - } - - tableToClass(table: PostgresTable): [PythonBaseModel, PythonTypedDict, PythonTypedDict] { - const schema = this.schemas[table.schema] - const select = new PythonBaseModel(table.name, schema, this.columnsToClassAttrs(table.id)) - const insert = new PythonTypedDict( - table.name, - 'Insert', - schema, - this.columnsToDictAttrs(table.id, false) - ) - const update = new PythonTypedDict( - table.name, - 'Update', - schema, - this.columnsToDictAttrs(table.id, true) - ) - return [select, insert, update] - } - - viewToClass(view: PostgresView): PythonBaseModel { - const attributes = this.columnsToClassAttrs(view.id) - return new PythonBaseModel(view.name, this.schemas[view.schema], attributes) - } - - matViewToClass(matview: PostgresMaterializedView): PythonBaseModel { - const attributes = this.columnsToClassAttrs(matview.id) - return new PythonBaseModel(matview.name, this.schemas[matview.schema], attributes) - } -} - -class PythonEnum implements Serializable { - name: string - variants: string[] - constructor(type: PostgresType) { - this.name = `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}` - this.variants = type.enums - } - serialize(): string { - const variants = this.variants.map((item) => `"${item}"`).join(', ') - return `${this.name}: TypeAlias = Literal[${variants}]` - } -} - -type PythonType = PythonListType | PythonSimpleType - -class PythonSimpleType implements Serializable { - name: string - constructor(name: string) { - this.name = name - } - serialize(): string { - return this.name - } -} - -class PythonListType implements Serializable { - inner: PythonType - constructor(inner: PythonType) { - this.inner = inner - } - serialize(): string { - return `List[${this.inner.serialize()}]` - } -} - -class PythonBaseModelAttr implements Serializable { - name: string - pg_name: string - py_type: PythonType - nullable: boolean - - constructor(name: string, py_type: PythonType, nullable: boolean) { - this.name = formatForPyAttributeName(name) - this.pg_name = name - this.py_type = py_type - this.nullable = nullable - } - - serialize(): string { - const py_type = this.nullable - ? `Optional[${this.py_type.serialize()}]` - : this.py_type.serialize() - return ` ${this.name}: ${py_type} = Field(alias="${this.pg_name}")` - } -} - -class PythonBaseModel implements Serializable { - name: string - table_name: string - schema: PostgresSchema - class_attributes: PythonBaseModelAttr[] - - constructor(name: string, schema: PostgresSchema, class_attributes: PythonBaseModelAttr[]) { - this.schema = schema - this.class_attributes = class_attributes - this.table_name = name - this.name = `${formatForPyClassName(schema.name)}${formatForPyClassName(name)}` - } - serialize(): string { - const attributes = - this.class_attributes.length > 0 - ? this.class_attributes.map((attr) => attr.serialize()).join('\n') - : ' pass' - return `class ${this.name}(BaseModel):\n${attributes}` - } -} - -class PythonTypedDictAttr implements Serializable { - name: string - pg_name: string - py_type: PythonType - nullable: boolean - not_required: boolean - - constructor(name: string, py_type: PythonType, nullable: boolean, required: boolean) { - this.name = formatForPyAttributeName(name) - this.pg_name = name - this.py_type = py_type - this.nullable = nullable - this.not_required = required - } - - serialize(): string { - const py_type = this.nullable - ? `Optional[${this.py_type.serialize()}]` - : this.py_type.serialize() - const annotation = `Annotated[${py_type}, Field(alias="${this.pg_name}")]` - const rhs = this.not_required ? `NotRequired[${annotation}]` : annotation - return ` ${this.name}: ${rhs}` - } -} - -class PythonTypedDict implements Serializable { - name: string - table_name: string - parent_class: string - schema: PostgresSchema - dict_attributes: PythonTypedDictAttr[] - operation: 'Insert' | 'Update' - - constructor( - name: string, - operation: 'Insert' | 'Update', - schema: PostgresSchema, - dict_attributes: PythonTypedDictAttr[], - parent_class: string = 'BaseModel' - ) { - this.schema = schema - this.dict_attributes = dict_attributes - this.table_name = name - this.name = `${formatForPyClassName(schema.name)}${formatForPyClassName(name)}` - this.parent_class = parent_class - this.operation = operation - } - serialize(): string { - const attributes = - this.dict_attributes.length > 0 - ? this.dict_attributes.map((attr) => attr.serialize()).join('\n') - : ' pass' - return `class ${this.name}${this.operation}(TypedDict):\n${attributes}` - } -} - -function concatLines(items: Serializable[]): string { - return items.map((item) => item.serialize()).join('\n\n') -} - -const PY_TYPE_MAP: Record = { - // Bool - bool: 'bool', - - // Numbers - int2: 'int', - int4: 'int', - int8: 'int', - float4: 'float', - float8: 'float', - numeric: 'float', - - // Strings - bytea: 'bytes', - bpchar: 'str', - varchar: 'str', - string: 'str', - date: 'datetime.date', - text: 'str', - citext: 'str', - time: 'datetime.time', - timetz: 'datetime.time', - timestamp: 'datetime.datetime', - timestamptz: 'datetime.datetime', - uuid: 'uuid.UUID', - vector: 'list[Any]', - interval: 'str', - - // JSON - json: 'Json[Any]', - jsonb: 'Json[Any]', - - // Range types (can be adjusted to more complex types if needed) - int4range: 'str', - int4multirange: 'str', - int8range: 'str', - int8multirange: 'str', - numrange: 'str', - nummultirange: 'str', - tsrange: 'str', - tsmultirange: 'str', - tstzrange: 'str', - tstzmultirange: 'str', - daterange: 'str', - datemultirange: 'str', - - // Miscellaneous types - void: 'None', - record: 'dict[str, Any]', -} as const - -/** - * Converts a Postgres name to PascalCase. - * - * @example - * ```ts - * formatForPyTypeName('pokedex') // Pokedex - * formatForPyTypeName('pokemon_center') // PokemonCenter - * formatForPyTypeName('victory-road') // VictoryRoad - * formatForPyTypeName('pokemon league') // PokemonLeague - * ``` - */ - -function formatForPyClassName(name: string): string { - return name - .split(/[^a-zA-Z0-9]/) - .map((word) => { - if (word) { - return `${word[0].toUpperCase()}${word.slice(1)}` - } else { - return '' - } - }) - .join('') -} -/** - * Converts a Postgres name to snake_case. - * - * @example - * ```ts - * formatForPyTypeName('Pokedex') // pokedex - * formatForPyTypeName('PokemonCenter') // pokemon_enter - * formatForPyTypeName('victory-road') // victory_road - * formatForPyTypeName('pokemon league') // pokemon_league - * ``` - */ -function formatForPyAttributeName(name: string): string { - return name - .split(/[^a-zA-Z0-9]+/) // Split on non-alphanumeric characters (like spaces, dashes, etc.) - .map((word) => word.toLowerCase()) // Convert each word to lowercase - .join('_') // Join with underscores -} diff --git a/src/server/templates/swift.ts b/src/server/templates/swift.ts deleted file mode 100644 index 69aec816..00000000 --- a/src/server/templates/swift.ts +++ /dev/null @@ -1,421 +0,0 @@ -import prettier from 'prettier' -import type { - PostgresColumn, - PostgresFunction, - PostgresMaterializedView, - PostgresSchema, - PostgresTable, - PostgresType, - PostgresView, -} from '../../lib/index.js' -import type { GeneratorMetadata } from '../../lib/generators.js' -import { PostgresForeignTable } from '../../lib/types.js' - -type Operation = 'Select' | 'Insert' | 'Update' -export type AccessControl = 'internal' | 'public' | 'private' | 'package' - -type SwiftGeneratorOptions = { - accessControl: AccessControl -} - -type SwiftEnumCase = { - formattedName: string - rawValue: string -} - -type SwiftEnum = { - formattedEnumName: string - protocolConformances: string[] - cases: SwiftEnumCase[] -} - -type SwiftAttribute = { - formattedAttributeName: string - formattedType: string - rawName: string - isIdentity: boolean -} - -type SwiftStruct = { - formattedStructName: string - protocolConformances: string[] - attributes: SwiftAttribute[] - codingKeysEnum: SwiftEnum | undefined -} - -function formatForSwiftSchemaName(schema: string): string { - return `${formatForSwiftTypeName(schema)}Schema` -} - -function pgEnumToSwiftEnum(pgEnum: PostgresType): SwiftEnum { - return { - formattedEnumName: formatForSwiftTypeName(pgEnum.name), - protocolConformances: ['String', 'Codable', 'Hashable', 'Sendable'], - cases: pgEnum.enums.map((case_) => { - return { formattedName: formatForSwiftPropertyName(case_), rawValue: case_ } - }), - } -} - -function pgTypeToSwiftStruct( - table: PostgresTable | PostgresForeignTable | PostgresView | PostgresMaterializedView, - columns: PostgresColumn[] | undefined, - operation: Operation, - { - types, - views, - tables, - }: { types: PostgresType[]; views: PostgresView[]; tables: PostgresTable[] } -): SwiftStruct { - const columnEntries: SwiftAttribute[] = - 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 { - rawName: column.name, - formattedAttributeName: formatForSwiftPropertyName(column.name), - formattedType: pgTypeToSwiftType(column.format, nullable, { types, views, tables }), - isIdentity: column.is_identity, - } - }) ?? [] - - return { - formattedStructName: `${formatForSwiftTypeName(table.name)}${operation}`, - attributes: columnEntries, - protocolConformances: ['Codable', 'Hashable', 'Sendable'], - codingKeysEnum: generateCodingKeysEnumFromAttributes(columnEntries), - } -} - -function generateCodingKeysEnumFromAttributes(attributes: SwiftAttribute[]): SwiftEnum | undefined { - return attributes.length > 0 - ? { - formattedEnumName: 'CodingKeys', - protocolConformances: ['String', 'CodingKey'], - cases: attributes.map((attribute) => { - return { - formattedName: attribute.formattedAttributeName, - rawValue: attribute.rawName, - } - }), - } - : undefined -} - -function pgCompositeTypeToSwiftStruct( - type: PostgresType, - { - types, - views, - tables, - }: { types: PostgresType[]; views: PostgresView[]; tables: PostgresTable[] } -): SwiftStruct { - const typeWithRetrievedAttributes = { - ...type, - attributes: type.attributes.map((attribute) => { - const type = types.find((type) => type.id === attribute.type_id) - return { - ...attribute, - type, - } - }), - } - - const attributeEntries: SwiftAttribute[] = typeWithRetrievedAttributes.attributes.map( - (attribute) => { - return { - formattedAttributeName: formatForSwiftTypeName(attribute.name), - formattedType: pgTypeToSwiftType(attribute.type!.format, false, { types, views, tables }), - rawName: attribute.name, - isIdentity: false, - } - } - ) - - return { - formattedStructName: formatForSwiftTypeName(type.name), - attributes: attributeEntries, - protocolConformances: ['Codable', 'Hashable', 'Sendable'], - codingKeysEnum: generateCodingKeysEnumFromAttributes(attributeEntries), - } -} - -function generateProtocolConformances(protocols: string[]): string { - return protocols.length === 0 ? '' : `: ${protocols.join(', ')}` -} - -function generateEnum( - enum_: SwiftEnum, - { accessControl, level }: SwiftGeneratorOptions & { level: number } -): string[] { - return [ - `${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(enum_.protocolConformances)} {`, - ...enum_.cases.map( - (case_) => `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"` - ), - `${ident(level)}}`, - ] -} - -function generateStruct( - struct: SwiftStruct, - { accessControl, level }: SwiftGeneratorOptions & { level: number } -): string[] { - const identity = struct.attributes.find((column) => column.isIdentity) - - let protocolConformances = struct.protocolConformances - if (identity) { - protocolConformances.push('Identifiable') - } - - let output = [ - `${ident(level)}${accessControl} struct ${struct.formattedStructName}${generateProtocolConformances(struct.protocolConformances)} {`, - ] - - if (identity && identity.formattedAttributeName !== 'id') { - output.push( - `${ident(level + 1)}${accessControl} var id: ${identity.formattedType} { ${identity.formattedAttributeName} }` - ) - } - - output.push( - ...struct.attributes.map( - (attribute) => - `${ident(level + 1)}${accessControl} let ${attribute.formattedAttributeName}: ${attribute.formattedType}` - ) - ) - - if (struct.codingKeysEnum) { - output.push(...generateEnum(struct.codingKeysEnum, { accessControl, level: level + 1 })) - } - - output.push(`${ident(level)}}`) - - return output -} - -export const apply = async ({ - schemas, - tables, - foreignTables, - views, - materializedViews, - columns, - types, - accessControl, -}: GeneratorMetadata & SwiftGeneratorOptions): Promise => { - const columnsByTableId = Object.fromEntries( - [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []]) - ) - - columns - .filter((c) => c.table_id in columnsByTableId) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .forEach((c) => columnsByTableId[c.table_id].push(c)) - - let output = [ - 'import Foundation', - 'import Supabase', - '', - ...schemas - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - .flatMap((schema) => { - const schemaTables = [...tables, ...foreignTables] - .filter((table) => table.schema === schema.name) - .sort(({ name: a }, { name: b }) => a.localeCompare(b)) - - const schemaViews = [...views, ...materializedViews] - .filter((table) => table.schema === schema.name) - .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 [ - `${accessControl} enum ${formatForSwiftSchemaName(schema.name)} {`, - ...schemaEnums.flatMap((enum_) => - generateEnum(pgEnumToSwiftEnum(enum_), { accessControl, level: 1 }) - ), - ...schemaTables.flatMap((table) => - (['Select', 'Insert', 'Update'] as Operation[]) - .map((operation) => - pgTypeToSwiftStruct(table, columnsByTableId[table.id], operation, { - types, - views, - tables, - }) - ) - .flatMap((struct) => generateStruct(struct, { accessControl, level: 1 })) - ), - ...schemaViews.flatMap((view) => - generateStruct( - pgTypeToSwiftStruct(view, columnsByTableId[view.id], 'Select', { - types, - views, - tables, - }), - { accessControl, level: 1 } - ) - ), - ...schemaCompositeTypes.flatMap((type) => - generateStruct(pgCompositeTypeToSwiftStruct(type, { types, views, tables }), { - accessControl, - level: 1, - }) - ), - '}', - ] - }), - ] - - return output.join('\n') -} - -// TODO: Make this more robust. Currently doesn't handle range types - returns them as string. -const pgTypeToSwiftType = ( - pgType: string, - nullable: boolean, - { - types, - views, - tables, - }: { types: PostgresType[]; views: PostgresView[]; tables: PostgresTable[] } -): string => { - let swiftType: string - - if (pgType === 'bool') { - swiftType = 'Bool' - } else if (pgType === 'int2') { - swiftType = 'Int16' - } else if (pgType === 'int4') { - swiftType = 'Int32' - } else if (pgType === 'int8') { - swiftType = 'Int64' - } else if (pgType === 'float4') { - swiftType = 'Float' - } else if (pgType === 'float8') { - swiftType = 'Double' - } else if (['numeric', 'decimal'].includes(pgType)) { - swiftType = 'Decimal' - } else if (pgType === 'uuid') { - swiftType = 'UUID' - } else if ( - [ - 'bytea', - 'bpchar', - 'varchar', - 'date', - 'text', - 'citext', - 'time', - 'timetz', - 'timestamp', - 'timestamptz', - 'interval', - 'vector', - ].includes(pgType) - ) { - swiftType = 'String' - } else if (['json', 'jsonb'].includes(pgType)) { - swiftType = 'AnyJSON' - } else if (pgType === 'void') { - swiftType = 'Void' - } else if (pgType === 'record') { - swiftType = 'JSONObject' - } else if (pgType.startsWith('_')) { - swiftType = `[${pgTypeToSwiftType(pgType.substring(1), false, { types, views, tables })}]` - } else { - const enumType = types.find((type) => type.name === pgType && type.enums.length > 0) - - const compositeTypes = [...types, ...views, ...tables].find((type) => type.name === pgType) - - if (enumType) { - swiftType = `${formatForSwiftTypeName(enumType.name)}` - } else if (compositeTypes) { - // Append a `Select` to the composite type, as that is how is named in the generated struct. - swiftType = `${formatForSwiftTypeName(compositeTypes.name)}Select` - } else { - swiftType = 'AnyJSON' - } - } - - return `${swiftType}${nullable ? '?' : ''}` -} - -function ident(level: number, options: { width: number } = { width: 2 }): string { - return ' '.repeat(level * options.width) -} - -/** - * Converts a Postgres name to PascalCase. - * - * @example - * ```ts - * formatForSwiftTypeName('pokedex') // Pokedex - * formatForSwiftTypeName('pokemon_center') // PokemonCenter - * formatForSwiftTypeName('victory-road') // VictoryRoad - * formatForSwiftTypeName('pokemon league') // PokemonLeague - * formatForSwiftTypeName('_key_id_context') // _KeyIdContext - * ``` - */ -function formatForSwiftTypeName(name: string): string { - // Preserve the initial underscore if it exists - let prefix = '' - if (name.startsWith('_')) { - prefix = '_' - name = name.slice(1) // Remove the initial underscore for processing - } - - return ( - prefix + - name - .split(/[^a-zA-Z0-9]+/) - .map((word) => { - if (word) { - return `${word[0].toUpperCase()}${word.slice(1)}` - } else { - return '' - } - }) - .join('') - ) -} - -const SWIFT_KEYWORDS = ['in', 'default', 'case'] - -/** - * Converts a Postgres name to pascalCase. - * - * @example - * ```ts - * formatForSwiftTypeName('pokedex') // pokedex - * formatForSwiftTypeName('pokemon_center') // pokemonCenter - * formatForSwiftTypeName('victory-road') // victoryRoad - * formatForSwiftTypeName('pokemon league') // pokemonLeague - * ``` - */ -function formatForSwiftPropertyName(name: string): string { - const propertyName = name - .split(/[^a-zA-Z0-9]/) - .map((word, index) => { - const lowerWord = word.toLowerCase() - return index !== 0 ? lowerWord.charAt(0).toUpperCase() + lowerWord.slice(1) : lowerWord - }) - .join('') - - return SWIFT_KEYWORDS.includes(propertyName) ? `\`${propertyName}\`` : propertyName -} diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts deleted file mode 100644 index 352c4ddc..00000000 --- a/src/server/templates/typescript.ts +++ /dev/null @@ -1,974 +0,0 @@ -import prettier from 'prettier' -import type { GeneratorMetadata } from '../../lib/generators.js' -import type { - PostgresColumn, - PostgresFunction, - PostgresSchema, - PostgresTable, - PostgresType, - PostgresView, -} from '../../lib/index.js' -import { - GENERATE_TYPES_DEFAULT_SCHEMA, - VALID_FUNCTION_ARGS_MODE, - VALID_UNNAMED_FUNCTION_ARG_TYPES, -} from '../constants.js' - -type TsRelationship = Pick< - GeneratorMetadata['relationships'][number], - 'foreign_key_name' | 'columns' | 'is_one_to_one' | 'referenced_relation' | 'referenced_columns' -> - -export const apply = async ({ - schemas, - tables, - foreignTables, - views, - materializedViews, - columns, - relationships, - functions, - types, - detectOneToOneRelationships, - postgrestVersion, -}: GeneratorMetadata & { - detectOneToOneRelationships: boolean - postgrestVersion?: string -}): Promise => { - schemas.sort((a, b) => a.name.localeCompare(b.name)) - relationships.sort( - (a, b) => - a.foreign_key_name.localeCompare(b.foreign_key_name) || - a.referenced_relation.localeCompare(b.referenced_relation) || - JSON.stringify(a.referenced_columns).localeCompare(JSON.stringify(b.referenced_columns)) - ) - const introspectionBySchema = Object.fromEntries<{ - tables: { - table: Pick - relationships: TsRelationship[] - }[] - views: { - view: PostgresView - relationships: TsRelationship[] - }[] - functions: { fn: PostgresFunction; inArgs: PostgresFunction['args'] }[] - enums: PostgresType[] - compositeTypes: PostgresType[] - }>( - schemas.map((s) => [ - s.name, - { tables: [], views: [], functions: [], enums: [], compositeTypes: [] }, - ]) - ) - const columnsByTableId: Record = {} - const tablesNamesByTableId: Record = {} - const relationTypeByIds = new Map() - // group types by id for quicker lookup - const typesById = new Map() - const tablesLike = [...tables, ...foreignTables, ...views, ...materializedViews] - - for (const tableLike of tablesLike) { - columnsByTableId[tableLike.id] = [] - tablesNamesByTableId[tableLike.id] = tableLike.name - } - for (const column of columns) { - if (column.table_id in columnsByTableId) { - columnsByTableId[column.table_id].push(column) - } - } - for (const tableId in columnsByTableId) { - columnsByTableId[tableId].sort((a, b) => a.name.localeCompare(b.name)) - } - - for (const type of types) { - typesById.set(type.id, type) - // Save all the types that are relation types for quicker lookup - if (type.type_relation_id) { - relationTypeByIds.set(type.id, type) - } - if (type.schema in introspectionBySchema) { - if (type.enums.length > 0) { - introspectionBySchema[type.schema].enums.push(type) - } - if (type.attributes.length > 0) { - introspectionBySchema[type.schema].compositeTypes.push(type) - } - } - } - - function getRelationships( - object: { schema: string; name: string }, - relationships: GeneratorMetadata['relationships'] - ): Pick< - GeneratorMetadata['relationships'][number], - 'foreign_key_name' | 'columns' | 'is_one_to_one' | 'referenced_relation' | 'referenced_columns' - >[] { - return relationships.filter( - (relationship) => - relationship.schema === object.schema && - relationship.referenced_schema === object.schema && - relationship.relation === object.name - ) - } - - function generateRelationshiptTsDefinition(relationship: TsRelationship): string { - return `{ - foreignKeyName: ${JSON.stringify(relationship.foreign_key_name)} - columns: ${JSON.stringify(relationship.columns)}${detectOneToOneRelationships ? `\nisOneToOne: ${relationship.is_one_to_one}` : ''} - referencedRelation: ${JSON.stringify(relationship.referenced_relation)} - referencedColumns: ${JSON.stringify(relationship.referenced_columns)} - }` - } - - for (const table of tables) { - if (table.schema in introspectionBySchema) { - introspectionBySchema[table.schema].tables.push({ - table, - relationships: getRelationships(table, relationships), - }) - } - } - for (const table of foreignTables) { - if (table.schema in introspectionBySchema) { - introspectionBySchema[table.schema].tables.push({ - table, - relationships: getRelationships(table, relationships), - }) - } - } - for (const view of views) { - if (view.schema in introspectionBySchema) { - introspectionBySchema[view.schema].views.push({ - view, - relationships: getRelationships(view, relationships), - }) - } - } - for (const materializedView of materializedViews) { - if (materializedView.schema in introspectionBySchema) { - introspectionBySchema[materializedView.schema].views.push({ - view: { - ...materializedView, - is_updatable: false, - }, - relationships: getRelationships(materializedView, relationships), - }) - } - } - // Helper function to get table/view name from relation id - const getTableNameFromRelationId = ( - relationId: number | null, - returnTypeId: number | null - ): string | null => { - if (!relationId) return null - - if (tablesNamesByTableId[relationId]) return tablesNamesByTableId[relationId] - // if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type - const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null - return reltype ? reltype.name : null - } - - for (const func of functions) { - if (func.schema in introspectionBySchema) { - func.args.sort((a, b) => a.name.localeCompare(b.name)) - // Get all input args (in, inout, variadic modes) - const inArgs = func.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode)) - - if ( - // Case 1: Function has no parameters - inArgs.length === 0 || - // Case 2: All input args are named - !inArgs.some(({ name }) => name === '') || - // Case 3: All unnamed args have default values AND are valid types - inArgs.every((arg) => { - if (arg.name === '') { - return arg.has_default && VALID_UNNAMED_FUNCTION_ARG_TYPES.has(arg.type_id) - } - return true - }) || - // Case 4: Single unnamed parameter of valid type (json, jsonb, text) - // Exclude all functions definitions that have only one single argument unnamed argument that isn't - // a json/jsonb/text as it won't be considered by PostgREST - (inArgs.length === 1 && - inArgs[0].name === '' && - (VALID_UNNAMED_FUNCTION_ARG_TYPES.has(inArgs[0].type_id) || - // OR if the function have a single unnamed args which is another table (embeded function) - (relationTypeByIds.get(inArgs[0].type_id) && - getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)) || - // OR if the function takes a table row but doesn't qualify as embedded (for error reporting) - (relationTypeByIds.get(inArgs[0].type_id) && - !getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)))) - ) { - introspectionBySchema[func.schema].functions.push({ fn: func, inArgs }) - } - } - } - for (const schema in introspectionBySchema) { - introspectionBySchema[schema].tables.sort((a, b) => a.table.name.localeCompare(b.table.name)) - introspectionBySchema[schema].views.sort((a, b) => a.view.name.localeCompare(b.view.name)) - introspectionBySchema[schema].functions.sort((a, b) => a.fn.name.localeCompare(b.fn.name)) - introspectionBySchema[schema].enums.sort((a, b) => a.name.localeCompare(b.name)) - introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name.localeCompare(b.name)) - } - - const getFunctionTsReturnType = (fn: PostgresFunction, returnType: string) => { - // Determine if this function should have SetofOptions - let setofOptionsInfo = '' - - const returnTableName = getTableNameFromRelationId( - fn.return_type_relation_id, - fn.return_type_id - ) - const returnsSetOfTable = fn.is_set_returning_function && fn.return_type_relation_id !== null - const returnsMultipleRows = fn.prorows !== null && fn.prorows > 1 - // Case 1: if the function returns a table, we need to add SetofOptions to allow selecting sub fields of the table - // Those can be used in rpc to select sub fields of a table - if (returnTableName) { - setofOptionsInfo = `SetofOptions: { - from: "*" - to: ${JSON.stringify(returnTableName)} - isOneToOne: ${Boolean(!returnsMultipleRows)} - isSetofReturn: ${fn.is_set_returning_function} - }` - } - // Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table - // and set the right "from" and "to" values to allow selecting from a table row - if (fn.args.length === 1) { - const relationType = relationTypeByIds.get(fn.args[0].type_id) - - // Only add SetofOptions for functions with table arguments (embedded functions) - // or specific functions that RETURNS table-name - if (relationType) { - const sourceTable = relationType.format - // Case 1: Standard embedded function with proper setof detection - if (returnsSetOfTable && returnTableName) { - setofOptionsInfo = `SetofOptions: { - from: ${JSON.stringify(sourceTable)} - to: ${JSON.stringify(returnTableName)} - isOneToOne: ${Boolean(!returnsMultipleRows)} - isSetofReturn: true - }` - } - // Case 2: Handle RETURNS table-name those are always a one to one relationship - else if (returnTableName && !returnsSetOfTable) { - const targetTable = returnTableName - setofOptionsInfo = `SetofOptions: { - from: ${JSON.stringify(sourceTable)} - to: ${JSON.stringify(targetTable)} - isOneToOne: true - isSetofReturn: false - }` - } - } - } - - return `${returnType}${fn.is_set_returning_function && returnsMultipleRows ? '[]' : ''} - ${setofOptionsInfo ? `${setofOptionsInfo}` : ''}` - } - - const getFunctionReturnType = (schema: PostgresSchema, fn: PostgresFunction): string => { - // Case 1: `returns table`. - const tableArgs = fn.args.filter(({ mode }) => mode === 'table') - if (tableArgs.length > 0) { - const argsNameAndType = tableArgs.map(({ name, type_id }) => { - const type = typesById.get(type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType } - }) - - return `{ - ${argsNameAndType.map(({ name, type }) => `${JSON.stringify(name)}: ${type}`)} - }` - } - - // Case 2: returns a relation's row type. - const relation = - introspectionBySchema[schema.name]?.tables.find( - ({ table: { id } }) => id === fn.return_type_relation_id - )?.table || - introspectionBySchema[schema.name]?.views.find( - ({ view: { id } }) => id === fn.return_type_relation_id - )?.view - if (relation) { - return `{ - ${columnsByTableId[relation.id] - .map((column) => - generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: column.is_nullable, - is_optional: false, - }, - { - types, - schemas, - tables, - views, - } - ) - ) - .join(',\n')} - }` - } - - // Case 3: returns base/array/composite/enum type. - const type = typesById.get(fn.return_type_id) - if (type) { - return pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - - return 'unknown' - } - // Special error case for functions that take table row but don't qualify as embedded functions - const hasTableRowError = (fn: PostgresFunction, inArgs: PostgresFunction['args']) => { - if ( - inArgs.length === 1 && - inArgs[0].name === '' && - relationTypeByIds.get(inArgs[0].type_id) && - !getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id) - ) { - return true - } - return false - } - - // Check for generic conflict cases that need error reporting - const getConflictError = ( - schema: PostgresSchema, - fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction['args'] }>, - fn: PostgresFunction, - inArgs: PostgresFunction['args'] - ) => { - // If there is a single function definition, there is no conflict - if (fns.length <= 1) return null - - // Generic conflict detection patterns - // Pattern 1: No-args vs default-args conflicts - if (inArgs.length === 0) { - const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => { - if (otherFn === fn) return false - return otherInArgs.length === 1 && otherInArgs[0].name === '' && otherInArgs[0].has_default - }) - - if (conflictingFns.length > 0) { - const conflictingFn = conflictingFns[0] - const returnTypeName = typesById.get(conflictingFn.fn.return_type_id)?.name || 'unknown' - return `Could not choose the best candidate function between: ${schema.name}.${fn.name}(), ${schema.name}.${fn.name}( => ${returnTypeName}). Try renaming the parameters or the function itself in the database so function overloading can be resolved` - } - } - - // Pattern 2: Same parameter name but different types (unresolvable overloads) - if (inArgs.length === 1 && inArgs[0].name !== '') { - const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => { - if (otherFn === fn) return false - return ( - otherInArgs.length === 1 && - otherInArgs[0].name === inArgs[0].name && - otherInArgs[0].type_id !== inArgs[0].type_id - ) - }) - - if (conflictingFns.length > 0) { - const allConflictingFunctions = [{ fn, inArgs }, ...conflictingFns] - const conflictList = allConflictingFunctions - .sort((a, b) => { - const aArgs = a.inArgs - const bArgs = b.inArgs - return (aArgs[0]?.type_id || 0) - (bArgs[0]?.type_id || 0) - }) - .map((f) => { - const args = f.inArgs - return `${schema.name}.${fn.name}(${args.map((a) => `${a.name || ''} => ${typesById.get(a.type_id)?.name || 'unknown'}`).join(', ')})` - }) - .join(', ') - - return `Could not choose the best candidate function between: ${conflictList}. Try renaming the parameters or the function itself in the database so function overloading can be resolved` - } - } - - return null - } - - const getFunctionSignatures = ( - schema: PostgresSchema, - fns: Array<{ fn: PostgresFunction; inArgs: PostgresFunction['args'] }> - ) => { - return fns - .map(({ fn, inArgs }) => { - let argsType = 'never' - let returnType = getFunctionReturnType(schema, fn) - - // Check for specific error cases - const conflictError = getConflictError(schema, fns, fn, inArgs) - if (conflictError) { - if (inArgs.length > 0) { - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById.get(type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - } - returnType = `{ error: true } & ${JSON.stringify(conflictError)}` - } else if (hasTableRowError(fn, inArgs)) { - // Special case for computed fields returning scalars functions - if (inArgs.length > 0) { - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById.get(type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - } - returnType = `{ error: true } & ${JSON.stringify(`the function ${schema.name}.${fn.name} with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache`)}` - } else if (inArgs.length > 0) { - const argsNameAndType = inArgs.map(({ name, type_id, has_default }) => { - const type = typesById.get(type_id) - let tsType = 'unknown' - if (type) { - tsType = pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }) - } - return { name, type: tsType, has_default } - }) - argsType = `{ ${argsNameAndType.map(({ name, type, has_default }) => `${JSON.stringify(name)}${has_default ? '?' : ''}: ${type}`)} }` - } - - return `{ Args: ${argsType}; Returns: ${getFunctionTsReturnType(fn, returnType)} }` - }) - .join(' |\n') - } - - const internal_supabase_schema = postgrestVersion - ? `// Allows to automatically instantiate createClient with right options - // instead of createClient(URL, KEY) - __InternalSupabase: { - PostgrestVersion: '${postgrestVersion}' - }` - : '' - - function generateNullableUnionTsType(tsType: string, isNullable: boolean) { - // Only add the null union if the type is not unknown as unknown already includes null - if (tsType === 'unknown' || tsType === 'any' || !isNullable) { - return tsType - } - return `${tsType} | null` - } - - function generateColumnTsDefinition( - schema: PostgresSchema, - column: { - name: string - format: string - is_nullable: boolean - is_optional: boolean - }, - context: { - types: PostgresType[] - schemas: PostgresSchema[] - tables: PostgresTable[] - views: PostgresView[] - } - ) { - return `${JSON.stringify(column.name)}${column.is_optional ? '?' : ''}: ${generateNullableUnionTsType(pgTypeToTsType(schema, column.format, context), column.is_nullable)}` - } - - let output = ` -export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] - -export type Database = { - ${internal_supabase_schema} - ${schemas.map((schema) => { - const { - tables: schemaTables, - views: schemaViews, - functions: schemaFunctions, - enums: schemaEnums, - compositeTypes: schemaCompositeTypes, - } = introspectionBySchema[schema.name] - return `${JSON.stringify(schema.name)}: { - Tables: { - ${ - schemaTables.length === 0 - ? '[_ in never]: never' - : schemaTables.map( - ({ table, relationships }) => `${JSON.stringify(table.name)}: { - Row: { - ${[ - ...columnsByTableId[table.id].map((column) => - generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: column.is_nullable, - is_optional: false, - }, - { types, schemas, tables, views } - ) - ), - ...schemaFunctions - .filter(({ fn }) => fn.argument_types === table.name) - .map(({ fn }) => { - return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}` - }), - ]} - } - Insert: { - ${columnsByTableId[table.id].map((column) => { - if (column.identity_generation === 'ALWAYS') { - return `${JSON.stringify(column.name)}?: never` - } - return generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: column.is_nullable, - is_optional: - column.is_nullable || - column.is_identity || - column.default_value !== null, - }, - { types, schemas, tables, views } - ) - })} - } - Update: { - ${columnsByTableId[table.id].map((column) => { - if (column.identity_generation === 'ALWAYS') { - return `${JSON.stringify(column.name)}?: never` - } - - return generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: column.is_nullable, - is_optional: true, - }, - { types, schemas, tables, views } - ) - })} - } - Relationships: [ - ${relationships.map(generateRelationshiptTsDefinition)} - ] - }` - ) - } - } - Views: { - ${ - schemaViews.length === 0 - ? '[_ in never]: never' - : schemaViews.map( - ({ view, relationships }) => `${JSON.stringify(view.name)}: { - Row: { - ${[ - ...columnsByTableId[view.id].map((column) => - generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: column.is_nullable, - is_optional: false, - }, - { types, schemas, tables, views } - ) - ), - ...schemaFunctions - .filter(({ fn }) => fn.argument_types === view.name) - .map( - ({ fn }) => - `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}` - ), - ]} - } - ${ - view.is_updatable - ? `Insert: { - ${columnsByTableId[view.id].map((column) => { - if (!column.is_updatable) { - return `${JSON.stringify(column.name)}?: never` - } - return generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: true, - is_optional: true, - }, - { types, schemas, tables, views } - ) - })} - } - Update: { - ${columnsByTableId[view.id].map((column) => { - if (!column.is_updatable) { - return `${JSON.stringify(column.name)}?: never` - } - return generateColumnTsDefinition( - schema, - { - name: column.name, - format: column.format, - is_nullable: true, - is_optional: true, - }, - { types, schemas, tables, views } - ) - })} - } - ` - : '' - }Relationships: [ - ${relationships.map(generateRelationshiptTsDefinition)} - ] - }` - ) - } - } - Functions: { - ${(() => { - if (schemaFunctions.length === 0) { - return '[_ in never]: never' - } - const schemaFunctionsGroupedByName = schemaFunctions.reduce( - (acc, curr) => { - acc[curr.fn.name] ??= [] - acc[curr.fn.name].push(curr) - return acc - }, - {} as Record - ) - for (const fnName in schemaFunctionsGroupedByName) { - schemaFunctionsGroupedByName[fnName].sort( - (a, b) => - a.fn.argument_types.localeCompare(b.fn.argument_types) || - a.fn.return_type.localeCompare(b.fn.return_type) - ) - } - - return Object.entries(schemaFunctionsGroupedByName) - .map(([fnName, fns]) => { - const functionSignatures = getFunctionSignatures(schema, fns) - return `${JSON.stringify(fnName)}:\n${functionSignatures}` - }) - .join(',\n') - })()} - } - Enums: { - ${ - 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)}: { - ${attributes.map(({ name, type_id }) => { - const type = typesById.get(type_id) - let tsType = 'unknown' - if (type) { - tsType = `${generateNullableUnionTsType( - pgTypeToTsType(schema, type.name, { - types, - schemas, - tables, - views, - }), - true - )}` - } - return `${JSON.stringify(name)}: ${tsType}` - })} - }` - ) - } - } - }` - })} -} - -type DatabaseWithoutInternals = Omit - -type DefaultSchema = DatabaseWithoutInternals[Extract] - -export type Tables< - DefaultSchemaTableNameOrOptions extends - | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) - | { schema: keyof DatabaseWithoutInternals }, - TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals - } - ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) - : never = never -> = DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & - DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { - Row: infer R - } - ? R - : never - : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) - ? (DefaultSchema["Tables"] & DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { - Row: infer R - } - ? R - : never - : never - -export type TablesInsert< - DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] - | { schema: keyof DatabaseWithoutInternals }, - TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals - } - ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] - : never = never -> = DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Insert: infer I - } - ? I - : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Insert: infer I - } - ? I - : never - : never - -export type TablesUpdate< - DefaultSchemaTableNameOrOptions extends - | keyof DefaultSchema["Tables"] - | { schema: keyof DatabaseWithoutInternals }, - TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals - } - ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] - : never = never -> = DefaultSchemaTableNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Update: infer U - } - ? U - : never - : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] - ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Update: infer U - } - ? U - : never - : never - -export type Enums< - DefaultSchemaEnumNameOrOptions extends - | keyof DefaultSchema["Enums"] - | { schema: keyof DatabaseWithoutInternals }, - EnumName extends DefaultSchemaEnumNameOrOptions extends { - schema: keyof DatabaseWithoutInternals - } - ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] - : never = never -> = DefaultSchemaEnumNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] - : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] - ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] - : never - -export type CompositeTypes< - PublicCompositeTypeNameOrOptions extends - | keyof DefaultSchema["CompositeTypes"] - | { schema: keyof DatabaseWithoutInternals }, - CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof DatabaseWithoutInternals - } - ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] - : never = never -> = PublicCompositeTypeNameOrOptions extends { schema: keyof DatabaseWithoutInternals } - ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] - : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] - ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] - : never - -export const Constants = { - ${schemas.map((schema) => { - const schemaEnums = introspectionBySchema[schema.name].enums - return `${JSON.stringify(schema.name)}: { - Enums: { - ${schemaEnums.map( - (enum_) => - `${JSON.stringify(enum_.name)}: [${enum_.enums - .map((variant) => JSON.stringify(variant)) - .join(', ')}]` - )} - } - }` - })} -} as const -` - - output = await prettier.format(output, { - parser: 'typescript', - semi: false, - }) - return output -} - -// TODO: Make this more robust. Currently doesn't handle range types - returns them as unknown. -export const pgTypeToTsType = ( - schema: PostgresSchema, - pgType: string, - { - types, - schemas, - tables, - views, - }: { - types: PostgresType[] - schemas: PostgresSchema[] - tables: PostgresTable[] - views: PostgresView[] - } -): string => { - if (pgType === 'bool') { - return 'boolean' - } else if (['int2', 'int4', 'int8', 'float4', 'float8', 'numeric'].includes(pgType)) { - return 'number' - } else if ( - [ - 'bytea', - 'bpchar', - 'varchar', - 'date', - 'text', - 'citext', - 'time', - 'timetz', - 'timestamp', - 'timestamptz', - 'uuid', - 'vector', - 'interval', - ].includes(pgType) - ) { - return 'string' - } else if (['json', 'jsonb'].includes(pgType)) { - return 'Json' - } else if (pgType === 'void') { - return 'undefined' - } else if (pgType === 'record') { - return 'Record' - } else if (pgType.startsWith('_')) { - return `(${pgTypeToTsType(schema, pgType.substring(1), { - types, - schemas, - tables, - views, - })})[]` - } else { - const enumTypes = types.filter((type) => type.name === pgType && type.enums.length > 0) - if (enumTypes.length > 0) { - const enumType = enumTypes.find((type) => type.schema === schema.name) || enumTypes[0] - if (schemas.some(({ name }) => name === enumType.schema)) { - return `Database[${JSON.stringify(enumType.schema)}]['Enums'][${JSON.stringify( - enumType.name - )}]` - } - return enumType.enums.map((variant) => JSON.stringify(variant)).join('|') - } - - const compositeTypes = types.filter( - (type) => type.name === pgType && type.attributes.length > 0 - ) - if (compositeTypes.length > 0) { - const compositeType = - compositeTypes.find((type) => type.schema === schema.name) || compositeTypes[0] - if (schemas.some(({ name }) => name === compositeType.schema)) { - return `Database[${JSON.stringify( - compositeType.schema - )}]['CompositeTypes'][${JSON.stringify(compositeType.name)}]` - } - return 'unknown' - } - - const tableRowTypes = tables.filter((table) => table.name === pgType) - if (tableRowTypes.length > 0) { - const tableRowType = - tableRowTypes.find((type) => type.schema === schema.name) || tableRowTypes[0] - if (schemas.some(({ name }) => name === tableRowType.schema)) { - return `Database[${JSON.stringify(tableRowType.schema)}]['Tables'][${JSON.stringify( - tableRowType.name - )}]['Row']` - } - return 'unknown' - } - - const viewRowTypes = views.filter((view) => view.name === pgType) - if (viewRowTypes.length > 0) { - const viewRowType = - viewRowTypes.find((type) => type.schema === schema.name) || viewRowTypes[0] - if (schemas.some(({ name }) => name === viewRowType.schema)) { - return `Database[${JSON.stringify(viewRowType.schema)}]['Views'][${JSON.stringify( - viewRowType.name - )}]['Row']` - } - return 'unknown' - } - - return 'unknown' - } -} diff --git a/test/server/templates/go.test.ts b/test/server/templates/go.test.ts deleted file mode 100644 index f1be6b50..00000000 --- a/test/server/templates/go.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { apply } from '../../../src/server/templates/go' -import type { GeneratorMetadata } from '../../../src/lib/generators' -import type { - PostgresColumn, - PostgresSchema, - PostgresTable, - PostgresType, -} from '../../../src/lib/types' - -const baseSchema: PostgresSchema = { - id: 1, - name: 'public', - owner: 'postgres', -} - -const baseTable = { - id: 1, - schema: 'public', - name: 'tickets', - rls_enabled: false, - rls_forced: false, - replica_identity: 'DEFAULT', - bytes: 0, - size: '0 bytes', - live_rows_estimate: 0, - dead_rows_estimate: 0, - comment: null, - primary_keys: [], - relationships: [], -} as unknown as Omit - -const userStatusEnum: PostgresType = { - id: 100, - name: 'user_status', - schema: 'public', - format: 'user_status', - enums: ['ACTIVE', 'INACTIVE'], - attributes: [], - comment: null, - type_relation_id: null, -} - -const baseColumn = (overrides: Partial): PostgresColumn => - ({ - table_id: 1, - schema: 'public', - table: 'tickets', - id: '1.1', - ordinal_position: 1, - name: 'col', - default_value: null, - data_type: 'text', - format: 'text', - is_identity: false, - identity_generation: null, - is_generated: false, - is_nullable: false, - is_updatable: true, - is_unique: false, - enums: [], - check: null, - comment: null, - ...overrides, - }) as PostgresColumn - -const buildMetadata = (columns: PostgresColumn[]): GeneratorMetadata => ({ - schemas: [baseSchema], - tables: [baseTable], - foreignTables: [], - views: [], - materializedViews: [], - columns, - relationships: [], - functions: [], - types: [userStatusEnum], -}) - -describe('go typegen pgTypeToGoType array fallback', () => { - test('non-nullable array of enum resolves to []string, not []interface{}', () => { - const result = apply( - buildMetadata([baseColumn({ name: 'tags', format: '_user_status', is_nullable: false })]) - ) - - expect(result).toMatch(/Tags\s+\[]string\b/) - expect(result).not.toMatch(/Tags\s+\[]interface\{\}/) - }) - - test('nullable array of enum resolves to []*string, not []interface{}', () => { - const result = apply( - buildMetadata([baseColumn({ name: 'tags', format: '_user_status', is_nullable: true })]) - ) - - expect(result).toMatch(/Tags\s+\[]\*string\b/) - expect(result).not.toMatch(/Tags\s+\[]interface\{\}/) - }) - - test('plain text array still resolves to []string', () => { - const result = apply( - buildMetadata([baseColumn({ name: 'tags', format: '_text', is_nullable: false })]) - ) - - expect(result).toMatch(/Tags\s+\[]string\b/) - }) -}) diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 50a0896b..39f87745 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -5268,82 +5268,19 @@ test('typegen: go', async () => { expect(body).toMatchInlineSnapshot(` "package database - type PublicUsersSelect struct { - Decimal *float64 \`json:"decimal"\` - Id int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` - UserUuid *string \`json:"user_uuid"\` - } - - type PublicUsersInsert struct { - Decimal *float64 \`json:"decimal"\` - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` - UserUuid *string \`json:"user_uuid"\` - } - - type PublicUsersUpdate struct { - Decimal *float64 \`json:"decimal"\` - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` - UserUuid *string \`json:"user_uuid"\` - } - - type PublicTodosSelect struct { - Details *string \`json:"details"\` - Id int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` - } - - type PublicTodosInsert struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` - } - - type PublicTodosUpdate struct { - Details *string \`json:"details"\` - Id *int64 \`json:"id"\` - UserId *int64 \`json:"user-id"\` - } - - type PublicUsersAuditSelect struct { - CreatedAt *string \`json:"created_at"\` - Id int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` - } - - type PublicUsersAuditInsert struct { - CreatedAt *string \`json:"created_at"\` - Id *int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` - } - - type PublicUsersAuditUpdate struct { - CreatedAt *string \`json:"created_at"\` - Id *int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId *int64 \`json:"user_id"\` - } - - type PublicUserDetailsSelect struct { - Details *string \`json:"details"\` - UserId int64 \`json:"user_id"\` + type PublicCategorySelect struct { + Id int32 \`json:"id"\` + Name string \`json:"name"\` } - type PublicUserDetailsInsert struct { - Details *string \`json:"details"\` - UserId int64 \`json:"user_id"\` + type PublicCategoryInsert struct { + Id *int32 \`json:"id"\` + Name string \`json:"name"\` } - type PublicUserDetailsUpdate struct { - Details *string \`json:"details"\` - UserId *int64 \`json:"user_id"\` + type PublicCategoryUpdate struct { + Id *int32 \`json:"id"\` + Name *string \`json:"name"\` } type PublicEmptySelect struct { @@ -5358,36 +5295,6 @@ test('typegen: go', async () => { } - 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 PublicTableWithPrimaryKeyOtherThanIdSelect struct { - Name *string \`json:"name"\` - OtherId int64 \`json:"other_id"\` - } - - type PublicTableWithPrimaryKeyOtherThanIdInsert struct { - Name *string \`json:"name"\` - OtherId *int64 \`json:"other_id"\` - } - - type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { - Name *string \`json:"name"\` - OtherId *int64 \`json:"other_id"\` - } - type PublicEventsSelect struct { CreatedAt string \`json:"created_at"\` Data interface{} \`json:"data"\` @@ -5469,21 +5376,6 @@ test('typegen: go', async () => { Id *int64 \`json:"id"\` } - type PublicCategorySelect struct { - Id int32 \`json:"id"\` - Name string \`json:"name"\` - } - - type PublicCategoryInsert struct { - Id *int32 \`json:"id"\` - Name string \`json:"name"\` - } - - type PublicCategoryUpdate struct { - Id *int32 \`json:"id"\` - Name *string \`json:"name"\` - } - type PublicMemesSelect struct { Category *int32 \`json:"category"\` CreatedAt string \`json:"created_at"\` @@ -5511,17 +5403,86 @@ test('typegen: go', async () => { Status *string \`json:"status"\` } - type PublicAViewSelect struct { - Id *int64 \`json:"id"\` + type PublicTableWithOtherTablesRowTypeSelect struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` } - type PublicTodosViewSelect struct { + type PublicTableWithOtherTablesRowTypeInsert struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithOtherTablesRowTypeUpdate struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdSelect struct { + Name *string \`json:"name"\` + OtherId int64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdInsert struct { + Name *string \`json:"name"\` + OtherId *int64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { + Name *string \`json:"name"\` + OtherId *int64 \`json:"other_id"\` + } + + type PublicTodosSelect struct { + Details *string \`json:"details"\` + Id int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } + + type PublicTodosInsert struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } + + type PublicTodosUpdate struct { Details *string \`json:"details"\` Id *int64 \`json:"id"\` UserId *int64 \`json:"user-id"\` } - type PublicUsersViewSelect struct { + type PublicUserDetailsSelect struct { + Details *string \`json:"details"\` + UserId int64 \`json:"user_id"\` + } + + type PublicUserDetailsInsert struct { + Details *string \`json:"details"\` + UserId int64 \`json:"user_id"\` + } + + type PublicUserDetailsUpdate struct { + Details *string \`json:"details"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersSelect struct { + Decimal *float64 \`json:"decimal"\` + Id int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` + } + + type PublicUsersInsert struct { + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` + } + + type PublicUsersUpdate struct { Decimal *float64 \`json:"decimal"\` Id *int64 \`json:"id"\` Name *string \`json:"name"\` @@ -5529,6 +5490,37 @@ test('typegen: go', async () => { UserUuid *string \`json:"user_uuid"\` } + type PublicUsersAuditSelect struct { + CreatedAt *string \`json:"created_at"\` + Id int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersAuditInsert struct { + CreatedAt *string \`json:"created_at"\` + Id *int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicUsersAuditUpdate struct { + CreatedAt *string \`json:"created_at"\` + Id *int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId *int64 \`json:"user_id"\` + } + + type PublicAViewSelect struct { + Id *int64 \`json:"id"\` + } + + type PublicTodosViewSelect struct { + Details *string \`json:"details"\` + Id *int64 \`json:"id"\` + UserId *int64 \`json:"user-id"\` + } + type PublicUserTodosSummaryViewSelect struct { TodoCount *int64 \`json:"todo_count"\` TodoDetails []*string \`json:"todo_details"\` @@ -5537,6 +5529,14 @@ test('typegen: go', async () => { UserStatus *string \`json:"user_status"\` } + type PublicUsersViewSelect struct { + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` + } + type PublicUsersViewWithMultipleRefsToUsersSelect struct { InitialId *int64 \`json:"initial_id"\` InitialName *string \`json:"initial_name"\` @@ -6642,75 +6642,21 @@ test('typegen: python', async () => { from pydantic import BaseModel, Field, Json - PublicUserStatus: TypeAlias = Literal["ACTIVE", "INACTIVE"] - PublicMemeStatus: TypeAlias = Literal["new", "old", "retired"] - class PublicUsers(BaseModel): - decimal: Optional[float] = Field(alias="decimal") - id: int = Field(alias="id") - name: Optional[str] = Field(alias="name") - status: Optional[PublicUserStatus] = Field(alias="status") - user_uuid: Optional[uuid.UUID] = Field(alias="user_uuid") - - class PublicUsersInsert(TypedDict): - decimal: NotRequired[Annotated[Optional[float], Field(alias="decimal")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - name: NotRequired[Annotated[Optional[str], Field(alias="name")]] - status: NotRequired[Annotated[Optional[PublicUserStatus], Field(alias="status")]] - user_uuid: NotRequired[Annotated[Optional[uuid.UUID], Field(alias="user_uuid")]] - - class PublicUsersUpdate(TypedDict): - decimal: NotRequired[Annotated[Optional[float], Field(alias="decimal")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - name: NotRequired[Annotated[Optional[str], Field(alias="name")]] - status: NotRequired[Annotated[Optional[PublicUserStatus], Field(alias="status")]] - user_uuid: NotRequired[Annotated[Optional[uuid.UUID], Field(alias="user_uuid")]] - - class PublicTodos(BaseModel): - details: Optional[str] = Field(alias="details") - id: int = Field(alias="id") - user_id: int = Field(alias="user-id") - - class PublicTodosInsert(TypedDict): - details: NotRequired[Annotated[Optional[str], Field(alias="details")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - user_id: Annotated[int, Field(alias="user-id")] - - class PublicTodosUpdate(TypedDict): - details: NotRequired[Annotated[Optional[str], Field(alias="details")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - user_id: NotRequired[Annotated[int, Field(alias="user-id")]] + PublicUserStatus: TypeAlias = Literal["ACTIVE", "INACTIVE"] - class PublicUsersAudit(BaseModel): - created_at: Optional[datetime.datetime] = Field(alias="created_at") + class PublicCategory(BaseModel): id: int = Field(alias="id") - previous_value: Optional[Json[Any]] = Field(alias="previous_value") - user_id: Optional[int] = Field(alias="user_id") + name: str = Field(alias="name") - class PublicUsersAuditInsert(TypedDict): - created_at: NotRequired[Annotated[Optional[datetime.datetime], Field(alias="created_at")]] + class PublicCategoryInsert(TypedDict): id: NotRequired[Annotated[int, Field(alias="id")]] - previous_value: NotRequired[Annotated[Optional[Json[Any]], Field(alias="previous_value")]] - user_id: NotRequired[Annotated[Optional[int], Field(alias="user_id")]] + name: Annotated[str, Field(alias="name")] - class PublicUsersAuditUpdate(TypedDict): - created_at: NotRequired[Annotated[Optional[datetime.datetime], Field(alias="created_at")]] + class PublicCategoryUpdate(TypedDict): id: NotRequired[Annotated[int, Field(alias="id")]] - previous_value: NotRequired[Annotated[Optional[Json[Any]], Field(alias="previous_value")]] - user_id: NotRequired[Annotated[Optional[int], Field(alias="user_id")]] - - class PublicUserDetails(BaseModel): - details: Optional[str] = Field(alias="details") - user_id: int = Field(alias="user_id") - - class PublicUserDetailsInsert(TypedDict): - details: NotRequired[Annotated[Optional[str], Field(alias="details")]] - user_id: Annotated[int, Field(alias="user_id")] - - class PublicUserDetailsUpdate(TypedDict): - details: NotRequired[Annotated[Optional[str], Field(alias="details")]] - user_id: NotRequired[Annotated[int, Field(alias="user_id")]] + name: NotRequired[Annotated[str, Field(alias="name")]] class PublicEmpty(BaseModel): pass @@ -6721,30 +6667,6 @@ test('typegen: python', async () => { class PublicEmptyUpdate(TypedDict): pass - class PublicTableWithOtherTablesRowType(BaseModel): - col1: Optional[PublicUserDetails] = Field(alias="col1") - col2: Optional[PublicAView] = Field(alias="col2") - - class PublicTableWithOtherTablesRowTypeInsert(TypedDict): - col1: NotRequired[Annotated[Optional[PublicUserDetails], Field(alias="col1")]] - col2: NotRequired[Annotated[Optional[PublicAView], Field(alias="col2")]] - - class PublicTableWithOtherTablesRowTypeUpdate(TypedDict): - col1: NotRequired[Annotated[Optional[PublicUserDetails], Field(alias="col1")]] - col2: NotRequired[Annotated[Optional[PublicAView], Field(alias="col2")]] - - class PublicTableWithPrimaryKeyOtherThanId(BaseModel): - name: Optional[str] = Field(alias="name") - other_id: int = Field(alias="other_id") - - class PublicTableWithPrimaryKeyOtherThanIdInsert(TypedDict): - name: NotRequired[Annotated[Optional[str], Field(alias="name")]] - other_id: NotRequired[Annotated[int, Field(alias="other_id")]] - - class PublicTableWithPrimaryKeyOtherThanIdUpdate(TypedDict): - name: NotRequired[Annotated[Optional[str], Field(alias="name")]] - other_id: NotRequired[Annotated[int, Field(alias="other_id")]] - class PublicEvents(BaseModel): created_at: datetime.datetime = Field(alias="created_at") data: Optional[Json[Any]] = Field(alias="data") @@ -6814,18 +6736,6 @@ test('typegen: python', async () => { duration_required: NotRequired[Annotated[str, Field(alias="duration_required")]] id: NotRequired[Annotated[int, Field(alias="id")]] - class PublicCategory(BaseModel): - id: int = Field(alias="id") - name: str = Field(alias="name") - - class PublicCategoryInsert(TypedDict): - id: NotRequired[Annotated[int, Field(alias="id")]] - name: Annotated[str, Field(alias="name")] - - class PublicCategoryUpdate(TypedDict): - id: NotRequired[Annotated[int, Field(alias="id")]] - name: NotRequired[Annotated[str, Field(alias="name")]] - class PublicMemes(BaseModel): category: Optional[int] = Field(alias="category") created_at: datetime.datetime = Field(alias="created_at") @@ -6850,21 +6760,104 @@ test('typegen: python', async () => { name: NotRequired[Annotated[str, Field(alias="name")]] status: NotRequired[Annotated[Optional[PublicMemeStatus], Field(alias="status")]] - class PublicAView(BaseModel): - id: Optional[int] = Field(alias="id") + class PublicTableWithOtherTablesRowType(BaseModel): + col1: Optional[PublicUserDetails] = Field(alias="col1") + col2: Optional[PublicAView] = Field(alias="col2") - class PublicTodosView(BaseModel): + class PublicTableWithOtherTablesRowTypeInsert(TypedDict): + col1: NotRequired[Annotated[Optional[PublicUserDetails], Field(alias="col1")]] + col2: NotRequired[Annotated[Optional[PublicAView], Field(alias="col2")]] + + class PublicTableWithOtherTablesRowTypeUpdate(TypedDict): + col1: NotRequired[Annotated[Optional[PublicUserDetails], Field(alias="col1")]] + col2: NotRequired[Annotated[Optional[PublicAView], Field(alias="col2")]] + + class PublicTableWithPrimaryKeyOtherThanId(BaseModel): + name: Optional[str] = Field(alias="name") + other_id: int = Field(alias="other_id") + + class PublicTableWithPrimaryKeyOtherThanIdInsert(TypedDict): + name: NotRequired[Annotated[Optional[str], Field(alias="name")]] + other_id: NotRequired[Annotated[int, Field(alias="other_id")]] + + class PublicTableWithPrimaryKeyOtherThanIdUpdate(TypedDict): + name: NotRequired[Annotated[Optional[str], Field(alias="name")]] + other_id: NotRequired[Annotated[int, Field(alias="other_id")]] + + class PublicTodos(BaseModel): details: Optional[str] = Field(alias="details") - id: Optional[int] = Field(alias="id") - user_id: Optional[int] = Field(alias="user-id") + id: int = Field(alias="id") + user_id: int = Field(alias="user-id") - class PublicUsersView(BaseModel): + class PublicTodosInsert(TypedDict): + details: NotRequired[Annotated[Optional[str], Field(alias="details")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + user_id: Annotated[int, Field(alias="user-id")] + + class PublicTodosUpdate(TypedDict): + details: NotRequired[Annotated[Optional[str], Field(alias="details")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + user_id: NotRequired[Annotated[int, Field(alias="user-id")]] + + class PublicUserDetails(BaseModel): + details: Optional[str] = Field(alias="details") + user_id: int = Field(alias="user_id") + + class PublicUserDetailsInsert(TypedDict): + details: NotRequired[Annotated[Optional[str], Field(alias="details")]] + user_id: Annotated[int, Field(alias="user_id")] + + class PublicUserDetailsUpdate(TypedDict): + details: NotRequired[Annotated[Optional[str], Field(alias="details")]] + user_id: NotRequired[Annotated[int, Field(alias="user_id")]] + + class PublicUsers(BaseModel): decimal: Optional[float] = Field(alias="decimal") - id: Optional[int] = Field(alias="id") + id: int = Field(alias="id") name: Optional[str] = Field(alias="name") status: Optional[PublicUserStatus] = Field(alias="status") user_uuid: Optional[uuid.UUID] = Field(alias="user_uuid") + class PublicUsersInsert(TypedDict): + decimal: NotRequired[Annotated[Optional[float], Field(alias="decimal")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + name: NotRequired[Annotated[Optional[str], Field(alias="name")]] + status: NotRequired[Annotated[Optional[PublicUserStatus], Field(alias="status")]] + user_uuid: NotRequired[Annotated[Optional[uuid.UUID], Field(alias="user_uuid")]] + + class PublicUsersUpdate(TypedDict): + decimal: NotRequired[Annotated[Optional[float], Field(alias="decimal")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + name: NotRequired[Annotated[Optional[str], Field(alias="name")]] + status: NotRequired[Annotated[Optional[PublicUserStatus], Field(alias="status")]] + user_uuid: NotRequired[Annotated[Optional[uuid.UUID], Field(alias="user_uuid")]] + + class PublicUsersAudit(BaseModel): + created_at: Optional[datetime.datetime] = Field(alias="created_at") + id: int = Field(alias="id") + previous_value: Optional[Json[Any]] = Field(alias="previous_value") + user_id: Optional[int] = Field(alias="user_id") + + class PublicUsersAuditInsert(TypedDict): + created_at: NotRequired[Annotated[Optional[datetime.datetime], Field(alias="created_at")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + previous_value: NotRequired[Annotated[Optional[Json[Any]], Field(alias="previous_value")]] + user_id: NotRequired[Annotated[Optional[int], Field(alias="user_id")]] + + class PublicUsersAuditUpdate(TypedDict): + created_at: NotRequired[Annotated[Optional[datetime.datetime], Field(alias="created_at")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + previous_value: NotRequired[Annotated[Optional[Json[Any]], Field(alias="previous_value")]] + user_id: NotRequired[Annotated[Optional[int], Field(alias="user_id")]] + + class PublicAView(BaseModel): + id: Optional[int] = Field(alias="id") + + class PublicTodosView(BaseModel): + details: Optional[str] = Field(alias="details") + id: Optional[int] = Field(alias="id") + user_id: Optional[int] = Field(alias="user-id") + class PublicUserTodosSummaryView(BaseModel): todo_count: Optional[int] = Field(alias="todo_count") todo_details: Optional[List[str]] = Field(alias="todo_details") @@ -6872,6 +6865,13 @@ test('typegen: python', async () => { user_name: Optional[str] = Field(alias="user_name") user_status: Optional[PublicUserStatus] = Field(alias="user_status") + class PublicUsersView(BaseModel): + decimal: Optional[float] = Field(alias="decimal") + id: Optional[int] = Field(alias="id") + name: Optional[str] = Field(alias="name") + status: Optional[PublicUserStatus] = Field(alias="status") + user_uuid: Optional[uuid.UUID] = Field(alias="user_uuid") + class PublicUsersViewWithMultipleRefsToUsers(BaseModel): initial_id: Optional[int] = Field(alias="initial_id") initial_name: Optional[str] = Field(alias="initial_name") diff --git a/test/types.test.ts b/test/types.test.ts index 8a213902..961d9e7f 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -1,7 +1,7 @@ import { expect, test, describe } from 'vitest' import { build } from '../src/server/app.js' import { TEST_CONNECTION_STRING } from './lib/utils.js' -import { pgTypeToTsType } from '../src/server/templates/typescript' +import { pgTypeToTsType } from '@supabase/postgrest-typegen' describe('server/routes/types', () => { test('should list types', async () => {