@kysera/dialects 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * PostgreSQL Dialect Adapter
3
+ */
4
+
5
+ import type { Kysely } from 'kysely';
6
+ import { sql } from 'kysely';
7
+ import type { DialectAdapter, DatabaseErrorLike } from '../types.js';
8
+
9
+ export class PostgresAdapter implements DialectAdapter {
10
+ readonly dialect = 'postgres' as const;
11
+
12
+ getDefaultPort(): number {
13
+ return 5432;
14
+ }
15
+
16
+ getCurrentTimestamp(): string {
17
+ return 'CURRENT_TIMESTAMP';
18
+ }
19
+
20
+ escapeIdentifier(identifier: string): string {
21
+ return `"${identifier.replace(/"/g, '""')}"`;
22
+ }
23
+
24
+ formatDate(date: Date): string {
25
+ return date.toISOString();
26
+ }
27
+
28
+ isUniqueConstraintError(error: unknown): boolean {
29
+ const e = error as DatabaseErrorLike;
30
+ const message = e.message?.toLowerCase() || '';
31
+ const code = e.code || '';
32
+ return code === '23505' || message.includes('unique constraint');
33
+ }
34
+
35
+ isForeignKeyError(error: unknown): boolean {
36
+ const e = error as DatabaseErrorLike;
37
+ const message = e.message?.toLowerCase() || '';
38
+ const code = e.code || '';
39
+ return code === '23503' || message.includes('foreign key constraint');
40
+ }
41
+
42
+ isNotNullError(error: unknown): boolean {
43
+ const e = error as DatabaseErrorLike;
44
+ const message = e.message?.toLowerCase() || '';
45
+ const code = e.code || '';
46
+ return code === '23502' || message.includes('not-null constraint');
47
+ }
48
+
49
+ async tableExists(db: Kysely<any>, tableName: string): Promise<boolean> {
50
+ try {
51
+ const result = await db
52
+ .selectFrom('information_schema.tables')
53
+ .select('table_name')
54
+ .where('table_name', '=', tableName)
55
+ .where('table_schema', '=', 'public')
56
+ .executeTakeFirst();
57
+ return !!result;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ async getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]> {
64
+ try {
65
+ const results = await db
66
+ .selectFrom('information_schema.columns')
67
+ .select('column_name')
68
+ .where('table_name', '=', tableName)
69
+ .where('table_schema', '=', 'public')
70
+ .execute();
71
+ return results.map((r) => r.column_name as string);
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ async getTables(db: Kysely<any>): Promise<string[]> {
78
+ try {
79
+ const results = await db
80
+ .selectFrom('information_schema.tables')
81
+ .select('table_name')
82
+ .where('table_schema', '=', 'public')
83
+ .where('table_type', '=', 'BASE TABLE')
84
+ .execute();
85
+ return results.map((r) => r.table_name as string);
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ async getDatabaseSize(db: Kysely<any>, databaseName?: string): Promise<number> {
92
+ try {
93
+ const result = await sql
94
+ .raw(`SELECT pg_database_size(${databaseName ? `'${databaseName}'` : 'current_database()'}) as size`)
95
+ .execute(db)
96
+ .then((r) => r.rows?.[0]);
97
+ return (result as { size?: number })?.size || 0;
98
+ } catch {
99
+ return 0;
100
+ }
101
+ }
102
+
103
+ async truncateTable(db: Kysely<any>, tableName: string): Promise<void> {
104
+ try {
105
+ await sql.raw(`TRUNCATE TABLE ${this.escapeIdentifier(tableName)} RESTART IDENTITY CASCADE`).execute(db);
106
+ } catch {
107
+ // Ignore errors for tables that might not exist or have constraints
108
+ }
109
+ }
110
+
111
+ async truncateAllTables(db: Kysely<any>, exclude: string[] = []): Promise<void> {
112
+ const tables = await this.getTables(db);
113
+ for (const table of tables) {
114
+ if (!exclude.includes(table)) {
115
+ await this.truncateTable(db, table);
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ export const postgresAdapter = new PostgresAdapter();
@@ -0,0 +1,109 @@
1
+ /**
2
+ * SQLite Dialect Adapter
3
+ */
4
+
5
+ import type { Kysely } from 'kysely';
6
+ import { sql } from 'kysely';
7
+ import type { DialectAdapter, DatabaseErrorLike } from '../types.js';
8
+
9
+ export class SQLiteAdapter implements DialectAdapter {
10
+ readonly dialect = 'sqlite' as const;
11
+
12
+ getDefaultPort(): null {
13
+ // SQLite is file-based, no port
14
+ return null;
15
+ }
16
+
17
+ getCurrentTimestamp(): string {
18
+ return "datetime('now')";
19
+ }
20
+
21
+ escapeIdentifier(identifier: string): string {
22
+ return `"${identifier.replace(/"/g, '""')}"`;
23
+ }
24
+
25
+ formatDate(date: Date): string {
26
+ return date.toISOString();
27
+ }
28
+
29
+ isUniqueConstraintError(error: unknown): boolean {
30
+ const e = error as DatabaseErrorLike;
31
+ const message = e.message?.toLowerCase() || '';
32
+ return message.includes('unique constraint failed');
33
+ }
34
+
35
+ isForeignKeyError(error: unknown): boolean {
36
+ const e = error as DatabaseErrorLike;
37
+ const message = e.message?.toLowerCase() || '';
38
+ return message.includes('foreign key constraint failed');
39
+ }
40
+
41
+ isNotNullError(error: unknown): boolean {
42
+ const e = error as DatabaseErrorLike;
43
+ const message = e.message?.toLowerCase() || '';
44
+ return message.includes('not null constraint failed');
45
+ }
46
+
47
+ async tableExists(db: Kysely<any>, tableName: string): Promise<boolean> {
48
+ try {
49
+ const result = await db
50
+ .selectFrom('sqlite_master')
51
+ .select('name')
52
+ .where('type', '=', 'table')
53
+ .where('name', '=', tableName)
54
+ .executeTakeFirst();
55
+ return !!result;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]> {
62
+ try {
63
+ const results = await sql.raw(`PRAGMA table_info(${tableName})`).execute(db);
64
+ return (results.rows as Array<{ name: string }>).map((r) => r.name);
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ async getTables(db: Kysely<any>): Promise<string[]> {
71
+ try {
72
+ const results = await db
73
+ .selectFrom('sqlite_master')
74
+ .select('name')
75
+ .where('type', '=', 'table')
76
+ .where('name', 'not like', 'sqlite_%')
77
+ .execute();
78
+ return results.map((r) => r.name as string);
79
+ } catch {
80
+ return [];
81
+ }
82
+ }
83
+
84
+ async getDatabaseSize(_db: Kysely<any>, _databaseName?: string): Promise<number> {
85
+ // SQLite database size requires file system access
86
+ // which is not available in a cross-runtime way
87
+ return 0;
88
+ }
89
+
90
+ async truncateTable(db: Kysely<any>, tableName: string): Promise<void> {
91
+ try {
92
+ // SQLite doesn't support TRUNCATE, use DELETE instead
93
+ await db.deleteFrom(tableName as any).execute();
94
+ } catch {
95
+ // Ignore errors for tables that might not exist
96
+ }
97
+ }
98
+
99
+ async truncateAllTables(db: Kysely<any>, exclude: string[] = []): Promise<void> {
100
+ const tables = await this.getTables(db);
101
+ for (const table of tables) {
102
+ if (!exclude.includes(table)) {
103
+ await this.truncateTable(db, table);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ export const sqliteAdapter = new SQLiteAdapter();
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Connection URL utilities
3
+ */
4
+
5
+ import type { DatabaseDialect, ConnectionConfig } from './types.js';
6
+ import { getAdapter } from './factory.js';
7
+
8
+ /**
9
+ * Parse database connection URL into ConnectionConfig
10
+ *
11
+ * @example
12
+ * const config = parseConnectionUrl('postgresql://user:pass@localhost:5432/mydb?ssl=true');
13
+ * // { host: 'localhost', port: 5432, database: 'mydb', user: 'user', password: 'pass', ssl: true }
14
+ */
15
+ export function parseConnectionUrl(url: string): ConnectionConfig {
16
+ const parsed = new URL(url);
17
+
18
+ return {
19
+ host: parsed.hostname,
20
+ port: parsed.port ? parseInt(parsed.port) : undefined,
21
+ database: parsed.pathname.slice(1),
22
+ user: parsed.username || undefined,
23
+ password: parsed.password || undefined,
24
+ ssl: parsed.searchParams.get('ssl') === 'true' || parsed.searchParams.get('sslmode') === 'require',
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Build connection URL from config
30
+ *
31
+ * @example
32
+ * const url = buildConnectionUrl('postgres', { host: 'localhost', database: 'mydb' });
33
+ * // 'postgresql://localhost:5432/mydb'
34
+ */
35
+ export function buildConnectionUrl(dialect: DatabaseDialect, config: ConnectionConfig): string {
36
+ const protocol = dialect === 'postgres' ? 'postgresql' : dialect;
37
+ const auth = config.user
38
+ ? config.password
39
+ ? `${config.user}:${config.password}@`
40
+ : `${config.user}@`
41
+ : '';
42
+
43
+ const host = config.host || 'localhost';
44
+ const port = config.port || getAdapter(dialect).getDefaultPort();
45
+ const database = config.database;
46
+
47
+ let url = port ? `${protocol}://${auth}${host}:${port}/${database}` : `${protocol}://${auth}${host}/${database}`;
48
+
49
+ if (config.ssl) {
50
+ url += '?ssl=true';
51
+ }
52
+
53
+ return url;
54
+ }
55
+
56
+ /**
57
+ * Get default port for a dialect
58
+ *
59
+ * @example
60
+ * getDefaultPort('postgres') // 5432
61
+ * getDefaultPort('mysql') // 3306
62
+ * getDefaultPort('sqlite') // null
63
+ */
64
+ export function getDefaultPort(dialect: DatabaseDialect): number | null {
65
+ return getAdapter(dialect).getDefaultPort();
66
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Dialect Adapter Factory
3
+ */
4
+
5
+ import type { DatabaseDialect, DialectAdapter } from './types.js';
6
+ import { PostgresAdapter, postgresAdapter } from './adapters/postgres.js';
7
+ import { MySQLAdapter, mysqlAdapter } from './adapters/mysql.js';
8
+ import { SQLiteAdapter, sqliteAdapter } from './adapters/sqlite.js';
9
+
10
+ const adapters: Record<DatabaseDialect, DialectAdapter> = {
11
+ postgres: postgresAdapter,
12
+ mysql: mysqlAdapter,
13
+ sqlite: sqliteAdapter,
14
+ };
15
+
16
+ /**
17
+ * Get a dialect adapter for the specified dialect
18
+ *
19
+ * @example
20
+ * const adapter = getAdapter('postgres');
21
+ * console.log(adapter.getDefaultPort()); // 5432
22
+ */
23
+ export function getAdapter(dialect: DatabaseDialect): DialectAdapter {
24
+ const adapter = adapters[dialect];
25
+ if (!adapter) {
26
+ throw new Error(`Unknown dialect: ${dialect}. Supported: postgres, mysql, sqlite`);
27
+ }
28
+ return adapter;
29
+ }
30
+
31
+ /**
32
+ * Create a new dialect adapter instance
33
+ *
34
+ * @example
35
+ * const adapter = createDialectAdapter('mysql');
36
+ */
37
+ export function createDialectAdapter(dialect: DatabaseDialect): DialectAdapter {
38
+ switch (dialect) {
39
+ case 'postgres':
40
+ return new PostgresAdapter();
41
+ case 'mysql':
42
+ return new MySQLAdapter();
43
+ case 'sqlite':
44
+ return new SQLiteAdapter();
45
+ default:
46
+ throw new Error(`Unknown dialect: ${dialect}. Supported: postgres, mysql, sqlite`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Register a custom dialect adapter
52
+ *
53
+ * @example
54
+ * registerAdapter(customAdapter);
55
+ */
56
+ export function registerAdapter(adapter: DialectAdapter): void {
57
+ adapters[adapter.dialect] = adapter;
58
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Dialect Helper Functions
3
+ *
4
+ * Standalone helper functions that accept dialect as parameter
5
+ * for backward compatibility with existing code.
6
+ */
7
+
8
+ import type { Kysely } from 'kysely';
9
+ import type { DatabaseDialect } from './types.js';
10
+ import { getAdapter } from './factory.js';
11
+
12
+ /**
13
+ * Check if table exists in the database
14
+ *
15
+ * @example
16
+ * const exists = await tableExists(db, 'users', 'postgres');
17
+ */
18
+ export async function tableExists(db: Kysely<any>, tableName: string, dialect: DatabaseDialect): Promise<boolean> {
19
+ return getAdapter(dialect).tableExists(db, tableName);
20
+ }
21
+
22
+ /**
23
+ * Get column names for a table
24
+ *
25
+ * @example
26
+ * const columns = await getTableColumns(db, 'users', 'postgres');
27
+ * // ['id', 'name', 'email', 'created_at']
28
+ */
29
+ export async function getTableColumns(
30
+ db: Kysely<any>,
31
+ tableName: string,
32
+ dialect: DatabaseDialect
33
+ ): Promise<string[]> {
34
+ return getAdapter(dialect).getTableColumns(db, tableName);
35
+ }
36
+
37
+ /**
38
+ * Get all tables in the database
39
+ *
40
+ * @example
41
+ * const tables = await getTables(db, 'postgres');
42
+ * // ['users', 'posts', 'comments']
43
+ */
44
+ export async function getTables(db: Kysely<any>, dialect: DatabaseDialect): Promise<string[]> {
45
+ return getAdapter(dialect).getTables(db);
46
+ }
47
+
48
+ /**
49
+ * Escape identifier for SQL (table names, column names, etc.)
50
+ *
51
+ * @example
52
+ * escapeIdentifier('my-table', 'postgres') // '"my-table"'
53
+ * escapeIdentifier('my-table', 'mysql') // '`my-table`'
54
+ */
55
+ export function escapeIdentifier(identifier: string, dialect: DatabaseDialect): string {
56
+ return getAdapter(dialect).escapeIdentifier(identifier);
57
+ }
58
+
59
+ /**
60
+ * Get SQL expression for current timestamp
61
+ *
62
+ * @example
63
+ * getCurrentTimestamp('postgres') // 'CURRENT_TIMESTAMP'
64
+ * getCurrentTimestamp('sqlite') // "datetime('now')"
65
+ */
66
+ export function getCurrentTimestamp(dialect: DatabaseDialect): string {
67
+ return getAdapter(dialect).getCurrentTimestamp();
68
+ }
69
+
70
+ /**
71
+ * Format date for database insertion
72
+ *
73
+ * @example
74
+ * formatDate(new Date(), 'postgres') // '2024-01-15T10:30:00.000Z'
75
+ * formatDate(new Date(), 'mysql') // '2024-01-15 10:30:00'
76
+ */
77
+ export function formatDate(date: Date, dialect: DatabaseDialect): string {
78
+ return getAdapter(dialect).formatDate(date);
79
+ }
80
+
81
+ /**
82
+ * Check if error is a unique constraint violation
83
+ *
84
+ * @example
85
+ * try {
86
+ * await db.insertInto('users').values({ email: 'duplicate@example.com' }).execute();
87
+ * } catch (error) {
88
+ * if (isUniqueConstraintError(error, 'postgres')) {
89
+ * console.log('Email already exists');
90
+ * }
91
+ * }
92
+ */
93
+ export function isUniqueConstraintError(error: unknown, dialect: DatabaseDialect): boolean {
94
+ return getAdapter(dialect).isUniqueConstraintError(error);
95
+ }
96
+
97
+ /**
98
+ * Check if error is a foreign key constraint violation
99
+ *
100
+ * @example
101
+ * if (isForeignKeyError(error, 'mysql')) {
102
+ * console.log('Referenced row does not exist');
103
+ * }
104
+ */
105
+ export function isForeignKeyError(error: unknown, dialect: DatabaseDialect): boolean {
106
+ return getAdapter(dialect).isForeignKeyError(error);
107
+ }
108
+
109
+ /**
110
+ * Check if error is a not-null constraint violation
111
+ *
112
+ * @example
113
+ * if (isNotNullError(error, 'sqlite')) {
114
+ * console.log('Required field is missing');
115
+ * }
116
+ */
117
+ export function isNotNullError(error: unknown, dialect: DatabaseDialect): boolean {
118
+ return getAdapter(dialect).isNotNullError(error);
119
+ }
120
+
121
+ /**
122
+ * Get database size in bytes
123
+ *
124
+ * @example
125
+ * const size = await getDatabaseSize(db, 'postgres');
126
+ * console.log(`Database size: ${size} bytes`);
127
+ */
128
+ export async function getDatabaseSize(
129
+ db: Kysely<any>,
130
+ dialect: DatabaseDialect,
131
+ databaseName?: string
132
+ ): Promise<number> {
133
+ return getAdapter(dialect).getDatabaseSize(db, databaseName);
134
+ }
135
+
136
+ /**
137
+ * Truncate all tables in the database (useful for testing)
138
+ *
139
+ * @example
140
+ * // Truncate all tables except migrations
141
+ * await truncateAllTables(db, 'postgres', ['kysely_migrations']);
142
+ */
143
+ export async function truncateAllTables(
144
+ db: Kysely<any>,
145
+ dialect: DatabaseDialect,
146
+ exclude: string[] = []
147
+ ): Promise<void> {
148
+ return getAdapter(dialect).truncateAllTables(db, exclude);
149
+ }
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @kysera/dialects
3
+ *
4
+ * Dialect-specific utilities for Kysely database operations.
5
+ * Supports PostgreSQL, MySQL, and SQLite with a unified adapter interface.
6
+ *
7
+ * @example
8
+ * // Using the adapter interface
9
+ * import { getAdapter } from '@kysera/dialects';
10
+ *
11
+ * const adapter = getAdapter('postgres');
12
+ * const exists = await adapter.tableExists(db, 'users');
13
+ * const columns = await adapter.getTableColumns(db, 'users');
14
+ *
15
+ * @example
16
+ * // Using helper functions (backward compatible)
17
+ * import { tableExists, escapeIdentifier, isUniqueConstraintError } from '@kysera/dialects';
18
+ *
19
+ * const exists = await tableExists(db, 'users', 'postgres');
20
+ * const escaped = escapeIdentifier('user-data', 'mysql');
21
+ *
22
+ * @example
23
+ * // Connection URL utilities
24
+ * import { parseConnectionUrl, buildConnectionUrl } from '@kysera/dialects';
25
+ *
26
+ * const config = parseConnectionUrl('postgresql://user:pass@localhost:5432/mydb');
27
+ * const url = buildConnectionUrl('postgres', { host: 'localhost', database: 'mydb' });
28
+ */
29
+
30
+ // Types
31
+ export type { DatabaseDialect, ConnectionConfig, DialectAdapter, DatabaseErrorLike } from './types.js';
32
+
33
+ // Factory and adapters
34
+ export { getAdapter, createDialectAdapter, registerAdapter } from './factory.js';
35
+ export { PostgresAdapter, postgresAdapter } from './adapters/postgres.js';
36
+ export { MySQLAdapter, mysqlAdapter } from './adapters/mysql.js';
37
+ export { SQLiteAdapter, sqliteAdapter } from './adapters/sqlite.js';
38
+
39
+ // Connection utilities
40
+ export { parseConnectionUrl, buildConnectionUrl, getDefaultPort } from './connection.js';
41
+
42
+ // Helper functions (standalone, backward compatible)
43
+ export {
44
+ tableExists,
45
+ getTableColumns,
46
+ getTables,
47
+ escapeIdentifier,
48
+ getCurrentTimestamp,
49
+ formatDate,
50
+ isUniqueConstraintError,
51
+ isForeignKeyError,
52
+ isNotNullError,
53
+ getDatabaseSize,
54
+ truncateAllTables,
55
+ } from './helpers.js';
package/src/types.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @kysera/dialects - Type Definitions
3
+ *
4
+ * Dialect-specific types and interfaces for database operations
5
+ */
6
+
7
+ import type { Kysely } from 'kysely';
8
+
9
+ /**
10
+ * Supported database dialects
11
+ */
12
+ export type DatabaseDialect = 'postgres' | 'mysql' | 'sqlite';
13
+
14
+ /**
15
+ * Database connection configuration
16
+ */
17
+ export interface ConnectionConfig {
18
+ host?: string | undefined;
19
+ port?: number | undefined;
20
+ database: string;
21
+ user?: string | undefined;
22
+ password?: string | undefined;
23
+ ssl?: boolean | undefined;
24
+ }
25
+
26
+ /**
27
+ * Interface for dialect-specific operations
28
+ */
29
+ export interface DialectAdapter {
30
+ /** The dialect this adapter handles */
31
+ readonly dialect: DatabaseDialect;
32
+
33
+ /** Get default port for this dialect */
34
+ getDefaultPort(): number | null;
35
+
36
+ /** Get SQL expression for current timestamp */
37
+ getCurrentTimestamp(): string;
38
+
39
+ /** Escape identifier for this dialect */
40
+ escapeIdentifier(identifier: string): string;
41
+
42
+ /** Format date for this dialect */
43
+ formatDate(date: Date): string;
44
+
45
+ /** Check if error is a unique constraint violation */
46
+ isUniqueConstraintError(error: unknown): boolean;
47
+
48
+ /** Check if error is a foreign key constraint violation */
49
+ isForeignKeyError(error: unknown): boolean;
50
+
51
+ /** Check if error is a not-null constraint violation */
52
+ isNotNullError(error: unknown): boolean;
53
+
54
+ /** Check if a table exists in the database */
55
+ tableExists(db: Kysely<any>, tableName: string): Promise<boolean>;
56
+
57
+ /** Get column names for a table */
58
+ getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]>;
59
+
60
+ /** Get all tables in the database */
61
+ getTables(db: Kysely<any>): Promise<string[]>;
62
+
63
+ /** Get database size in bytes */
64
+ getDatabaseSize(db: Kysely<any>, databaseName?: string): Promise<number>;
65
+
66
+ /** Truncate a single table */
67
+ truncateTable(db: Kysely<any>, tableName: string): Promise<void>;
68
+
69
+ /** Truncate all tables (for testing) */
70
+ truncateAllTables(db: Kysely<any>, exclude?: string[]): Promise<void>;
71
+ }
72
+
73
+ /**
74
+ * Error object shape for database error detection
75
+ */
76
+ export interface DatabaseErrorLike {
77
+ message?: string;
78
+ code?: string;
79
+ }