@pgpmjs/export 0.20.0 → 0.20.2

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.
@@ -2,6 +2,7 @@
2
2
  * Simple GraphQL HTTP client with pagination and authentication support.
3
3
  * Used by the GraphQL export flow to fetch data from the Constructive GraphQL API.
4
4
  */
5
+ import { unwrapGraphQLType } from './graphql-naming';
5
6
  export class GraphQLClient {
6
7
  endpoint;
7
8
  defaultHeaders;
@@ -136,4 +137,46 @@ export class GraphQLClient {
136
137
  }
137
138
  return parts.length > 0 ? `(${parts.join(', ')})` : '';
138
139
  }
140
+ /**
141
+ * Introspect a GraphQL type to discover its fields and their types.
142
+ * Used by the dynamic export flow to discover what fields are available
143
+ * instead of hardcoding them in META_TABLE_CONFIG.
144
+ *
145
+ * Returns a Map of camelCase field name → { typeName, list, nonNull }.
146
+ */
147
+ async introspectType(typeName) {
148
+ const result = await this.query(`
149
+ query IntrospectType($typeName: String!) {
150
+ __type(name: $typeName) {
151
+ fields {
152
+ name
153
+ type {
154
+ name
155
+ kind
156
+ ofType {
157
+ name
158
+ kind
159
+ ofType {
160
+ name
161
+ kind
162
+ ofType {
163
+ name
164
+ kind
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ `, { typeName });
173
+ const fields = new Map();
174
+ if (result.__type?.fields) {
175
+ for (const field of result.__type.fields) {
176
+ const typeInfo = unwrapGraphQLType(field.type);
177
+ fields.set(field.name, typeInfo);
178
+ }
179
+ }
180
+ return fields;
181
+ }
139
182
  }
@@ -14,6 +14,7 @@
14
14
  * column database_id -> databaseId
15
15
  */
16
16
  import { toCamelCase, toPascalCase, toSnakeCase, distinctPluralize, singularizeLast } from 'inflekt';
17
+ import { lookupByGqlType } from './type-map';
17
18
  /**
18
19
  * Get the GraphQL query field name for a given Postgres table name.
19
20
  * Mirrors the PostGraphile InflektPlugin's allRowsConnection inflector:
@@ -36,28 +37,7 @@ export const graphqlRowToPostgresRow = (row) => {
36
37
  }
37
38
  return result;
38
39
  };
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
- };
40
+ export { intervalToPostgres } from './interval-utils';
61
41
  /**
62
42
  * Convert an array of Postgres field names (with optional type hints) to a GraphQL fields fragment.
63
43
  * Handles composite types like 'interval' by expanding them into subfield selections.
@@ -74,3 +54,54 @@ export const buildFieldsFragment = (pgFieldNames, fieldTypes) => {
74
54
  return camel;
75
55
  }).join('\n ');
76
56
  };
57
+ /**
58
+ * Unwrap a GraphQL introspection type reference into its leaf type name and list status.
59
+ * PostGraphile wraps types like: { kind: NON_NULL, name: null, ofType: { kind: LIST, name: null, ofType: { kind: SCALAR, name: "UUID" } } }
60
+ * This function recursively unwraps ofType layers, detecting LIST wrappers via the `kind` field.
61
+ */
62
+ export const unwrapGraphQLType = (typeRef, parentKind) => {
63
+ if (!typeRef)
64
+ return { typeName: 'Unknown', kind: 'UNKNOWN', nonNull: false, list: false };
65
+ // If the type has a name, it's the leaf type
66
+ if (typeRef.name) {
67
+ const isList = parentKind === 'LIST';
68
+ return { typeName: typeRef.name, kind: typeRef.kind ?? 'UNKNOWN', nonNull: parentKind === 'NON_NULL', list: isList };
69
+ }
70
+ // If it has ofType, it's a wrapper (NON_NULL or LIST)
71
+ if (typeRef.ofType) {
72
+ return unwrapGraphQLType(typeRef.ofType, typeRef.kind ?? undefined);
73
+ }
74
+ return { typeName: 'Unknown', kind: 'UNKNOWN', nonNull: false, list: false };
75
+ };
76
+ /**
77
+ * Map GraphQL scalar/type names to FieldType values.
78
+ * Delegates to the canonical PG_TYPE_MAP in type-map.ts.
79
+ */
80
+ export const mapGraphQLTypeToFieldType = (gqlTypeName, isList = false) => {
81
+ // Handle list types — map to the array variants that exist in FieldType
82
+ if (isList) {
83
+ const inner = mapGraphQLTypeToFieldType(gqlTypeName, false);
84
+ // Only these array types exist in FieldType: uuid[], text[], jsonb[]
85
+ switch (inner) {
86
+ case 'uuid': return 'uuid[]';
87
+ case 'text': return 'text[]';
88
+ case 'jsonb': return 'jsonb[]';
89
+ default: return 'text'; // safe fallback for unsupported array types
90
+ }
91
+ }
92
+ // ID is a GraphQL-only type (relay-style) that maps to uuid;
93
+ // it has no direct PG udt_name counterpart in PG_TYPE_MAP.
94
+ if (gqlTypeName === 'ID')
95
+ return 'uuid';
96
+ const entry = lookupByGqlType(gqlTypeName);
97
+ return entry?.fieldType ?? 'text';
98
+ };
99
+ /**
100
+ * Derive the GraphQL type name (PascalCase singular) from a PostgreSQL table name.
101
+ * Mirrors PostGraphile's InflektPlugin type inflector:
102
+ * singularizeLast(toPascalCase(pgTableName))
103
+ * e.g. "user_auth_module" → "UserAuthModule"
104
+ */
105
+ export const getGraphQLTypeName = (pgTableName) => {
106
+ return singularizeLast(toPascalCase(pgTableName));
107
+ };
package/esm/index.js CHANGED
@@ -3,5 +3,7 @@ export * from './export-migrations';
3
3
  export * from './export-graphql';
