From 74b69266d5e4c24a6acee9ec709e7bae7a4a5549 Mon Sep 17 00:00:00 2001 From: FunamaYukina Date: Wed, 4 Dec 2024 09:27:00 +0900 Subject: [PATCH 1/5] [add] support for postgres format in cli build command --- frontend/packages/cli/README.md | 6 +- frontend/packages/cli/fixtures/input.sql | 14 ++-- frontend/packages/cli/package.json | 2 +- .../cli/src/cli/commands/buildCommand.ts | 9 ++- frontend/packages/cli/src/cli/index.test.ts | 28 +++++--- frontend/packages/cli/src/cli/index.ts | 3 +- .../cli/src/cli/runPreprocess.test.ts | 66 +++++++++++++++---- .../packages/cli/src/cli/runPreprocess.ts | 27 ++++++-- frontend/packages/cli/src/cli/smoke.test.ts | 2 +- frontend/packages/db-structure/src/parser.ts | 2 +- .../packages/db-structure/src/parser/index.ts | 2 +- 11 files changed, 120 insertions(+), 41 deletions(-) diff --git a/frontend/packages/cli/README.md b/frontend/packages/cli/README.md index e85f21352..5ef7de475 100644 --- a/frontend/packages/cli/README.md +++ b/frontend/packages/cli/README.md @@ -3,7 +3,7 @@ Command-line tool designed to generate a web application that displays ER diagrams. ```bash -$ liam erd build --input {your .sql} +$ liam erd build --input {your .sql} --format { schemarb | postgres } # Outputs the web application to the ./public and ./dist directories $ liam erd preview @@ -28,7 +28,7 @@ pnpm run build After building, you can invoke it locally with: ```bash -node ./dist-cli/bin/cli.js erd build --input ./fixtures/input.schema.rb +node ./dist-cli/bin/cli.js erd build --input ./fixtures/input.schema.rb --format schemarb ``` To make it globally accessible as `liam`, use: @@ -47,7 +47,7 @@ pnpm link --global ``` This command currently performs the following actions: - Builds the CLI. - - Executes the CLI with the command `erd build --input ./fixtures/input.schema.rb`. + - Executes the CLI with the command `erd build --input ./fixtures/input.schema.rb --format schemarb`. - Copies the generated `schema.json` to the `public/` directory and launches the Vite development server. ## Project File Structure diff --git a/frontend/packages/cli/fixtures/input.sql b/frontend/packages/cli/fixtures/input.sql index 691c79079..9d96fae68 100644 --- a/frontend/packages/cli/fixtures/input.sql +++ b/frontend/packages/cli/fixtures/input.sql @@ -1,5 +1,11 @@ -CREATE TABLE products ( - brand VARCHAR(255), - model VARCHAR(255), - year INT +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE posts ( + id SERIAL PRIMARY KEY, + user_id INT REFERENCES users(id) ); diff --git a/frontend/packages/cli/package.json b/frontend/packages/cli/package.json index 1bd73cd13..b0cc6764a 100644 --- a/frontend/packages/cli/package.json +++ b/frontend/packages/cli/package.json @@ -38,7 +38,7 @@ "build": "pnpm run '/^build:.*/'", "build:cli": "rollup -c && pnpm run cp:prism", "build:vite": "vite build", - "command:build": "node ./dist-cli/bin/cli.js erd build --input fixtures/input.schema.rb", + "command:build": "node ./dist-cli/bin/cli.js erd build --input fixtures/input.schema.rb --format schemarb", "cp:prism": "cp ../db-structure/node_modules/@ruby/prism/src/prism.wasm ./dist-cli/bin/prism.wasm", "dev": "pnpm command:build && cp dist/schema.json public/ && pnpm run '/^dev:.*/'", "dev:app": "vite", diff --git a/frontend/packages/cli/src/cli/commands/buildCommand.ts b/frontend/packages/cli/src/cli/commands/buildCommand.ts index 10ca15f7b..a43219144 100644 --- a/frontend/packages/cli/src/cli/commands/buildCommand.ts +++ b/frontend/packages/cli/src/cli/commands/buildCommand.ts @@ -3,11 +3,16 @@ import { existsSync } from 'node:fs' import { dirname } from 'node:path' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import type { SupportedFormat } from '@liam-hq/db-structure/parser' import { runPreprocess } from '../runPreprocess.js' -export const buildCommand = async (inputPath: string, outDir: string) => { +export const buildCommand = async ( + inputPath: string, + outDir: string, + format: SupportedFormat, +) => { // generate schema.json - runPreprocess(inputPath, outDir) + runPreprocess(inputPath, outDir, format) // generate index.html const __filename = fileURLToPath(import.meta.url) diff --git a/frontend/packages/cli/src/cli/index.test.ts b/frontend/packages/cli/src/cli/index.test.ts index 0b9e35b65..e2db73bcb 100644 --- a/frontend/packages/cli/src/cli/index.test.ts +++ b/frontend/packages/cli/src/cli/index.test.ts @@ -35,14 +35,24 @@ describe('program', () => { }) describe('commands', () => { - it('should call buildCommand when "build" command is executed', () => { - program.parse(['erd', 'build', '--input', 'path/to/file.sql'], { - from: 'user', - }) - expect(buildCommand).toHaveBeenCalledWith( - 'path/to/file.sql', - expect.stringMatching(/\/dist$/), - ) - }) + it.each([['schemarb'], ['postgres']])( + 'should call buildCommand with correct arguments for format %s', + (format) => { + const inputFile = './fixtures/input.schema.rb' + + program.parse( + ['erd', 'build', '--input', inputFile, '--format', format], + { + from: 'user', + }, + ) + + expect(buildCommand).toHaveBeenCalledWith( + inputFile, + expect.stringContaining('dist'), + format, + ) + }, + ) }) }) diff --git a/frontend/packages/cli/src/cli/index.ts b/frontend/packages/cli/src/cli/index.ts index 3c13bd71c..239892c69 100644 --- a/frontend/packages/cli/src/cli/index.ts +++ b/frontend/packages/cli/src/cli/index.ts @@ -15,6 +15,7 @@ erdCommand .command('build') .description('Build ERD html assets') .option('--input ', 'Path to the .sql file') - .action((options) => buildCommand(options.input, distDir)) + .option('--format ', 'Format of the input file') + .action((options) => buildCommand(options.input, distDir, options.format)) export { program } diff --git a/frontend/packages/cli/src/cli/runPreprocess.test.ts b/frontend/packages/cli/src/cli/runPreprocess.test.ts index 89a79c556..b581c645f 100644 --- a/frontend/packages/cli/src/cli/runPreprocess.test.ts +++ b/frontend/packages/cli/src/cli/runPreprocess.test.ts @@ -1,32 +1,72 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' +import type { SupportedFormat } from '@liam-hq/db-structure/parser' import { describe, expect, it } from 'vitest' import { runPreprocess } from './runPreprocess' describe('runPreprocess', () => { - it('should create schema.json with the SQL content in the specified directory', async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-distDir-')) - const inputPath = path.join(tmpDir, 'input.sql') + const testCases = [ + { + format: 'postgres', + inputFilename: 'input.sql', + content: 'CREATE TABLE test (id INT, name VARCHAR(255));', + }, + { + format: 'schemarb', + inputFilename: 'input.schema.rb', + content: ` + create_table "test" do |t| + t.integer "id" + t.string "name", limit: 255 + end + `, + }, + ] - const sqlContent = 'CREATE TABLE test (id INT, name VARCHAR(255));' - fs.writeFileSync(inputPath, sqlContent, 'utf8') + it.each(testCases)( + 'should create schema.json with the content in $format format', + async ({ format, inputFilename, content }) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-distDir-')) + const inputPath = path.join(tmpDir, inputFilename) - const outputFilePath = await runPreprocess(inputPath, tmpDir) - if (!outputFilePath) throw new Error('Implement the test') + fs.writeFileSync(inputPath, content, 'utf8') - expect(fs.existsSync(outputFilePath)).toBe(true) + const outputFilePath = await runPreprocess( + inputPath, + tmpDir, + format as SupportedFormat, + ) + if (!outputFilePath) throw new Error('Failed to run preprocess') - const outputContent = JSON.parse(fs.readFileSync(outputFilePath, 'utf8')) - expect(outputContent.tables).toBeDefined() - }) + expect(fs.existsSync(outputFilePath)).toBe(true) + + // Validate output file content + const outputContent = JSON.parse(fs.readFileSync(outputFilePath, 'utf8')) + expect(outputContent.tables).toBeDefined() + }, + ) it('should throw an error if the input file does not exist', async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-distDir-')) const nonExistentPath = path.join(tmpDir, 'non-existent.sql') - await expect(runPreprocess(nonExistentPath, tmpDir)).rejects.toThrow( - 'Invalid input path. Please provide a valid file.', + await expect( + runPreprocess(nonExistentPath, tmpDir, 'postgres'), + ).rejects.toThrow('Invalid input path. Please provide a valid file.') + }) + + it('should throw an error if the format is invalid', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-distDir-')) + const inputPath = path.join(tmpDir, 'input.sql') + fs.writeFileSync( + inputPath, + 'CREATE TABLE test (id INT, name VARCHAR(255));', + 'utf8', ) + + await expect( + runPreprocess(inputPath, tmpDir, 'invalid' as SupportedFormat), + ).rejects.toThrow('Invalid format') }) }) diff --git a/frontend/packages/cli/src/cli/runPreprocess.ts b/frontend/packages/cli/src/cli/runPreprocess.ts index dadcd5bd7..99ded4f96 100644 --- a/frontend/packages/cli/src/cli/runPreprocess.ts +++ b/frontend/packages/cli/src/cli/runPreprocess.ts @@ -1,17 +1,34 @@ import fs, { readFileSync } from 'node:fs' import path from 'node:path' -import { parse } from '@liam-hq/db-structure/parser' +import { type SupportedFormat, parse } from '@liam-hq/db-structure/parser' -export async function runPreprocess(inputPath: string, outputDir: string) { +export async function runPreprocess( + inputPath: string, + outputDir: string, + format: SupportedFormat, +) { if (!fs.existsSync(inputPath)) { throw new Error('Invalid input path. Please provide a valid file.') } const input = readFileSync(inputPath, 'utf8') - // TODO: Expand support to additional formats, e.g., 'postgres' - const format = 'schemarb' - const json = await parse(input, format) + let json = null + if ( + format === 'schemarb' || + format === 'postgres' || + format === 'schemarb-prism' + ) { + try { + json = await parse(input, format) + } catch (error) { + throw new Error( + `Failed to parse ${format} file: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } else { + throw new Error('Invalid format') + } const filePath = path.join(outputDir, 'schema.json') diff --git a/frontend/packages/cli/src/cli/smoke.test.ts b/frontend/packages/cli/src/cli/smoke.test.ts index 9aaa257dc..47419c146 100644 --- a/frontend/packages/cli/src/cli/smoke.test.ts +++ b/frontend/packages/cli/src/cli/smoke.test.ts @@ -45,7 +45,7 @@ describe('CLI Smoke Test', () => { await execAsync('rm -rf ./dist') try { const { stdout, stderr } = await execAsync( - 'npx --no-install . erd build --input fixtures/input.schema.rb', + 'npx --no-install . erd build --input fixtures/input.schema.rb --format schemarb', ) // NOTE: suppress the following warning: if ( diff --git a/frontend/packages/db-structure/src/parser.ts b/frontend/packages/db-structure/src/parser.ts index 966b1e0c9..51bae096b 100644 --- a/frontend/packages/db-structure/src/parser.ts +++ b/frontend/packages/db-structure/src/parser.ts @@ -1 +1 @@ -export { parse } from './parser/index.js' +export { parse, type SupportedFormat } from './parser/index.js' diff --git a/frontend/packages/db-structure/src/parser/index.ts b/frontend/packages/db-structure/src/parser/index.ts index 06f773697..9a526265c 100644 --- a/frontend/packages/db-structure/src/parser/index.ts +++ b/frontend/packages/db-structure/src/parser/index.ts @@ -2,7 +2,7 @@ import type { DBStructure } from 'src/schema/index.js' import { processor as schemarbProcessor } from './schemarb/index.js' import { processor as postgresqlProcessor } from './sql/index.js' -type SupportedFormat = 'schemarb' | 'postgres' +export type SupportedFormat = 'schemarb' | 'postgres' // TODO: Add error handling and tests export const parse = ( From 5f8c214d71e4e05820fcd5c7f62f093f56d9f749 Mon Sep 17 00:00:00 2001 From: FunamaYukina Date: Wed, 4 Dec 2024 12:06:54 +0900 Subject: [PATCH 2/5] Modify the error message to give more details. --- frontend/packages/cli/src/cli/runPreprocess.test.ts | 4 +++- frontend/packages/cli/src/cli/runPreprocess.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/packages/cli/src/cli/runPreprocess.test.ts b/frontend/packages/cli/src/cli/runPreprocess.test.ts index 3280b89fd..f7f2387b8 100644 --- a/frontend/packages/cli/src/cli/runPreprocess.test.ts +++ b/frontend/packages/cli/src/cli/runPreprocess.test.ts @@ -67,6 +67,8 @@ describe('runPreprocess', () => { await expect( runPreprocess(inputPath, tmpDir, 'invalid' as SupportedFormat), - ).rejects.toThrow('Invalid format') + ).rejects.toThrow( + '--format is missing, invalid, or specifies an unsupported format. Please provide a valid format (e.g., "schemarb" or "postgres").', + ) }) }) diff --git a/frontend/packages/cli/src/cli/runPreprocess.ts b/frontend/packages/cli/src/cli/runPreprocess.ts index 99ded4f96..240ef761f 100644 --- a/frontend/packages/cli/src/cli/runPreprocess.ts +++ b/frontend/packages/cli/src/cli/runPreprocess.ts @@ -27,7 +27,9 @@ export async function runPreprocess( ) } } else { - throw new Error('Invalid format') + throw new Error( + '--format is missing, invalid, or specifies an unsupported format. Please provide a valid format (e.g., "schemarb" or "postgres").', + ) } const filePath = path.join(outputDir, 'schema.json') From 1fb8040a000ad287c129951c98025dba533d09e9 Mon Sep 17 00:00:00 2001 From: FunamaYukina Date: Wed, 4 Dec 2024 16:10:31 +0900 Subject: [PATCH 3/5] Update README.md to clarify usage examples for ERD command --- frontend/packages/cli/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/packages/cli/README.md b/frontend/packages/cli/README.md index 5ef7de475..066960421 100644 --- a/frontend/packages/cli/README.md +++ b/frontend/packages/cli/README.md @@ -3,9 +3,17 @@ Command-line tool designed to generate a web application that displays ER diagrams. ```bash -$ liam erd build --input {your .sql} --format { schemarb | postgres } +$ liam erd build --input {your .sql} --format postgres # Outputs the web application to the ./public and ./dist directories +``` +```bash +# Or use a `db/schema.rb` file (from your Ruby on Rails app). +$ liam erd build --input {your schema.rb} --format schemarb +# Outputs the web application to the `./dist` directory +``` + +```bash $ liam erd preview # Launches the web application for preview ``` From 36395c4b2824e096142e3b03f61aadfe37fb4b1d Mon Sep 17 00:00:00 2001 From: FunamaYukina Date: Wed, 4 Dec 2024 16:23:33 +0900 Subject: [PATCH 4/5] delete unsupported format --- frontend/packages/cli/src/cli/runPreprocess.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/packages/cli/src/cli/runPreprocess.ts b/frontend/packages/cli/src/cli/runPreprocess.ts index 240ef761f..0a87e8318 100644 --- a/frontend/packages/cli/src/cli/runPreprocess.ts +++ b/frontend/packages/cli/src/cli/runPreprocess.ts @@ -14,11 +14,7 @@ export async function runPreprocess( const input = readFileSync(inputPath, 'utf8') let json = null - if ( - format === 'schemarb' || - format === 'postgres' || - format === 'schemarb-prism' - ) { + if (format === 'schemarb' || format === 'postgres') { try { json = await parse(input, format) } catch (error) { From 3eb9d6798767f667dd5a51e2e40eab43737bea9d Mon Sep 17 00:00:00 2001 From: FunamaYukina Date: Wed, 4 Dec 2024 18:04:13 +0900 Subject: [PATCH 5/5] add supportedFormatSchema --- .../packages/cli/src/cli/runPreprocess.ts | 27 +++++++++++-------- frontend/packages/db-structure/src/parser.ts | 6 ++++- .../packages/db-structure/src/parser/index.ts | 8 +++++- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/frontend/packages/cli/src/cli/runPreprocess.ts b/frontend/packages/cli/src/cli/runPreprocess.ts index 0a87e8318..11712cb8d 100644 --- a/frontend/packages/cli/src/cli/runPreprocess.ts +++ b/frontend/packages/cli/src/cli/runPreprocess.ts @@ -1,6 +1,11 @@ import fs, { readFileSync } from 'node:fs' import path from 'node:path' -import { type SupportedFormat, parse } from '@liam-hq/db-structure/parser' +import { + type SupportedFormat, + parse, + supportedFormatSchema, +} from '@liam-hq/db-structure/parser' +import * as v from 'valibot' export async function runPreprocess( inputPath: string, @@ -13,21 +18,21 @@ export async function runPreprocess( const input = readFileSync(inputPath, 'utf8') - let json = null - if (format === 'schemarb' || format === 'postgres') { - try { - json = await parse(input, format) - } catch (error) { - throw new Error( - `Failed to parse ${format} file: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } else { + if (!v.safeParse(supportedFormatSchema, format).success) { throw new Error( '--format is missing, invalid, or specifies an unsupported format. Please provide a valid format (e.g., "schemarb" or "postgres").', ) } + let json = null + try { + json = await parse(input, format) + } catch (error) { + throw new Error( + `Failed to parse ${format} file: ${error instanceof Error ? error.message : String(error)}`, + ) + } + const filePath = path.join(outputDir, 'schema.json') if (!fs.existsSync(outputDir)) { diff --git a/frontend/packages/db-structure/src/parser.ts b/frontend/packages/db-structure/src/parser.ts index 51bae096b..5f4f6fdec 100644 --- a/frontend/packages/db-structure/src/parser.ts +++ b/frontend/packages/db-structure/src/parser.ts @@ -1 +1,5 @@ -export { parse, type SupportedFormat } from './parser/index.js' +export { + parse, + type SupportedFormat, + supportedFormatSchema, +} from './parser/index.js' diff --git a/frontend/packages/db-structure/src/parser/index.ts b/frontend/packages/db-structure/src/parser/index.ts index e29415165..eb7096efe 100644 --- a/frontend/packages/db-structure/src/parser/index.ts +++ b/frontend/packages/db-structure/src/parser/index.ts @@ -1,8 +1,14 @@ +import * as v from 'valibot' import type { DBStructure } from '../schema/index.js' import { processor as schemarbProcessor } from './schemarb/index.js' import { processor as postgresqlProcessor } from './sql/index.js' -export type SupportedFormat = 'schemarb' | 'postgres' +export const supportedFormatSchema = v.union([ + v.literal('schemarb'), + v.literal('postgres'), +]) + +export type SupportedFormat = v.InferOutput // TODO: Add error handling and tests export const parse = (