@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.
package/README.md CHANGED
@@ -75,6 +75,10 @@ Common issues and solutions for pgpm, PostgreSQL, and testing.
75
75
  * [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
76
76
  * [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
77
77
 
78
+ ### 📚 Documentation & Skills
79
+
80
+ * [constructive-skills](https://github.com/constructive-io/constructive-skills): **📖 Platform documentation and AI agent skills** — feature catalog, blueprint reference, SDK guides (i18n, billing, limits, events, uploads, security, entities, search, AI), and deployment guides.
81
+
78
82
  ## Credits
79
83
 
80
84
  **🛠 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).**
@@ -6,8 +6,70 @@
6
6
  * generate SQL INSERT statements.
7
7
  */
8
8
  import { Parser } from 'csv-to-pg';
9
+ import { toSnakeCase } from 'inflekt';
9
10
  import { META_TABLE_CONFIG } from './export-utils';
10
- import { buildFieldsFragment, getGraphQLQueryName, graphqlRowToPostgresRow, intervalToPostgres } from './graphql-naming';
11
+ import { buildFieldsFragment, getGraphQLQueryName, getGraphQLTypeName, graphqlRowToPostgresRow, intervalToPostgres, mapGraphQLTypeToFieldType } from './graphql-naming';
12
+ import { lookupByGqlType } from './type-map';
13
+ /**
14
+ * Discover fields dynamically from a GraphQL type via introspection.
15
+ * Queries `__type` to enumerate fields and infer their `FieldType` via
16
+ * `mapGraphQLTypeToFieldType`. Tracks ENUM fields separately so callers
17
+ * can normalize their CONSTANT_CASE values back to lowercase.
18
+ *
19
+ * `typeOverrides` from the config are applied on top for special types
20
+ * (image, upload, url) that cannot be inferred from the GraphQL type alone.
21
+ */
22
+ const buildDynamicFieldsFromGraphQL = async (client, tableConfig) => {
23
+ const emptyResult = { fields: {}, enumFields: new Set() };
24
+ const typeName = tableConfig.gqlTypeName || getGraphQLTypeName(tableConfig.table);
25
+ try {
26
+ const introspectedFields = await client.introspectType(typeName);
27
+ const dynamicFields = {};
28
+ const enumFields = new Set();
29
+ for (const [camelName, typeInfo] of introspectedFields) {
30
+ const snakeName = toSnakeCase(camelName);
31
+ // Skip internal GraphQL fields
32
+ if (camelName.startsWith('__'))
33
+ continue;
34
+ // Track enum fields for lowercase normalization (custom inflector uppercases them)
35
+ if (typeInfo.kind === 'ENUM') {
36
+ enumFields.add(snakeName);
37
+ }
38
+ // Skip non-scalar fields (relations/computed columns like "database" of type Database)
39
+ // Only SCALAR and ENUM kinds can be selected without sub-field selections
40
+ // EXCEPTION: types registered as non-SCALAR in PG_TYPE_MAP (e.g. Interval=OBJECT)
41
+ // are handled via buildFieldsFragment sub-selections and intervalToPostgres conversion
42
+ if (typeInfo.kind !== 'SCALAR' && typeInfo.kind !== 'ENUM') {
43
+ const mapEntry = lookupByGqlType(typeInfo.typeName);
44
+ if (mapEntry && mapEntry.gqlKind !== 'SCALAR') {
45
+ dynamicFields[snakeName] = mapEntry.fieldType;
46
+ continue;
47
+ }
48
+ continue;
49
+ }
50
+ dynamicFields[snakeName] = mapGraphQLTypeToFieldType(typeInfo.typeName, typeInfo.list);
51
+ }
52
+ // Apply type overrides (e.g., image, upload, url)
53
+ if (tableConfig.typeOverrides) {
54
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.typeOverrides)) {
55
+ if (dynamicFields[fieldName]) {
56
+ dynamicFields[fieldName] = fieldType;
57
+ }
58
+ }
59
+ }
60
+ return { fields: dynamicFields, enumFields };
61
+ }
62
+ catch (err) {
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ if (message.includes('Cannot query field') ||
65
+ message.includes('is not defined by type') ||
66
+ message.includes('Unknown field')) {
67
+ // Type not available in the GraphQL schema — return empty
68
+ return emptyResult;
69
+ }
70
+ throw err;
71
+ }
72
+ };
11
73
  /**
12
74
  * Fetch metadata via GraphQL and generate SQL INSERT statements.
13
75
  * This is the GraphQL equivalent of exportMeta() in export-meta.ts.
@@ -18,8 +80,12 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
18
80
  const tableConfig = META_TABLE_CONFIG[key];
19
81
  if (!tableConfig)
20
82
  return;
21
- const pgFieldNames = Object.keys(tableConfig.fields);
22
- const graphqlFieldsFragment = buildFieldsFragment(pgFieldNames, tableConfig.fields);
83
+ // Build fields dynamically: either from hardcoded config or via introspection
84
+ const { fields: configFields, enumFields } = await buildDynamicFieldsFromGraphQL(client, tableConfig);
85
+ if (Object.keys(configFields).length === 0)
86
+ return;
87
+ const pgFieldNames = Object.keys(configFields);
88
+ const graphqlFieldsFragment = buildFieldsFragment(pgFieldNames, configFields);
23
89
  const graphqlQueryName = getGraphQLQueryName(tableConfig.table);
24
90
  // The 'database' table is fetched by id, not by database_id
25
91
  const condition = key === 'database'
@@ -30,13 +96,30 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
30
96
  if (rows.length > 0) {
31
97
  // Convert camelCase GraphQL keys back to snake_case for the Parser
32
98
  // Also convert interval objects back to Postgres interval strings
99
+ // and normalize enum values from CONSTANT_CASE back to lowercase
33
100
  const pgRows = rows.map(row => {
34
101
  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)) {
102
+ for (const [fieldName, fieldType] of Object.entries(configFields)) {
103
+ // Convert interval fields from {seconds, minutes, ...} objects to strings
37
104
  if (fieldType === 'interval' && pgRow[fieldName] && typeof pgRow[fieldName] === 'object') {
38
105
  pgRow[fieldName] = intervalToPostgres(pgRow[fieldName]);
39
106
  }
107
+ // Truncate timestamptz to second precision for parity with SQL flow
108
+ // PostGraphile's Datetime scalar preserves full millisecond precision,
109
+ // but the pg driver + our SQL flow truncates to .000Z via Date rounding
110
+ if (fieldType === 'timestamptz' && typeof pgRow[fieldName] === 'string') {
111
+ const d = new Date(pgRow[fieldName]);
112
+ if (!isNaN(d.getTime())) {
113
+ pgRow[fieldName] = new Date(Math.floor(d.getTime() / 1000) * 1000).toISOString();
114
+ }
115
+ }
116
+ }
117
+ // Normalize enum values: custom inflector uppercases them to CONSTANT_CASE,
118
+ // but PostgreSQL stores them in lowercase — convert back for parity with SQL flow
119
+ for (const fieldName of enumFields) {
120
+ if (typeof pgRow[fieldName] === 'string') {
121
+ pgRow[fieldName] = pgRow[fieldName].toLowerCase();
122
+ }
40
123
  }
41
124
  return pgRow;
42
125
  });
@@ -49,7 +132,7 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
49
132
  }
50
133
  }
51
134
  const dynamicFields = {};
52
- for (const [fieldName, fieldType] of Object.entries(tableConfig.fields)) {
135
+ for (const [fieldName, fieldType] of Object.entries(configFields)) {
53
136
  if (returnedKeys.has(fieldName)) {
54
137
  dynamicFields[fieldName] = fieldType;
55
138
  }
@@ -133,6 +216,7 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
133
216
  queryAndParse('permissions_module'),
134
217
  queryAndParse('limits_module'),
135
218
  queryAndParse('levels_module'),
219
+ queryAndParse('events_module'),
136
220
  queryAndParse('users_module'),
137
221
  queryAndParse('hierarchy_module'),
138
222
  queryAndParse('membership_types_module'),
@@ -142,13 +226,14 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
142
226
  queryAndParse('user_state_module'),
143
227
  queryAndParse('profiles_module'),
144
228
  queryAndParse('config_secrets_user_module'),
229
+ queryAndParse('user_credentials_module'),
230
+ queryAndParse('user_settings_module'),
145
231
  queryAndParse('connected_accounts_module'),
146
232
  queryAndParse('phone_numbers_module'),
147
233
  queryAndParse('crypto_addresses_module'),
148
234
  queryAndParse('crypto_auth_module'),
149
235
  queryAndParse('field_module'),
150
236
  queryAndParse('table_module'),
151
- queryAndParse('table_template_module'),
152
237
  queryAndParse('secure_table_provision'),
153
238
  queryAndParse('uuid_module'),
154
239
  queryAndParse('default_ids_module'),
@@ -166,8 +251,21 @@ export const exportGraphQLMeta = async ({ client, database_id }) => {
166
251
  queryAndParse('realtime_module'),
167
252
  queryAndParse('session_secrets_module'),
168
253
  queryAndParse('config_secrets_org_module'),
254
+ queryAndParse('config_secrets_module'),
255
+ queryAndParse('i18n_module'),
256
+ queryAndParse('agent_module'),
257
+ queryAndParse('function_module'),
258
+ queryAndParse('namespace_module'),
259
+ queryAndParse('merkle_store_module'),
260
+ queryAndParse('graph_module'),
261
+ queryAndParse('compute_log_module'),
262
+ queryAndParse('db_usage_module'),
263
+ queryAndParse('storage_log_module'),
264
+ queryAndParse('transfer_log_module'),
169
265
  queryAndParse('webauthn_auth_module'),
170
- queryAndParse('webauthn_credentials_module')
266
+ queryAndParse('webauthn_credentials_module'),
267
+ queryAndParse('inference_log_module'),
268
+ queryAndParse('rate_limit_meters_module')
171
269
  ]);
172
270
  return sql;
173
271
  };
@@ -1,6 +1,6 @@
1
1
  import { Parser } from 'csv-to-pg';
2
2
  import { getPgPool } from 'pg-cache';
3
- import { META_TABLE_CONFIG } from './export-utils';
3
+ import { META_TABLE_CONFIG, mapPgTypeToFieldType } from './export-utils';
4
4
  /**
5
5
  * Query actual columns from information_schema for a given table.
6
6
  * Returns a map of column_name -> udt_name (PostgreSQL type).
@@ -19,10 +19,10 @@ const getTableColumns = async (pool, schemaName, tableName) => {
19
19
  return columns;
20
20
  };
21
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
22
+ * Build dynamic fields config from the database via information_schema.
23
+ * - All fields are derived from `information_schema.columns` + `mapPgTypeToFieldType`.
24
+ * - `typeOverrides` from the config are applied on top for special types
25
+ * (image, upload, url) that cannot be inferred from PG types alone.
26
26
  */