4
4
  export * from './export-graphql-meta';
5
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';
6
+ export { getGraphQLQueryName, getGraphQLTypeName, graphqlRowToPostgresRow, buildFieldsFragment, mapGraphQLTypeToFieldType, unwrapGraphQLType } from './graphql-naming';
7
+ export { DB_REQUIRED_EXTENSIONS, SERVICE_REQUIRED_EXTENSIONS, META_COMMON_HEADER, META_COMMON_FOOTER, META_TABLE_ORDER, META_TABLE_CONFIG, mapPgTypeToFieldType, makeReplacer, preparePackage, normalizeOutdir, detectMissingModules, installMissingModules } from './export-utils';
8
+ export { PG_TYPE_MAP, lookupByPgUdt, lookupByGqlType } from './type-map';
9
+ export { intervalToPostgres, parsePgInterval } from './interval-utils';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Interval conversion utilities shared between graphql-naming.ts and tests.
3
+ *
4
+ * NOTE: @pgpmjs/csv-to-pg has its own formatInterval() in parse.ts which
5
+ * performs the same object→string conversion. That is a separate package with
6
+ * its own release cycle, so we do not cross-reference it here. If interval
7
+ * handling needs to be unified across packages, that would require an
8
+ * architectural change (shared dependency or monorepo util package).
9
+ */
10
+ /**
11
+ * Convert a PostgreSQL interval object (from GraphQL Interval type) back to a
12
+ * Postgres interval string.
13
+ * e.g. { years: 0, months: 0, days: 0, hours: 1, minutes: 30, seconds: 0 } → '1:30:00'
14
+ */
15
+ export const intervalToPostgres = (interval) => {
16
+ if (!interval)
17
+ return null;
18
+ const parts = [];
19
+ if (interval.years)
20
+ parts.push(`${interval.years} year${interval.years !== 1 ? 's' : ''}`);
21
+ if (interval.months)
22
+ parts.push(`${interval.months} mon${interval.months !== 1 ? 's' : ''}`);
23
+ if (interval.days)
24
+ parts.push(`${interval.days} day${interval.days !== 1 ? 's' : ''}`);
25
+ if (interval.hours)
26
+ parts.push(`${interval.hours}:${String(interval.minutes ?? 0).padStart(2, '0')}:${String(interval.seconds ?? 0).padStart(2, '0')}`);
27
+ else if (interval.minutes)
28
+ parts.push(`00:${String(interval.minutes).padStart(2, '0')}:${String(interval.seconds ?? 0).padStart(2, '0')}`);
29
+ else if (interval.seconds)
30
+ parts.push(`00:00:${String(interval.seconds).padStart(2, '0')}`);
31
+ return parts.length > 0 ? parts.join(' ') : '00:00:00';
32
+ };
33
+ /**
34
+ * Parse a PostgreSQL interval string into the object shape that PostGraphile's
35
+ * Interval type returns: { years, months, days, hours, minutes, seconds }.
36
+ *
37
+ * Handles formats like:
38
+ * '30 days' → { years: 0, months: 0, days: 30, hours: 0, minutes: 0, seconds: 0 }
39
+ * '1:30:00' → { years: 0, months: 0, days: 0, hours: 1, minutes: 30, seconds: 0 }
40
+ */
41
+ export const parsePgInterval = (value) => {
42
+ const result = { years: 0, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0 };
43
+ // Try HH:MM:SS format
44
+ const timeMatch = value.match(/^(\d+):(\d+):(\d+)/);
45
+ if (timeMatch) {
46
+ result.hours = parseInt(timeMatch[1], 10);
47
+ result.minutes = parseInt(timeMatch[2], 10);
48
+ result.seconds = parseInt(timeMatch[3], 10);
49
+ return result;
50
+ }
51
+ // Try descriptive format: 'N unit N unit ...'
52
+ const parts = value.trim().split(/\s+/);
53
+ for (let i = 0; i < parts.length - 1; i += 2) {
54
+ const num = parseInt(parts[i], 10);
55
+ const unit = parts[i + 1].toLowerCase();
56
+ if (unit.startsWith('year'))
57
+ result.years = num;
58
+ else if (unit.startsWith('mon'))
59
+ result.months = num;
60
+ else if (unit.startsWith('day'))
61
+ result.days = num;
62
+ else if (unit.startsWith('hour'))
63
+ result.hours = num;
64
+ else if (unit.startsWith('minute'))
65
+ result.minutes = num;
66
+ else if (unit.startsWith('second'))
67
+ result.seconds = num;
68
+ }
69
+ return result;
70
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Canonical PG → GraphQL → FieldType mapping table.
3
+ * Aligned with PostGraphile v5's PgCodecsPlugin type assignments:
4
+ * - int2, int4 → Int
5
+ * - int8 (bigint) → BigInt
6
+ * - numeric → BigFloat
7
+ * - float4, float8 → Float
8
+ * - interval → Interval (OBJECT kind, not SCALAR)
9
+ * - timestamptz, timestamp → Datetime
10
+ */
11
+ export const PG_TYPE_MAP = [
12
+ { pgUdtNames: ['uuid'], gqlTypeName: 'UUID', fieldType: 'uuid', gqlKind: 'SCALAR' },
13
+ { pgUdtNames: ['_uuid'], gqlTypeName: 'UUID', fieldType: 'uuid[]', gqlKind: 'SCALAR', isArray: true },
14
+ { pgUdtNames: ['text', 'varchar', 'bpchar', 'name', 'citext'], gqlTypeName: 'String', fieldType: 'text', gqlKind: 'SCALAR' },
15
+ { pgUdtNames: ['_text', '_varchar', '_citext'], gqlTypeName: 'String', fieldType: 'text[]', gqlKind: 'SCALAR', isArray: true },
16
+ { pgUdtNames: ['bool'], gqlTypeName: 'Boolean', fieldType: 'boolean', gqlKind: 'SCALAR' },
17
+ { pgUdtNames: ['jsonb', 'json'], gqlTypeName: 'JSON', fieldType: 'jsonb', gqlKind: 'SCALAR' },
18
+ { pgUdtNames: ['_jsonb'], gqlTypeName: 'JSON', fieldType: 'jsonb[]', gqlKind: 'SCALAR', isArray: true },
19
+ { pgUdtNames: ['int2', 'int4'], gqlTypeName: 'Int', fieldType: 'int', gqlKind: 'SCALAR' },
20
+ { pgUdtNames: ['int8'], gqlTypeName: 'BigInt', fieldType: 'int', gqlKind: 'SCALAR' },
21
+ { pgUdtNames: ['numeric'], gqlTypeName: 'BigFloat', fieldType: 'int', gqlKind: 'SCALAR' },
22
+ { pgUdtNames: ['float4', 'float8'], gqlTypeName: 'Float', fieldType: 'int', gqlKind: 'SCALAR' },
23
+ { pgUdtNames: ['interval'], gqlTypeName: 'Interval', fieldType: 'interval', gqlKind: 'OBJECT' },
24
+ { pgUdtNames: ['timestamptz', 'timestamp'], gqlTypeName: 'Datetime', fieldType: 'timestamptz', gqlKind: 'SCALAR' },
25
+ ];
26
+ // =============================================================================
27
+ // Lookup indices (built once at module load)
28
+ // =============================================================================
29
+ /** Reverse index: pgUdtName → TypeMapEntry */
30
+ const pgUdtIndex = new Map();
31
+ for (const entry of PG_TYPE_MAP) {
32
+ for (const udt of entry.pgUdtNames) {
33
+ pgUdtIndex.set(udt, entry);
34
+ }
35
+ }
36
+ /** Reverse index: gqlTypeName → TypeMapEntry (first match wins) */
37
+ const gqlTypeIndex = new Map();
38
+ for (const entry of PG_TYPE_MAP) {
39
+ if (!gqlTypeIndex.has(entry.gqlTypeName)) {
40
+ gqlTypeIndex.set(entry.gqlTypeName, entry);
41
+ }
42
+ }
43
+ /**
44
+ * Look up a TypeMapEntry by PostgreSQL udt_name.
45
+ * Returns undefined for unknown types (callers should fall back to 'text').
46
+ */
47
+ export const lookupByPgUdt = (udtName) => pgUdtIndex.get(udtName);
48
+ /**
49
+ * Look up a TypeMapEntry by GraphQL type name.
50
+ * Returns undefined for unknown types (callers should fall back to 'text').
51
+ */
52
+ export const lookupByGqlType = (gqlTypeName) => gqlTypeIndex.get(gqlTypeName);
@@ -9,8 +9,70 @@ exports.exportGraphQLMeta = void 0;
9
9
  * generate SQL INSERT statements.
10
10
  */
11
11
  const csv_to_pg_1 = require("csv-to-pg");
12
+ const inflekt_1 = require("inflekt");
12
13
  const export_utils_1 = require("./export-utils");
13
14
  const graphql_naming_1 = require("./graphql-naming");
15
+ const type_map_1 = require("./type-map");
16
+ /**
17
+ * Discover fields dynamically from a GraphQL type via introspection.
18
+ * Queries `__type` to enumerate fields and infer their `FieldType` via
19
+ * `mapGraphQLTypeToFieldType`. Tracks ENUM fields separately so callers
20
+ * can normalize their CONSTANT_CASE values back to lowercase.
21
+ *
22
+ * `typeOverrides` from the config are applied on top for special types
23
+ * (image, upload, url) that cannot be inferred from the GraphQL type alone.
24
+ */
25
+ const buildDynamicFieldsFromGraphQL = async (client, tableConfig) => {
26
+ const emptyResult = { fields: {}, enumFields: new Set() };
27
+ const typeName = tableConfig.gqlTypeName || (0, graphql_naming_1.getGraphQLTypeName)(tableConfig.table);
28
+ try {
29
+ const introspectedFields = await client.introspectType(typeName);
30
+ const dynamicFields = {};
31
+ const enumFields = new Set();
32
+ for (const [camelName, typeInfo] of introspectedFields) {
33
+ const snakeName = (0, inflekt_1.toSnakeCase)(camelName);
34
+ // Skip internal GraphQL fields
35
+ if (camelName.startsWith('__'))
36
+ continue;
37
+ // Track enum fields for lowercase normalization (custom inflector uppercases them)
38
+ if (typeInfo.kind === 'ENUM') {
39
+ enumFields.add(snakeName);
40
+ }
41
+ // Skip non-scalar fields (relations/computed columns like "database" of type Database)
42
+ // Only SCALAR and ENUM kinds can be selected without sub-field selections
43
+ // EXCEPTION: types registered as non-SCALAR in PG_TYPE_MAP (e.g. Interval=OBJECT)
44
+ // are handled via buildFieldsFragment sub-selections and intervalToPostgres conversion
45
+ if (typeInfo.kind !== 'SCALAR' && typeInfo.kind !== 'ENUM') {
46
+ const mapEntry = (0, type_map_1.lookupByGqlType)(typeInfo.typeName);
47
+ if (mapEntry && mapEntry.gqlKind !== 'SCALAR') {
48
+ dynamicFields[snakeName] = mapEntry.fieldType;
49
+ continue;
50
+ }
51
+ continue;
52
+ }
53
+ dynamicFields[snakeName] = (0, graphql_naming_1.mapGraphQLTypeToFieldType)(typeInfo.typeName, typeInfo.list);
54
+ }
55
+ // Apply type overrides (e.g., image, upload, url)
56
+ if (tableConfig.typeOverrides) {
57
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.typeOverrides)) {
58
+ if (dynamicFields[fieldName]) {
59
+ dynamicFields[fieldName] = fieldType;
60
+ }
61
+ }
62
+ }
63
+ return { fields: dynamicFields, enumFields };
64
+ }
65
+ catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ if (message.includes('Cannot query field') ||
68
+ message.includes('is not defined by type') ||
69
+ message.includes('Unknown field')) {
70
+ // Type not available in the GraphQL schema — return empty
71
+ return emptyResult;
72
+ }
73
+ throw err;
74
+ }
75
+ };
14
76
  /**
15
77
  * Fetch metadata via GraphQL and generate SQL INSERT statements.
16
78
  * This is the GraphQL equivalent of exportMeta() in export-meta.ts.
@@ -21,8 +83,12 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
21
83
  const tableConfig = export_utils_1.META_TABLE_CONFIG[key];
22
84
  if (!tableConfig)
23
85
  return;
24
- const pgFieldNames = Object.keys(tableConfig.fields);
25
- const graphqlFieldsFragment = (0, graphql_naming_1.buildFieldsFragment)(pgFieldNames, tableConfig.fields);
86
+ // Build fields dynamically: either from hardcoded config or via introspection
87
+ const { fields: configFields, enumFields } = await buildDynamicFieldsFromGraphQL(client, tableConfig);
88
+ if (Object.keys(configFields).length === 0)
89
+ return;
90
+ const pgFieldNames = Object.keys(configFields);
91
+ const graphqlFieldsFragment = (0, graphql_naming_1.buildFieldsFragment)(pgFieldNames, configFields);
26
92
  const graphqlQueryName = (0, graphql_naming_1.getGraphQLQueryName)(tableConfig.table);
27
93
  // The 'database' table is fetched by id, not by database_id
28
94
  const condition = key === 'database'
@@ -33,13 +99,30 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
33
99
  if (rows.length > 0) {
34
100
  // Convert camelCase GraphQL keys back to snake_case for the Parser
35
101
  // Also convert interval objects back to Postgres interval strings
102
+ // and normalize enum values from CONSTANT_CASE back to lowercase
36
103
  const pgRows = rows.map(row => {
37
104
  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)) {
105
+ for (const [fieldName, fieldType] of Object.entries(configFields)) {
106
+ // Convert interval fields from {seconds, minutes, ...} objects to strings
40
107
  if (fieldType === 'interval' && pgRow[fieldName] && typeof pgRow[fieldName] === 'object') {
41
108
  pgRow[fieldName] = (0, graphql_naming_1.intervalToPostgres)(pgRow[fieldName]);
42
109
  }
110
+ // Truncate timestamptz to second precision for parity with SQL flow
111
+ // PostGraphile's Datetime scalar preserves full millisecond precision,
112
+ // but the pg driver + our SQL flow truncates to .000Z via Date rounding
113
+ if (fieldType === 'timestamptz' && typeof pgRow[fieldName] === 'string') {
114
+ const d = new Date(pgRow[fieldName]);
115
+ if (!isNaN(d.getTime())) {
116
+ pgRow[fieldName] = new Date(Math.floor(d.getTime() / 1000) * 1000).toISOString();
117
+ }
118
+ }
119
+ }
120
+ // Normalize enum values: custom inflector uppercases them to CONSTANT_CASE,
121
+ // but PostgreSQL stores them in lowercase — convert back for parity with SQL flow
122
+ for (const fieldName of enumFields) {
123
+ if (typeof pgRow[fieldName] === 'string') {
124
+ pgRow[fieldName] = pgRow[fieldName].toLowerCase();
125
+ }
43
126
  }
44
127
  return pgRow;
45
128
  });
@@ -52,7 +135,7 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
52
135
  }
53
136
  }
54
137
  const dynamicFields = {};
55
- for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
138
+ for (const [fieldName, fieldType] of Object.entries(configFields)) {
56
139
  if (returnedKeys.has(fieldName)) {
57
140
  dynamicFields[fieldName] = fieldType;
58
141
  }
@@ -136,6 +219,7 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
136
219
  queryAndParse('permissions_module'),
137
220
  queryAndParse('limits_module'),
138
221
  queryAndParse('levels_module'),
222
+ queryAndParse('events_module'),
139
223
  queryAndParse('users_module'),
140
224
  queryAndParse('hierarchy_module'),
141
225
  queryAndParse('membership_types_module'),
@@ -145,13 +229,14 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
145
229
  queryAndParse('user_state_module'),
146
230
  queryAndParse('profiles_module'),
147
231
  queryAndParse('config_secrets_user_module'),
232
+ queryAndParse('user_credentials_module'),
233
+ queryAndParse('user_settings_module'),
148
234
  queryAndParse('connected_accounts_module'),
149
235
  queryAndParse('phone_numbers_module'),
150
236
  queryAndParse('crypto_addresses_module'),
151
237
  queryAndParse('crypto_auth_module'),
152
238
  queryAndParse('field_module'),
153
239
  queryAndParse('table_module'),
154
- queryAndParse('table_template_module'),
155
240
  queryAndParse('secure_table_provision'),
156
241
  queryAndParse('uuid_module'),
157
242
  queryAndParse('default_ids_module'),
@@ -169,8 +254,21 @@ const exportGraphQLMeta = async ({ client, database_id }) => {
169
254
  queryAndParse('realtime_module'),
170
255
  queryAndParse('session_secrets_module'),
171
256
  queryAndParse('config_secrets_org_module'),
257
+ queryAndParse('config_secrets_module'),
258
+ queryAndParse('i18n_module'),
259
+ queryAndParse('agent_module'),
260
+ queryAndParse('function_module'),
261
+ queryAndParse('namespace_module'),
262
+ queryAndParse('merkle_store_module'),
263
+ queryAndParse('graph_module'),
264
+ queryAndParse('compute_log_module'),
265
+ queryAndParse('db_usage_module'),
266
+ queryAndParse('storage_log_module'),
267
+ queryAndParse('transfer_log_module'),
172
268
  queryAndParse('webauthn_auth_module'),
173
- queryAndParse('webauthn_credentials_module')
269
+ queryAndParse('webauthn_credentials_module'),
270
+ queryAndParse('inference_log_module'),
271
+ queryAndParse('rate_limit_meters_module')
174
272
  ]);
175
273
  return sql;
176
274
  };
package/export-meta.js CHANGED
@@ -22,10 +22,10 @@ const getTableColumns = async (pool, schemaName, tableName) => {
22
22
  return columns;
23
23
  };
24
24
  /**
25
- * Build dynamic fields config by intersecting the hardcoded config with actual database columns.
26
- * - Only includes columns that exist in the database
27
- * - Preserves special type hints from config (image, upload, url) for columns that exist
28
- * - Infers types from PostgreSQL for columns not in config
25
+ * Build dynamic fields config from the database via information_schema.
26
+ * - All fields are derived from `information_schema.columns` + `mapPgTypeToFieldType`.
27
+ * - `typeOverrides` from the config are applied on top for special types
28
+ * (image, upload, url) that cannot be inferred from PG types alone.
29
29
  */
