supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
536 lines • 21.4 kB
JavaScript
;
/**
* Dynamic Column Mapping System
* Automatically discovers and maps semantic fields to actual database columns
* Replaces hardcoded column assumptions with intelligent field detection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DynamicColumnMapper = void 0;
const logger_1 = require("../core/utils/logger");
class DynamicColumnMapper {
constructor(config = this.getDefaultConfig()) {
this.semanticFields = new Map();
this.config = config;
this.initializeSemanticFields();
}
/**
* Create column mappings for all tables in the schema
*/
async createMappings(schemaInfo) {
logger_1.Logger.info('🗺️ Creating dynamic column mappings...');
const mappings = new Map();
for (const pattern of schemaInfo.patterns) {
const tableMap = await this.createTableMapping(pattern, schemaInfo);
mappings.set(pattern.name, tableMap);
logger_1.Logger.debug(`Created mapping for ${pattern.name}: ${tableMap.mappings.length} mappings, confidence ${(tableMap.confidence * 100).toFixed(1)}%`);
}
logger_1.Logger.success(`✅ Created column mappings for ${mappings.size} tables`);
return mappings;
}
/**
* Create column mapping for a specific table
*/
async createTableMapping(tablePattern, schemaInfo) {
const table = schemaInfo.tables.find(t => t.name === tablePattern.name);
if (!table) {
throw new Error(`Table ${tablePattern.name} not found in schema`);
}
const mappings = [];
const unmappedColumns = [...table.columns.map(c => c.name)];
const unmappedSemanticFields = [];
// Get semantic fields for this table type
const relevantSemanticFields = this.getSemanticFieldsForTable(tablePattern.suggestedRole);
// Apply custom mappings first (highest priority)
if (this.config.customMappings?.[table.name]) {
const customMappings = this.config.customMappings[table.name];
for (const [semanticField, actualColumn] of Object.entries(customMappings)) {
if (table.columns.find(c => c.name === actualColumn)) {
const mapping = this.createCustomMapping(semanticField, actualColumn, table.columns);
mappings.push(mapping);
this.removeFromArray(unmappedColumns, actualColumn);
this.removeFromArray(unmappedSemanticFields, semanticField);
}
}
}
// Create mappings for each semantic field
for (const semanticField of relevantSemanticFields) {
if (mappings.find(m => m.semanticField === semanticField.name)) {
continue; // Already mapped via custom mapping
}
const mapping = await this.findBestColumnMapping(semanticField, table.columns, unmappedColumns);
if (mapping) {
mappings.push(mapping);
this.removeFromArray(unmappedColumns, mapping.actualColumn);
}
else {
unmappedSemanticFields.push(semanticField.name);
}
}
// Calculate overall confidence
const confidence = this.calculateTableConfidence(mappings, relevantSemanticFields.length);
// Generate recommendations
const recommendations = this.generateRecommendations(table.name, mappings, unmappedColumns, unmappedSemanticFields, relevantSemanticFields);
return {
tableName: table.name,
mappings,
unmappedColumns,
unmappedSemanticFields,
confidence,
recommendations
};
}
/**
* Find the best column mapping for a semantic field
*/
async findBestColumnMapping(semanticField, columns, availableColumns) {
const candidates = [];
// Only consider available columns
const availableColumnObjects = columns.filter(c => availableColumns.includes(c.name));
for (const column of availableColumnObjects) {
const result = this.scoreColumnMatch(semanticField, column);
if (result.confidence >= this.config.minimumConfidence) {
candidates.push({
column,
confidence: result.confidence,
evidence: result.evidence
});
}
}
if (candidates.length === 0) {
return null;
}
// Sort by confidence and take the best match
candidates.sort((a, b) => b.confidence - a.confidence);
const bestMatch = candidates[0];
// Get alternative columns (lower confidence matches)
const alternatives = candidates.slice(1, 4).map(c => c.column.name);
return {
semanticField: semanticField.name,
actualColumn: bestMatch.column.name,
confidence: bestMatch.confidence,
evidence: bestMatch.evidence,
dataTypeMatch: this.isDataTypeCompatible(semanticField.dataType, bestMatch.column.type),
constraintMatch: this.isConstraintCompatible(semanticField, bestMatch.column),
alternativeColumns: alternatives.length > 0 ? alternatives : undefined
};
}
/**
* Score how well a column matches a semantic field
*/
scoreColumnMatch(semanticField, column) {
let confidence = 0;
const evidence = [];
// Exact name match (highest score)
if (column.name.toLowerCase() === semanticField.name.toLowerCase()) {
confidence += 0.5;
evidence.push('Exact name match');
}
// Alias match
for (const alias of semanticField.aliases) {
if (column.name.toLowerCase() === alias.toLowerCase()) {
confidence += 0.4;
evidence.push(`Alias match: ${alias}`);
break;
}
}
// Pattern match
if (this.config.enablePatternMatching) {
for (const pattern of semanticField.patterns) {
if (pattern.test(column.name)) {
confidence += 0.3;
evidence.push(`Pattern match: ${pattern.source}`);
break;
}
}
}
// Fuzzy match
if (this.config.enableFuzzyMatching) {
const fuzzyScore = this.calculateFuzzyMatch(semanticField.name, column.name);
if (fuzzyScore > 0.7) {
confidence += fuzzyScore * 0.2;
evidence.push(`Fuzzy match: ${(fuzzyScore * 100).toFixed(1)}%`);
}
}
// Data type compatibility
if (this.isDataTypeCompatible(semanticField.dataType, column.type)) {
confidence += 0.15;
evidence.push('Data type compatible');
}
// Constraint compatibility
if (this.isConstraintCompatible(semanticField, column)) {
confidence += 0.1;
evidence.push('Constraint compatible');
}
// Required field match
if (semanticField.required === !column.isNullable) {
confidence += 0.05;
evidence.push('Required constraint match');
}
return { confidence, evidence };
}
/**
* Check if data types are compatible
*/
isDataTypeCompatible(semanticType, columnType) {
const compatibilityMap = {
'string': ['text', 'varchar', 'character varying', 'char', 'character'],
'number': ['integer', 'bigint', 'smallint', 'decimal', 'numeric', 'real', 'double precision'],
'boolean': ['boolean', 'bool'],
'date': ['timestamp', 'timestamptz', 'date', 'time', 'timetz'],
'uuid': ['uuid'],
'email': ['text', 'varchar', 'character varying'],
'url': ['text', 'varchar', 'character varying'],
'json': ['json', 'jsonb']
};
const compatibleTypes = compatibilityMap[semanticType] || [];
return compatibleTypes.some(type => columnType.toLowerCase().includes(type.toLowerCase()));
}
/**
* Check if constraints are compatible
*/
isConstraintCompatible(semanticField, column) {
// Check if required field has NOT NULL constraint
if (semanticField.required && column.isNullable) {
return false;
}
// Check if email field has unique constraint (common pattern)
if (semanticField.dataType === 'email' && !column.isForeignKey) {
// Assume email fields should be unique (not a hard requirement)
return true;
}
return true;
}
/**
* Calculate fuzzy string match score
*/
calculateFuzzyMatch(str1, str2) {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
// Simple Levenshtein distance-based similarity
const distance = this.levenshteinDistance(s1, s2);
const maxLength = Math.max(s1.length, s2.length);
if (maxLength === 0)
return 1;
return 1 - (distance / maxLength);
}
/**
* Calculate Levenshtein distance between two strings
*/
levenshteinDistance(str1, str2) {
const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
for (let i = 0; i <= str1.length; i++)
matrix[0][i] = i;
for (let j = 0; j <= str2.length; j++)
matrix[j][0] = j;
for (let j = 1; j <= str2.length; j++) {
for (let i = 1; i <= str1.length; i++) {
const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(matrix[j][i - 1] + 1, // deletion
matrix[j - 1][i] + 1, // insertion
matrix[j - 1][i - 1] + indicator // substitution
);
}
}
return matrix[str2.length][str1.length];
}
/**
* Create a custom mapping with high confidence
*/
createCustomMapping(semanticField, actualColumn, columns) {
const column = columns.find(c => c.name === actualColumn);
return {
semanticField,
actualColumn,
confidence: 1.0,
evidence: ['Custom mapping configuration'],
dataTypeMatch: true, // Assume custom mappings are correct
constraintMatch: true
};
}
/**
* Calculate overall confidence for a table mapping
*/
calculateTableConfidence(mappings, totalSemanticFields) {
if (totalSemanticFields === 0)
return 1;
const totalConfidence = mappings.reduce((sum, mapping) => sum + mapping.confidence, 0);
const coverageRatio = mappings.length / totalSemanticFields;
return (totalConfidence / totalSemanticFields) * coverageRatio;
}
/**
* Generate recommendations for improving mappings
*/
generateRecommendations(tableName, mappings, unmappedColumns, unmappedSemanticFields, semanticFields) {
const recommendations = [];
// Low confidence mappings
for (const mapping of mappings) {
if (mapping.confidence < 0.7) {
recommendations.push({
type: 'use_alternative',
message: `Low confidence mapping for ${mapping.semanticField}. Consider using ${mapping.alternativeColumns?.[0] || 'a different column'}.`,
semanticField: mapping.semanticField,
currentColumn: mapping.actualColumn,
suggestedColumn: mapping.alternativeColumns?.[0],
priority: 'medium',
impact: 'May cause data seeding issues if mapping is incorrect'
});
}
}
// Important unmapped semantic fields
for (const fieldName of unmappedSemanticFields) {
const semanticField = semanticFields.find(f => f.name === fieldName);
if (semanticField?.required) {
recommendations.push({
type: 'add_column',
message: `Required field ${fieldName} could not be mapped. Consider adding a column or updating configuration.`,
semanticField: fieldName,
priority: 'high',
impact: 'User creation will fail without this field'
});
}
}
// Suggest column renames for close matches
for (const columnName of unmappedColumns) {
for (const fieldName of unmappedSemanticFields) {
const fuzzyScore = this.calculateFuzzyMatch(fieldName, columnName);
if (fuzzyScore > 0.8) {
recommendations.push({
type: 'column_rename',
message: `Column ${columnName} is similar to semantic field ${fieldName}. Consider renaming or adding to custom mappings.`,
semanticField: fieldName,
currentColumn: columnName,
priority: 'low',
impact: 'Could improve mapping confidence'
});
}
}
}
return recommendations;
}
/**
* Initialize semantic field definitions
*/
initializeSemanticFields() {
const userFields = [
{
name: 'id',
description: 'Primary key identifier',
aliases: ['user_id', 'account_id', 'profile_id'],
patterns: [/^.*_?id$/i],
dataType: 'uuid',
required: true,
category: 'identity'
},
{
name: 'email',
description: 'Email address',
aliases: ['email_address', 'user_email', 'mail'],
patterns: [/email/i, /mail/i],
dataType: 'email',
required: true,
category: 'identity',
validation: {
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
}
},
{
name: 'name',
description: 'Full name or display name',
aliases: ['full_name', 'display_name', 'username', 'first_name', 'last_name'],
patterns: [/name$/i, /^name/i],
dataType: 'string',
required: true,
category: 'identity',
validation: {
minLength: 1,
maxLength: 255
}
},
{
name: 'username',
description: 'Unique username or handle',
aliases: ['handle', 'user_name', 'login'],
patterns: [/username/i, /handle/i],
dataType: 'string',
required: false,
category: 'identity',
validation: {
minLength: 3,
maxLength: 50
}
},
{
name: 'avatar',
description: 'Profile picture URL',
aliases: ['avatar_url', 'picture_url', 'profile_image', 'image_url', 'photo_url'],
patterns: [/avatar/i, /picture/i, /image/i, /photo/i],
dataType: 'url',
required: false,
category: 'content'
},
{
name: 'bio',
description: 'Biography or about text',
aliases: ['about', 'description', 'profile_text', 'summary'],
patterns: [/bio$/i, /about/i, /description/i],
dataType: 'string',
required: false,
category: 'content',
validation: {
maxLength: 1000
}
},
{
name: 'created_at',
description: 'Creation timestamp',
aliases: ['date_created', 'created_date', 'created_time'],
patterns: [/created/i, /created_at/i],
dataType: 'date',
required: false,
category: 'metadata'
},
{
name: 'updated_at',
description: 'Last update timestamp',
aliases: ['date_updated', 'updated_date', 'modified_at', 'last_modified'],
patterns: [/updated/i, /modified/i],
dataType: 'date',
required: false,
category: 'metadata'
}
];
const contentFields = [
{
name: 'title',
description: 'Content title or name',
aliases: ['name', 'subject', 'heading'],
patterns: [/title/i, /subject/i],
dataType: 'string',
required: true,
category: 'content'
},
{
name: 'content',
description: 'Main content body',
aliases: ['body', 'text', 'description', 'details'],
patterns: [/content/i, /body/i, /text/i],
dataType: 'string',
required: false,
category: 'content'
},
{
name: 'author',
description: 'Content author reference',
aliases: ['user_id', 'creator_id', 'owner_id', 'account_id'],
patterns: [/author/i, /creator/i, /owner/i, /_id$/i],
dataType: 'uuid',
required: true,
category: 'relationship'
},
{
name: 'category',
description: 'Content category',
aliases: ['category_id', 'type', 'classification'],
patterns: [/category/i, /type/i],
dataType: 'string',
required: false,
category: 'metadata'
},
{
name: 'published',
description: 'Publication status',
aliases: ['is_published', 'published_at', 'is_public', 'status'],
patterns: [/published/i, /public/i, /status/i],
dataType: 'boolean',
required: false,
category: 'metadata'
}
];
this.semanticFields.set('user', userFields);
this.semanticFields.set('content', contentFields);
this.semanticFields.set('association', []);
this.semanticFields.set('system', []);
}
/**
* Get semantic fields for a table type
*/
getSemanticFieldsForTable(tableRole) {
return this.semanticFields.get(tableRole) || [];
}
/**
* Get default configuration
*/
getDefaultConfig() {
return {
strictMode: false,
enablePatternMatching: true,
enableFuzzyMatching: true,
minimumConfidence: 0.3,
preferExactMatches: true,
allowMultipleMappings: false
};
}
/**
* Utility method to remove item from array
*/
removeFromArray(array, item) {
const index = array.indexOf(item);
if (index > -1) {
array.splice(index, 1);
}
}
/**
* Get mapping for a specific table and semantic field
*/
getMapping(tableMappings, tableName, semanticField) {
const tableMap = tableMappings.get(tableName);
if (!tableMap)
return null;
const mapping = tableMap.mappings.find(m => m.semanticField === semanticField);
return mapping?.actualColumn || null;
}
/**
* Validate existing mappings against current schema
*/
async validateMappings(tableMappings, schemaInfo) {
const invalidMappings = [];
const missingTables = [];
for (const [tableName, tableMap] of tableMappings) {
const table = schemaInfo.tables.find(t => t.name === tableName);
if (!table) {
missingTables.push(tableName);
continue;
}
for (const mapping of tableMap.mappings) {
const column = table.columns.find(c => c.name === mapping.actualColumn);
if (!column) {
invalidMappings.push({
table: tableName,
field: mapping.semanticField,
reason: `Column ${mapping.actualColumn} no longer exists`
});
}
}
}
return {
valid: invalidMappings.length === 0 && missingTables.length === 0,
invalidMappings,
missingTables
};
}
/**
* Export mappings to configuration format
*/
exportMappings(tableMappings) {
const config = {};
for (const [tableName, tableMap] of tableMappings) {
config[tableName] = {};
for (const mapping of tableMap.mappings) {
config[tableName][mapping.semanticField] = mapping.actualColumn;
}
}
return config;
}
}
exports.DynamicColumnMapper = DynamicColumnMapper;
//# sourceMappingURL=dynamic-column-mapper.js.map