Skip to content

Commit

Permalink
fix(typegen): swift generator (#790)
Browse files Browse the repository at this point in the history
* fix(typegen): swift generator

* chore: add swift type generation to README
  • Loading branch information
grdsdev authored Jul 2, 2024
1 parent bc156de commit f39902d
Show file tree
Hide file tree
Showing 4 changed files with 424 additions and 333 deletions.
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

0 comments on commit f39902d

Please sign in to comment.