UNPKG

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
"use strict"; 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-';