diff --git a/package.json b/package.json index 365de941..45b3532a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "docs:export": "PG_META_EXPORT_DOCS=true node --loader ts-node/esm src/server/server.ts > openapi.json", "gen:types:typescript": "PG_META_GENERATE_TYPES=typescript node --loader ts-node/esm src/server/server.ts", "gen:types:go": "PG_META_GENERATE_TYPES=go node --loader ts-node/esm src/server/server.ts", + "gen:types:swift": "PG_META_GENERATE_TYPES=swift node --loader ts-node/esm src/server/server.ts", "start": "node dist/server/server.js", "dev": "trap 'npm run db:clean' INT && run-s db:clean db:run && nodemon --exec node --loader ts-node/esm src/server/server.ts | pino-pretty --colorize", "test": "run-s db:clean db:run test:run db:clean", diff --git a/src/server/constants.ts b/src/server/constants.ts index 86415b2a..f7447c45 100644 --- a/src/server/constants.ts +++ b/src/server/constants.ts @@ -1,6 +1,7 @@ import crypto from 'crypto' import { PoolConfig } from 'pg' import { getSecret } from '../lib/secrets.js' +import { AccessControl } from './templates/swift.js' export const PG_META_HOST = process.env.PG_META_HOST || '0.0.0.0' export const PG_META_PORT = Number(process.env.PG_META_PORT || 1337) @@ -40,7 +41,8 @@ export const GENERATE_TYPES_INCLUDED_SCHEMAS = GENERATE_TYPES : [] export const GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS = process.env.PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS === 'true' - +export const GENERATE_TYPES_SWIFT_ACCESS_CONTROL = + (process.env.PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL as AccessControl) || 'internal' export const DEFAULT_POOL_CONFIG: PoolConfig = { max: 1, connectionTimeoutMillis: PG_CONN_TIMEOUT_SECS * 1000, diff --git a/src/server/routes/generators/swift.ts b/src/server/routes/generators/swift.ts new file mode 100644 index 00000000..2c802fe2 --- /dev/null +++ b/src/server/routes/generators/swift.ts @@ -0,0 +1,40 @@ +import type { FastifyInstance } from 'fastify' +import { PostgresMeta } from '../../../lib/index.js' +import { DEFAULT_POOL_CONFIG } from '../../constants.js' +import { extractRequestForLogging } from '../../utils.js' +import { apply as applySwiftTemplate, AccessControl } from '../../templates/swift.js' +import { getGeneratorMetadata } from '../../../lib/generators.js' + +export default async (fastify: FastifyInstance) => { + fastify.get<{ + Headers: { pg: string } + Querystring: { + excluded_schemas?: string + included_schemas?: string + access_control?: AccessControl + } + }>('/', async (request, reply) => { + const connectionString = request.headers.pg + const excludedSchemas = + request.query.excluded_schemas?.split(',').map((schema) => schema.trim()) ?? [] + const includedSchemas = + request.query.included_schemas?.split(',').map((schema) => schema.trim()) ?? [] + const accessControl = request.query.access_control ?? 'internal' + + const pgMeta: PostgresMeta = new PostgresMeta({ ...DEFAULT_POOL_CONFIG, connectionString }) + const { data: generatorMeta, error: generatorMetaError } = await getGeneratorMetadata(pgMeta, { + includedSchemas, + excludedSchemas, + }) + if (generatorMetaError) { + request.log.error({ error: generatorMetaError, request: extractRequestForLogging(request) }) + reply.code(500) + return { error: generatorMetaError.message } + } + + return applySwiftTemplate({ + ...generatorMeta, + accessControl, + }) + }) +} diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 99678515..55b85618 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -20,6 +20,7 @@ import TypesRoute from './types.js' import ViewsRoute from './views.js' import TypeScriptTypeGenRoute from './generators/typescript.js' import GoTypeGenRoute from './generators/go.js' +import SwiftTypeGenRoute from './generators/swift.js' import { PG_CONNECTION, CRYPTO_KEY } from '../constants.js' export default async (fastify: FastifyInstance) => { @@ -65,4 +66,5 @@ export default async (fastify: FastifyInstance) => { fastify.register(ViewsRoute, { prefix: '/views' }) fastify.register(TypeScriptTypeGenRoute, { prefix: '/generators/typescript' }) fastify.register(GoTypeGenRoute, { prefix: '/generators/go' }) + fastify.register(SwiftTypeGenRoute, { prefix: '/generators/swift' }) } diff --git a/src/server/server.ts b/src/server/server.ts index 12371e8a..1c915f33 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -9,12 +9,14 @@ import { GENERATE_TYPES, GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, GENERATE_TYPES_INCLUDED_SCHEMAS, + GENERATE_TYPES_SWIFT_ACCESS_CONTROL, PG_CONNECTION, PG_META_HOST, PG_META_PORT, } 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 { getGeneratorMetadata } from '../lib/generators.js' const logger = pino({ @@ -52,6 +54,11 @@ async function getTypeOutput(): Promise { detectOneToOneRelationships: GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS, }) break + case 'swift': + output = await applySwiftTemplate({ + ...generatorMetadata, + accessControl: GENERATE_TYPES_SWIFT_ACCESS_CONTROL, + }) case 'go': output = applyGoTemplate(generatorMetadata) break diff --git a/src/server/templates/swift.ts b/src/server/templates/swift.ts new file mode 100644 index 00000000..a10972e4 --- /dev/null +++ b/src/server/templates/swift.ts @@ -0,0 +1,391 @@ +import prettier from 'prettier' +import type { + PostgresColumn, + PostgresFunction, + PostgresMaterializedView, + PostgresSchema, + PostgresTable, + PostgresType, + PostgresView, +} from '../../lib/index.js' +import type { GeneratorMetadata } from '../../lib/generators.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 | 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, + views, + materializedViews, + columns, + types, + accessControl, +}: GeneratorMetadata & SwiftGeneratorOptions): Promise => { + 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) + const enums = types + .filter((type) => type.enums.length > 0) + .sort(({ name: a }, { name: b }) => a.localeCompare(b)) + + const swiftEnums = enums.map((enum_) => { + return { schema: enum_.schema, enum_: pgEnumToSwiftEnum(enum_) } + }) + + const swiftStructForTables = tables.flatMap((table) => + (['Select', 'Insert', 'Update'] as Operation[]).map((operation) => + pgTypeToSwiftStruct(table, columnsByTableId[table.id], operation, { types, views, tables }) + ) + ) + + const swiftStructForViews = views.map((view) => + pgTypeToSwiftStruct(view, columnsByTableId[view.id], 'Select', { types, views, tables }) + ) + + const swiftStructForMaterializedViews = materializedViews.map((materializedView) => + pgTypeToSwiftStruct(materializedView, columnsByTableId[materializedView.id], 'Select', { + types, + views, + tables, + }) + ) + + const swiftStructForCompositeTypes = compositeTypes.map((type) => + pgCompositeTypeToSwiftStruct(type, { types, views, tables }) + ) + + let output = [ + 'import Foundation', + 'import Supabase', + '', + ...schemas.flatMap((schema) => [ + `${accessControl} enum ${formatForSwiftSchemaName(schema.name)} {`, + ...swiftEnums.flatMap(({ enum_ }) => generateEnum(enum_, { accessControl, level: 1 })), + ...swiftStructForTables.flatMap((struct) => + generateStruct(struct, { accessControl, level: 1 }) + ), + ...swiftStructForViews.flatMap((struct) => + generateStruct(struct, { accessControl, level: 1 }) + ), + ...swiftStructForMaterializedViews.flatMap((struct) => + generateStruct(struct, { accessControl, level: 1 }) + ), + ...swiftStructForCompositeTypes.flatMap((struct) => + generateStruct(struct, { 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 (pgType === 'uuid') { + swiftType = 'UUID' + } else if ( + [ + 'bytea', + 'bpchar', + 'varchar', + 'date', + 'text', + 'citext', + 'time', + 'timetz', + 'timestamp', + 'timestamptz', + '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 + * ``` + */ +function formatForSwiftTypeName(name: string): string { + return name + .split(/[^a-zA-Z0-9]/) + .map((word) => `${word[0].toUpperCase()}${word.slice(1)}`) + .join('') +} + +/** + * 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 { + return 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('') +} diff --git a/test/db/00-init.sql b/test/db/00-init.sql index 4a09c445..e28a0b16 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -132,3 +132,8 @@ create table table_with_other_tables_row_type ( col1 user_details, col2 a_view ); + +create table table_with_primary_key_other_than_id ( + other_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name text +); diff --git a/test/lib/tables.ts b/test/lib/tables.ts index 6b588d24..c4c934e7 100644 --- a/test/lib/tables.ts +++ b/test/lib/tables.ts @@ -114,19 +114,19 @@ test('list', async () => { ], "relationships": [ { - "constraint_name": "user_details_user_id_fkey", - "source_column_name": "user_id", + "constraint_name": "todos_user-id_fkey", + "source_column_name": "user-id", "source_schema": "public", - "source_table_name": "user_details", + "source_table_name": "todos", "target_column_name": "id", "target_table_name": "users", "target_table_schema": "public", }, { - "constraint_name": "todos_user-id_fkey", - "source_column_name": "user-id", + "constraint_name": "user_details_user_id_fkey", + "source_column_name": "user_id", "source_schema": "public", - "source_table_name": "todos", + "source_table_name": "user_details", "target_column_name": "id", "target_table_name": "users", "target_table_schema": "public", @@ -178,19 +178,19 @@ test('list without columns', async () => { ], "relationships": [ { - "constraint_name": "user_details_user_id_fkey", - "source_column_name": "user_id", + "constraint_name": "todos_user-id_fkey", + "source_column_name": "user-id", "source_schema": "public", - "source_table_name": "user_details", + "source_table_name": "todos", "target_column_name": "id", "target_table_name": "users", "target_table_schema": "public", }, { - "constraint_name": "todos_user-id_fkey", - "source_column_name": "user-id", + "constraint_name": "user_details_user_id_fkey", + "source_column_name": "user_id", "source_schema": "public", - "source_table_name": "todos", + "source_table_name": "user_details", "target_column_name": "id", "target_table_name": "users", "target_table_schema": "public", diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 4614d434..19744628 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -103,6 +103,21 @@ test('typegen: typescript', async () => { } Relationships: [] } + table_with_primary_key_other_than_id: { + Row: { + name: string | null + other_id: number + } + Insert: { + name?: string | null + other_id?: number + } + Update: { + name?: string | null + other_id?: number + } + Relationships: [] + } todos: { Row: { details: string | null @@ -605,6 +620,21 @@ test('typegen w/ one-to-one relationships', async () => { } Relationships: [] } + table_with_primary_key_other_than_id: { + Row: { + name: string | null + other_id: number + } + Insert: { + name?: string | null + other_id?: number + } + Update: { + name?: string | null + other_id?: number + } + Relationships: [] + } todos: { Row: { details: string | null @@ -1119,6 +1149,21 @@ test('typegen: typescript w/ one-to-one relationships', async () => { } Relationships: [] } + table_with_primary_key_other_than_id: { + Row: { + name: string | null + other_id: number + } + Insert: { + name?: string | null + other_id?: number + } + Update: { + name?: string | null + other_id?: number + } + Relationships: [] + } todos: { Row: { details: string | null @@ -1531,173 +1576,814 @@ test('typegen: go', async () => { expect(body).toMatchInlineSnapshot(` "package database -import "database/sql" + import "database/sql" -type PublicUsersSelect struct { - Id int64 \`json:"id"\` - Name sql.NullString \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicUsersSelect struct { + Id int64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` + } -type PublicUsersInsert struct { - Id sql.NullInt64 \`json:"id"\` - Name sql.NullString \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicUsersInsert struct { + Id sql.NullInt64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` + } -type PublicUsersUpdate struct { - Id sql.NullInt64 \`json:"id"\` - Name sql.NullString \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicUsersUpdate struct { + Id sql.NullInt64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` + } -type PublicTodosSelect struct { - Details sql.NullString \`json:"details"\` - Id int64 \`json:"id"\` - UserId int64 \`json:"user-id"\` -} + type PublicTodosSelect struct { + Details sql.NullString \`json:"details"\` + Id int64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } -type PublicTodosInsert struct { - Details sql.NullString \`json:"details"\` - Id sql.NullInt64 \`json:"id"\` - UserId int64 \`json:"user-id"\` -} + type PublicTodosInsert struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId int64 \`json:"user-id"\` + } -type PublicTodosUpdate struct { - Details sql.NullString \`json:"details"\` - Id sql.NullInt64 \`json:"id"\` - UserId sql.NullInt64 \`json:"user-id"\` -} + type PublicTodosUpdate struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId sql.NullInt64 \`json:"user-id"\` + } -type PublicUsersAuditSelect struct { - CreatedAt sql.NullString \`json:"created_at"\` - Id int64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId sql.NullInt64 \`json:"user_id"\` -} + type PublicUsersAuditSelect struct { + CreatedAt sql.NullString \`json:"created_at"\` + Id int64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId sql.NullInt64 \`json:"user_id"\` + } -type PublicUsersAuditInsert struct { - CreatedAt sql.NullString \`json:"created_at"\` - Id sql.NullInt64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId sql.NullInt64 \`json:"user_id"\` -} + type PublicUsersAuditInsert struct { + CreatedAt sql.NullString \`json:"created_at"\` + Id sql.NullInt64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId sql.NullInt64 \`json:"user_id"\` + } -type PublicUsersAuditUpdate struct { - CreatedAt sql.NullString \`json:"created_at"\` - Id sql.NullInt64 \`json:"id"\` - PreviousValue interface{} \`json:"previous_value"\` - UserId sql.NullInt64 \`json:"user_id"\` -} + type PublicUsersAuditUpdate struct { + CreatedAt sql.NullString \`json:"created_at"\` + Id sql.NullInt64 \`json:"id"\` + PreviousValue interface{} \`json:"previous_value"\` + UserId sql.NullInt64 \`json:"user_id"\` + } -type PublicUserDetailsSelect struct { - Details sql.NullString \`json:"details"\` - UserId int64 \`json:"user_id"\` -} + type PublicUserDetailsSelect struct { + Details sql.NullString \`json:"details"\` + UserId int64 \`json:"user_id"\` + } -type PublicUserDetailsInsert struct { - Details sql.NullString \`json:"details"\` - UserId int64 \`json:"user_id"\` -} + type PublicUserDetailsInsert struct { + Details sql.NullString \`json:"details"\` + UserId int64 \`json:"user_id"\` + } -type PublicUserDetailsUpdate struct { - Details sql.NullString \`json:"details"\` - UserId sql.NullInt64 \`json:"user_id"\` -} + type PublicUserDetailsUpdate struct { + Details sql.NullString \`json:"details"\` + UserId sql.NullInt64 \`json:"user_id"\` + } -type PublicEmptySelect struct { + type PublicEmptySelect struct { -} + } -type PublicEmptyInsert struct { + type PublicEmptyInsert struct { -} + } -type PublicEmptyUpdate struct { + type PublicEmptyUpdate struct { -} + } -type PublicTableWithOtherTablesRowTypeSelect struct { - Col1 interface{} \`json:"col1"\` - Col2 interface{} \`json:"col2"\` -} + type PublicTableWithOtherTablesRowTypeSelect struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } -type PublicTableWithOtherTablesRowTypeInsert 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 PublicTableWithOtherTablesRowTypeUpdate struct { + Col1 interface{} \`json:"col1"\` + Col2 interface{} \`json:"col2"\` + } -type PublicCategorySelect struct { - Id int32 \`json:"id"\` - Name string \`json:"name"\` -} + type PublicTableWithPrimaryKeyOtherThanIdSelect struct { + Name sql.NullString \`json:"name"\` + OtherId int64 \`json:"other_id"\` + } -type PublicCategoryInsert struct { - Id sql.NullInt32 \`json:"id"\` - Name string \`json:"name"\` -} + type PublicTableWithPrimaryKeyOtherThanIdInsert struct { + Name sql.NullString \`json:"name"\` + OtherId sql.NullInt64 \`json:"other_id"\` + } + + type PublicTableWithPrimaryKeyOtherThanIdUpdate struct { + Name sql.NullString \`json:"name"\` + OtherId sql.NullInt64 \`json:"other_id"\` + } + + type PublicCategorySelect struct { + Id int32 \`json:"id"\` + Name string \`json:"name"\` + } + + type PublicCategoryInsert struct { + Id sql.NullInt32 \`json:"id"\` + Name string \`json:"name"\` + } + + type PublicCategoryUpdate struct { + Id sql.NullInt32 \`json:"id"\` + Name sql.NullString \`json:"name"\` + } + + type PublicMemesSelect struct { + Category sql.NullInt32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id int32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status sql.NullString \`json:"status"\` + } + + type PublicMemesInsert struct { + Category sql.NullInt32 \`json:"category"\` + CreatedAt string \`json:"created_at"\` + Id sql.NullInt32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name string \`json:"name"\` + Status sql.NullString \`json:"status"\` + } -type PublicCategoryUpdate struct { - Id sql.NullInt32 \`json:"id"\` - Name sql.NullString \`json:"name"\` -} + type PublicMemesUpdate struct { + Category sql.NullInt32 \`json:"category"\` + CreatedAt sql.NullString \`json:"created_at"\` + Id sql.NullInt32 \`json:"id"\` + Metadata interface{} \`json:"metadata"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` + } -type PublicMemesSelect struct { - Category sql.NullInt32 \`json:"category"\` - CreatedAt string \`json:"created_at"\` - Id int32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name string \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicTodosViewSelect struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId sql.NullInt64 \`json:"user-id"\` + } -type PublicMemesInsert struct { - Category sql.NullInt32 \`json:"category"\` - CreatedAt string \`json:"created_at"\` - Id sql.NullInt32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name string \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicUsersViewSelect struct { + Id sql.NullInt64 \`json:"id"\` + Name sql.NullString \`json:"name"\` + Status sql.NullString \`json:"status"\` + } -type PublicMemesUpdate struct { - Category sql.NullInt32 \`json:"category"\` - CreatedAt sql.NullString \`json:"created_at"\` - Id sql.NullInt32 \`json:"id"\` - Metadata interface{} \`json:"metadata"\` - Name sql.NullString \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicAViewSelect struct { + Id sql.NullInt64 \`json:"id"\` + } -type PublicTodosViewSelect struct { - Details sql.NullString \`json:"details"\` - Id sql.NullInt64 \`json:"id"\` - UserId sql.NullInt64 \`json:"user-id"\` -} + type PublicTodosMatviewSelect struct { + Details sql.NullString \`json:"details"\` + Id sql.NullInt64 \`json:"id"\` + UserId sql.NullInt64 \`json:"user-id"\` + } -type PublicUsersViewSelect struct { - Id sql.NullInt64 \`json:"id"\` - Name sql.NullString \`json:"name"\` - Status sql.NullString \`json:"status"\` -} + type PublicCompositeTypeWithArrayAttribute struct { + MyTextArray interface{} \`json:"my_text_array"\` + }" + `) +}) -type PublicAViewSelect struct { - Id sql.NullInt64 \`json:"id"\` -} +test('typegen: swift', async () => { + const { body } = await app.inject({ method: 'GET', path: '/generators/swift' }) + expect(body).toMatchInlineSnapshot(` + "import Foundation + import Supabase + + internal enum PublicSchema { + internal enum MemeStatus: String, Codable, Hashable, Sendable { + case new = "new" + case old = "old" + case retired = "retired" + } + internal enum UserStatus: String, Codable, Hashable, Sendable { + case active = "ACTIVE" + case inactive = "INACTIVE" + } + internal struct UsersSelect: Codable, Hashable, Sendable, Identifiable { + internal let id: Int64 + internal let name: String? + internal let status: UserStatus? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + internal struct UsersInsert: Codable, Hashable, Sendable, Identifiable { + internal let id: Int64? + internal let name: String? + internal let status: UserStatus? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + internal struct UsersUpdate: Codable, Hashable, Sendable, Identifiable { + internal let id: Int64? + internal let name: String? + internal let status: UserStatus? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + internal struct TodosSelect: Codable, Hashable, Sendable, Identifiable { + internal let details: String? + internal let id: Int64 + internal let userId: Int64 + internal enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + internal struct TodosInsert: Codable, Hashable, Sendable, Identifiable { + internal let details: String? + internal let id: Int64? + internal let userId: Int64 + internal enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + internal struct TodosUpdate: Codable, Hashable, Sendable, Identifiable { + internal let details: String? + internal let id: Int64? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + internal struct UsersAuditSelect: Codable, Hashable, Sendable, Identifiable { + internal let createdAt: String? + internal let id: Int64 + internal let previousValue: AnyJSON? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case id = "id" + case previousValue = "previous_value" + case userId = "user_id" + } + } + internal struct UsersAuditInsert: Codable, Hashable, Sendable, Identifiable { + internal let createdAt: String? + internal let id: Int64? + internal let previousValue: AnyJSON? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case id = "id" + case previousValue = "previous_value" + case userId = "user_id" + } + } + internal struct UsersAuditUpdate: Codable, Hashable, Sendable, Identifiable { + internal let createdAt: String? + internal let id: Int64? + internal let previousValue: AnyJSON? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case id = "id" + case previousValue = "previous_value" + case userId = "user_id" + } + } + internal struct UserDetailsSelect: Codable, Hashable, Sendable { + internal let details: String? + internal let userId: Int64 + internal enum CodingKeys: String, CodingKey { + case details = "details" + case userId = "user_id" + } + } + internal struct UserDetailsInsert: Codable, Hashable, Sendable { + internal let details: String? + internal let userId: Int64 + internal enum CodingKeys: String, CodingKey { + case details = "details" + case userId = "user_id" + } + } + internal struct UserDetailsUpdate: Codable, Hashable, Sendable { + internal let details: String? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case details = "details" + case userId = "user_id" + } + } + internal struct EmptySelect: Codable, Hashable, Sendable { + } + internal struct EmptyInsert: Codable, Hashable, Sendable { + } + internal struct EmptyUpdate: Codable, Hashable, Sendable { + } + internal struct TableWithOtherTablesRowTypeSelect: Codable, Hashable, Sendable { + internal let col1: UserDetailsSelect? + internal let col2: AViewSelect? + internal enum CodingKeys: String, CodingKey { + case col1 = "col1" + case col2 = "col2" + } + } + internal struct TableWithOtherTablesRowTypeInsert: Codable, Hashable, Sendable { + internal let col1: UserDetailsSelect? + internal let col2: AViewSelect? + internal enum CodingKeys: String, CodingKey { + case col1 = "col1" + case col2 = "col2" + } + } + internal struct TableWithOtherTablesRowTypeUpdate: Codable, Hashable, Sendable { + internal let col1: UserDetailsSelect? + internal let col2: AViewSelect? + internal enum CodingKeys: String, CodingKey { + case col1 = "col1" + case col2 = "col2" + } + } + internal struct TableWithPrimaryKeyOtherThanIdSelect: Codable, Hashable, Sendable, Identifiable { + internal var id: Int64 { otherId } + internal let name: String? + internal let otherId: Int64 + internal enum CodingKeys: String, CodingKey { + case name = "name" + case otherId = "other_id" + } + } + internal struct TableWithPrimaryKeyOtherThanIdInsert: Codable, Hashable, Sendable, Identifiable { + internal var id: Int64? { otherId } + internal let name: String? + internal let otherId: Int64? + internal enum CodingKeys: String, CodingKey { + case name = "name" + case otherId = "other_id" + } + } + internal struct TableWithPrimaryKeyOtherThanIdUpdate: Codable, Hashable, Sendable, Identifiable { + internal var id: Int64? { otherId } + internal let name: String? + internal let otherId: Int64? + internal enum CodingKeys: String, CodingKey { + case name = "name" + case otherId = "other_id" + } + } + internal struct CategorySelect: Codable, Hashable, Sendable { + internal let id: Int32 + internal let name: String + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + } + } + internal struct CategoryInsert: Codable, Hashable, Sendable { + internal let id: Int32? + internal let name: String + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + } + } + internal struct CategoryUpdate: Codable, Hashable, Sendable { + internal let id: Int32? + internal let name: String? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + } + } + internal struct MemesSelect: Codable, Hashable, Sendable { + internal let category: Int32? + internal let createdAt: String + internal let id: Int32 + internal let metadata: AnyJSON? + internal let name: String + internal let status: MemeStatus? + internal enum CodingKeys: String, CodingKey { + case category = "category" + case createdAt = "created_at" + case id = "id" + case metadata = "metadata" + case name = "name" + case status = "status" + } + } + internal struct MemesInsert: Codable, Hashable, Sendable { + internal let category: Int32? + internal let createdAt: String + internal let id: Int32? + internal let metadata: AnyJSON? + internal let name: String + internal let status: MemeStatus? + internal enum CodingKeys: String, CodingKey { + case category = "category" + case createdAt = "created_at" + case id = "id" + case metadata = "metadata" + case name = "name" + case status = "status" + } + } + internal struct MemesUpdate: Codable, Hashable, Sendable { + internal let category: Int32? + internal let createdAt: String? + internal let id: Int32? + internal let metadata: AnyJSON? + internal let name: String? + internal let status: MemeStatus? + internal enum CodingKeys: String, CodingKey { + case category = "category" + case createdAt = "created_at" + case id = "id" + case metadata = "metadata" + case name = "name" + case status = "status" + } + } + internal struct TodosViewSelect: Codable, Hashable, Sendable { + internal let details: String? + internal let id: Int64? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + internal struct UsersViewSelect: Codable, Hashable, Sendable { + internal let id: Int64? + internal let name: String? + internal let status: UserStatus? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + internal struct AViewSelect: Codable, Hashable, Sendable { + internal let id: Int64? + internal enum CodingKeys: String, CodingKey { + case id = "id" + } + } + internal struct TodosMatviewSelect: Codable, Hashable, Sendable { + internal let details: String? + internal let id: Int64? + internal let userId: Int64? + internal enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + internal struct CompositeTypeWithArrayAttribute: Codable, Hashable, Sendable { + internal let MyTextArray: AnyJSON + internal enum CodingKeys: String, CodingKey { + case MyTextArray = "my_text_array" + } + } + }" + `) +}) -type PublicTodosMatviewSelect struct { - Details sql.NullString \`json:"details"\` - Id sql.NullInt64 \`json:"id"\` - UserId sql.NullInt64 \`json:"user-id"\` -} +test('typegen: swift w/ public access control', async () => { + const { body } = await app.inject({ + method: 'GET', + path: '/generators/swift', + query: { access_control: 'public' }, + }) + expect(body).toMatchInlineSnapshot(` + "import Foundation + import Supabase -type PublicCompositeTypeWithArrayAttribute struct { - MyTextArray interface{} \`json:"my_text_array"\` -}" + public enum PublicSchema { + public enum MemeStatus: String, Codable, Hashable, Sendable { + case new = "new" + case old = "old" + case retired = "retired" + } + public enum UserStatus: String, Codable, Hashable, Sendable { + case active = "ACTIVE" + case inactive = "INACTIVE" + } + public struct UsersSelect: Codable, Hashable, Sendable, Identifiable { + public let id: Int64 + public let name: String? + public let status: UserStatus? + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + public struct UsersInsert: Codable, Hashable, Sendable, Identifiable { + public let id: Int64? + public let name: String? + public let status: UserStatus? + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + public struct UsersUpdate: Codable, Hashable, Sendable, Identifiable { + public let id: Int64? + public let name: String? + public let status: UserStatus? + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + public struct TodosSelect: Codable, Hashable, Sendable, Identifiable { + public let details: String? + public let id: Int64 + public let userId: Int64 + public enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + public struct TodosInsert: Codable, Hashable, Sendable, Identifiable { + public let details: String? + public let id: Int64? + public let userId: Int64 + public enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + public struct TodosUpdate: Codable, Hashable, Sendable, Identifiable { + public let details: String? + public let id: Int64? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + public struct UsersAuditSelect: Codable, Hashable, Sendable, Identifiable { + public let createdAt: String? + public let id: Int64 + public let previousValue: AnyJSON? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case id = "id" + case previousValue = "previous_value" + case userId = "user_id" + } + } + public struct UsersAuditInsert: Codable, Hashable, Sendable, Identifiable { + public let createdAt: String? + public let id: Int64? + public let previousValue: AnyJSON? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case id = "id" + case previousValue = "previous_value" + case userId = "user_id" + } + } + public struct UsersAuditUpdate: Codable, Hashable, Sendable, Identifiable { + public let createdAt: String? + public let id: Int64? + public let previousValue: AnyJSON? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case createdAt = "created_at" + case id = "id" + case previousValue = "previous_value" + case userId = "user_id" + } + } + public struct UserDetailsSelect: Codable, Hashable, Sendable { + public let details: String? + public let userId: Int64 + public enum CodingKeys: String, CodingKey { + case details = "details" + case userId = "user_id" + } + } + public struct UserDetailsInsert: Codable, Hashable, Sendable { + public let details: String? + public let userId: Int64 + public enum CodingKeys: String, CodingKey { + case details = "details" + case userId = "user_id" + } + } + public struct UserDetailsUpdate: Codable, Hashable, Sendable { + public let details: String? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case details = "details" + case userId = "user_id" + } + } + public struct EmptySelect: Codable, Hashable, Sendable { + } + public struct EmptyInsert: Codable, Hashable, Sendable { + } + public struct EmptyUpdate: Codable, Hashable, Sendable { + } + public struct TableWithOtherTablesRowTypeSelect: Codable, Hashable, Sendable { + public let col1: UserDetailsSelect? + public let col2: AViewSelect? + public enum CodingKeys: String, CodingKey { + case col1 = "col1" + case col2 = "col2" + } + } + public struct TableWithOtherTablesRowTypeInsert: Codable, Hashable, Sendable { + public let col1: UserDetailsSelect? + public let col2: AViewSelect? + public enum CodingKeys: String, CodingKey { + case col1 = "col1" + case col2 = "col2" + } + } + public struct TableWithOtherTablesRowTypeUpdate: Codable, Hashable, Sendable { + public let col1: UserDetailsSelect? + public let col2: AViewSelect? + public enum CodingKeys: String, CodingKey { + case col1 = "col1" + case col2 = "col2" + } + } + public struct TableWithPrimaryKeyOtherThanIdSelect: Codable, Hashable, Sendable, Identifiable { + public var id: Int64 { otherId } + public let name: String? + public let otherId: Int64 + public enum CodingKeys: String, CodingKey { + case name = "name" + case otherId = "other_id" + } + } + public struct TableWithPrimaryKeyOtherThanIdInsert: Codable, Hashable, Sendable, Identifiable { + public var id: Int64? { otherId } + public let name: String? + public let otherId: Int64? + public enum CodingKeys: String, CodingKey { + case name = "name" + case otherId = "other_id" + } + } + public struct TableWithPrimaryKeyOtherThanIdUpdate: Codable, Hashable, Sendable, Identifiable { + public var id: Int64? { otherId } + public let name: String? + public let otherId: Int64? + public enum CodingKeys: String, CodingKey { + case name = "name" + case otherId = "other_id" + } + } + public struct CategorySelect: Codable, Hashable, Sendable { + public let id: Int32 + public let name: String + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + } + } + public struct CategoryInsert: Codable, Hashable, Sendable { + public let id: Int32? + public let name: String + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + } + } + public struct CategoryUpdate: Codable, Hashable, Sendable { + public let id: Int32? + public let name: String? + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + } + } + public struct MemesSelect: Codable, Hashable, Sendable { + public let category: Int32? + public let createdAt: String + public let id: Int32 + public let metadata: AnyJSON? + public let name: String + public let status: MemeStatus? + public enum CodingKeys: String, CodingKey { + case category = "category" + case createdAt = "created_at" + case id = "id" + case metadata = "metadata" + case name = "name" + case status = "status" + } + } + public struct MemesInsert: Codable, Hashable, Sendable { + public let category: Int32? + public let createdAt: String + public let id: Int32? + public let metadata: AnyJSON? + public let name: String + public let status: MemeStatus? + public enum CodingKeys: String, CodingKey { + case category = "category" + case createdAt = "created_at" + case id = "id" + case metadata = "metadata" + case name = "name" + case status = "status" + } + } + public struct MemesUpdate: Codable, Hashable, Sendable { + public let category: Int32? + public let createdAt: String? + public let id: Int32? + public let metadata: AnyJSON? + public let name: String? + public let status: MemeStatus? + public enum CodingKeys: String, CodingKey { + case category = "category" + case createdAt = "created_at" + case id = "id" + case metadata = "metadata" + case name = "name" + case status = "status" + } + } + public struct TodosViewSelect: Codable, Hashable, Sendable { + public let details: String? + public let id: Int64? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + public struct UsersViewSelect: Codable, Hashable, Sendable { + public let id: Int64? + public let name: String? + public let status: UserStatus? + public enum CodingKeys: String, CodingKey { + case id = "id" + case name = "name" + case status = "status" + } + } + public struct AViewSelect: Codable, Hashable, Sendable { + public let id: Int64? + public enum CodingKeys: String, CodingKey { + case id = "id" + } + } + public struct TodosMatviewSelect: Codable, Hashable, Sendable { + public let details: String? + public let id: Int64? + public let userId: Int64? + public enum CodingKeys: String, CodingKey { + case details = "details" + case id = "id" + case userId = "user-id" + } + } + public struct CompositeTypeWithArrayAttribute: Codable, Hashable, Sendable { + public let MyTextArray: AnyJSON + public enum CodingKeys: String, CodingKey { + case MyTextArray = "my_text_array" + } + } + }" `) })