@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 ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
4
+ Copyright (c) 2025 Constructive <developers@constructive.io>
5
+ Copyright (c) 2020-present, Interweb, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @pgpmjs/export
2
+
3
+ <p align="center" width="100%">
4
+ <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
5
+ </p>
6
+
7
+ <p align="center" width="100%">
8
+ <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
+ <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10
+ </a>
11
+ <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
+ <a href="https://www.npmjs.com/package/@pgpmjs/export"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=pgpm%2Fexport%2Fpackage.json"/></a>
13
+ </p>
14
+
15
+ Export tools for extracting database migrations from existing PostgreSQL databases. Supports both direct SQL queries and GraphQL-based data fetching.
16
+
17
+ ## Features
18
+
19
+ - **SQL Export** — Extract migrations directly from a PostgreSQL database via `pg` queries
20
+ - **GraphQL Export** — Extract migrations via a PostGraphile GraphQL endpoint
21
+ - **Cross-flow parity** — Both flows produce identical output for the same source data
22
+ - **Metadata export** — Export metaschema, services, and module metadata tables
23
+
24
+ ## Usage
25
+
26
+ ```typescript
27
+ import { exportMigrations, exportGraphQL, GraphQLClient } from '@pgpmjs/export';
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Education and Tutorials
33
+
34
+ 1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
35
+ Get started with modular databases in minutes. Install prerequisites and deploy your first module.
36
+
37
+ 2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
38
+ Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
39
+
40
+ 3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
41
+ Master the workflow for adding, organizing, and managing database changes with pgpm.
42
+
43
+ 4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
44
+ Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
45
+
46
+ 5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
47
+ Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
48
+
49
+ 6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
50
+ Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
51
+
52
+ 7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
53
+ Common issues and solutions for pgpm, PostgreSQL, and testing.
54
+
55
+ ## Related Constructive Tooling
56
+
57
+ ### 📦 Package Management
58
+
59
+ * [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
60
+
61
+ ### 🧪 Testing
62
+
63
+ * [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
64
+ * [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
65
+ * [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
66
+ * [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
67
+ * [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
68
+
69
+ ### 🧠 Parsing & AST
70
+
71
+ * [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
72
+ * [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
73
+ * [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
74
+ * [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
75
+ * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
76
+ * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
77
+
78
+ ## Credits
79
+
80
+ **🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
81
+
82
+ ## Disclaimer
83
+
84
+ AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
85
+
86
+ No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
@@ -0,0 +1,151 @@
1
+ /**
2
+ * GraphQL equivalent of export-meta.ts.
3
+ *
4
+ * Fetches metadata from metaschema_public, services_public, and metaschema_modules_public
5
+ * via GraphQL queries instead of direct SQL, then uses the same csv-to-pg Parser to
6
+ * generate SQL INSERT statements.
7
+ */
8
+ import { Parser } from 'csv-to-pg';
9
+ import { META_TABLE_CONFIG } from './export-utils';
10
+ import { buildFieldsFragment, getGraphQLQueryName, graphqlRowToPostgresRow, intervalToPostgres } from './graphql-naming';
11
+ /**
12
+ * Fetch metadata via GraphQL and generate SQL INSERT statements.
13
+ * This is the GraphQL equivalent of exportMeta() in export-meta.ts.
14
+ */
15
+ export const exportGraphQLMeta = async ({ client, database_id }) => {
16
+ const sql = {};
17
+ const queryAndParse = async (key) => {
18
+ const tableConfig = META_TABLE_CONFIG[key];
19
+ if (!tableConfig)
20
+ return;
21
+ const pgFieldNames = Object.keys(tableConfig.fields);
22
+ const graphqlFieldsFragment = buildFieldsFragment(pgFieldNames, tableConfig.fields);
23
+ const graphqlQueryName = getGraphQLQueryName(tableConfig.table);
24
+ // The 'database' table is fetched by id, not by database_id
25
+ const condition = key === 'database'
26
+ ? { id: database_id }
27
+ : { databaseId: database_id };
28
+ try {
29
+ const rows = await client.fetchAllNodes(graphqlQueryName, graphqlFieldsFragment, condition);
30
+ if (rows.length > 0) {
31
+ // Convert camelCase GraphQL keys back to snake_case for the Parser
32
+ // Also convert interval objects back to Postgres interval strings
33
+ const pgRows = rows.map(row => {
34
+ const pgRow = graphqlRowToPostgresRow(row);
35
+ // Convert any interval fields from {seconds, minutes, ...} objects to strings
36
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
37
+ if (fieldType === 'interval' && pgRow[fieldName] && typeof pgRow[fieldName] === 'object') {
38
+ pgRow[fieldName] = intervalToPostgres(pgRow[fieldName]);
39
+ }
40
+ }
41
+ return pgRow;
42
+ });
43
+ // Filter fields to only those that exist in the returned data
44
+ // This mirrors the dynamic field building in the SQL version
45
+ const returnedKeys = new Set();
46
+ for (const row of pgRows) {
47
+ for (const k of Object.keys(row)) {
48
+ returnedKeys.add(k);
49
+ }
50
+ }
51
+ const dynamicFields = {};
52
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
53
+ if (returnedKeys.has(fieldName)) {
54
+ dynamicFields[fieldName] = fieldType;
55
+ }
56
+ }
57
+ if (Object.keys(dynamicFields).length === 0)
58
+ return;
59
+ const parser = new Parser({
60
+ schema: tableConfig.schema,
61
+ table: tableConfig.table,
62
+ conflictDoNothing: tableConfig.conflictDoNothing,
63
+ fields: dynamicFields
64
+ });
65
+ const parsed = await parser.parse(pgRows);
66
+ if (parsed) {
67
+ sql[key] = parsed;
68
+ }
69
+ }
70
+ }
71
+ catch (err) {
72
+ // If the GraphQL query fails (e.g. table not exposed), skip silently
73
+ // similar to how the SQL version handles 42P01 (undefined_table)
74
+ const message = err instanceof Error ? err.message : String(err);
75
+ if (message.includes('Cannot query field') ||
76
+ message.includes('is not defined by type') ||
77
+ message.includes('Unknown field') ||
78
+ (message.includes('Field') && message.includes('not found'))) {
79
+ // Field/table not available in the GraphQL schema — skip
80
+ return;
81
+ }
82
+ throw err;
83
+ }
84
+ };
85
+ // Batch queries by schema group — independent HTTP requests run in parallel
86
+ // within each group for significant speedup over sequential awaits.
87
+ // metaschema_public tables
88
+ await Promise.all([
89
+ queryAndParse('database'),
90
+ queryAndParse('database_extension'),
91
+ queryAndParse('schema'),
92
+ queryAndParse('table'),
93
+ queryAndParse('field'),
94
+ queryAndParse('policy'),
95
+ queryAndParse('index'),
96
+ queryAndParse('trigger'),
97
+ queryAndParse('trigger_function'),
98
+ queryAndParse('rls_function'),
99
+ queryAndParse('foreign_key_constraint'),
100
+ queryAndParse('primary_key_constraint'),
101
+ queryAndParse('unique_constraint'),
102
+ queryAndParse('check_constraint'),
103
+ queryAndParse('full_text_search'),
104
+ queryAndParse('schema_grant'),
105
+ queryAndParse('table_grant'),
106
+ queryAndParse('default_privilege')
107
+ ]);
108
+ // services_public tables
109
+ await Promise.all([
110
+ queryAndParse('domains'),
111
+ queryAndParse('sites'),
112
+ queryAndParse('apis'),
113
+ queryAndParse('apps'),
114
+ queryAndParse('site_modules'),
115
+ queryAndParse('site_themes'),
116
+ queryAndParse('site_metadata'),
117
+ queryAndParse('api_modules'),
118
+ queryAndParse('api_extensions'),
119
+ queryAndParse('api_schemas')
120
+ ]);
121
+ // metaschema_modules_public tables
122
+ await Promise.all([
123
+ queryAndParse('rls_module'),
124
+ queryAndParse('user_auth_module'),
125
+ queryAndParse('memberships_module'),
126
+ queryAndParse('permissions_module'),
127
+ queryAndParse('limits_module'),
128
+ queryAndParse('levels_module'),
129
+ queryAndParse('users_module'),
130
+ queryAndParse('hierarchy_module'),
131
+ queryAndParse('membership_types_module'),
132
+ queryAndParse('invites_module'),
133
+ queryAndParse('emails_module'),
134
+ queryAndParse('sessions_module'),
135
+ queryAndParse('secrets_module'),
136
+ queryAndParse('profiles_module'),
137
+ queryAndParse('encrypted_secrets_module'),
138
+ queryAndParse('connected_accounts_module'),
139
+ queryAndParse('phone_numbers_module'),
140
+ queryAndParse('crypto_addresses_module'),
141
+ queryAndParse('crypto_auth_module'),
142
+ queryAndParse('field_module'),
143
+ queryAndParse('table_module'),
144
+ queryAndParse('table_template_module'),
145
+ queryAndParse('secure_table_provision'),
146
+ queryAndParse('uuid_module'),
147
+ queryAndParse('default_ids_module'),
148
+ queryAndParse('denormalized_table_field')
149
+ ]);
150
+ return sql;
151
+ };
@@ -0,0 +1,176 @@
1
+ import { writePgpmFiles, writePgpmPlan } from '@pgpmjs/core';
2
+ import { createClient } from '@pgpmjs/migrate-client';
3
+ import { GraphQLClient } from './graphql-client';
4
+ import { exportGraphQLMeta } from './export-graphql-meta';
5
+ import { graphqlRowToPostgresRow } from './graphql-naming';
6
+ import { DB_REQUIRED_EXTENSIONS, SERVICE_REQUIRED_EXTENSIONS, META_COMMON_HEADER, META_COMMON_FOOTER, META_TABLE_ORDER, detectMissingModules, installMissingModules, makeReplacer, preparePackage, normalizeOutdir } from './export-utils';
7
+ export 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 }) => {
8
+ const normalizedOutdir = normalizeOutdir(outdir);
9
+ const svcOutdir = normalizeOutdir(serviceOutdir || outdir);
10
+ const name = extensionName;
11
+ const schemasForReplacement = skipSchemaRenaming
12
+ ? []
13
+ : schemas.filter((schema) => schema_names.includes(schema.schema_name));
14
+ const { replacer } = makeReplacer({
15
+ schemas: schemasForReplacement,
16
+ name
17
+ });
18
+ // =========================================================================
19
+ // 1. Fetch sql_actions via @pgpmjs/migrate-client ORM (db_migrate endpoint)
20
+ // =========================================================================
21
+ let sqlActionRows = [];
22
+ if (migrateEndpoint) {
23
+ console.log(`Fetching sql_actions from ${migrateEndpoint}...`);
24
+ const db = createClient({
25
+ endpoint: migrateEndpoint,
26
+ headers: {
27
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
28
+ ...migrateHeaders
29
+ }
30
+ });
31
+ try {
32
+ // Paginate through all sql_actions for this database using the ORM.
33
+ // The ORM generates `where: SqlActionFilter` which uses the filter plugin's
34
+ // `equalTo` operator — the correct approach for Constructive's PostGraphile APIs.
35
+ let hasNextPage = true;
36
+ let afterCursor;
37
+ const PAGE_SIZE = 100;
38
+ while (hasNextPage) {
39
+ const result = await db.sqlAction.findMany({
40
+ select: {
41
+ id: true,
42
+ databaseId: true,
43
+ name: true,
44
+ deploy: true,
45
+ revert: true,
46
+ verify: true,
47
+ content: true,
48
+ deps: true,
49
+ action: true,
50
+ actionId: true,
51
+ actorId: true,
52
+ payload: true
53
+ },
54
+ where: {
55
+ databaseId: { equalTo: databaseId }
56
+ },
57
+ orderBy: ['ID_ASC'],
58
+ first: PAGE_SIZE,
59
+ ...(afterCursor ? { after: afterCursor } : {})
60
+ }).unwrap();
61
+ const connection = result.sqlActions;
62
+ for (const node of connection.nodes) {
63
+ sqlActionRows.push(graphqlRowToPostgresRow(node));
64
+ }
65
+ hasNextPage = connection.pageInfo?.hasNextPage ?? false;
66
+ afterCursor = connection.pageInfo?.endCursor ?? undefined;
67
+ }
68
+ console.log(` Found ${sqlActionRows.length} sql_actions`);
69
+ }
70
+ catch (err) {
71
+ console.warn(` Warning: Could not fetch sql_actions: ${err instanceof Error ? err.message : err}`);
72
+ }
73
+ }
74
+ else {
75
+ console.log('No migrate endpoint provided, skipping sql_actions export.');
76
+ }
77
+ const opts = {
78
+ name,
79
+ replacer,
80
+ outdir: normalizedOutdir,
81
+ author
82
+ };
83
+ const dbExtensionDesc = extensionDesc || `${name} database schema for ${databaseName}`;
84
+ if (sqlActionRows.length > 0) {
85
+ const dbMissingResult = await detectMissingModules(project, [...DB_REQUIRED_EXTENSIONS], prompter, argv);
86
+ const dbModuleDir = await preparePackage({
87
+ project,
88
+ author,
89
+ outdir: normalizedOutdir,
90
+ name,
91
+ description: dbExtensionDesc,
92
+ extensions: [...DB_REQUIRED_EXTENSIONS],
93
+ prompter,
94
+ repoName,
95
+ username
96
+ });
97
+ if (dbMissingResult.shouldInstall) {
98
+ await installMissingModules(dbModuleDir, dbMissingResult.missingModules);
99
+ }
100
+ writePgpmPlan(sqlActionRows, opts);
101
+ writePgpmFiles(sqlActionRows, opts);
102
+ }
103
+ else {
104
+ console.log('No sql_actions found. Skipping database module export.');
105
+ }
106
+ // =========================================================================
107
+ // 2. Fetch meta/services data via GraphQL
108
+ // =========================================================================
109
+ console.log(`Fetching metadata from ${metaEndpoint}...`);
110
+ const metaClient = new GraphQLClient({ endpoint: metaEndpoint, token, headers });
111
+ const metaResult = await exportGraphQLMeta({
112
+ client: metaClient,
113
+ database_id: databaseId
114
+ });
115
+ const metaTableCount = Object.keys(metaResult).length;
116
+ console.log(` Fetched ${metaTableCount} meta tables with data`);
117
+ if (metaTableCount > 0) {
118
+ const metaDesc = metaExtensionDesc || `${metaExtensionName} service utilities for managing domains, APIs, and services`;
119
+ const svcMissingResult = await detectMissingModules(project, [...SERVICE_REQUIRED_EXTENSIONS], prompter, argv);
120
+ const svcModuleDir = await preparePackage({
121
+ project,
122
+ author,
123
+ outdir: svcOutdir,
124
+ name: metaExtensionName,
125
+ description: metaDesc,
126
+ extensions: [...SERVICE_REQUIRED_EXTENSIONS],
127
+ prompter,
128
+ repoName,
129
+ username
130
+ });
131
+ if (svcMissingResult.shouldInstall) {
132
+ await installMissingModules(svcModuleDir, svcMissingResult.missingModules);
133
+ }
134
+ const metaSchemasForReplacement = skipSchemaRenaming
135
+ ? []
136
+ : schemas.filter((schema) => schema_names.includes(schema.schema_name));
137
+ const metaReplacer = makeReplacer({
138
+ schemas: metaSchemasForReplacement,
139
+ name: metaExtensionName,
140
+ // Use extensionName for schema prefix — the services metadata references
141
+ // schemas owned by the application package (e.g. agent_db_auth_public),
142
+ // not the services package (agent_db_services_auth_public)
143
+ schemaPrefix: name
144
+ });
145
+ const metaPackage = [];
146
+ const tablesWithContent = [];
147
+ for (const tableName of META_TABLE_ORDER) {
148
+ const tableSql = metaResult[tableName];
149
+ if (tableSql) {
150
+ const replacedSql = metaReplacer.replacer(tableSql);
151
+ const deps = tableName === 'database'
152
+ ? []
153
+ : tablesWithContent.length > 0
154
+ ? [`migrate/${tablesWithContent[tablesWithContent.length - 1]}`]
155
+ : [];
156
+ metaPackage.push({
157
+ deps,
158
+ deploy: `migrate/${tableName}`,
159
+ content: `${META_COMMON_HEADER}
160
+
161
+ ${replacedSql}
162
+
163
+ ${META_COMMON_FOOTER}
164
+ `
165
+ });
166
+ tablesWithContent.push(tableName);
167
+ }
168
+ }
169
+ opts.replacer = metaReplacer.replacer;
170
+ opts.name = metaExtensionName;
171
+ opts.outdir = svcOutdir;
172
+ writePgpmPlan(metaPackage, opts);
173
+ writePgpmFiles(metaPackage, opts);
174
+ }
175
+ console.log('GraphQL export complete.');
176
+ };
@@ -0,0 +1,162 @@
1
+ import { Parser } from 'csv-to-pg';
2
+ import { getPgPool } from 'pg-cache';
3
+ import { META_TABLE_CONFIG } from './export-utils';
4
+ /**
5
+ * Query actual columns from information_schema for a given table.
6
+ * Returns a map of column_name -> udt_name (PostgreSQL type).
7
+ */
8
+ const getTableColumns = async (pool, schemaName, tableName) => {
9
+ const result = await pool.query(`
10
+ SELECT column_name, udt_name
11
+ FROM information_schema.columns
12
+ WHERE table_schema = $1 AND table_name = $2
13
+ ORDER BY ordinal_position
14
+ `, [schemaName, tableName]);
15
+ const columns = new Map();
16
+ for (const row of result.rows) {
17
+ columns.set(row.column_name, row.udt_name);
18
+ }
19
+ return columns;
20
+ };
21
+ /**
22
+ * Build dynamic fields config by intersecting the hardcoded config with actual database columns.
23
+ * - Only includes columns that exist in the database
24
+ * - Preserves special type hints from config (image, upload, url) for columns that exist
25
+ * - Infers types from PostgreSQL for columns not in config
26
+ */
27
+ const buildDynamicFields = async (pool, tableConfig) => {
28
+ const actualColumns = await getTableColumns(pool, tableConfig.schema, tableConfig.table);
29
+ if (actualColumns.size === 0) {
30
+ // Table doesn't exist, return empty fields
31
+ return {};
32
+ }
33
+ const dynamicFields = {};
34
+ // For each column in the hardcoded config, check if it exists in the database
35
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
36
+ if (actualColumns.has(fieldName)) {
37
+ // Column exists - use the config's type hint (preserves special types like 'image', 'upload', 'url')
38
+ dynamicFields[fieldName] = fieldType;
39
+ }
40
+ // If column doesn't exist in database, skip it (this fixes the bug)
41
+ }
42
+ return dynamicFields;
43
+ };
44
+ export const exportMeta = async ({ opts, dbname, database_id }) => {
45
+ const pool = getPgPool({
46
+ ...opts.pg,
47
+ database: dbname
48
+ });
49
+ const sql = {};
50
+ // Cache for dynamically built parsers
51
+ const parsers = {};
52
+ // Build parser dynamically by querying actual columns from the database
53
+ const getParser = async (key) => {
54
+ if (parsers[key]) {
55
+ return parsers[key];
56
+ }
57
+ const tableConfig = META_TABLE_CONFIG[key];
58
+ if (!tableConfig) {
59
+ return null;
60
+ }
61
+ // Build fields dynamically based on actual database columns
62
+ const dynamicFields = await buildDynamicFields(pool, tableConfig);
63
+ if (Object.keys(dynamicFields).length === 0) {
64
+ // No columns found (table doesn't exist or no matching columns)
65
+ return null;
66
+ }
67
+ const parser = new Parser({
68
+ schema: tableConfig.schema,
69
+ table: tableConfig.table,
70
+ conflictDoNothing: tableConfig.conflictDoNothing,
71
+ fields: dynamicFields
72
+ });
73
+ parsers[key] = parser;
74
+ return parser;
75
+ };
76
+ const queryAndParse = async (key, query) => {
77
+ try {
78
+ const parser = await getParser(key);
79
+ if (!parser) {
80
+ return;
81
+ }
82
+ const result = await pool.query(query, [database_id]);
83
+ if (result.rows.length) {
84
+ const parsed = await parser.parse(result.rows);
85
+ if (parsed) {
86
+ sql[key] = parsed;
87
+ }
88
+ }
89
+ }
90
+ catch (err) {
91
+ const pgError = err;
92
+ if (pgError.code === '42P01') {
93
+ return;
94
+ }
95
+ throw err;
96
+ }
97
+ };
98
+ // =============================================================================
99
+ // metaschema_public tables
100
+ // =============================================================================
101
+ await queryAndParse('database', `SELECT * FROM metaschema_public.database WHERE id = $1 ORDER BY id`);
102
+ await queryAndParse('database_extension', `SELECT * FROM metaschema_public.database_extension WHERE database_id = $1 ORDER BY id`);
103
+ await queryAndParse('schema', `SELECT * FROM metaschema_public.schema WHERE database_id = $1 ORDER BY id`);
104
+ await queryAndParse('table', `SELECT * FROM metaschema_public.table WHERE database_id = $1 ORDER BY id`);
105
+ await queryAndParse('field', `SELECT * FROM metaschema_public.field WHERE database_id = $1 ORDER BY id`);
106
+ await queryAndParse('policy', `SELECT * FROM metaschema_public.policy WHERE database_id = $1 ORDER BY id`);
107
+ await queryAndParse('index', `SELECT * FROM metaschema_public.index WHERE database_id = $1 ORDER BY id`);
108
+ await queryAndParse('trigger', `SELECT * FROM metaschema_public.trigger WHERE database_id = $1 ORDER BY id`);
109
+ await queryAndParse('trigger_function', `SELECT * FROM metaschema_public.trigger_function WHERE database_id = $1 ORDER BY id`);
110
+ await queryAndParse('rls_function', `SELECT * FROM metaschema_public.rls_function WHERE database_id = $1 ORDER BY id`);
111
+ await queryAndParse('foreign_key_constraint', `SELECT * FROM metaschema_public.foreign_key_constraint WHERE database_id = $1 ORDER BY id`);
112
+ await queryAndParse('primary_key_constraint', `SELECT * FROM metaschema_public.primary_key_constraint WHERE database_id = $1 ORDER BY id`);
113
+ await queryAndParse('unique_constraint', `SELECT * FROM metaschema_public.unique_constraint WHERE database_id = $1 ORDER BY id`);
114
+ await queryAndParse('check_constraint', `SELECT * FROM metaschema_public.check_constraint WHERE database_id = $1 ORDER BY id`);
115
+ await queryAndParse('full_text_search', `SELECT * FROM metaschema_public.full_text_search WHERE database_id = $1 ORDER BY id`);
116
+ await queryAndParse('schema_grant', `SELECT * FROM metaschema_public.schema_grant WHERE database_id = $1 ORDER BY id`);
117
+ await queryAndParse('table_grant', `SELECT * FROM metaschema_public.table_grant WHERE database_id = $1 ORDER BY id`);
118
+ await queryAndParse('default_privilege', `SELECT * FROM metaschema_public.default_privilege WHERE database_id = $1 ORDER BY id`);
119
+ // =============================================================================
120
+ // services_public tables
121
+ // =============================================================================
122
+ await queryAndParse('domains', `SELECT * FROM services_public.domains WHERE database_id = $1 ORDER BY id`);
123
+ await queryAndParse('sites', `SELECT * FROM services_public.sites WHERE database_id = $1 ORDER BY id`);
124
+ await queryAndParse('apis', `SELECT * FROM services_public.apis WHERE database_id = $1 ORDER BY id`);
125
+ await queryAndParse('apps', `SELECT * FROM services_public.apps WHERE database_id = $1 ORDER BY id`);
126
+ await queryAndParse('site_modules', `SELECT * FROM services_public.site_modules WHERE database_id = $1 ORDER BY id`);
127
+ await queryAndParse('site_themes', `SELECT * FROM services_public.site_themes WHERE database_id = $1 ORDER BY id`);
128
+ await queryAndParse('site_metadata', `SELECT * FROM services_public.site_metadata WHERE database_id = $1 ORDER BY id`);
129
+ await queryAndParse('api_modules', `SELECT * FROM services_public.api_modules WHERE database_id = $1 ORDER BY id`);
130
+ await queryAndParse('api_extensions', `SELECT * FROM services_public.api_extensions WHERE database_id = $1 ORDER BY id`);
131
+ await queryAndParse('api_schemas', `SELECT * FROM services_public.api_schemas WHERE database_id = $1 ORDER BY id`);
132
+ // =============================================================================
133
+ // metaschema_modules_public tables
134
+ // =============================================================================
135
+ await queryAndParse('rls_module', `SELECT * FROM metaschema_modules_public.rls_module WHERE database_id = $1 ORDER BY id`);
136
+ await queryAndParse('user_auth_module', `SELECT * FROM metaschema_modules_public.user_auth_module WHERE database_id = $1 ORDER BY id`);
137
+ await queryAndParse('memberships_module', `SELECT * FROM metaschema_modules_public.memberships_module WHERE database_id = $1 ORDER BY id`);
138
+ await queryAndParse('permissions_module', `SELECT * FROM metaschema_modules_public.permissions_module WHERE database_id = $1 ORDER BY id`);
139
+ await queryAndParse('limits_module', `SELECT * FROM metaschema_modules_public.limits_module WHERE database_id = $1 ORDER BY id`);
140
+ await queryAndParse('levels_module', `SELECT * FROM metaschema_modules_public.levels_module WHERE database_id = $1 ORDER BY id`);
141
+ await queryAndParse('users_module', `SELECT * FROM metaschema_modules_public.users_module WHERE database_id = $1 ORDER BY id`);
142
+ await queryAndParse('hierarchy_module', `SELECT * FROM metaschema_modules_public.hierarchy_module WHERE database_id = $1 ORDER BY id`);
143
+ await queryAndParse('membership_types_module', `SELECT * FROM metaschema_modules_public.membership_types_module WHERE database_id = $1 ORDER BY id`);
144
+ await queryAndParse('invites_module', `SELECT * FROM metaschema_modules_public.invites_module WHERE database_id = $1 ORDER BY id`);
145
+ await queryAndParse('emails_module', `SELECT * FROM metaschema_modules_public.emails_module WHERE database_id = $1 ORDER BY id`);
146
+ await queryAndParse('sessions_module', `SELECT * FROM metaschema_modules_public.sessions_module WHERE database_id = $1 ORDER BY id`);
147
+ await queryAndParse('secrets_module', `SELECT * FROM metaschema_modules_public.secrets_module WHERE database_id = $1 ORDER BY id`);
148
+ await queryAndParse('profiles_module', `SELECT * FROM metaschema_modules_public.profiles_module WHERE database_id = $1 ORDER BY id`);
149
+ await queryAndParse('encrypted_secrets_module', `SELECT * FROM metaschema_modules_public.encrypted_secrets_module WHERE database_id = $1 ORDER BY id`);
150
+ await queryAndParse('connected_accounts_module', `SELECT * FROM metaschema_modules_public.connected_accounts_module WHERE database_id = $1 ORDER BY id`);
151
+ await queryAndParse('phone_numbers_module', `SELECT * FROM metaschema_modules_public.phone_numbers_module WHERE database_id = $1 ORDER BY id`);
152
+ await queryAndParse('crypto_addresses_module', `SELECT * FROM metaschema_modules_public.crypto_addresses_module WHERE database_id = $1 ORDER BY id`);
153
+ await queryAndParse('crypto_auth_module', `SELECT * FROM metaschema_modules_public.crypto_auth_module WHERE database_id = $1 ORDER BY id`);
154
+ await queryAndParse('field_module', `SELECT * FROM metaschema_modules_public.field_module WHERE database_id = $1 ORDER BY id`);
155
+ await queryAndParse('table_module', `SELECT * FROM metaschema_modules_public.table_module WHERE database_id = $1 ORDER BY id`);
156
+ await queryAndParse('table_template_module', `SELECT * FROM metaschema_modules_public.table_template_module WHERE database_id = $1 ORDER BY id`);
157
+ await queryAndParse('secure_table_provision', `SELECT * FROM metaschema_modules_public.secure_table_provision WHERE database_id = $1 ORDER BY id`);
158
+ await queryAndParse('uuid_module', `SELECT * FROM metaschema_modules_public.uuid_module WHERE database_id = $1 ORDER BY id`);
159
+ await queryAndParse('default_ids_module', `SELECT * FROM metaschema_modules_public.default_ids_module WHERE database_id = $1 ORDER BY id`);
160
+ await queryAndParse('denormalized_table_field', `SELECT * FROM metaschema_modules_public.denormalized_table_field WHERE database_id = $1 ORDER BY id`);
161
+ return sql;
162
+ };