feat: add multi-dialect schema initialization
This commit is contained in:
163
src/adapters/dialect.ts
Normal file
163
src/adapters/dialect.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { Kysely, MysqlDialect, PostgresDialect, SqliteDialect } from 'kysely';
|
||||
import { createPool as createMysqlPool } from 'mysql2';
|
||||
import { Pool as PostgresPool } from 'pg';
|
||||
|
||||
import type { IdentityDatabaseSchema } from '../types/database';
|
||||
import { IdentityDBConfigurationError } from '../core/errors';
|
||||
|
||||
export interface SqliteConnectionConfig {
|
||||
client: 'sqlite';
|
||||
filename: string;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export interface PostgresConnectionConfig {
|
||||
client: 'postgres';
|
||||
connectionString?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
database?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
ssl?: boolean;
|
||||
}
|
||||
|
||||
export interface MysqlConnectionConfig {
|
||||
client: 'mysql' | 'mariadb';
|
||||
uri?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
database?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export type IdentityDBConnectionConfig =
|
||||
| SqliteConnectionConfig
|
||||
| PostgresConnectionConfig
|
||||
| MysqlConnectionConfig;
|
||||
|
||||
export interface DatabaseConnection {
|
||||
client: IdentityDBConnectionConfig['client'];
|
||||
db: Kysely<IdentityDatabaseSchema>;
|
||||
destroy: () => Promise<void>;
|
||||
}
|
||||
|
||||
export async function createDatabase(
|
||||
config: IdentityDBConnectionConfig,
|
||||
): Promise<DatabaseConnection> {
|
||||
switch (config.client) {
|
||||
case 'sqlite': {
|
||||
const sqlite = new Database(config.filename, {
|
||||
readonly: config.readonly ?? false,
|
||||
});
|
||||
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
const db = new Kysely<IdentityDatabaseSchema>({
|
||||
dialect: new SqliteDialect({
|
||||
database: sqlite,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
client: config.client,
|
||||
db,
|
||||
destroy: async () => {
|
||||
await db.destroy();
|
||||
sqlite.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'postgres': {
|
||||
const pool = new PostgresPool({
|
||||
connectionString: config.connectionString,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
database: config.database,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
ssl: config.ssl ? { rejectUnauthorized: false } : undefined,
|
||||
});
|
||||
|
||||
const db = new Kysely<IdentityDatabaseSchema>({
|
||||
dialect: new PostgresDialect({ pool }),
|
||||
});
|
||||
|
||||
return {
|
||||
client: config.client,
|
||||
db,
|
||||
destroy: async () => {
|
||||
await db.destroy();
|
||||
await pool.end();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'mysql':
|
||||
case 'mariadb': {
|
||||
const mysqlOptions: {
|
||||
host?: string;
|
||||
port?: number;
|
||||
database?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
} = {};
|
||||
|
||||
if (config.host !== undefined) {
|
||||
mysqlOptions.host = config.host;
|
||||
}
|
||||
|
||||
if (config.port !== undefined) {
|
||||
mysqlOptions.port = config.port;
|
||||
}
|
||||
|
||||
if (config.database !== undefined) {
|
||||
mysqlOptions.database = config.database;
|
||||
}
|
||||
|
||||
if (config.user !== undefined) {
|
||||
mysqlOptions.user = config.user;
|
||||
}
|
||||
|
||||
if (config.password !== undefined) {
|
||||
mysqlOptions.password = config.password;
|
||||
}
|
||||
|
||||
const pool = config.uri
|
||||
? createMysqlPool(config.uri)
|
||||
: createMysqlPool(mysqlOptions);
|
||||
|
||||
const db = new Kysely<IdentityDatabaseSchema>({
|
||||
dialect: new MysqlDialect({ pool }),
|
||||
});
|
||||
|
||||
return {
|
||||
client: config.client,
|
||||
db,
|
||||
destroy: async () => {
|
||||
await db.destroy();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
pool.end((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default: {
|
||||
const neverClient: never = config;
|
||||
throw new IdentityDBConfigurationError(
|
||||
`Unsupported database client: ${JSON.stringify(neverClient)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/adapters/index.ts
Normal file
1
src/adapters/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dialect';
|
||||
13
src/core/errors.ts
Normal file
13
src/core/errors.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export class IdentityDBError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'IdentityDBError';
|
||||
}
|
||||
}
|
||||
|
||||
export class IdentityDBConfigurationError extends IdentityDBError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'IdentityDBConfigurationError';
|
||||
}
|
||||
}
|
||||
68
src/core/migrations.ts
Normal file
68
src/core/migrations.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { Kysely } from 'kysely';
|
||||
|
||||
import {
|
||||
FACTS_TABLE,
|
||||
FACT_TOPICS_TABLE,
|
||||
TOPICS_TABLE,
|
||||
} from './schema';
|
||||
import type { IdentityDatabaseSchema } from '../types/database';
|
||||
|
||||
export async function initializeSchema(
|
||||
db: Kysely<IdentityDatabaseSchema>,
|
||||
): Promise<void> {
|
||||
await db.schema
|
||||
.createTable(TOPICS_TABLE)
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (column) => column.primaryKey())
|
||||
.addColumn('name', 'text', (column) => column.notNull())
|
||||
.addColumn('normalized_name', 'text', (column) => column.notNull().unique())
|
||||
.addColumn('category', 'text', (column) => column.notNull())
|
||||
.addColumn('granularity', 'text', (column) => column.notNull())
|
||||
.addColumn('description', 'text')
|
||||
.addColumn('metadata', 'text')
|
||||
.addColumn('created_at', 'text', (column) => column.notNull())
|
||||
.addColumn('updated_at', 'text', (column) => column.notNull())
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable(FACTS_TABLE)
|
||||
.ifNotExists()
|
||||
.addColumn('id', 'text', (column) => column.primaryKey())
|
||||
.addColumn('statement', 'text', (column) => column.notNull())
|
||||
.addColumn('summary', 'text')
|
||||
.addColumn('source', 'text')
|
||||
.addColumn('confidence', 'real')
|
||||
.addColumn('metadata', 'text')
|
||||
.addColumn('created_at', 'text', (column) => column.notNull())
|
||||
.addColumn('updated_at', 'text', (column) => column.notNull())
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable(FACT_TOPICS_TABLE)
|
||||
.ifNotExists()
|
||||
.addColumn('fact_id', 'text', (column) =>
|
||||
column.notNull().references(`${FACTS_TABLE}.id`).onDelete('cascade'),
|
||||
)
|
||||
.addColumn('topic_id', 'text', (column) =>
|
||||
column.notNull().references(`${TOPICS_TABLE}.id`).onDelete('cascade'),
|
||||
)
|
||||
.addColumn('role', 'text')
|
||||
.addColumn('position', 'integer', (column) => column.notNull())
|
||||
.addColumn('created_at', 'text', (column) => column.notNull())
|
||||
.addPrimaryKeyConstraint('fact_topics_pk', ['fact_id', 'topic_id', 'position'])
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('fact_topics_topic_id_idx')
|
||||
.ifNotExists()
|
||||
.on(FACT_TOPICS_TABLE)
|
||||
.column('topic_id')
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex('fact_topics_fact_id_idx')
|
||||
.ifNotExists()
|
||||
.on(FACT_TOPICS_TABLE)
|
||||
.column('fact_id')
|
||||
.execute();
|
||||
}
|
||||
Reference in New Issue
Block a user