Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Component, EventEmitter, Input, OnInit, Output, signal, inject, computed } from '@angular/core';
import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common';
import { Component, computed, EventEmitter, Input, inject, OnInit, Output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TextFieldModule } from '@angular/cdk/text-field';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Parser } from 'node-sql-parser';
import { TableSchemaService, SchemaChangeResponse } from 'src/app/services/table-schema.service';
import { SchemaChangeResponse, TableSchemaService } from 'src/app/services/table-schema.service';
import { SchemaDiagramViewerComponent } from './schema-diagram-viewer/schema-diagram-viewer.component';

interface ParsedColumn {
Expand Down Expand Up @@ -95,9 +95,7 @@ export class EditDatabaseSchemaComponent implements OnInit {
return null;
});

protected chatMessages = computed(() =>
this.messages().filter(m => m.role !== 'diagram'),
);
protected chatMessages = computed(() => this.messages().filter((m) => m.role !== 'diagram'));

ngOnInit(): void {
if (!this.connectionID) {
Expand All @@ -121,7 +119,7 @@ export class EditDatabaseSchemaComponent implements OnInit {
const prompt = this.userPrompt().trim();
if (!prompt || this.submitting()) return;

this.messages.update(msgs => [...msgs, { role: 'user', text: prompt }]);
this.messages.update((msgs) => [...msgs, { role: 'user', text: prompt }]);
this.userPrompt.set('');
this.submitting.set(true);

Expand All @@ -132,16 +130,28 @@ export class EditDatabaseSchemaComponent implements OnInit {
}
if (result && result.changes.length > 0) {
this.applied.set(false);
const previewSource = this._buildMermaidFromChanges(result.changes);
const parsedChanges = result.changes.map(c => this._parseChange(c));
this.messages.update(msgs => {
const next: ChatMessage[] = [...msgs, {
role: 'ai',
text: `I've generated ${result.changes.length} change(s) for your database. Review them below and approve or reject.`,
changes: result.changes,
parsedChanges,
batchId: result.batchId,
}];
let previewSource = '';
try {
const preview = await this._tableSchema.previewDiagram(
this.connectionID,
result.changes.map((c) => c.forwardSql),
);
previewSource = preview.diagram ?? '';
} catch {
// preview is supplementary — don't block the change list on a diagram failure
}
const parsedChanges = result.changes.map((c) => this._parseChange(c));
this.messages.update((msgs) => {
const next: ChatMessage[] = [
...msgs,
{
role: 'ai',
text: `I've generated ${result.changes.length} change(s) for your database. Review them below and approve or reject.`,
changes: result.changes,
parsedChanges,
batchId: result.batchId,
},
];
if (previewSource) {
next.push({
role: 'diagram',
Expand All @@ -152,17 +162,23 @@ export class EditDatabaseSchemaComponent implements OnInit {
return next;
});
} else {
this.messages.update(msgs => [...msgs, {
role: 'ai',
text: 'I could not generate any schema changes for that prompt. Could you describe your database in more detail?',
}]);
this.messages.update((msgs) => [
...msgs,
{
role: 'ai',
text: 'I could not generate any schema changes for that prompt. Could you describe your database in more detail?',
},
]);
}
} catch (err: unknown) {
const error = err as { error?: { message?: string }; message?: string };
this.messages.update(msgs => [...msgs, {
role: 'error',
text: error?.error?.message || error?.message || 'Failed to generate schema changes.',
}]);
this.messages.update((msgs) => [
...msgs,
{
role: 'error',
text: error?.error?.message || error?.message || 'Failed to generate schema changes.',
},
]);
} finally {
this.submitting.set(false);
}
Expand All @@ -177,29 +193,37 @@ export class EditDatabaseSchemaComponent implements OnInit {
try {
const result = await this._tableSchema.approveBatch(batch.batchId, true);
if (result) {
const failed = result.changes.filter(c => c.status === 'failed');
const failed = result.changes.filter((c) => c.status === 'failed');
if (failed.length > 0) {
this.messages.update(msgs => [...msgs, {
role: 'error',
text: `${failed.length} change(s) failed: ${failed.map(c => c.executionError).join('; ')}`,
}]);
this.messages.update((msgs) => [
...msgs,
{
role: 'error',
text: `${failed.length} change(s) failed: ${failed.map((c) => c.executionError).join('; ')}`,
},
]);
} else {
this.applied.set(true);
this.messages.update(msgs => msgs.map(m =>
m === batch ? { ...m, batchId: undefined } : m
).concat({
role: 'ai',
text: 'All changes applied successfully! Your tables have been created.',
}));
this.messages.update((msgs) =>
msgs
.map((m) => (m === batch ? { ...m, batchId: undefined } : m))
.concat({
role: 'ai',
text: 'All changes applied successfully! Your tables have been created.',
}),
);
this._loadDiagram('Updated Database Structure');
}
}
} catch (err: unknown) {
const error = err as { error?: { message?: string }; message?: string };
this.messages.update(msgs => [...msgs, {
role: 'error',
text: error?.error?.message || error?.message || 'Failed to apply schema changes.',
}]);
this.messages.update((msgs) => [
...msgs,
{
role: 'error',
text: error?.error?.message || error?.message || 'Failed to apply schema changes.',
},
]);
} finally {
this.applying.set(false);
}
Expand All @@ -210,13 +234,15 @@ export class EditDatabaseSchemaComponent implements OnInit {
if (!batch?.batchId) return;

await this._tableSchema.rejectBatch(batch.batchId);
this.messages.update(msgs => msgs
.filter(m => !(m.role === 'diagram' && m.text === 'Schema Preview'))
.map(m => m === batch ? { ...m, batchId: undefined } : m)
.concat({
role: 'ai',
text: 'Changes rejected. Feel free to describe what you need differently.',
}));
this.messages.update((msgs) =>
msgs
.filter((m) => !(m.role === 'diagram' && m.text === 'Schema Preview'))
.map((m) => (m === batch ? { ...m, batchId: undefined } : m))
.concat({
role: 'ai',
text: 'Changes rejected. Feel free to describe what you need differently.',
}),
);
}

onOpenTables() {
Expand All @@ -236,11 +262,14 @@ export class EditDatabaseSchemaComponent implements OnInit {
const diagram = await this._tableSchema.fetchDiagram(this.connectionID);
const source = diagram?.diagram ?? '';
if (this._mermaidHasEntities(source)) {
this.messages.update(msgs => [...msgs, {
role: 'diagram' as const,
text: label,
diagramSource: '```mermaid\n' + source + '\n```',
}]);
this.messages.update((msgs) => [
...msgs,
{
role: 'diagram' as const,
text: label,
diagramSource: '```mermaid\n' + source + '\n```',
},
]);
}
} catch {
// Diagram is supplementary - don't show error if it fails
Expand All @@ -257,107 +286,6 @@ export class EditDatabaseSchemaComponent implements OnInit {
return false;
}

private _buildMermaidFromChanges(changes: SchemaChangeResponse[]): string {
const parser = new Parser();
const tables: { name: string; columns: { type: string; name: string; pk: boolean; fk: boolean }[] }[] = [];
const relationKeys = new Set<string>();
const relations: { from: string; to: string }[] = [];

for (const change of changes) {
const sql = change.forwardSql?.trim();
if (!sql) continue;

let parsed: unknown;
try {
parsed = parser.astify(sql, { database: 'PostgresQL' });
} catch {
try {
parsed = parser.astify(sql, { database: 'MySQL' });
} catch {
continue;
}
}

const nodes = Array.isArray(parsed) ? parsed : [parsed];
for (const node of nodes) {
const ast = node as Record<string, unknown> | null;
if (!ast || ast['type'] !== 'create' || ast['keyword'] !== 'table') continue;

const tableName = this._extractTableName(ast['table']);
if (!tableName) continue;

const columns: { type: string; name: string; pk: boolean; fk: boolean }[] = [];
const colIndex = new Map<string, number>();
const defs = (ast['create_definitions'] as unknown[]) ?? [];

for (const rawDef of defs) {
const def = rawDef as Record<string, unknown>;
if (def['resource'] === 'column') {
const colName = this._extractColumnName(def['column']);
if (!colName) continue;
const dataType = (def['definition'] as { dataType?: string } | undefined)?.dataType ?? '';
const primary = def['primary'] as string | undefined;
const pk = primary === 'primary key' || primary === 'key';
const ref = this._extractReferenceTable(def['reference_definition']);
const fk = !!ref;
if (ref) this._pushRelation(relations, relationKeys, tableName, ref);
colIndex.set(colName, columns.length);
columns.push({ name: colName, type: dataType.toLowerCase(), pk, fk });
} else if (def['resource'] === 'constraint') {
const ctype = (def['constraint_type'] as string | undefined)?.toLowerCase();
const colRefs = (def['definition'] as unknown[]) ?? [];
if (ctype === 'primary key') {
for (const c of colRefs) {
const name = this._extractColumnName(c);
if (!name) continue;
const i = colIndex.get(name);
if (i !== undefined) columns[i].pk = true;
}
} else if (ctype === 'foreign key') {
const ref = this._extractReferenceTable(def['reference_definition']);
if (!ref) continue;
for (const c of colRefs) {
const name = this._extractColumnName(c);
if (name) {
const i = colIndex.get(name);
if (i !== undefined) columns[i].fk = true;
}
}
this._pushRelation(relations, relationKeys, tableName, ref);
}
}
}

if (columns.length > 0) tables.push({ name: tableName, columns });
}
}

if (tables.length === 0) return '';

let out = 'erDiagram\n';
for (const rel of relations) {
if (tables.some(t => t.name === rel.to)) {
out += ` ${rel.to} ||--o{ ${rel.from} : has\n`;
}
}
for (const t of tables) {
out += ` ${t.name} {\n`;
for (const col of t.columns) {
const tag = col.pk ? ' PK' : col.fk ? ' FK' : '';
out += ` ${col.type || 'string'} ${col.name}${tag}\n`;
}
out += ` }\n`;
}
return out;
}

private _extractTableName(value: unknown): string | null {
if (!value) return null;
const first = Array.isArray(value) ? value[0] : value;
const name = (first as { table?: unknown })?.table;
return typeof name === 'string' ? name : null;
}

private _extractColumnName(value: unknown): string | null {
if (!value) return null;
const inner = (value as { column?: unknown }).column ?? value;
Expand All @@ -376,18 +304,6 @@ export class EditDatabaseSchemaComponent implements OnInit {
return typeof name === 'string' ? name : null;
}

private _pushRelation(
relations: { from: string; to: string }[],
seen: Set<string>,
from: string,
to: string,
): void {
const key = `${from}|${to}`;
if (seen.has(key)) return;
seen.add(key);
relations.push({ from, to });
}

private _changeMeta(changeType: string): { action: string; icon: string; tone: 'add' | 'edit' | 'remove' } {
const map: Record<string, { action: string; icon: string; tone: 'add' | 'edit' | 'remove' }> = {
CREATE_TABLE: { action: 'Create table', icon: 'add_box', tone: 'add' },
Expand Down Expand Up @@ -415,11 +331,16 @@ export class EditDatabaseSchemaComponent implements OnInit {
ROLLBACK: { action: 'Rollback', icon: 'undo', tone: 'edit' },
};
const upper = (changeType ?? '').toUpperCase();
return map[upper] ?? {
action: upper.replace(/_/g, ' ').toLowerCase().replace(/\b\w/, c => c.toUpperCase()),
icon: 'code',
tone: 'edit',
};
return (
map[upper] ?? {
action: upper
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/, (c) => c.toUpperCase()),
icon: 'code',
tone: 'edit',
}
);
}

private _parseChange(change: SchemaChangeResponse): ParsedChange {
Expand Down Expand Up @@ -515,7 +436,7 @@ export class EditDatabaseSchemaComponent implements OnInit {
const v = (inner as { value?: unknown }).value;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
if ((inner as { type?: string }).type === 'function') {
const name = ((inner as { name?: { name?: { value?: string }[] } }).name?.name?.[0]?.value) ?? '';
const name = (inner as { name?: { name?: { value?: string }[] } }).name?.name?.[0]?.value ?? '';
return name ? `${name.toUpperCase()}()` : undefined;
}
return undefined;
Expand Down
Loading
Loading