import { test, expect } from '@playwright/test'; import { createClient } from '@supabase/supabase-js'; /** * Schema Validation Tests * * These tests validate that submission tables, version tables, and main entity tables * have consistent schemas to prevent field mismatches during the approval pipeline. * * Critical validations: * 1. Submission tables must have all fields from main tables (except auto-generated) * 2. Version tables must have all fields from main tables plus version metadata * 3. Critical functions must reference correct column names * 4. Required NOT NULL fields must be present in all tables */ const supabase = createClient( 'https://ydvtmnrszybqnbcqbdcy.supabase.co', process.env.SUPABASE_SERVICE_ROLE_KEY || '' ); interface ColumnDefinition { column_name: string; data_type: string; is_nullable: string; column_default: string | null; } interface TableSchema { [columnName: string]: ColumnDefinition; } // Fields that are expected to be different or missing in submission tables const EXCLUDED_FIELDS = [ 'id', // Submission tables have their own ID 'created_at', // Managed differently in submissions 'updated_at', // Managed differently in submissions 'view_count_all', // Calculated fields not in submissions 'view_count_30d', 'view_count_7d', 'average_rating', 'review_count', 'installations_count', // Only for ride_models 'is_test_data', // Test data flag ]; // Version-specific metadata fields (expected to be extra in version tables) const VERSION_METADATA_FIELDS = [ 'version_id', 'version_number', 'change_type', 'change_reason', 'is_current', 'created_by', 'created_at', 'submission_id', 'is_test_data', ]; async function getTableSchema(tableName: string): Promise { const { data, error } = await supabase .from('information_schema.columns' as any) .select('column_name, data_type, is_nullable, column_default') .eq('table_schema', 'public') .eq('table_name', tableName); if (error) throw error; const schema: TableSchema = {}; data?.forEach((col: any) => { schema[col.column_name] = col; }); return schema; } function normalizeColumnName(name: string): string { // Handle known version table variations const mapping: { [key: string]: string } = { 'height_requirement_cm': 'height_requirement', 'gforce_max': 'max_g_force', 'inversions_count': 'inversions', 'height_meters': 'max_height_meters', 'drop_meters': 'drop_height_meters', }; return mapping[name] || name; } test.describe('Schema Validation - Entity Tables', () => { test('parks: submission table matches main table schema', async () => { const mainSchema = await getTableSchema('parks'); const submissionSchema = await getTableSchema('park_submissions'); const mismatches: string[] = []; const missingFields: string[] = []; // Check each field in main table exists in submission table for (const [fieldName, fieldDef] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; if (!submissionSchema[fieldName]) { missingFields.push(fieldName); } else { // Check data type matches const mainType = fieldDef.data_type; const submissionType = submissionSchema[fieldName].data_type; if (mainType !== submissionType) { mismatches.push( `${fieldName}: main=${mainType}, submission=${submissionType}` ); } } } expect(missingFields, `park_submissions is missing fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(mismatches, `park_submissions has type mismatches: ${mismatches.join('; ')}` ).toHaveLength(0); }); test('rides: submission table matches main table schema', async () => { const mainSchema = await getTableSchema('rides'); const submissionSchema = await getTableSchema('ride_submissions'); const mismatches: string[] = []; const missingFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; if (!submissionSchema[fieldName]) { missingFields.push(fieldName); } else { const mainType = fieldDef.data_type; const submissionType = submissionSchema[fieldName].data_type; if (mainType !== submissionType) { mismatches.push( `${fieldName}: main=${mainType}, submission=${submissionType}` ); } } } expect(missingFields, `ride_submissions is missing fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(mismatches, `ride_submissions has type mismatches: ${mismatches.join('; ')}` ).toHaveLength(0); }); test('companies: submission table matches main table schema', async () => { const mainSchema = await getTableSchema('companies'); const submissionSchema = await getTableSchema('company_submissions'); const mismatches: string[] = []; const missingFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; if (!submissionSchema[fieldName]) { missingFields.push(fieldName); } else { const mainType = fieldDef.data_type; const submissionType = submissionSchema[fieldName].data_type; if (mainType !== submissionType) { mismatches.push( `${fieldName}: main=${mainType}, submission=${submissionType}` ); } } } expect(missingFields, `company_submissions is missing fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(mismatches, `company_submissions has type mismatches: ${mismatches.join('; ')}` ).toHaveLength(0); }); test('ride_models: submission table matches main table schema', async () => { const mainSchema = await getTableSchema('ride_models'); const submissionSchema = await getTableSchema('ride_model_submissions'); const mismatches: string[] = []; const missingFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; if (!submissionSchema[fieldName]) { missingFields.push(fieldName); } else { const mainType = fieldDef.data_type; const submissionType = submissionSchema[fieldName].data_type; if (mainType !== submissionType) { mismatches.push( `${fieldName}: main=${mainType}, submission=${submissionType}` ); } } } expect(missingFields, `ride_model_submissions is missing fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(mismatches, `ride_model_submissions has type mismatches: ${mismatches.join('; ')}` ).toHaveLength(0); }); }); test.describe('Schema Validation - Version Tables', () => { test('park_versions: has all main table fields plus version metadata', async () => { const mainSchema = await getTableSchema('parks'); const versionSchema = await getTableSchema('park_versions'); const missingFields: string[] = []; // Check all main table fields exist in version table for (const [fieldName] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; const normalizedName = normalizeColumnName(fieldName); if (!versionSchema[fieldName] && !versionSchema[normalizedName]) { missingFields.push(fieldName); } } // Check all version metadata fields exist const missingMetadata: string[] = []; for (const metaField of VERSION_METADATA_FIELDS) { if (!versionSchema[metaField]) { missingMetadata.push(metaField); } } expect(missingFields, `park_versions is missing main table fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(missingMetadata, `park_versions is missing version metadata: ${missingMetadata.join(', ')}` ).toHaveLength(0); }); test('ride_versions: has all main table fields plus version metadata', async () => { const mainSchema = await getTableSchema('rides'); const versionSchema = await getTableSchema('ride_versions'); const missingFields: string[] = []; for (const [fieldName] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; const normalizedName = normalizeColumnName(fieldName); if (!versionSchema[fieldName] && !versionSchema[normalizedName]) { missingFields.push(fieldName); } } const missingMetadata: string[] = []; for (const metaField of VERSION_METADATA_FIELDS) { if (!versionSchema[metaField]) { missingMetadata.push(metaField); } } expect(missingFields, `ride_versions is missing main table fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(missingMetadata, `ride_versions is missing version metadata: ${missingMetadata.join(', ')}` ).toHaveLength(0); }); test('company_versions: has all main table fields plus version metadata', async () => { const mainSchema = await getTableSchema('companies'); const versionSchema = await getTableSchema('company_versions'); const missingFields: string[] = []; for (const [fieldName] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; const normalizedName = normalizeColumnName(fieldName); if (!versionSchema[fieldName] && !versionSchema[normalizedName]) { missingFields.push(fieldName); } } const missingMetadata: string[] = []; for (const metaField of VERSION_METADATA_FIELDS) { if (!versionSchema[metaField]) { missingMetadata.push(metaField); } } expect(missingFields, `company_versions is missing main table fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(missingMetadata, `company_versions is missing version metadata: ${missingMetadata.join(', ')}` ).toHaveLength(0); }); test('ride_model_versions: has all main table fields plus version metadata', async () => { const mainSchema = await getTableSchema('ride_models'); const versionSchema = await getTableSchema('ride_model_versions'); const missingFields: string[] = []; for (const [fieldName] of Object.entries(mainSchema)) { if (EXCLUDED_FIELDS.includes(fieldName)) continue; const normalizedName = normalizeColumnName(fieldName); if (!versionSchema[fieldName] && !versionSchema[normalizedName]) { missingFields.push(fieldName); } } const missingMetadata: string[] = []; for (const metaField of VERSION_METADATA_FIELDS) { if (!versionSchema[metaField]) { missingMetadata.push(metaField); } } expect(missingFields, `ride_model_versions is missing main table fields: ${missingFields.join(', ')}` ).toHaveLength(0); expect(missingMetadata, `ride_model_versions is missing version metadata: ${missingMetadata.join(', ')}` ).toHaveLength(0); }); }); test.describe('Schema Validation - Critical Fields', () => { test('rides table does NOT have ride_type column', async () => { const ridesSchema = await getTableSchema('rides'); expect(ridesSchema['ride_type']).toBeUndefined(); }); test('rides table DOES have category column (NOT NULL)', async () => { const ridesSchema = await getTableSchema('rides'); expect(ridesSchema['category']).toBeDefined(); expect(ridesSchema['category'].is_nullable).toBe('NO'); }); test('ride_models table DOES have both category and ride_type columns', async () => { const rideModelsSchema = await getTableSchema('ride_models'); expect(rideModelsSchema['category']).toBeDefined(); expect(rideModelsSchema['category'].is_nullable).toBe('NO'); expect(rideModelsSchema['ride_type']).toBeDefined(); }); test('all entity tables have required base fields', async () => { const requiredFields = ['id', 'name', 'slug', 'created_at', 'updated_at']; const tables = ['parks', 'rides', 'companies', 'ride_models']; for (const table of tables) { const schema = await getTableSchema(table); for (const field of requiredFields) { expect(schema[field], `${table} is missing required field: ${field}` ).toBeDefined(); } } }); test('all submission tables have submission_id foreign key', async () => { const submissionTables = [ 'park_submissions', 'ride_submissions', 'company_submissions', 'ride_model_submissions', 'photo_submissions', ]; for (const table of submissionTables) { const schema = await getTableSchema(table); expect(schema['submission_id'], `${table} is missing submission_id foreign key` ).toBeDefined(); expect(schema['submission_id'].is_nullable).toBe('NO'); } }); test('all version tables have version metadata fields', async () => { const versionTables = [ 'park_versions', 'ride_versions', 'company_versions', 'ride_model_versions', ]; const requiredVersionFields = [ 'version_id', 'version_number', 'change_type', 'is_current', ]; for (const table of versionTables) { const schema = await getTableSchema(table); for (const field of requiredVersionFields) { expect(schema[field], `${table} is missing required version field: ${field}` ).toBeDefined(); } } }); }); test.describe('Schema Validation - Function Parameter Alignment', () => { test('verify create_entity_from_submission function exists', async () => { const { data, error } = await supabase .rpc('pg_get_functiondef', { funcid: 'create_entity_from_submission'::any } as any) .single(); // Function should exist (will error if not) expect(error).toBeNull(); }); test('verify update_entity_from_submission function exists', async () => { const { data, error } = await supabase .rpc('pg_get_functiondef', { funcid: 'update_entity_from_submission'::any } as any) .single(); // Function should exist (will error if not) expect(error).toBeNull(); }); test('verify process_approval_transaction function exists', async () => { const { data, error } = await supabase.rpc('pg_catalog.pg_function_is_visible', { funcid: 'process_approval_transaction'::any } as any); // Function should be visible expect(data).toBeTruthy(); }); }); test.describe('Schema Validation - Known Field Name Variations', () => { test('ride_versions uses height_requirement_cm instead of height_requirement', async () => { const versionSchema = await getTableSchema('ride_versions'); expect(versionSchema['height_requirement_cm']).toBeDefined(); expect(versionSchema['height_requirement']).toBeUndefined(); }); test('ride_versions uses gforce_max instead of max_g_force', async () => { const versionSchema = await getTableSchema('ride_versions'); expect(versionSchema['gforce_max']).toBeDefined(); expect(versionSchema['max_g_force']).toBeUndefined(); }); test('ride_versions uses inversions_count instead of inversions', async () => { const versionSchema = await getTableSchema('ride_versions'); expect(versionSchema['inversions_count']).toBeDefined(); expect(versionSchema['inversions']).toBeUndefined(); }); test('ride_versions uses height_meters instead of max_height_meters', async () => { const versionSchema = await getTableSchema('ride_versions'); expect(versionSchema['height_meters']).toBeDefined(); expect(versionSchema['max_height_meters']).toBeUndefined(); }); test('ride_versions uses drop_meters instead of drop_height_meters', async () => { const versionSchema = await getTableSchema('ride_versions'); expect(versionSchema['drop_meters']).toBeDefined(); expect(versionSchema['drop_height_meters']).toBeUndefined(); }); }); test.describe('Schema Validation - Submission Items', () => { test('submission_items has all required foreign key columns', async () => { const schema = await getTableSchema('submission_items'); const requiredFKs = [ 'submission_id', 'park_submission_id', 'ride_submission_id', 'company_submission_id', 'ride_model_submission_id', 'photo_submission_id', 'timeline_event_submission_id', 'depends_on', // For dependency chain ]; for (const fk of requiredFKs) { expect(schema[fk], `submission_items is missing FK: ${fk}` ).toBeDefined(); } }); test('submission_items has required metadata fields', async () => { const schema = await getTableSchema('submission_items'); const requiredFields = [ 'item_type', 'action_type', 'status', 'order_index', ]; for (const field of requiredFields) { expect(schema[field], `submission_items is missing field: ${field}` ).toBeDefined(); } }); });