@supabase/postgrest-typegen 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,531 +0,0 @@
1
- import type {
2
- GeneratorMetadata,
3
- PostgresColumn,
4
- PostgresForeignTable,
5
- PostgresMaterializedView,
6
- PostgresTable,
7
- PostgresType,
8
- PostgresView,
9
- } from "../types.ts";
10
- import { compareStrings } from "../collation.ts";
11
-
12
- type Operation = "Select" | "Insert" | "Update";
13
- export type AccessControl = "internal" | "public" | "private" | "package";
14
-
15
- export interface GenerateSwiftOptions {
16
- /** Swift access-control level applied to generated declarations. Default `'internal'`. */
17
- accessControl?: AccessControl;
18
- }
19
-
20
- type SwiftGeneratorOptions = {
21
- accessControl: AccessControl;
22
- };
23
-
24
- type SwiftEnumCase = {
25
- formattedName: string;
26
- rawValue: string;
27
- };
28
-
29
- type SwiftEnum = {
30
- formattedEnumName: string;
31
- protocolConformances: string[];
32
- cases: SwiftEnumCase[];
33
- };
34
-
35
- type SwiftAttribute = {
36
- formattedAttributeName: string;
37
- formattedType: string;
38
- rawName: string;
39
- isIdentity: boolean;
40
- };
41
-
42
- type SwiftStruct = {
43
- formattedStructName: string;
44
- protocolConformances: string[];
45
- attributes: SwiftAttribute[];
46
- codingKeysEnum: SwiftEnum | undefined;
47
- };
48
-
49
- function formatForSwiftSchemaName(schema: string): string {
50
- return `${formatForSwiftTypeName(schema)}Schema`;
51
- }
52
-
53
- function pgEnumToSwiftEnum(pgEnum: PostgresType): SwiftEnum {
54
- return {
55
- formattedEnumName: formatForSwiftTypeName(pgEnum.name),
56
- protocolConformances: ["String", "Codable", "Hashable", "Sendable"],
57
- cases: pgEnum.enums.map((case_) => {
58
- return {
59
- formattedName: formatForSwiftPropertyName(case_),
60
- rawValue: case_,
61
- };
62
- }),
63
- };
64
- }
65
-
66
- function pgTypeToSwiftStruct(
67
- table:
68
- | PostgresTable
69
- | PostgresForeignTable
70
- | PostgresView
71
- | PostgresMaterializedView,
72
- columns: PostgresColumn[] | undefined,
73
- operation: Operation,
74
- {
75
- types,
76
- views,
77
- tables,
78
- }: { types: PostgresType[]; views: PostgresView[]; tables: PostgresTable[] },
79
- ): SwiftStruct {
80
- const columnEntries: SwiftAttribute[] =
81
- columns?.map((column) => {
82
- let nullable: boolean;
83
-
84
- if (operation === "Insert") {
85
- nullable =
86
- column.is_nullable ||
87
- column.is_identity ||
88
- column.is_generated ||
89
- !!column.default_value;
90
- } else if (operation === "Update") {
91
- nullable = true;
92
- } else {
93
- nullable = column.is_nullable;
94
- }
95
-
96
- return {
97
- rawName: column.name,
98
- formattedAttributeName: formatForSwiftPropertyName(column.name),
99
- formattedType: pgTypeToSwiftType(column.format, nullable, {
100
- types,
101
- views,
102
- tables,
103
- }),
104
- isIdentity: column.is_identity,
105
- };
106
- }) ?? [];
107
-
108
- return {
109
- formattedStructName: `${formatForSwiftTypeName(table.name)}${operation}`,
110
- attributes: columnEntries,
111
- protocolConformances: ["Codable", "Hashable", "Sendable"],
112
- codingKeysEnum: generateCodingKeysEnumFromAttributes(columnEntries),
113
- };
114
- }
115
-
116
- function generateCodingKeysEnumFromAttributes(
117
- attributes: SwiftAttribute[],
118
- ): SwiftEnum | undefined {
119
- return attributes.length > 0
120
- ? {
121
- formattedEnumName: "CodingKeys",
122
- protocolConformances: ["String", "CodingKey"],
123
- cases: attributes.map((attribute) => {
124
- return {
125
- formattedName: attribute.formattedAttributeName,
126
- rawValue: attribute.rawName,
127
- };
128
- }),
129
- }
130
- : undefined;
131
- }
132
-
133
- function pgCompositeTypeToSwiftStruct(
134
- type: PostgresType,
135
- {
136
- types,
137
- views,
138
- tables,
139
- }: { types: PostgresType[]; views: PostgresView[]; tables: PostgresTable[] },
140
- ): SwiftStruct {
141
- const typeWithRetrievedAttributes = {
142
- ...type,
143
- attributes: type.attributes.map((attribute) => {
144
- const type = types.find((type) => type.id === attribute.type_id);
145
- return {
146
- ...attribute,
147
- type,
148
- };
149
- }),
150
- };
151
-
152
- const attributeEntries: SwiftAttribute[] =
153
- typeWithRetrievedAttributes.attributes.map((attribute) => {
154
- return {
155
- formattedAttributeName: formatForSwiftTypeName(attribute.name),
156
- formattedType: pgTypeToSwiftType(attribute.type!.format, false, {
157
- types,
158
- views,
159
- tables,
160
- }),
161
- rawName: attribute.name,
162
- isIdentity: false,
163
- };
164
- });
165
-
166
- return {
167
- formattedStructName: formatForSwiftTypeName(type.name),
168
- attributes: attributeEntries,
169
- protocolConformances: ["Codable", "Hashable", "Sendable"],
170
- codingKeysEnum: generateCodingKeysEnumFromAttributes(attributeEntries),
171
- };
172
- }
173
-
174
- function generateProtocolConformances(protocols: string[]): string {
175
- return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
176
- }
177
-
178
- /**
179
- * Serializes a database-provided value as a Swift string literal. Backslashes
180
- * would otherwise start escape or interpolation sequences (`\(`), double
181
- * quotes would terminate the literal early, and line breaks or control
182
- * characters are not allowed in single-line literals, so all of them are
183
- * escaped per Swift's string literal grammar. U+2028 and U+2029 are legal in
184
- * Swift literals but are escaped anyway because other tooling mishandles
185
- * them. Swift unescapes at compile time, so raw values round-trip unchanged.
186
- */
187
- function swiftStringLiteral(value: string): string {
188
- let escaped = "";
189
- for (const character of value) {
190
- switch (character) {
191
- case "\\":
192
- escaped += "\\\\";
193
- break;
194
- case '"':
195
- escaped += '\\"';
196
- break;
197
- case "\t":
198
- escaped += "\\t";
199
- break;
200
- case "\n":
201
- escaped += "\\n";
202
- break;
203
- case "\r":
204
- escaped += "\\r";
205
- break;
206
- default: {
207
- const codePoint = character.codePointAt(0)!;
208
- escaped +=
209
- codePoint < 0x20 ||
210
- codePoint === 0x7f ||
211
- codePoint === 0x2028 ||
212
- codePoint === 0x2029
213
- ? `\\u{${codePoint.toString(16)}}`
214
- : character;
215
- }
216
- }
217
- }
218
- return `"${escaped}"`;
219
- }
220
-
221
- function generateEnum(
222
- enum_: SwiftEnum,
223
- { accessControl, level }: SwiftGeneratorOptions & { level: number },
224
- ): string[] {
225
- return [
226
- `${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(
227
- enum_.protocolConformances,
228
- )} {`,
229
- ...enum_.cases.map(
230
- (case_) =>
231
- `${ident(level + 1)}case ${case_.formattedName} = ${swiftStringLiteral(case_.rawValue)}`,
232
- ),
233
- `${ident(level)}}`,
234
- ];
235
- }
236
-
237
- function generateStruct(
238
- struct: SwiftStruct,
239
- { accessControl, level }: SwiftGeneratorOptions & { level: number },
240
- ): string[] {
241
- const identity = struct.attributes.find((column) => column.isIdentity);
242
-
243
- let protocolConformances = struct.protocolConformances;
244
- if (identity) {
245
- protocolConformances.push("Identifiable");
246
- }
247
-
248
- let output = [
249
- `${ident(level)}${accessControl} struct ${struct.formattedStructName}${generateProtocolConformances(
250
- struct.protocolConformances,
251
- )} {`,
252
- ];
253
-
254
- if (identity && identity.formattedAttributeName !== "id") {
255
- output.push(
256
- `${ident(level + 1)}${accessControl} var id: ${identity.formattedType} { ${identity.formattedAttributeName} }`,
257
- );
258
- }
259
-
260
- output.push(
261
- ...struct.attributes.map(
262
- (attribute) =>
263
- `${ident(level + 1)}${accessControl} let ${attribute.formattedAttributeName}: ${attribute.formattedType}`,
264
- ),
265
- );
266
-
267
- if (struct.codingKeysEnum) {
268
- output.push(
269
- ...generateEnum(struct.codingKeysEnum, {
270
- accessControl,
271
- level: level + 1,
272
- }),
273
- );
274
- }
275
-
276
- output.push(`${ident(level)}}`);
277
-
278
- return output;
279
- }
280
-
281
- /**
282
- * Emits tables/views/materialized views in `GeneratorMetadata` order. Pass
283
- * input pre-sorted with `sortGeneratorMetadata` for deterministic output.
284
- */
285
- export const generateSwift = (
286
- metadata: GeneratorMetadata,
287
- opts: GenerateSwiftOptions = {},
288
- ): string => {
289
- const {
290
- schemas,
291
- tables,
292
- foreignTables,
293
- views,
294
- materializedViews,
295
- columns,
296
- types,
297
- } = metadata;
298
- const { accessControl = "internal" } = opts;
299
- const columnsByTableId = Object.fromEntries<PostgresColumn[]>(
300
- [...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [
301
- t.id,
302
- [],
303
- ]),
304
- );
305
-
306
- for (const column of columns) {
307
- columnsByTableId[column.table_id]?.push(column);
308
- }
309
-
310
- let output = [
311
- "import Foundation",
312
- "import Supabase",
313
- "",
314
- ...schemas.flatMap((schema) => {
315
- const schemaTables = [...tables, ...foreignTables]
316
- .filter((table) => table.schema === schema.name)
317
- .sort(({ name: a }, { name: b }) => compareStrings(a, b));
318
-
319
- const schemaViews = [...views, ...materializedViews]
320
- .filter((table) => table.schema === schema.name)
321
- .sort(({ name: a }, { name: b }) => compareStrings(a, b));
322
-
323
- const schemaEnums = types.filter(
324
- (type) => type.schema === schema.name && type.enums.length > 0,
325
- );
326
-
327
- const schemaCompositeTypes = types.filter(
328
- (type) => type.schema === schema.name && type.attributes.length > 0,
329
- );
330
-
331
- return [
332
- `${accessControl} enum ${formatForSwiftSchemaName(schema.name)} {`,
333
- ...schemaEnums.flatMap((enum_) =>
334
- generateEnum(pgEnumToSwiftEnum(enum_), { accessControl, level: 1 }),
335
- ),
336
- ...schemaTables.flatMap((table) =>
337
- (["Select", "Insert", "Update"] as Operation[])
338
- .map((operation) =>
339
- pgTypeToSwiftStruct(
340
- table,
341
- columnsByTableId[table.id],
342
- operation,
343
- {
344
- types,
345
- views,
346
- tables,
347
- },
348
- ),
349
- )
350
- .flatMap((struct) =>
351
- generateStruct(struct, { accessControl, level: 1 }),
352
- ),
353
- ),
354
- ...schemaViews.flatMap((view) =>
355
- generateStruct(
356
- pgTypeToSwiftStruct(view, columnsByTableId[view.id], "Select", {
357
- types,
358
- views,
359
- tables,
360
- }),
361
- { accessControl, level: 1 },
362
- ),
363
- ),
364
- ...schemaCompositeTypes.flatMap((type) =>
365
- generateStruct(
366
- pgCompositeTypeToSwiftStruct(type, { types, views, tables }),
367
- {
368
- accessControl,
369
- level: 1,
370
- },
371
- ),
372
- ),
373
- "}",
374
- ];
375
- }),
376
- ];
377
-
378
- return `${output.join("\n")}\n`;
379
- };
380
-
381
- // TODO: Make this more robust. Currently doesn't handle range types - returns them as string.
382
- const pgTypeToSwiftType = (
383
- pgType: string,
384
- nullable: boolean,
385
- {
386
- types,
387
- views,
388
- tables,
389
- }: { types: PostgresType[]; views: PostgresView[]; tables: PostgresTable[] },
390
- ): string => {
391
- let swiftType: string;
392
-
393
- if (pgType === "bool") {
394
- swiftType = "Bool";
395
- } else if (pgType === "int2") {
396
- swiftType = "Int16";
397
- } else if (pgType === "int4") {
398
- swiftType = "Int32";
399
- } else if (pgType === "int8") {
400
- swiftType = "Int64";
401
- } else if (pgType === "float4") {
402
- swiftType = "Float";
403
- } else if (pgType === "float8") {
404
- swiftType = "Double";
405
- } else if (["numeric", "decimal"].includes(pgType)) {
406
- swiftType = "Decimal";
407
- } else if (pgType === "uuid") {
408
- swiftType = "UUID";
409
- } else if (
410
- [
411
- "bytea",
412
- "bpchar",
413
- "varchar",
414
- "date",
415
- "text",
416
- "citext",
417
- "time",
418
- "timetz",
419
- "timestamp",
420
- "timestamptz",
421
- "interval",
422
- "vector",
423
- ].includes(pgType)
424
- ) {
425
- swiftType = "String";
426
- } else if (["json", "jsonb"].includes(pgType)) {
427
- swiftType = "AnyJSON";
428
- // `void` and `record` are pseudo-types: Postgres rejects them as a column or
429
- // composite attribute type ("column has pseudo-type void"), and this
430
- // generator only renders columns and attributes, never functions. So these
431
- // two arms are unreachable today and deliberately untested. They are kept
432
- // because they are the mappings to use if Swift ever emits functions, which
433
- // is where these types do appear.
434
- } else if (pgType === "void") {
435
- swiftType = "Void";
436
- } else if (pgType === "record") {
437
- swiftType = "JSONObject";
438
- } else if (pgType.startsWith("_")) {
439
- swiftType = `[${pgTypeToSwiftType(pgType.substring(1), false, { types, views, tables })}]`;
440
- } else {
441
- const enumType = types.find(
442
- (type) => type.name === pgType && type.enums.length > 0,
443
- );
444
-
445
- const compositeTypes = [...types, ...views, ...tables].find(
446
- (type) => type.name === pgType,
447
- );
448
-
449
- if (enumType) {
450
- swiftType = `${formatForSwiftTypeName(enumType.name)}`;
451
- } else if (compositeTypes) {
452
- // Append a `Select` to the composite type, as that is how is named in the generated struct.
453
- swiftType = `${formatForSwiftTypeName(compositeTypes.name)}Select`;
454
- } else {
455
- swiftType = "AnyJSON";
456
- }
457
- }
458
-
459
- return `${swiftType}${nullable ? "?" : ""}`;
460
- };
461
-
462
- function ident(
463
- level: number,
464
- options: { width: number } = { width: 2 },
465
- ): string {
466
- return " ".repeat(level * options.width);
467
- }
468
-
469
- /**
470
- * Converts a Postgres name to PascalCase.
471
- *
472
- * @example
473
- * ```ts
474
- * formatForSwiftTypeName('pokedex') // Pokedex
475
- * formatForSwiftTypeName('pokemon_center') // PokemonCenter
476
- * formatForSwiftTypeName('victory-road') // VictoryRoad
477
- * formatForSwiftTypeName('pokemon league') // PokemonLeague
478
- * formatForSwiftTypeName('_key_id_context') // _KeyIdContext
479
- * ```
480
- */
481
- function formatForSwiftTypeName(name: string): string {
482
- // Preserve the initial underscore if it exists
483
- let prefix = "";
484
- if (name.startsWith("_")) {
485
- prefix = "_";
486
- name = name.slice(1); // Remove the initial underscore for processing
487
- }
488
-
489
- return (
490
- prefix +
491
- name
492
- .split(/[^a-zA-Z0-9]+/)
493
- .map((word) => {
494
- if (word) {
495
- return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
496
- } else {
497
- return "";
498
- }
499
- })
500
- .join("")
501
- );
502
- }
503
-
504
- const SWIFT_KEYWORDS = ["in", "default", "case"];
505
-
506
- /**
507
- * Converts a Postgres name to pascalCase.
508
- *
509
- * @example
510
- * ```ts
511
- * formatForSwiftTypeName('pokedex') // pokedex
512
- * formatForSwiftTypeName('pokemon_center') // pokemonCenter
513
- * formatForSwiftTypeName('victory-road') // victoryRoad
514
- * formatForSwiftTypeName('pokemon league') // pokemonLeague
515
- * ```
516
- */
517
- function formatForSwiftPropertyName(name: string): string {
518
- const propertyName = name
519
- .split(/[^a-zA-Z0-9]/)
520
- .map((word, index) => {
521
- const lowerWord = word.toLowerCase();
522
- return index !== 0
523
- ? lowerWord.charAt(0).toUpperCase() + lowerWord.slice(1)
524
- : lowerWord;
525
- })
526
- .join("");
527
-
528
- return SWIFT_KEYWORDS.includes(propertyName)
529
- ? `\`${propertyName}\``
530
- : propertyName;
531
- }