@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.
@@ -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;
@@ -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 {};