Skip to content

Commit f39902d

Browse files
authored
fix(typegen): swift generator (#790)
* fix(typegen): swift generator * chore: add swift type generation to README
1 parent bc156de commit f39902d

File tree

4 files changed

+424
-333
lines changed

4 files changed

+424
-333
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Helpers:
6767
- [ ] `/generators`
6868
- [ ] GET `/openapi`: Generate Open API
6969
- [ ] GET `/typescript`: Generate Typescript types
70+
- [ ] GET `/swift`: Generate Swift types (beta)
7071

7172
## Quickstart
7273

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

106107
- `typescript`
107108
- `go`
109+
- `swift` (beta)
108110

109111
To use your own database connection string instead of the provided test database, run:
110112
`PG_META_DB_URL=postgresql://postgres:postgres@localhost:5432/postgres npm run gen:types:<lang>`

src/server/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ export const GENERATE_TYPES_INCLUDED_SCHEMAS = GENERATE_TYPES
4141
: []
4242
export const GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS =
4343
process.env.PG_META_GENERATE_TYPES_DETECT_ONE_TO_ONE_RELATIONSHIPS === 'true'
44-
export const GENERATE_TYPES_SWIFT_ACCESS_CONTROL =
45-
(process.env.PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL as AccessControl) || 'internal'
44+
export const GENERATE_TYPES_SWIFT_ACCESS_CONTROL = process.env
45+
.PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL
46+
? (process.env.PG_META_GENERATE_TYPES_SWIFT_ACCESS_CONTROL as AccessControl)
47+
: 'internal'
4648
export const DEFAULT_POOL_CONFIG: PoolConfig = {
4749
max: 1,
4850
connectionTimeoutMillis: PG_CONN_TIMEOUT_SECS * 1000,

src/server/templates/swift.ts

Lines changed: 89 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
PostgresView,
1010
} from '../../lib/index.js'
1111
import type { GeneratorMetadata } from '../../lib/generators.js'
12+
import { PostgresForeignTable } from '../../lib/types.js'
1213

1314
type Operation = 'Select' | 'Insert' | 'Update'
1415
export type AccessControl = 'internal' | 'public' | 'private' | 'package'
@@ -57,7 +58,7 @@ function pgEnumToSwiftEnum(pgEnum: PostgresType): SwiftEnum {
5758
}
5859

5960
function pgTypeToSwiftStruct(
60-
table: PostgresTable | PostgresView | PostgresMaterializedView,
61+
table: PostgresTable | PostgresForeignTable | PostgresView | PostgresMaterializedView,
6162
columns: PostgresColumn[] | undefined,
6263
operation: Operation,
6364
{
@@ -205,75 +206,80 @@ function generateStruct(
205206
export const apply = async ({
206207
schemas,
207208
tables,
209+
foreignTables,
208210
views,
209211
materializedViews,
210212
columns,
211213
types,
212214
accessControl,
213215
}: GeneratorMetadata & SwiftGeneratorOptions): Promise<string> => {
214-
const columnsByTableId = columns
215-
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
216-
.reduce(
217-
(acc, curr) => {
218-
acc[curr.table_id] ??= []
219-
acc[curr.table_id].push(curr)
220-
return acc
221-
},
222-
{} as Record<string, PostgresColumn[]>
223-
)
224-
225-
const compositeTypes = types.filter((type) => type.attributes.length > 0)
226-
const enums = types
227-
.filter((type) => type.enums.length > 0)
228-
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
229-
230-
const swiftEnums = enums.map((enum_) => {
231-
return { schema: enum_.schema, enum_: pgEnumToSwiftEnum(enum_) }
232-
})
233-
234-
const swiftStructForTables = tables.flatMap((table) =>
235-
(['Select', 'Insert', 'Update'] as Operation[]).map((operation) =>
236-
pgTypeToSwiftStruct(table, columnsByTableId[table.id], operation, { types, views, tables })
237-
)
238-
)
239-
240-
const swiftStructForViews = views.map((view) =>
241-
pgTypeToSwiftStruct(view, columnsByTableId[view.id], 'Select', { types, views, tables })
216+
const columnsByTableId = Object.fromEntries<PostgresColumn[]>(
217+
[...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [t.id, []])
242218
)
243219

244-
const swiftStructForMaterializedViews = materializedViews.map((materializedView) =>
245-
pgTypeToSwiftStruct(materializedView, columnsByTableId[materializedView.id], 'Select', {
246-
types,
247-
views,
248-
tables,
249-
})
250-
)
251-
252-
const swiftStructForCompositeTypes = compositeTypes.map((type) =>
253-
pgCompositeTypeToSwiftStruct(type, { types, views, tables })
254-
)
220+
columns
221+
.filter((c) => c.table_id in columnsByTableId)
222+
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
223+
.forEach((c) => columnsByTableId[c.table_id].push(c))
255224

256225
let output = [
257226
'import Foundation',
258227
'import Supabase',
259228
'',
260-
...schemas.flatMap((schema) => [
261-
`${accessControl} enum ${formatForSwiftSchemaName(schema.name)} {`,
262-
...swiftEnums.flatMap(({ enum_ }) => generateEnum(enum_, { accessControl, level: 1 })),
263-
...swiftStructForTables.flatMap((struct) =>
264-
generateStruct(struct, { accessControl, level: 1 })
265-
),
266-
...swiftStructForViews.flatMap((struct) =>
267-
generateStruct(struct, { accessControl, level: 1 })
268-
),
269-
...swiftStructForMaterializedViews.flatMap((struct) =>
270-
generateStruct(struct, { accessControl, level: 1 })
271-
),
272-
...swiftStructForCompositeTypes.flatMap((struct) =>
273-
generateStruct(struct, { accessControl, level: 1 })
274-
),
275-
'}',
276-
]),
229+
...schemas
230+
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
231+
.flatMap((schema) => {
232+
const schemaTables = [...tables, ...foreignTables]
233+
.filter((table) => table.schema === schema.name)
234+
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
235+
236+
const schemaViews = [...views, ...materializedViews]
237+
.filter((table) => table.schema === schema.name)
238+
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
239+
240+
const schemaEnums = types
241+
.filter((type) => type.schema === schema.name && type.enums.length > 0)
242+
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
243+
244+
const schemaCompositeTypes = types
245+
.filter((type) => type.schema === schema.name && type.attributes.length > 0)
246+
.sort(({ name: a }, { name: b }) => a.localeCompare(b))
247+
248+
return [
249+
`${accessControl} enum ${formatForSwiftSchemaName(schema.name)} {`,
250+
...schemaEnums.flatMap((enum_) =>
251+
generateEnum(pgEnumToSwiftEnum(enum_), { accessControl, level: 1 })
252+
),
253+
...schemaTables.flatMap((table) =>
254+
(['Select', 'Insert', 'Update'] as Operation[])
255+
.map((operation) =>
256+
pgTypeToSwiftStruct(table, columnsByTableId[table.id], operation, {
257+
types,
258+
views,
259+
tables,
260+
})
261+
)
262+
.flatMap((struct) => generateStruct(struct, { accessControl, level: 1 }))
263+
),
264+
...schemaViews.flatMap((view) =>
265+
generateStruct(
266+
pgTypeToSwiftStruct(view, columnsByTableId[view.id], 'Select', {
267+
types,
268+
views,
269+
tables,
270+
}),
271+
{ accessControl, level: 1 }
272+
)
273+
),
274+
...schemaCompositeTypes.flatMap((type) =>
275+
generateStruct(pgCompositeTypeToSwiftStruct(type, { types, views, tables }), {
276+
accessControl,
277+
level: 1,
278+
})
279+
),
280+
'}',
281+
]
282+
}),
277283
]
278284

279285
return output.join('\n')
@@ -360,15 +366,34 @@ function ident(level: number, options: { width: number } = { width: 2 }): string
360366
* formatForSwiftTypeName('pokemon_center') // PokemonCenter
361367
* formatForSwiftTypeName('victory-road') // VictoryRoad
362368
* formatForSwiftTypeName('pokemon league') // PokemonLeague
369+
* formatForSwiftTypeName('_key_id_context') // _KeyIdContext
363370
* ```
364371
*/
365372
function formatForSwiftTypeName(name: string): string {
366-
return name
367-
.split(/[^a-zA-Z0-9]/)
368-
.map((word) => `${word[0].toUpperCase()}${word.slice(1)}`)
369-
.join('')
373+
// Preserve the initial underscore if it exists
374+
let prefix = ''
375+
if (name.startsWith('_')) {
376+
prefix = '_'
377+
name = name.slice(1) // Remove the initial underscore for processing
378+
}
379+
380+
return (
381+
prefix +
382+
name
383+
.split(/[^a-zA-Z0-9]+/)
384+
.map((word) => {
385+
if (word) {
386+
return `${word[0].toUpperCase()}${word.slice(1)}`
387+
} else {
388+
return ''
389+
}
390+
})
391+
.join('')
392+
)
370393
}
371394

395+
const SWIFT_KEYWORDS = ['in', 'default']
396+
372397
/**
373398
* Converts a Postgres name to pascalCase.
374399
*
@@ -381,11 +406,13 @@ function formatForSwiftTypeName(name: string): string {
381406
* ```
382407
*/
383408
function formatForSwiftPropertyName(name: string): string {
384-
return name
409+
const propertyName = name
385410
.split(/[^a-zA-Z0-9]/)
386411
.map((word, index) => {
387412
const lowerWord = word.toLowerCase()
388413
return index !== 0 ? lowerWord.charAt(0).toUpperCase() + lowerWord.slice(1) : lowerWord
389414
})
390415
.join('')
416+
417+
return SWIFT_KEYWORDS.includes(propertyName) ? `\`${propertyName}\`` : propertyName
391418
}

0 commit comments

Comments
 (0)