supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
642 lines • 26.5 kB
JavaScript
"use strict";
/**
* Schema Introspection System v2.2.0
* Dynamically discovers database structure, constraints, and relationships
* Now includes deep PostgreSQL constraint discovery and business logic parsing
* Part of the constraint-aware architecture evolution
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaIntrospector = void 0;
const logger_1 = require("../core/utils/logger");
const constraint_discovery_engine_1 = require("../features/analysis/constraint-discovery-engine");
class SchemaIntrospector {
constructor(client) {
this.cache = new Map();
this.introspectionCache = null;
this.client = client;
this.constraintEngine = new constraint_discovery_engine_1.ConstraintDiscoveryEngine(client);
}
/**
* Perform comprehensive schema introspection
*/
async introspectSchema() {
if (this.introspectionCache) {
return this.introspectionCache;
}
logger_1.Logger.info('🔍 Starting comprehensive schema introspection...');
const result = {
tables: [],
relationships: [],
patterns: [],
constraints: {
userCreationConstraints: [],
dataIntegrityRules: [],
businessLogicConstraints: []
},
framework: {
type: 'custom',
version: 'unknown',
confidence: 0,
evidence: []
},
recommendations: []
};
try {
// Step 1: Discover all tables
result.tables = await this.discoverTables();
logger_1.Logger.debug(`Discovered ${result.tables.length} tables`);
// Step 2: Analyze relationships
result.relationships = await this.analyzeRelationships(result.tables);
logger_1.Logger.debug(`Found ${result.relationships.length} relationships`);
// Step 3: Identify table patterns and roles
result.patterns = await this.identifyTablePatterns(result.tables, result.relationships);
logger_1.Logger.debug(`Identified ${result.patterns.length} table patterns`);
// Step 4: Extract legacy constraints (maintained for compatibility)
result.constraints = await this.extractConstraints(result.tables, result.relationships);
logger_1.Logger.debug(`Extracted legacy constraints: ${Object.values(result.constraints).flat().length} total`);
// Step 4.5: v2.2.0 Deep constraint discovery
try {
const tableNames = result.tables.map(t => t.name);
const discoveredConstraints = await this.constraintEngine.discoverConstraints(tableNames);
result.constraints.discoveredConstraints = discoveredConstraints;
// Merge discovered business rules into legacy format for compatibility
const discoveredRules = discoveredConstraints.businessRules.map(rule => this.convertEngineRuleToLegacy(rule));
result.constraints.businessLogicConstraints.push(...discoveredRules);
logger_1.Logger.success(`✅ v2.2.0 Deep constraint discovery: ${discoveredConstraints.businessRules.length} business rules found`);
}
catch (error) {
logger_1.Logger.warn(`v2.2.0 constraint discovery failed: ${error.message}`);
// Continue without deep discovery - maintain backward compatibility
}
// Step 5: Detect framework
result.framework = await this.detectFramework(result.tables, result.patterns);
logger_1.Logger.debug(`Framework detected: ${result.framework.type} (confidence: ${result.framework.confidence})`);
// Step 6: Generate recommendations
result.recommendations = await this.generateRecommendations(result);
logger_1.Logger.debug(`Generated ${result.recommendations.length} recommendations`);
this.introspectionCache = result;
logger_1.Logger.success('✅ Schema introspection completed');
return result;
}
catch (error) {
logger_1.Logger.error('Schema introspection failed:', error);
throw new Error(`Schema introspection failed: ${error.message}`);
}
}
/**
* Discover all tables with their columns, constraints, and metadata
*/
async discoverTables() {
const tables = [];
// Get basic table information
const { data: tableData, error: tableError } = await this.client
.from('information_schema.tables')
.select('table_name, table_schema')
.eq('table_schema', 'public')
.eq('table_type', 'BASE TABLE');
if (tableError) {
logger_1.Logger.warn('Could not query information_schema, falling back to table discovery');
return this.fallbackTableDiscovery();
}
for (const table of tableData || []) {
const tableName = table.table_name;
try {
const tableInfo = {
name: tableName,
schema: table.table_schema,
columns: await this.getTableColumns(tableName),
constraints: await this.getTableConstraints(tableName),
indexes: await this.getTableIndexes(tableName),
triggers: await this.getTableTriggers(tableName),
rowCount: await this.getTableRowCount(tableName),
hasData: false
};
tableInfo.hasData = tableInfo.rowCount > 0;
tables.push(tableInfo);
}
catch (error) {
logger_1.Logger.warn(`Failed to introspect table ${tableName}: ${error.message}`);
// Continue with other tables
}
}
return tables;
}
/**
* Fallback table discovery when information_schema is not accessible
*/
async fallbackTableDiscovery() {
const commonTables = [
'profiles', 'accounts', 'users', 'setups', 'posts', 'categories',
'teams', 'organizations', 'memberships', 'subscriptions', 'roles',
'invitations', 'notifications', 'media_attachments', 'gear_items',
'base_templates', 'reviews', 'trips', 'modifications'
];
const tables = [];
for (const tableName of commonTables) {
if (await this.tableExists(tableName)) {
try {
const tableInfo = {
name: tableName,
schema: 'public',
columns: await this.getTableColumns(tableName),
constraints: [], // Limited info in fallback mode
indexes: [],
triggers: [],
rowCount: await this.getTableRowCount(tableName),
hasData: false
};
tableInfo.hasData = tableInfo.rowCount > 0;
tables.push(tableInfo);
}
catch (error) {
logger_1.Logger.debug(`Fallback discovery failed for ${tableName}: ${error.message}`);
}
}
}
return tables;
}
/**
* Get detailed column information for a table
*/
async getTableColumns(tableName) {
const { data, error } = await this.client
.from('information_schema.columns')
.select(`
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
`)
.eq('table_name', tableName)
.eq('table_schema', 'public')
.order('ordinal_position');
if (error) {
// Fallback: try to get columns by querying the table
return this.fallbackGetColumns(tableName);
}
const columns = [];
for (const col of data || []) {
const column = {
name: col.column_name,
type: col.data_type,
isNullable: col.is_nullable === 'YES',
defaultValue: col.column_default,
isPrimaryKey: false, // Will be set when we analyze constraints
isForeignKey: false,
maxLength: col.character_maximum_length
};
// Handle enum types
if (col.data_type === 'USER-DEFINED') {
column.enumValues = await this.getEnumValues(tableName, col.column_name);
}
columns.push(column);
}
return columns;
}
/**
* Fallback method to get column info when information_schema is not available
*/
async fallbackGetColumns(tableName) {
try {
// Try to do a select with limit 0 to get column info from error or metadata
const { error } = await this.client
.from(tableName)
.select('*')
.limit(0);
// For now, return minimal column info
// In a full implementation, we could parse error messages or use other methods
return [
{
name: 'id',
type: 'uuid',
isNullable: false,
defaultValue: null,
isPrimaryKey: true,
isForeignKey: false
}
];
}
catch (error) {
logger_1.Logger.debug(`Fallback column discovery failed for ${tableName}: ${error.message}`);
return [];
}
}
/**
* Get table constraints (primary keys, foreign keys, checks, etc.)
*/
async getTableConstraints(tableName) {
// This would query information_schema.table_constraints and related tables
// Implementation details depend on PostgreSQL system catalogs
const constraints = [];
try {
// Get constraint information from information_schema
const { data, error } = await this.client
.from('information_schema.table_constraints')
.select('constraint_name, constraint_type')
.eq('table_name', tableName)
.eq('table_schema', 'public');
if (!error && data) {
for (const constraint of data) {
// Get detailed constraint info (columns, references, etc.)
const constraintDetails = await this.getConstraintDetails(constraint.constraint_name, tableName);
if (constraintDetails) {
constraints.push(constraintDetails);
}
}
}
}
catch (error) {
logger_1.Logger.debug(`Constraint discovery failed for ${tableName}: ${error.message}`);
}
return constraints;
}
/**
* Get detailed information about a specific constraint
*/
async getConstraintDetails(constraintName, tableName) {
// Implementation would query information_schema.key_column_usage, referential_constraints, etc.
// For now, return a basic structure
return {
name: constraintName,
type: 'PRIMARY KEY', // Would be determined from actual query
columns: ['id'], // Would be determined from actual query
isDeferrable: false
};
}
/**
* Analyze relationships between tables
*/
async analyzeRelationships(tables) {
const relationships = [];
for (const table of tables) {
for (const constraint of table.constraints) {
if (constraint.type === 'FOREIGN KEY' && constraint.referencedTable) {
const relationship = {
fromTable: table.name,
fromColumn: constraint.columns[0],
toTable: constraint.referencedTable,
toColumn: constraint.referencedColumns?.[0] || 'id',
relationshipType: this.determineRelationshipType(table.name, constraint),
cascadeDelete: constraint.onDelete === 'CASCADE',
isRequired: !table.columns.find(col => col.name === constraint.columns[0])?.isNullable
};
relationships.push(relationship);
}
}
}
return relationships;
}
/**
* Identify table patterns and their likely roles in the application
*/
async identifyTablePatterns(tables, relationships) {
const patterns = [];
for (const table of tables) {
const pattern = await this.analyzeTablePattern(table, relationships);
if (pattern) {
patterns.push(pattern);
}
}
return patterns;
}
/**
* Analyze a single table to determine its pattern and role
*/
async analyzeTablePattern(table, relationships) {
const columnNames = table.columns.map(col => col.name.toLowerCase());
const evidence = [];
let confidence = 0;
let suggestedRole = 'system';
const columnMappings = {};
// Analyze column patterns to determine table role
// User table patterns
if (this.hasUserTableColumns(columnNames)) {
suggestedRole = 'user';
confidence += 30;
evidence.push('Has user-like columns (email, name, etc.)');
columnMappings.email = this.findColumnVariants(columnNames, ['email', 'email_address', 'user_email']);
columnMappings.name = this.findColumnVariants(columnNames, ['name', 'display_name', 'full_name', 'username']);
columnMappings.avatar = this.findColumnVariants(columnNames, ['avatar_url', 'picture_url', 'profile_image', 'image_url']);
columnMappings.bio = this.findColumnVariants(columnNames, ['bio', 'about', 'description']);
}
// Content table patterns
if (this.hasContentTableColumns(columnNames)) {
suggestedRole = 'content';
confidence += 25;
evidence.push('Has content-like columns (title, description, etc.)');
columnMappings.title = this.findColumnVariants(columnNames, ['title', 'name', 'subject']);
columnMappings.content = this.findColumnVariants(columnNames, ['content', 'body', 'description', 'text']);
columnMappings.author = this.findColumnVariants(columnNames, ['user_id', 'author_id', 'creator_id', 'account_id']);
}
// Association table patterns
if (this.hasAssociationTableColumns(columnNames, relationships)) {
suggestedRole = 'association';
confidence += 20;
evidence.push('Has association-like structure (multiple foreign keys)');
}
// MakerKit-specific patterns
if (this.hasMakerKitColumns(columnNames)) {
confidence += 15;
evidence.push('Has MakerKit-specific columns');
}
// Auth table patterns
if (table.name.includes('auth') || table.schema === 'auth') {
suggestedRole = 'auth';
confidence += 40;
evidence.push('Located in auth schema or has auth-related name');
}
if (confidence < 10) {
return null; // Not confident enough in the pattern
}
return {
name: table.name,
confidence: Math.min(confidence / 100, 1),
evidence,
suggestedRole,
columnMappings: this.cleanColumnMappings(columnMappings)
};
}
/**
* Helper methods for pattern recognition
*/
hasUserTableColumns(columns) {
const userColumns = ['email', 'name', 'username', 'display_name', 'full_name'];
return userColumns.some(col => columns.includes(col));
}
hasContentTableColumns(columns) {
const contentColumns = ['title', 'content', 'body', 'description'];
return contentColumns.some(col => columns.includes(col));
}
hasAssociationTableColumns(columns, relationships) {
const tableRelationships = relationships.filter(rel => rel.fromTable === columns[0]);
return tableRelationships.length >= 2; // Has multiple foreign keys
}
hasMakerKitColumns(columns) {
const makerkitColumns = ['primary_owner_user_id', 'is_personal_account', 'slug'];
return makerkitColumns.some(col => columns.includes(col));
}
findColumnVariants(columns, variants) {
return variants.filter(variant => columns.includes(variant));
}
cleanColumnMappings(mappings) {
const cleaned = {};
for (const [key, values] of Object.entries(mappings)) {
if (values.length > 0) {
cleaned[key] = values;
}
}
return cleaned;
}
/**
* Extract business constraints and rules from the schema
*/
async extractConstraints(tables, relationships) {
const userCreationConstraints = [];
const dataIntegrityRules = [];
const businessLogicConstraints = [];
// Analyze each table for constraint patterns
for (const table of tables) {
// Look for user creation constraints
if (table.name === 'profiles' || table.name === 'accounts') {
const constraints = await this.analyzeUserCreationConstraints(table, relationships);
userCreationConstraints.push(...constraints);
}
// Look for data integrity rules
const integrityRules = await this.analyzeDataIntegrityRules(table);
dataIntegrityRules.push(...integrityRules);
// Look for business logic constraints
const businessRules = await this.analyzeBusinessLogicConstraints(table);
businessLogicConstraints.push(...businessRules);
}
return {
userCreationConstraints,
dataIntegrityRules,
businessLogicConstraints
};
}
/**
* Detect framework type and version based on schema patterns
*/
async detectFramework(tables, patterns) {
const evidence = [];
let type = 'custom';
let version = 'unknown';
let confidence = 0;
const tableNames = tables.map(t => t.name);
// MakerKit detection
if (tableNames.includes('accounts') && tableNames.includes('memberships')) {
type = 'makerkit';
confidence += 40;
evidence.push('Has accounts and memberships tables');
// Version detection based on table patterns
if (tableNames.includes('role_permissions') && tableNames.includes('billing_customers')) {
version = 'v3';
confidence += 20;
evidence.push('Has v3-specific tables (role_permissions, billing_customers)');
}
else if (tableNames.includes('subscriptions') && tableNames.includes('roles')) {
version = 'v2';
confidence += 15;
evidence.push('Has v2-specific tables (subscriptions, roles)');
}
else {
version = 'v1';
confidence += 10;
evidence.push('Basic MakerKit pattern detected');
}
}
// Check for MakerKit-specific column patterns
const accountsTable = tables.find(t => t.name === 'accounts');
if (accountsTable) {
const accountsColumns = accountsTable.columns.map(c => c.name);
if (accountsColumns.includes('primary_owner_user_id')) {
confidence += 15;
evidence.push('Has MakerKit-specific account structure');
}
}
return {
type,
version,
confidence: Math.min(confidence / 100, 1),
evidence
};
}
/**
* Generate recommendations based on introspection results
*/
async generateRecommendations(result) {
const recommendations = [];
// Analyze patterns for recommendations
const userTables = result.patterns.filter(p => p.suggestedRole === 'user');
if (userTables.length === 0) {
recommendations.push({
type: 'warning',
message: 'No user tables detected. User creation may fail.',
priority: 'high',
suggestedAction: 'Ensure you have a profiles, accounts, or users table'
});
}
else if (userTables.length > 1) {
recommendations.push({
type: 'configuration',
message: 'Multiple user tables detected. Specify primary user table in config.',
priority: 'medium',
suggestedAction: 'Set schema.primaryUserTable in your configuration'
});
}
// Check for missing relationships
const orphanTables = result.tables.filter(table => !result.relationships.some(rel => rel.fromTable === table.name || rel.toTable === table.name));
if (orphanTables.length > 0) {
recommendations.push({
type: 'optimization',
message: `${orphanTables.length} tables have no relationships. This may indicate isolated data.`,
priority: 'low',
suggestedAction: 'Review table relationships for proper data seeding'
});
}
return recommendations;
}
/**
* Helper methods for constraint analysis
*/
async analyzeUserCreationConstraints(table, relationships) {
const constraints = [];
// Look for CHECK constraints that might affect user creation
for (const constraint of table.constraints) {
if (constraint.type === 'CHECK' && constraint.checkDefinition) {
constraints.push({
table: table.name,
rule: constraint.name,
type: 'conditional_insert',
description: `Check constraint: ${constraint.checkDefinition}`,
sqlCondition: constraint.checkDefinition,
requiresValidation: true
});
}
}
return constraints;
}
async analyzeDataIntegrityRules(table) {
const rules = [];
// Analyze NOT NULL constraints
const requiredColumns = table.columns.filter(col => !col.isNullable && !col.isPrimaryKey);
for (const col of requiredColumns) {
rules.push({
table: table.name,
rule: `${col.name}_required`,
type: 'required_relationship',
description: `Column ${col.name} is required`,
sqlCondition: `${col.name} IS NOT NULL`,
requiresValidation: true
});
}
return rules;
}
async analyzeBusinessLogicConstraints(table) {
const constraints = [];
// Look for business logic patterns in triggers
for (const trigger of table.triggers) {
constraints.push({
table: table.name,
rule: trigger.name,
type: 'business_rule',
description: `Trigger ${trigger.name} enforces business logic`,
sqlCondition: `-- Trigger: ${trigger.functionName}`,
requiresValidation: false
});
}
return constraints;
}
/**
* Utility methods
*/
async tableExists(tableName) {
try {
const { error } = await this.client.from(tableName).select('*').limit(1);
return !error;
}
catch {
return false;
}
}
async getTableRowCount(tableName) {
try {
const { count, error } = await this.client
.from(tableName)
.select('*', { count: 'exact', head: true });
return error ? 0 : (count || 0);
}
catch {
return 0;
}
}
async getTableIndexes(tableName) {
// Would query pg_indexes or information_schema.statistics
return [];
}
async getTableTriggers(tableName) {
// Would query information_schema.triggers
return [];
}
async getEnumValues(tableName, columnName) {
// Would query pg_enum for enum values
return [];
}
determineRelationshipType(tableName, constraint) {
// Simple heuristic - could be more sophisticated
if (constraint.columns.length === 1 && constraint.referencedColumns?.length === 1) {
return 'one_to_many';
}
return 'many_to_many';
}
/**
* v2.2.0: Convert engine business rule to legacy format for compatibility
*/
convertEngineRuleToLegacy(rule) {
return {
table: rule.table,
rule: rule.name,
type: this.mapEngineRuleType(rule.type),
description: rule.errorMessage || rule.condition,
sqlCondition: rule.sqlPattern,
requiresValidation: rule.action !== 'allow'
};
}
/**
* v2.2.0: Map engine rule types to legacy types
*/
mapEngineRuleType(type) {
switch (type) {
case 'validation': return 'conditional_insert';
case 'dependency': return 'required_relationship';
case 'business_logic': return 'business_rule';
case 'transformation': return 'value_constraint';
default: return 'business_rule';
}
}
/**
* v2.2.0: Access discovered constraints metadata
*/
getDiscoveredConstraints() {
return this.introspectionCache?.constraints?.discoveredConstraints || null;
}
/**
* Clear the introspection cache
*/
clearCache() {
this.introspectionCache = null;
this.cache.clear();
this.constraintEngine.clearCache(); // v2.2.0: Clear constraint engine cache
}
/**
* Get cached introspection result
*/
getCachedResult() {
return this.introspectionCache;
}
}
exports.SchemaIntrospector = SchemaIntrospector;
//# sourceMappingURL=schema-introspector.js.map