Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/add-liam-schema-format.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"`,
),
])
})
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/cli/src/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('program', () => {
expect(formatOption).toBeDefined()
expect(formatOption?.flags).toBe('--format <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)',
)
})

Expand Down
29 changes: 29 additions & 0 deletions frontend/packages/schema/src/parser/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
4 changes: 4 additions & 0 deletions frontend/packages/schema/src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
113 changes: 113 additions & 0 deletions frontend/packages/schema/src/parser/liam/index.test.ts
Original file line number Diff line number Diff line change
@@ -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: {} })
})
})
37 changes: 37 additions & 0 deletions frontend/packages/schema/src/parser/liam/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +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'

const parseJson = Result.fromThrowable(
(s: string) => JSON.parse(s),
(error) =>
error instanceof Error ? error : new Error('Failed to parse JSON'),
)

const parseSchema = (
data: unknown,
): Result<v.InferOutput<typeof schemaSchema>, 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<ProcessResult> => {
const result = parseJson(str).andThen(parseSchema)

if (result.isOk()) {
return {
value: result.value,
errors: [],
}
}

return {
value: { tables: {}, enums: {}, extensions: {} },
errors: [result.error],
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const supportedFormatSchema = v.picklist([
'prisma',
'drizzle',
'tbls',
'liam',
])

export type SupportedFormat = v.InferOutput<typeof supportedFormatSchema>