quick-erd
Version:
quick and easy text-based ERD + code generator for migration, query, typescript types and orm entity
395 lines (394 loc) • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelationTypes = exports.Parser = void 0;
exports.parse = parse;
exports.unwrapQuotedName = unwrapQuotedName;
const enum_1 = require("./enum");
const meta_1 = require("./meta");
function parse(input) {
const parser = new Parser();
parser.parse(input);
return parser;
}
class Parser {
table_list = [];
line_list = [];
zoom;
view;
textBgColor;
diagramTextColor;
textColor;
diagramBgColor;
tableBgColor;
tableTextColor;
parse(input) {
input.split('\n').forEach(line => {
line = stripComments(line);
if (!line)
return;
this.line_list.push(line);
});
this.table_list = [];
while (this.hasTable()) {
this.table_list.push(this.parseTable());
}
this.parseMeta(input);
}
parseMeta(input) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
const zoom = +input.match(meta_1.zoomValueRegex)?.[1];
if (zoom)
this.zoom = zoom;
const view = input.match(meta_1.viewPositionRegex);
if (view)
this.view = { x: +view[1], y: +view[2] };
const textBgColor = input.match(meta_1.textBgColorRegex);
if (textBgColor)
this.textBgColor = textBgColor[1];
const textColor = input.match(meta_1.textColorRegex);
if (textColor)
this.textColor = textColor[1];
const diagramBgColor = input.match(meta_1.diagramBgColorRegex);
if (diagramBgColor)
this.diagramBgColor = diagramBgColor[1];
const diagramTextColor = input.match(meta_1.diagramTextColorRegex);
if (diagramTextColor)
this.diagramTextColor = diagramTextColor[1];
const tableBgColor = input.match(meta_1.tableBgColorRegex);
if (tableBgColor)
this.tableBgColor = tableBgColor[1];
const tableTextColor = input.match(meta_1.tableTextColorRegex);
if (tableTextColor)
this.tableTextColor = tableTextColor[1];
input.match(meta_1.tableNameRegex_g)?.forEach(line => {
const match = line.match(meta_1.tableNameRegex) || [];
const name = match[1];
const x = +match[2];
const y = +match[3];
const color = match[4];
const table = this.table_list.find(table => table.name == name);
if (table)
table.position = { x, y, color };
});
}
peekLine() {
if (this.line_list.length === 0) {
throw new Error('no reminding line');
}
return this.line_list[0];
}
hasTable() {
while (this.line_list[0] === '')
this.line_list.shift();
return this.line_list[0] && this.line_list[1]?.startsWith('-');
}
parseTable() {
const name = this.parseName();
this.parseEmptyLine();
this.skipLine('-');
const field_list = parseAll(() => {
// skip empty lines
if (this.hasTable()) {
throw new Error('end of table');
}
return this.parseField();
});
const has_primary_key = field_list.some(field => field.is_primary_key);
if (!has_primary_key) {
const field = field_list.find(field => field.name === 'id');
if (field) {
field.is_primary_key = true;
}
}
return { name, field_list };
}
parseField() {
const field = {
name: this.parseName(),
type: '',
is_null: false,
is_unique: false,
is_primary_key: false,
is_unsigned: false,
is_zerofill: false,
default_value: undefined,
references: undefined,
collate: undefined,
};
for (;;) {
this.parseFieldModifiers(field);
const token = this.parseType();
if (!token)
break;
switch (token.toUpperCase()) {
case 'DEFAULT':
// TODO parse default value
field.default_value = this.parseDefaultValue(field);
continue;
case 'FK':
field.references = this.parseForeignKeyReference(field);
continue;
case 'COLLATE':
field.collate = this.parseCollateValue();
continue;
default:
if (field.type) {
console.debug('unexpected token:', {
field_name: field.name,
type: field.type,
token: token,
});
continue;
}
field.type = token;
}
}
field.type ||= defaultFieldType;
this.skipLine();
return field;
}
parseCollateValue() {
let line = this.peekLine();
const match = line.match(/^[\w-]+/);
if (!match) {
throw new Error('missing collate value');
}
const value = match[0];
line = line.slice(value.length).trim();
this.line_list[0] = line;
return value;
}
parseFieldModifiers(field) {
let line = this.peekLine();
loop: for (;;) {
let match = line.match(/^not null\s*/i);
if (match) {
field.is_null = false;
line = line.slice(match[0].length);
continue;
}
match = line.match(/^\w+/);
if (!match)
break;
const token = match[0];
switch (token.toUpperCase()) {
case 'NULL':
field.is_null = true;
break;
case 'UNIQUE':
field.is_unique = true;
break;
case 'UNSIGNED':
field.is_unsigned = true;
break;
case 'ZEROFILL':
field.is_zerofill = true;
break;
case 'PK':
field.is_primary_key = true;
break;
default:
// no single token modifier
break loop;
}
line = line.replace(token, '').trim();
}
this.line_list[0] = line;
}
skipLine(line = '') {
if (this.line_list[0]?.startsWith(line)) {
this.line_list.shift();
}
}
parseEmptyLine() {
const line = this.line_list[0]?.trim();
if (line !== '') {
throw new NonEmptyLineError(line);
}
this.line_list.shift();
}
parseName() {
let line = this.peekLine();
const match = line.match(/["'`a-zA-Z0-9_]+/);
if (!match) {
throw new ParseNameError(line);
}
const name = match[0];
line = line.replace(name, '').trim();
this.line_list[0] = line;
return unwrapQuotedName(name);
}
parseType() {
let line = this.peekLine();
let match = line.match(/^not null\s*/i);
if (match) {
line = line.slice(match[0].length);
}
match = line.match(/^\w+\(.*?\)/) || line.match(/^[a-zA-Z0-9_(),"']+/);
if (!match) {
return;
}
const name = match[0];
line = line.replace(name, '').trim();
this.line_list[0] = line;
if (name.match(/^enum/i)) {
return (0, enum_1.formatEnum)(name);
}
return name;
}
parseDefaultValue(field) {
this.parseFieldModifiers(field);
let line = this.peekLine();
let end;
if (line[0] === '"') {
end = line.indexOf('"', 1) + 1;
}
else if (line[0] === "'") {
end = line.indexOf("'", 1) + 1;
}
else if (line[0] === '`') {
end = line.indexOf('`', 1) + 1;
}
else if (line.includes(' ')) {
end = line.indexOf(' ');
}
else {
end = line.length - 1;
}
const value = line.slice(0, end + 1);
line = line.replace(value, '').trim();
this.line_list[0] = line;
return value;
}
parseRelationType() {
let line = this.peekLine();
for (const type of exports.RelationTypes) {
if (line.startsWith(type)) {
line = line.slice(type.length).trim();
this.line_list[0] = line;
return type;
}
}
line = line.trim();
let match = line.match(/^\w+/);
if (line && !match) {
throw new ParseRelationTypeError(line);
}
return defaultRelationType;
}
parseForeignKeyReference(field) {
let ref_field_name = field.name;
this.parseFieldModifiers(field);
if (ref_field_name.endsWith('_id') && this.peekLine() === '') {
return {
table: ref_field_name.replace(/_id$/, ''),
field: 'id',
type: defaultRelationType,
};
}
this.parseFieldModifiers(field);
const type = this.parseRelationType();
this.parseFieldModifiers(field);
const table = this.parseName();
this.parseFieldModifiers(field);
const line = this.peekLine();
let field_name;
if (line == '') {
field_name = 'id';
}
else if (line.startsWith('.')) {
this.line_list[0] = line.slice(1);
field_name = this.parseName();
}
else {
throw new ParseForeignKeyReferenceError(line);
}
return { type, table, field: field_name };
}
}
exports.Parser = Parser;
class NonEmptyLineError extends Error {
}
class LineError extends Error {
line;
constructor(line, message) {
super(message);
this.line = line;
}
}
class ParseNameError extends LineError {
}
class ParseRelationTypeError extends LineError {
}
class ParseForeignKeyReferenceError extends LineError {
line;
constructor(line) {
super(line, `expect '.', got '${line[0]}'`);
this.line = line;
}
}
function parseAll(fn) {
const result_list = [];
for (;;) {
try {
result_list.push(fn());
}
catch (error) {
return result_list;
}
}
}
const quotes = ['"', "'", '`'];
function unwrapQuotedName(name) {
for (const quote of quotes) {
if (name.startsWith(quote) && name.endsWith(quote)) {
name = name.slice(1, name.length - 1);
break;
}
}
return name;
}
function stripComments(line) {
// check for enum with comment symbols
let match = line.match(/(enum\(.*?\))/i);
if (match) {
let parts = line.split(match[0]);
let before = parts[0];
let strippedBefore = stripComments(before);
if (strippedBefore !== before.trim()) {
return strippedBefore;
}
let enumStr = match[0];
let after = parts.slice(1).join(enumStr);
return before + enumStr + stripComments(after);
}
line = line
.trim()
// strip `#` comments
.replace(/#.*/, '')
// strip `//` comments
.replace(/\/\/.*/, '')
.trim();
// if line is only dashes, keep it as delimiter after table name, otherwise strip it as comment
if (!line.match(/^-+$/)) {
// strip `--` comments
line = line.replace(/--.*/, '').trim();
}
return line;
}
exports.RelationTypes = [
// 3 chars tokens first
'>0-',
'-0<',
'>-<',
'0-0',
// then 2 chars tokens
'>-',
'-<',
'0-',
'-0',
// finally 1 char tokens
'-',
];
const defaultFieldType = 'integer';
const defaultRelationType = '>0-';