@0xobelisk/graphql-server
Version:
Tookit for interacting with dubhe graphql server
318 lines (315 loc) • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.UniversalSubscriptionsPlugin = void 0;
exports.generateStoreTablesInfo = generateStoreTablesInfo;
exports.createUniversalSubscriptionsPlugin = createUniversalSubscriptionsPlugin;
const postgraphile_1 = require("postgraphile");
const logger_1 = require("./utils/logger");
// Cached table information
let cachedTables = {};
let schemaGenerated = false;
/**
* Dynamically retrieve schema information for all store tables
*/
async function discoverStoreTables(pgClient) {
if (schemaGenerated && Object.keys(cachedTables).length > 0) {
return cachedTables;
}
try {
logger_1.subscriptionLogger.info('Starting discovery of database store table structure...');
// 1. Get all store_* tables
const tablesResult = await pgClient.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name LIKE 'store_%'
ORDER BY table_name
`);
const tables = {};
// 2. Get detailed information for each table
for (const tableRow of tablesResult.rows) {
const fullTableName = tableRow.table_name;
const tableName = fullTableName.replace(/^store_/, '');
const tableInfo = await getTableInfo(pgClient, fullTableName);
tables[tableName] = tableInfo;
}
cachedTables = tables;
schemaGenerated = true;
logger_1.subscriptionLogger.info(`Discovered ${Object.keys(tables).length} store tables: ${Object.keys(tables).join(', ')}`);
return tables;
}
catch (error) {
logger_1.subscriptionLogger.error('Failed to discover store tables', error);
return {};
}
}
/**
* Generate pre-built table information - called at server startup
*/
async function generateStoreTablesInfo(pgPool) {
const pgClient = await pgPool.connect();
try {
const tables = await discoverStoreTables(pgClient);
logger_1.subscriptionLogger.info(`Pre-generated schema information for ${Object.keys(tables).length} tables`);
return tables;
}
finally {
pgClient.release();
}
}
/**
* Simplified tools plugin - only provides basic query functionality, let PostGraphile's built-in listen subscriptions work normally
*/
function createUniversalSubscriptionsPlugin(preGeneratedTables) {
return (0, postgraphile_1.makeExtendSchemaPlugin)((_build) => {
logger_1.subscriptionLogger.info('Enabling simplified tools plugin - only keeping basic query functionality');
// Use pre-generated table information if available
if (preGeneratedTables && Object.keys(preGeneratedTables).length > 0) {
cachedTables = preGeneratedTables;
schemaGenerated = true;
}
const tableNames = Object.keys(cachedTables);
logger_1.subscriptionLogger.info(`Discovered store tables: ${tableNames.join(', ')}`);
return {
typeDefs: (0, postgraphile_1.gql) `
extend type Query {
"""
Get Schema information for all store tables
"""
storeSchema: JSON
"""
Query data from specified store table
"""
storeData(table: String!): JSON
"""
Get list of all available store table names
"""
availableStoreTables: [String!]!
}
# Removed custom subscription types, now only use PostGraphile's built-in listen subscriptions
`,
resolvers: {
Query: {
storeSchema: async (root, args, context, _info) => {
const { pgClient } = context;
try {
const tables = await discoverStoreTables(pgClient);
return {
tables,
generatedAt: new Date().toISOString()
};
}
catch (error) {
return {
error: error.message,
tables: {}
};
}
},
storeData: async (root, args, context, _info) => {
return await executeTableQuery(context, args.table);
},
availableStoreTables: async (root, args, context, _info) => {
const { pgClient } = context;
try {
const tables = await discoverStoreTables(pgClient);
return Object.keys(tables);
}
catch (error) {
logger_1.subscriptionLogger.error('Failed to get available table list', error);
return [];
}
}
}
}
};
});
}
// Default plugin export (for backward compatibility)
exports.UniversalSubscriptionsPlugin = createUniversalSubscriptionsPlugin();
// =========================
// Database query functions
// =========================
/**
* Get detailed table information (columns, primary keys, data statistics, etc.)
*/
async function getTableInfo(pgClient, fullTableName) {
const tableName = fullTableName.replace(/^store_/, '');
// 1. Get column information
const columnsResult = await pgClient.query(`
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position
`, [fullTableName]);
// 2. Get primary key information
const primaryKeysResult = await pgClient.query(`
SELECT column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = $1
AND tc.constraint_type = 'PRIMARY KEY'
ORDER BY kcu.ordinal_position
`, [fullTableName]);
// 3. Try to get primary key information from table_fields table (if exists)
let tableFieldsKeys = [];
try {
const tableFieldsResult = await pgClient.query(`
SELECT field_name
FROM table_fields
WHERE table_name = $1 AND is_key = true
ORDER BY field_name
`, [tableName]);
tableFieldsKeys = tableFieldsResult.rows.map((row) => row.field_name);
}
catch (_e) {
// table_fields table may not exist, ignore error
}
// 4. Get data statistics
const statsResult = await pgClient.query(`
SELECT count(*) as row_count
FROM ${fullTableName}
`);
// 5. Get table size information
const sizeResult = await pgClient.query(`
SELECT
pg_size_pretty(pg_total_relation_size($1)) as total_size,
pg_size_pretty(pg_relation_size($1)) as table_size
`, [fullTableName]);
const columns = columnsResult.rows.map((row) => ({
columnName: row.column_name,
dataType: row.data_type,
isNullable: row.is_nullable === 'YES',
defaultValue: row.column_default,
maxLength: row.character_maximum_length,
precision: row.numeric_precision,
scale: row.numeric_scale
}));
const primaryKeys = primaryKeysResult.rows.map((row) => row.column_name);
return {
tableName,
fullTableName,
columns,
primaryKeys: primaryKeys.length > 0 ? primaryKeys : tableFieldsKeys,
statistics: {
rowCount: parseInt(statsResult.rows[0]?.row_count || '0'),
totalSize: sizeResult.rows[0]?.total_size || 'unknown',
tableSize: sizeResult.rows[0]?.table_size || 'unknown'
},
generatedAt: new Date().toISOString()
};
}
/**
* Dynamically execute table queries
*/
async function executeTableQuery(context, tableName) {
const { pgClient } = context;
const fullTableName = `store_${tableName}`;
try {
logger_1.subscriptionLogger.debug(`Executing table query: ${fullTableName}`);
// 1. Get table information
const tableInfo = cachedTables[tableName] || (await getTableInfo(pgClient, fullTableName));
if (tableInfo.columns.length === 0) {
return {
nodes: [],
totalCount: 0,
tableName,
generatedAt: new Date().toISOString()
};
}
// 2. Build dynamic nodeId expression
const nodeIdExpression = buildNodeIdExpression(tableInfo);
// 3. Build query fields
const columnFields = tableInfo.columns
.map((col) => `'${col.columnName}', ${col.columnName}`)
.join(', ');
// 4. Build WHERE condition
const whereCondition = buildWhereCondition(tableInfo);
// 5. Execute query
const sql = `
SELECT
COALESCE(
json_agg(
json_build_object(
'nodeId', ${nodeIdExpression},
${columnFields}
)
),
'[]'::json
) as nodes,
count(*) as total_count
FROM ${fullTableName}
WHERE ${whereCondition}
`;
logger_1.subscriptionLogger.debug(`Executing SQL: ${sql}`);
const result = await pgClient.query(sql);
const row = result.rows[0];
const data = {
nodes: row?.nodes || [],
totalCount: parseInt(row?.total_count || '0'),
tableName,
generatedAt: new Date().toISOString()
};
logger_1.subscriptionLogger.debug(`Query result: ${fullTableName} found ${data.totalCount} records`);
return data;
}
catch (error) {
logger_1.subscriptionLogger.error(`Failed to query ${fullTableName}`, error);
return {
nodes: [],
totalCount: 0,
tableName,
generatedAt: new Date().toISOString(),
error: error.message
};
}
}
/**
* Dynamically build NodeId expression
*/
function buildNodeIdExpression(tableInfo) {
const { tableName, primaryKeys, columns } = tableInfo;
if (primaryKeys.length > 0) {
// Use primary keys to build nodeId
const keyExpression = primaryKeys
.map((key) => `COALESCE(${key}::text, 'null')`)
.join(" || ':' || ");
return `encode(('${tableName}:' || ${keyExpression})::bytea, 'base64')`;
}
// If no primary key, use first column
const firstColumn = columns[0]?.columnName || 'unknown';
return `encode(('${tableName}:' || COALESCE(${firstColumn}::text, 'unknown'))::bytea, 'base64')`;
}
/**
* Dynamically build WHERE condition - completely generic, no hardcoded field names
*/
function buildWhereCondition(tableInfo) {
const { primaryKeys, columns } = tableInfo;
// 1. Prioritize primary key fields as filter conditions (most reliable)
if (primaryKeys.length > 0) {
const conditions = primaryKeys.map((key) => `${key} IS NOT NULL`);
return conditions.join(' AND ');
}
// 2. If no primary key, find first non-null field (reduce empty data)
const nonNullableColumns = columns.filter((col) => !col.isNullable);
if (nonNullableColumns.length > 0) {
return `${nonNullableColumns[0].columnName} IS NOT NULL`;
}
// 3. If all fields can be null, use first field for basic filtering
if (columns.length > 0) {
return `${columns[0].columnName} IS NOT NULL`;
}
// 4. Final fallback - return all rows (no filtering)
return 'true';
}
// Removed getLatestInsertedData function, now only use listen subscriptions
// Removed getLatestInsertedDataSince function, now only use simple listen subscriptions
//# sourceMappingURL=universal-subscriptions.js.map