27
27
  const buildDynamicFields = async (pool, tableConfig) => {
28
28
  const actualColumns = await getTableColumns(pool, tableConfig.schema, tableConfig.table);
@@ -31,13 +31,17 @@ const buildDynamicFields = async (pool, tableConfig) => {
31
31
  return {};
32
32
  }
33
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;
34
+ // Derive all fields from information_schema
35
+ for (const [columnName, udtName] of actualColumns) {
36
+ dynamicFields[columnName] = mapPgTypeToFieldType(udtName);
37
+ }
38
+ // Apply type overrides (image, upload, url)
39
+ if (tableConfig.typeOverrides) {
40
+ for (const [fieldName, fieldType] of Object.entries(tableConfig.typeOverrides)) {
41
+ if (dynamicFields[fieldName]) {
42
+ dynamicFields[fieldName] = fieldType;
43
+ }
39
44
  }
40
- // If column doesn't exist in database, skip it (this fixes the bug)
41
45
  }
42
46
  return dynamicFields;
43
47
  };
@@ -47,8 +51,9 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
47
51
  database: dbname
48
52
  });
49
53
  const sql = {};
50
- // Cache for dynamically built parsers
54
+ // Cache for dynamically built parsers and their field configs
51
55
  const parsers = {};
56
+ const parserFields = {};
52
57
  // Build parser dynamically by querying actual columns from the database
