Skip to content
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
f90324c
feat: added ability to copy table schema & query (filters or without …
Rudra-Sankha-Sinhamahapatra Dec 31, 2025
e9e365e
fix: using cn
Rudra-Sankha-Sinhamahapatra Dec 31, 2025
877bc49
feat: adds enum support & added parsing for special char or hypens ta…
Rudra-Sankha-Sinhamahapatra Jan 2, 2026
ffd302b
feat: add label property to columns and update related components for…
letstri Jan 2, 2026
8f57b15
feat: refactor copy dialog to use tabs for format selection and updat…
letstri Jan 2, 2026
f56bb17
feat: adds data types support and added size calculation on columns
Rudra-Sankha-Sinhamahapatra Jan 2, 2026
2423a7d
feat: adds foreign key support
Rudra-Sankha-Sinhamahapatra Jan 2, 2026
4e9eb62
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Jan 6, 2026
09dd7ef
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 6, 2026
4fed82d
fix: query formatting, prisma where, syntaxes, added mappings
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
255c557
refactor: removed eslint expect error
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
1863f6c
chore: merge with main
Rudra-Sankha-Sinhamahapatra Jan 7, 2026
207542f
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 8, 2026
fa1aebf
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 8, 2026
75b365b
refactor: merge with main
Rudra-Sankha-Sinhamahapatra Jan 10, 2026
3803285
fix: merge conflicts
Rudra-Sankha-Sinhamahapatra Jan 23, 2026
2583117
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 23, 2026
1ee614e
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 26, 2026
06c2a9c
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 26, 2026
8944ab6
fix: prisma 1 to many relationship
Rudra-Sankha-Sinhamahapatra Jan 26, 2026
4dc0687
feat: adds foreign key relationship on drizzle
Rudra-Sankha-Sinhamahapatra Jan 26, 2026
7b144d7
feat: added lengths for columns in zod and prisma
Rudra-Sankha-Sinhamahapatra Jan 26, 2026
ff850e9
feat: adds indexes support
Rudra-Sankha-Sinhamahapatra Jan 26, 2026
39e0041
refactor: moved generators to separate folders
Rudra-Sankha-Sinhamahapatra Jan 26, 2026
74194d4
feat: add change-case package and update sidebar link component
letstri Jan 26, 2026
3882b9c
refactor: streamline schema generation functions
letstri Jan 26, 2026
82e1097
fix: alignments of table structure and added null for ts
Rudra-Sankha-Sinhamahapatra Jan 27, 2026
7834dbe
fix: changing drizzle case to camelCase
Rudra-Sankha-Sinhamahapatra Jan 27, 2026
1760716
feat: adds camelCase to zod & ts schema
Rudra-Sankha-Sinhamahapatra Jan 27, 2026
c6cf086
refactor: replaced toPascalCase with pascalCase
Rudra-Sankha-Sinhamahapatra Jan 27, 2026
9d60380
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
ba825d2
fix: prisma model name & key conventions
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
bbfc467
fix: handles drizzle for special cases
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
be6af9b
fix: handles special case for ts,zod,kysely
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
a218d0e
fix: using unknown for json type on fields of zod & ts
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
a72f401
fix: moving definitions to bottom
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
6f192b2
fix: zod import
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
c6925d2
fix: prisma file structure spacing
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
c1392c3
fix: revert back to correct sql enums position
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
2c2a556
fix: prisma schema structue formattin
Rudra-Sankha-Sinhamahapatra Jan 28, 2026
b0ea8a6
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 28, 2026
a85ff49
refactor: improved copy functionality and refactor format handling
letstri Jan 28, 2026
e09e94f
refactor: enhance dialog structure and copy button integration
letstri Jan 28, 2026
1c3026f
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 29, 2026
fb2d7bd
refactor: reorganize Prisma filter value type and update max length n…
letstri Jan 29, 2026
9ffaa1a
refactor: update query generation functions to use destructured param…
letstri Jan 30, 2026
b21db98
Merge branch 'main' of https://github.com/wannabespace/conar into fea…
letstri Jan 30, 2026
ff35936
refactor(dialects): introduce cold dialects
letstri Jan 30, 2026
cb5d26d
refactor(generators): update query and schema functions
letstri Jan 30, 2026
88688d7
feat: adds db onUpdate CasCade,onDelete CasCade & 1 to 1 relationship
Rudra-Sankha-Sinhamahapatra Jan 30, 2026
f84a58e
fix: merge conflict
Rudra-Sankha-Sinhamahapatra Jan 30, 2026
8918c39
feat(schema): adds default and SERIAL for postgres
Rudra-Sankha-Sinhamahapatra Jan 30, 2026
8be8930
refactor(dialects): rename getColdKysely to getColdDialect for clarity
letstri Jan 31, 2026
033f1f6
Merge branch 'feat/copy-table-queryAndSchema' of https://github.com/w…
letstri Jan 31, 2026
3619ce6
refactor(generators): streamline enum handling in schema generation
letstri Jan 31, 2026
ada1a45
refactor(generators): update getColumnType to return 'any' as default…
letstri Jan 31, 2026
70f29aa
refactor(generators): enhance schema generation and type handling acr…
letstri Jan 31, 2026
de11555
refactor(generators): simplify type handling in schema generation by …
letstri Jan 31, 2026
3fbaa9d
refactor(generators): optimize schema generation by consolidating col…
letstri Jan 31, 2026
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
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"arktype": "catalog:",
"babel-plugin-react-compiler": "catalog:",
"better-auth": "catalog:",
"change-case": "catalog:",
"date-fns": "catalog:",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/components/sidebar-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,29 @@ import type { ComponentProps } from 'react'
import { cn } from '@conar/ui/lib/utils'
import { Link } from '@tanstack/react-router'

const baseClasses = 'flex w-full items-center gap-2 rounded-md border px-2 py-1 text-sm'
const activeClasses = 'border-primary/20 bg-primary/10 text-primary hover:bg-primary/20'
const inactiveClasses = 'border-transparent text-foreground hover:bg-accent/30'

export function SidebarButton({ className, active, ...props }: ComponentProps<'button'> & { active?: boolean }) {
return (
<button
type="button"
className={cn(baseClasses, className, active ? activeClasses : inactiveClasses)}
{...props}
/>
)
}

export function SidebarLink({
className,
...props
}: LinkProps & Omit<ComponentProps<'a'>, 'children'>) {
return (
<Link
activeProps={{ className: 'border-primary/20 bg-primary/10 text-primary hover:bg-primary/20' }}
inactiveProps={{ className: 'border-transparent text-foreground hover:bg-accent/30' }}
className={cn(`
flex w-full items-center gap-2 rounded-md border px-2 py-1 text-sm
`, className)}
activeProps={{ className: activeClasses }}
inactiveProps={{ className: inactiveClasses }}
className={cn(baseClasses, className)}
{...props}
/>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
export interface Column {
id: string
type?: string
label?: string
enum?: string
isArray?: boolean
isEditable?: boolean
isNullable?: boolean
maxLength?: number | null
precision?: number | null
scale?: number | null
unique?: string
primaryKey?: string
foreign?: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { GeneratorFormat } from './utils'
import { ConnectionType } from '@conar/shared/enums/connection-type'

export const GENERATOR_COMPATIBILITY: Partial<Record<GeneratorFormat, ConnectionType[]>> = {
prisma: [ConnectionType.Postgres, ConnectionType.MySQL, ConnectionType.MSSQL],
drizzle: [ConnectionType.Postgres, ConnectionType.MySQL, ConnectionType.MSSQL, ConnectionType.ClickHouse],
kysely: [ConnectionType.Postgres, ConnectionType.MySQL, ConnectionType.MSSQL, ConnectionType.ClickHouse],
}
188 changes: 188 additions & 0 deletions apps/desktop/src/entities/connection/generators/formats/drizzle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { ActiveFilter } from '@conar/shared/filters'
import type { Column } from '../../components/table/utils'
import type { enumType } from '../../sql/enums'
import type { Index } from '../utils'
import { ConnectionType } from '@conar/shared/enums/connection-type'
import { camelCase, pascalCase } from 'change-case'
import * as templates from '../templates'
import { findEnum, getColumnType, groupIndexes, safePascalCase } from '../utils'

export function generateQueryDrizzle(table: string, filters: ActiveFilter[]) {
const varName = camelCase(safePascalCase(table))

const conditions = filters.map((f) => {
const op = f.ref.operator.toUpperCase()
const col = `${varName}.${camelCase(f.column)}`
const val = JSON.stringify(f.values[0])
const arrVal = JSON.stringify(f.values)

switch (op) {
case '=': return `eq(${col}, ${val})`
case '!=': return `ne(${col}, ${val})`
case '>': return `gt(${col}, ${val})`
case '>=': return `gte(${col}, ${val})`
case '<': return `lt(${col}, ${val})`
case '<=': return `lte(${col}, ${val})`
case 'IS NULL': return `isNull(${col})`
case 'IS NOT NULL': return `isNotNull(${col})`
case 'IN': return `inArray(${col}, ${arrVal})`
case 'NOT IN': return `notInArray(${col}, ${arrVal})`
case 'LIKE': return `like(${col}, ${val})`
case 'ILIKE': return `ilike(${col}, ${val})`
default: return undefined
}
}).filter(Boolean).join(',\n ')

return templates.drizzleQueryTemplate(varName, conditions)
}

const dialectConfig: Record<ConnectionType, { tableFunc: string, importPath: string, enumFunc: string }> = {
postgres: { tableFunc: 'pgTable', importPath: 'drizzle-orm/pg-core', enumFunc: 'pgEnum' },
mysql: { tableFunc: 'mysqlTable', importPath: 'drizzle-orm/mysql-core', enumFunc: 'mysqlEnum' },
mssql: { tableFunc: 'mssqlTable', importPath: 'drizzle-orm/mssql-core', enumFunc: '' },
clickhouse: { tableFunc: 'clickhouseTable', importPath: 'drizzle-orm/clickhouse-core', enumFunc: 'enum' },
}

export function generateSchemaDrizzle({ table, columns, enums = [], dialect = ConnectionType.Postgres, indexes = [] }: { table: string, columns: Column[], enums?: typeof enumType.infer[], dialect?: ConnectionType, indexes?: Index[] }) {
const config = dialectConfig[dialect]
const { tableFunc, importPath, enumFunc } = config

const imports = new Set<string>()
const foreignKeyImports = new Set<string>()
const extras: string[] = []

const varName = camelCase(safePascalCase(table))

const cols = columns.map((c) => {
let typeFunc = getColumnType(c.type, 'drizzle', dialect)
imports.add(typeFunc)

let enumVarName = ''

const match = findEnum(c, table, enums)
if (enumFunc && match?.values.length) {
const eName = match.name || `${table}_${c.id}`
enumVarName = `${eName}Enum`

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enum variable name is created by simply appending 'Enum' to the enum name (${eName}Enum). If the enum name already ends with 'Enum', this would create a variable like enumNameEnumEnum. Consider using a more robust naming strategy or checking if the name already ends with 'Enum' before appending it.

Suggested change
enumVarName = `${eName}Enum`
enumVarName = eName.endsWith('Enum') ? eName : `${eName}Enum`

Copilot uses AI. Check for mistakes.
const valuesList = match.values.map(v => `'${v}'`).join(', ')

imports.add(enumFunc)
extras.push(`export const ${enumVarName} = ${enumFunc}('${eName}', [${valuesList}]);`)

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enum values and enum names are not properly escaped when generating the Drizzle schema. If an enum value or name contains a single quote, it will break the generated code. The values and names should be escaped using .replace(/'/g, "\\'") or similar before being wrapped in single quotes.

Suggested change
const valuesList = match.values.map(v => `'${v}'`).join(', ')
imports.add(enumFunc)
extras.push(`export const ${enumVarName} = ${enumFunc}('${eName}', [${valuesList}]);`)
const escapedEName = eName.replace(/'/g, "\\'")
const valuesList = match.values.map(v => `'${v.replace(/'/g, "\\'")}'`).join(', ')
imports.add(enumFunc)
extras.push(`export const ${enumVarName} = ${enumFunc}('${escapedEName}', [${valuesList}]);`)

Copilot uses AI. Check for mistakes.

typeFunc = enumVarName
}

const key = camelCase(c.id)
const safeKey = /^[a-z_$][\w$]*$/i.test(key) ? key : `'${key}'`

let chain = ''
if (enumVarName) {
chain = `${enumVarName}('${c.id}')`
}
else {
let options = ''
if (c.maxLength && c.maxLength !== -1 && ['varchar', 'char', 'nvarchar'].includes(typeFunc)) {
options = `, { length: ${c.maxLength} }`
}
else if (typeFunc === 'decimal' && c.precision) {
options = `, { precision: ${c.precision}${c.scale ? `, scale: ${c.scale}` : ''} }`
}
chain = `${typeFunc}('${c.id}'${options})`
}

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column IDs are not properly escaped when generating the Drizzle schema. If a column ID contains a single quote, it will break the generated code. The column ID should be escaped using .replace(/'/g, "\\'") before being wrapped in single quotes in the chain construction.

Copilot uses AI. Check for mistakes.

if (!c.isNullable)
chain += '.notNull()'
if (c.primaryKey)
chain += '.primaryKey()'
if (c.unique && !c.primaryKey)
chain += '.unique()'

if (c.foreign && dialect !== 'clickhouse') {
const isValidRef = /^[a-z_$][\w$]*$/i.test(c.foreign.table)
const refTable = isValidRef ? c.foreign.table : pascalCase(c.foreign.table)
chain += `.references(() => ${refTable}.${camelCase(c.foreign.column)})`

foreignKeyImports.add(`import { ${refTable} } from './${c.foreign.table}';`)
}

return ` ${safeKey}: ${chain},`
}).join('\n')

const relationships: string[] = []

columns.forEach((c) => {
if (c.foreign && dialect !== 'clickhouse') {
const isValidRef = /^[a-z_$][\w$]*$/i.test(c.foreign.table)
const refTable = isValidRef ? c.foreign.table : pascalCase(c.foreign.table)
const fieldName = camelCase(c.id.replace(/(_id|Id)$/, ''))
relationships.push(` ${fieldName}: one(${refTable}, {\n fields: [${varName}.${camelCase(c.id)}],\n references: [${refTable}.${camelCase(c.foreign.column)}],\n }),`)
}

if (c.references?.length) {
c.references.forEach((ref) => {
const isValidRef = /^[a-z_$][\w$]*$/i.test(ref.table)
const refTable = isValidRef ? ref.table : pascalCase(ref.table)
const fieldName = camelCase(ref.table)

relationships.push(` ${fieldName}: many(${refTable}),`)

foreignKeyImports.add(`import { ${refTable} } from './${ref.table}';`)
})
}
})

const explicitIndexes = groupIndexes(indexes, table).filter(idx => !idx.isPrimary && dialect !== 'clickhouse' && !(idx.isUnique && idx.columns.length === 1 && columns.find(c => c.id === idx.columns[0] && c.unique)))

let extraConfig = ''
if (explicitIndexes.length > 0) {
const idxDecls: string[] = []
let usedIndex = false
let usedUnique = false

explicitIndexes.forEach((idx) => {
const func = idx.isUnique ? 'uniqueIndex' : 'index'
if (idx.isUnique)
usedUnique = true
else
usedIndex = true

const onCols = idx.columns.map((col) => {
const key = camelCase(col)
const isSafe = /^[a-z_$][\w$]*$/i.test(key)
return isSafe ? `t.${key}` : `t['${key}']`
}).join(', ')

idxDecls.push(` ${func}('${idx.name}').on(${onCols}),`)
})

if (usedIndex)
imports.add('index')
if (usedUnique)
imports.add('uniqueIndex')

extraConfig = idxDecls.join('\n')
}

const base = templates.drizzleSchemaTemplate(table, Array.from(imports), cols, tableFunc, importPath, extraConfig)

const splitIdx = base.indexOf('\n\nexport')
const baseImports = splitIdx !== -1 ? base.slice(0, splitIdx) : ''
const baseBody = splitIdx !== -1 ? base.slice(splitIdx).trim() : base

const allImports = [
relationships.length > 0 ? 'import { relations } from \'drizzle-orm\';' : null,
...foreignKeyImports,
baseImports,
].filter(Boolean).join('\n')

const definitions = [
extras.join('\n\n'),
baseBody,
]

if (relationships.length > 0) {
const relName = `${varName}Relations`
definitions.push(`export const ${relName} = relations(${varName}, ({ one, many }) => ({\n${relationships.join('\n')}\n}));`)
}

return `${allImports}\n\n${definitions.filter(Boolean).join('\n\n')}`
}
47 changes: 47 additions & 0 deletions apps/desktop/src/entities/connection/generators/formats/kysely.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ActiveFilter } from '@conar/shared/filters'
import type { Column } from '../../components/table/utils'
import type { enumType } from '../../sql/enums'
import { ConnectionType } from '@conar/shared/enums/connection-type'
import * as templates from '../templates'
import { findEnum, formatValue, getColumnType } from '../utils'

export function generateQueryKysely(table: string, filters: ActiveFilter[]) {
const conditions = filters.map((f) => {
const op = f.ref.operator.toUpperCase()
const col = f.column
if (f.ref.hasValue === false) {
return `'${col}', '${f.ref.operator.toLowerCase()}'`
}
else if (f.ref.isArray) {
const method = op === 'IN' ? 'in' : 'not in'
return `'${col}', '${method}', ${JSON.stringify(f.values)}`
}
else {
return `'${col}', '${f.ref.operator}', ${formatValue(f.values[0])}`

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column names are not properly escaped when generating the Kysely query. If a column name contains a single quote, it will break the generated code. The column names should be escaped using .replace(/'/g, "\\'") before being wrapped in single quotes.

Suggested change
if (f.ref.hasValue === false) {
return `'${col}', '${f.ref.operator.toLowerCase()}'`
}
else if (f.ref.isArray) {
const method = op === 'IN' ? 'in' : 'not in'
return `'${col}', '${method}', ${JSON.stringify(f.values)}`
}
else {
return `'${col}', '${f.ref.operator}', ${formatValue(f.values[0])}`
const safeCol = col.replace(/'/g, "\\'")
if (f.ref.hasValue === false) {
return `'${safeCol}', '${f.ref.operator.toLowerCase()}'`
}
else if (f.ref.isArray) {
const method = op === 'IN' ? 'in' : 'not in'
return `'${safeCol}', '${method}', ${JSON.stringify(f.values)}`
}
else {
return `'${safeCol}', '${f.ref.operator}', ${formatValue(f.values[0])}`

Copilot uses AI. Check for mistakes.
}
}).filter(Boolean).join(')\n .where(')

return templates.kyselyQueryTemplate(table, conditions)
}

export function generateSchemaKysely({ table, columns, enums = [], dialect = ConnectionType.Postgres }: { table: string, columns: Column[], enums?: typeof enumType.infer[], dialect?: ConnectionType }) {
const body = columns.map((c) => {
let tsType = getColumnType(c.type, 'ts', dialect)

const match = findEnum(c, table, enums)
if (match?.values.length) {
tsType = match.values.map(v => `'${v}'`).join(' | ')

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enum values are not properly escaped when generating the Kysely type. If an enum value contains a single quote, it will break the generated code. The values should be escaped using .replace(/'/g, "\\'") or similar before being wrapped in single quotes.

Suggested change
tsType = match.values.map(v => `'${v}'`).join(' | ')
tsType = match.values.map(v => `'${v.replace(/'/g, "\\'")}'`).join(' | ')

Copilot uses AI. Check for mistakes.
if (c.type === 'set')
tsType = `(${tsType})[]`
}

const isGenerated = c.primaryKey
let typeDef = isGenerated ? `Generated<${tsType}>` : tsType
if (c.isNullable)
typeDef += ' | null'
const safeKey = /^[a-z_$][\w$]*$/i.test(c.id) ? c.id : `'${c.id}'`

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column IDs are not properly escaped when generating the Kysely schema. If a column ID contains a single quote, it will break the generated code. The safeKey generation already handles escaping for non-valid JavaScript identifiers by wrapping in quotes, but the actual value inside those quotes needs to be escaped using .replace(/'/g, "\\'") to handle quotes within the ID itself.

Suggested change
const safeKey = /^[a-z_$][\w$]*$/i.test(c.id) ? c.id : `'${c.id}'`
const safeKey = /^[a-z_$][\w$]*$/i.test(c.id) ? c.id : `'${c.id.replace(/'/g, "\\'")}'`

Copilot uses AI. Check for mistakes.
return ` ${safeKey}: ${typeDef};`
}).join('\n')

return templates.kyselySchemaTemplate(table, body)
}
Loading