Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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>