53
58
  const getParser = async (key) => {
54
59
  if (parsers[key]) {
@@ -64,6 +69,7 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
64
69
  // No columns found (table doesn't exist or no matching columns)
65
70
  return null;
66
71
  }
72
+ parserFields[key] = dynamicFields;
67
73
  const parser = new Parser({
68
74
  schema: tableConfig.schema,
69
75
  table: tableConfig.table,
@@ -81,6 +87,23 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
81
87
  }
82
88
  const result = await pool.query(query, [database_id]);
83
89
  if (result.rows.length) {
90
+ // Truncate timestamptz to second precision to match PostGraphile's Datetime scalar
91
+ // which truncates milliseconds in the GraphQL flow
92
+ const fields = parserFields[key];
93
+ if (fields) {
94
+ for (const row of result.rows) {
95
+ for (const [fieldName, fieldType] of Object.entries(fields)) {
96
+ if (fieldType === 'timestamptz') {
97
+ const val = row[fieldName];
98
+ if (val instanceof Date) {
99
+ // Truncate to second precision and convert to ISO string
100
+ // so both SQL and GraphQL flows pass the same value type to the Parser
101
+ row[fieldName] = new Date(Math.floor(val.getTime() / 1000) * 1000).toISOString();
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
84
107
  const parsed = await parser.parse(result.rows);
85
108
  if (parsed) {
86
109
  sql[key] = parsed;
@@ -145,6 +168,7 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
145
168
  await queryAndParse('permissions_module', `SELECT * FROM metaschema_modules_public.permissions_module WHERE database_id = $1 ORDER BY id`);
146
169
  await queryAndParse('limits_module', `SELECT * FROM metaschema_modules_public.limits_module WHERE database_id = $1 ORDER BY id`);
147
170
  await queryAndParse('levels_module', `SELECT * FROM metaschema_modules_public.levels_module WHERE database_id = $1 ORDER BY id`);
171
+ await queryAndParse('events_module', `SELECT * FROM metaschema_modules_public.events_module WHERE database_id = $1 ORDER BY id`);
148
172
  await queryAndParse('users_module', `SELECT * FROM metaschema_modules_public.users_module WHERE database_id = $1 ORDER BY id`);
149
173
  await queryAndParse('hierarchy_module', `SELECT * FROM metaschema_modules_public.hierarchy_module WHERE database_id = $1 ORDER BY id`);
150
174
  await queryAndParse('membership_types_module', `SELECT * FROM metaschema_modules_public.membership_types_module WHERE database_id = $1 ORDER BY id`);
@@ -154,13 +178,14 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
154
178
  await queryAndParse('user_state_module', `SELECT * FROM metaschema_modules_public.user_state_module WHERE database_id = $1 ORDER BY id`);
155
179
  await queryAndParse('profiles_module', `SELECT * FROM metaschema_modules_public.profiles_module WHERE database_id = $1 ORDER BY id`);
156
180
  await queryAndParse('config_secrets_user_module', `SELECT * FROM metaschema_modules_public.config_secrets_user_module WHERE database_id = $1 ORDER BY id`);
181
+ await queryAndParse('user_credentials_module', `SELECT * FROM metaschema_modules_public.user_credentials_module WHERE database_id = $1 ORDER BY id`);
182
+ await queryAndParse('user_settings_module', `SELECT * FROM metaschema_modules_public.user_settings_module WHERE database_id = $1 ORDER BY id`);
157
183
  await queryAndParse('connected_accounts_module', `SELECT * FROM metaschema_modules_public.connected_accounts_module WHERE database_id = $1 ORDER BY id`);
158
184
  await queryAndParse('phone_numbers_module', `SELECT * FROM metaschema_modules_public.phone_numbers_module WHERE database_id = $1 ORDER BY id`);
159
185
  await queryAndParse('crypto_addresses_module', `SELECT * FROM metaschema_modules_public.crypto_addresses_module WHERE database_id = $1 ORDER BY id`);
160
186
  await queryAndParse('crypto_auth_module', `SELECT * FROM metaschema_modules_public.crypto_auth_module WHERE database_id = $1 ORDER BY id`);
161
187
  await queryAndParse('field_module', `SELECT * FROM metaschema_modules_public.field_module WHERE database_id = $1 ORDER BY id`);
162
188
  await queryAndParse('table_module', `SELECT * FROM metaschema_modules_public.table_module WHERE database_id = $1 ORDER BY id`);
163
- await queryAndParse('table_template_module', `SELECT * FROM metaschema_modules_public.table_template_module WHERE database_id = $1 ORDER BY id`);
164
189
  await queryAndParse('secure_table_provision', `SELECT * FROM metaschema_modules_public.secure_table_provision WHERE database_id = $1 ORDER BY id`);
165
190
  await queryAndParse('uuid_module', `SELECT * FROM metaschema_modules_public.uuid_module WHERE database_id = $1 ORDER BY id`);
166
191
  await queryAndParse('default_ids_module', `SELECT * FROM metaschema_modules_public.default_ids_module WHERE database_id = $1 ORDER BY id`);
@@ -178,7 +203,20 @@ export const exportMeta = async ({ opts, dbname, database_id }) => {
178
203
  await queryAndParse('realtime_module', `SELECT * FROM metaschema_modules_public.realtime_module WHERE database_id = $1 ORDER BY id`);
179
204
  await queryAndParse('session_secrets_module', `SELECT * FROM metaschema_modules_public.session_secrets_module WHERE database_id = $1 ORDER BY id`);
180
205
  await queryAndParse('config_secrets_org_module', `SELECT * FROM metaschema_modules_public.config_secrets_org_module WHERE database_id = $1 ORDER BY id`);
206
+ await queryAndParse('config_secrets_module', `SELECT * FROM metaschema_modules_public.config_secrets_module WHERE database_id = $1 ORDER BY id`);
207
+ await queryAndParse('i18n_module', `SELECT * FROM metaschema_modules_public.i18n_module WHERE database_id = $1 ORDER BY id`);
208
+ await queryAndParse('agent_module', `SELECT * FROM metaschema_modules_public.agent_module WHERE database_id = $1 ORDER BY id`);
209
+ await queryAndParse('function_module', `SELECT * FROM metaschema_modules_public.function_module WHERE database_id = $1 ORDER BY id`);
210
+ await queryAndParse('namespace_module', `SELECT * FROM metaschema_modules_public.namespace_module WHERE database_id = $1 ORDER BY id`);
211
+ await queryAndParse('merkle_store_module', `SELECT * FROM metaschema_modules_public.merkle_store_module WHERE database_id = $1 ORDER BY id`);
212
+ await queryAndParse('graph_module', `SELECT * FROM metaschema_modules_public.graph_module WHERE database_id = $1 ORDER BY id`);
213
+ await queryAndParse('compute_log_module', `SELECT * FROM metaschema_modules_public.compute_log_module WHERE database_id = $1 ORDER BY id`);
214
+ await queryAndParse('db_usage_module', `SELECT * FROM metaschema_modules_public.db_usage_module WHERE database_id = $1 ORDER BY id`);
215
+ await queryAndParse('storage_log_module', `SELECT * FROM metaschema_modules_public.storage_log_module WHERE database_id = $1 ORDER BY id`);
216
+ await queryAndParse('transfer_log_module', `SELECT * FROM metaschema_modules_public.transfer_log_module WHERE database_id = $1 ORDER BY id`);
181
217
  await queryAndParse('webauthn_auth_module', `SELECT * FROM metaschema_modules_public.webauthn_auth_module WHERE database_id = $1 ORDER BY id`);
182
218
  await queryAndParse('webauthn_credentials_module', `SELECT * FROM metaschema_modules_public.webauthn_credentials_module WHERE database_id = $1 ORDER BY id`);
219
+ await queryAndParse('inference_log_module', `SELECT * FROM metaschema_modules_public.inference_log_module WHERE database_id = $1 ORDER BY id`);
220
+ await queryAndParse('rate_limit_meters_module', `SELECT * FROM metaschema_modules_public.rate_limit_meters_module WHERE database_id = $1 ORDER BY id`);
183
221
  return sql;
184
222
  };