@supabase/postgrest-typegen 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/generation/constants.d.ts +9 -0
- package/dist/generation/constants.js +10 -0
- package/dist/generation/go.d.ts +6 -0
- package/dist/generation/go.js +233 -0
- package/dist/generation/index.d.ts +5 -0
- package/dist/generation/index.js +5 -0
- package/dist/generation/python.d.ts +6 -0
- package/dist/generation/python.js +339 -0
- package/dist/generation/swift.d.ts +11 -0
- package/dist/generation/swift.js +309 -0
- package/dist/generation/typescript.d.ts +32 -0
- package/dist/generation/typescript.js +796 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/introspection/index.d.ts +27 -0
- package/dist/introspection/index.js +70 -0
- package/dist/introspection/normalize.d.ts +31 -0
- package/dist/introspection/normalize.js +65 -0
- package/dist/introspection/relationships.d.ts +47 -0
- package/dist/introspection/relationships.js +85 -0
- package/dist/introspection/sql/columns.sql.d.ts +7 -0
- package/dist/introspection/sql/columns.sql.js +122 -0
- package/dist/introspection/sql/common.d.ts +16 -0
- package/dist/introspection/sql/common.js +1 -0
- package/dist/introspection/sql/foreign_tables.sql.d.ts +6 -0
- package/dist/introspection/sql/foreign_tables.sql.js +18 -0
- package/dist/introspection/sql/functions.sql.d.ts +5 -0
- package/dist/introspection/sql/functions.sql.js +148 -0
- package/dist/introspection/sql/helpers.d.ts +12 -0
- package/dist/introspection/sql/helpers.js +28 -0
- package/dist/introspection/sql/materialized_views.sql.d.ts +4 -0
- package/dist/introspection/sql/materialized_views.sql.js +19 -0
- package/dist/introspection/sql/primary_keys.sql.d.ts +4 -0
- package/dist/introspection/sql/primary_keys.sql.js +32 -0
- package/dist/introspection/sql/schemas.sql.d.ts +6 -0
- package/dist/introspection/sql/schemas.sql.js +24 -0
- package/dist/introspection/sql/table.sql.d.ts +4 -0
- package/dist/introspection/sql/table.sql.js +41 -0
- package/dist/introspection/sql/table_relationships.sql.d.ts +2 -0
- package/dist/introspection/sql/table_relationships.sql.js +49 -0
- package/dist/introspection/sql/types.sql.d.ts +5 -0
- package/dist/introspection/sql/types.sql.js +65 -0
- package/dist/introspection/sql/views.sql.d.ts +4 -0
- package/dist/introspection/sql/views.sql.js +20 -0
- package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -0
- package/dist/introspection/sql/views_key_dependencies.sql.js +195 -0
- package/dist/sort.d.ts +35 -0
- package/dist/sort.js +48 -0
- package/dist/types.d.ts +372 -0
- package/dist/types.js +207 -0
- package/package.json +87 -0
- package/src/generation/constants.ts +11 -0
- package/src/generation/go.ts +356 -0
- package/src/generation/index.ts +13 -0
- package/src/generation/python.ts +461 -0
- package/src/generation/swift.ts +481 -0
- package/src/generation/typescript.ts +1144 -0
- package/src/index.ts +4 -0
- package/src/introspection/index.ts +124 -0
- package/src/introspection/normalize.ts +71 -0
- package/src/introspection/relationships.ts +173 -0
- package/src/introspection/sql/columns.sql.ts +131 -0
- package/src/introspection/sql/common.ts +18 -0
- package/src/introspection/sql/foreign_tables.sql.ts +26 -0
- package/src/introspection/sql/functions.sql.ts +159 -0
- package/src/introspection/sql/helpers.ts +33 -0
- package/src/introspection/sql/materialized_views.sql.ts +25 -0
- package/src/introspection/sql/primary_keys.sql.ts +38 -0
- package/src/introspection/sql/schemas.sql.ts +32 -0
- package/src/introspection/sql/table.sql.ts +47 -0
- package/src/introspection/sql/table_relationships.sql.ts +53 -0
- package/src/introspection/sql/types.sql.ts +74 -0
- package/src/introspection/sql/views.sql.ts +26 -0
- package/src/introspection/sql/views_key_dependencies.sql.ts +199 -0
- package/src/sort.ts +102 -0
- package/src/types.ts +237 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits tables/views/materialized views in `GeneratorMetadata` order. Pass
|
|
3
|
+
* input pre-sorted with `sortGeneratorMetadata` for deterministic output.
|
|
4
|
+
*/
|
|
5
|
+
export const generatePython = ({ schemas, tables, views, materializedViews, columns, types, }) => {
|
|
6
|
+
const ctx = new PythonContext(types, columns, schemas);
|
|
7
|
+
// Used for efficient lookup of types by schema name
|
|
8
|
+
const schemasNames = new Set(schemas.map((schema) => schema.name));
|
|
9
|
+
const py_tables = tables.flatMap((table) => {
|
|
10
|
+
const py_class_and_methods = ctx.tableToClass(table);
|
|
11
|
+
return py_class_and_methods;
|
|
12
|
+
});
|
|
13
|
+
const composite_types = types
|
|
14
|
+
// We always include system schemas, so we need to filter out types that are not in the included schemas
|
|
15
|
+
.filter((type) => type.attributes.length > 0 && schemasNames.has(type.schema))
|
|
16
|
+
.map((type) => ctx.typeToClass(type));
|
|
17
|
+
const py_views = views.map((view) => ctx.viewToClass(view));
|
|
18
|
+
const py_matviews = materializedViews.map((matview) => ctx.matViewToClass(matview));
|
|
19
|
+
let output = `
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import datetime
|
|
23
|
+
import uuid
|
|
24
|
+
from typing import (
|
|
25
|
+
Annotated,
|
|
26
|
+
Any,
|
|
27
|
+
List,
|
|
28
|
+
Literal,
|
|
29
|
+
NotRequired,
|
|
30
|
+
Optional,
|
|
31
|
+
TypeAlias,
|
|
32
|
+
TypedDict,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from pydantic import BaseModel, Field, Json
|
|
36
|
+
|
|
37
|
+
${concatLines(Object.values(ctx.user_enums))}
|
|
38
|
+
|
|
39
|
+
${concatLines(py_tables)}
|
|
40
|
+
|
|
41
|
+
${concatLines(py_views)}
|
|
42
|
+
|
|
43
|
+
${concatLines(py_matviews)}
|
|
44
|
+
|
|
45
|
+
${concatLines(composite_types)}
|
|
46
|
+
|
|
47
|
+
`.trim();
|
|
48
|
+
return output;
|
|
49
|
+
};
|
|
50
|
+
class PythonContext {
|
|
51
|
+
types;
|
|
52
|
+
user_enums;
|
|
53
|
+
columns;
|
|
54
|
+
schemas;
|
|
55
|
+
constructor(types, columns, schemas) {
|
|
56
|
+
this.schemas = Object.fromEntries(schemas.map((schema) => [schema.name, schema]));
|
|
57
|
+
this.types = Object.fromEntries(types.map((type) => [type.name, type]));
|
|
58
|
+
this.columns = columns.reduce((acc, curr) => {
|
|
59
|
+
acc[curr.table_id] ??= [];
|
|
60
|
+
acc[curr.table_id].push(curr);
|
|
61
|
+
return acc;
|
|
62
|
+
}, {});
|
|
63
|
+
this.user_enums = Object.fromEntries(types
|
|
64
|
+
.filter((type) => type.enums.length > 0)
|
|
65
|
+
.map((type) => [type.name, new PythonEnum(type)]));
|
|
66
|
+
}
|
|
67
|
+
resolveTypeName(name) {
|
|
68
|
+
if (name in this.user_enums) {
|
|
69
|
+
return this.user_enums[name].name;
|
|
70
|
+
}
|
|
71
|
+
if (name in PY_TYPE_MAP) {
|
|
72
|
+
return PY_TYPE_MAP[name];
|
|
73
|
+
}
|
|
74
|
+
if (name in this.types) {
|
|
75
|
+
const type = this.types[name];
|
|
76
|
+
const schema = type.schema;
|
|
77
|
+
return `${formatForPyClassName(schema)}${formatForPyClassName(type.name)}`;
|
|
78
|
+
}
|
|
79
|
+
return "Any";
|
|
80
|
+
}
|
|
81
|
+
parsePgType(pg_type) {
|
|
82
|
+
if (pg_type.startsWith("_")) {
|
|
83
|
+
const inner_str = pg_type.slice(1);
|
|
84
|
+
const inner = this.parsePgType(inner_str);
|
|
85
|
+
return new PythonListType(inner);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const type_name = this.resolveTypeName(pg_type);
|
|
89
|
+
return new PythonSimpleType(type_name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
typeToClass(type) {
|
|
93
|
+
const types = Object.values(this.types);
|
|
94
|
+
const attributes = type.attributes.map((attribute) => {
|
|
95
|
+
const type = types.find((type) => type.id === attribute.type_id);
|
|
96
|
+
return {
|
|
97
|
+
...attribute,
|
|
98
|
+
type,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const attributeEntries = attributes.map((attribute) => {
|
|
102
|
+
const type = this.parsePgType(attribute.type.name);
|
|
103
|
+
return new PythonBaseModelAttr(attribute.name, type, false);
|
|
104
|
+
});
|
|
105
|
+
const schema = this.schemas[type.schema];
|
|
106
|
+
return new PythonBaseModel(type.name, schema, attributeEntries);
|
|
107
|
+
}
|
|
108
|
+
columnsToClassAttrs(table_id) {
|
|
109
|
+
const attrs = this.columns[table_id] ?? [];
|
|
110
|
+
return attrs.map((col) => {
|
|
111
|
+
const type = this.parsePgType(col.format);
|
|
112
|
+
return new PythonBaseModelAttr(col.name, type, col.is_nullable);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
columnsToDictAttrs(table_id, not_required) {
|
|
116
|
+
const attrs = this.columns[table_id] ?? [];
|
|
117
|
+
return attrs.map((col) => {
|
|
118
|
+
const type = this.parsePgType(col.format);
|
|
119
|
+
return new PythonTypedDictAttr(col.name, type, col.is_nullable, not_required ||
|
|
120
|
+
col.is_nullable ||
|
|
121
|
+
col.is_identity ||
|
|
122
|
+
col.default_value !== null);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
tableToClass(table) {
|
|
126
|
+
const schema = this.schemas[table.schema];
|
|
127
|
+
const select = new PythonBaseModel(table.name, schema, this.columnsToClassAttrs(table.id));
|
|
128
|
+
const insert = new PythonTypedDict(table.name, "Insert", schema, this.columnsToDictAttrs(table.id, false));
|
|
129
|
+
const update = new PythonTypedDict(table.name, "Update", schema, this.columnsToDictAttrs(table.id, true));
|
|
130
|
+
return [select, insert, update];
|
|
131
|
+
}
|
|
132
|
+
viewToClass(view) {
|
|
133
|
+
const attributes = this.columnsToClassAttrs(view.id);
|
|
134
|
+
return new PythonBaseModel(view.name, this.schemas[view.schema], attributes);
|
|
135
|
+
}
|
|
136
|
+
matViewToClass(matview) {
|
|
137
|
+
const attributes = this.columnsToClassAttrs(matview.id);
|
|
138
|
+
return new PythonBaseModel(matview.name, this.schemas[matview.schema], attributes);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
class PythonEnum {
|
|
142
|
+
name;
|
|
143
|
+
variants;
|
|
144
|
+
constructor(type) {
|
|
145
|
+
this.name = `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
|
|
146
|
+
this.variants = type.enums;
|
|
147
|
+
}
|
|
148
|
+
serialize() {
|
|
149
|
+
const variants = this.variants.map((item) => `"${item}"`).join(", ");
|
|
150
|
+
return `${this.name}: TypeAlias = Literal[${variants}]`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
class PythonSimpleType {
|
|
154
|
+
name;
|
|
155
|
+
constructor(name) {
|
|
156
|
+
this.name = name;
|
|
157
|
+
}
|
|
158
|
+
serialize() {
|
|
159
|
+
return this.name;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
class PythonListType {
|
|
163
|
+
inner;
|
|
164
|
+
constructor(inner) {
|
|
165
|
+
this.inner = inner;
|
|
166
|
+
}
|
|
167
|
+
serialize() {
|
|
168
|
+
return `List[${this.inner.serialize()}]`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
class PythonBaseModelAttr {
|
|
172
|
+
name;
|
|
173
|
+
pg_name;
|
|
174
|
+
py_type;
|
|
175
|
+
nullable;
|
|
176
|
+
constructor(name, py_type, nullable) {
|
|
177
|
+
this.name = formatForPyAttributeName(name);
|
|
178
|
+
this.pg_name = name;
|
|
179
|
+
this.py_type = py_type;
|
|
180
|
+
this.nullable = nullable;
|
|
181
|
+
}
|
|
182
|
+
serialize() {
|
|
183
|
+
const py_type = this.nullable
|
|
184
|
+
? `Optional[${this.py_type.serialize()}]`
|
|
185
|
+
: this.py_type.serialize();
|
|
186
|
+
return ` ${this.name}: ${py_type} = Field(alias="${this.pg_name}")`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
class PythonBaseModel {
|
|
190
|
+
name;
|
|
191
|
+
table_name;
|
|
192
|
+
schema;
|
|
193
|
+
class_attributes;
|
|
194
|
+
constructor(name, schema, class_attributes) {
|
|
195
|
+
this.schema = schema;
|
|
196
|
+
this.class_attributes = class_attributes;
|
|
197
|
+
this.table_name = name;
|
|
198
|
+
this.name = `${formatForPyClassName(schema.name)}${formatForPyClassName(name)}`;
|
|
199
|
+
}
|
|
200
|
+
serialize() {
|
|
201
|
+
const attributes = this.class_attributes.length > 0
|
|
202
|
+
? this.class_attributes.map((attr) => attr.serialize()).join("\n")
|
|
203
|
+
: " pass";
|
|
204
|
+
return `class ${this.name}(BaseModel):\n${attributes}`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
class PythonTypedDictAttr {
|
|
208
|
+
name;
|
|
209
|
+
pg_name;
|
|
210
|
+
py_type;
|
|
211
|
+
nullable;
|
|
212
|
+
not_required;
|
|
213
|
+
constructor(name, py_type, nullable, required) {
|
|
214
|
+
this.name = formatForPyAttributeName(name);
|
|
215
|
+
this.pg_name = name;
|
|
216
|
+
this.py_type = py_type;
|
|
217
|
+
this.nullable = nullable;
|
|
218
|
+
this.not_required = required;
|
|
219
|
+
}
|
|
220
|
+
serialize() {
|
|
221
|
+
const py_type = this.nullable
|
|
222
|
+
? `Optional[${this.py_type.serialize()}]`
|
|
223
|
+
: this.py_type.serialize();
|
|
224
|
+
const annotation = `Annotated[${py_type}, Field(alias="${this.pg_name}")]`;
|
|
225
|
+
const rhs = this.not_required ? `NotRequired[${annotation}]` : annotation;
|
|
226
|
+
return ` ${this.name}: ${rhs}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
class PythonTypedDict {
|
|
230
|
+
name;
|
|
231
|
+
table_name;
|
|
232
|
+
parent_class;
|
|
233
|
+
schema;
|
|
234
|
+
dict_attributes;
|
|
235
|
+
operation;
|
|
236
|
+
constructor(name, operation, schema, dict_attributes, parent_class = "BaseModel") {
|
|
237
|
+
this.schema = schema;
|
|
238
|
+
this.dict_attributes = dict_attributes;
|
|
239
|
+
this.table_name = name;
|
|
240
|
+
this.name = `${formatForPyClassName(schema.name)}${formatForPyClassName(name)}`;
|
|
241
|
+
this.parent_class = parent_class;
|
|
242
|
+
this.operation = operation;
|
|
243
|
+
}
|
|
244
|
+
serialize() {
|
|
245
|
+
const attributes = this.dict_attributes.length > 0
|
|
246
|
+
? this.dict_attributes.map((attr) => attr.serialize()).join("\n")
|
|
247
|
+
: " pass";
|
|
248
|
+
return `class ${this.name}${this.operation}(TypedDict):\n${attributes}`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function concatLines(items) {
|
|
252
|
+
return items.map((item) => item.serialize()).join("\n\n");
|
|
253
|
+
}
|
|
254
|
+
const PY_TYPE_MAP = {
|
|
255
|
+
// Bool
|
|
256
|
+
bool: "bool",
|
|
257
|
+
// Numbers
|
|
258
|
+
int2: "int",
|
|
259
|
+
int4: "int",
|
|
260
|
+
int8: "int",
|
|
261
|
+
float4: "float",
|
|
262
|
+
float8: "float",
|
|
263
|
+
numeric: "float",
|
|
264
|
+
// Strings
|
|
265
|
+
bytea: "bytes",
|
|
266
|
+
bpchar: "str",
|
|
267
|
+
varchar: "str",
|
|
268
|
+
string: "str",
|
|
269
|
+
date: "datetime.date",
|
|
270
|
+
text: "str",
|
|
271
|
+
citext: "str",
|
|
272
|
+
time: "datetime.time",
|
|
273
|
+
timetz: "datetime.time",
|
|
274
|
+
timestamp: "datetime.datetime",
|
|
275
|
+
timestamptz: "datetime.datetime",
|
|
276
|
+
uuid: "uuid.UUID",
|
|
277
|
+
vector: "list[Any]",
|
|
278
|
+
interval: "str",
|
|
279
|
+
// JSON
|
|
280
|
+
json: "Json[Any]",
|
|
281
|
+
jsonb: "Json[Any]",
|
|
282
|
+
// Range types (can be adjusted to more complex types if needed)
|
|
283
|
+
int4range: "str",
|
|
284
|
+
int4multirange: "str",
|
|
285
|
+
int8range: "str",
|
|
286
|
+
int8multirange: "str",
|
|
287
|
+
numrange: "str",
|
|
288
|
+
nummultirange: "str",
|
|
289
|
+
tsrange: "str",
|
|
290
|
+
tsmultirange: "str",
|
|
291
|
+
tstzrange: "str",
|
|
292
|
+
tstzmultirange: "str",
|
|
293
|
+
daterange: "str",
|
|
294
|
+
datemultirange: "str",
|
|
295
|
+
// Miscellaneous types
|
|
296
|
+
void: "None",
|
|
297
|
+
record: "dict[str, Any]",
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Converts a Postgres name to PascalCase.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* formatForPyTypeName('pokedex') // Pokedex
|
|
305
|
+
* formatForPyTypeName('pokemon_center') // PokemonCenter
|
|
306
|
+
* formatForPyTypeName('victory-road') // VictoryRoad
|
|
307
|
+
* formatForPyTypeName('pokemon league') // PokemonLeague
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
function formatForPyClassName(name) {
|
|
311
|
+
return name
|
|
312
|
+
.split(/[^a-zA-Z0-9]/)
|
|
313
|
+
.map((word) => {
|
|
314
|
+
if (word) {
|
|
315
|
+
return `${word[0].toUpperCase()}${word.slice(1)}`;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
.join("");
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Converts a Postgres name to snake_case.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* formatForPyTypeName('Pokedex') // pokedex
|
|
329
|
+
* formatForPyTypeName('PokemonCenter') // pokemon_enter
|
|
330
|
+
* formatForPyTypeName('victory-road') // victory_road
|
|
331
|
+
* formatForPyTypeName('pokemon league') // pokemon_league
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
function formatForPyAttributeName(name) {
|
|
335
|
+
return name
|
|
336
|
+
.split(/[^a-zA-Z0-9]+/) // Split on non-alphanumeric characters (like spaces, dashes, etc.)
|
|
337
|
+
.map((word) => word.toLowerCase()) // Convert each word to lowercase
|
|
338
|
+
.join("_"); // Join with underscores
|
|
339
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GeneratorMetadata } from "../types.ts";
|
|
2
|
+
export type AccessControl = "internal" | "public" | "private" | "package";
|
|
3
|
+
export interface GenerateSwiftOptions {
|
|
4
|
+
/** Swift access-control level applied to generated declarations. Default `'internal'`. */
|
|
5
|
+
accessControl?: AccessControl;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Emits tables/views/materialized views in `GeneratorMetadata` order. Pass
|
|
9
|
+
* input pre-sorted with `sortGeneratorMetadata` for deterministic output.
|
|
10
|
+
*/
|
|
11
|
+
export declare const generateSwift: (metadata: GeneratorMetadata, opts?: GenerateSwiftOptions) => string;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
function formatForSwiftSchemaName(schema) {
|
|
2
|
+
return `${formatForSwiftTypeName(schema)}Schema`;
|
|
3
|
+
}
|
|
4
|
+
function pgEnumToSwiftEnum(pgEnum) {
|
|
5
|
+
return {
|
|
6
|
+
formattedEnumName: formatForSwiftTypeName(pgEnum.name),
|
|
7
|
+
protocolConformances: ["String", "Codable", "Hashable", "Sendable"],
|
|
8
|
+
cases: pgEnum.enums.map((case_) => {
|
|
9
|
+
return {
|
|
10
|
+
formattedName: formatForSwiftPropertyName(case_),
|
|
11
|
+
rawValue: case_,
|
|
12
|
+
};
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function pgTypeToSwiftStruct(table, columns, operation, { types, views, tables, }) {
|
|
17
|
+
const columnEntries = columns?.map((column) => {
|
|
18
|
+
let nullable;
|
|
19
|
+
if (operation === "Insert") {
|
|
20
|
+
nullable =
|
|
21
|
+
column.is_nullable ||
|
|
22
|
+
column.is_identity ||
|
|
23
|
+
column.is_generated ||
|
|
24
|
+
!!column.default_value;
|
|
25
|
+
}
|
|
26
|
+
else if (operation === "Update") {
|
|
27
|
+
nullable = true;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
nullable = column.is_nullable;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
rawName: column.name,
|
|
34
|
+
formattedAttributeName: formatForSwiftPropertyName(column.name),
|
|
35
|
+
formattedType: pgTypeToSwiftType(column.format, nullable, {
|
|
36
|
+
types,
|
|
37
|
+
views,
|
|
38
|
+
tables,
|
|
39
|
+
}),
|
|
40
|
+
isIdentity: column.is_identity,
|
|
41
|
+
};
|
|
42
|
+
}) ?? [];
|
|
43
|
+
return {
|
|
44
|
+
formattedStructName: `${formatForSwiftTypeName(table.name)}${operation}`,
|
|
45
|
+
attributes: columnEntries,
|
|
46
|
+
protocolConformances: ["Codable", "Hashable", "Sendable"],
|
|
47
|
+
codingKeysEnum: generateCodingKeysEnumFromAttributes(columnEntries),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function generateCodingKeysEnumFromAttributes(attributes) {
|
|
51
|
+
return attributes.length > 0
|
|
52
|
+
? {
|
|
53
|
+
formattedEnumName: "CodingKeys",
|
|
54
|
+
protocolConformances: ["String", "CodingKey"],
|
|
55
|
+
cases: attributes.map((attribute) => {
|
|
56
|
+
return {
|
|
57
|
+
formattedName: attribute.formattedAttributeName,
|
|
58
|
+
rawValue: attribute.rawName,
|
|
59
|
+
};
|
|
60
|
+
}),
|
|
61
|
+
}
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
function pgCompositeTypeToSwiftStruct(type, { types, views, tables, }) {
|
|
65
|
+
const typeWithRetrievedAttributes = {
|
|
66
|
+
...type,
|
|
67
|
+
attributes: type.attributes.map((attribute) => {
|
|
68
|
+
const type = types.find((type) => type.id === attribute.type_id);
|
|
69
|
+
return {
|
|
70
|
+
...attribute,
|
|
71
|
+
type,
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
const attributeEntries = typeWithRetrievedAttributes.attributes.map((attribute) => {
|
|
76
|
+
return {
|
|
77
|
+
formattedAttributeName: formatForSwiftTypeName(attribute.name),
|
|
78
|
+
formattedType: pgTypeToSwiftType(attribute.type.format, false, {
|
|
79
|
+
types,
|
|
80
|
+
views,
|
|
81
|
+
tables,
|
|
82
|
+
}),
|
|
83
|
+
rawName: attribute.name,
|
|
84
|
+
isIdentity: false,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
formattedStructName: formatForSwiftTypeName(type.name),
|
|
89
|
+
attributes: attributeEntries,
|
|
90
|
+
protocolConformances: ["Codable", "Hashable", "Sendable"],
|
|
91
|
+
codingKeysEnum: generateCodingKeysEnumFromAttributes(attributeEntries),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function generateProtocolConformances(protocols) {
|
|
95
|
+
return protocols.length === 0 ? "" : `: ${protocols.join(", ")}`;
|
|
96
|
+
}
|
|
97
|
+
function generateEnum(enum_, { accessControl, level }) {
|
|
98
|
+
return [
|
|
99
|
+
`${ident(level)}${accessControl} enum ${enum_.formattedEnumName}${generateProtocolConformances(enum_.protocolConformances)} {`,
|
|
100
|
+
...enum_.cases.map((case_) => `${ident(level + 1)}case ${case_.formattedName} = "${case_.rawValue}"`),
|
|
101
|
+
`${ident(level)}}`,
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
function generateStruct(struct, { accessControl, level }) {
|
|
105
|
+
const identity = struct.attributes.find((column) => column.isIdentity);
|
|
106
|
+
let protocolConformances = struct.protocolConformances;
|
|
107
|
+
if (identity) {
|
|
108
|
+
protocolConformances.push("Identifiable");
|
|
109
|
+
}
|
|
110
|
+
let output = [
|
|
111
|
+
`${ident(level)}${accessControl} struct ${struct.formattedStructName}${generateProtocolConformances(struct.protocolConformances)} {`,
|
|
112
|
+
];
|
|
113
|
+
if (identity && identity.formattedAttributeName !== "id") {
|
|
114
|
+
output.push(`${ident(level + 1)}${accessControl} var id: ${identity.formattedType} { ${identity.formattedAttributeName} }`);
|
|
115
|
+
}
|
|
116
|
+
output.push(...struct.attributes.map((attribute) => `${ident(level + 1)}${accessControl} let ${attribute.formattedAttributeName}: ${attribute.formattedType}`));
|
|
117
|
+
if (struct.codingKeysEnum) {
|
|
118
|
+
output.push(...generateEnum(struct.codingKeysEnum, {
|
|
119
|
+
accessControl,
|
|
120
|
+
level: level + 1,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
output.push(`${ident(level)}}`);
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Emits tables/views/materialized views in `GeneratorMetadata` order. Pass
|
|
128
|
+
* input pre-sorted with `sortGeneratorMetadata` for deterministic output.
|
|
129
|
+
*/
|
|
130
|
+
export const generateSwift = (metadata, opts = {}) => {
|
|
131
|
+
const { schemas, tables, foreignTables, views, materializedViews, columns, types, } = metadata;
|
|
132
|
+
const { accessControl = "internal" } = opts;
|
|
133
|
+
const columnsByTableId = Object.fromEntries([...tables, ...foreignTables, ...views, ...materializedViews].map((t) => [
|
|
134
|
+
t.id,
|
|
135
|
+
[],
|
|
136
|
+
]));
|
|
137
|
+
columns
|
|
138
|
+
.filter((c) => c.table_id in columnsByTableId)
|
|
139
|
+
.forEach((c) => columnsByTableId[c.table_id].push(c));
|
|
140
|
+
let output = [
|
|
141
|
+
"import Foundation",
|
|
142
|
+
"import Supabase",
|
|
143
|
+
"",
|
|
144
|
+
...schemas.flatMap((schema) => {
|
|
145
|
+
const schemaTables = [...tables, ...foreignTables]
|
|
146
|
+
.filter((table) => table.schema === schema.name)
|
|
147
|
+
.sort(({ name: a }, { name: b }) => a.localeCompare(b));
|
|
148
|
+
const schemaViews = [...views, ...materializedViews]
|
|
149
|
+
.filter((table) => table.schema === schema.name)
|
|
150
|
+
.sort(({ name: a }, { name: b }) => a.localeCompare(b));
|
|
151
|
+
const schemaEnums = types.filter((type) => type.schema === schema.name && type.enums.length > 0);
|
|
152
|
+
const schemaCompositeTypes = types.filter((type) => type.schema === schema.name && type.attributes.length > 0);
|
|
153
|
+
return [
|
|
154
|
+
`${accessControl} enum ${formatForSwiftSchemaName(schema.name)} {`,
|
|
155
|
+
...schemaEnums.flatMap((enum_) => generateEnum(pgEnumToSwiftEnum(enum_), { accessControl, level: 1 })),
|
|
156
|
+
...schemaTables.flatMap((table) => ["Select", "Insert", "Update"]
|
|
157
|
+
.map((operation) => pgTypeToSwiftStruct(table, columnsByTableId[table.id], operation, {
|
|
158
|
+
types,
|
|
159
|
+
views,
|
|
160
|
+
tables,
|
|
161
|
+
}))
|
|
162
|
+
.flatMap((struct) => generateStruct(struct, { accessControl, level: 1 }))),
|
|
163
|
+
...schemaViews.flatMap((view) => generateStruct(pgTypeToSwiftStruct(view, columnsByTableId[view.id], "Select", {
|
|
164
|
+
types,
|
|
165
|
+
views,
|
|
166
|
+
tables,
|
|
167
|
+
}), { accessControl, level: 1 })),
|
|
168
|
+
...schemaCompositeTypes.flatMap((type) => generateStruct(pgCompositeTypeToSwiftStruct(type, { types, views, tables }), {
|
|
169
|
+
accessControl,
|
|
170
|
+
level: 1,
|
|
171
|
+
})),
|
|
172
|
+
"}",
|
|
173
|
+
];
|
|
174
|
+
}),
|
|
175
|
+
];
|
|
176
|
+
return output.join("\n");
|
|
177
|
+
};
|
|
178
|
+
// TODO: Make this more robust. Currently doesn't handle range types - returns them as string.
|
|
179
|
+
const pgTypeToSwiftType = (pgType, nullable, { types, views, tables, }) => {
|
|
180
|
+
let swiftType;
|
|
181
|
+
if (pgType === "bool") {
|
|
182
|
+
swiftType = "Bool";
|
|
183
|
+
}
|
|
184
|
+
else if (pgType === "int2") {
|
|
185
|
+
swiftType = "Int16";
|
|
186
|
+
}
|
|
187
|
+
else if (pgType === "int4") {
|
|
188
|
+
swiftType = "Int32";
|
|
189
|
+
}
|
|
190
|
+
else if (pgType === "int8") {
|
|
191
|
+
swiftType = "Int64";
|
|
192
|
+
}
|
|
193
|
+
else if (pgType === "float4") {
|
|
194
|
+
swiftType = "Float";
|
|
195
|
+
}
|
|
196
|
+
else if (pgType === "float8") {
|
|
197
|
+
swiftType = "Double";
|
|
198
|
+
}
|
|
199
|
+
else if (["numeric", "decimal"].includes(pgType)) {
|
|
200
|
+
swiftType = "Decimal";
|
|
201
|
+
}
|
|
202
|
+
else if (pgType === "uuid") {
|
|
203
|
+
swiftType = "UUID";
|
|
204
|
+
}
|
|
205
|
+
else if ([
|
|
206
|
+
"bytea",
|
|
207
|
+
"bpchar",
|
|
208
|
+
"varchar",
|
|
209
|
+
"date",
|
|
210
|
+
"text",
|
|
211
|
+
"citext",
|
|
212
|
+
"time",
|
|
213
|
+
"timetz",
|
|
214
|
+
"timestamp",
|
|
215
|
+
"timestamptz",
|
|
216
|
+
"interval",
|
|
217
|
+
"vector",
|
|
218
|
+
].includes(pgType)) {
|
|
219
|
+
swiftType = "String";
|
|
220
|
+
}
|
|
221
|
+
else if (["json", "jsonb"].includes(pgType)) {
|
|
222
|
+
swiftType = "AnyJSON";
|
|
223
|
+
}
|
|
224
|
+
else if (pgType === "void") {
|
|
225
|
+
swiftType = "Void";
|
|
226
|
+
}
|
|
227
|
+
else if (pgType === "record") {
|
|
228
|
+
swiftType = "JSONObject";
|
|
229
|
+
}
|
|
230
|
+
else if (pgType.startsWith("_")) {
|
|
231
|
+
swiftType = `[${pgTypeToSwiftType(pgType.substring(1), false, { types, views, tables })}]`;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const enumType = types.find((type) => type.name === pgType && type.enums.length > 0);
|
|
235
|
+
const compositeTypes = [...types, ...views, ...tables].find((type) => type.name === pgType);
|
|
236
|
+
if (enumType) {
|
|
237
|
+
swiftType = `${formatForSwiftTypeName(enumType.name)}`;
|
|
238
|
+
}
|
|
239
|
+
else if (compositeTypes) {
|
|
240
|
+
// Append a `Select` to the composite type, as that is how is named in the generated struct.
|
|
241
|
+
swiftType = `${formatForSwiftTypeName(compositeTypes.name)}Select`;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
swiftType = "AnyJSON";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return `${swiftType}${nullable ? "?" : ""}`;
|
|
248
|
+
};
|
|
249
|
+
function ident(level, options = { width: 2 }) {
|
|
250
|
+
return " ".repeat(level * options.width);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Converts a Postgres name to PascalCase.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* formatForSwiftTypeName('pokedex') // Pokedex
|
|
258
|
+
* formatForSwiftTypeName('pokemon_center') // PokemonCenter
|
|
259
|
+
* formatForSwiftTypeName('victory-road') // VictoryRoad
|
|
260
|
+
* formatForSwiftTypeName('pokemon league') // PokemonLeague
|
|
261
|
+
* formatForSwiftTypeName('_key_id_context') // _KeyIdContext
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
function formatForSwiftTypeName(name) {
|
|
265
|
+
// Preserve the initial underscore if it exists
|
|
266
|
+
let prefix = "";
|
|
267
|
+
if (name.startsWith("_")) {
|
|
268
|
+
prefix = "_";
|
|
269
|
+
name = name.slice(1); // Remove the initial underscore for processing
|
|
270
|
+
}
|
|
271
|
+
return (prefix +
|
|
272
|
+
name
|
|
273
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
274
|
+
.map((word) => {
|
|
275
|
+
if (word) {
|
|
276
|
+
return `${word[0].toUpperCase()}${word.slice(1)}`;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
return "";
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
.join(""));
|
|
283
|
+
}
|
|
284
|
+
const SWIFT_KEYWORDS = ["in", "default", "case"];
|
|
285
|
+
/**
|
|
286
|
+
* Converts a Postgres name to pascalCase.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* formatForSwiftTypeName('pokedex') // pokedex
|
|
291
|
+
* formatForSwiftTypeName('pokemon_center') // pokemonCenter
|
|
292
|
+
* formatForSwiftTypeName('victory-road') // victoryRoad
|
|
293
|
+
* formatForSwiftTypeName('pokemon league') // pokemonLeague
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
function formatForSwiftPropertyName(name) {
|
|
297
|
+
const propertyName = name
|
|
298
|
+
.split(/[^a-zA-Z0-9]/)
|
|
299
|
+
.map((word, index) => {
|
|
300
|
+
const lowerWord = word.toLowerCase();
|
|
301
|
+
return index !== 0
|
|
302
|
+
? lowerWord.charAt(0).toUpperCase() + lowerWord.slice(1)
|
|
303
|
+
: lowerWord;
|
|
304
|
+
})
|
|
305
|
+
.join("");
|
|
306
|
+
return SWIFT_KEYWORDS.includes(propertyName)
|
|
307
|
+
? `\`${propertyName}\``
|
|
308
|
+
: propertyName;
|
|
309
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { GeneratorMetadata, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
|
|
2
|
+
export interface GenerateTypescriptOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Emit `isOneToOne` on each relationship entry. Mirrors postgres-meta's
|
|
5
|
+
* `detect_one_to_one_relationships` query flag. Default `false`.
|
|
6
|
+
*/
|
|
7
|
+
detectOneToOneRelationships?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* When set, emit `__InternalSupabase.PostgrestVersion` so `createClient`
|
|
10
|
+
* can be instantiated with the right options automatically.
|
|
11
|
+
*/
|
|
12
|
+
postgrestVersion?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Schema treated as the default for the generated `Tables`/`Enums`/etc.
|
|
15
|
+
* helper types. Replaces postgres-meta's `GENERATE_TYPES_DEFAULT_SCHEMA`
|
|
16
|
+
* env read. Default `'public'`.
|
|
17
|
+
*/
|
|
18
|
+
defaultSchema?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Does not re-sort collections; pass input pre-sorted with
|
|
22
|
+
* `sortGeneratorMetadata` first. Still merge-sorts tables+foreign tables and
|
|
23
|
+
* views+materialized views per schema, since that cross-collection grouping
|
|
24
|
+
* can't be expressed by the single-collection `sortGeneratorMetadata` pass.
|
|
25
|
+
*/
|
|
26
|
+
export declare const generateTypescript: (metadata: GeneratorMetadata, opts?: GenerateTypescriptOptions) => Promise<string>;
|
|
27
|
+
export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string, { types, schemas, tables, views, }: {
|
|
28
|
+
types: PostgresType[];
|
|
29
|
+
schemas: PostgresSchema[];
|
|
30
|
+
tables: PostgresTable[];
|
|
31
|
+
views: PostgresView[];
|
|
32
|
+
}, typeSchema?: string) => string;
|