@supabase/postgrest-typegen 0.2.1 → 0.2.3
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 +13 -5
- package/dist/collation.d.ts +2 -0
- package/dist/collation.js +15 -0
- package/dist/generation/index.d.ts +1 -1
- package/dist/generation/python.js +21 -15
- package/dist/generation/swift.js +9 -2
- package/dist/generation/typescript.d.ts +18 -7
- package/dist/generation/typescript.js +102 -79
- package/dist/introspection/index.js +6 -23
- package/dist/introspection/relationships.d.ts +4 -4
- package/dist/introspection/relationships.js +4 -11
- package/dist/introspection/sql/columns.sql.d.ts +2 -7
- package/dist/introspection/sql/columns.sql.js +23 -17
- package/dist/introspection/sql/common.d.ts +7 -15
- package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
- package/dist/introspection/sql/foreign_tables.sql.js +0 -5
- package/dist/introspection/sql/functions.sql.d.ts +2 -5
- package/dist/introspection/sql/functions.sql.js +0 -33
- package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
- package/dist/introspection/sql/materialized_views.sql.js +0 -5
- package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
- package/dist/introspection/sql/primary_keys.sql.js +0 -4
- package/dist/introspection/sql/schemas.sql.d.ts +2 -6
- package/dist/introspection/sql/schemas.sql.js +2 -6
- package/dist/introspection/sql/table.sql.d.ts +2 -4
- package/dist/introspection/sql/table.sql.js +0 -5
- package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
- package/dist/introspection/sql/types.sql.d.ts +5 -5
- package/dist/introspection/sql/types.sql.js +6 -17
- package/dist/introspection/sql/views.sql.d.ts +2 -4
- package/dist/introspection/sql/views.sql.js +0 -5
- package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
- package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
- package/dist/sort.js +32 -16
- package/dist/types.d.ts +4 -4
- package/dist/types.js +6 -4
- package/package.json +4 -4
- package/src/collation.ts +16 -0
- package/src/generation/index.ts +1 -0
- package/src/generation/python.ts +28 -15
- package/src/generation/swift.ts +9 -2
- package/src/generation/typescript.ts +161 -104
- package/src/introspection/index.ts +7 -32
- package/src/introspection/relationships.ts +10 -19
- package/src/introspection/sql/columns.sql.ts +25 -26
- package/src/introspection/sql/common.ts +7 -17
- package/src/introspection/sql/foreign_tables.sql.ts +2 -13
- package/src/introspection/sql/functions.sql.ts +2 -44
- package/src/introspection/sql/materialized_views.sql.ts +2 -11
- package/src/introspection/sql/primary_keys.sql.ts +2 -10
- package/src/introspection/sql/schemas.sql.ts +4 -14
- package/src/introspection/sql/table.sql.ts +2 -11
- package/src/introspection/sql/table_relationships.sql.ts +2 -4
- package/src/introspection/sql/types.sql.ts +6 -26
- package/src/introspection/sql/views.sql.ts +2 -11
- package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
- package/src/sort.ts +37 -18
- package/src/types.ts +6 -4
package/README.md
CHANGED
|
@@ -17,11 +17,19 @@ JSON output, not inside `postgrest-typegen`.
|
|
|
17
17
|
|
|
18
18
|
The TypeScript, Go, Python, and Swift generators bundled here are a
|
|
19
19
|
**deliberate transition**, not the target architecture for new languages.
|
|
20
|
-
They were ported byte-
|
|
21
|
-
`supabase gen types`
|
|
22
|
-
|
|
23
|
-
move out to their own SDK repos too (tracked in SDK-1641), not yet
|
|
24
|
-
— sequenced after postgres-meta's cutover to this package settles.
|
|
20
|
+
They were ported byte-for-byte from postgres-meta's own templates so
|
|
21
|
+
`supabase gen types` kept working unchanged; postgres-meta has since deleted
|
|
22
|
+
its copies and consumes this package instead. It's decided that all four
|
|
23
|
+
eventually move out to their own SDK repos too (tracked in SDK-1641), not yet
|
|
24
|
+
scheduled — sequenced after postgres-meta's cutover to this package settles.
|
|
25
|
+
|
|
26
|
+
> [!IMPORTANT]
|
|
27
|
+
> **We are no longer accepting contributions that fix the bundled generators.**
|
|
28
|
+
> Because each of the four is moving out to its own language's SDK repository,
|
|
29
|
+
> a fix landed here would have to be made a second time in the destination
|
|
30
|
+
> repository, and postgres-meta and the CLI only pick it up once they bump
|
|
31
|
+
> their pinned version of this package. Please open an issue describing the
|
|
32
|
+
> bug instead, so it can be tracked and carried across the move.
|
|
25
33
|
|
|
26
34
|
## Design
|
|
27
35
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one string comparison every ordering in this package uses.
|
|
3
|
+
*
|
|
4
|
+
* Generated output is ordered by name, and `String.prototype.localeCompare`
|
|
5
|
+
* without a locale follows the host's default locale, so the same database
|
|
6
|
+
* produced differently ordered code on differently configured machines (a
|
|
7
|
+
* Swedish host sorts `ä` after `z`, an English one next to `a`). Pinning the
|
|
8
|
+
* collator to English keeps the order postgres-meta's output has on the
|
|
9
|
+
* English CI runners and makes it the same everywhere else.
|
|
10
|
+
*/
|
|
11
|
+
const collator = new Intl.Collator("en");
|
|
12
|
+
/** Compare two strings with the pinned English collation. */
|
|
13
|
+
export function compareStrings(a, b) {
|
|
14
|
+
return collator.compare(a, b);
|
|
15
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { sortGeneratorMetadata } from "../sort.ts";
|
|
2
2
|
export { generateGo } from "./go.ts";
|
|
3
3
|
export { generatePython } from "./python.ts";
|
|
4
|
-
export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, } from "./typescript.ts";
|
|
4
|
+
export { generateTypescript, type GenerateTypescriptOptions, pgTypeToTsType, type TypeResolutionContext, } from "./typescript.ts";
|
|
5
5
|
export { type AccessControl, generateSwift, type GenerateSwiftOptions, } from "./swift.ts";
|
|
@@ -53,7 +53,7 @@ class PythonContext {
|
|
|
53
53
|
schemas;
|
|
54
54
|
constructor(types, columns, schemas) {
|
|
55
55
|
this.schemas = Object.fromEntries(schemas.map((schema) => [schema.name, schema]));
|
|
56
|
-
this.types = Object.fromEntries(types.map((type) => [type.name, type]));
|
|
56
|
+
this.types = Object.fromEntries(types.map((type) => [qualifiedTypeName(type.schema, type.name), type]));
|
|
57
57
|
this.columns = columns.reduce((acc, curr) => {
|
|
58
58
|
acc[curr.table_id] ??= [];
|
|
59
59
|
acc[curr.table_id].push(curr);
|
|
@@ -61,30 +61,35 @@ class PythonContext {
|
|
|
61
61
|
}, {});
|
|
62
62
|
this.user_enums = Object.fromEntries(types
|
|
63
63
|
.filter((type) => type.enums.length > 0)
|
|
64
|
-
.map((type) => [
|
|
64
|
+
.map((type) => [
|
|
65
|
+
qualifiedTypeName(type.schema, type.name),
|
|
66
|
+
new PythonEnum(type),
|
|
67
|
+
]));
|
|
65
68
|
}
|
|
66
|
-
resolveTypeName(name) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
resolveTypeName(name, typeSchema) {
|
|
70
|
+
const qualified = qualifiedTypeName(typeSchema, name);
|
|
71
|
+
if (qualified in this.user_enums) {
|
|
72
|
+
return this.user_enums[qualified].name;
|
|
69
73
|
}
|
|
70
74
|
if (name in PY_TYPE_MAP) {
|
|
71
75
|
return PY_TYPE_MAP[name];
|
|
72
76
|
}
|
|
73
|
-
if (
|
|
74
|
-
const type = this.types[
|
|
75
|
-
|
|
76
|
-
return `${formatForPyClassName(schema)}${formatForPyClassName(type.name)}`;
|
|
77
|
+
if (qualified in this.types) {
|
|
78
|
+
const type = this.types[qualified];
|
|
79
|
+
return `${formatForPyClassName(type.schema)}${formatForPyClassName(type.name)}`;
|
|
77
80
|
}
|
|
78
81
|
return "Any";
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
// Array types live in the schema of their element type, so `typeSchema`
|
|
84
|
+
// carries over to the element.
|
|
85
|
+
parsePgType(pg_type, typeSchema) {
|
|
81
86
|
if (pg_type.startsWith("_")) {
|
|
82
87
|
const inner_str = pg_type.slice(1);
|
|
83
|
-
const inner = this.parsePgType(inner_str);
|
|
88
|
+
const inner = this.parsePgType(inner_str, typeSchema);
|
|
84
89
|
return new PythonListType(inner);
|
|
85
90
|
}
|
|
86
91
|
else {
|
|
87
|
-
const type_name = this.resolveTypeName(pg_type);
|
|
92
|
+
const type_name = this.resolveTypeName(pg_type, typeSchema);
|
|
88
93
|
return new PythonSimpleType(type_name);
|
|
89
94
|
}
|
|
90
95
|
}
|
|
@@ -98,7 +103,7 @@ class PythonContext {
|
|
|
98
103
|
};
|
|
99
104
|
});
|
|
100
105
|
const attributeEntries = attributes.map((attribute) => {
|
|
101
|
-
const type = this.parsePgType(attribute.type.name);
|
|
106
|
+
const type = this.parsePgType(attribute.type.name, attribute.type.schema);
|
|
102
107
|
// Composite type attributes cannot carry NOT NULL constraints in
|
|
103
108
|
// Postgres, so every field is inherently nullable.
|
|
104
109
|
return new PythonBaseModelAttr(attribute.name, type, true);
|
|
@@ -109,14 +114,14 @@ class PythonContext {
|
|
|
109
114
|
columnsToClassAttrs(table_id) {
|
|
110
115
|
const attrs = this.columns[table_id] ?? [];
|
|
111
116
|
return attrs.map((col) => {
|
|
112
|
-
const type = this.parsePgType(col.format);
|
|
117
|
+
const type = this.parsePgType(col.format, col.type_schema);
|
|
113
118
|
return new PythonBaseModelAttr(col.name, type, col.is_nullable);
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
121
|
columnsToDictAttrs(table_id, not_required) {
|
|
117
122
|
const attrs = this.columns[table_id] ?? [];
|
|
118
123
|
return attrs.map((col) => {
|
|
119
|
-
const type = this.parsePgType(col.format);
|
|
124
|
+
const type = this.parsePgType(col.format, col.type_schema);
|
|
120
125
|
return new PythonTypedDictAttr(col.name, type, col.is_nullable, not_required ||
|
|
121
126
|
col.is_nullable ||
|
|
122
127
|
col.is_identity ||
|
|
@@ -139,6 +144,7 @@ class PythonContext {
|
|
|
139
144
|
return new PythonBaseModel(matview.name, this.schemas[matview.schema], attributes);
|
|
140
145
|
}
|
|
141
146
|
}
|
|
147
|
+
const qualifiedTypeName = (schema, name) => `${schema}.${name}`;
|
|
142
148
|
class PythonEnum {
|
|
143
149
|
name;
|
|
144
150
|
variants;
|
package/dist/generation/swift.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareStrings } from "../collation.js";
|
|
1
2
|
function formatForSwiftSchemaName(schema) {
|
|
2
3
|
return `${formatForSwiftTypeName(schema)}Schema`;
|
|
3
4
|
}
|
|
@@ -186,10 +187,10 @@ export const generateSwift = (metadata, opts = {}) => {
|
|
|
186
187
|
...schemas.flatMap((schema) => {
|
|
187
188
|
const schemaTables = [...tables, ...foreignTables]
|
|
188
189
|
.filter((table) => table.schema === schema.name)
|
|
189
|
-
.sort(({ name: a }, { name: b }) => a
|
|
190
|
+
.sort(({ name: a }, { name: b }) => compareStrings(a, b));
|
|
190
191
|
const schemaViews = [...views, ...materializedViews]
|
|
191
192
|
.filter((table) => table.schema === schema.name)
|
|
192
|
-
.sort(({ name: a }, { name: b }) => a
|
|
193
|
+
.sort(({ name: a }, { name: b }) => compareStrings(a, b));
|
|
193
194
|
const schemaEnums = types.filter((type) => type.schema === schema.name && type.enums.length > 0);
|
|
194
195
|
const schemaCompositeTypes = types.filter((type) => type.schema === schema.name && type.attributes.length > 0);
|
|
195
196
|
return [
|
|
@@ -262,6 +263,12 @@ const pgTypeToSwiftType = (pgType, nullable, { types, views, tables, }) => {
|
|
|
262
263
|
}
|
|
263
264
|
else if (["json", "jsonb"].includes(pgType)) {
|
|
264
265
|
swiftType = "AnyJSON";
|
|
266
|
+
// `void` and `record` are pseudo-types: Postgres rejects them as a column or
|
|
267
|
+
// composite attribute type ("column has pseudo-type void"), and this
|
|
268
|
+
// generator only renders columns and attributes, never functions. So these
|
|
269
|
+
// two arms are unreachable today and deliberately untested. They are kept
|
|
270
|
+
// because they are the mappings to use if Swift ever emits functions, which
|
|
271
|
+
// is where these types do appear.
|
|
265
272
|
}
|
|
266
273
|
else if (pgType === "void") {
|
|
267
274
|
swiftType = "Void";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GeneratorMetadata, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
|
|
1
|
+
import type { GeneratorMetadata, PostgresForeignTable, PostgresMaterializedView, PostgresSchema, PostgresTable, PostgresType, PostgresView } from "../types.ts";
|
|
2
2
|
export interface GenerateTypescriptOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Emit `isOneToOne` on each relationship entry. Mirrors postgres-meta's
|
|
@@ -24,6 +24,22 @@ export interface GenerateTypescriptOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
format?: (code: string) => Promise<string>;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Collections `pgTypeToTsType` resolves a relation-typed value against.
|
|
29
|
+
*
|
|
30
|
+
* Foreign tables are generated under `Tables` and materialized views under
|
|
31
|
+
* `Views`, exactly like their plain counterparts, so a value typed as one has
|
|
32
|
+
* a `Row` to point at and belongs here. Both are optional so the exported
|
|
33
|
+
* `pgTypeToTsType` signature stays backwards compatible.
|
|
34
|
+
*/
|
|
35
|
+
export type TypeResolutionContext = {
|
|
36
|
+
types: PostgresType[];
|
|
37
|
+
schemas: PostgresSchema[];
|
|
38
|
+
tables: PostgresTable[];
|
|
39
|
+
views: PostgresView[];
|
|
40
|
+
foreignTables?: PostgresForeignTable[];
|
|
41
|
+
materializedViews?: PostgresMaterializedView[];
|
|
42
|
+
};
|
|
27
43
|
/**
|
|
28
44
|
* Does not re-sort collections; pass input pre-sorted with
|
|
29
45
|
* `sortGeneratorMetadata` first. Still merge-sorts tables+foreign tables and
|
|
@@ -31,9 +47,4 @@ export interface GenerateTypescriptOptions {
|
|
|
31
47
|
* can't be expressed by the single-collection `sortGeneratorMetadata` pass.
|
|
32
48
|
*/
|
|
33
49
|
export declare const generateTypescript: (metadata: GeneratorMetadata, opts?: GenerateTypescriptOptions) => Promise<string>;
|
|
34
|
-
export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string,
|
|
35
|
-
types: PostgresType[];
|
|
36
|
-
schemas: PostgresSchema[];
|
|
37
|
-
tables: PostgresTable[];
|
|
38
|
-
views: PostgresView[];
|
|
39
|
-
}, typeSchema?: string) => string;
|
|
50
|
+
export declare const pgTypeToTsType: (schema: PostgresSchema, pgType: string, context: TypeResolutionContext, typeSchema?: string) => string;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// disable comment would corrupt the emitted string.
|
|
6
6
|
import { format as oxfmtFormat } from "oxfmt";
|
|
7
7
|
import { VALID_FUNCTION_ARGS_MODE, VALID_UNNAMED_FUNCTION_ARG_TYPES, } from "./constants.js";
|
|
8
|
+
import { compareStrings } from "../collation.js";
|
|
8
9
|
const defaultFormat = async (code) => {
|
|
9
10
|
const { code: formatted, errors } = await oxfmtFormat("output.ts", code, {
|
|
10
11
|
semi: false,
|
|
@@ -26,6 +27,16 @@ const defaultFormat = async (code) => {
|
|
|
26
27
|
export const generateTypescript = async (metadata, opts = {}) => {
|
|
27
28
|
const { schemas, tables, foreignTables, views, materializedViews, columns, relationships, functions, types, } = metadata;
|
|
28
29
|
const { detectOneToOneRelationships = false, postgrestVersion, defaultSchema = "public", format = defaultFormat, } = opts;
|
|
30
|
+
// Every relation-typed value resolves against the same collections, so build
|
|
31
|
+
// the lookup context once rather than reassembling it at each call site.
|
|
32
|
+
const typeContext = {
|
|
33
|
+
types,
|
|
34
|
+
schemas,
|
|
35
|
+
tables,
|
|
36
|
+
views,
|
|
37
|
+
foreignTables,
|
|
38
|
+
materializedViews,
|
|
39
|
+
};
|
|
29
40
|
// Ordering of all collections (incl. relationships, columns, args below) is
|
|
30
41
|
// provided by `sortGeneratorMetadata`; this generator no longer re-sorts.
|
|
31
42
|
const introspectionBySchema = Object.fromEntries(schemas.map((s) => [
|
|
@@ -178,11 +189,11 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
178
189
|
// views — so the cross-collection name sort can't be expressed by the
|
|
179
190
|
// single-collection `sortGeneratorMetadata` pass and stays here.
|
|
180
191
|
for (const schema in introspectionBySchema) {
|
|
181
|
-
introspectionBySchema[schema].tables.sort((a, b) => a.table.name
|
|
182
|
-
introspectionBySchema[schema].views.sort((a, b) => a.view.name
|
|
183
|
-
introspectionBySchema[schema].functions.sort((a, b) => a.fn.name
|
|
184
|
-
introspectionBySchema[schema].enums.sort((a, b) => a.name
|
|
185
|
-
introspectionBySchema[schema].compositeTypes.sort((a, b) => a.name
|
|
192
|
+
introspectionBySchema[schema].tables.sort((a, b) => compareStrings(a.table.name, b.table.name));
|
|
193
|
+
introspectionBySchema[schema].views.sort((a, b) => compareStrings(a.view.name, b.view.name));
|
|
194
|
+
introspectionBySchema[schema].functions.sort((a, b) => compareStrings(a.fn.name, b.fn.name));
|
|
195
|
+
introspectionBySchema[schema].enums.sort((a, b) => compareStrings(a.name, b.name));
|
|
196
|
+
introspectionBySchema[schema].compositeTypes.sort((a, b) => compareStrings(a.name, b.name));
|
|
186
197
|
}
|
|
187
198
|
const getFunctionTsReturnType = (fn, returnType) => {
|
|
188
199
|
// Determine if this function should have SetofOptions
|
|
@@ -244,12 +255,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
244
255
|
const type = typesById.get(type_id);
|
|
245
256
|
let tsType = "unknown";
|
|
246
257
|
if (type) {
|
|
247
|
-
tsType = pgTypeToTsType(schema, type.name,
|
|
248
|
-
types,
|
|
249
|
-
schemas,
|
|
250
|
-
tables,
|
|
251
|
-
views,
|
|
252
|
-
});
|
|
258
|
+
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
253
259
|
}
|
|
254
260
|
return { name, type: tsType };
|
|
255
261
|
});
|
|
@@ -269,24 +275,14 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
269
275
|
type_schema: column.type_schema,
|
|
270
276
|
is_nullable: column.is_nullable,
|
|
271
277
|
is_optional: false,
|
|
272
|
-
},
|
|
273
|
-
types,
|
|
274
|
-
schemas,
|
|
275
|
-
tables,
|
|
276
|
-
views,
|
|
277
|
-
}))
|
|
278
|
+
}, typeContext))
|
|
278
279
|
.join(",\n")}
|
|
279
280
|
}`;
|
|
280
281
|
}
|
|
281
282
|
// Case 3: returns base/array/composite/enum type.
|
|
282
283
|
const type = typesById.get(fn.return_type_id);
|
|
283
284
|
if (type) {
|
|
284
|
-
return pgTypeToTsType(schema, type.name,
|
|
285
|
-
types,
|
|
286
|
-
schemas,
|
|
287
|
-
tables,
|
|
288
|
-
views,
|
|
289
|
-
});
|
|
285
|
+
return pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
290
286
|
}
|
|
291
287
|
return "unknown";
|
|
292
288
|
};
|
|
@@ -369,12 +365,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
369
365
|
const type = typesById.get(type_id);
|
|
370
366
|
let tsType = "unknown";
|
|
371
367
|
if (type) {
|
|
372
|
-
tsType = pgTypeToTsType(schema, type.name,
|
|
373
|
-
types,
|
|
374
|
-
schemas,
|
|
375
|
-
tables,
|
|
376
|
-
views,
|
|
377
|
-
});
|
|
368
|
+
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
378
369
|
}
|
|
379
370
|
return { name, type: tsType, has_default };
|
|
380
371
|
});
|
|
@@ -389,12 +380,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
389
380
|
const type = typesById.get(type_id);
|
|
390
381
|
let tsType = "unknown";
|
|
391
382
|
if (type) {
|
|
392
|
-
tsType = pgTypeToTsType(schema, type.name,
|
|
393
|
-
types,
|
|
394
|
-
schemas,
|
|
395
|
-
tables,
|
|
396
|
-
views,
|
|
397
|
-
});
|
|
383
|
+
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
398
384
|
}
|
|
399
385
|
return { name, type: tsType, has_default };
|
|
400
386
|
});
|
|
@@ -407,12 +393,7 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
407
393
|
const type = typesById.get(type_id);
|
|
408
394
|
let tsType = "unknown";
|
|
409
395
|
if (type) {
|
|
410
|
-
tsType = pgTypeToTsType(schema, type.name,
|
|
411
|
-
types,
|
|
412
|
-
schemas,
|
|
413
|
-
tables,
|
|
414
|
-
views,
|
|
415
|
-
});
|
|
396
|
+
tsType = pgTypeToTsType(schema, type.name, typeContext, type.schema);
|
|
416
397
|
}
|
|
417
398
|
return { name, type: tsType, has_default };
|
|
418
399
|
});
|
|
@@ -444,6 +425,38 @@ export const generateTypescript = async (metadata, opts = {}) => {
|
|
|
444
425
|
function generateColumnTsDefinition(schema, column, context) {
|
|
445
426
|
return `${JSON.stringify(column.name)}${column.is_optional ? "?" : ""}: ${generateNullableUnionTsType(pgTypeToTsType(schema, column.format, context, column.type_schema), column.is_nullable)}`;
|
|
446
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Is `fn` a PostgREST computed field on `relation` (a table or view)?
|
|
430
|
+
*
|
|
431
|
+
* A computed field is a function taking a single argument of the relation's
|
|
432
|
+
* own composite type, so it is selectable as if it were a column and belongs
|
|
433
|
+
* in `Row`.
|
|
434
|
+
*
|
|
435
|
+
* Matching on the argument's type OID rather than on `argument_types` is what
|
|
436
|
+
* makes named parameters work. `argument_types` is
|
|
437
|
+
* `pg_get_function_arguments()`, which renders the parameter *name* when the
|
|
438
|
+
* parameter has one — `category category` rather than `category` — and
|
|
439
|
+
* schema-qualifies the type when it is not visible in `search_path`. Both
|
|
440
|
+
* forms failed a bare string comparison against the relation name, so
|
|
441
|
+
* `create function name_translated(category category)` silently lost its
|
|
442
|
+
* computed field while the unnamed `name_translated(category)` kept it.
|
|
443
|
+
* See supabase/postgres-meta#1034.
|
|
444
|
+
*/
|
|
445
|
+
const isComputedFieldOf = (fn, inArgs, relation) => {
|
|
446
|
+
if (inArgs.length === 1) {
|
|
447
|
+
// `relationTypeByIds` is already "types backed by a relation", so a hit
|
|
448
|
+
// identifies the relation regardless of how the argument was spelled.
|
|
449
|
+
// A `variadic` argument is an array type and never appears here.
|
|
450
|
+
const argType = relationTypeByIds.get(inArgs[0].type_id);
|
|
451
|
+
if (argType) {
|
|
452
|
+
return argType.type_relation_id === relation.id;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Fall back to the historical comparison when the argument's type is not in
|
|
456
|
+
// the metadata (callers may pass a reduced `types` list), so a computed
|
|
457
|
+
// field that resolved before keeps resolving.
|
|
458
|
+
return fn.argument_types === relation.name;
|
|
459
|
+
};
|
|
447
460
|
let output = `
|
|
448
461
|
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
|
449
462
|
|
|
@@ -464,9 +477,9 @@ export type Database = {
|
|
|
464
477
|
type_schema: column.type_schema,
|
|
465
478
|
is_nullable: column.is_nullable,
|
|
466
479
|
is_optional: false,
|
|
467
|
-
},
|
|
480
|
+
}, typeContext)),
|
|
468
481
|
...schemaFunctions
|
|
469
|
-
.filter(({ fn }) => fn
|
|
482
|
+
.filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, table))
|
|
470
483
|
.map(({ fn }) => {
|
|
471
484
|
return `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`;
|
|
472
485
|
}),
|
|
@@ -486,7 +499,7 @@ export type Database = {
|
|
|
486
499
|
is_optional: column.is_nullable ||
|
|
487
500
|
column.is_identity ||
|
|
488
501
|
column.default_value !== null,
|
|
489
|
-
},
|
|
502
|
+
}, typeContext);
|
|
490
503
|
})}
|
|
491
504
|
}
|
|
492
505
|
Update: {
|
|
@@ -501,7 +514,7 @@ export type Database = {
|
|
|
501
514
|
type_schema: column.type_schema,
|
|
502
515
|
is_nullable: column.is_nullable,
|
|
503
516
|
is_optional: true,
|
|
504
|
-
},
|
|
517
|
+
}, typeContext);
|
|
505
518
|
})}
|
|
506
519
|
}
|
|
507
520
|
Relationships: [
|
|
@@ -521,13 +534,16 @@ export type Database = {
|
|
|
521
534
|
type_schema: column.type_schema,
|
|
522
535
|
is_nullable: column.is_nullable,
|
|
523
536
|
is_optional: false,
|
|
524
|
-
},
|
|
537
|
+
}, typeContext)),
|
|
525
538
|
...schemaFunctions
|
|
526
|
-
.filter(({ fn }) => fn
|
|
539
|
+
.filter(({ fn, inArgs }) => isComputedFieldOf(fn, inArgs, view))
|
|
527
540
|
.map(({ fn }) => `${JSON.stringify(fn.name)}: ${generateNullableUnionTsType(getFunctionReturnType(schema, fn), true)}`),
|
|
528
541
|
]}
|
|
529
542
|
}
|
|
530
|
-
${
|
|
543
|
+
${
|
|
544
|
+
// Metadata predating the trigger-aware flags (still valid
|
|
545
|
+
// version 1 documents) falls back to the old gate.
|
|
546
|
+
(view.is_insert_enabled ?? view.is_updatable)
|
|
531
547
|
? `Insert: {
|
|
532
548
|
${columnsByTableId[view.id].map((column) => {
|
|
533
549
|
if (!column.is_updatable) {
|
|
@@ -539,11 +555,11 @@ export type Database = {
|
|
|
539
555
|
type_schema: column.type_schema,
|
|
540
556
|
is_nullable: true,
|
|
541
557
|
is_optional: true,
|
|
542
|
-
},
|
|
558
|
+
}, typeContext);
|
|
543
559
|
})}
|
|
544
560
|
}
|
|
545
561
|
`
|
|
546
|
-
: ""}${view.is_update_enabled
|
|
562
|
+
: ""}${(view.is_update_enabled ?? view.is_updatable)
|
|
547
563
|
? `Update: {
|
|
548
564
|
${columnsByTableId[view.id].map((column) => {
|
|
549
565
|
if (!column.is_updatable) {
|
|
@@ -555,7 +571,7 @@ export type Database = {
|
|
|
555
571
|
type_schema: column.type_schema,
|
|
556
572
|
is_nullable: true,
|
|
557
573
|
is_optional: true,
|
|
558
|
-
},
|
|
574
|
+
}, typeContext);
|
|
559
575
|
})}
|
|
560
576
|
}
|
|
561
577
|
`
|
|
@@ -575,8 +591,8 @@ export type Database = {
|
|
|
575
591
|
return acc;
|
|
576
592
|
}, {});
|
|
577
593
|
for (const fnName in schemaFunctionsGroupedByName) {
|
|
578
|
-
schemaFunctionsGroupedByName[fnName].sort((a, b) => a.fn.argument_types
|
|
579
|
-
a.fn.return_type
|
|
594
|
+
schemaFunctionsGroupedByName[fnName].sort((a, b) => compareStrings(a.fn.argument_types, b.fn.argument_types) ||
|
|
595
|
+
compareStrings(a.fn.return_type, b.fn.return_type));
|
|
580
596
|
}
|
|
581
597
|
return Object.entries(schemaFunctionsGroupedByName)
|
|
582
598
|
.map(([fnName, fns]) => {
|
|
@@ -601,12 +617,7 @@ export type Database = {
|
|
|
601
617
|
const type = typesById.get(type_id);
|
|
602
618
|
let tsType = "unknown";
|
|
603
619
|
if (type) {
|
|
604
|
-
tsType = `${generateNullableUnionTsType(pgTypeToTsType(schema, type.name,
|
|
605
|
-
types,
|
|
606
|
-
schemas,
|
|
607
|
-
tables,
|
|
608
|
-
views,
|
|
609
|
-
}), true)}`;
|
|
620
|
+
tsType = `${generateNullableUnionTsType(pgTypeToTsType(schema, type.name, typeContext, type.schema), true)}`;
|
|
610
621
|
}
|
|
611
622
|
return `${JSON.stringify(name)}: ${tsType}`;
|
|
612
623
|
})}
|
|
@@ -738,12 +749,14 @@ export const Constants = {
|
|
|
738
749
|
return output;
|
|
739
750
|
};
|
|
740
751
|
// TODO: Make this more robust. Currently doesn't handle range types - returns them as unknown.
|
|
741
|
-
export const pgTypeToTsType = (schema, pgType,
|
|
742
|
-
// The schema that actually owns `pgType
|
|
743
|
-
//
|
|
744
|
-
// back to `schema.name` (the
|
|
745
|
-
//
|
|
752
|
+
export const pgTypeToTsType = (schema, pgType, context,
|
|
753
|
+
// The schema that actually owns `pgType`, from the column's `type_schema` or
|
|
754
|
+
// the resolved `PostgresType.schema`. Disambiguates same-named enums,
|
|
755
|
+
// composites and relations across schemas. Falls back to `schema.name` (the
|
|
756
|
+
// schema being generated) when not provided, which is only a guess, so pass
|
|
757
|
+
// it whenever the owning schema is known.
|
|
746
758
|
typeSchema) => {
|
|
759
|
+
const { types, schemas, tables, views, foreignTables = [], materializedViews = [], } = context;
|
|
747
760
|
if (pgType === "bool") {
|
|
748
761
|
return "boolean";
|
|
749
762
|
}
|
|
@@ -777,7 +790,7 @@ typeSchema) => {
|
|
|
777
790
|
return "Record<string, unknown>";
|
|
778
791
|
}
|
|
779
792
|
else if (pgType.startsWith("_")) {
|
|
780
|
-
return `(${pgTypeToTsType(schema, pgType.substring(1),
|
|
793
|
+
return `(${pgTypeToTsType(schema, pgType.substring(1), context, typeSchema)})[]`;
|
|
781
794
|
}
|
|
782
795
|
else {
|
|
783
796
|
const preferredSchema = typeSchema ?? schema.name;
|
|
@@ -799,21 +812,31 @@ typeSchema) => {
|
|
|
799
812
|
}
|
|
800
813
|
return "unknown";
|
|
801
814
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
815
|
+
// A relation-typed value carries only a bare type name, so several relations
|
|
816
|
+
// can share it and `preferredSchema` is what disambiguates them. Match on
|
|
817
|
+
// the schema across every relation kind before falling back to kind order,
|
|
818
|
+
// otherwise a relation in an unrelated schema outranks the one the caller
|
|
819
|
+
// meant. Foreign tables are generated under `Tables` and materialized views
|
|
820
|
+
// under `Views`, alongside their plain counterparts.
|
|
821
|
+
const relationKinds = [
|
|
822
|
+
["Tables", tables],
|
|
823
|
+
["Tables", foreignTables],
|
|
824
|
+
["Views", views],
|
|
825
|
+
["Views", materializedViews],
|
|
826
|
+
];
|
|
827
|
+
const findRelation = (matches) => {
|
|
828
|
+
for (const [kind, relations] of relationKinds) {
|
|
829
|
+
const relation = relations.find(matches);
|
|
830
|
+
if (relation)
|
|
831
|
+
return { kind, relation };
|
|
808
832
|
}
|
|
809
|
-
return
|
|
810
|
-
}
|
|
811
|
-
const
|
|
812
|
-
if (
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
return `Database[${JSON.stringify(viewRowType.schema)}]['Views'][${JSON.stringify(viewRowType.name)}]['Row']`;
|
|
833
|
+
return undefined;
|
|
834
|
+
};
|
|
835
|
+
const match = findRelation(({ name, schema: relationSchema }) => name === pgType && relationSchema === preferredSchema) ?? findRelation(({ name }) => name === pgType);
|
|
836
|
+
if (match) {
|
|
837
|
+
const { kind, relation } = match;
|
|
838
|
+
if (schemas.some(({ name }) => name === relation.schema)) {
|
|
839
|
+
return `Database[${JSON.stringify(relation.schema)}]['${kind}'][${JSON.stringify(relation.name)}]['Row']`;
|
|
817
840
|
}
|
|
818
841
|
return "unknown";
|
|
819
842
|
}
|
|
@@ -22,47 +22,30 @@ import { VIEWS_SQL } from "./sql/views.sql.js";
|
|
|
22
22
|
* responsibility — unlike `getGeneratorMetadata`, this does not end the pool.
|
|
23
23
|
*/
|
|
24
24
|
export async function introspect(db, opts = {}) {
|
|
25
|
-
const includedSchemas = opts.includedSchemas ?? [];
|
|
26
|
-
const excludedSchemas = opts.excludedSchemas ?? [];
|
|
27
|
-
// Managers receive `undefined` (not an empty array) when no filter is set.
|
|
28
|
-
const included = includedSchemas.length > 0 ? includedSchemas : undefined;
|
|
29
|
-
const excluded = excludedSchemas.length > 0 ? excludedSchemas : undefined;
|
|
30
25
|
// Most queries exclude the system schemas by default; foreign tables and
|
|
31
26
|
// materialized views do not (matching the respective manager `list()` calls).
|
|
32
|
-
const systemExcludingFilter = filterByList(
|
|
33
|
-
const plainFilter = filterByList(
|
|
27
|
+
const systemExcludingFilter = filterByList(opts.includedSchemas, opts.excludedSchemas, DEFAULT_SYSTEM_SCHEMAS);
|
|
28
|
+
const plainFilter = filterByList(opts.includedSchemas, opts.excludedSchemas);
|
|
34
29
|
const queryRows = async (sql) => normalizeRows((await db.query(sql)).rows);
|
|
35
30
|
// Issued concurrently, matching postgres-meta's CLI path which ran its nine
|
|
36
31
|
// introspection queries under Promise.all. A pooled Queryable runs them in
|
|
37
32
|
// parallel; a single-connection one pipelines them, which is no worse than
|
|
38
33
|
// awaiting each in turn.
|
|
39
34
|
const [schemas, tables, foreignTables, views, materializedViews, columns, primaryKeys, relationships, functions, types,] = await Promise.all([
|
|
40
|
-
queryRows(SCHEMAS_SQL({
|
|
41
|
-
includeSystemSchemas: false,
|
|
42
|
-
nameFilter: systemExcludingFilter,
|
|
43
|
-
})),
|
|
35
|
+
queryRows(SCHEMAS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
44
36
|
queryRows(TABLES_SQL({ schemaFilter: systemExcludingFilter })),
|
|
45
37
|
queryRows(FOREIGN_TABLES_SQL({ schemaFilter: plainFilter })),
|
|
46
38
|
queryRows(VIEWS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
47
39
|
queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
|
|
48
40
|
queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
49
41
|
queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
50
|
-
listRelationships(db,
|
|
51
|
-
includedSchemas: included,
|
|
52
|
-
excludedSchemas: excluded,
|
|
53
|
-
}),
|
|
42
|
+
listRelationships(db, systemExcludingFilter),
|
|
54
43
|
queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
55
|
-
|
|
56
|
-
queryRows(TYPES_SQL({
|
|
57
|
-
schemaFilter: "",
|
|
58
|
-
includeTableTypes: true,
|
|
59
|
-
includeArrayTypes: true,
|
|
60
|
-
})),
|
|
44
|
+
queryRows(TYPES_SQL),
|
|
61
45
|
]);
|
|
62
46
|
return {
|
|
63
47
|
version: GENERATOR_METADATA_VERSION,
|
|
64
|
-
schemas
|
|
65
|
-
(includedSchemas.length === 0 || includedSchemas.includes(name))),
|
|
48
|
+
schemas,
|
|
66
49
|
tables,
|
|
67
50
|
foreignTables,
|
|
68
51
|
views,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PostgresRelationship } from "../types.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Queryable } from "./index.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Port of `postgres-meta/src/lib/PostgresMetaRelationships.ts`.
|
|
5
5
|
*
|
|
@@ -39,9 +39,9 @@ export type ViewKeyDependency = {
|
|
|
39
39
|
export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[]): PostgresRelationship[];
|
|
40
40
|
/**
|
|
41
41
|
* List all relationships (table↔table plus the view-derived ones) for the
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
|
|
43
|
+
* `introspect()` builds with the system schemas excluded. Errors from the
|
|
44
44
|
* injected `Queryable` propagate by throwing — callers adapt.
|
|
45
45
|
*/
|
|
46
|
-
export declare function listRelationships(db: Queryable,
|
|
46
|
+
export declare function listRelationships(db: Queryable, schemaFilter: string): Promise<PostgresRelationship[]>;
|
|
47
47
|
export {};
|