30
30
  const buildDynamicFields = async (pool, tableConfig) => {
31
31
  const actualColumns = await getTableColumns(pool, tableConfig.schema, tableConfig.table);
@@ -34,13 +34,17 @@ const buildDynamicFields = async (pool, tableConfig) => {
34
34
  return {};
35
35
  }
36
36
  const dynamicFields = {};
37
- // For each column in the hardcoded config, check if it exists in the database
38
- for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
39
- if (actualColumns.has(fieldName)) {
40
- // Column exists - use the config's type hint (preserves special types like 'image', 'upload', 'url')
41
- dynamicFields[fieldName] = fieldType;
37
+ // Derive all fields from information_schema
38
+ for (const [columnName, udtName] of actualColumns) {
39
+ dynamicFields[columnName] = (0, export_utils_1.mapPgTypeToFieldType)(udtName);
40
+ }
41
+ // Apply type overrides (image, upload, url)
42
+ if (tableConfig.typeOverrides) {
43
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.typeOverrides)) {
44
+ if (dynamicFields[fieldName]) {
45
+ dynamicFields[fieldName] = fieldType;
46
+ }
42
47
  }
43
- // If column doesn't exist in database, skip it (this fixes the bug)
44
48
  }
45
49
  return dynamicFields;
46
50
  };
