@supabase/postgrest-typegen 0.6.0 → 0.7.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/dist/introspection/index.d.ts +6 -0
- package/dist/introspection/index.js +1 -1
- package/dist/introspection/relationships.d.ts +15 -7
- package/dist/introspection/relationships.js +41 -19
- package/dist/introspection/sql/views_key_dependencies.sql.d.ts +3 -1
- package/dist/introspection/sql/views_key_dependencies.sql.js +14 -2
- package/package.json +4 -4
- package/src/introspection/index.ts +7 -1
- package/src/introspection/relationships.ts +84 -26
- package/src/introspection/sql/views_key_dependencies.sql.ts +17 -3
|
@@ -13,6 +13,12 @@ export interface Queryable {
|
|
|
13
13
|
export interface IntrospectOptions {
|
|
14
14
|
includedSchemas?: string[];
|
|
15
15
|
excludedSchemas?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* PostgREST's `db-extra-search-path` schemas. A view in a generated schema
|
|
18
|
+
* that selects from a view in one of them keeps the relationships of the
|
|
19
|
+
* underlying tables, as in PostgREST. Nothing in these schemas is generated.
|
|
20
|
+
*/
|
|
21
|
+
extraSearchPath?: string[];
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Introspect a database into the `GeneratorMetadata` contract.
|
|
@@ -42,7 +42,7 @@ export async function introspect(db, opts = {}) {
|
|
|
42
42
|
queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
|
|
43
43
|
queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
44
44
|
queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
45
|
-
listRelationships(db, systemExcludingFilter),
|
|
45
|
+
listRelationships(db, systemExcludingFilter, opts.extraSearchPath),
|
|
46
46
|
queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
47
47
|
queryRows(TYPES_SQL),
|
|
48
48
|
queryRows(VIEW_DEFINITIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
@@ -10,7 +10,8 @@ import type { Queryable } from "./index.ts";
|
|
|
10
10
|
* by mapping a base table's foreign key columns onto the view columns that
|
|
11
11
|
* select them (`VIEWS_KEY_DEPENDENCIES_SQL` + the cartesian-product
|
|
12
12
|
* expansion below). This second part is pure TypeScript, not SQL — ported
|
|
13
|
-
*
|
|
13
|
+
* from postgres-meta, except that the expansion sees every non-system
|
|
14
|
+
* schema's foreign keys, as PostgREST does (see `listRelationships`).
|
|
14
15
|
*
|
|
15
16
|
* Adapted from:
|
|
16
17
|
* https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L392
|
|
@@ -33,15 +34,22 @@ export type ViewKeyDependency = {
|
|
|
33
34
|
* view↔view relationships implied by the view key dependencies. Pure function
|
|
34
35
|
* (no database) so the cartesian-product expansion can be unit-tested directly.
|
|
35
36
|
*
|
|
36
|
-
* Ported
|
|
37
|
-
* `allViewM2oAndO2oRelationships` block).
|
|
37
|
+
* Ported from `PostgresMetaRelationships.list()` (the body of the
|
|
38
|
+
* `allViewM2oAndO2oRelationships` block). Every table relationship feeds the
|
|
39
|
+
* expansion, but a table→view relationship is emitted on the table itself, so
|
|
40
|
+
* `isTableEmitted` keeps it to the tables the caller returns relationships for.
|
|
38
41
|
*/
|
|
39
|
-
export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[]): PostgresRelationship[];
|
|
42
|
+
export declare function expandViewRelationships(allTableM2oAndO2oRelationships: PostgresRelationship[], viewsKeyDependencies: ViewKeyDependency[], isTableEmitted: (relationship: PostgresRelationship) => boolean): PostgresRelationship[];
|
|
40
43
|
/**
|
|
41
44
|
* List all relationships (table↔table plus the view-derived ones) for the
|
|
42
45
|
* schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
|
|
43
|
-
* `introspect()` builds with the system schemas excluded.
|
|
44
|
-
*
|
|
46
|
+
* `introspect()` builds with the system schemas excluded. Like PostgREST, view
|
|
47
|
+
* expansion sees the foreign keys of every non-system schema, so a view over a
|
|
48
|
+
* table in a schema that is not selected keeps the table's foreign keys; only
|
|
49
|
+
* the views and tables in the selected schemas get relationships. Views in the
|
|
50
|
+
* `extraSearchPath` schemas are followed to their base tables but get no
|
|
51
|
+
* relationships of their own. Errors from the injected `Queryable` propagate
|
|
52
|
+
* by throwing — callers adapt.
|
|
45
53
|
*/
|
|
46
|
-
export declare function listRelationships(db: Queryable, schemaFilter: string): Promise<PostgresRelationship[]>;
|
|
54
|
+
export declare function listRelationships(db: Queryable, schemaFilter: string, extraSearchPath?: string[]): Promise<PostgresRelationship[]>;
|
|
47
55
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.js";
|
|
1
2
|
import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.js";
|
|
2
3
|
import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.js";
|
|
3
4
|
/**
|
|
@@ -5,10 +6,22 @@ import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.js"
|
|
|
5
6
|
* view↔view relationships implied by the view key dependencies. Pure function
|
|
6
7
|
* (no database) so the cartesian-product expansion can be unit-tested directly.
|
|
7
8
|
*
|
|
8
|
-
* Ported
|
|
9
|
-
* `allViewM2oAndO2oRelationships` block).
|
|
9
|
+
* Ported from `PostgresMetaRelationships.list()` (the body of the
|
|
10
|
+
* `allViewM2oAndO2oRelationships` block). Every table relationship feeds the
|
|
11
|
+
* expansion, but a table→view relationship is emitted on the table itself, so
|
|
12
|
+
* `isTableEmitted` keeps it to the tables the caller returns relationships for.
|
|
10
13
|
*/
|
|
11
|
-
export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies) {
|
|
14
|
+
export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKeyDependencies, isTableEmitted) {
|
|
15
|
+
const keyDepsByConstraint = new Map();
|
|
16
|
+
for (const vkd of viewsKeyDependencies) {
|
|
17
|
+
const key = keyDependencyKey(vkd.table_schema, vkd.table_name, vkd.constraint_name, vkd.constraint_type);
|
|
18
|
+
let bucket = keyDepsByConstraint.get(key);
|
|
19
|
+
if (!bucket) {
|
|
20
|
+
bucket = [];
|
|
21
|
+
keyDepsByConstraint.set(key, bucket);
|
|
22
|
+
}
|
|
23
|
+
bucket.push(vkd);
|
|
24
|
+
}
|
|
12
25
|
return allTableM2oAndO2oRelationships.flatMap((r) => {
|
|
13
26
|
const expandKeyDepCols = (colDeps) => {
|
|
14
27
|
const tableColumns = colDeps.map(({ table_column }) => table_column);
|
|
@@ -19,14 +32,8 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
|
|
|
19
32
|
viewColumns,
|
|
20
33
|
}));
|
|
21
34
|
};
|
|
22
|
-
const viewToTableKeyDeps =
|
|
23
|
-
|
|
24
|
-
vkd.constraint_name === r.foreign_key_name &&
|
|
25
|
-
vkd.constraint_type === "f");
|
|
26
|
-
const tableToViewKeyDeps = viewsKeyDependencies.filter((vkd) => vkd.table_schema === r.referenced_schema &&
|
|
27
|
-
vkd.table_name === r.referenced_relation &&
|
|
28
|
-
vkd.constraint_name === r.foreign_key_name &&
|
|
29
|
-
vkd.constraint_type === "f_ref");
|
|
35
|
+
const viewToTableKeyDeps = keyDepsByConstraint.get(keyDependencyKey(r.schema, r.relation, r.foreign_key_name, "f")) ?? [];
|
|
36
|
+
const tableToViewKeyDeps = keyDepsByConstraint.get(keyDependencyKey(r.referenced_schema, r.referenced_relation, r.foreign_key_name, "f_ref")) ?? [];
|
|
30
37
|
const viewToTableRelationships = viewToTableKeyDeps.flatMap((vtkd) => expandKeyDepCols(vtkd.column_dependencies).map(({ viewColumns }) => ({
|
|
31
38
|
foreign_key_name: r.foreign_key_name,
|
|
32
39
|
schema: vtkd.view_schema,
|
|
@@ -37,7 +44,7 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
|
|
|
37
44
|
referenced_relation: r.referenced_relation,
|
|
38
45
|
referenced_columns: r.referenced_columns,
|
|
39
46
|
})));
|
|
40
|
-
const tableToViewRelationships = tableToViewKeyDeps.flatMap((tvkd) => expandKeyDepCols(tvkd.column_dependencies).map(({ viewColumns }) => ({
|
|
47
|
+
const tableToViewRelationships = (isTableEmitted(r) ? tableToViewKeyDeps : []).flatMap((tvkd) => expandKeyDepCols(tvkd.column_dependencies).map(({ viewColumns }) => ({
|
|
41
48
|
foreign_key_name: r.foreign_key_name,
|
|
42
49
|
schema: r.schema,
|
|
43
50
|
relation: r.relation,
|
|
@@ -67,12 +74,27 @@ export function expandViewRelationships(allTableM2oAndO2oRelationships, viewsKey
|
|
|
67
74
|
/**
|
|
68
75
|
* List all relationships (table↔table plus the view-derived ones) for the
|
|
69
76
|
* schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
|
|
70
|
-
* `introspect()` builds with the system schemas excluded.
|
|
71
|
-
*
|
|
77
|
+
* `introspect()` builds with the system schemas excluded. Like PostgREST, view
|
|
78
|
+
* expansion sees the foreign keys of every non-system schema, so a view over a
|
|
79
|
+
* table in a schema that is not selected keeps the table's foreign keys; only
|
|
80
|
+
* the views and tables in the selected schemas get relationships. Views in the
|
|
81
|
+
* `extraSearchPath` schemas are followed to their base tables but get no
|
|
82
|
+
* relationships of their own. Errors from the injected `Queryable` propagate
|
|
83
|
+
* by throwing — callers adapt.
|
|
72
84
|
*/
|
|
73
|
-
export async function listRelationships(db, schemaFilter) {
|
|
74
|
-
const
|
|
75
|
-
const { rows: viewsKeyDependencies } = await
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
export async function listRelationships(db, schemaFilter, extraSearchPath = []) {
|
|
86
|
+
const tableRelationshipsQuery = db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter }));
|
|
87
|
+
const [{ rows: tableRelationships }, { rows: allTableRelationships }, { rows: viewsKeyDependencies },] = await Promise.all([
|
|
88
|
+
tableRelationshipsQuery,
|
|
89
|
+
schemaFilter === NON_SYSTEM_SCHEMAS_FILTER
|
|
90
|
+
? tableRelationshipsQuery
|
|
91
|
+
: db.query(TABLE_RELATIONSHIPS_SQL({ schemaFilter: NON_SYSTEM_SCHEMAS_FILTER })),
|
|
92
|
+
db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter, extraSearchPath })),
|
|
93
|
+
]);
|
|
94
|
+
const emittedKeys = new Set(tableRelationships.map(relationshipKey));
|
|
95
|
+
const allViewM2oAndO2oRelationships = expandViewRelationships(allTableRelationships, viewsKeyDependencies, (r) => emittedKeys.has(relationshipKey(r)));
|
|
96
|
+
return tableRelationships.concat(allViewM2oAndO2oRelationships);
|
|
78
97
|
}
|
|
98
|
+
const NON_SYSTEM_SCHEMAS_FILTER = filterByList(undefined, undefined, DEFAULT_SYSTEM_SCHEMAS);
|
|
99
|
+
const relationshipKey = (r) => JSON.stringify([r.schema, r.relation, r.foreign_key_name]);
|
|
100
|
+
const keyDependencyKey = (tableSchema, tableName, constraintName, constraintType) => JSON.stringify([tableSchema, tableName, constraintName, constraintType]);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { filterByList } from "./helpers.js";
|
|
1
2
|
import { literal } from "./pg-format.js";
|
|
2
3
|
export const VIEWS_KEY_DEPENDENCIES_SQL = (props) => /* SQL */ `
|
|
3
4
|
-- Adapted from
|
|
@@ -27,7 +28,6 @@ pks_fks as (
|
|
|
27
28
|
from pg_constraint
|
|
28
29
|
left join lateral unnest(confkey) with ordinality as _(col, ord) on true
|
|
29
30
|
where contype='f'
|
|
30
|
-
${props.schemaFilter ? `and connamespace::regnamespace::text ${props.schemaFilter}` : ""}
|
|
31
31
|
),
|
|
32
32
|
views as (
|
|
33
33
|
select
|
|
@@ -39,7 +39,7 @@ views as (
|
|
|
39
39
|
join pg_namespace n on n.oid = c.relnamespace
|
|
40
40
|
join pg_rewrite r on r.ev_class = c.oid
|
|
41
41
|
where c.relkind in ('v', 'm')
|
|
42
|
-
${props
|
|
42
|
+
${viewSchemaFilter(props)}
|
|
43
43
|
),
|
|
44
44
|
transform_json as (
|
|
45
45
|
select
|
|
@@ -120,6 +120,18 @@ group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conn
|
|
|
120
120
|
-- make sure we only return key for which all columns are referenced in the view - no partial PKs or FKs
|
|
121
121
|
having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
|
|
122
122
|
`;
|
|
123
|
+
/**
|
|
124
|
+
* Like PostgREST, the views followed to their base tables also include the
|
|
125
|
+
* `db-extra-search-path` schemas; the recursion still starts from the selected
|
|
126
|
+
* schemas only.
|
|
127
|
+
*/
|
|
128
|
+
function viewSchemaFilter({ schemaFilter, extraSearchPath = [], }) {
|
|
129
|
+
if (!schemaFilter)
|
|
130
|
+
return "";
|
|
131
|
+
if (extraSearchPath.length === 0)
|
|
132
|
+
return `and n.nspname ${schemaFilter}`;
|
|
133
|
+
return `and (n.nspname ${schemaFilter} or n.nspname ${filterByList(extraSearchPath)})`;
|
|
134
|
+
}
|
|
123
135
|
/**
|
|
124
136
|
* The rewrites, innermost first, that turn the `pg_node_tree` text of a view
|
|
125
137
|
* definition into JSON exposing only `targetList`, `resno`, `resorigtbl` and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/postgrest-typegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Type generation for PostgREST from PostgreSQL schemas",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codegen",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"test": "bun scripts/run-tests.ts test/"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"arktype": "2.2.
|
|
66
|
+
"arktype": "2.2.9"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@testcontainers/postgresql": "^12.1.0",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"@types/pg": "^8.23.1",
|
|
76
76
|
"knip": "^6.32.3",
|
|
77
77
|
"oxfmt": "0.66.0",
|
|
78
|
-
"oxlint": "1.
|
|
79
|
-
"oxlint-tsgolint": "7.0.
|
|
78
|
+
"oxlint": "1.90.0",
|
|
79
|
+
"oxlint-tsgolint": "7.0.2005",
|
|
80
80
|
"pg": "^8.23.0",
|
|
81
81
|
"testcontainers": "^12.1.0",
|
|
82
82
|
"typescript": "7.0.2"
|
|
@@ -31,6 +31,12 @@ export interface Queryable {
|
|
|
31
31
|
export interface IntrospectOptions {
|
|
32
32
|
includedSchemas?: string[];
|
|
33
33
|
excludedSchemas?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* PostgREST's `db-extra-search-path` schemas. A view in a generated schema
|
|
36
|
+
* that selects from a view in one of them keeps the relationships of the
|
|
37
|
+
* underlying tables, as in PostgREST. Nothing in these schemas is generated.
|
|
38
|
+
*/
|
|
39
|
+
extraSearchPath?: string[];
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -83,7 +89,7 @@ export async function introspect(
|
|
|
83
89
|
queryRows(MATERIALIZED_VIEWS_SQL({ schemaFilter: plainFilter })),
|
|
84
90
|
queryRows(COLUMNS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
85
91
|
queryRows(PRIMARY_KEYS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
86
|
-
listRelationships(db, systemExcludingFilter),
|
|
92
|
+
listRelationships(db, systemExcludingFilter, opts.extraSearchPath),
|
|
87
93
|
queryRows(FUNCTIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
88
94
|
queryRows(TYPES_SQL),
|
|
89
95
|
queryRows(VIEW_DEFINITIONS_SQL({ schemaFilter: systemExcludingFilter })),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PostgresRelationship } from "../types.ts";
|
|
2
2
|
import type { Queryable } from "./index.ts";
|
|
3
|
+
import { DEFAULT_SYSTEM_SCHEMAS, filterByList } from "./sql/helpers.ts";
|
|
3
4
|
import { TABLE_RELATIONSHIPS_SQL } from "./sql/table_relationships.sql.ts";
|
|
4
5
|
import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.ts";
|
|
5
6
|
|
|
@@ -13,7 +14,8 @@ import { VIEWS_KEY_DEPENDENCIES_SQL } from "./sql/views_key_dependencies.sql.ts"
|
|
|
13
14
|
* by mapping a base table's foreign key columns onto the view columns that
|
|
14
15
|
* select them (`VIEWS_KEY_DEPENDENCIES_SQL` + the cartesian-product
|
|
15
16
|
* expansion below). This second part is pure TypeScript, not SQL — ported
|
|
16
|
-
*
|
|
17
|
+
* from postgres-meta, except that the expansion sees every non-system
|
|
18
|
+
* schema's foreign keys, as PostgREST does (see `listRelationships`).
|
|
17
19
|
*
|
|
18
20
|
* Adapted from:
|
|
19
21
|
* https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L392
|
|
@@ -39,13 +41,32 @@ export type ViewKeyDependency = {
|
|
|
39
41
|
* view↔view relationships implied by the view key dependencies. Pure function
|
|
40
42
|
* (no database) so the cartesian-product expansion can be unit-tested directly.
|
|
41
43
|
*
|
|
42
|
-
* Ported
|
|
43
|
-
* `allViewM2oAndO2oRelationships` block).
|
|
44
|
+
* Ported from `PostgresMetaRelationships.list()` (the body of the
|
|
45
|
+
* `allViewM2oAndO2oRelationships` block). Every table relationship feeds the
|
|
46
|
+
* expansion, but a table→view relationship is emitted on the table itself, so
|
|
47
|
+
* `isTableEmitted` keeps it to the tables the caller returns relationships for.
|
|
44
48
|
*/
|
|
45
49
|
export function expandViewRelationships(
|
|
46
50
|
allTableM2oAndO2oRelationships: PostgresRelationship[],
|
|
47
51
|
viewsKeyDependencies: ViewKeyDependency[],
|
|
52
|
+
isTableEmitted: (relationship: PostgresRelationship) => boolean,
|
|
48
53
|
): PostgresRelationship[] {
|
|
54
|
+
const keyDepsByConstraint = new Map<string, ViewKeyDependency[]>();
|
|
55
|
+
for (const vkd of viewsKeyDependencies) {
|
|
56
|
+
const key = keyDependencyKey(
|
|
57
|
+
vkd.table_schema,
|
|
58
|
+
vkd.table_name,
|
|
59
|
+
vkd.constraint_name,
|
|
60
|
+
vkd.constraint_type,
|
|
61
|
+
);
|
|
62
|
+
let bucket = keyDepsByConstraint.get(key);
|
|
63
|
+
if (!bucket) {
|
|
64
|
+
bucket = [];
|
|
65
|
+
keyDepsByConstraint.set(key, bucket);
|
|
66
|
+
}
|
|
67
|
+
bucket.push(vkd);
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
return allTableM2oAndO2oRelationships.flatMap((r) => {
|
|
50
71
|
const expandKeyDepCols = (
|
|
51
72
|
colDeps: ColDep[],
|
|
@@ -68,20 +89,19 @@ export function expandViewRelationships(
|
|
|
68
89
|
}));
|
|
69
90
|
};
|
|
70
91
|
|
|
71
|
-
const viewToTableKeyDeps =
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
);
|
|
92
|
+
const viewToTableKeyDeps =
|
|
93
|
+
keyDepsByConstraint.get(
|
|
94
|
+
keyDependencyKey(r.schema, r.relation, r.foreign_key_name, "f"),
|
|
95
|
+
) ?? [];
|
|
96
|
+
const tableToViewKeyDeps =
|
|
97
|
+
keyDepsByConstraint.get(
|
|
98
|
+
keyDependencyKey(
|
|
99
|
+
r.referenced_schema,
|
|
100
|
+
r.referenced_relation,
|
|
101
|
+
r.foreign_key_name,
|
|
102
|
+
"f_ref",
|
|
103
|
+
),
|
|
104
|
+
) ?? [];
|
|
85
105
|
|
|
86
106
|
const viewToTableRelationships = viewToTableKeyDeps.flatMap((vtkd) =>
|
|
87
107
|
expandKeyDepCols(vtkd.column_dependencies).map(({ viewColumns }) => ({
|
|
@@ -96,7 +116,9 @@ export function expandViewRelationships(
|
|
|
96
116
|
})),
|
|
97
117
|
);
|
|
98
118
|
|
|
99
|
-
const tableToViewRelationships =
|
|
119
|
+
const tableToViewRelationships = (
|
|
120
|
+
isTableEmitted(r) ? tableToViewKeyDeps : []
|
|
121
|
+
).flatMap((tvkd) =>
|
|
100
122
|
expandKeyDepCols(tvkd.column_dependencies).map(({ viewColumns }) => ({
|
|
101
123
|
foreign_key_name: r.foreign_key_name,
|
|
102
124
|
schema: r.schema,
|
|
@@ -139,26 +161,62 @@ export function expandViewRelationships(
|
|
|
139
161
|
/**
|
|
140
162
|
* List all relationships (table↔table plus the view-derived ones) for the
|
|
141
163
|
* schemas selected by `schemaFilter`, the `IN (…)`/`NOT IN (…)` fragment
|
|
142
|
-
* `introspect()` builds with the system schemas excluded.
|
|
143
|
-
*
|
|
164
|
+
* `introspect()` builds with the system schemas excluded. Like PostgREST, view
|
|
165
|
+
* expansion sees the foreign keys of every non-system schema, so a view over a
|
|
166
|
+
* table in a schema that is not selected keeps the table's foreign keys; only
|
|
167
|
+
* the views and tables in the selected schemas get relationships. Views in the
|
|
168
|
+
* `extraSearchPath` schemas are followed to their base tables but get no
|
|
169
|
+
* relationships of their own. Errors from the injected `Queryable` propagate
|
|
170
|
+
* by throwing — callers adapt.
|
|
144
171
|
*/
|
|
145
172
|
export async function listRelationships(
|
|
146
173
|
db: Queryable,
|
|
147
174
|
schemaFilter: string,
|
|
175
|
+
extraSearchPath: string[] = [],
|
|
148
176
|
): Promise<PostgresRelationship[]> {
|
|
149
|
-
const
|
|
177
|
+
const tableRelationshipsQuery = db.query(
|
|
150
178
|
TABLE_RELATIONSHIPS_SQL({ schemaFilter }),
|
|
151
179
|
);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
const [
|
|
181
|
+
{ rows: tableRelationships },
|
|
182
|
+
{ rows: allTableRelationships },
|
|
183
|
+
{ rows: viewsKeyDependencies },
|
|
184
|
+
] = await Promise.all([
|
|
185
|
+
tableRelationshipsQuery,
|
|
186
|
+
schemaFilter === NON_SYSTEM_SCHEMAS_FILTER
|
|
187
|
+
? tableRelationshipsQuery
|
|
188
|
+
: db.query(
|
|
189
|
+
TABLE_RELATIONSHIPS_SQL({ schemaFilter: NON_SYSTEM_SCHEMAS_FILTER }),
|
|
190
|
+
),
|
|
191
|
+
db.query(VIEWS_KEY_DEPENDENCIES_SQL({ schemaFilter, extraSearchPath })),
|
|
192
|
+
]);
|
|
155
193
|
|
|
194
|
+
const emittedKeys = new Set(
|
|
195
|
+
(tableRelationships as PostgresRelationship[]).map(relationshipKey),
|
|
196
|
+
);
|
|
156
197
|
const allViewM2oAndO2oRelationships = expandViewRelationships(
|
|
157
|
-
|
|
198
|
+
allTableRelationships as PostgresRelationship[],
|
|
158
199
|
viewsKeyDependencies as ViewKeyDependency[],
|
|
200
|
+
(r) => emittedKeys.has(relationshipKey(r)),
|
|
159
201
|
);
|
|
160
202
|
|
|
161
|
-
return (
|
|
203
|
+
return (tableRelationships as PostgresRelationship[]).concat(
|
|
162
204
|
allViewM2oAndO2oRelationships,
|
|
163
205
|
);
|
|
164
206
|
}
|
|
207
|
+
|
|
208
|
+
const NON_SYSTEM_SCHEMAS_FILTER = filterByList(
|
|
209
|
+
undefined,
|
|
210
|
+
undefined,
|
|
211
|
+
DEFAULT_SYSTEM_SCHEMAS,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const relationshipKey = (r: PostgresRelationship) =>
|
|
215
|
+
JSON.stringify([r.schema, r.relation, r.foreign_key_name]);
|
|
216
|
+
|
|
217
|
+
const keyDependencyKey = (
|
|
218
|
+
tableSchema: string,
|
|
219
|
+
tableName: string,
|
|
220
|
+
constraintName: string,
|
|
221
|
+
constraintType: ViewKeyDependency["constraint_type"],
|
|
222
|
+
) => JSON.stringify([tableSchema, tableName, constraintName, constraintType]);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { SchemaFilterProps } from "./common.ts";
|
|
2
|
+
import { filterByList } from "./helpers.ts";
|
|
2
3
|
import { literal } from "./pg-format.ts";
|
|
3
4
|
|
|
4
5
|
export const VIEWS_KEY_DEPENDENCIES_SQL = (
|
|
5
|
-
props: SchemaFilterProps,
|
|
6
|
+
props: SchemaFilterProps & { extraSearchPath?: string[] },
|
|
6
7
|
) => /* SQL */ `
|
|
7
8
|
-- Adapted from
|
|
8
9
|
-- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L820
|
|
@@ -31,7 +32,6 @@ pks_fks as (
|
|
|
31
32
|
from pg_constraint
|
|
32
33
|
left join lateral unnest(confkey) with ordinality as _(col, ord) on true
|
|
33
34
|
where contype='f'
|
|
34
|
-
${props.schemaFilter ? `and connamespace::regnamespace::text ${props.schemaFilter}` : ""}
|
|
35
35
|
),
|
|
36
36
|
views as (
|
|
37
37
|
select
|
|
@@ -43,7 +43,7 @@ views as (
|
|
|
43
43
|
join pg_namespace n on n.oid = c.relnamespace
|
|
44
44
|
join pg_rewrite r on r.ev_class = c.oid
|
|
45
45
|
where c.relkind in ('v', 'm')
|
|
46
|
-
${props
|
|
46
|
+
${viewSchemaFilter(props)}
|
|
47
47
|
),
|
|
48
48
|
transform_json as (
|
|
49
49
|
select
|
|
@@ -125,6 +125,20 @@ group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conn
|
|
|
125
125
|
having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
|
|
126
126
|
`;
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Like PostgREST, the views followed to their base tables also include the
|
|
130
|
+
* `db-extra-search-path` schemas; the recursion still starts from the selected
|
|
131
|
+
* schemas only.
|
|
132
|
+
*/
|
|
133
|
+
function viewSchemaFilter({
|
|
134
|
+
schemaFilter,
|
|
135
|
+
extraSearchPath = [],
|
|
136
|
+
}: SchemaFilterProps & { extraSearchPath?: string[] }): string {
|
|
137
|
+
if (!schemaFilter) return "";
|
|
138
|
+
if (extraSearchPath.length === 0) return `and n.nspname ${schemaFilter}`;
|
|
139
|
+
return `and (n.nspname ${schemaFilter} or n.nspname ${filterByList(extraSearchPath)})`;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
/**
|
|
129
143
|
* One text rewrite of the `pg_node_tree` to JSON conversion: a `replace`, or
|
|
130
144
|
* a global `regexp_replace` when `isRegex` is set.
|