@pgpmjs/export 0.1.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/LICENSE +23 -0
- package/README.md +86 -0
- package/esm/export-graphql-meta.js +151 -0
- package/esm/export-graphql.js +176 -0
- package/esm/export-meta.js +162 -0
- package/esm/export-migrations.js +171 -0
- package/esm/export-utils.js +1159 -0
- package/esm/graphql-client.js +139 -0
- package/esm/graphql-naming.js +76 -0
- package/esm/index.js +7 -0
- package/export-graphql-meta.d.ts +13 -0
- package/export-graphql-meta.js +155 -0
- package/export-graphql.d.ts +52 -0
- package/export-graphql.js +180 -0
- package/export-meta.d.ts +9 -0
- package/export-meta.js +166 -0
- package/export-migrations.d.ts +35 -0
- package/export-migrations.js +175 -0
- package/export-utils.d.ts +127 -0
- package/export-utils.js +1170 -0
- package/graphql-client.d.ts +25 -0
- package/graphql-client.js +143 -0
- package/graphql-naming.d.ts +24 -0
- package/graphql-naming.js +83 -0
- package/index.d.ts +8 -0
- package/index.js +40 -0
- package/package.json +60 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple GraphQL HTTP client with pagination and authentication support.
|
|
3
|
+
* Used by the GraphQL export flow to fetch data from the Constructive GraphQL API.
|
|
4
|
+
*/
|
|
5
|
+
export class GraphQLClient {
|
|
6
|
+
endpoint;
|
|
7
|
+
defaultHeaders;
|
|
8
|
+
constructor({ endpoint, token, headers }) {
|
|
9
|
+
this.endpoint = endpoint;
|
|
10
|
+
this.defaultHeaders = {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
13
|
+
...headers
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Execute a single GraphQL query with retry for transient network errors.
|
|
18
|
+
*/
|
|
19
|
+
async query(queryString, variables, retries = 3) {
|
|
20
|
+
const body = { query: queryString };
|
|
21
|
+
if (variables) {
|
|
22
|
+
body.variables = variables;
|
|
23
|
+
}
|
|
24
|
+
let lastError;
|
|
25
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(this.endpoint, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: this.defaultHeaders,
|
|
30
|
+
body: JSON.stringify(body)
|
|
31
|
+
});
|
|
32
|
+
// Try to parse JSON even on non-200 responses (GraphQL servers often
|
|
33
|
+
// return 400 with a JSON body containing error details)
|
|
34
|
+
let json;
|
|
35
|
+
try {
|
|
36
|
+
json = (await response.json());
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const hint = response.status === 404
|
|
41
|
+
? `\n Hint: Check that the API is configured for this endpoint and required headers (e.g. X-Meta-Schema) are set.`
|
|
42
|
+
: '';
|
|
43
|
+
throw new Error(`GraphQL request failed: ${response.status} ${response.statusText} at ${this.endpoint}${hint}`);
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`GraphQL response is not valid JSON (got ${response.headers.get('content-type') || 'unknown content-type'}) from ${this.endpoint}.\n Hint: The endpoint may be serving a different app (e.g. Next.js dashboard). Try using http://[::1]:<port>/graphql to target the GraphQL server directly.`);
|
|
46
|
+
}
|
|
47
|
+
if (json.errors?.length) {
|
|
48
|
+
const messages = json.errors.map(e => e.message).join('; ');
|
|
49
|
+
throw new Error(`GraphQL errors: ${messages}`);
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const hint = response.status === 404
|
|
53
|
+
? `\n Hint: Check that the API is configured for this endpoint and required headers (e.g. X-Meta-Schema) are set.`
|
|
54
|
+
: '';
|
|
55
|
+
throw new Error(`GraphQL request failed: ${response.status} ${response.statusText} at ${this.endpoint}${hint}`);
|
|
56
|
+
}
|
|
57
|
+
if (!json.data) {
|
|
58
|
+
throw new Error('GraphQL response missing data');
|
|
59
|
+
}
|
|
60
|
+
return json.data;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
64
|
+
const cause = lastError.cause;
|
|
65
|
+
const isTransient = cause?.code === 'ECONNRESET' ||
|
|
66
|
+
cause?.code === 'ECONNREFUSED' ||
|
|
67
|
+
cause?.code === 'UND_ERR_SOCKET' ||
|
|
68
|
+
lastError.message.includes('fetch failed');
|
|
69
|
+
if (isTransient && attempt < retries - 1) {
|
|
70
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
|
|
71
|
+
console.warn(` Retry ${attempt + 1}/${retries - 1}: ${cause?.code || lastError.message} — waiting ${delay}ms...`);
|
|
72
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw lastError;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw lastError;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fetch all rows from a paginated GraphQL connection, handling cursor-based pagination.
|
|
82
|
+
* Returns all nodes across all pages.
|
|
83
|
+
*/
|
|
84
|
+
async fetchAllNodes(queryFieldName, fieldsFragment, condition, pageSize = 100, orderBy = 'ID_ASC') {
|
|
85
|
+
const allNodes = [];
|
|
86
|
+
let hasNextPage = true;
|
|
87
|
+
let afterCursor = null;
|
|
88
|
+
while (hasNextPage) {
|
|
89
|
+
const args = this.buildConnectionArgs(condition, pageSize, afterCursor, orderBy);
|
|
90
|
+
const queryString = `{
|
|
91
|
+
${queryFieldName}${args} {
|
|
92
|
+
nodes {
|
|
93
|
+
${fieldsFragment}
|
|
94
|
+
}
|
|
95
|
+
pageInfo {
|
|
96
|
+
hasNextPage
|
|
97
|
+
endCursor
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}`;
|
|
101
|
+
const data = await this.query(queryString);
|
|
102
|
+
const connection = data[queryFieldName];
|
|
103
|
+
if (!connection?.nodes) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
allNodes.push(...connection.nodes);
|
|
107
|
+
hasNextPage = connection.pageInfo?.hasNextPage ?? false;
|
|
108
|
+
afterCursor = connection.pageInfo?.endCursor ?? null;
|
|
109
|
+
}
|
|
110
|
+
return allNodes;
|
|
111
|
+
}
|
|
112
|
+
buildConnectionArgs(condition, first, after, orderBy) {
|
|
113
|
+
const parts = [];
|
|
114
|
+
if (first) {
|
|
115
|
+
parts.push(`first: ${first}`);
|
|
116
|
+
}
|
|
117
|
+
if (after) {
|
|
118
|
+
parts.push(`after: "${after}"`);
|
|
119
|
+
}
|
|
120
|
+
if (condition && Object.keys(condition).length > 0) {
|
|
121
|
+
const filterParts = Object.entries(condition)
|
|
122
|
+
.map(([k, v]) => {
|
|
123
|
+
if (typeof v === 'string')
|
|
124
|
+
return `${k}: { equalTo: "${v}" }`;
|
|
125
|
+
if (typeof v === 'boolean')
|
|
126
|
+
return `${k}: { equalTo: ${v} }`;
|
|
127
|
+
if (typeof v === 'number')
|
|
128
|
+
return `${k}: { equalTo: ${v} }`;
|
|
129
|
+
return `${k}: { equalTo: "${v}" }`;
|
|
130
|
+
})
|
|
131
|
+
.join(', ');
|
|
132
|
+
parts.push(`where: { ${filterParts} }`);
|
|
133
|
+
}
|
|
134
|
+
if (orderBy) {
|
|
135
|
+
parts.push(`orderBy: ${orderBy}`);
|
|
136
|
+
}
|
|
137
|
+
return parts.length > 0 ? `(${parts.join(', ')})` : '';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for mapping between PostgreSQL names and PostGraphile GraphQL names.
|
|
3
|
+
*
|
|
4
|
+
* PostGraphile's inflection (with InflektPreset) transforms:
|
|
5
|
+
* - Table names: snake_case -> camelCase (pluralized for collections)
|
|
6
|
+
* - Column names: snake_case -> camelCase
|
|
7
|
+
* - Schema prefix is stripped (tables are exposed without schema prefix)
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* metaschema_public.database -> databases (query), Database (type)
|
|
11
|
+
* metaschema_public.foreign_key_constraint -> foreignKeyConstraints
|
|
12
|
+
* services_public.api_schemas -> apiSchemas
|
|
13
|
+
* db_migrate.sql_actions -> sqlActions
|
|
14
|
+
* column database_id -> databaseId
|
|
15
|
+
*/
|
|
16
|
+
import { camelize, distinctPluralize, singularizeLast, underscore } from 'inflekt';
|
|
17
|
+
/**
|
|
18
|
+
* Get the GraphQL query field name for a given Postgres table name.
|
|
19
|
+
* Mirrors the PostGraphile InflektPlugin's allRowsConnection inflector:
|
|
20
|
+
* camelize(distinctPluralize(singularizeLast(camelize(pgTableName))), true)
|
|
21
|
+
*/
|
|
22
|
+
export const getGraphQLQueryName = (pgTableName) => {
|
|
23
|
+
const pascal = camelize(pgTableName);
|
|
24
|
+
const singularized = singularizeLast(pascal);
|
|
25
|
+
return camelize(distinctPluralize(singularized), true);
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Convert a row of GraphQL camelCase keys back to Postgres snake_case keys.
|
|
29
|
+
* This is needed because the csv-to-pg Parser expects snake_case column names.
|
|
30
|
+
* Only transforms top-level keys — nested objects (e.g. JSONB values) are left intact.
|
|
31
|
+
*/
|
|
32
|
+
export const graphqlRowToPostgresRow = (row) => {
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, value] of Object.entries(row)) {
|
|
35
|
+
result[underscore(key)] = value;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Convert a PostgreSQL interval object (from GraphQL Interval type) back to a Postgres interval string.
|
|
41
|
+
* e.g. { years: 0, months: 0, days: 0, hours: 1, minutes: 30, seconds: 0 } -> '1 hour 30 minutes'
|
|
42
|
+
*/
|
|
43
|
+
export const intervalToPostgres = (interval) => {
|
|
44
|
+
if (!interval)
|
|
45
|
+
return null;
|
|
46
|
+
const parts = [];
|
|
47
|
+
if (interval.years)
|
|
48
|
+
parts.push(`${interval.years} year${interval.years !== 1 ? 's' : ''}`);
|
|
49
|
+
if (interval.months)
|
|
50
|
+
parts.push(`${interval.months} mon${interval.months !== 1 ? 's' : ''}`);
|
|
51
|
+
if (interval.days)
|
|
52
|
+
parts.push(`${interval.days} day${interval.days !== 1 ? 's' : ''}`);
|
|
53
|
+
if (interval.hours)
|
|
54
|
+
parts.push(`${interval.hours}:${String(interval.minutes ?? 0).padStart(2, '0')}:${String(interval.seconds ?? 0).padStart(2, '0')}`);
|
|
55
|
+
else if (interval.minutes)
|
|
56
|
+
parts.push(`00:${String(interval.minutes).padStart(2, '0')}:${String(interval.seconds ?? 0).padStart(2, '0')}`);
|
|
57
|
+
else if (interval.seconds)
|
|
58
|
+
parts.push(`00:00:${String(interval.seconds).padStart(2, '0')}`);
|
|
59
|
+
return parts.length > 0 ? parts.join(' ') : '00:00:00';
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Convert an array of Postgres field names (with optional type hints) to a GraphQL fields fragment.
|
|
63
|
+
* Handles composite types like 'interval' by expanding them into subfield selections.
|
|
64
|
+
* e.g. [['id', 'uuid'], ['sessions_default_expiration', 'interval']] ->
|
|
65
|
+
* 'id\nsessionsDefaultExpiration { seconds minutes hours days months years }'
|
|
66
|
+
*/
|
|
67
|
+
export const buildFieldsFragment = (pgFieldNames, fieldTypes) => {
|
|
68
|
+
return pgFieldNames.map(name => {
|
|
69
|
+
const camel = camelize(name, true);
|
|
70
|
+
const fieldType = fieldTypes?.[name];
|
|
71
|
+
if (fieldType === 'interval') {
|
|
72
|
+
return `${camel} { seconds minutes hours days months years }`;
|
|
73
|
+
}
|
|
74
|
+
return camel;
|
|
75
|
+
}).join('\n ');
|
|
76
|
+
};
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './export-meta';
|
|
2
|
+
export * from './export-migrations';
|
|
3
|
+
export * from './export-graphql';
|
|
4
|
+
export * from './export-graphql-meta';
|
|
5
|
+
export { GraphQLClient } from './graphql-client';
|
|
6
|
+
export { getGraphQLQueryName, graphqlRowToPostgresRow, buildFieldsFragment, intervalToPostgres } from './graphql-naming';
|
|
7
|
+
export { DB_REQUIRED_EXTENSIONS, SERVICE_REQUIRED_EXTENSIONS, META_COMMON_HEADER, META_COMMON_FOOTER, META_TABLE_ORDER, META_TABLE_CONFIG, makeReplacer, preparePackage, normalizeOutdir, detectMissingModules, installMissingModules } from './export-utils';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GraphQLClient } from './graphql-client';
|
|
2
|
+
export interface ExportGraphQLMetaParams {
|
|
3
|
+
/** GraphQL client configured for the meta/services API endpoint */
|
|
4
|
+
client: GraphQLClient;
|
|
5
|
+
/** The database_id to filter by */
|
|
6
|
+
database_id: string;
|
|
7
|
+
}
|
|
8
|
+
export type ExportGraphQLMetaResult = Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetch metadata via GraphQL and generate SQL INSERT statements.
|
|
11
|
+
* This is the GraphQL equivalent of exportMeta() in export-meta.ts.
|
|
12
|
+
*/
|
|
13
|
+
export declare const exportGraphQLMeta: ({ client, database_id }: ExportGraphQLMetaParams) => Promise<ExportGraphQLMetaResult>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportGraphQLMeta = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* GraphQL equivalent of export-meta.ts.
|
|
6
|
+
*
|
|
7
|
+
* Fetches metadata from metaschema_public, services_public, and metaschema_modules_public
|
|
8
|
+
* via GraphQL queries instead of direct SQL, then uses the same csv-to-pg Parser to
|
|
9
|
+
* generate SQL INSERT statements.
|
|
10
|
+
*/
|
|
11
|
+
const csv_to_pg_1 = require("csv-to-pg");
|
|
12
|
+
const export_utils_1 = require("./export-utils");
|
|
13
|
+
const graphql_naming_1 = require("./graphql-naming");
|
|
14
|
+
/**
|
|
15
|
+
* Fetch metadata via GraphQL and generate SQL INSERT statements.
|
|
16
|
+
* This is the GraphQL equivalent of exportMeta() in export-meta.ts.
|
|
17
|
+
*/
|
|
18
|
+
const exportGraphQLMeta = async ({ client, database_id }) => {
|
|
19
|
+
const sql = {};
|
|
20
|
+
const queryAndParse = async (key) => {
|
|
21
|
+
const tableConfig = export_utils_1.META_TABLE_CONFIG[key];
|
|
22
|
+
if (!tableConfig)
|
|
23
|
+
return;
|
|
24
|
+
const pgFieldNames = Object.keys(tableConfig.fields);
|
|
25
|
+
const graphqlFieldsFragment = (0, graphql_naming_1.buildFieldsFragment)(pgFieldNames, tableConfig.fields);
|
|
26
|
+
const graphqlQueryName = (0, graphql_naming_1.getGraphQLQueryName)(tableConfig.table);
|
|
27
|
+
// The 'database' table is fetched by id, not by database_id
|
|
28
|
+
const condition = key === 'database'
|
|
29
|
+
? { id: database_id }
|
|
30
|
+
: { databaseId: database_id };
|
|
31
|
+
try {
|
|
32
|
+
const rows = await client.fetchAllNodes(graphqlQueryName, graphqlFieldsFragment, condition);
|
|
33
|
+
if (rows.length > 0) {
|
|
34
|
+
// Convert camelCase GraphQL keys back to snake_case for the Parser
|
|
35
|
+
// Also convert interval objects back to Postgres interval strings
|
|
36
|
+
const pgRows = rows.map(row => {
|
|
37
|
+
const pgRow = (0, graphql_naming_1.graphqlRowToPostgresRow)(row);
|
|
38
|
+
// Convert any interval fields from {seconds, minutes, ...} objects to strings
|
|
39
|
+
for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
|
|
40
|
+
if (fieldType === 'interval' && pgRow[fieldName] && typeof pgRow[fieldName] === 'object') {
|
|
41
|
+
pgRow[fieldName] = (0, graphql_naming_1.intervalToPostgres)(pgRow[fieldName]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return pgRow;
|
|
45
|
+
});
|
|
46
|
+
// Filter fields to only those that exist in the returned data
|
|
47
|
+
// This mirrors the dynamic field building in the SQL version
|
|
48
|
+
const returnedKeys = new Set();
|
|
49
|
+
for (const row of pgRows) {
|
|
50
|
+
for (const k of Object.keys(row)) {
|
|
51
|
+
returnedKeys.add(k);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const dynamicFields = {};
|
|
55
|
+
for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
|
|
56
|
+
if (returnedKeys.has(fieldName)) {
|
|
57
|
+
dynamicFields[fieldName] = fieldType;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(dynamicFields).length === 0)
|
|
61
|
+
return;
|
|
62
|
+
const parser = new csv_to_pg_1.Parser({
|
|
63
|
+
schema: tableConfig.schema,
|
|
64
|
+
table: tableConfig.table,
|
|
65
|
+
conflictDoNothing: tableConfig.conflictDoNothing,
|
|
66
|
+
fields: dynamicFields
|
|
67
|
+
});
|
|
68
|
+
const parsed = await parser.parse(pgRows);
|
|
69
|
+
if (parsed) {
|
|
70
|
+
sql[key] = parsed;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// If the GraphQL query fails (e.g. table not exposed), skip silently
|
|
76
|
+
// similar to how the SQL version handles 42P01 (undefined_table)
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
if (message.includes('Cannot query field') ||
|
|
79
|
+
message.includes('is not defined by type') ||
|
|
80
|
+
message.includes('Unknown field') ||
|
|
81
|
+
(message.includes('Field') && message.includes('not found'))) {
|
|
82
|
+
// Field/table not available in the GraphQL schema — skip
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
// Batch queries by schema group — independent HTTP requests run in parallel
|
|
89
|
+
// within each group for significant speedup over sequential awaits.
|
|
90
|
+
// metaschema_public tables
|
|
91
|
+
await Promise.all([
|
|
92
|
+
queryAndParse('database'),
|
|
93
|
+
queryAndParse('database_extension'),
|
|
94
|
+
queryAndParse('schema'),
|
|
95
|
+
queryAndParse('table'),
|
|
96
|
+
queryAndParse('field'),
|
|
97
|
+
queryAndParse('policy'),
|
|
98
|
+
queryAndParse('index'),
|
|
99
|
+
queryAndParse('trigger'),
|
|
100
|
+
queryAndParse('trigger_function'),
|
|
101
|
+
queryAndParse('rls_function'),
|
|
102
|
+
queryAndParse('foreign_key_constraint'),
|
|
103
|
+
queryAndParse('primary_key_constraint'),
|
|
104
|
+
queryAndParse('unique_constraint'),
|
|
105
|
+
queryAndParse('check_constraint'),
|
|
106
|
+
queryAndParse('full_text_search'),
|
|
107
|
+
queryAndParse('schema_grant'),
|
|
108
|
+
queryAndParse('table_grant'),
|
|
109
|
+
queryAndParse('default_privilege')
|
|
110
|
+
]);
|
|
111
|
+
// services_public tables
|
|
112
|
+
await Promise.all([
|
|
113
|
+
queryAndParse('domains'),
|
|
114
|
+
queryAndParse('sites'),
|
|
115
|
+
queryAndParse('apis'),
|
|
116
|
+
queryAndParse('apps'),
|
|
117
|
+
queryAndParse('site_modules'),
|
|
118
|
+
queryAndParse('site_themes'),
|
|
119
|
+
queryAndParse('site_metadata'),
|
|
120
|
+
queryAndParse('api_modules'),
|
|
121
|
+
queryAndParse('api_extensions'),
|
|
122
|
+
queryAndParse('api_schemas')
|
|
123
|
+
]);
|
|
124
|
+
// metaschema_modules_public tables
|
|
125
|
+
await Promise.all([
|
|
126
|
+
queryAndParse('rls_module'),
|
|
127
|
+
queryAndParse('user_auth_module'),
|
|
128
|
+
queryAndParse('memberships_module'),
|
|
129
|
+
queryAndParse('permissions_module'),
|
|
130
|
+
queryAndParse('limits_module'),
|
|
131
|
+
queryAndParse('levels_module'),
|
|
132
|
+
queryAndParse('users_module'),
|
|
133
|
+
queryAndParse('hierarchy_module'),
|
|
134
|
+
queryAndParse('membership_types_module'),
|
|
135
|
+
queryAndParse('invites_module'),
|
|
136
|
+
queryAndParse('emails_module'),
|
|
137
|
+
queryAndParse('sessions_module'),
|
|
138
|
+
queryAndParse('secrets_module'),
|
|
139
|
+
queryAndParse('profiles_module'),
|
|
140
|
+
queryAndParse('encrypted_secrets_module'),
|
|
141
|
+
queryAndParse('connected_accounts_module'),
|
|
142
|
+
queryAndParse('phone_numbers_module'),
|
|
143
|
+
queryAndParse('crypto_addresses_module'),
|
|
144
|
+
queryAndParse('crypto_auth_module'),
|
|
145
|
+
queryAndParse('field_module'),
|
|
146
|
+
queryAndParse('table_module'),
|
|
147
|
+
queryAndParse('table_template_module'),
|
|
148
|
+
queryAndParse('secure_table_provision'),
|
|
149
|
+
queryAndParse('uuid_module'),
|
|
150
|
+
queryAndParse('default_ids_module'),
|
|
151
|
+
queryAndParse('denormalized_table_field')
|
|
152
|
+
]);
|
|
153
|
+
return sql;
|
|
154
|
+
};
|
|
155
|
+
exports.exportGraphQLMeta = exportGraphQLMeta;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL-based export orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* This is a standalone GraphQL export flow that mirrors export-migrations.ts
|
|
5
|
+
* but fetches all data via GraphQL queries instead of direct SQL.
|
|
6
|
+
*
|
|
7
|
+
* Per Dan's guidance: "I would NOT do branching in those existing files.
|
|
8
|
+
* I would make the GraphQL flow its entire own flow at first."
|
|
9
|
+
*/
|
|
10
|
+
import { Inquirerer } from 'inquirerer';
|
|
11
|
+
import { PgpmPackage } from '@pgpmjs/core';
|
|
12
|
+
import { Schema } from './export-utils';
|
|
13
|
+
export interface ExportGraphQLOptions {
|
|
14
|
+
project: PgpmPackage;
|
|
15
|
+
/** GraphQL endpoint for metaschema/services data (e.g. http://private.localhost:3002/graphql) */
|
|
16
|
+
metaEndpoint: string;
|
|
17
|
+
/** GraphQL endpoint for db_migrate data (e.g. http://db_migrate.localhost:3000/graphql) */
|
|
18
|
+
migrateEndpoint?: string;
|
|
19
|
+
/** Extra headers for the migrate endpoint (e.g. Host header for subdomain routing) */
|
|
20
|
+
migrateHeaders?: Record<string, string>;
|
|
21
|
+
/** Bearer token for authentication */
|
|
22
|
+
token?: string;
|
|
23
|
+
/** Extra headers to send with GraphQL requests (e.g. X-Meta-Schema) */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Database ID to export */
|
|
26
|
+
databaseId: string;
|
|
27
|
+
/** Database display name */
|
|
28
|
+
databaseName: string;
|
|
29
|
+
/** Schema names selected for export */
|
|
30
|
+
schema_names: string[];
|
|
31
|
+
/** Schema rows (with name and schema_name) for the replacer */
|
|
32
|
+
schemas: Schema[];
|
|
33
|
+
/** Author string */
|
|
34
|
+
author: string;
|
|
35
|
+
/** Output directory for packages */
|
|
36
|
+
outdir: string;
|
|
37
|
+
/** Extension name for the DB module */
|
|
38
|
+
extensionName: string;
|
|
39
|
+
/** Description for the DB extension */
|
|
40
|
+
extensionDesc?: string;
|
|
41
|
+
/** Extension name for the service/meta module */
|
|
42
|
+
metaExtensionName: string;
|
|
43
|
+
/** Description for the service/meta extension */
|
|
44
|
+
metaExtensionDesc?: string;
|
|
45
|
+
prompter?: Inquirerer;
|
|
46
|
+
argv?: Record<string, any>;
|
|
47
|
+
repoName?: string;
|
|
48
|
+
username?: string;
|
|
49
|
+
serviceOutdir?: string;
|
|
50
|
+
skipSchemaRenaming?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare const exportGraphQL: ({ project, metaEndpoint, migrateEndpoint, token, headers, migrateHeaders, databaseId, databaseName, schema_names, schemas, author, outdir, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, argv, repoName, username, serviceOutdir, skipSchemaRenaming }: ExportGraphQLOptions) => Promise<void>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportGraphQL = void 0;
|
|
4
|
+
const core_1 = require("@pgpmjs/core");
|
|
5
|
+
const migrate_client_1 = require("@pgpmjs/migrate-client");
|
|
6
|
+
const graphql_client_1 = require("./graphql-client");
|
|
7
|
+
const export_graphql_meta_1 = require("./export-graphql-meta");
|
|
8
|
+
const graphql_naming_1 = require("./graphql-naming");
|
|
9
|
+
const export_utils_1 = require("./export-utils");
|
|
10
|
+
const exportGraphQL = async ({ project, metaEndpoint, migrateEndpoint, token, headers, migrateHeaders, databaseId, databaseName, schema_names, schemas, author, outdir, extensionName, extensionDesc, metaExtensionName, metaExtensionDesc, prompter, argv, repoName, username, serviceOutdir, skipSchemaRenaming = false }) => {
|
|
11
|
+
const normalizedOutdir = (0, export_utils_1.normalizeOutdir)(outdir);
|
|
12
|
+
const svcOutdir = (0, export_utils_1.normalizeOutdir)(serviceOutdir || outdir);
|
|
13
|
+
const name = extensionName;
|
|
14
|
+
const schemasForReplacement = skipSchemaRenaming
|
|
15
|
+
? []
|
|
16
|
+
: schemas.filter((schema) => schema_names.includes(schema.schema_name));
|
|
17
|
+
const { replacer } = (0, export_utils_1.makeReplacer)({
|
|
18
|
+
schemas: schemasForReplacement,
|
|
19
|
+
name
|
|
20
|
+
});
|
|
21
|
+
// =========================================================================
|
|
22
|
+
// 1. Fetch sql_actions via @pgpmjs/migrate-client ORM (db_migrate endpoint)
|
|
23
|
+
// =========================================================================
|
|
24
|
+
let sqlActionRows = [];
|
|
25
|
+
if (migrateEndpoint) {
|
|
26
|
+
console.log(`Fetching sql_actions from ${migrateEndpoint}...`);
|
|
27
|
+
const db = (0, migrate_client_1.createClient)({
|
|
28
|
+
endpoint: migrateEndpoint,
|
|
29
|
+
headers: {
|
|
30
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
31
|
+
...migrateHeaders
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
// Paginate through all sql_actions for this database using the ORM.
|
|
36
|
+
// The ORM generates `where: SqlActionFilter` which uses the filter plugin's
|
|
37
|
+
// `equalTo` operator — the correct approach for Constructive's PostGraphile APIs.
|
|
38
|
+
let hasNextPage = true;
|
|
39
|
+
let afterCursor;
|
|
40
|
+
const PAGE_SIZE = 100;
|
|
41
|
+
while (hasNextPage) {
|
|
42
|
+
const result = await db.sqlAction.findMany({
|
|
43
|
+
select: {
|
|
44
|
+
id: true,
|
|
45
|
+
databaseId: true,
|
|
46
|
+
name: true,
|
|
47
|
+
deploy: true,
|
|
48
|
+
revert: true,
|
|
49
|
+
verify: true,
|
|
50
|
+
content: true,
|
|
51
|
+
deps: true,
|
|
52
|
+
action: true,
|
|
53
|
+
actionId: true,
|
|
54
|
+
actorId: true,
|
|
55
|
+
payload: true
|
|
56
|
+
},
|
|
57
|
+
where: {
|
|
58
|
+
databaseId: { equalTo: databaseId }
|
|
59
|
+
},
|
|
60
|
+
orderBy: ['ID_ASC'],
|
|
61
|
+
first: PAGE_SIZE,
|
|
62
|
+
...(afterCursor ? { after: afterCursor } : {})
|
|
63
|
+
}).unwrap();
|
|
64
|
+
const connection = result.sqlActions;
|
|
65
|
+
for (const node of connection.nodes) {
|
|
66
|
+
sqlActionRows.push((0, graphql_naming_1.graphqlRowToPostgresRow)(node));
|
|
67
|
+
}
|
|
68
|
+
hasNextPage = connection.pageInfo?.hasNextPage ?? false;
|
|
69
|
+
afterCursor = connection.pageInfo?.endCursor ?? undefined;
|
|
70
|
+
}
|
|
71
|
+
console.log(` Found ${sqlActionRows.length} sql_actions`);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.warn(` Warning: Could not fetch sql_actions: ${err instanceof Error ? err.message : err}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log('No migrate endpoint provided, skipping sql_actions export.');
|
|
79
|
+
}
|
|
80
|
+
const opts = {
|
|
81
|
+
name,
|
|
82
|
+
replacer,
|
|
83
|
+
outdir: normalizedOutdir,
|
|
84
|
+
author
|
|
85
|
+
};
|
|
86
|
+
const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
|
|
87
|
+
if (sqlActionRows.length > 0) {
|
|
88
|
+
const dbMissingResult = await (0, export_utils_1.detectMissingModules)(project, [...export_utils_1.DB_REQUIRED_EXTENSIONS], prompter, argv);
|
|
89
|
+
const dbModuleDir = await (0, export_utils_1.preparePackage)({
|
|
90
|
+
project,
|
|
91
|
+
author,
|
|
92
|
+
outdir: normalizedOutdir,
|
|
93
|
+
name,
|
|
94
|
+
description: dbExtensionDesc,
|
|
95
|
+
extensions: [...export_utils_1.DB_REQUIRED_EXTENSIONS],
|
|
96
|
+
prompter,
|
|
97
|
+
repoName,
|
|
98
|
+
username
|
|
99
|
+
});
|
|
100
|
+
if (dbMissingResult.shouldInstall) {
|
|
101
|
+
await (0, export_utils_1.installMissingModules)(dbModuleDir, dbMissingResult.missingModules);
|
|
102
|
+
}
|
|
103
|
+
(0, core_1.writePgpmPlan)(sqlActionRows, opts);
|
|
104
|
+
(0, core_1.writePgpmFiles)(sqlActionRows, opts);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log('No sql_actions found. Skipping database module export.');
|
|
108
|
+
}
|
|
109
|
+
// =========================================================================
|
|
110
|
+
// 2. Fetch meta/services data via GraphQL
|
|
111
|
+
// =========================================================================
|
|
112
|
+
console.log(`Fetching metadata from ${metaEndpoint}...`);
|
|
113
|
+
const metaClient = new graphql_client_1.GraphQLClient({ endpoint: metaEndpoint, token, headers });
|
|
114
|
+
const metaResult = await (0, export_graphql_meta_1.exportGraphQLMeta)({
|
|
115
|
+
client: metaClient,
|
|
116
|
+
database_id: databaseId
|
|
117
|
+
});
|
|
118
|
+
const metaTableCount = Object.keys(metaResult).length;
|
|
119
|
+
console.log(` Fetched ${metaTableCount} meta tables with data`);
|
|
120
|
+
if (metaTableCount > 0) {
|
|
121
|
+
const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
|
|
122
|
+
const svcMissingResult = await (0, export_utils_1.detectMissingModules)(project, [...export_utils_1.SERVICE_REQUIRED_EXTENSIONS], prompter, argv);
|
|
123
|
+
const svcModuleDir = await (0, export_utils_1.preparePackage)({
|
|
124
|
+
project,
|
|
125
|
+
author,
|
|
126
|
+
outdir: svcOutdir,
|
|
127
|
+
name: metaExtensionName,
|
|
128
|
+
description: metaDesc,
|
|
129
|
+
extensions: [...export_utils_1.SERVICE_REQUIRED_EXTENSIONS],
|
|
130
|
+
prompter,
|
|
131
|
+
repoName,
|
|
132
|
+
username
|
|
133
|
+
});
|
|
134
|
+
if (svcMissingResult.shouldInstall) {
|
|
135
|
+
await (0, export_utils_1.installMissingModules)(svcModuleDir, svcMissingResult.missingModules);
|
|
136
|
+
}
|
|
137
|
+
const metaSchemasForReplacement = skipSchemaRenaming
|
|
138
|
+
? []
|
|
139
|
+
: schemas.filter((schema) => schema_names.includes(schema.schema_name));
|
|
140
|
+
const metaReplacer = (0, export_utils_1.makeReplacer)({
|
|
141
|
+
schemas: metaSchemasForReplacement,
|
|
142
|
+
name: metaExtensionName,
|
|
143
|
+
// Use extensionName for schema prefix — the services metadata references
|
|
144
|
+
// schemas owned by the application package (e.g. agent_db_auth_public),
|
|
145
|
+
// not the services package (agent_db_services_auth_public)
|
|
146
|
+
schemaPrefix: name
|
|
147
|
+
});
|
|
148
|
+
const metaPackage = [];
|
|
149
|
+
const tablesWithContent = [];
|
|
150
|
+
for (const tableName of export_utils_1.META_TABLE_ORDER) {
|
|
151
|
+
const tableSql = metaResult[tableName];
|
|
152
|
+
if (tableSql) {
|
|
153
|
+
const replacedSql = metaReplacer.replacer(tableSql);
|
|
154
|
+
const deps = tableName === 'database'
|
|
155
|
+
? []
|
|
156
|
+
: tablesWithContent.length > 0
|
|
157
|
+
? [`migrate/${tablesWithContent[tablesWithContent.length - 1]}`]
|
|
158
|
+
: [];
|
|
159
|
+
metaPackage.push({
|
|
160
|
+
deps,
|
|
161
|
+
deploy: `migrate/${tableName}`,
|
|
162
|
+
content: `${export_utils_1.META_COMMON_HEADER}
|
|
163
|
+
|
|
164
|
+
${replacedSql}
|
|
165
|
+
|
|
166
|
+
${export_utils_1.META_COMMON_FOOTER}
|
|
167
|
+
`
|
|
168
|
+
});
|
|
169
|
+
tablesWithContent.push(tableName);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
opts.replacer = metaReplacer.replacer;
|
|
173
|
+
opts.name = metaExtensionName;
|
|
174
|
+
opts.outdir = svcOutdir;
|
|
175
|
+
(0, core_1.writePgpmPlan)(metaPackage, opts);
|
|
176
|
+
(0, core_1.writePgpmFiles)(metaPackage, opts);
|
|
177
|
+
}
|
|
178
|
+
console.log('GraphQL export complete.');
|
|
179
|
+
};
|
|
180
|
+
exports.exportGraphQL = exportGraphQL;
|
package/export-meta.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PgpmOptions } from '@pgpmjs/types';
|
|
2
|
+
interface ExportMetaParams {
|
|
3
|
+
opts: PgpmOptions;
|
|
4
|
+
dbname: string;
|
|
5
|
+
database_id: string;
|
|
6
|
+
}
|
|
7
|
+
export type ExportMetaResult = Record<string, string>;
|
|
8
|
+
export declare const exportMeta: ({ opts, dbname, database_id }: ExportMetaParams) => Promise<ExportMetaResult>;
|
|
9
|
+
export {};
|