From a9017053adaa8ad1030de7bc8c40d884b3a92145 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:04:25 +0000 Subject: [PATCH 1/5] feat(schema): Add support for Liam Schema format JSON files - Add 'liam' to supported format schema - Implement liam processor that validates JSON against schemaSchema - Add comprehensive tests for liam format parser - No conversion needed - Liam format is the internal target format Fixes #5802 Co-Authored-By: hirotaka.miyagi@route06.co.jp --- .../packages/schema/src/parser/index.test.ts | 29 +++++ frontend/packages/schema/src/parser/index.ts | 4 + .../schema/src/parser/liam/index.test.ts | 113 ++++++++++++++++++ .../packages/schema/src/parser/liam/index.ts | 33 +++++ .../src/parser/supportedFormat/schema.ts | 1 + 5 files changed, 180 insertions(+) create mode 100644 frontend/packages/schema/src/parser/liam/index.test.ts create mode 100644 frontend/packages/schema/src/parser/liam/index.ts diff --git a/frontend/packages/schema/src/parser/index.test.ts b/frontend/packages/schema/src/parser/index.test.ts index 9deec0ae83..be8af48210 100644 --- a/frontend/packages/schema/src/parser/index.test.ts +++ b/frontend/packages/schema/src/parser/index.test.ts @@ -25,4 +25,33 @@ describe(parse, () => { const { value } = await parse(schemaText, 'postgres') expect(value).toMatchSnapshot() }) + + it('should parse liam schema JSON correctly', async () => { + const schemaJson = JSON.stringify({ + tables: { + users: { + name: 'users', + columns: { + id: { + name: 'id', + type: 'integer', + notNull: true, + default: null, + check: null, + comment: null, + }, + }, + indexes: {}, + constraints: {}, + comment: null, + }, + }, + enums: {}, + extensions: {}, + }) + + const { value, errors } = await parse(schemaJson, 'liam') + expect(errors).toEqual([]) + expect(value.tables['users']).toBeDefined() + }) }) diff --git a/frontend/packages/schema/src/parser/index.ts b/frontend/packages/schema/src/parser/index.ts index 7c97e111bf..bf9d4644f6 100644 --- a/frontend/packages/schema/src/parser/index.ts +++ b/frontend/packages/schema/src/parser/index.ts @@ -34,5 +34,9 @@ export const parse = async ( const { processor } = await import('./tbls/index.js') return processor(str) } + case 'liam': { + const { processor } = await import('./liam/index.js') + return processor(str) + } } } diff --git a/frontend/packages/schema/src/parser/liam/index.test.ts b/frontend/packages/schema/src/parser/liam/index.test.ts new file mode 100644 index 0000000000..a7d85118f7 --- /dev/null +++ b/frontend/packages/schema/src/parser/liam/index.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from 'vitest' +import { aColumn, anEnum, aSchema, aTable } from '../../schema/index.js' +import { processor } from './index.js' + +describe('liam processor', () => { + it('should parse valid Liam Schema JSON correctly', async () => { + const input = JSON.stringify({ + tables: { + users: { + name: 'users', + columns: { + id: { + name: 'id', + type: 'integer', + notNull: true, + default: null, + check: null, + comment: null, + }, + email: { + name: 'email', + type: 'varchar(255)', + notNull: false, + default: null, + check: null, + comment: 'User email address', + }, + }, + indexes: {}, + constraints: {}, + comment: 'Users table', + }, + }, + enums: {}, + extensions: {}, + }) + + const { value, errors } = await processor(input) + + expect(errors).toEqual([]) + expect(value).toEqual( + aSchema({ + tables: { + users: aTable({ + name: 'users', + columns: { + id: aColumn({ + name: 'id', + type: 'integer', + notNull: true, + comment: null, + }), + email: aColumn({ + name: 'email', + type: 'varchar(255)', + notNull: false, + comment: 'User email address', + }), + }, + comment: 'Users table', + }), + }, + }), + ) + }) + + it('should handle schema with enums', async () => { + const input = JSON.stringify({ + tables: {}, + enums: { + status: { + name: 'status', + values: ['active', 'inactive'], + comment: null, + }, + }, + extensions: {}, + }) + + const { value, errors } = await processor(input) + + expect(errors).toEqual([]) + expect(value.enums).toEqual({ + status: anEnum({ + name: 'status', + values: ['active', 'inactive'], + comment: null, + }), + }) + }) + + it('should return error for invalid JSON', async () => { + const input = 'invalid json{' + + const { value, errors } = await processor(input) + + expect(errors.length).toBeGreaterThan(0) + expect(value).toEqual({ tables: {}, enums: {}, extensions: {} }) + }) + + it('should return error for invalid schema structure', async () => { + const input = JSON.stringify({ + tables: 'not an object', + enums: {}, + extensions: {}, + }) + + const { value, errors } = await processor(input) + + expect(errors.length).toBeGreaterThan(0) + expect(value).toEqual({ tables: {}, enums: {}, extensions: {} }) + }) +}) diff --git a/frontend/packages/schema/src/parser/liam/index.ts b/frontend/packages/schema/src/parser/liam/index.ts new file mode 100644 index 0000000000..5ba4270584 --- /dev/null +++ b/frontend/packages/schema/src/parser/liam/index.ts @@ -0,0 +1,33 @@ +import * as v from 'valibot' +import { schemaSchema } from '../../schema/index.js' +import type { ProcessResult } from '../types.js' + +export const processor = async (str: string): Promise => { + try { + const parsed = JSON.parse(str) + const result = v.safeParse(schemaSchema, parsed) + + if (result.success) { + return { + value: result.output, + errors: [], + } + } + + return { + value: { tables: {}, enums: {}, extensions: {} }, + errors: [ + new Error( + `Invalid Liam Schema format: ${JSON.stringify(result.issues)}`, + ), + ], + } + } catch (error) { + return { + value: { tables: {}, enums: {}, extensions: {} }, + errors: [ + error instanceof Error ? error : new Error('Failed to parse JSON'), + ], + } + } +} diff --git a/frontend/packages/schema/src/parser/supportedFormat/schema.ts b/frontend/packages/schema/src/parser/supportedFormat/schema.ts index 2ec4505f26..a1c030a8e1 100644 --- a/frontend/packages/schema/src/parser/supportedFormat/schema.ts +++ b/frontend/packages/schema/src/parser/supportedFormat/schema.ts @@ -6,6 +6,7 @@ export const supportedFormatSchema = v.picklist([ 'prisma', 'drizzle', 'tbls', + 'liam', ]) export type SupportedFormat = v.InferOutput From e4aec274216b72af9eb11692c4edafa93812d56c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:16:03 +0000 Subject: [PATCH 2/5] test(cli): Update tests to include 'liam' in supported formats - Update format option description test to expect 'liam' format - Update error message test to include 'liam' in expected error text - Fixes CI failure in frontend-ci check Co-Authored-By: hirotaka.miyagi@route06.co.jp --- frontend/packages/cli/src/cli/erdCommand/runPreprocess.test.ts | 2 +- frontend/packages/cli/src/cli/index.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/packages/cli/src/cli/erdCommand/runPreprocess.test.ts b/frontend/packages/cli/src/cli/erdCommand/runPreprocess.test.ts index 3b8f69387d..a79abdcdb6 100644 --- a/frontend/packages/cli/src/cli/erdCommand/runPreprocess.test.ts +++ b/frontend/packages/cli/src/cli/erdCommand/runPreprocess.test.ts @@ -72,7 +72,7 @@ describe('runPreprocess', () => { expect(errors).toEqual([ new ArgumentError( `--format is missing, invalid, or specifies an unsupported format. Please provide a valid format. -Invalid type: Expected ("schemarb" | "postgres" | "prisma" | "drizzle" | "tbls") but received "invalid"`, +Invalid type: Expected ("schemarb" | "postgres" | "prisma" | "drizzle" | "tbls" | "liam") but received "invalid"`, ), ]) }) diff --git a/frontend/packages/cli/src/cli/index.test.ts b/frontend/packages/cli/src/cli/index.test.ts index b76ea1f8b1..e0cf96cd6e 100644 --- a/frontend/packages/cli/src/cli/index.test.ts +++ b/frontend/packages/cli/src/cli/index.test.ts @@ -51,7 +51,7 @@ describe('program', () => { expect(formatOption).toBeDefined() expect(formatOption?.flags).toBe('--format ') expect(formatOption?.description).toBe( - 'Format of the input file (schemarb|postgres|prisma|drizzle|tbls)', + 'Format of the input file (schemarb|postgres|prisma|drizzle|tbls|liam)', ) }) From 20100017945ceed736ec83916fd22e1d76e1609a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 06:26:43 +0000 Subject: [PATCH 3/5] chore: Add changeset for Liam Schema format support Co-Authored-By: hirotaka.miyagi@route06.co.jp --- .changeset/add-liam-schema-format.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/add-liam-schema-format.md diff --git a/.changeset/add-liam-schema-format.md b/.changeset/add-liam-schema-format.md new file mode 100644 index 0000000000..9dc7cee84a --- /dev/null +++ b/.changeset/add-liam-schema-format.md @@ -0,0 +1,8 @@ +--- +"@liam-hq/schema": minor +"@liam-hq/cli": patch +--- + +- Add support for Liam Schema format JSON files in ERD page parser +- Liam format requires no conversion - just JSON parsing and validation against schemaSchema +- Users can specify format using `?format=liam` query parameter From 623b79f7956a22cae37a700a7916ce6fe46140c0 Mon Sep 17 00:00:00 2001 From: MH4GF Date: Tue, 7 Oct 2025 16:33:00 +0900 Subject: [PATCH 4/5] refactor(schema): Replace try-catch with neverthrow in liam parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace try-catch error handling with neverthrow's Result pattern for cleaner functional error composition using Result.fromThrowable and explicit Result handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../packages/schema/src/parser/liam/index.ts | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/frontend/packages/schema/src/parser/liam/index.ts b/frontend/packages/schema/src/parser/liam/index.ts index 5ba4270584..e1039ad72c 100644 --- a/frontend/packages/schema/src/parser/liam/index.ts +++ b/frontend/packages/schema/src/parser/liam/index.ts @@ -1,33 +1,37 @@ +import { err, ok, Result } from 'neverthrow' import * as v from 'valibot' import { schemaSchema } from '../../schema/index.js' import type { ProcessResult } from '../types.js' -export const processor = async (str: string): Promise => { - try { - const parsed = JSON.parse(str) - const result = v.safeParse(schemaSchema, parsed) +const parseJson = Result.fromThrowable( + (s: string) => JSON.parse(s), + (error) => + error instanceof Error ? error : new Error('Failed to parse JSON'), +) - if (result.success) { - return { - value: result.output, - errors: [], - } - } +const parseSchema = ( + data: unknown, +): Result, Error> => { + const result = v.safeParse(schemaSchema, data) + if (result.success) { + return ok(result.output) + } + const errorMessage = result.issues.map((issue) => issue.message).join(', ') + return err(new Error(`Invalid Liam Schema format: ${errorMessage}`)) +} +export const processor = async (str: string): Promise => { + const result = parseJson(str).andThen(parseSchema) + + if (result.isOk()) { return { - value: { tables: {}, enums: {}, extensions: {} }, - errors: [ - new Error( - `Invalid Liam Schema format: ${JSON.stringify(result.issues)}`, - ), - ], - } - } catch (error) { - return { - value: { tables: {}, enums: {}, extensions: {} }, - errors: [ - error instanceof Error ? error : new Error('Failed to parse JSON'), - ], + value: result.value, + errors: [], } } + + return { + value: { tables: {}, enums: {}, extensions: {} }, + errors: [result.error], + } } From 3276e9bcad87560ac79731c83eafd733dac51fbc Mon Sep 17 00:00:00 2001 From: Hirotaka Miyagi <31152321+MH4GF@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:56:50 +0900 Subject: [PATCH 5/5] chore changeset --- .changeset/add-liam-schema-format.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/add-liam-schema-format.md b/.changeset/add-liam-schema-format.md index 9dc7cee84a..098a59d93a 100644 --- a/.changeset/add-liam-schema-format.md +++ b/.changeset/add-liam-schema-format.md @@ -4,5 +4,5 @@ --- - Add support for Liam Schema format JSON files in ERD page parser -- Liam format requires no conversion - just JSON parsing and validation against schemaSchema -- Users can specify format using `?format=liam` query parameter + - Liam format requires no conversion - just JSON parsing and validation against schemaSchema + - Users can specify format using `?format=liam` query parameter