-
-
Notifications
You must be signed in to change notification settings - Fork 108
feat(schema): adds table schema & query copy #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 45 commits
f90324c
e9e365e
877bc49
ffd302b
8f57b15
f56bb17
2423a7d
4e9eb62
09dd7ef
4fed82d
255c557
1863f6c
207542f
fa1aebf
75b365b
3803285
2583117
1ee614e
06c2a9c
8944ab6
4dc0687
7b144d7
ff850e9
39e0041
74194d4
3882b9c
82e1097
7834dbe
1760716
c6cf086
9d60380
ba825d2
bbfc467
be6af9b
a218d0e
a72f401
6f192b2
c6925d2
c1392c3
2c2a556
b0ea8a6
a85ff49
e09e94f
1c3026f
fb2d7bd
9ffaa1a
b21db98
ff35936
cb5d26d
88688d7
f84a58e
8918c39
8be8930
033f1f6
3619ce6
ada1a45
70f29aa
de11555
3fbaa9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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], | ||
| } |
| 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` | ||||||||||||||||||||
| const valuesList = match.values.map(v => `'${v}'`).join(', ') | ||||||||||||||||||||
|
|
||||||||||||||||||||
| imports.add(enumFunc) | ||||||||||||||||||||
| extras.push(`export const ${enumVarName} = ${enumFunc}('${eName}', [${valuesList}]);`) | ||||||||||||||||||||
|
||||||||||||||||||||
| 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
AI
Jan 28, 2026
There was a problem hiding this comment.
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.
| 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])}` | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| 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
AI
Jan 28, 2026
There was a problem hiding this comment.
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.
| tsType = match.values.map(v => `'${v}'`).join(' | ') | |
| tsType = match.values.map(v => `'${v.replace(/'/g, "\\'")}'`).join(' | ') |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
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.
| 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, "\\'")}'` |
There was a problem hiding this comment.
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 likeenumNameEnumEnum. Consider using a more robust naming strategy or checking if the name already ends with 'Enum' before appending it.