supa-seed
Version:
A constraint-aware, framework-agnostic database seeding framework with deep PostgreSQL business logic discovery and MakerKit integration support
1,108 lines (1,104 loc) • 59.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaAdapter = void 0;
const logger_1 = require("./utils/logger");
const enhanced_supabase_client_1 = require("./utils/enhanced-supabase-client");
class JWTAuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'JWTAuthenticationError';
}
}
class SchemaAdapter {
constructor(client, configOverride, supabaseUrl, supabaseKey) {
this.client = client;
this.supabaseKey = supabaseKey;
this.schemaInfo = null;
this.configOverride = configOverride;
// Store the URL for local environment detection
this.supabaseUrl = supabaseUrl || process.env.SUPABASE_URL || 'http://127.0.0.1:54321';
}
/**
* Check if this is a local Supabase environment
*/
isLocalSupabaseEnvironment() {
return (0, enhanced_supabase_client_1.isLocalSupabaseEnvironment)(this.supabaseUrl);
}
/**
* Detect the database schema and return comprehensive compatibility info
*/
async detectSchema() {
if (this.schemaInfo) {
return this.schemaInfo;
}
console.log('🔍 Detecting database schema...');
// Check if this is a local Supabase environment
const isLocalSupabase = this.isLocalSupabaseEnvironment();
if (isLocalSupabase) {
logger_1.Logger.debug('Local Supabase environment detected, using enhanced local detection patterns');
}
const schemaInfo = {
hasAccounts: false,
hasUsers: false,
hasProfiles: false,
hasSetups: false,
hasCategories: false,
hasTeams: false,
hasOrganizations: false,
accountsTableStructure: 'simple',
primaryUserTable: 'accounts',
// Enhanced detection fields
makerkitVersion: 'none',
customTables: [],
detectedRelationships: [],
assetCompatibility: {
supportsImages: false,
supportsMarkdown: false,
supportsJson: false,
contentTables: [],
userContentRelationships: [],
mediaStoragePattern: 'url_only'
},
frameworkType: 'custom'
};
// Check for core tables
schemaInfo.hasAccounts = await this.tableExists('accounts');
schemaInfo.hasProfiles = await this.tableExists('profiles');
schemaInfo.hasSetups = await this.tableExists('setups');
schemaInfo.hasCategories = await this.tableExists('categories');
schemaInfo.hasTeams = await this.tableExists('teams');
schemaInfo.hasOrganizations = await this.tableExists('organizations');
// Check for MakerKit-specific tables
const hasMemberships = await this.tableExists('memberships');
const hasSubscriptions = await this.tableExists('subscriptions');
const hasRoles = await this.tableExists('roles');
const hasInvitations = await this.tableExists('invitations');
const hasNotifications = await this.tableExists('notifications');
// Additional MakerKit v2/v3 tables mentioned in beta test bug report
const hasRolePermissions = await this.tableExists('role_permissions');
const hasBillingCustomers = await this.tableExists('billing_customers');
const hasGearItems = await this.tableExists('gear_items');
const hasReviews = await this.tableExists('reviews');
// Check for content and media tables
const hasPosts = await this.tableExists('posts');
const hasMediaAttachments = await this.tableExists('media_attachments');
const hasGear = await this.tableExists('gear');
const hasBaseTemplates = await this.tableExists('base_templates');
const hasTrips = await this.tableExists('trips');
const hasModifications = await this.tableExists('modifications');
// Detect custom tables (beyond standard MakerKit)
schemaInfo.customTables = await this.detectCustomTables([
'accounts', 'profiles', 'memberships', 'subscriptions', 'roles',
'invitations', 'notifications', 'posts', 'media_attachments'
]);
// Check if auth.users has records (standard Supabase pattern)
schemaInfo.hasUsers = await this.checkAuthUsers();
// Enhanced MakerKit version detection
schemaInfo.makerkitVersion = await this.detectMakerKitVersion({
hasAccounts: schemaInfo.hasAccounts,
hasMemberships,
hasSubscriptions,
hasRoles,
hasInvitations,
hasNotifications,
hasRolePermissions,
hasBillingCustomers,
hasGearItems,
hasReviews
});
// Determine account table structure with enhanced detection
if (schemaInfo.hasAccounts && hasMemberships) {
schemaInfo.accountsTableStructure = 'makerkit';
}
else if (schemaInfo.hasAccounts) {
const accountsStructure = await this.detectAccountsStructure();
schemaInfo.accountsTableStructure = accountsStructure;
}
// Detect framework type based on table patterns
schemaInfo.frameworkType = this.detectFrameworkTypeEnhanced({
makerkitVersion: schemaInfo.makerkitVersion,
hasGear,
hasBaseTemplates,
hasSetups: schemaInfo.hasSetups,
hasTrips,
customTables: schemaInfo.customTables
});
// Detect relationships for better association intelligence
schemaInfo.detectedRelationships = await this.detectTableRelationships();
// Analyze asset compatibility
schemaInfo.assetCompatibility = await this.analyzeAssetCompatibility({
hasMediaAttachments,
hasPosts,
hasSetups: schemaInfo.hasSetups,
customTables: schemaInfo.customTables
});
// Determine primary user table (prioritize config override)
schemaInfo.primaryUserTable = this.determinePrimaryUserTable(schemaInfo);
this.schemaInfo = schemaInfo;
console.log('📋 Enhanced schema detected:', {
primaryUserTable: schemaInfo.primaryUserTable,
structure: schemaInfo.accountsTableStructure,
makerkitVersion: schemaInfo.makerkitVersion,
frameworkType: schemaInfo.frameworkType,
customTables: schemaInfo.customTables.length,
relationships: schemaInfo.detectedRelationships.length,
assetCompatibility: {
images: schemaInfo.assetCompatibility.supportsImages,
markdown: schemaInfo.assetCompatibility.supportsMarkdown,
storage: schemaInfo.assetCompatibility.mediaStoragePattern
}
});
return schemaInfo;
}
/**
* Get the appropriate user creation strategy based on detected schema
*/
getUserCreationStrategy() {
if (!this.schemaInfo) {
throw new Error('Schema not detected. Call detectSchema() first.');
}
// Check config override first
if (this.configOverride?.schema?.primaryUserTable === 'accounts' ||
this.configOverride?.primaryUserTable === 'accounts') {
logger_1.Logger.debug('Using simple-accounts strategy due to config override');
return 'simple-accounts';
}
// Original logic with MakerKit detection improvements
if (this.schemaInfo.hasAccounts && this.schemaInfo.accountsTableStructure === 'makerkit') {
return 'makerkit-profiles';
}
else if (this.schemaInfo.hasProfiles && this.schemaInfo.hasTeams) {
return 'makerkit-profiles';
}
else if (this.schemaInfo.hasProfiles && this.schemaInfo.hasUsers) {
return 'custom-profiles';
}
else if (this.schemaInfo.hasAccounts) {
return 'simple-accounts';
}
else {
return 'custom-profiles';
}
}
/**
* Create a user using the appropriate strategy for the detected schema
*/
async createUserForSchema(userData) {
const strategy = this.getUserCreationStrategy();
try {
switch (strategy) {
case 'simple-accounts':
return await this.createSimpleAccountUser(userData);
case 'makerkit-profiles':
return await this.createMakerkitUser(userData);
case 'custom-profiles':
return await this.createProfilesUser(userData);
default:
throw new Error(`Unknown user creation strategy: ${strategy}`);
}
}
catch (error) {
console.error(`Failed to create user with strategy ${strategy}:`, error);
return { id: '', success: false, error: error.message };
}
}
/**
* Get the correct column name for a field, checking config first then what actually exists
*/
async getColumnMapping(tableName, expectedField, fallbacks = []) {
// First check if config override specifies a mapping
const configMapping = this.getConfigColumnMapping(tableName, expectedField);
if (configMapping && await this.columnExists(tableName, configMapping)) {
return configMapping;
}
// Fall back to checking all options
const allOptions = [expectedField, ...fallbacks];
for (const option of allOptions) {
if (await this.columnExists(tableName, option)) {
return option;
}
}
return null;
}
/**
* Get column mapping from configuration
*/
getConfigColumnMapping(tableName, expectedField) {
if (!this.configOverride?.schema)
return null;
// Map expected fields to config field names
const fieldMappings = {
// Profile/user table mappings
'email': 'emailField',
'picture_url': 'pictureField',
'avatar_url': 'pictureField',
'bio': 'bioField',
'username': 'usernameField',
'name': 'nameField',
'display_name': 'nameField',
// Base template mappings
'description': 'descriptionField',
'type': 'typeField',
'make': 'makeField',
'model': 'modelField',
'year': 'yearField'
};
const configFieldName = fieldMappings[expectedField];
if (!configFieldName)
return null;
// Check different table configurations
if (tableName === 'profiles' && this.configOverride.schema.userTable) {
return this.configOverride.schema.userTable[configFieldName] || null;
}
if (tableName === 'base_templates' && this.configOverride.schema.baseTemplateTable) {
return this.configOverride.schema.baseTemplateTable[configFieldName] || null;
}
return null;
}
async tableExists(tableName) {
try {
// For local Supabase environments, add extra timeout and retry logic
const isLocal = this.isLocalSupabaseEnvironment();
if (isLocal) {
// Local environments might be slower, provide better error details
const { error } = await this.client
.from(tableName)
.select('id')
.limit(1);
const exists = !error;
if (exists) {
logger_1.Logger.debug(`Table '${tableName}' found in local environment`);
}
else {
// Check for JWT authentication errors specifically
if (error?.message?.includes('JWSError') || error?.message?.includes('JWSInvalidSignature')) {
logger_1.Logger.debug(`JWT authentication error for table '${tableName}': ${error.message}`);
// Only throw on the first table to avoid spamming the user
if (tableName === 'accounts') {
const isServiceRole = this.supabaseKey && (0, enhanced_supabase_client_1.isServiceRoleKey)(this.supabaseKey);
const keyType = isServiceRole ? 'service role key' : 'authentication key';
throw new JWTAuthenticationError(`
${keyType} authentication failed in local environment.
This version of supa-seed includes enhanced JWT handling, but you're still experiencing authentication issues.
**Immediate workaround**: Use anon key instead:
1. Get anon key: supabase status | grep "anon key"
2. Retry: npx supa-seed detect --url "${this.supabaseUrl}" --key "[ANON_KEY]"
**Root cause**: JWT signature validation issue between service role keys and local Supabase environments.
Anon keys work perfectly and have sufficient permissions for all supa-seed operations.
Technical details: ${error.message}
`.trim());
}
return false;
}
logger_1.Logger.debug(`Table '${tableName}' not found in local environment: ${error?.message || 'Unknown error'}`);
}
return exists;
}
else {
// Cloud environments use standard detection
const { error } = await this.client.from(tableName).select('*').limit(1);
return !error;
}
}
catch (error) {
// Re-throw JWT authentication errors with helpful message
if (error instanceof JWTAuthenticationError) {
throw error;
}
logger_1.Logger.debug(`Table existence check failed for '${tableName}': ${error.message}`);
return false;
}
}
async checkAuthUsers() {
try {
// Try to access auth.users indirectly through a query that would work
// if users exist in the auth schema
const { data, error } = await this.client.auth.admin.listUsers({
page: 1,
perPage: 1
});
return !error;
}
catch {
return false;
}
}
async detectAccountsStructure() {
try {
// Test if the table has MakerKit-specific fields by trying to select them
const { error: primaryOwnerError } = await this.client
.from('accounts')
.select('primary_owner_user_id')
.limit(1);
if (!primaryOwnerError) {
logger_1.Logger.debug('Found primary_owner_user_id field - MakerKit pattern detected');
return 'makerkit';
}
// Test for other MakerKit indicators
const { error: slugError } = await this.client
.from('accounts')
.select('slug, is_personal_account')
.limit(1);
if (!slugError) {
logger_1.Logger.debug('Found slug field - MakerKit pattern detected');
return 'makerkit';
}
// Check for simple pattern with id field
const { error: idError } = await this.client
.from('accounts')
.select('id')
.limit(1);
if (!idError) {
logger_1.Logger.debug('Simple accounts pattern detected');
return 'simple';
}
return 'custom';
}
catch (error) {
logger_1.Logger.debug('Account structure detection failed:', error);
return 'custom';
}
}
determinePrimaryUserTable(schemaInfo) {
// First check if config override specifies primary user table
if (this.configOverride?.schema?.primaryUserTable) {
const configTable = this.configOverride.schema.primaryUserTable;
logger_1.Logger.debug(`Using config override for primary user table: ${configTable}`);
return configTable;
}
// Also check direct primaryUserTable property
if (this.configOverride?.primaryUserTable) {
const configTable = this.configOverride.primaryUserTable;
logger_1.Logger.debug(`Using config override for primary user table: ${configTable}`);
return configTable;
}
// Prioritize accounts table for MakerKit patterns
if (schemaInfo.hasAccounts && schemaInfo.accountsTableStructure === 'makerkit') {
logger_1.Logger.debug('Detected MakerKit pattern with accounts table, using accounts as primary');
return 'accounts';
}
// Fall back to original logic
if (schemaInfo.hasProfiles && schemaInfo.hasUsers) {
return 'profiles';
}
else if (schemaInfo.hasAccounts) {
return 'accounts';
}
else {
return 'users';
}
}
async createSimpleAccountUser(userData) {
// Original supa-seed logic - create auth user then account
const userId = userData.id || crypto.randomUUID();
// Create auth user
const { error: authError } = await this.client.auth.admin.createUser({
id: userId,
email: userData.email,
password: 'password123',
email_confirm: true,
user_metadata: {
full_name: userData.name,
username: userData.username,
}
});
if (authError) {
return { id: '', success: false, error: `Auth user creation failed: ${authError.message}` };
}
// Create account record with proper fields based on schema
const accountData = {
email: userData.email,
name: userData.name,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
// Add optional fields if provided - MakerKit accounts table structure
if (userData.bio || userData.username) {
// For MakerKit, additional data like bio and username go in public_data JSONB field
accountData.public_data = {};
if (userData.bio)
accountData.public_data.bio = userData.bio;
if (userData.username)
accountData.public_data.username = userData.username;
}
if (userData.picture_url)
accountData.picture_url = userData.picture_url;
// For MakerKit-style accounts, use primary_owner_user_id
if (this.schemaInfo?.accountsTableStructure === 'makerkit') {
accountData.primary_owner_user_id = userId;
accountData.is_personal_account = true;
// Don't set slug for personal accounts due to constraint
}
else {
// For simple accounts, use id field
accountData.id = userId;
}
// Check if account already exists before trying to create it
const { data: existingAccount } = await this.client
.from('accounts')
.select('email, id')
.eq('email', userData.email)
.single();
if (existingAccount) {
logger_1.Logger.debug(`Account for ${userData.email} already exists, using existing record`);
return { id: existingAccount.id, success: true };
}
const { error: accountError } = await this.client
.from('accounts')
.insert(accountData);
if (accountError) {
// If it's a duplicate key error, check if the record was actually created
if (accountError.code === '23505') {
const { data: createdAccount } = await this.client
.from('accounts')
.select('email, id')
.eq('email', userData.email)
.single();
if (createdAccount) {
logger_1.Logger.debug(`Account for ${userData.email} was created despite duplicate key error`);
return { id: createdAccount.id, success: true };
}
}
logger_1.Logger.debug('Account creation error:', accountError);
return { id: '', success: false, error: `Account creation failed: ${accountError.message}` };
}
logger_1.Logger.debug(`Successfully created account for ${userData.email} with ID ${userId}`);
return { id: userId, success: true };
}
async createMakerkitUser(userData) {
// Makerkit pattern: auth user + profile + account/team
const userId = userData.id || crypto.randomUUID();
// Create auth user
const { error: authError } = await this.client.auth.admin.createUser({
id: userId,
email: userData.email,
password: 'password123',
email_confirm: true,
user_metadata: {
full_name: userData.name,
}
});
if (authError) {
return { id: '', success: false, error: `Auth user creation failed: ${authError.message}` };
}
// Dynamically determine profile column mappings (config-aware)
const emailColumn = await this.getColumnMapping('profiles', 'email', ['email_address', 'user_email']);
const pictureColumn = await this.getColumnMapping('profiles', 'picture_url', ['avatar_url', 'profile_image_url', 'image_url']);
const bioColumn = await this.getColumnMapping('profiles', 'bio', ['about', 'description']);
const usernameColumn = await this.getColumnMapping('profiles', 'username', ['handle', 'user_name']);
const nameColumn = await this.getColumnMapping('profiles', 'name', ['display_name', 'full_name', 'username']);
// Build profile data with only columns that exist
const profileData = {
id: userId,
updated_at: new Date().toISOString(),
};
// Add email field if column exists and data provided
if (emailColumn && userData.email) {
profileData[emailColumn] = userData.email;
}
else if (userData.email) {
logger_1.Logger.debug(`Email field not found in profiles table - skipping email`);
}
// Add name field if column exists and data provided
if (nameColumn && userData.name) {
profileData[nameColumn] = userData.name;
}
else if (userData.name) {
logger_1.Logger.debug(`Name field not found in profiles table - skipping name`);
}
// Add optional fields only if columns exist and data provided
if (pictureColumn && userData.picture_url) {
profileData[pictureColumn] = userData.picture_url;
}
else if (userData.picture_url) {
// Log missing column but don't fail
logger_1.Logger.debug(`Picture field not found in profiles table - skipping avatar`);
}
if (bioColumn && userData.bio) {
profileData[bioColumn] = userData.bio;
}
else if (userData.bio) {
logger_1.Logger.debug(`Bio field not found in profiles table - skipping bio`);
}
if (usernameColumn && userData.username) {
profileData[usernameColumn] = userData.username;
}
else if (userData.username) {
logger_1.Logger.debug(`Username field not found in profiles table - skipping username`);
}
// CRITICAL FIX: Create account FIRST to satisfy PostgreSQL constraint
// "Profiles can only be created for personal accounts" requires account to exist before profile
if (this.schemaInfo?.hasAccounts) {
try {
// NEW: Pre-check for MakerKit personal account constraints
const constraintCheck = await this.checkPersonalAccountConstraints();
if (!constraintCheck.canCreate) {
const errorMsg = `Cannot create personal account: ${constraintCheck.reason}. ` +
`Existing personal accounts: ${constraintCheck.existingCount}, ` +
`Maximum allowed: ${constraintCheck.maxAllowed}`;
logger_1.Logger.warn(errorMsg);
return { id: '', success: false, error: errorMsg };
}
const { error: accountError } = await this.client
.from('accounts')
.insert({
id: crypto.randomUUID(),
name: `${userData.name}'s Account`,
primary_owner_user_id: userId,
slug: null, // CRITICAL: MakerKit constraint requires slug = null for personal accounts
is_personal_account: true, // CRITICAL: Must be true for profile creation to succeed
});
if (accountError) {
// Check if this is the specific constraint violation we're trying to prevent
if (accountError.message.includes('unique_personal_account')) {
const enhancedError = `Personal account constraint violation: MakerKit limits personal accounts per workspace. ` +
`Current constraint prevents additional personal accounts. ` +
`Consider using existing accounts or configuring team accounts instead.`;
return { id: '', success: false, error: enhancedError };
}
return { id: '', success: false, error: `Account creation failed: ${accountError.message}` };
}
logger_1.Logger.debug(`✅ Account created for ${userData.email} with is_personal_account=true`);
}
catch (error) {
return { id: '', success: false, error: `Account creation failed: ${error.message}` };
}
}
// Now create profile (after account exists with is_personal_account=true)
const { error: profileError } = await this.client
.from('profiles')
.insert(profileData);
if (profileError) {
// Enhanced error reporting with schema introspection hints
let errorMsg = `Profile creation failed: ${profileError.message}`;
if (profileError.message.includes('column')) {
// Use comprehensive schema validation for better error messages
const schemaValidation = await this.validateTableSchema('profiles', {
email: ['email', 'email_address', 'user_email'],
name: ['name', 'display_name', 'full_name', 'username'],
picture: ['picture_url', 'avatar_url', 'profile_image_url', 'image_url'],
bio: ['bio', 'about', 'description'],
username: ['username', 'handle', 'user_name']
});
errorMsg += `\n\nSchema Analysis:`;
errorMsg += `\n Available columns: ${schemaValidation.availableColumns.join(', ')}`;
if (schemaValidation.suggestions.length > 0) {
errorMsg += `\n\nMapping suggestions:`;
schemaValidation.suggestions.forEach(suggestion => {
errorMsg += `\n - ${suggestion}`;
});
}
errorMsg += `\n\nCurrent mappings:`;
if (emailColumn)
errorMsg += `\n - Email: '${emailColumn}' ✅`;
if (nameColumn)
errorMsg += `\n - Name: '${nameColumn}' ✅`;
if (pictureColumn)
errorMsg += `\n - Picture: '${pictureColumn}' ✅`;
if (bioColumn)
errorMsg += `\n - Bio: '${bioColumn}' ✅`;
if (usernameColumn)
errorMsg += `\n - Username: '${usernameColumn}' ✅`;
errorMsg += `\n\nTo fix: Update your config schema.userTable fields to match your database columns.`;
}
return { id: '', success: false, error: errorMsg };
}
return { id: userId, success: true };
}
async createProfilesUser(userData) {
// Generic profiles pattern: auth user + profile
const userId = userData.id || crypto.randomUUID();
// Create auth user
const { error: authError } = await this.client.auth.admin.createUser({
id: userId,
email: userData.email,
password: 'password123',
email_confirm: true,
user_metadata: {
full_name: userData.name,
}
});
if (authError) {
return { id: '', success: false, error: `Auth user creation failed: ${authError.message}` };
}
// Dynamically determine profile column mappings (config-aware)
const emailColumn = await this.getColumnMapping('profiles', 'email', ['email_address', 'user_email']);
const pictureColumn = await this.getColumnMapping('profiles', 'picture_url', ['avatar_url', 'profile_image_url', 'image_url']);
const bioColumn = await this.getColumnMapping('profiles', 'bio', ['about', 'description']);
const usernameColumn = await this.getColumnMapping('profiles', 'username', ['handle', 'user_name']);
const nameColumn = await this.getColumnMapping('profiles', 'name', ['display_name', 'full_name']);
// Build profile data with only columns that exist
const profileData = {
id: userId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
// Add email field if column exists and data provided
if (emailColumn && userData.email) {
profileData[emailColumn] = userData.email;
}
else if (userData.email) {
logger_1.Logger.debug(`Email field not found in profiles table - skipping email`);
}
// Add required fields with fallbacks
if (nameColumn) {
profileData[nameColumn] = userData.name;
}
else {
profileData.name = userData.name; // fallback
}
// Add optional fields only if columns exist and data provided
if (pictureColumn && userData.picture_url) {
profileData[pictureColumn] = userData.picture_url;
}
else if (userData.picture_url) {
// Log missing column but don't fail
logger_1.Logger.debug(`Picture field not found in profiles table - skipping avatar`);
}
if (bioColumn && userData.bio) {
profileData[bioColumn] = userData.bio;
}
else if (userData.bio) {
logger_1.Logger.debug(`Bio field not found in profiles table - skipping bio`);
}
if (usernameColumn && userData.username) {
profileData[usernameColumn] = userData.username;
}
else if (userData.username) {
logger_1.Logger.debug(`Username field not found in profiles table - skipping username`);
}
// Create profile
const { error: profileError } = await this.client
.from('profiles')
.insert(profileData);
if (profileError) {
// Enhanced error reporting with schema introspection hints
let errorMsg = `Profile creation failed: ${profileError.message}`;
if (profileError.message.includes('column')) {
// Use comprehensive schema validation for better error messages
const schemaValidation = await this.validateTableSchema('profiles', {
email: ['email', 'email_address', 'user_email'],
name: ['name', 'display_name', 'full_name', 'username'],
picture: ['picture_url', 'avatar_url', 'profile_image_url', 'image_url'],
bio: ['bio', 'about', 'description'],
username: ['username', 'handle', 'user_name']
});
errorMsg += `\n\nSchema Analysis:`;
errorMsg += `\n Available columns: ${schemaValidation.availableColumns.join(', ')}`;
if (schemaValidation.suggestions.length > 0) {
errorMsg += `\n\nMapping suggestions:`;
schemaValidation.suggestions.forEach(suggestion => {
errorMsg += `\n - ${suggestion}`;
});
}
errorMsg += `\n\nCurrent mappings:`;
if (emailColumn)
errorMsg += `\n - Email: '${emailColumn}' ✅`;
if (nameColumn)
errorMsg += `\n - Name: '${nameColumn}' ✅`;
if (pictureColumn)
errorMsg += `\n - Picture: '${pictureColumn}' ✅`;
if (bioColumn)
errorMsg += `\n - Bio: '${bioColumn}' ✅`;
if (usernameColumn)
errorMsg += `\n - Username: '${usernameColumn}' ✅`;
errorMsg += `\n\nTo fix: Update your config schema.userTable fields to match your database columns.`;
}
return { id: '', success: false, error: errorMsg };
}
return { id: userId, success: true };
}
/**
* Check if we can create additional personal accounts based on MakerKit constraints
*/
async checkPersonalAccountConstraints() {
try {
// Check if we can detect the unique_personal_account constraint
const hasConstraint = await this.detectUniquePersonalAccountConstraint();
if (!hasConstraint) {
// No constraint detected, allow creation
return {
canCreate: true,
reason: 'No personal account constraints detected',
existingCount: 0,
maxAllowed: Infinity
};
}
// Count existing personal accounts
const { data: existingAccounts, error } = await this.client
.from('accounts')
.select('id')
.eq('is_personal_account', true);
if (error) {
logger_1.Logger.warn('Failed to check existing personal accounts:', error);
// If we can't check, allow creation but warn
return {
canCreate: true,
reason: 'Unable to verify constraint limits',
existingCount: 0,
maxAllowed: -1
};
}
const existingCount = existingAccounts?.length || 0;
// MakerKit typically allows 1 personal account per workspace
// This is a common pattern we've observed
const maxAllowed = 1;
if (existingCount >= maxAllowed) {
return {
canCreate: false,
reason: 'MakerKit unique_personal_account constraint limit reached',
existingCount,
maxAllowed
};
}
return {
canCreate: true,
reason: 'Within personal account limits',
existingCount,
maxAllowed
};
}
catch (error) {
logger_1.Logger.warn('Error checking personal account constraints:', error);
// If constraint checking fails, allow creation but with warning
return {
canCreate: true,
reason: 'Constraint checking failed, proceeding with caution',
existingCount: -1,
maxAllowed: -1
};
}
}
/**
* Detect if the unique_personal_account constraint exists
*/
async detectUniquePersonalAccountConstraint() {
try {
// Try to query the constraint from the information schema
const { data, error } = await this.client
.from('information_schema.table_constraints')
.select('constraint_name')
.eq('table_name', 'accounts')
.ilike('constraint_name', '%unique_personal_account%');
if (error) {
logger_1.Logger.debug('Failed to query constraints from information_schema:', error);
return false;
}
return data && data.length > 0;
}
catch (error) {
logger_1.Logger.debug('Error detecting unique_personal_account constraint:', error);
return false;
}
}
/**
* Get the appropriate table name for setups/content based on schema
*/
getSetupsTableName() {
if (!this.schemaInfo) {
throw new Error('Schema not detected. Call detectSchema() first.');
}
// Look for common alternatives to 'setups'
const possibleTables = ['setups', 'posts', 'content', 'projects', 'items'];
// For now, default to 'setups' but in the future we could detect
// which table is most appropriate
return 'setups';
}
/**
* Get the appropriate foreign key for user relationships
*/
getUserForeignKey() {
// Check config override first
if (this.configOverride?.schema?.setupsTable?.userField) {
return this.configOverride.schema.setupsTable.userField;
}
// Check for MakerKit patterns - they use account_id for user references
if (this.hasMakerKitPattern()) {
return 'account_id';
}
const strategy = this.getUserCreationStrategy();
switch (strategy) {
case 'simple-accounts':
return 'account_id';
case 'makerkit-profiles':
case 'custom-profiles':
return 'user_id';
default:
return 'account_id';
}
}
/**
* Detect if this is a MakerKit schema pattern
*/
hasMakerKitPattern() {
// If we detect accounts table as primary user table, it's likely MakerKit
const primaryTable = this.schemaInfo?.primaryUserTable;
return primaryTable === 'accounts';
}
/**
* Detect MakerKit version based on table patterns and schema structure
*/
async detectMakerKitVersion(tables) {
const { hasAccounts, hasMemberships, hasSubscriptions, hasRoles, hasInvitations, hasNotifications, hasRolePermissions, hasBillingCustomers, hasGearItems, hasReviews } = tables;
// No MakerKit if no accounts table
if (!hasAccounts) {
return 'none';
}
// Check for v3 indicators (latest MakerKit with comprehensive feature set)
if (hasAccounts && hasMemberships && hasSubscriptions && hasRoles && hasInvitations && hasNotifications && hasRolePermissions && hasBillingCustomers) {
// Check for v3-specific account structure
const hasAdvancedAccountFields = await this.checkAccountFields([
'primary_owner_user_id', 'slug', 'is_personal_account', 'public_data'
]);
if (hasAdvancedAccountFields) {
logger_1.Logger.debug('Detected MakerKit v3 pattern with full feature set including role_permissions and billing_customers');
return 'v3';
}
}
// Check for v2 indicators (mid-version with core multi-tenancy)
// Enhanced detection for MakerKit v2 based on bug report patterns
if (hasAccounts && hasMemberships && hasSubscriptions && hasRoles && (hasRolePermissions || hasInvitations)) {
const hasV2AccountFields = await this.checkAccountFields([
'primary_owner_user_id', 'is_personal_account'
]);
if (hasV2AccountFields) {
logger_1.Logger.debug('Detected MakerKit v2 pattern with multi-tenancy and role permissions');
return 'v2';
}
}
// Check for v1 indicators (basic MakerKit with accounts and memberships)
if (hasAccounts && hasMemberships) {
logger_1.Logger.debug('Detected MakerKit v1 pattern with basic accounts');
return 'v1';
}
// Has accounts but not standard MakerKit pattern
if (hasAccounts) {
logger_1.Logger.debug('Detected custom accounts pattern (non-MakerKit)');
return 'custom';
}
return 'none';
}
/**
* Check if specific fields exist in the accounts table
*/
async checkAccountFields(fields) {
try {
const selectFields = fields.join(', ');
const { error } = await this.client
.from('accounts')
.select(selectFields)
.limit(1);
return !error;
}
catch {
return false;
}
}
/**
* Detect custom tables beyond standard MakerKit schema
*/
async detectCustomTables(standardTables) {
try {
// Fallback: try common custom table names since we can't easily query information_schema
const commonCustomTables = [
'setups', 'gear', 'base_templates', 'trips', 'modifications',
'posts', 'media_attachments', 'conversations', 'messages',
'user_preferences', 'audit_logs', 'analytics_events'
];
const customTables = [];
for (const table of commonCustomTables) {
if (await this.tableExists(table) && !standardTables.includes(table)) {
customTables.push(table);
}
}
return customTables;
}
catch (error) {
logger_1.Logger.debug('Custom table detection failed:', error);
return [];
}
}
/**
* Detect table relationships for better association intelligence
*/
async detectTableRelationships() {
const relationships = [];
try {
// Common relationship patterns to check
const commonRelationships = [
{ from: 'setups', fromCol: 'user_id', to: 'auth.users', toCol: 'id' },
{ from: 'setups', fromCol: 'account_id', to: 'accounts', toCol: 'id' },
{ from: 'posts', fromCol: 'author_id', to: 'accounts', toCol: 'id' },
{ from: 'media_attachments', fromCol: 'setup_id', to: 'setups', toCol: 'id' },
{ from: 'setup_gear_items', fromCol: 'setup_id', to: 'setups', toCol: 'id' },
{ from: 'memberships', fromCol: 'account_id', to: 'accounts', toCol: 'id' },
{ from: 'memberships', fromCol: 'user_id', to: 'auth.users', toCol: 'id' }
];
for (const rel of commonRelationships) {
const fromExists = await this.tableExists(rel.from);
const toExists = await this.tableExists(rel.to.replace('auth.', ''));
if (fromExists && toExists) {
// Try to detect the relationship by checking column existence
const hasColumn = await this.columnExists(rel.from, rel.fromCol);
if (hasColumn) {
relationships.push({
fromTable: rel.from,
fromColumn: rel.fromCol,
toTable: rel.to,
toColumn: rel.toCol,
relationshipType: 'one_to_many', // Default assumption
cascadeDelete: false // Default assumption
});
}
}
}
logger_1.Logger.debug(`Detected ${relationships.length} table relationships`);
return relationships;
}
catch (error) {
logger_1.Logger.debug('Relationship detection failed:', error);
return [];
}
}
/**
* Check if a column exists in a table
*/
async columnExists(tableName, columnName) {
try {
const { error } = await this.client
.from(tableName)
.select(columnName)
.limit(1);
return !error;
}
catch {
return false;
}
}
/**
* Get actual column names from database schema
*/
async getTableColumns(tableName) {
try {
const { data, error } = await this.client
.from('information_schema.columns')
.select('column_name')
.eq('table_name', tableName)
.eq('table_schema', 'public');
if (error) {
// Fallback: try to get columns by doing a select with limit 0
const { error: selectError } = await this.client
.from(tableName)
.select('*')
.limit(0);
if (selectError && selectError.message.includes('column')) {
logger_1.Logger.debug(`Unable to introspect ${tableName} columns: ${selectError.message}`);
return [];
}
return [];
}
return data?.map(row => row.column_name) || [];
}
catch (error) {
logger_1.Logger.debug(`Schema introspection failed for ${tableName}: ${error}`);
return [];
}
}
/**
* Validate that required columns exist for a table and provide helpful error messages
*/
async validateTableSchema(tableName, requiredFields) {
const availableColumns = await this.getTableColumns(tableName);
const missingFields = [];
const suggestions = [];
for (const [fieldName, possibleColumns] of Object.entries(requiredFields)) {
const foundColumn = possibleColumns.find(col => availableColumns.includes(col));
if (!foundColumn) {
missingFields.push(fieldName);
// Find similar column names as suggestions
const similarCols = availableColumns.filter(col => possibleColumns.some(possible => col.toLowerCase().includes(possible.toLowerCase()) ||
possible.toLowerCase().includes(col.toLowerCase())));
if (similarCols.length > 0) {
suggestions.push(`For ${fieldName}, consider mapping to: ${similarCols.join(', ')}`);
}
}
}
return {
valid: missingFields.length === 0,
missingFields,
availableColumns,
suggestions
};
}
/**
* Detect framework type based on comprehensive analysis
*/
detectFrameworkTypeEnhanced(analysis) {
const { makerkitVersion, hasGear, hasBaseTemplates, hasSetups, hasTrips, customTables } = analysis;
// Check for Wildernest pattern (MakerKit + outdoor platform tables)
if (makerkitVersion !== 'none' && hasGear && hasBaseTemplates && hasSetups) {
logger_1.Logger.debug('Detected Wildernest-style outdoor platform');
return 'wildernest';
}
// Standard MakerKit patterns
if (makerkitVersion === 'v3' || makerkitVersion === 'v2' || makerkitVersion === 'v1') {
logger_1.Logger.debug(`Detected MakerKit ${makerkitVersion} framework`);
return 'makerkit';
}
// Simple framework (basic Supabase without MakerKit)
if (makerkitVersion === 'none' && customTables.length < 3) {
logger_1.Logger.debug('Detected simple Supabase framework');
return 'simple';
}
// Custom framework with significant customizations
logger_1.Logger.debug('Detected custom framework with extensive customizations');
return 'custom';
}
/**
* Analyze asset compatibility for hybrid seeding
*/
async analyzeAssetCompatibility(tables) {
const { hasMediaAttachments, hasPosts, hasSetups, customTables } = tables;
const compatibility = {
supportsImages: false,
supportsMarkdown: false,
supportsJson: false,
contentTables: [],
userContentRelationships: [],
mediaStoragePattern: 'url_only'
};
// Check for image support
if (hasMediaAttachments) {
compatibility.supportsImages = true;
compatibility.mediaStoragePattern = 'supabase_storage';
}
// Check for markdown content support
if (hasPosts) {
compatibility.supportsMarkdown = await this.checkMarkdownSupport('posts');
compatibility.contentTables.push('posts');
}
if (hasSetups) {
const setupMarkdown = await this.checkMarkdownSupport('setups');
if (setupMarkdown) {
compatibility.supportsMarkdown = true;
}
compatibility.contentTables.push('setups');
}
// Check custom tables for content patterns
for (const table of customTables) {
const hasContent = await this.checkContentColumns(table);
if (hasContent) {
compatibility.contentTables.push(table);
}
}
// JSON support is generally available in Postgres
compatibility.supportsJson = true;
logger_1.Logger.debug('Asset compatibility analysis:', compatibility);
return compatibility;
}
/**
* Check if a table supports markdown content
*/
async checkMarkdownSupport(tableName) {
const markdownColumns = ['content', 'description', 'body', 'markdown', 'readme'];
for (const colum