-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathexport-graphql-meta.ts
More file actions
135 lines (118 loc) · 4.57 KB
/
export-graphql-meta.ts
File metadata and controls
135 lines (118 loc) · 4.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* GraphQL equivalent of export-meta.ts.
*
* Fetches metadata from metaschema_public, services_public, and metaschema_modules_public
* via GraphQL queries instead of direct SQL, then uses the same csv-to-pg Parser to
* generate SQL INSERT statements.
*/
import { Parser } from 'csv-to-pg';
import { FieldType, META_TABLE_CONFIG, META_TABLE_ORDER } from './export-utils';
import { GraphQLClient } from './graphql-client';
import {
buildFieldsFragment,
getGraphQLQueryName,
graphqlRowToPostgresRow,
intervalToPostgres
} from './graphql-naming';
export interface ExportGraphQLMetaParams {
/** GraphQL client configured for the meta/services API endpoint */
client: GraphQLClient;
/** The database_id to filter by */
database_id: string;
}
export type ExportGraphQLMetaResult = Record<string, string>;
/**
* Fetch metadata via GraphQL and generate SQL INSERT statements.
* This is the GraphQL equivalent of exportMeta() in export-meta.ts.
*/
export const exportGraphQLMeta = async ({
client,
database_id
}: ExportGraphQLMetaParams): Promise<ExportGraphQLMetaResult> => {
const sql: Record<string, string> = {};
const queryAndParse = async (key: string) => {
const tableConfig = META_TABLE_CONFIG[key];
if (!tableConfig) return;
const pgFieldNames = Object.keys(tableConfig.fields);
const graphqlFieldsFragment = buildFieldsFragment(pgFieldNames, tableConfig.fields);
const graphqlQueryName = getGraphQLQueryName(tableConfig.table);
// The 'database' table is fetched by id, not by database_id
const condition = key === 'database'
? { id: database_id }
: { databaseId: database_id };
try {
const rows = await client.fetchAllNodes(
graphqlQueryName,
graphqlFieldsFragment,
condition
);
if (rows.length > 0) {
// Convert camelCase GraphQL keys back to snake_case for the Parser
// Also convert interval objects back to Postgres interval strings
const pgRows = rows.map(row => {
const pgRow = graphqlRowToPostgresRow(row);
// Convert any interval fields from {seconds, minutes, ...} objects to strings
for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
if (fieldType === 'interval' && pgRow[fieldName] && typeof pgRow[fieldName] === 'object') {
pgRow[fieldName] = intervalToPostgres(pgRow[fieldName] as Record<string, number | null>);
}
}
return pgRow;
});
// Filter fields to only those that exist in the returned data
// This mirrors the dynamic field building in the SQL version
const returnedKeys = new Set<string>();
for (const row of pgRows) {
for (const k of Object.keys(row)) {
returnedKeys.add(k);
}
}
const dynamicFields: Record<string, FieldType> = {};
for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
if (returnedKeys.has(fieldName)) {
dynamicFields[fieldName] = fieldType;
}
}
if (Object.keys(dynamicFields).length === 0) return;
const parser = new Parser({
schema: tableConfig.schema,
table: tableConfig.table,
conflictDoNothing: tableConfig.conflictDoNothing,
fields: dynamicFields
});
const parsed = await parser.parse(pgRows);
if (parsed) {
sql[key] = parsed;
}
}
} catch (err: unknown) {
// If the GraphQL query fails (e.g. table not exposed), skip silently
// similar to how the SQL version handles 42P01 (undefined_table)
const message = err instanceof Error ? err.message : String(err);
if (
message.includes('Cannot query field') ||
message.includes('is not defined by type') ||
message.includes('Unknown field') ||
(message.includes('Field') && message.includes('not found'))
) {
// Field/table not available in the GraphQL schema — skip
return;
}
throw err;
}
};
// Group tables by schema for parallel execution within each group.
// Uses META_TABLE_ORDER as the single source of truth for which tables to export.
const by_schema = new Map<string, string[]>();
for (const key of META_TABLE_ORDER) {
const config = META_TABLE_CONFIG[key];
if (!config) continue;
const group = by_schema.get(config.schema) || [];
group.push(key);
by_schema.set(config.schema, group);
}
for (const [, keys] of by_schema) {
await Promise.all(keys.map(key => queryAndParse(key)));
}
return sql;
};