@travetto/model-sql
Version:
SQL backing for the travetto model module, with real-time modeling support for SQL schemas.
130 lines (117 loc) • 3.7 kB
text/typescript
import { AsyncContext, WithAsyncContext } from '@travetto/context';
import { ModelRegistry } from '@travetto/model';
import { Class } from '@travetto/runtime';
import { ChangeEvent } from '@travetto/registry';
import { SchemaChange } from '@travetto/schema';
import { Connected, Transactional } from './connection/decorator.ts';
import { SQLDialect } from './dialect/base.ts';
import { SQLModelUtil, VisitStack } from './util.ts';
import { Connection } from './connection/base.ts';
/**
* Manage creation/updating of all tables
*/
export class TableManager {
#dialect: SQLDialect;
context: AsyncContext;
constructor(context: AsyncContext, dialect: SQLDialect) {
this.#dialect = dialect;
this.context = context;
}
#exec<T = unknown>(sql: string): Promise<{ records: T[], count: number }> {
return this.#dialect.executeSQL<T>(sql);
}
/**
* Create all needed tables for a given class
*/
async exportTables(cls: Class): Promise<string[]> {
const out: string[] = [];
for (const op of this.#dialect.getCreateAllTablesSQL(cls)) {
out.push(op);
}
const indices = ModelRegistry.get(cls).indices;
if (indices) {
for (const op of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
out.push(op);
}
}
return out;
}
/**
* Create all needed tables for a given class
*/
async createTables(cls: Class): Promise<void> {
for (const op of this.#dialect.getCreateAllTablesSQL(cls)) {
await this.#exec(op);
}
const indices = ModelRegistry.get(cls).indices;
if (indices) {
for (const op of this.#dialect.getCreateAllIndicesSQL(cls, indices)) {
try {
await this.#exec(op);
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
if (!/\bexists|duplicate\b/i.test(err.message)) {
throw err;
}
}
}
}
}
/**
* Drop all tables for a given class
*/
async dropTables(cls: Class): Promise<void> {
for (const op of this.#dialect.getDropAllTablesSQL(cls)) {
await this.#exec(op);
}
}
/**
* Drop all tables for a given class
*/
async truncateTables(cls: Class): Promise<void> {
for (const op of this.#dialect.getTruncateAllTablesSQL(cls)) {
await this.#exec(op);
}
}
/**
* Get a valid connection
*/
get conn(): Connection {
return this.#dialect.conn;
}
/**
* When the schema changes, update SQL
*/
async changeSchema(cls: Class, change: SchemaChange): Promise<void> {
try {
const rootStack = SQLModelUtil.classToStack(cls);
const changes = change.subs.reduce<Record<ChangeEvent<unknown>['type'], VisitStack[][]>>((acc, v) => {
const path = v.path.map(f => ({ ...f }));
for (const ev of v.fields) {
acc[ev.type].push([...rootStack, ...path, { ...(ev.type === 'removing' ? ev.prev : ev.curr)! }]);
}
return acc;
}, { added: [], changed: [], removing: [] });
await Promise.all(changes.added.map(v => this.#dialect.executeSQL(this.#dialect.getAddColumnSQL(v))));
await Promise.all(changes.changed.map(v => this.#dialect.executeSQL(this.#dialect.getModifyColumnSQL(v))));
await Promise.all(changes.removing.map(v => this.#dialect.executeSQL(this.#dialect.getDropColumnSQL(v))));
} catch (err) {
// Failed to change
console.error('Unable to change field', { error: err });
}
}
}