@pgpmjs/export 1.16.10 → 1.16.12
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/catalog-projection.d.ts +13 -0
- package/catalog-projection.js +51 -0
- package/esm/catalog-projection.js +47 -0
- package/esm/export-graphql-meta.js +31 -3
- package/esm/export-meta.js +25 -2
- package/esm/export-utils.js +6 -0
- package/esm/graphql-client.js +2 -0
- package/esm/meta-export-tables.json +16 -5
- package/export-graphql-meta.js +31 -3
- package/export-meta.js +25 -2
- package/export-utils.d.ts +7 -0
- package/export-utils.js +6 -0
- package/graphql-client.js +2 -0
- package/meta-export-tables.json +16 -5
- package/package.json +9 -9
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
3
|
+
* routing_public.apis by catalog_private.tg_apis_catalog_sync(). During
|
|
4
|
+
* migration replay the sync trigger is skipped (session_replication_role),
|
|
5
|
+
* and the catalog tables cannot be queried through the meta API (bare-name
|
|
6
|
+
* collisions with routing_public) — so the projection is materialized at
|
|
7
|
+
* export time, mirroring the trigger mapping 1:1. resolve_route() needs
|
|
8
|
+
* these rows to build resolved_config for api targets.
|
|
9
|
+
*
|
|
10
|
+
* Both the SQL and GraphQL export flows run this projection so their output
|
|
11
|
+
* stays byte-identical (cross-flow parity).
|
|
12
|
+
*/
|
|
13
|
+
export declare const projectCatalogApis: (apisRows: Record<string, unknown>[]) => Promise<string | undefined>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectCatalogApis = void 0;
|
|
4
|
+
const csv_to_pg_1 = require("csv-to-pg");
|
|
5
|
+
/**
|
|
6
|
+
* Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
7
|
+
* routing_public.apis by catalog_private.tg_apis_catalog_sync(). During
|
|
8
|
+
* migration replay the sync trigger is skipped (session_replication_role),
|
|
9
|
+
* and the catalog tables cannot be queried through the meta API (bare-name
|
|
10
|
+
* collisions with routing_public) — so the projection is materialized at
|
|
11
|
+
* export time, mirroring the trigger mapping 1:1. resolve_route() needs
|
|
12
|
+
* these rows to build resolved_config for api targets.
|
|
13
|
+
*
|
|
14
|
+
* Both the SQL and GraphQL export flows run this projection so their output
|
|
15
|
+
* stays byte-identical (cross-flow parity).
|
|
16
|
+
*/
|
|
17
|
+
const projectCatalogApis = async (apisRows) => {
|
|
18
|
+
if (!apisRows.length)
|
|
19
|
+
return undefined;
|
|
20
|
+
const projected = apisRows.map((r) => ({
|
|
21
|
+
id: r.id,
|
|
22
|
+
owner_scope: 'database',
|
|
23
|
+
owner_key: r.database_id,
|
|
24
|
+
is_visible: r.is_published ?? false,
|
|
25
|
+
database_id: r.database_id,
|
|
26
|
+
name: r.name,
|
|
27
|
+
dbname: r.dbname,
|
|
28
|
+
role_name: r.role_name,
|
|
29
|
+
anon_role: r.anon_role,
|
|
30
|
+
config: r.config ?? null
|
|
31
|
+
}));
|
|
32
|
+
const parser = new csv_to_pg_1.Parser({
|
|
33
|
+
schema: 'catalog_private',
|
|
34
|
+
table: 'apis',
|
|
35
|
+
fields: {
|
|
36
|
+
id: 'uuid',
|
|
37
|
+
owner_scope: 'text',
|
|
38
|
+
owner_key: 'uuid',
|
|
39
|
+
is_visible: 'boolean',
|
|
40
|
+
database_id: 'uuid',
|
|
41
|
+
name: 'text',
|
|
42
|
+
dbname: 'text',
|
|
43
|
+
role_name: 'text',
|
|
44
|
+
anon_role: 'text',
|
|
45
|
+
config: 'jsonb'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const parsed = await parser.parse(projected);
|
|
49
|
+
return parsed || undefined;
|
|
50
|
+
};
|
|
51
|
+
exports.projectCatalogApis = projectCatalogApis;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Parser } from 'csv-to-pg';
|
|
2
|
+
/**
|
|
3
|
+
* Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
4
|
+
* routing_public.apis by catalog_private.tg_apis_catalog_sync(). During
|
|
5
|
+
* migration replay the sync trigger is skipped (session_replication_role),
|
|
6
|
+
* and the catalog tables cannot be queried through the meta API (bare-name
|
|
7
|
+
* collisions with routing_public) — so the projection is materialized at
|
|
8
|
+
* export time, mirroring the trigger mapping 1:1. resolve_route() needs
|
|
9
|
+
* these rows to build resolved_config for api targets.
|
|
10
|
+
*
|
|
11
|
+
* Both the SQL and GraphQL export flows run this projection so their output
|
|
12
|
+
* stays byte-identical (cross-flow parity).
|
|
13
|
+
*/
|
|
14
|
+
export const projectCatalogApis = async (apisRows) => {
|
|
15
|
+
if (!apisRows.length)
|
|
16
|
+
return undefined;
|
|
17
|
+
const projected = apisRows.map((r) => ({
|
|
18
|
+
id: r.id,
|
|
19
|
+
owner_scope: 'database',
|
|
20
|
+
owner_key: r.database_id,
|
|
21
|
+
is_visible: r.is_published ?? false,
|
|
22
|
+
database_id: r.database_id,
|
|
23
|
+
name: r.name,
|
|
24
|
+
dbname: r.dbname,
|
|
25
|
+
role_name: r.role_name,
|
|
26
|
+
anon_role: r.anon_role,
|
|
27
|
+
config: r.config ?? null
|
|
28
|
+
}));
|
|
29
|
+
const parser = new Parser({
|
|
30
|
+
schema: 'catalog_private',
|
|
31
|
+
table: 'apis',
|
|
32
|
+
fields: {
|
|
33
|
+
id: 'uuid',
|
|
34
|
+
owner_scope: 'text',
|
|
35
|
+
owner_key: 'uuid',
|
|
36
|
+
is_visible: 'boolean',
|
|
37
|
+
database_id: 'uuid',
|
|
38
|
+
name: 'text',
|
|
39
|
+
dbname: 'text',
|
|
40
|
+
role_name: 'text',
|
|
41
|
+
anon_role: 'text',
|
|
42
|
+
config: 'jsonb'
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const parsed = await parser.parse(projected);
|
|
46
|
+
return parsed || undefined;
|
|
47
|
+
};
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Parser } from 'csv-to-pg';
|
|
10
10
|
import { toSnakeCase } from 'inflekt';
|
|
11
|
+
import { projectCatalogApis } from './catalog-projection';
|
|
11
12
|
import { getTimestampDefaultColumnsForTable, META_TABLE_CONFIG, META_TABLE_ORDER } from './export-utils';
|
|
12
13
|
import { buildFieldsFragment, getGraphQLQueryName, getGraphQLTypeName, graphqlRowToPostgresRow, intervalToPostgres, mapGraphQLTypeToFieldType } from './graphql-naming';
|
|
13
14
|
import { lookupByGqlType } from './type-map';
|
|
@@ -100,6 +101,18 @@ const buildDynamicFieldsFromGraphQL = async (client, tableConfig) => {
|
|
|
100
101
|
*/
|
|
101
102
|
export const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
102
103
|
const sql = {};
|
|
104
|
+
// Raw rows per key (post GraphQL→Postgres conversion), kept for derived
|
|
105
|
+
// projections (see catalog plane derivation below).
|
|
106
|
+
const rawRows = {};
|
|
107
|
+
// Binding tables (hostname_bindings, route_bindings) carry no database_id;
|
|
108
|
+
// tenant ownership flows through domain_id → routing_public.domains. Fetch
|
|
109
|
+
// the tenant's domain ids once so those keys can be filtered by
|
|
110
|
+
// domainId IN (...).
|
|
111
|
+
let domainIds = [];
|
|
112
|
+
if (META_TABLE_ORDER.some((k) => META_TABLE_CONFIG[k]?.filterViaDomainIds)) {
|
|
113
|
+
const domainRows = await client.fetchAllNodes(getGraphQLQueryName('domains'), 'id', { databaseId: database_id });
|
|
114
|
+
domainIds = domainRows.map((r) => r.id);
|
|
115
|
+
}
|
|
103
116
|
const queryAndParse = async (key) => {
|
|
104
117
|
const tableConfig = META_TABLE_CONFIG[key];
|
|
105
118
|
if (!tableConfig)
|
|
@@ -107,8 +120,7 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
107
120
|
// Schema-qualified manifest keys (e.g. catalog_private.apis)
|
|
108
121
|
// mark tables whose name collides with a table in another plane. GraphQL
|
|
109
122
|
// type/query names are derived from the bare table name, so these cannot
|
|
110
|
-
// be addressed unambiguously through the meta API
|
|
111
|
-
// exports them.
|
|
123
|
+
// be addressed unambiguously through the meta API in a mixed build.
|
|
112
124
|
if (key.includes('.'))
|
|
113
125
|
return;
|
|
114
126
|
// Build fields dynamically: either from hardcoded config or via introspection
|
|
@@ -121,7 +133,9 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
121
133
|
// The 'database' table is fetched by id, not by database_id
|
|
122
134
|
const condition = key === 'database'
|
|
123
135
|
? { id: database_id }
|
|
124
|
-
:
|
|
136
|
+
: tableConfig.filterViaDomainIds
|
|
137
|
+
? { domainId: domainIds }
|
|
138
|
+
: { databaseId: database_id };
|
|
125
139
|
try {
|
|
126
140
|
const rows = await client.fetchAllNodes(graphqlQueryName, graphqlFieldsFragment, condition);
|
|
127
141
|
if (rows.length > 0) {
|
|
@@ -170,6 +184,7 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
170
184
|
}
|
|
171
185
|
if (Object.keys(dynamicFields).length === 0)
|
|
172
186
|
return;
|
|
187
|
+
rawRows[key] = pgRows;
|
|
173
188
|
// Omit columnDefaults columns from row data so the Parser never sees them.
|
|
174
189
|
// configFields already excludes them (via buildDynamicFieldsFromGraphQL),
|
|
175
190
|
// so dynamicFields won't contain them either — but the pgRow data still does.
|
|
@@ -220,5 +235,18 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
220
235
|
for (const keys of keysBySchema.values()) {
|
|
221
236
|
await Promise.all(keys.map(key => queryAndParse(key)));
|
|
222
237
|
}
|
|
238
|
+
// Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
239
|
+
// routing_public.apis (catalog_private.tg_apis_catalog_sync). The sync
|
|
240
|
+
// trigger is skipped during migration replay (session_replication_role),
|
|
241
|
+
// and the catalog tables can't be queried through the meta API (bare-name
|
|
242
|
+
// collisions with routing_public) — so the projection is materialized here.
|
|
243
|
+
// Shared with the SQL flow (see catalog-projection.ts) for cross-flow parity.
|
|
244
|
+
const apisRows = rawRows['apis'];
|
|
245
|
+
if (sql['apis'] && apisRows?.length) {
|
|
246
|
+
const parsed = await projectCatalogApis(apisRows);
|
|
247
|
+
if (parsed) {
|
|
248
|
+
sql['catalog_private.apis'] = parsed;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
223
251
|
return sql;
|
|
224
252
|
};
|
package/esm/export-meta.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Parser } from 'csv-to-pg';
|
|
2
2
|
import { getPgPool } from 'pg-cache';
|
|
3
|
+
import { projectCatalogApis } from './catalog-projection';
|
|
3
4
|
import { getTableColumnsWithDefaults, isTimestampDefaultColumn, mapPgTypeToFieldType, META_TABLE_CONFIG, META_TABLE_ORDER } from './export-utils';
|
|
4
5
|
/**
|
|
5
6
|
* Build dynamic fields config from the database via information_schema.
|
|
@@ -60,6 +61,9 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
|
60
61
|
database: dbname
|
|
61
62
|
});
|
|
62
63
|
const sql = {};
|
|
64
|
+
// Raw rows per key (post column-default stripping), kept for derived
|
|
65
|
+
// projections (see catalog plane projection below).
|
|
66
|
+
const rawRows = {};
|
|
63
67
|
// Cache for dynamically built parsers and their field configs
|
|
64
68
|
const parsers = {};
|
|
65
69
|
const parserFields = {};
|
|
@@ -124,6 +128,7 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
}
|
|
131
|
+
rawRows[key] = result.rows;
|
|
127
132
|
const parsed = await parser.parse(result.rows);
|
|
128
133
|
if (parsed) {
|
|
129
134
|
sql[key] = parsed;
|
|
@@ -143,8 +148,26 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
|
143
148
|
// itself, which is keyed by id.
|
|
144
149
|
for (const key of META_TABLE_ORDER) {
|
|
145
150
|
const tableConfig = META_TABLE_CONFIG[key];
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
// Binding tables (hostname_bindings, route_bindings) carry no database_id;
|
|
152
|
+
// tenant ownership flows through domain_id → routing_public.domains.
|
|
153
|
+
const filterSql = key === 'database'
|
|
154
|
+
? 'id = $1'
|
|
155
|
+
: tableConfig.filterViaDomainIds
|
|
156
|
+
? 'domain_id IN (SELECT id FROM routing_public.domains WHERE database_id = $1)'
|
|
157
|
+
: 'database_id = $1';
|
|
158
|
+
await queryAndParse(key, `SELECT * FROM ${tableConfig.schema}.${tableConfig.table} WHERE ${filterSql} ORDER BY id`);
|
|
159
|
+
}
|
|
160
|
+
// Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
161
|
+
// routing_public.apis (catalog_private.tg_apis_catalog_sync). The sync
|
|
162
|
+
// trigger is skipped during migration replay (session_replication_role),
|
|
163
|
+
// so the projection is materialized here. Shared with the GraphQL flow
|
|
164
|
+
// (see catalog-projection.ts) for cross-flow parity.
|
|
165
|
+
const apisRows = rawRows['apis'];
|
|
166
|
+
if (sql['apis'] && apisRows?.length) {
|
|
167
|
+
const parsed = await projectCatalogApis(apisRows);
|
|
168
|
+
if (parsed) {
|
|
169
|
+
sql['catalog_private.apis'] = parsed;
|
|
170
|
+
}
|
|
148
171
|
}
|
|
149
172
|
return sql;
|
|
150
173
|
};
|
package/esm/export-utils.js
CHANGED
|
@@ -143,6 +143,12 @@ SET session_replication_role TO DEFAULT;
|
|
|
143
143
|
* values must come from DDL defaults at deploy time.
|
|
144
144
|
*/
|
|
145
145
|
export const META_TABLE_OVERRIDES = {
|
|
146
|
+
hostname_bindings: {
|
|
147
|
+
filterViaDomainIds: true
|
|
148
|
+
},
|
|
149
|
+
route_bindings: {
|
|
150
|
+
filterViaDomainIds: true
|
|
151
|
+
},
|
|
146
152
|
sites: {
|
|
147
153
|
typeOverrides: {
|
|
148
154
|
og_image: 'image',
|
package/esm/graphql-client.js
CHANGED
|
@@ -121,6 +121,8 @@ export class GraphQLClient {
|
|
|
121
121
|
if (condition && Object.keys(condition).length > 0) {
|
|
122
122
|
const filterParts = Object.entries(condition)
|
|
123
123
|
.map(([k, v]) => {
|
|
124
|
+
if (Array.isArray(v))
|
|
125
|
+
return `${k}: { in: [${v.map((item) => `"${item}"`).join(', ')}] }`;
|
|
124
126
|
if (typeof v === 'string')
|
|
125
127
|
return `${k}: { equalTo: "${v}" }`;
|
|
126
128
|
if (typeof v === 'boolean')
|
|
@@ -591,6 +591,16 @@
|
|
|
591
591
|
"schema": "routing_public",
|
|
592
592
|
"table": "routes"
|
|
593
593
|
},
|
|
594
|
+
{
|
|
595
|
+
"key": "hostname_bindings",
|
|
596
|
+
"schema": "routing_public",
|
|
597
|
+
"table": "hostname_bindings"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"key": "route_bindings",
|
|
601
|
+
"schema": "routing_public",
|
|
602
|
+
"table": "route_bindings"
|
|
603
|
+
},
|
|
594
604
|
{
|
|
595
605
|
"key": "site_app_links",
|
|
596
606
|
"schema": "routing_public",
|
|
@@ -671,11 +681,6 @@
|
|
|
671
681
|
"schema": "catalog_private",
|
|
672
682
|
"table": "domains"
|
|
673
683
|
},
|
|
674
|
-
{
|
|
675
|
-
"key": "functions",
|
|
676
|
-
"schema": "catalog_private",
|
|
677
|
-
"table": "functions"
|
|
678
|
-
},
|
|
679
684
|
{
|
|
680
685
|
"key": "namespaces",
|
|
681
686
|
"schema": "catalog_private",
|
|
@@ -957,6 +962,12 @@
|
|
|
957
962
|
"created_at",
|
|
958
963
|
"updated_at"
|
|
959
964
|
],
|
|
965
|
+
"routing_public.hostname_bindings": [
|
|
966
|
+
"updated_at"
|
|
967
|
+
],
|
|
968
|
+
"routing_public.route_bindings": [
|
|
969
|
+
"updated_at"
|
|
970
|
+
],
|
|
960
971
|
"routing_public.site_app_links": [
|
|
961
972
|
"created_at",
|
|
962
973
|
"updated_at"
|
package/export-graphql-meta.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.exportGraphQLMeta = void 0;
|
|
|
11
11
|
*/
|
|
12
12
|
const csv_to_pg_1 = require("csv-to-pg");
|
|
13
13
|
const inflekt_1 = require("inflekt");
|
|
14
|
+
const catalog_projection_1 = require("./catalog-projection");
|
|
14
15
|
const export_utils_1 = require("./export-utils");
|
|
15
16
|
const graphql_naming_1 = require("./graphql-naming");
|
|
16
17
|
const type_map_1 = require("./type-map");
|
|
@@ -103,6 +104,18 @@ const buildDynamicFieldsFromGraphQL = async (client, tableConfig) => {
|
|
|
103
104
|
*/
|
|
104
105
|
const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
105
106
|
const sql = {};
|
|
107
|
+
// Raw rows per key (post GraphQL→Postgres conversion), kept for derived
|
|
108
|
+
// projections (see catalog plane derivation below).
|
|
109
|
+
const rawRows = {};
|
|
110
|
+
// Binding tables (hostname_bindings, route_bindings) carry no database_id;
|
|
111
|
+
// tenant ownership flows through domain_id → routing_public.domains. Fetch
|
|
112
|
+
// the tenant's domain ids once so those keys can be filtered by
|
|
113
|
+
// domainId IN (...).
|
|
114
|
+
let domainIds = [];
|
|
115
|
+
if (export_utils_1.META_TABLE_ORDER.some((k) => export_utils_1.META_TABLE_CONFIG[k]?.filterViaDomainIds)) {
|
|
116
|
+
const domainRows = await client.fetchAllNodes((0, graphql_naming_1.getGraphQLQueryName)('domains'), 'id', { databaseId: database_id });
|
|
117
|
+
domainIds = domainRows.map((r) => r.id);
|
|
118
|
+
}
|
|
106
119
|
const queryAndParse = async (key) => {
|
|
107
120
|
const tableConfig = export_utils_1.META_TABLE_CONFIG[key];
|
|
108
121
|
if (!tableConfig)
|
|
@@ -110,8 +123,7 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
110
123
|
// Schema-qualified manifest keys (e.g. catalog_private.apis)
|
|
111
124
|
// mark tables whose name collides with a table in another plane. GraphQL
|
|
112
125
|
// type/query names are derived from the bare table name, so these cannot
|
|
113
|
-
// be addressed unambiguously through the meta API
|
|
114
|
-
// exports them.
|
|
126
|
+
// be addressed unambiguously through the meta API in a mixed build.
|
|
115
127
|
if (key.includes('.'))
|
|
116
128
|
return;
|
|
117
129
|
// Build fields dynamically: either from hardcoded config or via introspection
|
|
@@ -124,7 +136,9 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
124
136
|
// The 'database' table is fetched by id, not by database_id
|
|
125
137
|
const condition = key === 'database'
|
|
126
138
|
? { id: database_id }
|
|
127
|
-
:
|
|
139
|
+
: tableConfig.filterViaDomainIds
|
|
140
|
+
? { domainId: domainIds }
|
|
141
|
+
: { databaseId: database_id };
|
|
128
142
|
try {
|
|
129
143
|
const rows = await client.fetchAllNodes(graphqlQueryName, graphqlFieldsFragment, condition);
|
|
130
144
|
if (rows.length > 0) {
|
|
@@ -173,6 +187,7 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
173
187
|
}
|
|
174
188
|
if (Object.keys(dynamicFields).length === 0)
|
|
175
189
|
return;
|
|
190
|
+
rawRows[key] = pgRows;
|
|
176
191
|
// Omit columnDefaults columns from row data so the Parser never sees them.
|
|
177
192
|
// configFields already excludes them (via buildDynamicFieldsFromGraphQL),
|
|
178
193
|
// so dynamicFields won't contain them either — but the pgRow data still does.
|
|
@@ -223,6 +238,19 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
|
223
238
|
for (const keys of keysBySchema.values()) {
|
|
224
239
|
await Promise.all(keys.map(key => queryAndParse(key)));
|
|
225
240
|
}
|
|
241
|
+
// Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
242
|
+
// routing_public.apis (catalog_private.tg_apis_catalog_sync). The sync
|
|
243
|
+
// trigger is skipped during migration replay (session_replication_role),
|
|
244
|
+
// and the catalog tables can't be queried through the meta API (bare-name
|
|
245
|
+
// collisions with routing_public) — so the projection is materialized here.
|
|
246
|
+
// Shared with the SQL flow (see catalog-projection.ts) for cross-flow parity.
|
|
247
|
+
const apisRows = rawRows['apis'];
|
|
248
|
+
if (sql['apis'] && apisRows?.length) {
|
|
249
|
+
const parsed = await (0, catalog_projection_1.projectCatalogApis)(apisRows);
|
|
250
|
+
if (parsed) {
|
|
251
|
+
sql['catalog_private.apis'] = parsed;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
226
254
|
return sql;
|
|
227
255
|
};
|
|
228
256
|
exports.exportGraphQLMeta = exportGraphQLMeta;
|
package/export-meta.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.exportMeta = void 0;
|
|
4
4
|
const csv_to_pg_1 = require("csv-to-pg");
|
|
5
5
|
const pg_cache_1 = require("pg-cache");
|
|
6
|
+
const catalog_projection_1 = require("./catalog-projection");
|
|
6
7
|
const export_utils_1 = require("./export-utils");
|
|
7
8
|
/**
|
|
8
9
|
* Build dynamic fields config from the database via information_schema.
|
|
@@ -63,6 +64,9 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
|
63
64
|
database: dbname
|
|
64
65
|
});
|
|
65
66
|
const sql = {};
|
|
67
|
+
// Raw rows per key (post column-default stripping), kept for derived
|
|
68
|
+
// projections (see catalog plane projection below).
|
|
69
|
+
const rawRows = {};
|
|
66
70
|
// Cache for dynamically built parsers and their field configs
|
|
67
71
|
const parsers = {};
|
|
68
72
|
const parserFields = {};
|
|
@@ -127,6 +131,7 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
}
|
|
134
|
+
rawRows[key] = result.rows;
|
|
130
135
|
const parsed = await parser.parse(result.rows);
|
|
131
136
|
if (parsed) {
|
|
132
137
|
sql[key] = parsed;
|
|
@@ -146,8 +151,26 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
|
|
|
146
151
|
// itself, which is keyed by id.
|
|
147
152
|
for (const key of export_utils_1.META_TABLE_ORDER) {
|
|
148
153
|
const tableConfig = export_utils_1.META_TABLE_CONFIG[key];
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
// Binding tables (hostname_bindings, route_bindings) carry no database_id;
|
|
155
|
+
// tenant ownership flows through domain_id → routing_public.domains.
|
|
156
|
+
const filterSql = key === 'database'
|
|
157
|
+
? 'id = $1'
|
|
158
|
+
: tableConfig.filterViaDomainIds
|
|
159
|
+
? 'domain_id IN (SELECT id FROM routing_public.domains WHERE database_id = $1)'
|
|
160
|
+
: 'database_id = $1';
|
|
161
|
+
await queryAndParse(key, `SELECT * FROM ${tableConfig.schema}.${tableConfig.table} WHERE ${filterSql} ORDER BY id`);
|
|
162
|
+
}
|
|
163
|
+
// Catalog plane projection: catalog_private.apis is trigger-derived from
|
|
164
|
+
// routing_public.apis (catalog_private.tg_apis_catalog_sync). The sync
|
|
165
|
+
// trigger is skipped during migration replay (session_replication_role),
|
|
166
|
+
// so the projection is materialized here. Shared with the GraphQL flow
|
|
167
|
+
// (see catalog-projection.ts) for cross-flow parity.
|
|
168
|
+
const apisRows = rawRows['apis'];
|
|
169
|
+
if (sql['apis'] && apisRows?.length) {
|
|
170
|
+
const parsed = await (0, catalog_projection_1.projectCatalogApis)(apisRows);
|
|
171
|
+
if (parsed) {
|
|
172
|
+
sql['catalog_private.apis'] = parsed;
|
|
173
|
+
}
|
|
151
174
|
}
|
|
152
175
|
return sql;
|
|
153
176
|
};
|
package/export-utils.d.ts
CHANGED
|
@@ -62,6 +62,13 @@ export interface TableConfig {
|
|
|
62
62
|
conflictDoNothing?: boolean;
|
|
63
63
|
typeOverrides?: Record<string, FieldType>;
|
|
64
64
|
gqlTypeName?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Table has no database_id column; rows belong to a tenant through their
|
|
67
|
+
* domain_id FK (e.g. hostname_bindings, route_bindings). The export filters
|
|
68
|
+
* them by `domainId in (<tenant's domain ids>)` — the tenant's domain ids
|
|
69
|
+
* are pre-fetched once per export run.
|
|
70
|
+
*/
|
|
71
|
+
filterViaDomainIds?: boolean;
|
|
65
72
|
/** Columns whose values are environment-specific and should be excluded from the
|
|
66
73
|
* exported INSERT so that the column's DDL DEFAULT applies at deploy time.
|
|
67
74
|
* Key = column name, Value = the SQL expression the column defaults to (for documentation).
|
package/export-utils.js
CHANGED
|
@@ -153,6 +153,12 @@ exports.buildMetaRevertScript = buildMetaRevertScript;
|
|
|
153
153
|
* values must come from DDL defaults at deploy time.
|
|
154
154
|
*/
|
|
155
155
|
exports.META_TABLE_OVERRIDES = {
|
|
156
|
+
hostname_bindings: {
|
|
157
|
+
filterViaDomainIds: true
|
|
158
|
+
},
|
|
159
|
+
route_bindings: {
|
|
160
|
+
filterViaDomainIds: true
|
|
161
|
+
},
|
|
156
162
|
sites: {
|
|
157
163
|
typeOverrides: {
|
|
158
164
|
og_image: 'image',
|
package/graphql-client.js
CHANGED
|
@@ -124,6 +124,8 @@ class GraphQLClient {
|
|
|
124
124
|
if (condition && Object.keys(condition).length > 0) {
|
|
125
125
|
const filterParts = Object.entries(condition)
|
|
126
126
|
.map(([k, v]) => {
|
|
127
|
+
if (Array.isArray(v))
|
|
128
|
+
return `${k}: { in: [${v.map((item) => `"${item}"`).join(', ')}] }`;
|
|
127
129
|
if (typeof v === 'string')
|
|
128
130
|
return `${k}: { equalTo: "${v}" }`;
|
|
129
131
|
if (typeof v === 'boolean')
|
package/meta-export-tables.json
CHANGED
|
@@ -591,6 +591,16 @@
|
|
|
591
591
|
"schema": "routing_public",
|
|
592
592
|
"table": "routes"
|
|
593
593
|
},
|
|
594
|
+
{
|
|
595
|
+
"key": "hostname_bindings",
|
|
596
|
+
"schema": "routing_public",
|
|
597
|
+
"table": "hostname_bindings"
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"key": "route_bindings",
|
|
601
|
+
"schema": "routing_public",
|
|
602
|
+
"table": "route_bindings"
|
|
603
|
+
},
|
|
594
604
|
{
|
|
595
605
|
"key": "site_app_links",
|
|
596
606
|
"schema": "routing_public",
|
|
@@ -671,11 +681,6 @@
|
|
|
671
681
|
"schema": "catalog_private",
|
|
672
682
|
"table": "domains"
|
|
673
683
|
},
|
|
674
|
-
{
|
|
675
|
-
"key": "functions",
|
|
676
|
-
"schema": "catalog_private",
|
|
677
|
-
"table": "functions"
|
|
678
|
-
},
|
|
679
684
|
{
|
|
680
685
|
"key": "namespaces",
|
|
681
686
|
"schema": "catalog_private",
|
|
@@ -957,6 +962,12 @@
|
|
|
957
962
|
"created_at",
|
|
958
963
|
"updated_at"
|
|
959
964
|
],
|
|
965
|
+
"routing_public.hostname_bindings": [
|
|
966
|
+
"updated_at"
|
|
967
|
+
],
|
|
968
|
+
"routing_public.route_bindings": [
|
|
969
|
+
"updated_at"
|
|
970
|
+
],
|
|
960
971
|
"routing_public.site_app_links": [
|
|
961
972
|
"created_at",
|
|
962
973
|
"updated_at"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/export",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.12",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM export tools for SQL and GraphQL database migration extraction",
|
|
6
6
|
"main": "index.js",
|
|
@@ -41,22 +41,22 @@
|
|
|
41
41
|
"@pgsql/types": "^18.0.0",
|
|
42
42
|
"@types/pg": "^8.20.4",
|
|
43
43
|
"makage": "^0.3.0",
|
|
44
|
-
"pgsql-test": "^5.10.
|
|
44
|
+
"pgsql-test": "^5.10.8"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@constructive-io/query-builder": "^3.10.4",
|
|
48
|
-
"@pgpmjs/core": "^7.25.
|
|
49
|
-
"@pgpmjs/migrate-client": "^1.11.
|
|
48
|
+
"@pgpmjs/core": "^7.25.5",
|
|
49
|
+
"@pgpmjs/migrate-client": "^1.11.11",
|
|
50
50
|
"@pgpmjs/naming-spec": "^0.3.0",
|
|
51
|
-
"@pgpmjs/transform": "^0.23.
|
|
52
|
-
"@pgpmjs/types": "^2.50.
|
|
51
|
+
"@pgpmjs/transform": "^0.23.5",
|
|
52
|
+
"@pgpmjs/types": "^2.50.4",
|
|
53
53
|
"csv-to-pg": "^4.10.4",
|
|
54
54
|
"glob": "^13.0.6",
|
|
55
55
|
"inflekt": "^0.8.1",
|
|
56
56
|
"inquirerer": "^4.9.3",
|
|
57
57
|
"pg": "^8.21.0",
|
|
58
|
-
"pg-cache": "^3.25.
|
|
59
|
-
"pg-env": "^1.29.
|
|
58
|
+
"pg-cache": "^3.25.5",
|
|
59
|
+
"pg-env": "^1.29.2"
|
|
60
60
|
},
|
|
61
|
-
"gitHead": "
|
|
61
|
+
"gitHead": "eacbe10ce544848c93a4c17fc8a8474d1a788a18"
|
|
62
62
|
}
|