mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
ditto/db: make adapters use classes instead of static classes
This commit is contained in:
parent
398d79b45e
commit
63c0f8b032
9 changed files with 155 additions and 103 deletions
|
|
@ -6,6 +6,7 @@ export interface DittoDB extends AsyncDisposable {
|
||||||
readonly kysely: Kysely<DittoTables>;
|
readonly kysely: Kysely<DittoTables>;
|
||||||
readonly poolSize: number;
|
readonly poolSize: number;
|
||||||
readonly availableConnections: number;
|
readonly availableConnections: number;
|
||||||
|
migrate(): Promise<void>;
|
||||||
listen(channel: string, callback: (payload: string) => void): void;
|
listen(channel: string, callback: (payload: string) => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
52
packages/db/DittoPgMigrator.ts
Normal file
52
packages/db/DittoPgMigrator.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { logi } from '@soapbox/logi';
|
||||||
|
import { FileMigrationProvider, type Kysely, Migrator } from 'kysely';
|
||||||
|
|
||||||
|
import type { JsonValue } from '@std/json';
|
||||||
|
|
||||||
|
export class DittoPgMigrator {
|
||||||
|
private migrator: Migrator;
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
constructor(private kysely: Kysely<any>) {
|
||||||
|
this.migrator = new Migrator({
|
||||||
|
db: this.kysely,
|
||||||
|
provider: new FileMigrationProvider({
|
||||||
|
fs,
|
||||||
|
path,
|
||||||
|
migrationFolder: new URL(import.meta.resolve('./migrations')).pathname,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async migrate(): Promise<void> {
|
||||||
|
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Running migrations...', state: 'started' });
|
||||||
|
const { results, error } = await this.migrator.migrateToLatest();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logi({
|
||||||
|
level: 'fatal',
|
||||||
|
ns: 'ditto.db.migration',
|
||||||
|
msg: 'Migration failed.',
|
||||||
|
state: 'failed',
|
||||||
|
results: results as unknown as JsonValue,
|
||||||
|
error: error instanceof Error ? error : null,
|
||||||
|
});
|
||||||
|
throw new Error('Migration failed.');
|
||||||
|
} else {
|
||||||
|
if (!results?.length) {
|
||||||
|
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Everything up-to-date.', state: 'skipped' });
|
||||||
|
} else {
|
||||||
|
logi({
|
||||||
|
level: 'info',
|
||||||
|
ns: 'ditto.db.migration',
|
||||||
|
msg: 'Migrations finished!',
|
||||||
|
state: 'migrated',
|
||||||
|
results: results as unknown as JsonValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,9 @@ import { assertEquals } from '@std/assert';
|
||||||
|
|
||||||
import { DittoPglite } from './DittoPglite.ts';
|
import { DittoPglite } from './DittoPglite.ts';
|
||||||
|
|
||||||
Deno.test('DittoPglite.create', async () => {
|
Deno.test('DittoPglite', async () => {
|
||||||
const db = DittoPglite.create('memory://');
|
const db = new DittoPglite('memory://');
|
||||||
|
await db.migrate();
|
||||||
|
|
||||||
assertEquals(db.poolSize, 1);
|
assertEquals(db.poolSize, 1);
|
||||||
assertEquals(db.availableConnections, 1);
|
assertEquals(db.availableConnections, 1);
|
||||||
|
|
|
||||||
|
|
@ -4,42 +4,50 @@ import { PgliteDialect } from '@soapbox/kysely-pglite';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
|
|
||||||
import { KyselyLogger } from '../KyselyLogger.ts';
|
import { KyselyLogger } from '../KyselyLogger.ts';
|
||||||
|
import { DittoPgMigrator } from '../DittoPgMigrator.ts';
|
||||||
import { isWorker } from '../utils/worker.ts';
|
import { isWorker } from '../utils/worker.ts';
|
||||||
|
|
||||||
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
||||||
import type { DittoTables } from '../DittoTables.ts';
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
export class DittoPglite {
|
export class DittoPglite implements DittoDB {
|
||||||
static create(databaseUrl: string, opts?: DittoDBOpts): DittoDB {
|
readonly poolSize = 1;
|
||||||
|
readonly availableConnections = 1;
|
||||||
|
readonly kysely: Kysely<DittoTables>;
|
||||||
|
|
||||||
|
private pglite: PGlite;
|
||||||
|
private migrator: DittoPgMigrator;
|
||||||
|
|
||||||
|
constructor(databaseUrl: string, opts?: DittoDBOpts) {
|
||||||
const url = new URL(databaseUrl);
|
const url = new URL(databaseUrl);
|
||||||
|
|
||||||
if (url.protocol === 'file:' && isWorker()) {
|
if (url.protocol === 'file:' && isWorker()) {
|
||||||
throw new Error('PGlite is not supported in worker threads.');
|
throw new Error('PGlite is not supported in worker threads.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const pglite = new PGlite(databaseUrl, {
|
this.pglite = new PGlite(databaseUrl, {
|
||||||
extensions: { pg_trgm },
|
extensions: { pg_trgm },
|
||||||
debug: opts?.debug,
|
debug: opts?.debug,
|
||||||
});
|
});
|
||||||
|
|
||||||
const kysely = new Kysely<DittoTables>({
|
this.kysely = new Kysely<DittoTables>({
|
||||||
dialect: new PgliteDialect({ database: pglite }),
|
dialect: new PgliteDialect({ database: this.pglite }),
|
||||||
log: KyselyLogger,
|
log: KyselyLogger,
|
||||||
});
|
});
|
||||||
|
|
||||||
const listen = (channel: string, callback: (payload: string) => void): void => {
|
this.migrator = new DittoPgMigrator(this.kysely);
|
||||||
pglite.listen(channel, callback);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
kysely,
|
this.pglite.listen(channel, callback);
|
||||||
poolSize: 1,
|
}
|
||||||
availableConnections: 1,
|
|
||||||
listen,
|
async migrate(): Promise<void> {
|
||||||
[Symbol.asyncDispose]: async () => {
|
await this.migrator.migrate();
|
||||||
await pglite.close();
|
}
|
||||||
await kysely.destroy();
|
|
||||||
},
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
};
|
await this.pglite.close();
|
||||||
|
await this.kysely.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { DittoPolyPg } from './DittoPolyPg.ts';
|
import { DittoPolyPg } from './DittoPolyPg.ts';
|
||||||
|
|
||||||
Deno.test('DittoPolyPg', async () => {
|
Deno.test('DittoPolyPg', async () => {
|
||||||
const db = DittoPolyPg.create('memory://');
|
const db = new DittoPolyPg('memory://');
|
||||||
await DittoPolyPg.migrate(db.kysely);
|
await db.migrate();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,53 @@
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
import path from 'node:path';
|
|
||||||
|
|
||||||
import { logi } from '@soapbox/logi';
|
|
||||||
import { FileMigrationProvider, type Kysely, Migrator } from 'kysely';
|
|
||||||
|
|
||||||
import { DittoPglite } from './DittoPglite.ts';
|
import { DittoPglite } from './DittoPglite.ts';
|
||||||
import { DittoPostgres } from './DittoPostgres.ts';
|
import { DittoPostgres } from './DittoPostgres.ts';
|
||||||
|
|
||||||
import type { JsonValue } from '@std/json';
|
import type { Kysely } from 'kysely';
|
||||||
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
||||||
import type { DittoTables } from '../DittoTables.ts';
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
/** Creates either a PGlite or Postgres connection depending on the databaseUrl. */
|
/** Creates either a PGlite or Postgres connection depending on the databaseUrl. */
|
||||||
export class DittoPolyPg {
|
export class DittoPolyPg implements DittoDB {
|
||||||
|
private adapter: DittoDB;
|
||||||
|
|
||||||
/** Open a new database connection. */
|
/** Open a new database connection. */
|
||||||
static create(databaseUrl: string, opts?: DittoDBOpts): DittoDB {
|
constructor(databaseUrl: string, opts?: DittoDBOpts) {
|
||||||
const { protocol } = new URL(databaseUrl);
|
const { protocol } = new URL(databaseUrl);
|
||||||
|
|
||||||
switch (protocol) {
|
switch (protocol) {
|
||||||
case 'file:':
|
case 'file:':
|
||||||
case 'memory:':
|
case 'memory:':
|
||||||
return DittoPglite.create(databaseUrl, opts);
|
this.adapter = new DittoPglite(databaseUrl, opts);
|
||||||
|
break;
|
||||||
case 'postgres:':
|
case 'postgres:':
|
||||||
case 'postgresql:':
|
case 'postgresql:':
|
||||||
return DittoPostgres.create(databaseUrl, opts);
|
this.adapter = new DittoPostgres(databaseUrl, opts);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('Unsupported database URL.');
|
throw new Error('Unsupported database URL.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Migrate the database to the latest version. */
|
get kysely(): Kysely<DittoTables> {
|
||||||
static async migrate(kysely: Kysely<DittoTables>) {
|
return this.adapter.kysely;
|
||||||
const migrator = new Migrator({
|
}
|
||||||
db: kysely,
|
|
||||||
provider: new FileMigrationProvider({
|
|
||||||
fs,
|
|
||||||
path,
|
|
||||||
migrationFolder: new URL(import.meta.resolve('../migrations')).pathname,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Running migrations...', state: 'started' });
|
async migrate(): Promise<void> {
|
||||||
const { results, error } = await migrator.migrateToLatest();
|
await this.adapter.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
if (error) {
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
logi({
|
this.adapter.listen(channel, callback);
|
||||||
level: 'fatal',
|
}
|
||||||
ns: 'ditto.db.migration',
|
|
||||||
msg: 'Migration failed.',
|
get poolSize(): number {
|
||||||
state: 'failed',
|
return this.adapter.poolSize;
|
||||||
results: results as unknown as JsonValue,
|
}
|
||||||
error: error instanceof Error ? error : null,
|
|
||||||
});
|
get availableConnections(): number {
|
||||||
throw new Error('Migration failed.');
|
return this.adapter.availableConnections;
|
||||||
} else {
|
}
|
||||||
if (!results?.length) {
|
|
||||||
logi({ level: 'info', ns: 'ditto.db.migration', msg: 'Everything up-to-date.', state: 'skipped' });
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
} else {
|
await this.adapter[Symbol.asyncDispose]();
|
||||||
logi({
|
|
||||||
level: 'info',
|
|
||||||
ns: 'ditto.db.migration',
|
|
||||||
msg: 'Migrations finished!',
|
|
||||||
state: 'migrated',
|
|
||||||
results: results as unknown as JsonValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,53 +12,54 @@ import {
|
||||||
import { type PostgresJSDialectConfig, PostgresJSDriver } from 'kysely-postgres-js';
|
import { type PostgresJSDialectConfig, PostgresJSDriver } from 'kysely-postgres-js';
|
||||||
import postgres from 'postgres';
|
import postgres from 'postgres';
|
||||||
|
|
||||||
|
import { DittoPgMigrator } from '../DittoPgMigrator.ts';
|
||||||
import { KyselyLogger } from '../KyselyLogger.ts';
|
import { KyselyLogger } from '../KyselyLogger.ts';
|
||||||
|
|
||||||
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
import type { DittoDB, DittoDBOpts } from '../DittoDB.ts';
|
||||||
import type { DittoTables } from '../DittoTables.ts';
|
import type { DittoTables } from '../DittoTables.ts';
|
||||||
|
|
||||||
export class DittoPostgres {
|
export class DittoPostgres implements DittoDB {
|
||||||
static create(databaseUrl: string, opts?: DittoDBOpts): DittoDB {
|
private pg: ReturnType<typeof postgres>;
|
||||||
const pg = postgres(databaseUrl, { max: opts?.poolSize });
|
private migrator: DittoPgMigrator;
|
||||||
|
|
||||||
const kysely = new Kysely<DittoTables>({
|
readonly kysely: Kysely<DittoTables>;
|
||||||
|
|
||||||
|
constructor(databaseUrl: string, opts?: DittoDBOpts) {
|
||||||
|
this.pg = postgres(databaseUrl, { max: opts?.poolSize });
|
||||||
|
|
||||||
|
this.kysely = new Kysely<DittoTables>({
|
||||||
dialect: {
|
dialect: {
|
||||||
createAdapter() {
|
createAdapter: () => new PostgresAdapter(),
|
||||||
return new PostgresAdapter();
|
createDriver: () =>
|
||||||
},
|
new PostgresJSDriver({ postgres: this.pg as unknown as PostgresJSDialectConfig['postgres'] }),
|
||||||
createDriver() {
|
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||||
return new PostgresJSDriver({
|
createQueryCompiler: () => new DittoPostgresQueryCompiler(),
|
||||||
postgres: pg as unknown as PostgresJSDialectConfig['postgres'],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createIntrospector(db) {
|
|
||||||
return new PostgresIntrospector(db);
|
|
||||||
},
|
|
||||||
createQueryCompiler() {
|
|
||||||
return new DittoPostgresQueryCompiler();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
log: KyselyLogger,
|
log: KyselyLogger,
|
||||||
});
|
});
|
||||||
|
|
||||||
const listen = (channel: string, callback: (payload: string) => void): void => {
|
this.migrator = new DittoPgMigrator(this.kysely);
|
||||||
pg.listen(channel, callback);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
listen(channel: string, callback: (payload: string) => void): void {
|
||||||
kysely,
|
this.pg.listen(channel, callback);
|
||||||
get poolSize() {
|
}
|
||||||
return pg.connections.open;
|
|
||||||
},
|
async migrate(): Promise<void> {
|
||||||
get availableConnections() {
|
await this.migrator.migrate();
|
||||||
return pg.connections.idle;
|
}
|
||||||
},
|
|
||||||
listen,
|
get poolSize(): number {
|
||||||
[Symbol.asyncDispose]: async () => {
|
return this.pg.connections.open;
|
||||||
await pg.end();
|
}
|
||||||
await kysely.destroy();
|
|
||||||
},
|
get availableConnections(): number {
|
||||||
};
|
return this.pg.connections.idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
async [Symbol.asyncDispose](): Promise<void> {
|
||||||
|
await this.pg.end();
|
||||||
|
await this.kysely.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { DummyDB } from './DummyDB.ts';
|
||||||
|
|
||||||
Deno.test('DummyDB', async () => {
|
Deno.test('DummyDB', async () => {
|
||||||
const db = new DummyDB();
|
const db = new DummyDB();
|
||||||
|
await db.migrate();
|
||||||
|
|
||||||
const rows = await db.kysely.selectFrom('nostr_events').selectAll().execute();
|
const rows = await db.kysely.selectFrom('nostr_events').selectAll().execute();
|
||||||
|
|
||||||
assertEquals(rows, []);
|
assertEquals(rows, []);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ export class DummyDB implements DittoDB {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migrate(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
[Symbol.asyncDispose](): Promise<void> {
|
[Symbol.asyncDispose](): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue