Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(typegen): swift generator #790

Merged
merged 2 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Helpers:
- [ ] `/generators`
- [ ] GET `/openapi`: Generate Open API
- [ ] GET `/typescript`: Generate Typescript types
- [ ] GET `/swift`: Generate Swift types (beta)

## Quickstart

Expand Down Expand Up @@ -105,6 +106,7 @@ where `<lang>` is one of:

- `typescript`
- `go`
- `swift` (beta)

To use your own database connection string instead of the provided test database, run:
`PG_META_DB_URL=postgresql://postgres:postgres@localhost:5432/postgres npm run gen:types:<lang>`
Expand Down
6 changes: 4 additions & 2 deletions src/server/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ 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 GENERATE_TYPES_SWIFT_ACCESS_CONTROL = process.env
.PG_META_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,
Expand Down
151 changes: 89 additions & 62 deletions src/server/templates/swift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
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'
Expand Down Expand Up @@ -57,7 +58,7 @@ function pgEnumToSwiftEnum(pgEnum: PostgresType): SwiftEnum {
}

function pgTypeToSwiftStruct(
table: PostgresTable | PostgresView | PostgresMaterializedView,
table: PostgresTable | PostgresForeignTable | PostgresView | PostgresMaterializedView,
columns: PostgresColumn[] | undefined,
operation: Operation,
{
Expand Down Expand Up @@ -205,75 +206,80 @@ function generateStruct(
export const apply = async ({
schemas,
tables,
foreignTables,
views,
materializedViews,
columns,
types,
accessControl,
}: GeneratorMetadata & SwiftGeneratorOptions): Promise<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<string, PostgresColumn[]>
)

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 columnsByTableId = Object.fromEntries<PostgresColumn[]>(
[...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []])
)

const swiftStructForMaterializedViews = materializedViews.map((materializedView) =>
pgTypeToSwiftStruct(materializedView, columnsByTableId[materializedView.id], 'Select', {
types,
views,
tables,
})
)

const swiftStructForCompositeTypes = compositeTypes.map((type) =>
pgCompositeTypeToSwiftStruct(type, { types, views, tables })
)
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.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 })
),
'}',
]),
...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')
Expand Down Expand Up @@ -360,15 +366,34 @@ function ident(level: number, options: { width: number } = { width: 2 }): string
* formatForSwiftTypeName('pokemon_center') // PokemonCenter
* formatForSwiftTypeName('victory-road') // VictoryRoad
* formatForSwiftTypeName('pokemon league') // PokemonLeague
* formatForSwiftTypeName('_key_id_context') // _KeyIdContext
* ```
*/
function formatForSwiftTypeName(name: string): string {
return name
.split(/[^a-zA-Z0-9]/)
.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`)
.join('')
// 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']

/**
* Converts a Postgres name to pascalCase.
*
Expand All @@ -381,11 +406,13 @@ function formatForSwiftTypeName(name: string): string {
* ```
*/
function formatForSwiftPropertyName(name: string): string {
return name
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
}
Loading