feat: add multi-dialect schema initialization
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -13,6 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4",
|
||||
@@ -140,6 +141,8 @@
|
||||
|
||||
"@types/node": ["@types/node@24.12.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
|
||||
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