@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,25 @@
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
+ interface GraphQLClientOptions {
6
+ endpoint: string;
7
+ token?: string;
8
+ headers?: Record<string, string>;
9
+ }
10
+ export declare class GraphQLClient {
11
+ private endpoint;
12
+ private defaultHeaders;
13
+ constructor({ endpoint, token, headers }: GraphQLClientOptions);
14
+ /**
15
+ * Execute a single GraphQL query with retry for transient network errors.
16
+ */
17
+ query<T = any>(queryString: string, variables?: Record<string, unknown>, retries?: number): Promise<T>;
18
+ /**
19
+ * Fetch all rows from a paginated GraphQL connection, handling cursor-based pagination.
20
+ * Returns all nodes across all pages.
21
+ */
22
+ fetchAllNodes<T = Record<string, unknown>>(queryFieldName: string, fieldsFragment: string, condition?: Record<string, unknown>, pageSize?: number, orderBy?: string): Promise<T[]>;
23
+ private buildConnectionArgs;
24
+ }
25
+ export {};
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Simple GraphQL HTTP client with pagination and authentication support.
4
+ * Used by the GraphQL export flow to fetch data from the Constructive GraphQL API.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.GraphQLClient = void 0;
8
+ class GraphQLClient {
9
+ endpoint;
10
+ defaultHeaders;
11
+ constructor({ endpoint, token, headers }) {
12
+ this.endpoint = endpoint;
13
+ this.defaultHeaders = {
14
+ 'Content-Type': 'application/json',
15
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
16
+ ...headers
17
+ };
18
+ }
19
+ /**
20
+ * Execute a single GraphQL query with retry for transient network errors.
21
+ */
22
+ async query(queryString, variables, retries = 3) {
23
+ const body = { query: queryString };
24
+ if (variables) {
25
+ body.variables = variables;
26
+ }
27
+ let lastError;
28
+ for (let attempt = 0; attempt < retries; attempt++) {
29
+ try {
30
+ const response = await fetch(this.endpoint, {
31
+ method: 'POST',
32
+ headers: this.defaultHeaders,
33
+ body: JSON.stringify(body)
34
+ });
35
+ // Try to parse JSON even on non-200 responses (GraphQL servers often
36
+ // return 400 with a JSON body containing error details)
37
+ let json;
38
+ try {
39
+ json = (await response.json());
40
+ }
41
+ catch {
42
+ if (!response.ok) {
43
+ const hint = response.status === 404
44
+ ? `\n Hint: Check that the API is configured for this endpoint and required headers (e.g. X-Meta-Schema) are set.`
45
+ : '';
46
+ throw new Error(`GraphQL request failed: ${response.status} ${response.statusText} at ${this.endpoint}${hint}`);
47
+ }
48
+ 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.`);
49
+ }
50
+ if (json.errors?.length) {
51
+ const messages = json.errors.map(e => e.message).join('; ');
52
+ throw new Error(`GraphQL errors: ${messages}`);
53
+ }
54
+ if (!response.ok) {
55
+ const hint = response.status === 404
56
+ ? `\n Hint: Check that the API is configured for this endpoint and required headers (e.g. X-Meta-Schema) are set.`
57
+ : '';
58
+ throw new Error(`GraphQL request failed: ${response.status} ${response.statusText} at ${this.endpoint}${hint}`);
59
+ }
60
+ if (!json.data) {
61
+ throw new Error('GraphQL response missing data');
62
+ }
63
+ return json.data;
64
+ }
65
+ catch (err) {
66
+ lastError = err instanceof Error ? err : new Error(String(err));
67
+ const cause = lastError.cause;
68
+ const isTransient = cause?.code === 'ECONNRESET' ||
69
+ cause?.code === 'ECONNREFUSED' ||
70
+ cause?.code === 'UND_ERR_SOCKET' ||
71
+ lastError.message.includes('fetch failed');
72
+ if (isTransient && attempt < retries - 1) {
73
+ const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
74
+ console.warn(` Retry ${attempt + 1}/${retries - 1}: ${cause?.code || lastError.message} — waiting ${delay}ms...`);
75
+ await new Promise(resolve => setTimeout(resolve, delay));
76
+ continue;
77
+ }
78
+ throw lastError;
79
+ }
80
+ }
81
+ throw lastError;
82
+ }
83
+ /**
84
+ * Fetch all rows from a paginated GraphQL connection, handling cursor-based pagination.
85
+ * Returns all nodes across all pages.
86
+ */
87
+ async fetchAllNodes(queryFieldName, fieldsFragment, condition, pageSize = 100, orderBy = 'ID_ASC') {
88
+ const allNodes = [];
89
+ let hasNextPage = true;
90
+ let afterCursor = null;
91
+ while (hasNextPage) {
92
+ const args = this.buildConnectionArgs(condition, pageSize, afterCursor, orderBy);
93
+ const queryString = `{
94
+ ${queryFieldName}${args} {
95
+ nodes {
96
+ ${fieldsFragment}
97
+ }
98
+ pageInfo {
99
+ hasNextPage
100
+ endCursor
101
+ }
102
+ }
103
+ }`;
104
+ const data = await this.query(queryString);
105
+ const connection = data[queryFieldName];
106
+ if (!connection?.nodes) {
107
+ break;
108
+ }
109
+ allNodes.push(...connection.nodes);
110
+ hasNextPage = connection.pageInfo?.hasNextPage ?? false;
111
+ afterCursor = connection.pageInfo?.endCursor ?? null;
112
+ }
113
+ return allNodes;
114
+ }
115
+ buildConnectionArgs(condition, first, after, orderBy) {
116
+ const parts = [];
117
+ if (first) {
118
+ parts.push(`first: ${first}`);
119
+ }
120
+ if (after) {
121
+ parts.push(`after: "${after}"`);
122
+ }
123
+ if (condition && Object.keys(condition).length > 0) {
124
+ const filterParts = Object.entries(condition)
125
+ .map(([k, v]) => {
126
+ if (typeof v === 'string')
127
+ return `${k}: { equalTo: "${v}" }`;
128
+ if (typeof v === 'boolean')
129
+ return `${k}: { equalTo: ${v} }`;
130
+ if (typeof v === 'number')
131
+ return `${k}: { equalTo: ${v} }`;
132
+ return `${k}: { equalTo: "${v}" }`;
133
+ })
134
+ .join(', ');
135
+ parts.push(`where: { ${filterParts} }`);
136
+ }
137
+ if (orderBy) {
138
+ parts.push(`orderBy: ${orderBy}`);
139
+ }
140
+ return parts.length > 0 ? `(${parts.join(', ')})` : '';
141
+ }
142
+ }
143
+ exports.GraphQLClient = GraphQLClient;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Get the GraphQL query field name for a given Postgres table name.
3
+ * Mirrors the PostGraphile InflektPlugin's allRowsConnection inflector:
4
+ * camelize(distinctPluralize(singularizeLast(camelize(pgTableName))), true)
5
+ */
6
+ export declare const getGraphQLQueryName: (pgTableName: string) => string;
7
+ /**
8
+ * Convert a row of GraphQL camelCase keys back to Postgres snake_case keys.
9
+ * This is needed because the csv-to-pg Parser expects snake_case column names.
10
+ * Only transforms top-level keys — nested objects (e.g. JSONB values) are left intact.
11
+ */
12
+ export declare const graphqlRowToPostgresRow: (row: Record<string, unknown>) => Record<string, unknown>;
13
+ /**
14
+ * Convert a PostgreSQL interval object (from GraphQL Interval type) back to a Postgres interval string.
15
+ * e.g. { years: 0, months: 0, days: 0, hours: 1, minutes: 30, seconds: 0 } -> '1 hour 30 minutes'
16
+ */
17
+ export declare const intervalToPostgres: (interval: Record<string, number | null> | null) => string | null;
18
+ /**
19
+ * Convert an array of Postgres field names (with optional type hints) to a GraphQL fields fragment.
20
+ * Handles composite types like 'interval' by expanding them into subfield selections.
21
+ * e.g. [['id', 'uuid'], ['sessions_default_expiration', 'interval']] ->
22
+ * 'id\nsessionsDefaultExpiration { seconds minutes hours days months years }'
23
+ */
24
+ export declare const buildFieldsFragment: (pgFieldNames: string[], fieldTypes?: Record<string, string>) => string;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFieldsFragment = exports.intervalToPostgres = exports.graphqlRowToPostgresRow = exports.getGraphQLQueryName = void 0;
4
+ /**
5
+ * Helpers for mapping between PostgreSQL names and PostGraphile GraphQL names.
6
+ *
7
+ * PostGraphile's inflection (with InflektPreset) transforms:
8
+ * - Table names: snake_case -> camelCase (pluralized for collections)
9
+ * - Column names: snake_case -> camelCase
10
+ * - Schema prefix is stripped (tables are exposed without schema prefix)
11
+ *
12
+ * Examples:
13
+ * metaschema_public.database -> databases (query), Database (type)
14
+ * metaschema_public.foreign_key_constraint -> foreignKeyConstraints
15
+ * services_public.api_schemas -> apiSchemas
16
+ * db_migrate.sql_actions -> sqlActions
17
+ * column database_id -> databaseId
18
+ */
19
+ const inflekt_1 = require("inflekt");
20
+ /**
21
+ * Get the GraphQL query field name for a given Postgres table name.
22
+ * Mirrors the PostGraphile InflektPlugin's allRowsConnection inflector:
23
+ * camelize(distinctPluralize(singularizeLast(camelize(pgTableName))), true)
24
+ */
25
+ const getGraphQLQueryName = (pgTableName) => {
26
+ const pascal = (0, inflekt_1.camelize)(pgTableName);
27
+ const singularized = (0, inflekt_1.singularizeLast)(pascal);
28
+ return (0, inflekt_1.camelize)((0, inflekt_1.distinctPluralize)(singularized), true);
29
+ };
30
+ exports.getGraphQLQueryName = getGraphQLQueryName;
31
+ /**
32
+ * Convert a row of GraphQL camelCase keys back to Postgres snake_case keys.
33
+ * This is needed because the csv-to-pg Parser expects snake_case column names.
34
+ * Only transforms top-level keys — nested objects (e.g. JSONB values) are left intact.
35
+ */
36
+ const graphqlRowToPostgresRow = (row) => {
37
+ const result = {};
38
+ for (const [key, value] of Object.entries(row)) {
39
+ result[(0, inflekt_1.underscore)(key)] = value;
40
+ }
41
+ return result;
42
+ };
43
+ exports.graphqlRowToPostgresRow = graphqlRowToPostgresRow;
44
+ /**
45
+ * Convert a PostgreSQL interval object (from GraphQL Interval type) back to a Postgres interval string.
46
+ * e.g. { years: 0, months: 0, days: 0, hours: 1, minutes: 30, seconds: 0 } -> '1 hour 30 minutes'
47
+ */
48
+ const intervalToPostgres = (interval) => {
49
+ if (!interval)
50
+ return null;
51
+ const parts = [];
52
+ if (interval.years)
53
+ parts.push(`${interval.years} year${interval.years !== 1 ? 's' : ''}`);
54
+ if (interval.months)
55
+ parts.push(`${interval.months} mon${interval.months !== 1 ? 's' : ''}`);
56
+ if (interval.days)
57
+ parts.push(`${interval.days} day${interval.days !== 1 ? 's' : ''}`);
58
+ if (interval.hours)
59
+ parts.push(`${interval.hours}:${String(interval.minutes ?? 0).padStart(2, '0')}:${String(interval.seconds ?? 0).padStart(2, '0')}`);
60
+ else if (interval.minutes)
61
+ parts.push(`00:${String(interval.minutes).padStart(2, '0')}:${String(interval.seconds ?? 0).padStart(2, '0')}`);
62
+ else if (interval.seconds)
63
+ parts.push(`00:00:${String(interval.seconds).padStart(2, '0')}`);
64
+ return parts.length > 0 ? parts.join(' ') : '00:00:00';
65
+ };
66
+ exports.intervalToPostgres = intervalToPostgres;
67
+ /**
68
+ * Convert an array of Postgres field names (with optional type hints) to a GraphQL fields fragment.
69
+ * Handles composite types like 'interval' by expanding them into subfield selections.
70
+ * e.g. [['id', 'uuid'], ['sessions_default_expiration', 'interval']] ->
71
+ * 'id\nsessionsDefaultExpiration { seconds minutes hours days months years }'
72
+ */
73
+ const buildFieldsFragment = (pgFieldNames, fieldTypes) => {
74
+ return pgFieldNames.map(name => {
75
+ const camel = (0, inflekt_1.camelize)(name, true);
76
+ const fieldType = fieldTypes?.[name];
77
+ if (fieldType === 'interval') {
78
+ return `${camel} { seconds minutes hours days months years }`;
79
+ }
80
+ return camel;
81
+ }).join('\n ');
82
+ };
83
+ exports.buildFieldsFragment = buildFieldsFragment;
package/index.d.ts ADDED
@@ -0,0 +1,8 @@
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';
8
+ export type { FieldType, TableConfig, Schema, MakeReplacerOptions, ReplacerResult, PreparePackageOptions, MissingModulesResult } from './export-utils';
package/index.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.installMissingModules = exports.detectMissingModules = exports.normalizeOutdir = exports.preparePackage = exports.makeReplacer = exports.META_TABLE_CONFIG = exports.META_TABLE_ORDER = exports.META_COMMON_FOOTER = exports.META_COMMON_HEADER = exports.SERVICE_REQUIRED_EXTENSIONS = exports.DB_REQUIRED_EXTENSIONS = exports.intervalToPostgres = exports.buildFieldsFragment = exports.graphqlRowToPostgresRow = exports.getGraphQLQueryName = exports.GraphQLClient = void 0;
18
+ __exportStar(require("./export-meta"), exports);
19
+ __exportStar(require("./export-migrations"), exports);
20
+ __exportStar(require("./export-graphql"), exports);
21
+ __exportStar(require("./export-graphql-meta"), exports);
22
+ var graphql_client_1 = require("./graphql-client");
23
+ Object.defineProperty(exports, "GraphQLClient", { enumerable: true, get: function () { return graphql_client_1.GraphQLClient; } });
24
+ var graphql_naming_1 = require("./graphql-naming");
25
+ Object.defineProperty(exports, "getGraphQLQueryName", { enumerable: true, get: function () { return graphql_naming_1.getGraphQLQueryName; } });
26
+ Object.defineProperty(exports, "graphqlRowToPostgresRow", { enumerable: true, get: function () { return graphql_naming_1.graphqlRowToPostgresRow; } });
27
+ Object.defineProperty(exports, "buildFieldsFragment", { enumerable: true, get: function () { return graphql_naming_1.buildFieldsFragment; } });
28
+ Object.defineProperty(exports, "intervalToPostgres", { enumerable: true, get: function () { return graphql_naming_1.intervalToPostgres; } });
29
+ var export_utils_1 = require("./export-utils");
30
+ Object.defineProperty(exports, "DB_REQUIRED_EXTENSIONS", { enumerable: true, get: function () { return export_utils_1.DB_REQUIRED_EXTENSIONS; } });
31
+ Object.defineProperty(exports, "SERVICE_REQUIRED_EXTENSIONS", { enumerable: true, get: function () { return export_utils_1.SERVICE_REQUIRED_EXTENSIONS; } });
32
+ Object.defineProperty(exports, "META_COMMON_HEADER", { enumerable: true, get: function () { return export_utils_1.META_COMMON_HEADER; } });
33
+ Object.defineProperty(exports, "META_COMMON_FOOTER", { enumerable: true, get: function () { return export_utils_1.META_COMMON_FOOTER; } });
34
+ Object.defineProperty(exports, "META_TABLE_ORDER", { enumerable: true, get: function () { return export_utils_1.META_TABLE_ORDER; } });
35
+ Object.defineProperty(exports, "META_TABLE_CONFIG", { enumerable: true, get: function () { return export_utils_1.META_TABLE_CONFIG; } });
36
+ Object.defineProperty(exports, "makeReplacer", { enumerable: true, get: function () { return export_utils_1.makeReplacer; } });
37
+ Object.defineProperty(exports, "preparePackage", { enumerable: true, get: function () { return export_utils_1.preparePackage; } });
38
+ Object.defineProperty(exports, "normalizeOutdir", { enumerable: true, get: function () { return export_utils_1.normalizeOutdir; } });
39
+ Object.defineProperty(exports, "detectMissingModules", { enumerable: true, get: function () { return export_utils_1.detectMissingModules; } });
40
+ Object.defineProperty(exports, "installMissingModules", { enumerable: true, get: function () { return export_utils_1.installMissingModules; } });
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@pgpmjs/export",
3
+ "version": "0.1.0",
4
+ "author": "Constructive <developers@constructive.io>",
5
+ "description": "PGPM export tools for SQL and GraphQL database migration extraction",
6
+ "main": "index.js",
7
+ "module": "esm/index.js",
8
+ "types": "index.d.ts",
9
+ "homepage": "https://github.com/constructive-io/constructive",
10
+ "license": "MIT",
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "directory": "dist"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/constructive-io/constructive"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/constructive-io/constructive/issues"
21
+ },
22
+ "scripts": {
23
+ "clean": "makage clean",
24
+ "prepack": "npm run build",
25
+ "build": "makage build",
26
+ "build:dev": "makage build --dev",
27
+ "lint": "eslint . --fix",
28
+ "test": "jest",
29
+ "test:watch": "jest --watch"
30
+ },
31
+ "keywords": [
32
+ "database",
33
+ "migration",
34
+ "export",
35
+ "postgresql",
36
+ "pgpm",
37
+ "pgpmjs",
38
+ "graphql"
39
+ ],
40
+ "devDependencies": {
41
+ "@pgsql/types": "^17.6.2",
42
+ "@types/pg": "^8.18.0",
43
+ "makage": "^0.1.12",
44
+ "pgsql-test": "^4.6.0"
45
+ },
46
+ "dependencies": {
47
+ "@pgpmjs/core": "^6.9.0",
48
+ "@pgpmjs/migrate-client": "^0.2.0",
49
+ "@pgpmjs/types": "^2.19.3",
50
+ "csv-to-pg": "^3.10.5",
51
+ "glob": "^13.0.6",
52
+ "inflekt": "^0.3.3",
53
+ "inquirerer": "^4.7.0",
54
+ "komoji": "^0.8.1",
55
+ "pg": "^8.20.0",
56
+ "pg-cache": "^3.3.4",
57
+ "pg-env": "^1.7.3"
58
+ },
59
+ "gitHead": "f42b4df50a1cdbcb03b3c52200d953b5e7b24507"
60
+ }