mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-29 11:03:58 +00:00
305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
import { Parser } from '@dbml/core';
|
|
import type { Diagram } from '@/lib/domain/diagram';
|
|
import { generateDiagramId, generateId } from '@/lib/utils';
|
|
import type { DBTable } from '@/lib/domain/db-table';
|
|
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
|
|
import type { DBField } from '@/lib/domain/db-field';
|
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
|
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
|
|
import { randomColor } from '@/lib/colors';
|
|
import { DatabaseType } from '@/lib/domain/database-type';
|
|
|
|
interface DBMLTypeArgs {
|
|
length?: number;
|
|
precision?: number;
|
|
scale?: number;
|
|
values?: string[]; // For enum types
|
|
}
|
|
|
|
interface DBMLField {
|
|
name: string;
|
|
type: {
|
|
type_name: string;
|
|
args?: DBMLTypeArgs;
|
|
};
|
|
unique?: boolean;
|
|
pk?: boolean;
|
|
not_null?: boolean;
|
|
increment?: boolean;
|
|
}
|
|
|
|
interface DBMLIndexColumn {
|
|
value: string;
|
|
type?: string;
|
|
length?: number;
|
|
order?: 'asc' | 'desc';
|
|
}
|
|
|
|
interface DBMLIndex {
|
|
columns: string | (string | DBMLIndexColumn)[];
|
|
unique?: boolean;
|
|
name?: string;
|
|
}
|
|
|
|
interface DBMLTable {
|
|
name: string;
|
|
schema?: string | { name: string };
|
|
fields: DBMLField[];
|
|
indexes?: DBMLIndex[];
|
|
}
|
|
|
|
interface DBMLEndpoint {
|
|
tableName: string;
|
|
fieldNames: string[];
|
|
relation: string;
|
|
}
|
|
|
|
interface DBMLRef {
|
|
endpoints: [DBMLEndpoint, DBMLEndpoint];
|
|
}
|
|
|
|
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
|
|
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
|
|
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
|
|
if (matchedType) return matchedType;
|
|
const typeMap: Record<string, string> = {
|
|
int: 'integer',
|
|
varchar: 'varchar',
|
|
bool: 'boolean',
|
|
number: 'numeric',
|
|
string: 'varchar',
|
|
text: 'text',
|
|
timestamp: 'timestamp',
|
|
datetime: 'timestamp',
|
|
float: 'float',
|
|
double: 'double',
|
|
decimal: 'decimal',
|
|
bigint: 'bigint',
|
|
smallint: 'smallint',
|
|
char: 'char',
|
|
};
|
|
const mappedType = typeMap[normalizedType];
|
|
if (mappedType) {
|
|
const foundType = genericDataTypes.find((t) => t.id === mappedType);
|
|
if (foundType) return foundType;
|
|
}
|
|
return genericDataTypes.find((t) => t.id === 'varchar')!;
|
|
};
|
|
|
|
const determineCardinality = (
|
|
field: DBField,
|
|
referencedField: DBField
|
|
): { sourceCardinality: string; targetCardinality: string } => {
|
|
const isSourceUnique = field.unique || field.primaryKey;
|
|
const isTargetUnique = referencedField.unique || referencedField.primaryKey;
|
|
if (isSourceUnique && isTargetUnique) {
|
|
return { sourceCardinality: 'one', targetCardinality: 'one' };
|
|
} else if (isSourceUnique) {
|
|
return { sourceCardinality: 'one', targetCardinality: 'many' };
|
|
} else if (isTargetUnique) {
|
|
return { sourceCardinality: 'many', targetCardinality: 'one' };
|
|
} else {
|
|
return { sourceCardinality: 'many', targetCardinality: 'many' };
|
|
}
|
|
};
|
|
|
|
export const importDBMLToDiagram = async (
|
|
dbmlContent: string
|
|
): Promise<Diagram> => {
|
|
try {
|
|
const parser = new Parser();
|
|
const parsedData = parser.parse(dbmlContent, 'dbml');
|
|
const dbmlData = parsedData.schemas[0];
|
|
|
|
// Extract only the necessary data from the parsed DBML
|
|
const extractedData = {
|
|
tables: (dbmlData.tables as unknown as DBMLTable[]).map(
|
|
(table) => ({
|
|
name: table.name,
|
|
schema: table.schema,
|
|
fields: table.fields.map((field: DBMLField) => ({
|
|
name: field.name,
|
|
type: field.type,
|
|
unique: field.unique,
|
|
pk: field.pk,
|
|
not_null: field.not_null,
|
|
increment: field.increment,
|
|
})),
|
|
indexes:
|
|
table.indexes?.map((dbmlIndex) => {
|
|
let indexColumns: string[];
|
|
|
|
// Handle composite index case "(col1, col2)"
|
|
if (typeof dbmlIndex.columns === 'string') {
|
|
if (dbmlIndex.columns.includes('(')) {
|
|
// Composite index
|
|
const columnsStr =
|
|
dbmlIndex.columns.replace(/[()]/g, '');
|
|
indexColumns = columnsStr
|
|
.split(',')
|
|
.map((c) => c.trim());
|
|
} else {
|
|
// Single column
|
|
indexColumns = [dbmlIndex.columns.trim()];
|
|
}
|
|
} else {
|
|
// Handle array of columns
|
|
indexColumns = Array.isArray(dbmlIndex.columns)
|
|
? dbmlIndex.columns.map((col) =>
|
|
typeof col === 'object' &&
|
|
'value' in col
|
|
? (col.value as string).trim()
|
|
: (col as string).trim()
|
|
)
|
|
: [String(dbmlIndex.columns).trim()];
|
|
}
|
|
|
|
// Generate a consistent index name
|
|
const indexName =
|
|
dbmlIndex.name ||
|
|
`idx_${table.name}_${indexColumns.join('_')}`;
|
|
|
|
return {
|
|
columns: indexColumns,
|
|
unique: dbmlIndex.unique || false,
|
|
name: indexName,
|
|
};
|
|
}) || [],
|
|
})
|
|
),
|
|
refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
|
|
endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
|
|
(endpoint) => ({
|
|
tableName: endpoint.tableName,
|
|
fieldNames: endpoint.fieldNames,
|
|
relation: endpoint.relation,
|
|
})
|
|
),
|
|
})),
|
|
};
|
|
|
|
// Convert DBML tables to ChartDB table objects
|
|
const tables: DBTable[] = extractedData.tables.map((table, index) => {
|
|
const row = Math.floor(index / 4);
|
|
const col = index % 4;
|
|
const tableSpacing = 300;
|
|
|
|
// Create fields first so we have their IDs
|
|
const fields = table.fields.map((field) => ({
|
|
id: generateId(),
|
|
name: field.name.replace(/['"]/g, ''),
|
|
type: mapDBMLTypeToGenericType(field.type.type_name),
|
|
nullable: !field.not_null,
|
|
primaryKey: field.pk || false,
|
|
unique: field.unique || false,
|
|
createdAt: Date.now(),
|
|
}));
|
|
|
|
// Convert DBML indexes to ChartDB indexes
|
|
const indexes =
|
|
table.indexes?.map((dbmlIndex) => {
|
|
const fieldIds = dbmlIndex.columns.map((columnName) => {
|
|
const field = fields.find((f) => f.name === columnName);
|
|
if (!field) {
|
|
throw new Error(
|
|
`Index references non-existent column: ${columnName}`
|
|
);
|
|
}
|
|
return field.id;
|
|
});
|
|
|
|
return {
|
|
id: generateId(),
|
|
name:
|
|
dbmlIndex.name ||
|
|
`idx_${table.name}_${dbmlIndex.columns.join('_')}`,
|
|
fieldIds,
|
|
unique: dbmlIndex.unique || false,
|
|
createdAt: Date.now(),
|
|
};
|
|
}) || [];
|
|
|
|
return {
|
|
id: generateId(),
|
|
name: table.name.replace(/['"]/g, ''),
|
|
schema:
|
|
typeof table.schema === 'string'
|
|
? table.schema
|
|
: table.schema?.name || '',
|
|
order: index,
|
|
fields,
|
|
indexes,
|
|
x: col * tableSpacing,
|
|
y: row * tableSpacing,
|
|
color: randomColor(),
|
|
isView: false,
|
|
createdAt: Date.now(),
|
|
};
|
|
});
|
|
|
|
// Create relationships using the refs
|
|
const relationships: DBRelationship[] = extractedData.refs.map(
|
|
(ref) => {
|
|
const [source, target] = ref.endpoints;
|
|
const sourceTable = tables.find(
|
|
(t) =>
|
|
t.name === source.tableName.replace(/['"]/g, '') &&
|
|
(!source.tableName.includes('.') ||
|
|
t.schema === source.tableName.split('.')[0])
|
|
);
|
|
const targetTable = tables.find(
|
|
(t) =>
|
|
t.name === target.tableName.replace(/['"]/g, '') &&
|
|
(!target.tableName.includes('.') ||
|
|
t.schema === target.tableName.split('.')[0])
|
|
);
|
|
|
|
if (!sourceTable || !targetTable) {
|
|
throw new Error('Invalid relationship: tables not found');
|
|
}
|
|
|
|
const sourceField = sourceTable.fields.find(
|
|
(f) => f.name === source.fieldNames[0].replace(/['"]/g, '')
|
|
);
|
|
const targetField = targetTable.fields.find(
|
|
(f) => f.name === target.fieldNames[0].replace(/['"]/g, '')
|
|
);
|
|
|
|
if (!sourceField || !targetField) {
|
|
throw new Error('Invalid relationship: fields not found');
|
|
}
|
|
|
|
const { sourceCardinality, targetCardinality } =
|
|
determineCardinality(sourceField, targetField);
|
|
|
|
return {
|
|
id: generateId(),
|
|
name: `${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`,
|
|
sourceSchema: sourceTable.schema,
|
|
targetSchema: targetTable.schema,
|
|
sourceTableId: sourceTable.id,
|
|
targetTableId: targetTable.id,
|
|
sourceFieldId: sourceField.id,
|
|
targetFieldId: targetField.id,
|
|
sourceCardinality: sourceCardinality as Cardinality,
|
|
targetCardinality: targetCardinality as Cardinality,
|
|
createdAt: Date.now(),
|
|
};
|
|
}
|
|
);
|
|
|
|
return {
|
|
id: generateDiagramId(),
|
|
name: 'DBML Import',
|
|
databaseType: DatabaseType.GENERIC,
|
|
tables,
|
|
relationships,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
} catch (error) {
|
|
console.error('DBML parsing error:', error);
|
|
throw error;
|
|
}
|
|
};
|