@supabase/postgrest-typegen 0.2.4 → 0.3.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.
- package/README.md +17 -7
- package/dist/generation/go.js +2 -3
- package/dist/generation/python.js +21 -13
- package/dist/generation/swift.js +4 -4
- package/dist/generation/typescript.d.ts +5 -3
- package/dist/generation/typescript.js +108 -96
- package/package.json +13 -3
- package/src/generation/go.ts +2 -3
- package/src/generation/python.ts +24 -13
- package/src/generation/swift.ts +4 -4
- package/src/generation/typescript.ts +123 -98
package/README.md
CHANGED
|
@@ -113,19 +113,27 @@ breaks at read time.
|
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
115
|
import {
|
|
116
|
-
generateTypescript, // async (
|
|
116
|
+
generateTypescript, // async (formats its output)
|
|
117
117
|
generateGo,
|
|
118
118
|
generatePython,
|
|
119
119
|
generateSwift,
|
|
120
120
|
} from "@supabase/postgrest-typegen/generation";
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
| Function | Options
|
|
124
|
-
| -------------------- |
|
|
125
|
-
| `generateTypescript` | `{ detectOneToOneRelationships?, postgrestVersion?, defaultSchema? }`
|
|
126
|
-
| `generateGo` | —
|
|
127
|
-
| `generatePython` | —
|
|
128
|
-
| `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }`
|
|
123
|
+
| Function | Options |
|
|
124
|
+
| -------------------- | ------------------------------------------------------------------------------ |
|
|
125
|
+
| `generateTypescript` | `{ detectOneToOneRelationships?, postgrestVersion?, defaultSchema?, format? }` |
|
|
126
|
+
| `generateGo` | — |
|
|
127
|
+
| `generatePython` | — |
|
|
128
|
+
| `generateSwift` | `{ accessControl?: 'internal' \| 'public' \| 'private' \| 'package' }` |
|
|
129
|
+
|
|
130
|
+
`generateTypescript` formats its output with `oxfmt` unless `format` is
|
|
131
|
+
supplied. `oxfmt` is an optional peer dependency, imported only when that
|
|
132
|
+
default runs: install it to use the default formatter, or pass your own
|
|
133
|
+
`format` (an identity function, for byte-identical unformatted output, or a
|
|
134
|
+
worker-pool-backed formatter) and it is never loaded or needed. Calling
|
|
135
|
+
`generateTypescript` without `format` when `oxfmt` is missing throws an error
|
|
136
|
+
saying so.
|
|
129
137
|
|
|
130
138
|
## Installation
|
|
131
139
|
|
|
@@ -134,6 +142,8 @@ Not yet published; consumed in-repo for now (`packages/postgrest-typegen`).
|
|
|
134
142
|
```bash
|
|
135
143
|
# pg is a peer of your application, not bundled here
|
|
136
144
|
npm install pg
|
|
145
|
+
# only if you rely on generateTypescript's default formatter
|
|
146
|
+
npm install oxfmt@0.66.0
|
|
137
147
|
```
|
|
138
148
|
|
|
139
149
|
## Releasing
|
package/dist/generation/go.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const generateGo = ({ schemas, tables, views, materializedViews, columns, types, }) => {
|
|
6
6
|
const columnsByTableId = columns.reduce((acc, curr) => {
|
|
7
|
-
acc[curr.table_id] ??= [];
|
|
8
|
-
acc[curr.table_id].push(curr);
|
|
7
|
+
(acc[curr.table_id] ??= []).push(curr);
|
|
9
8
|
return acc;
|
|
10
9
|
}, {});
|
|
11
10
|
const compositeTypes = types.filter((type) => type.attributes.length > 0);
|
|
@@ -50,7 +49,7 @@ function formatForGoTypeName(name) {
|
|
|
50
49
|
.split(/[^a-zA-Z0-9]/)
|
|
51
50
|
.map((word) => {
|
|
52
51
|
if (word) {
|
|
53
|
-
return `${word
|
|
52
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
54
53
|
}
|
|
55
54
|
else {
|
|
56
55
|
return "";
|
|
@@ -55,8 +55,7 @@ class PythonContext {
|
|
|
55
55
|
this.schemas = Object.fromEntries(schemas.map((schema) => [schema.name, schema]));
|
|
56
56
|
this.types = Object.fromEntries(types.map((type) => [qualifiedTypeName(type.schema, type.name), type]));
|
|
57
57
|
this.columns = columns.reduce((acc, curr) => {
|
|
58
|
-
acc[curr.table_id] ??= [];
|
|
59
|
-
acc[curr.table_id].push(curr);
|
|
58
|
+
(acc[curr.table_id] ??= []).push(curr);
|
|
60
59
|
return acc;
|
|
61
60
|
}, {});
|
|
62
61
|
this.user_enums = Object.fromEntries(types
|
|
@@ -66,16 +65,25 @@ class PythonContext {
|
|
|
66
65
|
new PythonEnum(type),
|
|
67
66
|
]));
|
|
68
67
|
}
|
|
68
|
+
getSchema(name) {
|
|
69
|
+
const schema = this.schemas[name];
|
|
70
|
+
if (!schema) {
|
|
71
|
+
throw new Error(`Schema ${JSON.stringify(name)} is missing from the generator metadata`);
|
|
72
|
+
}
|
|
73
|
+
return schema;
|
|
74
|
+
}
|
|
69
75
|
resolveTypeName(name, typeSchema) {
|
|
70
76
|
const qualified = qualifiedTypeName(typeSchema, name);
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
const userEnum = this.user_enums[qualified];
|
|
78
|
+
if (userEnum) {
|
|
79
|
+
return userEnum.name;
|
|
73
80
|
}
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
const mappedType = PY_TYPE_MAP[name];
|
|
82
|
+
if (mappedType !== undefined) {
|
|
83
|
+
return mappedType;
|
|
76
84
|
}
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
const type = this.types[qualified];
|
|
86
|
+
if (type) {
|
|
79
87
|
return `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
|
|
80
88
|
}
|
|
81
89
|
return "Any";
|
|
@@ -108,7 +116,7 @@ class PythonContext {
|
|
|
108
116
|
// Postgres, so every field is inherently nullable.
|
|
109
117
|
return new PythonBaseModelAttr(attribute.name, type, true);
|
|
110
118
|
});
|
|
111
|
-
const schema = this.
|
|
119
|
+
const schema = this.getSchema(type.schema);
|
|
112
120
|
return new PythonBaseModel(type.name, schema, attributeEntries);
|
|
113
121
|
}
|
|
114
122
|
columnsToClassAttrs(table_id) {
|
|
@@ -129,7 +137,7 @@ class PythonContext {
|
|
|
129
137
|
});
|
|
130
138
|
}
|
|
131
139
|
tableToClass(table) {
|
|
132
|
-
const schema = this.
|
|
140
|
+
const schema = this.getSchema(table.schema);
|
|
133
141
|
const select = new PythonBaseModel(table.name, schema, this.columnsToClassAttrs(table.id));
|
|
134
142
|
const insert = new PythonTypedDict(table.name, "Insert", schema, this.columnsToDictAttrs(table.id, false));
|
|
135
143
|
const update = new PythonTypedDict(table.name, "Update", schema, this.columnsToDictAttrs(table.id, true));
|
|
@@ -137,11 +145,11 @@ class PythonContext {
|
|
|
137
145
|
}
|
|
138
146
|
viewToClass(view) {
|
|
139
147
|
const attributes = this.columnsToClassAttrs(view.id);
|
|
140
|
-
return new PythonBaseModel(view.name, this.
|
|
148
|
+
return new PythonBaseModel(view.name, this.getSchema(view.schema), attributes);
|
|
141
149
|
}
|
|
142
150
|
matViewToClass(matview) {
|
|
143
151
|
const attributes = this.columnsToClassAttrs(matview.id);
|
|
144
|
-
return new PythonBaseModel(matview.name, this.
|
|
152
|
+
return new PythonBaseModel(matview.name, this.getSchema(matview.schema), attributes);
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
const qualifiedTypeName = (schema, name) => `${schema}.${name}`;
|
|
@@ -332,7 +340,7 @@ function formatForPyClassName(name) {
|
|
|
332
340
|
.split(/[^a-zA-Z0-9]/)
|
|
333
341
|
.map((word) => {
|
|
334
342
|
if (word) {
|
|
335
|
-
return `${word
|
|
343
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
336
344
|
}
|
|
337
345
|
else {
|
|
338
346
|
return "";
|
package/dist/generation/swift.js
CHANGED
|
@@ -177,9 +177,9 @@ export const generateSwift = (metadata, opts = {}) => {
|
|
|
177
177
|
t.id,
|
|
178
178
|
[],
|
|
179
179
|
]));
|
|
180
|
-
columns
|
|
181
|
-
.
|
|
182
|
-
|
|
180
|
+
for (const column of columns) {
|
|
181
|
+
columnsByTableId[column.table_id]?.push(column);
|
|
182
|
+
}
|
|
183
183
|
let output = [
|
|
184
184
|
"import Foundation",
|
|
185
185
|
"import Supabase",
|
|
@@ -322,7 +322,7 @@ function formatForSwiftTypeName(name) {
|
|
|
322
322
|
.split(/[^a-zA-Z0-9]+/)
|
|
323
323
|
.map((word) => {
|
|
324
324
|
if (word) {
|
|
325
|
-
return `${word
|
|
325
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
326
326
|
}
|
|
327
327
|
else {
|
|
328
328
|
return "";
|
|
@@ -17,9 +17,11 @@ export interface GenerateTypescriptOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
defaultSchema?: string;
|
|
19
19
|
/**
|
|
20
|
-
* Formatter used on the generated output. Defaults to `oxfmt`,
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Formatter used on the generated output. Defaults to `oxfmt`, an optional
|
|
21
|
+
* peer dependency that is imported only when this default runs, so callers
|
|
22
|
+
* that always pass their own formatter need not install it. Callers on a
|
|
23
|
+
* latency-sensitive request path (e.g. postgres-meta's hosted
|
|
24
|
+
* `/generators/typescript` route) can substitute a worker-pool-backed
|
|
23
25
|
* formatter here instead of blocking the event loop on every call.
|
|
24
26
|
*/
|
|
25
27
|
format?: (code: string) => Promise<string>;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
// oxlint-disable typescript/restrict-template-expressions -- this is a verbatim port of
|
|
2
|
-
// postgres-meta's template, which intentionally interpolates arrays/values into template
|
|
3
|
-
// literals (relying on Array#toString) to build the output. Preserving that is required for
|
|
4
|
-
// byte-parity, and the offending expressions sit inside template literals where a targeted
|
|
5
|
-
// disable comment would corrupt the emitted string.
|
|
6
|
-
import { format as oxfmtFormat } from "oxfmt";
|
|
7
1
|
import { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./constants.js";
|
|
8
2
|
import { compareStrings } from "../collation.js";
|
|
3
|
+
const loadOxfmt = async () => {
|
|
4
|
+
try {
|
|
5
|
+
return await import("oxfmt");
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
throw new Error("oxfmt is not installed. It is an optional peer dependency that only the default formatter needs: install it, or pass a `format` option to generateTypescript.", { cause: error });
|
|
9
|
+
}
|
|
10
|
+
};
|
|
9
11
|
const defaultFormat = async (code) => {
|
|
10
|
-
const {
|
|
12
|
+
const { format } = await loadOxfmt();
|
|
13
|
+
const { code: formatted, errors } = await format("output.ts", code, {
|
|
11
14
|
semi: false,
|
|
12
15
|
printWidth: 80,
|
|
13
16
|
});
|
|
@@ -59,23 +62,33 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
59
62
|
tablesNamesByTableId[tableLike.id] = tableLike.name;
|
|
60
63
|
}
|
|
61
64
|
for (const column of columns) {
|
|
62
|
-
|
|
63
|
-
columnsByTableId[column.table_id].push(column);
|
|
64
|
-
}
|
|
65
|
+
columnsByTableId[column.table_id]?.push(column);
|
|
65
66
|
}
|
|
67
|
+
const getColumns = (tableId) => {
|
|
68
|
+
const tableColumns = columnsByTableId[tableId];
|
|
69
|
+
if (!tableColumns) {
|
|
70
|
+
throw new Error(`Relation ${tableId} is missing from the generator metadata`);
|
|
71
|
+
}
|
|
72
|
+
return tableColumns;
|
|
73
|
+
};
|
|
74
|
+
const getSchemaIntrospection = (schemaName) => {
|
|
75
|
+
const schemaIntrospection = introspectionBySchema[schemaName];
|
|
76
|
+
if (!schemaIntrospection) {
|
|
77
|
+
throw new Error(`Schema ${JSON.stringify(schemaName)} is missing from the generator metadata`);
|
|
78
|
+
}
|
|
79
|
+
return schemaIntrospection;
|
|
80
|
+
};
|
|
66
81
|
for (const type of types) {
|
|
67
82
|
typesById.set(type.id, type);
|
|
68
83
|
// Save all the types that are relation types for quicker lookup
|
|
69
84
|
if (type.type_relation_id) {
|
|
70
85
|
relationTypeByIds.set(type.id, type);
|
|
71
86
|
}
|
|
72
|
-
if (type.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
introspectionBySchema[type.schema].compositeTypes.push(type);
|
|
78
|
-
}
|
|
87
|
+
if (type.enums.length > 0) {
|
|
88
|
+
introspectionBySchema[type.schema]?.enums.push(type);
|
|
89
|
+
}
|
|
90
|
+
if (type.attributes.length > 0) {
|
|
91
|
+
introspectionBySchema[type.schema]?.compositeTypes.push(type);
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
const relationshipsByRelation = new Map();
|
|
@@ -105,56 +118,51 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
105
118
|
}`;
|
|
106
119
|
}
|
|
107
120
|
for (const table of tables) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
}
|
|
121
|
+
introspectionBySchema[table.schema]?.tables.push({
|
|
122
|
+
table,
|
|
123
|
+
relationships: getRelationships(table),
|
|
124
|
+
});
|
|
114
125
|
}
|
|
115
126
|
for (const table of foreignTables) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
}
|
|
127
|
+
introspectionBySchema[table.schema]?.tables.push({
|
|
128
|
+
table,
|
|
129
|
+
relationships: getRelationships(table),
|
|
130
|
+
});
|
|
122
131
|
}
|
|
123
132
|
for (const view of views) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
}
|
|
133
|
+
introspectionBySchema[view.schema]?.views.push({
|
|
134
|
+
view,
|
|
135
|
+
relationships: getRelationships(view),
|
|
136
|
+
});
|
|
130
137
|
}
|
|
131
138
|
for (const materializedView of materializedViews) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
}
|
|
139
|
+
introspectionBySchema[materializedView.schema]?.views.push({
|
|
140
|
+
view: {
|
|
141
|
+
...materializedView,
|
|
142
|
+
is_updatable: false,
|
|
143
|
+
is_insert_enabled: false,
|
|
144
|
+
is_update_enabled: false,
|
|
145
|
+
},
|
|
146
|
+
relationships: getRelationships(materializedView),
|
|
147
|
+
});
|
|
143
148
|
}
|
|
144
149
|
// Helper function to get table/view name from relation id
|
|
145
150
|
const getTableNameFromRelationId = (relationId, returnTypeId) => {
|
|
146
151
|
if (!relationId)
|
|
147
152
|
return null;
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
const tableName = tablesNamesByTableId[relationId];
|
|
154
|
+
if (tableName)
|
|
155
|
+
return tableName;
|
|
150
156
|
// if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type
|
|
151
157
|
const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null;
|
|
152
158
|
return reltype ? reltype.name : null;
|
|
153
159
|
};
|
|
154
160
|
for (const func of functions) {
|
|
155
|
-
|
|
161
|
+
const schemaIntrospection = introspectionBySchema[func.schema];
|
|
162
|
+
if (schemaIntrospection) {
|
|
156
163
|
// Get all input args (in, inout, variadic modes)
|
|
157
164
|
const inArgs = func.args.filter(({ mode }) => VALID_FUNCTION_ARGS_MODE.has(mode));
|
|
165
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
158
166
|
if (
|
|
159
167
|
// Case 1: Function has no parameters
|
|
160
168
|
inArgs.length === 0 ||
|
|
@@ -171,16 +179,16 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
171
179
|
// Case 4: Single unnamed parameter of valid type (json, jsonb, text)
|
|
172
180
|
// Exclude all functions definitions that have only one single argument unnamed argument that isn't
|
|
173
181
|
// a json/jsonb/text as it won't be considered by PostgREST
|
|
174
|
-
(
|
|
175
|
-
|
|
176
|
-
(VALID_UNNAMED_FUNCTION_ARG_TYPES.has(
|
|
182
|
+
(soleInArg &&
|
|
183
|
+
soleInArg.name === "" &&
|
|
184
|
+
(VALID_UNNAMED_FUNCTION_ARG_TYPES.has(soleInArg.type_id) ||
|
|
177
185
|
// OR if the function have a single unnamed args which is another table (embeded function)
|
|
178
|
-
(relationTypeByIds.get(
|
|
186
|
+
(relationTypeByIds.get(soleInArg.type_id) &&
|
|
179
187
|
getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id)) ||
|
|
180
188
|
// OR if the function takes a table row but doesn't qualify as embedded (for error reporting)
|
|
181
|
-
(relationTypeByIds.get(
|
|
189
|
+
(relationTypeByIds.get(soleInArg.type_id) &&
|
|
182
190
|
!getTableNameFromRelationId(func.return_type_relation_id, func.return_type_id))))) {
|
|
183
|
-
|
|
191
|
+
schemaIntrospection.functions.push({ fn: func, inArgs });
|
|
184
192
|
}
|
|
185
193
|
}
|
|
186
194
|
}
|
|
@@ -188,12 +196,12 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
188
196
|
// combines tables + foreign tables, `.views` combines views + materialized
|
|
189
197
|
// views — so the cross-collection name sort can't be expressed by the
|
|
190
198
|
// single-collection `sortGeneratorMetadata` pass and stays here.
|
|
191
|
-
for (const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
for (const schemaIntrospection of Object.values(introspectionBySchema)) {
|
|
200
|
+
schemaIntrospection.tables.sort((a, b) => compareStrings(a.table.name, b.table.name));
|
|
201
|
+
schemaIntrospection.views.sort((a, b) => compareStrings(a.view.name, b.view.name));
|
|
202
|
+
schemaIntrospection.functions.sort((a, b) => compareStrings(a.fn.name, b.fn.name));
|
|
203
|
+
schemaIntrospection.enums.sort((a, b) => compareStrings(a.name, b.name));
|
|
204
|
+
schemaIntrospection.compositeTypes.sort((a, b) => compareStrings(a.name, b.name));
|
|
197
205
|
}
|
|
198
206
|
const getFunctionTsReturnType = (fn, returnType) => {
|
|
199
207
|
// Determine if this function should have SetofOptions
|
|
@@ -213,8 +221,9 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
213
221
|
}
|
|
214
222
|
// Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table
|
|
215
223
|
// and set the right "from" and "to" values to allow selecting from a table row
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
const soleArg = fn.args.length === 1 ? fn.args[0] : undefined;
|
|
225
|
+
if (soleArg) {
|
|
226
|
+
const relationType = relationTypeByIds.get(soleArg.type_id);
|
|
218
227
|
// Only add SetofOptions for functions with table arguments (embedded functions)
|
|
219
228
|
// or specific functions that RETURNS table-name
|
|
220
229
|
if (relationType) {
|
|
@@ -268,7 +277,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
268
277
|
introspectionBySchema[schema.name]?.views.find(({ view: { id } }) => id === fn.return_type_relation_id)?.view;
|
|
269
278
|
if (relation) {
|
|
270
279
|
return `{
|
|
271
|
-
${
|
|
280
|
+
${getColumns(relation.id)
|
|
272
281
|
.map((column) => generateColumnTsDefinition(schema, {
|
|
273
282
|
name: column.name,
|
|
274
283
|
format: column.format,
|
|
@@ -288,9 +297,10 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
288
297
|
};
|
|
289
298
|
// Special error case for functions that take table row but don't qualify as embedded functions
|
|
290
299
|
const hasTableRowError = (fn, inArgs) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
301
|
+
if (soleInArg &&
|
|
302
|
+
soleInArg.name === "" &&
|
|
303
|
+
relationTypeByIds.get(soleInArg.type_id) &&
|
|
294
304
|
!getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id)) {
|
|
295
305
|
return true;
|
|
296
306
|
}
|
|
@@ -301,30 +311,33 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
301
311
|
// If there is a single function definition, there is no conflict
|
|
302
312
|
if (fns.length <= 1)
|
|
303
313
|
return null;
|
|
314
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
304
315
|
// Generic conflict detection patterns
|
|
305
316
|
// Pattern 1: No-args vs default-args conflicts
|
|
306
317
|
if (inArgs.length === 0) {
|
|
307
318
|
const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => {
|
|
308
319
|
if (otherFn === fn)
|
|
309
320
|
return false;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
321
|
+
const otherSoleInArg = otherInArgs.length === 1 ? otherInArgs[0] : undefined;
|
|
322
|
+
return (otherSoleInArg &&
|
|
323
|
+
otherSoleInArg.name === "" &&
|
|
324
|
+
otherSoleInArg.has_default);
|
|
313
325
|
});
|
|
314
|
-
|
|
315
|
-
|
|
326
|
+
const conflictingFn = conflictingFns[0];
|
|
327
|
+
if (conflictingFn) {
|
|
316
328
|
const returnTypeName = typesById.get(conflictingFn.fn.return_type_id)?.name || "unknown";
|
|
317
329
|
return `Could not choose the best candidate function between: ${schema.name}.${fn.name}(), ${schema.name}.${fn.name}( => ${returnTypeName}). Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
|
|
318
330
|
}
|
|
319
331
|
}
|
|
320
332
|
// Pattern 2: Same parameter name but different types (unresolvable overloads)
|
|
321
|
-
if (
|
|
333
|
+
if (soleInArg && soleInArg.name !== "") {
|
|
322
334
|
const conflictingFns = fns.filter(({ fn: otherFn, inArgs: otherInArgs }) => {
|
|
323
335
|
if (otherFn === fn)
|
|
324
336
|
return false;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
const otherSoleInArg = otherInArgs.length === 1 ? otherInArgs[0] : undefined;
|
|
338
|
+
return (otherSoleInArg &&
|
|
339
|
+
otherSoleInArg.name === soleInArg.name &&
|
|
340
|
+
otherSoleInArg.type_id !== soleInArg.type_id);
|
|
328
341
|
});
|
|
329
342
|
if (conflictingFns.length > 0) {
|
|
330
343
|
const allConflictingFunctions = [{ fn, inArgs }, ...conflictingFns];
|
|
@@ -443,11 +456,12 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
443
456
|
* See supabase/postgres-meta#1034.
|
|
444
457
|
*/
|
|
445
458
|
const isComputedFieldOf = (fn, inArgs, relation) => {
|
|
446
|
-
|
|
459
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
460
|
+
if (soleInArg) {
|
|
447
461
|
// `relationTypeByIds` is already "types backed by a relation", so a hit
|
|
448
462
|
// identifies the relation regardless of how the argument was spelled.
|
|
449
463
|
// A `variadic` argument is an array type and never appears here.
|
|
450
|
-
const argType = relationTypeByIds.get(
|
|
464
|
+
const argType = relationTypeByIds.get(soleInArg.type_id);
|
|
451
465
|
if (argType) {
|
|
452
466
|
return argType.type_relation_id === relation.id;
|
|
453
467
|
}
|
|
@@ -463,7 +477,7 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un
|
|
|
463
477
|
export type Database = {
|
|
464
478
|
${internal_supabase_schema}
|
|
465
479
|
${schemas.map((schema) => {
|
|
466
|
-
const { tables: schemaTables, views: schemaViews, functions: schemaFunctions, enums: schemaEnums, compositeTypes: schemaCompositeTypes, } =
|
|
480
|
+
const { tables: schemaTables, views: schemaViews, functions: schemaFunctions, enums: schemaEnums, compositeTypes: schemaCompositeTypes, } = getSchemaIntrospection(schema.name);
|
|
467
481
|
return `${JSON.stringify(schema.name)}: {
|
|
468
482
|
Tables: {
|
|
469
483
|
${schemaTables.length === 0
|
|
@@ -471,7 +485,7 @@ export type Database = {
|
|
|
471
485
|
: schemaTables.map(({ table, relationships, }) => `${JSON.stringify(table.name)}: {
|
|
472
486
|
Row: {
|
|
473
487
|
${[
|
|
474
|
-
...
|
|
488
|
+
...getColumns(table.id).map((column) => generateColumnTsDefinition(schema, {
|
|
475
489
|
name: column.name,
|
|
476
490
|
format: column.format,
|
|
477
491
|
type_schema: column.type_schema,
|
|
@@ -486,7 +500,7 @@ export type Database = {
|
|
|
486
500
|
]}
|
|
487
501
|
}
|
|
488
502
|
Insert: {
|
|
489
|
-
${
|
|
503
|
+
${getColumns(table.id).map((column) => {
|
|
490
504
|
if (column.identity_generation === "ALWAYS" ||
|
|
491
505
|
column.is_generated) {
|
|
492
506
|
return `${JSON.stringify(column.name)}?: never`;
|
|
@@ -503,7 +517,7 @@ export type Database = {
|
|
|
503
517
|
})}
|
|
504
518
|
}
|
|
505
519
|
Update: {
|
|
506
|
-
${
|
|
520
|
+
${getColumns(table.id).map((column) => {
|
|
507
521
|
if (column.identity_generation === "ALWAYS" ||
|
|
508
522
|
column.is_generated) {
|
|
509
523
|
return `${JSON.stringify(column.name)}?: never`;
|
|
@@ -528,7 +542,7 @@ export type Database = {
|
|
|
528
542
|
: schemaViews.map(({ view, relationships, }) => `${JSON.stringify(view.name)}: {
|
|
529
543
|
Row: {
|
|
530
544
|
${[
|
|
531
|
-
...
|
|
545
|
+
...getColumns(view.id).map((column) => generateColumnTsDefinition(schema, {
|
|
532
546
|
name: column.name,
|
|
533
547
|
format: column.format,
|
|
534
548
|
type_schema: column.type_schema,
|
|
@@ -545,7 +559,7 @@ export type Database = {
|
|
|
545
559
|
// version 1 documents) falls back to the old gate.
|
|
546
560
|
(view.is_insert_enabled ?? view.is_updatable)
|
|
547
561
|
? `Insert: {
|
|
548
|
-
${
|
|
562
|
+
${getColumns(view.id).map((column) => {
|
|
549
563
|
if (!column.is_updatable) {
|
|
550
564
|
return `${JSON.stringify(column.name)}?: never`;
|
|
551
565
|
}
|
|
@@ -561,7 +575,7 @@ export type Database = {
|
|
|
561
575
|
`
|
|
562
576
|
: ""}${(view.is_update_enabled ?? view.is_updatable)
|
|
563
577
|
? `Update: {
|
|
564
|
-
${
|
|
578
|
+
${getColumns(view.id).map((column) => {
|
|
565
579
|
if (!column.is_updatable) {
|
|
566
580
|
return `${JSON.stringify(column.name)}?: never`;
|
|
567
581
|
}
|
|
@@ -586,12 +600,11 @@ export type Database = {
|
|
|
586
600
|
return "[_ in never]: never";
|
|
587
601
|
}
|
|
588
602
|
const schemaFunctionsGroupedByName = schemaFunctions.reduce((acc, curr) => {
|
|
589
|
-
acc[curr.fn.name] ??= [];
|
|
590
|
-
acc[curr.fn.name].push(curr);
|
|
603
|
+
(acc[curr.fn.name] ??= []).push(curr);
|
|
591
604
|
return acc;
|
|
592
605
|
}, {});
|
|
593
|
-
for (const
|
|
594
|
-
|
|
606
|
+
for (const fns of Object.values(schemaFunctionsGroupedByName)) {
|
|
607
|
+
fns.sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
|
|
595
608
|
compareStrings(a.fn.return_type, b.fn.return_type));
|
|
596
609
|
}
|
|
597
610
|
return Object.entries(schemaFunctionsGroupedByName)
|
|
@@ -734,7 +747,7 @@ export type CompositeTypes<
|
|
|
734
747
|
|
|
735
748
|
export const Constants = {
|
|
736
749
|
${schemas.map((schema) => {
|
|
737
|
-
const schemaEnums =
|
|
750
|
+
const schemaEnums = getSchemaIntrospection(schema.name).enums;
|
|
738
751
|
return `${JSON.stringify(schema.name)}: {
|
|
739
752
|
Enums: {
|
|
740
753
|
${schemaEnums.map((enum_) => `${JSON.stringify(enum_.name)}: [${enum_.enums
|
|
@@ -795,18 +808,17 @@ typeSchema) => {
|
|
|
795
808
|
else {
|
|
796
809
|
const preferredSchema = typeSchema ?? schema.name;
|
|
797
810
|
const enumTypes = types.filter((type) => type.name === pgType && type.enums.length > 0);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
enumTypes[0];
|
|
811
|
+
const enumType = enumTypes.find((type) => type.schema === preferredSchema) ?? enumTypes[0];
|
|
812
|
+
if (enumType) {
|
|
801
813
|
if (schemas.some(({ name }) => name === enumType.schema)) {
|
|
802
814
|
return `Database[${JSON.stringify(enumType.schema)}]['Enums'][${JSON.stringify(enumType.name)}]`;
|
|
803
815
|
}
|
|
804
816
|
return enumType.enums.map((variant) => JSON.stringify(variant)).join("|");
|
|
805
817
|
}
|
|
806
818
|
const compositeTypes = types.filter((type) => type.name === pgType && type.attributes.length > 0);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
819
|
+
const compositeType = compositeTypes.find((type) => type.schema === preferredSchema) ??
|
|
820
|
+
compositeTypes[0];
|
|
821
|
+
if (compositeType) {
|
|
810
822
|
if (schemas.some(({ name }) => name === compositeType.schema)) {
|
|
811
823
|
return `Database[${JSON.stringify(compositeType.schema)}]['CompositeTypes'][${JSON.stringify(compositeType.name)}]`;
|
|
812
824
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/postgrest-typegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Type generation for PostgREST from PostgreSQL schemas",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -55,29 +55,39 @@
|
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsc --project tsconfig.build.json",
|
|
57
57
|
"check-types": "tsc --noEmit",
|
|
58
|
+
"check-types:consumer": "tsc --project tsconfig.consumer.json",
|
|
58
59
|
"format-and-lint": "oxfmt --check . && oxlint --deny-warnings src test scripts",
|
|
59
60
|
"format-and-lint:fix": "oxfmt . && oxlint --fix src test scripts",
|
|
60
61
|
"knip": "knip",
|
|
61
62
|
"test": "bun scripts/run-tests.ts test/"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
|
-
"arktype": "2.2.3"
|
|
65
|
-
"oxfmt": "0.66.0"
|
|
65
|
+
"arktype": "2.2.3"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testcontainers/postgresql": "^12.1.0",
|
|
69
|
+
"@tsconfig/bun": "^1.0.11",
|
|
69
70
|
"@tsconfig/node-ts": "^23.6.4",
|
|
70
71
|
"@tsconfig/node24": "^24.0.5",
|
|
71
72
|
"@types/bun": "^1.4.0",
|
|
72
73
|
"@types/node": "^26.3.0",
|
|
73
74
|
"@types/pg": "^8.23.1",
|
|
74
75
|
"knip": "^6.32.3",
|
|
76
|
+
"oxfmt": "0.66.0",
|
|
75
77
|
"oxlint": "1.83.0",
|
|
76
78
|
"oxlint-tsgolint": "7.0.2002",
|
|
77
79
|
"pg": "^8.23.0",
|
|
78
80
|
"testcontainers": "^12.1.0",
|
|
79
81
|
"typescript": "7.0.2"
|
|
80
82
|
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"oxfmt": "0.66.0"
|
|
85
|
+
},
|
|
86
|
+
"peerDependenciesMeta": {
|
|
87
|
+
"oxfmt": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
90
|
+
},
|
|
81
91
|
"engines": {
|
|
82
92
|
"node": ">=22.12.0"
|
|
83
93
|
}
|
package/src/generation/go.ts
CHANGED
|
@@ -24,8 +24,7 @@ export const generateGo = ({
|
|
|
24
24
|
}: GeneratorMetadata): string => {
|
|
25
25
|
const columnsByTableId = columns.reduce(
|
|
26
26
|
(acc, curr) => {
|
|
27
|
-
acc[curr.table_id] ??= [];
|
|
28
|
-
acc[curr.table_id].push(curr);
|
|
27
|
+
(acc[curr.table_id] ??= []).push(curr);
|
|
29
28
|
return acc;
|
|
30
29
|
},
|
|
31
30
|
{} as Record<string, PostgresColumn[]>,
|
|
@@ -110,7 +109,7 @@ function formatForGoTypeName(name: string): string {
|
|
|
110
109
|
.split(/[^a-zA-Z0-9]/)
|
|
111
110
|
.map((word) => {
|
|
112
111
|
if (word) {
|
|
113
|
-
return `${word
|
|
112
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
114
113
|
} else {
|
|
115
114
|
return "";
|
|
116
115
|
}
|
package/src/generation/python.ts
CHANGED
|
@@ -93,8 +93,7 @@ class PythonContext {
|
|
|
93
93
|
);
|
|
94
94
|
this.columns = columns.reduce(
|
|
95
95
|
(acc, curr) => {
|
|
96
|
-
acc[curr.table_id] ??= [];
|
|
97
|
-
acc[curr.table_id].push(curr);
|
|
96
|
+
(acc[curr.table_id] ??= []).push(curr);
|
|
98
97
|
return acc;
|
|
99
98
|
},
|
|
100
99
|
{} as Record<number, PostgresColumn[]>,
|
|
@@ -109,16 +108,28 @@ class PythonContext {
|
|
|
109
108
|
);
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
getSchema(name: string): PostgresSchema {
|
|
112
|
+
const schema = this.schemas[name];
|
|
113
|
+
if (!schema) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Schema ${JSON.stringify(name)} is missing from the generator metadata`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return schema;
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
resolveTypeName(name: string, typeSchema: string): string {
|
|
113
122
|
const qualified = qualifiedTypeName(typeSchema, name);
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
const userEnum = this.user_enums[qualified];
|
|
124
|
+
if (userEnum) {
|
|
125
|
+
return userEnum.name;
|
|
116
126
|
}
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
const mappedType = PY_TYPE_MAP[name];
|
|
128
|
+
if (mappedType !== undefined) {
|
|
129
|
+
return mappedType;
|
|
119
130
|
}
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
const type = this.types[qualified];
|
|
132
|
+
if (type) {
|
|
122
133
|
return `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
|
|
123
134
|
}
|
|
124
135
|
return "Any";
|
|
@@ -158,7 +169,7 @@ class PythonContext {
|
|
|
158
169
|
},
|
|
159
170
|
);
|
|
160
171
|
|
|
161
|
-
const schema = this.
|
|
172
|
+
const schema = this.getSchema(type.schema);
|
|
162
173
|
return new PythonBaseModel(type.name, schema, attributeEntries);
|
|
163
174
|
}
|
|
164
175
|
|
|
@@ -192,7 +203,7 @@ class PythonContext {
|
|
|
192
203
|
tableToClass(
|
|
193
204
|
table: PostgresTable,
|
|
194
205
|
): [PythonBaseModel, PythonTypedDict, PythonTypedDict] {
|
|
195
|
-
const schema = this.
|
|
206
|
+
const schema = this.getSchema(table.schema);
|
|
196
207
|
const select = new PythonBaseModel(
|
|
197
208
|
table.name,
|
|
198
209
|
schema,
|
|
@@ -217,7 +228,7 @@ class PythonContext {
|
|
|
217
228
|
const attributes = this.columnsToClassAttrs(view.id);
|
|
218
229
|
return new PythonBaseModel(
|
|
219
230
|
view.name,
|
|
220
|
-
this.
|
|
231
|
+
this.getSchema(view.schema),
|
|
221
232
|
attributes,
|
|
222
233
|
);
|
|
223
234
|
}
|
|
@@ -226,7 +237,7 @@ class PythonContext {
|
|
|
226
237
|
const attributes = this.columnsToClassAttrs(matview.id);
|
|
227
238
|
return new PythonBaseModel(
|
|
228
239
|
matview.name,
|
|
229
|
-
this.
|
|
240
|
+
this.getSchema(matview.schema),
|
|
230
241
|
attributes,
|
|
231
242
|
);
|
|
232
243
|
}
|
|
@@ -463,7 +474,7 @@ function formatForPyClassName(name: string): string {
|
|
|
463
474
|
.split(/[^a-zA-Z0-9]/)
|
|
464
475
|
.map((word) => {
|
|
465
476
|
if (word) {
|
|
466
|
-
return `${word
|
|
477
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
467
478
|
} else {
|
|
468
479
|
return "";
|
|
469
480
|
}
|
package/src/generation/swift.ts
CHANGED
|
@@ -303,9 +303,9 @@ export const generateSwift = (
|
|
|
303
303
|
]),
|
|
304
304
|
);
|
|
305
305
|
|
|
306
|
-
columns
|
|
307
|
-
.
|
|
308
|
-
|
|
306
|
+
for (const column of columns) {
|
|
307
|
+
columnsByTableId[column.table_id]?.push(column);
|
|
308
|
+
}
|
|
309
309
|
|
|
310
310
|
let output = [
|
|
311
311
|
"import Foundation",
|
|
@@ -492,7 +492,7 @@ function formatForSwiftTypeName(name: string): string {
|
|
|
492
492
|
.split(/[^a-zA-Z0-9]+/)
|
|
493
493
|
.map((word) => {
|
|
494
494
|
if (word) {
|
|
495
|
-
return `${word
|
|
495
|
+
return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
496
496
|
} else {
|
|
497
497
|
return "";
|
|
498
498
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// literals (relying on Array#toString) to build the output. Preserving that is required for
|
|
4
4
|
// byte-parity, and the offending expressions sit inside template literals where a targeted
|
|
5
5
|
// disable comment would corrupt the emitted string.
|
|
6
|
-
import { format as oxfmtFormat } from "oxfmt";
|
|
7
6
|
import type {
|
|
8
7
|
GeneratorMetadata,
|
|
9
8
|
PostgresColumn,
|
|
@@ -39,16 +38,30 @@ export interface GenerateTypescriptOptions {
|
|
|
39
38
|
*/
|
|
40
39
|
defaultSchema?: string;
|
|
41
40
|
/**
|
|
42
|
-
* Formatter used on the generated output. Defaults to `oxfmt`,
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Formatter used on the generated output. Defaults to `oxfmt`, an optional
|
|
42
|
+
* peer dependency that is imported only when this default runs, so callers
|
|
43
|
+
* that always pass their own formatter need not install it. Callers on a
|
|
44
|
+
* latency-sensitive request path (e.g. postgres-meta's hosted
|
|
45
|
+
* `/generators/typescript` route) can substitute a worker-pool-backed
|
|
45
46
|
* formatter here instead of blocking the event loop on every call.
|
|
46
47
|
*/
|
|
47
48
|
format?: (code: string) => Promise<string>;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
const loadOxfmt = async () => {
|
|
52
|
+
try {
|
|
53
|
+
return await import("oxfmt");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"oxfmt is not installed. It is an optional peer dependency that only the default formatter needs: install it, or pass a `format` option to generateTypescript.",
|
|
57
|
+
{ cause: error },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
50
62
|
const defaultFormat = async (code: string): Promise<string> => {
|
|
51
|
-
const {
|
|
63
|
+
const { format } = await loadOxfmt();
|
|
64
|
+
const { code: formatted, errors } = await format("output.ts", code, {
|
|
52
65
|
semi: false,
|
|
53
66
|
printWidth: 80,
|
|
54
67
|
});
|
|
@@ -162,10 +175,26 @@ export const generateTypescript = async (
|
|
|
162
175
|
tablesNamesByTableId[tableLike.id] = tableLike.name;
|
|
163
176
|
}
|
|
164
177
|
for (const column of columns) {
|
|
165
|
-
|
|
166
|
-
columnsByTableId[column.table_id].push(column);
|
|
167
|
-
}
|
|
178
|
+
columnsByTableId[column.table_id]?.push(column);
|
|
168
179
|
}
|
|
180
|
+
const getColumns = (tableId: number): PostgresColumn[] => {
|
|
181
|
+
const tableColumns = columnsByTableId[tableId];
|
|
182
|
+
if (!tableColumns) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Relation ${tableId} is missing from the generator metadata`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return tableColumns;
|
|
188
|
+
};
|
|
189
|
+
const getSchemaIntrospection = (schemaName: string) => {
|
|
190
|
+
const schemaIntrospection = introspectionBySchema[schemaName];
|
|
191
|
+
if (!schemaIntrospection) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Schema ${JSON.stringify(schemaName)} is missing from the generator metadata`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return schemaIntrospection;
|
|
197
|
+
};
|
|
169
198
|
|
|
170
199
|
for (const type of types) {
|
|
171
200
|
typesById.set(type.id, type);
|
|
@@ -173,13 +202,11 @@ export const generateTypescript = async (
|
|
|
173
202
|
if (type.type_relation_id) {
|
|
174
203
|
relationTypeByIds.set(type.id, type);
|
|
175
204
|
}
|
|
176
|
-
if (type.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
introspectionBySchema[type.schema].compositeTypes.push(type);
|
|
182
|
-
}
|
|
205
|
+
if (type.enums.length > 0) {
|
|
206
|
+
introspectionBySchema[type.schema]?.enums.push(type);
|
|
207
|
+
}
|
|
208
|
+
if (type.attributes.length > 0) {
|
|
209
|
+
introspectionBySchema[type.schema]?.compositeTypes.push(type);
|
|
183
210
|
}
|
|
184
211
|
}
|
|
185
212
|
|
|
@@ -233,41 +260,33 @@ export const generateTypescript = async (
|
|
|
233
260
|
}
|
|
234
261
|
|
|
235
262
|
for (const table of tables) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
241
|
-
}
|
|
263
|
+
introspectionBySchema[table.schema]?.tables.push({
|
|
264
|
+
table,
|
|
265
|
+
relationships: getRelationships(table),
|
|
266
|
+
});
|
|
242
267
|
}
|
|
243
268
|
for (const table of foreignTables) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
}
|
|
269
|
+
introspectionBySchema[table.schema]?.tables.push({
|
|
270
|
+
table,
|
|
271
|
+
relationships: getRelationships(table),
|
|
272
|
+
});
|
|
250
273
|
}
|
|
251
274
|
for (const view of views) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
257
|
-
}
|
|
275
|
+
introspectionBySchema[view.schema]?.views.push({
|
|
276
|
+
view,
|
|
277
|
+
relationships: getRelationships(view),
|
|
278
|
+
});
|
|
258
279
|
}
|
|
259
280
|
for (const materializedView of materializedViews) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
});
|
|
270
|
-
}
|
|
281
|
+
introspectionBySchema[materializedView.schema]?.views.push({
|
|
282
|
+
view: {
|
|
283
|
+
...materializedView,
|
|
284
|
+
is_updatable: false,
|
|
285
|
+
is_insert_enabled: false,
|
|
286
|
+
is_update_enabled: false,
|
|
287
|
+
},
|
|
288
|
+
relationships: getRelationships(materializedView),
|
|
289
|
+
});
|
|
271
290
|
}
|
|
272
291
|
// Helper function to get table/view name from relation id
|
|
273
292
|
const getTableNameFromRelationId = (
|
|
@@ -276,20 +295,22 @@ export const generateTypescript = async (
|
|
|
276
295
|
): string | null => {
|
|
277
296
|
if (!relationId) return null;
|
|
278
297
|
|
|
279
|
-
|
|
280
|
-
|
|
298
|
+
const tableName = tablesNamesByTableId[relationId];
|
|
299
|
+
if (tableName) return tableName;
|
|
281
300
|
// if it's a composite type we use the type name as relation name to allow sub-selecting fields of the composite type
|
|
282
301
|
const reltype = returnTypeId ? relationTypeByIds.get(returnTypeId) : null;
|
|
283
302
|
return reltype ? reltype.name : null;
|
|
284
303
|
};
|
|
285
304
|
|
|
286
305
|
for (const func of functions) {
|
|
287
|
-
|
|
306
|
+
const schemaIntrospection = introspectionBySchema[func.schema];
|
|
307
|
+
if (schemaIntrospection) {
|
|
288
308
|
// Get all input args (in, inout, variadic modes)
|
|
289
309
|
const inArgs = func.args.filter(({ mode }) =>
|
|
290
310
|
VALID_FUNCTION_ARGS_MODE.has(mode),
|
|
291
311
|
);
|
|
292
312
|
|
|
313
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
293
314
|
if (
|
|
294
315
|
// Case 1: Function has no parameters
|
|
295
316
|
inArgs.length === 0 ||
|
|
@@ -308,23 +329,23 @@ export const generateTypescript = async (
|
|
|
308
329
|
// Case 4: Single unnamed parameter of valid type (json, jsonb, text)
|
|
309
330
|
// Exclude all functions definitions that have only one single argument unnamed argument that isn't
|
|
310
331
|
// a json/jsonb/text as it won't be considered by PostgREST
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
(VALID_UNNAMED_FUNCTION_ARG_TYPES.has(
|
|
332
|
+
(soleInArg &&
|
|
333
|
+
soleInArg.name === "" &&
|
|
334
|
+
(VALID_UNNAMED_FUNCTION_ARG_TYPES.has(soleInArg.type_id) ||
|
|
314
335
|
// OR if the function have a single unnamed args which is another table (embeded function)
|
|
315
|
-
(relationTypeByIds.get(
|
|
336
|
+
(relationTypeByIds.get(soleInArg.type_id) &&
|
|
316
337
|
getTableNameFromRelationId(
|
|
317
338
|
func.return_type_relation_id,
|
|
318
339
|
func.return_type_id,
|
|
319
340
|
)) ||
|
|
320
341
|
// OR if the function takes a table row but doesn't qualify as embedded (for error reporting)
|
|
321
|
-
(relationTypeByIds.get(
|
|
342
|
+
(relationTypeByIds.get(soleInArg.type_id) &&
|
|
322
343
|
!getTableNameFromRelationId(
|
|
323
344
|
func.return_type_relation_id,
|
|
324
345
|
func.return_type_id,
|
|
325
346
|
))))
|
|
326
347
|
) {
|
|
327
|
-
|
|
348
|
+
schemaIntrospection.functions.push({ fn: func, inArgs });
|
|
328
349
|
}
|
|
329
350
|
}
|
|
330
351
|
}
|
|
@@ -332,20 +353,18 @@ export const generateTypescript = async (
|
|
|
332
353
|
// combines tables + foreign tables, `.views` combines views + materialized
|
|
333
354
|
// views — so the cross-collection name sort can't be expressed by the
|
|
334
355
|
// single-collection `sortGeneratorMetadata` pass and stays here.
|
|
335
|
-
for (const
|
|
336
|
-
|
|
356
|
+
for (const schemaIntrospection of Object.values(introspectionBySchema)) {
|
|
357
|
+
schemaIntrospection.tables.sort((a, b) =>
|
|
337
358
|
compareStrings(a.table.name, b.table.name),
|
|
338
359
|
);
|
|
339
|
-
|
|
360
|
+
schemaIntrospection.views.sort((a, b) =>
|
|
340
361
|
compareStrings(a.view.name, b.view.name),
|
|
341
362
|
);
|
|
342
|
-
|
|
363
|
+
schemaIntrospection.functions.sort((a, b) =>
|
|
343
364
|
compareStrings(a.fn.name, b.fn.name),
|
|
344
365
|
);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
);
|
|
348
|
-
introspectionBySchema[schema].compositeTypes.sort((a, b) =>
|
|
366
|
+
schemaIntrospection.enums.sort((a, b) => compareStrings(a.name, b.name));
|
|
367
|
+
schemaIntrospection.compositeTypes.sort((a, b) =>
|
|
349
368
|
compareStrings(a.name, b.name),
|
|
350
369
|
);
|
|
351
370
|
}
|
|
@@ -376,8 +395,9 @@ export const generateTypescript = async (
|
|
|
376
395
|
}
|
|
377
396
|
// Case 2: if the function has a single table argument, we need to add SetofOptions to allow selecting sub fields of the table
|
|
378
397
|
// and set the right "from" and "to" values to allow selecting from a table row
|
|
379
|
-
|
|
380
|
-
|
|
398
|
+
const soleArg = fn.args.length === 1 ? fn.args[0] : undefined;
|
|
399
|
+
if (soleArg) {
|
|
400
|
+
const relationType = relationTypeByIds.get(soleArg.type_id);
|
|
381
401
|
|
|
382
402
|
// Only add SetofOptions for functions with table arguments (embedded functions)
|
|
383
403
|
// or specific functions that RETURNS table-name
|
|
@@ -445,7 +465,7 @@ export const generateTypescript = async (
|
|
|
445
465
|
)?.view;
|
|
446
466
|
if (relation) {
|
|
447
467
|
return `{
|
|
448
|
-
${
|
|
468
|
+
${getColumns(relation.id)
|
|
449
469
|
.map((column) =>
|
|
450
470
|
generateColumnTsDefinition(
|
|
451
471
|
schema,
|
|
@@ -476,10 +496,11 @@ export const generateTypescript = async (
|
|
|
476
496
|
fn: PostgresFunction,
|
|
477
497
|
inArgs: PostgresFunction["args"],
|
|
478
498
|
) => {
|
|
499
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
479
500
|
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
relationTypeByIds.get(
|
|
501
|
+
soleInArg &&
|
|
502
|
+
soleInArg.name === "" &&
|
|
503
|
+
relationTypeByIds.get(soleInArg.type_id) &&
|
|
483
504
|
!getTableNameFromRelationId(fn.return_type_relation_id, fn.return_type_id)
|
|
484
505
|
) {
|
|
485
506
|
return true;
|
|
@@ -496,6 +517,7 @@ export const generateTypescript = async (
|
|
|
496
517
|
) => {
|
|
497
518
|
// If there is a single function definition, there is no conflict
|
|
498
519
|
if (fns.length <= 1) return null;
|
|
520
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
499
521
|
|
|
500
522
|
// Generic conflict detection patterns
|
|
501
523
|
// Pattern 1: No-args vs default-args conflicts
|
|
@@ -503,16 +525,18 @@ export const generateTypescript = async (
|
|
|
503
525
|
const conflictingFns = fns.filter(
|
|
504
526
|
({ fn: otherFn, inArgs: otherInArgs }) => {
|
|
505
527
|
if (otherFn === fn) return false;
|
|
528
|
+
const otherSoleInArg =
|
|
529
|
+
otherInArgs.length === 1 ? otherInArgs[0] : undefined;
|
|
506
530
|
return (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
531
|
+
otherSoleInArg &&
|
|
532
|
+
otherSoleInArg.name === "" &&
|
|
533
|
+
otherSoleInArg.has_default
|
|
510
534
|
);
|
|
511
535
|
},
|
|
512
536
|
);
|
|
513
537
|
|
|
514
|
-
|
|
515
|
-
|
|
538
|
+
const conflictingFn = conflictingFns[0];
|
|
539
|
+
if (conflictingFn) {
|
|
516
540
|
const returnTypeName =
|
|
517
541
|
typesById.get(conflictingFn.fn.return_type_id)?.name || "unknown";
|
|
518
542
|
return `Could not choose the best candidate function between: ${schema.name}.${fn.name}(), ${schema.name}.${fn.name}( => ${returnTypeName}). Try renaming the parameters or the function itself in the database so function overloading can be resolved`;
|
|
@@ -520,14 +544,16 @@ export const generateTypescript = async (
|
|
|
520
544
|
}
|
|
521
545
|
|
|
522
546
|
// Pattern 2: Same parameter name but different types (unresolvable overloads)
|
|
523
|
-
if (
|
|
547
|
+
if (soleInArg && soleInArg.name !== "") {
|
|
524
548
|
const conflictingFns = fns.filter(
|
|
525
549
|
({ fn: otherFn, inArgs: otherInArgs }) => {
|
|
526
550
|
if (otherFn === fn) return false;
|
|
551
|
+
const otherSoleInArg =
|
|
552
|
+
otherInArgs.length === 1 ? otherInArgs[0] : undefined;
|
|
527
553
|
return (
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
554
|
+
otherSoleInArg &&
|
|
555
|
+
otherSoleInArg.name === soleInArg.name &&
|
|
556
|
+
otherSoleInArg.type_id !== soleInArg.type_id
|
|
531
557
|
);
|
|
532
558
|
},
|
|
533
559
|
);
|
|
@@ -711,11 +737,12 @@ export const generateTypescript = async (
|
|
|
711
737
|
inArgs: PostgresFunction["args"],
|
|
712
738
|
relation: { id: number; name: string },
|
|
713
739
|
) => {
|
|
714
|
-
|
|
740
|
+
const soleInArg = inArgs.length === 1 ? inArgs[0] : undefined;
|
|
741
|
+
if (soleInArg) {
|
|
715
742
|
// `relationTypeByIds` is already "types backed by a relation", so a hit
|
|
716
743
|
// identifies the relation regardless of how the argument was spelled.
|
|
717
744
|
// A `variadic` argument is an array type and never appears here.
|
|
718
|
-
const argType = relationTypeByIds.get(
|
|
745
|
+
const argType = relationTypeByIds.get(soleInArg.type_id);
|
|
719
746
|
if (argType) {
|
|
720
747
|
return argType.type_relation_id === relation.id;
|
|
721
748
|
}
|
|
@@ -738,7 +765,7 @@ export type Database = {
|
|
|
738
765
|
functions: schemaFunctions,
|
|
739
766
|
enums: schemaEnums,
|
|
740
767
|
compositeTypes: schemaCompositeTypes,
|
|
741
|
-
} =
|
|
768
|
+
} = getSchemaIntrospection(schema.name);
|
|
742
769
|
return `${JSON.stringify(schema.name)}: {
|
|
743
770
|
Tables: {
|
|
744
771
|
${
|
|
@@ -751,7 +778,7 @@ export type Database = {
|
|
|
751
778
|
}) => `${JSON.stringify(table.name)}: {
|
|
752
779
|
Row: {
|
|
753
780
|
${[
|
|
754
|
-
...
|
|
781
|
+
...getColumns(table.id).map((column) =>
|
|
755
782
|
generateColumnTsDefinition(
|
|
756
783
|
schema,
|
|
757
784
|
{
|
|
@@ -777,7 +804,7 @@ export type Database = {
|
|
|
777
804
|
]}
|
|
778
805
|
}
|
|
779
806
|
Insert: {
|
|
780
|
-
${
|
|
807
|
+
${getColumns(table.id).map((column) => {
|
|
781
808
|
if (
|
|
782
809
|
column.identity_generation === "ALWAYS" ||
|
|
783
810
|
column.is_generated
|
|
@@ -801,7 +828,7 @@ export type Database = {
|
|
|
801
828
|
})}
|
|
802
829
|
}
|
|
803
830
|
Update: {
|
|
804
|
-
${
|
|
831
|
+
${getColumns(table.id).map((column) => {
|
|
805
832
|
if (
|
|
806
833
|
column.identity_generation === "ALWAYS" ||
|
|
807
834
|
column.is_generated
|
|
@@ -840,7 +867,7 @@ export type Database = {
|
|
|
840
867
|
}) => `${JSON.stringify(view.name)}: {
|
|
841
868
|
Row: {
|
|
842
869
|
${[
|
|
843
|
-
...
|
|
870
|
+
...getColumns(view.id).map((column) =>
|
|
844
871
|
generateColumnTsDefinition(
|
|
845
872
|
schema,
|
|
846
873
|
{
|
|
@@ -871,7 +898,7 @@ export type Database = {
|
|
|
871
898
|
// version 1 documents) falls back to the old gate.
|
|
872
899
|
(view.is_insert_enabled ?? view.is_updatable)
|
|
873
900
|
? `Insert: {
|
|
874
|
-
${
|
|
901
|
+
${getColumns(view.id).map((column) => {
|
|
875
902
|
if (!column.is_updatable) {
|
|
876
903
|
return `${JSON.stringify(column.name)}?: never`;
|
|
877
904
|
}
|
|
@@ -893,7 +920,7 @@ export type Database = {
|
|
|
893
920
|
}${
|
|
894
921
|
(view.is_update_enabled ?? view.is_updatable)
|
|
895
922
|
? `Update: {
|
|
896
|
-
${
|
|
923
|
+
${getColumns(view.id).map((column) => {
|
|
897
924
|
if (!column.is_updatable) {
|
|
898
925
|
return `${JSON.stringify(column.name)}?: never`;
|
|
899
926
|
}
|
|
@@ -926,14 +953,13 @@ export type Database = {
|
|
|
926
953
|
}
|
|
927
954
|
const schemaFunctionsGroupedByName = schemaFunctions.reduce(
|
|
928
955
|
(acc, curr) => {
|
|
929
|
-
acc[curr.fn.name] ??= [];
|
|
930
|
-
acc[curr.fn.name].push(curr);
|
|
956
|
+
(acc[curr.fn.name] ??= []).push(curr);
|
|
931
957
|
return acc;
|
|
932
958
|
},
|
|
933
959
|
{} as Record<string, typeof schemaFunctions>,
|
|
934
960
|
);
|
|
935
|
-
for (const
|
|
936
|
-
|
|
961
|
+
for (const fns of Object.values(schemaFunctionsGroupedByName)) {
|
|
962
|
+
fns.sort(
|
|
937
963
|
(a, b) =>
|
|
938
964
|
compareStrings(a.fn.argument_types, b.fn.argument_types) ||
|
|
939
965
|
compareStrings(a.fn.return_type, b.fn.return_type),
|
|
@@ -1100,7 +1126,7 @@ export type CompositeTypes<
|
|
|
1100
1126
|
|
|
1101
1127
|
export const Constants = {
|
|
1102
1128
|
${schemas.map((schema) => {
|
|
1103
|
-
const schemaEnums =
|
|
1129
|
+
const schemaEnums = getSchemaIntrospection(schema.name).enums;
|
|
1104
1130
|
return `${JSON.stringify(schema.name)}: {
|
|
1105
1131
|
Enums: {
|
|
1106
1132
|
${schemaEnums.map(
|
|
@@ -1176,10 +1202,9 @@ export const pgTypeToTsType = (
|
|
|
1176
1202
|
const enumTypes = types.filter(
|
|
1177
1203
|
(type) => type.name === pgType && type.enums.length > 0,
|
|
1178
1204
|
);
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
enumTypes[0];
|
|
1205
|
+
const enumType =
|
|
1206
|
+
enumTypes.find((type) => type.schema === preferredSchema) ?? enumTypes[0];
|
|
1207
|
+
if (enumType) {
|
|
1183
1208
|
if (schemas.some(({ name }) => name === enumType.schema)) {
|
|
1184
1209
|
return `Database[${JSON.stringify(enumType.schema)}]['Enums'][${JSON.stringify(
|
|
1185
1210
|
enumType.name,
|
|
@@ -1191,10 +1216,10 @@ export const pgTypeToTsType = (
|
|
|
1191
1216
|
const compositeTypes = types.filter(
|
|
1192
1217
|
(type) => type.name === pgType && type.attributes.length > 0,
|
|
1193
1218
|
);
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1219
|
+
const compositeType =
|
|
1220
|
+
compositeTypes.find((type) => type.schema === preferredSchema) ??
|
|
1221
|
+
compositeTypes[0];
|
|
1222
|
+
if (compositeType) {
|
|
1198
1223
|
if (schemas.some(({ name }) => name === compositeType.schema)) {
|
|
1199
1224
|
return `Database[${JSON.stringify(
|
|
1200
1225
|
compositeType.schema,
|