import { Environment } from '../value-objects/Environment';
import { TableInfo } from './TableInfo';
/**
* 🎯 SEMANTIC INTENT: DatabaseSchema is the aggregate root for complete database metadata
*
* WHY: Database schema represents the complete structural snapshot at a point in time
* - Aggregate root: Coordinates all tables, relationships, indexes
* - Semantic boundary: Database is the consistency boundary for schema operations
* - Observable from D1 REST API (sqlite_master + PRAGMA statements)
* - Environment semantic preserved: dev/staging/prod context
*
* AGGREGATE ROOT: Top-level entity coordinating all schema metadata
* OBSERVABLE PROPERTIES: All metadata directly observable from database
* IMMUTABILITY: Frozen snapshot - schema doesn't change after fetch
* INTENT PRESERVATION: Environment semantic maintained through lifecycle
*/
export class DatabaseSchema {
public readonly name: string;
public readonly environment: Environment;
public readonly tables: readonly TableInfo[];
public readonly fetchedAt: Date;
constructor(name: string, environment: Environment, tables: TableInfo[], fetchedAt: Date) {
if (!name || name.trim().length === 0) {
throw new Error('Database name cannot be empty');
}
if (tables.length === 0) {
throw new Error('Database must have at least one table');
}
this.name = name.trim();
this.environment = environment;
this.tables = Object.freeze([...tables]);
this.fetchedAt = fetchedAt;
Object.freeze(this);
}
/**
* Get table by name
*
* @returns TableInfo or undefined if not found
*/
getTable(tableName: string): TableInfo | undefined {
return this.tables.find((t) => t.name === tableName);
}
/**
* Get all tables that reference a specific table
*
* Semantic: Find dependent tables (children in relationships)
*/
getTablesThatReference(tableName: string): TableInfo[] {
return this.tables.filter((t) => t.getReferencedTables().includes(tableName));
}
/**
* Get all tables that a specific table references
*
* Semantic: Find dependencies (parents in relationships)
*/
getTablesReferencedBy(tableName: string): string[] {
const table = this.getTable(tableName);
return table ? table.getReferencedTables() : [];
}
/**
* Get table count
*/
getTableCount(): number {
return this.tables.length;
}
/**
* Get tables without primary keys
*
* Semantic: Primary keys establish entity identity - tables without them are incomplete
*/
getTablesWithoutPrimaryKey(): TableInfo[] {
return this.tables.filter((t) => !t.hasPrimaryKey());
}
/**
* Get tables with foreign keys (has relationships)
*/
getTablesWithForeignKeys(): TableInfo[] {
return this.tables.filter((t) => t.hasForeignKeys());
}
/**
* Get all views (non-base tables)
*/
getViews(): TableInfo[] {
return this.tables.filter((t) => t.isView());
}
/**
* Get all base tables (non-views)
*/
getBaseTables(): TableInfo[] {
return this.tables.filter((t) => !t.isView());
}
/**
* Check if schema was fetched recently (within specified minutes)
*
* Semantic: Fresh schema data vs stale data (for caching decisions)
*/
isFresh(withinMinutes: number = 10): boolean {
const now = new Date();
const ageMs = now.getTime() - this.fetchedAt.getTime();
const ageMinutes = ageMs / (1000 * 60);
return ageMinutes <= withinMinutes;
}
/**
* Get schema age in minutes
*/
getAgeInMinutes(): number {
const now = new Date();
const ageMs = now.getTime() - this.fetchedAt.getTime();
return ageMs / (1000 * 60);
}
}