@@ -50,8 +54,9 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
50
54
  database: dbname
51
55
  });
52
56
  const sql = {};
53
- // Cache for dynamically built parsers
57
+ // Cache for dynamically built parsers and their field configs
54
58
  const parsers = {};
59
+ const parserFields = {};
55
60
  // Build parser dynamically by querying actual columns from the database
56
61
  const getParser = async (key) => {
57
62
  if (parsers[key]) {
@@ -67,6 +72,7 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
67
72
  // No columns found (table doesn't exist or no matching columns)
68
73
  return null;
69
74
  }
75
+ parserFields[key] = dynamicFields;
70
76
  const parser = new csv_to_pg_1.Parser({
71
77
  schema: tableConfig.schema,
72
78
  table: tableConfig.table,
@@ -84,6 +90,23 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
84
90
  }
85
91
  const result = await pool.query(query, [database_id]);
86
92
  if (result.rows.length) {
93
+ // Truncate timestamptz to second precision to match PostGraphile's Datetime scalar
94
+ // which truncates milliseconds in the GraphQL flow
95
+ const fields = parserFields[key];
96
+ if (fields) {
97
+ for (const row of result.rows) {
98
+ for (const [fieldName, fieldType] of Object.entries(fields)) {
99
+ if (fieldType === 'timestamptz') {
100
+ const val = row[fieldName];
101
+ if (val instanceof Date) {
102
+ // Truncate to second precision and convert to ISO string
103
+ // so both SQL and GraphQL flows pass the same value type to the Parser
104
+ row[fieldName] = new Date(Math.floor(val.getTime() / 1000) * 1000).toISOString();
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
87
110
  const parsed = await parser.parse(result.rows);
88
111
  if (parsed) {
89
112
  sql[key] = parsed;
@@ -148,6 +171,7 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
148
171
  await queryAndParse('permissions_module', `SELECT * FROM metaschema_modules_public.permissions_module WHERE database_id = $1 ORDER BY id`);
149
172
  await queryAndParse('limits_module', `SELECT * FROM metaschema_modules_public.limits_module WHERE database_id = $1 ORDER BY id`);
150
173
  await queryAndParse('levels_module', `SELECT * FROM metaschema_modules_public.levels_module WHERE database_id = $1 ORDER BY id`);
174
+ await queryAndParse('events_module', `SELECT * FROM metaschema_modules_public.events_module WHERE database_id = $1 ORDER BY id`);
151
175
  await queryAndParse('users_module', `SELECT * FROM metaschema_modules_public.users_module WHERE database_id = $1 ORDER BY id`);
152
176
  await queryAndParse('hierarchy_module', `SELECT * FROM metaschema_modules_public.hierarchy_module WHERE database_id = $1 ORDER BY id`);
153
177
  await queryAndParse('membership_types_module', `SELECT * FROM metaschema_modules_public.membership_types_module WHERE database_id = $1 ORDER BY id`);
@@ -157,13 +181,14 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
157
181
  await queryAndParse('user_state_module', `SELECT * FROM metaschema_modules_public.user_state_module WHERE database_id = $1 ORDER BY id`);
158
182
  await queryAndParse('profiles_module', `SELECT * FROM metaschema_modules_public.profiles_module WHERE database_id = $1 ORDER BY id`);
159
183
  await queryAndParse('config_secrets_user_module', `SELECT * FROM metaschema_modules_public.config_secrets_user_module WHERE database_id = $1 ORDER BY id`);
184
+ await queryAndParse('user_credentials_module', `SELECT * FROM metaschema_modules_public.user_credentials_module WHERE database_id = $1 ORDER BY id`);
185
+ await queryAndParse('user_settings_module', `SELECT * FROM metaschema_modules_public.user_settings_module WHERE database_id = $1 ORDER BY id`);
160
186
  await queryAndParse('connected_accounts_module', `SELECT * FROM metaschema_modules_public.connected_accounts_module WHERE database_id = $1 ORDER BY id`);
161
187
  await queryAndParse('phone_numbers_module', `SELECT * FROM metaschema_modules_public.phone_numbers_module WHERE database_id = $1 ORDER BY id`);
162
188
  await queryAndParse('crypto_addresses_module', `SELECT * FROM metaschema_modules_public.crypto_addresses_module WHERE database_id = $1 ORDER BY id`);
163
189
  await queryAndParse('crypto_auth_module', `SELECT * FROM metaschema_modules_public.crypto_auth_module WHERE database_id = $1 ORDER BY id`);
164
190
  await queryAndParse('field_module', `SELECT * FROM metaschema_modules_public.field_module WHERE database_id = $1 ORDER BY id`);
165
191
  await queryAndParse('table_module', `SELECT * FROM metaschema_modules_public.table_module WHERE database_id = $1 ORDER BY id`);
166
- await queryAndParse('table_template_module', `SELECT * FROM metaschema_modules_public.table_template_module WHERE database_id = $1 ORDER BY id`);
167
192
  await queryAndParse('secure_table_provision', `SELECT * FROM metaschema_modules_public.secure_table_provision WHERE database_id = $1 ORDER BY id`);
168
193
  await queryAndParse('uuid_module', `SELECT * FROM metaschema_modules_public.uuid_module WHERE database_id = $1 ORDER BY id`);
169
194
  await queryAndParse('default_ids_module', `SELECT * FROM metaschema_modules_public.default_ids_module WHERE database_id = $1 ORDER BY id`);
@@ -181,8 +206,21 @@ const exportMeta = async ({ opts, dbname, database_id }) => {
181
206
  await queryAndParse('realtime_module', `SELECT * FROM metaschema_modules_public.realtime_module WHERE database_id = $1 ORDER BY id`);
182
207
  await queryAndParse('session_secrets_module', `SELECT * FROM metaschema_modules_public.session_secrets_module WHERE database_id = $1 ORDER BY id`);
183
208
  await queryAndParse('config_secrets_org_module', `SELECT * FROM metaschema_modules_public.config_secrets_org_module WHERE database_id = $1 ORDER BY id`);
209
+ await queryAndParse('config_secrets_module', `SELECT * FROM metaschema_modules_public.config_secrets_module WHERE database_id = $1 ORDER BY id`);
210
+ await queryAndParse('i18n_module', `SELECT * FROM metaschema_modules_public.i18n_module WHERE database_id = $1 ORDER BY id`);
211
+ await queryAndParse('agent_module', `SELECT * FROM metaschema_modules_public.agent_module WHERE database_id = $1 ORDER BY id`);
212
+ await queryAndParse('function_module', `SELECT * FROM metaschema_modules_public.function_module WHERE database_id = $1 ORDER BY id`);
213
+ await queryAndParse('namespace_module', `SELECT * FROM metaschema_modules_public.namespace_module WHERE database_id = $1 ORDER BY id`);
214
+ await queryAndParse('merkle_store_module', `SELECT * FROM metaschema_modules_public.merkle_store_module WHERE database_id = $1 ORDER BY id`);
215
+ await queryAndParse('graph_module', `SELECT * FROM metaschema_modules_public.graph_module WHERE database_id = $1 ORDER BY id`);
216
+ await queryAndParse('compute_log_module', `SELECT * FROM metaschema_modules_public.compute_log_module WHERE database_id = $1 ORDER BY id`);
217
+ await queryAndParse('db_usage_module', `SELECT * FROM metaschema_modules_public.db_usage_module WHERE database_id = $1 ORDER BY id`);
218
+ await queryAndParse('storage_log_module', `SELECT * FROM metaschema_modules_public.storage_log_module WHERE database_id = $1 ORDER BY id`);
219
+ await queryAndParse('transfer_log_module', `SELECT * FROM metaschema_modules_public.transfer_log_module WHERE database_id = $1 ORDER BY id`);
184
220
  await queryAndParse('webauthn_auth_module', `SELECT * FROM metaschema_modules_public.webauthn_auth_module WHERE database_id = $1 ORDER BY id`);
185
221
  await queryAndParse('webauthn_credentials_module', `SELECT * FROM metaschema_modules_public.webauthn_credentials_module WHERE database_id = $1 ORDER BY id`);
222
+ await queryAndParse('inference_log_module', `SELECT * FROM metaschema_modules_public.inference_log_module WHERE database_id = $1 ORDER BY id`);
223
+ await queryAndParse('rate_limit_meters_module', `SELECT * FROM metaschema_modules_public.rate_limit_meters_module WHERE database_id = $1 ORDER BY id`);
186
224
  return sql;
187
225
  };
188
226
  exports.exportMeta = exportMeta;
package/export-utils.d.ts CHANGED
@@ -4,7 +4,13 @@ import { PgpmPackage } from '@pgpmjs/core';
4
4
  * Required extensions for database schema exports.
5
5
  * Includes native PostgreSQL extensions and pgpm modules.
6
6
  */
7
- export declare const DB_REQUIRED_EXTENSIONS: readonly ["plpgsql", "uuid-ossp", "citext", "pgcrypto", "btree_gin", "btree_gist", "pg_textsearch", "pg_trgm", "postgis", "hstore", "vector", "ltree", "metaschema-schema", "pgpm-inflection", "pgpm-utils", "pgpm-database-jobs", "pgpm-jwt-claims", "pgpm-stamps", "pgpm-base32", "pgpm-totp", "pgpm-types", "pgpm-ltree-helpers", "pgpm-partman"];
7
+ export declare const DB_REQUIRED_EXTENSIONS: readonly ["plpgsql", "uuid-ossp", "citext", "pgcrypto", "btree_gin", "btree_gist", "pg_textsearch", "pg_trgm", "postgis", "hstore", "vector", "ltree", "metaschema-schema", "pgpm-inflection", "pgpm-uuid", "pgpm-utils", "pgpm-database-jobs", "pgpm-jwt-claims", "pgpm-stamps", "pgpm-base32", "pgpm-totp", "pgpm-types", "pgpm-ltree-helpers", "pgpm-partman"];
8
+ /**
9
+ * Map PostgreSQL data types to FieldType values.
10
+ * Uses udt_name from information_schema which gives the base type name.
11
+ * Delegates to the canonical PG_TYPE_MAP in type-map.ts.
12
+ */
13
+ export declare const mapPgTypeToFieldType: (udtName: string) => FieldType;
8
14
  /**
9
15
  * Required extensions for service/meta exports.
10
16
  * Includes native PostgreSQL extensions and pgpm modules for metadata management.
@@ -23,24 +29,25 @@ export declare const META_COMMON_FOOTER = "\nSET session_replication_role TO DEF
23
29
  * Ordered list of meta tables for export.
24
30
  * Tables are processed in this order to satisfy foreign key dependencies.
25
31
  */
26
- export declare const META_TABLE_ORDER: readonly ["database", "schema", "function", "table", "field", "spatial_relation", "policy", "index", "trigger", "trigger_function", "rls_function", "foreign_key_constraint", "primary_key_constraint", "unique_constraint", "check_constraint", "full_text_search", "schema_grant", "table_grant", "default_privilege", "domains", "sites", "apis", "apps", "site_modules", "site_themes", "site_metadata", "api_modules", "api_extensions", "api_schemas", "database_settings", "api_settings", "rls_settings", "cors_settings", "pubkey_settings", "webauthn_settings", "rls_module", "user_auth_module", "memberships_module", "permissions_module", "limits_module", "events_module", "users_module", "hierarchy_module", "membership_types_module", "invites_module", "emails_module", "sessions_module", "user_state_module", "profiles_module", "config_secrets_user_module", "connected_accounts_module", "phone_numbers_module", "crypto_addresses_module", "crypto_auth_module", "field_module", "table_module", "secure_table_provision", "uuid_module", "default_ids_module", "denormalized_table_field", "relation_provision", "entity_type_provision", "rate_limits_module", "storage_module", "billing_module", "billing_provider_module", "devices_module", "identity_providers_module", "notifications_module", "plans_module", "realtime_module", "session_secrets_module", "config_secrets_org_module", "webauthn_auth_module", "webauthn_credentials_module", "inference_log_module", "rate_limit_meters_module"];
32
+ export declare const META_TABLE_ORDER: readonly ["database", "schema", "function", "table", "field", "spatial_relation", "policy", "index", "trigger", "trigger_function", "rls_function", "foreign_key_constraint", "primary_key_constraint", "unique_constraint", "check_constraint", "full_text_search", "schema_grant", "table_grant", "default_privilege", "domains", "sites", "apis", "apps", "site_modules", "site_themes", "site_metadata", "api_modules", "api_extensions", "api_schemas", "database_settings", "api_settings", "rls_settings", "cors_settings", "pubkey_settings", "webauthn_settings", "rls_module", "user_auth_module", "memberships_module", "permissions_module", "limits_module", "levels_module", "events_module", "users_module", "hierarchy_module", "membership_types_module", "invites_module", "emails_module", "sessions_module", "user_state_module", "profiles_module", "config_secrets_user_module", "user_credentials_module", "user_settings_module", "connected_accounts_module", "phone_numbers_module", "crypto_addresses_module", "crypto_auth_module", "field_module", "table_module", "secure_table_provision", "uuid_module", "default_ids_module", "denormalized_table_field", "relation_provision", "entity_type_provision", "rate_limits_module", "storage_module", "billing_module", "billing_provider_module", "devices_module", "identity_providers_module", "notifications_module", "plans_module", "realtime_module", "session_secrets_module", "config_secrets_org_module", "config_secrets_module", "i18n_module", "agent_module", "function_module", "namespace_module", "merkle_store_module", "graph_module", "compute_log_module", "db_usage_module", "storage_log_module", "transfer_log_module", "webauthn_auth_module", "webauthn_credentials_module", "inference_log_module", "rate_limit_meters_module"];
27
33
  export type FieldType = 'uuid' | 'uuid[]' | 'text' | 'text[]' | 'boolean' | 'image' | 'upload' | 'url' | 'jsonb' | 'jsonb[]' | 'int' | 'interval' | 'timestamptz';
28
34
  export interface TableConfig {
29
35
  schema: string;
30
36
  table: string;
31
37
  conflictDoNothing?: boolean;
32
- fields: Record<string, FieldType>;
38
+ typeOverrides?: Record<string, FieldType>;
39
+ gqlTypeName?: string;
33
40
  }
34
41
  /**
35
42
  * Shared metadata table configuration.
36
43
  *
37
- * This is the **superset** of fields needed by both the SQL export flow
38
- * (export-meta.ts) and the GraphQL export flow (export-graphql-meta.ts).
39
- * Each flow dynamically filters to only the fields that actually exist:
40
- * - SQL flow: uses buildDynamicFields() to intersect with information_schema
41
- * - GraphQL flow: filters to fields present in the returned data
44
+ * Fields are discovered dynamically at runtime via introspection:
45
+ * - SQL flow: uses information_schema.columns + mapPgTypeToFieldType()
46
+ * - GraphQL flow: uses __type introspection + mapGraphQLTypeToFieldType()
47
+ *
48
+ * Only `typeOverrides` are hardcoded for special types (image, upload, url)
49
+ * that cannot be inferred from database/GraphQL types alone.
42
50
  *
43
- * Adding a field here that doesn't exist in a particular environment is safe.
44
51
  */
45
52
  export declare const META_TABLE_CONFIG: Record<string, TableConfig>;
46
53
  export interface Schema {