Commit 833239a9 authored by William Naslund's avatar William Naslund

Setup basic column interface

parent bc690c9b
import { DBTableInformation } from "../decorators/db-table";
import { DBFieldInformation } from "../decorators";
/** Manages table columns */
export interface DBColumnAdapter {
/** Lists the columns in a table */
list(table: DBTableInformation): Promise<string[]>;
/** Adds a column to a table */
add(table: DBTableInformation, field: DBFieldInformation): Promise<void>;
/** Modifies a column in a table to match its definition */
modify(table: DBTableInformation, field: DBFieldInformation): Promise<void>;
/** Drops a column from a table */
drop(table: DBTableInformation, fieldName: string): Promise<void>;
}
......@@ -2,6 +2,7 @@ import { DBSchemaAdapter } from "./schema-adapter";
import { DBTableAdapter } from "./table-adapter";
import { DBCommandAdapter } from "./command-adapter";
import { DBModelConstructor } from "../model";
import { DBColumnAdapter } from "./column-adapter";
/** A collection of adapters to interface with a database system */
export interface DBAdapter {
......@@ -10,7 +11,7 @@ export interface DBAdapter {
command: DBCommandAdapter;
schema: DBSchemaAdapter;
table: DBTableAdapter;
column: DBColumnAdapter;
/** Closes the database connection */
close(): Promise<void>;
......
export * from './database-adapter';
export * from './command-adapter';
export * from './schema-adapter';
export * from './table-adapter';
\ No newline at end of file
export * from './table-adapter';
export * from './column-adapter';
\ No newline at end of file
/** Manges schemas in a database */
/** Manages schemas in a database */
export interface DBSchemaAdapter {
/** Creates a schema if it does not exist */
......
import { DBTableInformation } from "../decorators";
/** A table adapter manages tables in a database */
export interface DBTableAdapter {
/** List tables */
list(schema: string): Promise<string[]>;
/** Creates an empty table if it does not already exist */
verify(table: DBTableInformation): Promise<void>;
/** Drops a table */
drop(table: DBTableInformation): Promise<void>;
}
import { DBAdapter } from "./adapter/database-adapter";
import { DBModelConstructor } from "./model";
import { getTableInformation } from "./internal/meta-keys";
import { getTableInformation, getTableFields } from "./internal/meta-keys";
export class Database {
/** Tables in this database */
private readonly tables: DBModelConstructor<any>[] = [];
private readonly tables = new Set<DBModelConstructor<any>>();
constructor(
private readonly adapter: DBAdapter
......@@ -13,13 +13,47 @@ export class Database {
/** Registers and migrates a table in the database */
async register(modelType: DBModelConstructor<any>) {
const tableInfo = getTableInformation(modelType);
await this.adapter.schema.verifySchema(tableInfo.options.schema);
register(modelType: DBModelConstructor<any>) {
this.tables.add(modelType);
return this;
}
/** Migrates the database to its currently registered components */
async migrate() {
for(const modelType of this.tables) {
const tableInfo = getTableInformation(modelType);
// Schema
await this.adapter.schema.verifySchema(tableInfo.options.schema);
// Table
await this.adapter.table.verify(tableInfo);
// Columns
const existingColumns = (await this.adapter.column.list(tableInfo)).map(name => name.toUpperCase());
for(const fieldInfo of getTableFields(modelType)) {
if(existingColumns.includes(fieldInfo.name.toUpperCase())) {
// Existing Column
await this.adapter.column.modify(tableInfo, fieldInfo);
existingColumns.splice(existingColumns.indexOf(fieldInfo.name.toUpperCase()), 1);
} else {
this.tables.push(modelType);
// New Column
await this.adapter.column.add(tableInfo, fieldInfo);
}
}
for(const orphanField of existingColumns) {
await this.adapter.column.drop(tableInfo, orphanField);
}
}
}
/** Drops all registered schemas */
async delete() {
const deletedSchemas = new Set<string>();
......@@ -34,6 +68,7 @@ export class Database {
}
}
/** Closes the database connection */
async close() {
await this.adapter.close();
......
import "reflect-metadata";
import { DBModel } from "../model";
import { TABLE_FIELDS } from "../internal/meta-keys";
import { TABLE_FIELDS, FIELD_INFO } from "../internal/meta-keys";
import { DBType } from "../types";
export interface DBFieldInformation {
readonly name: string;
readonly type: DBType;
readonly propertyKey: string | symbol;
}
/** Registers a field on a model */
export function DBField(name: string) {
export function DBField(name: string, type: DBType) {
return function(proto: DBModel, propertyKey: string | symbol) {
let fieldSet: DBFieldInformation[];
let fieldSet: DBFieldInformation[] = Reflect.getMetadata(TABLE_FIELDS, proto.constructor) || [];
if(Reflect.hasMetadata(TABLE_FIELDS, proto.constructor)) {
fieldSet = Reflect.getMetadata(TABLE_FIELDS, proto.constructor);
} else {
fieldSet = [];
Reflect.defineMetadata(TABLE_FIELDS, proto.constructor, fieldSet);
}
const fieldInfo: DBFieldInformation = {
name: name,
type: type,
propertyKey: propertyKey
};
fieldSet.push(fieldInfo);
fieldSet.push({
name: name
});
Reflect.defineMetadata(TABLE_FIELDS, fieldSet, proto.constructor);
Reflect.defineMetadata(FIELD_INFO, fieldInfo, proto.constructor, propertyKey);
};
}
......@@ -11,6 +11,7 @@ export interface DBTableInformation {
/** Table options */
export interface DBTableOptions {
schema: string;
id: string | symbol;
}
......
......@@ -4,4 +4,5 @@ export * from './database';
export * from './postgres/pg-database-adapter';
export * from './decorators/index';
export * from './adapter/index';
\ No newline at end of file
export * from './adapter/index';
export * from './types/index';
import { DBModelConstructor } from "../model";
import { DBTableInformation } from "../decorators";
import { DBTableInformation, DBFieldInformation } from "../decorators";
/** Identifier for table information */
export const TABLE_INFO = Symbol();
......@@ -7,8 +7,21 @@ export const TABLE_INFO = Symbol();
/** Identifier for table field lists */
export const TABLE_FIELDS = Symbol();
/** Identifier for field information */
export const FIELD_INFO = Symbol();
/** Loads the table information for a constructor */
export function getTableInformation(modelType: DBModelConstructor<any>): DBTableInformation {
return Reflect.getMetadata(TABLE_INFO, modelType);
}
/** Loads the fields for a table */
export function getTableFields(modelType: DBModelConstructor<any>): DBFieldInformation[] {
return Reflect.getMetadata(TABLE_FIELDS, modelType) || [];
}
/** Loads the information for a field */
export function getFieldInformation(modelType: DBModelConstructor<any>, propertyKey: string | symbol): DBFieldInformation {
return Reflect.getMetadata(FIELD_INFO, modelType, propertyKey);
}
import { DBColumnAdapter } from "../adapter/column-adapter";
import { PGAdapter } from "./pg-database-adapter";
import { DBTableInformation, DBFieldInformation } from "../decorators";
import { DBIdSequence } from "../types";
export class PGColumnAdapter implements DBColumnAdapter {
constructor(
private readonly db: PGAdapter
) { }
async list(table: DBTableInformation) {
const res = await this.db.command.query(`
SELECT column_name
FROM information_schema.columns
WHERE
table_schema = $1
AND table_name = $2
`, [ table.options.schema, table.name ]);
return res.map(row => row['column_name']);
}
async add(table: DBTableInformation, field: DBFieldInformation) {
await this.db.command.run(async transaction => {
await transaction.query(`
ALTER TABLE ${table.options.schema}.${table.name}
ADD ${field.name} ${field.type.create()}
`);
});
}
async modify(table: DBTableInformation, field: DBFieldInformation) {
await this.db.command.run(async transaction => {
switch(field.type.constructor) {
case DBIdSequence:
await transaction.query(`
ALTER TABLE ${table.options.schema}.${table.name}
ALTER COLUMN ${field.name}
TYPE integer
`);
break;
default:
await transaction.query(`
ALTER TABLE ${table.options.schema}.${table.name}
ALTER COLUMN ${field.name}
TYPE ${field.type.create()}
`);
break;
}
});
}
async drop(table: DBTableInformation, fieldName: string) {
await this.db.command.run(async transaction => {
await transaction.query(`
ALTER TABLE ${table.options.schema}.${table.name}
DROP COLUMN ${fieldName} CASCADE
`);
});
}
}
......@@ -2,9 +2,9 @@ import "reflect-metadata";
import { DBAdapter } from "../adapter/database-adapter";
import { PGSchemaAdapter } from "./pg-schema-adapter";
import { PGCommandAdapter } from "./pg-command-adapter";
import { PGTableAdapter } from "./pg-table-adapter";
import { PGColumnAdapter } from "./pg-column-adapter";
import { Pool } from "pg";
import { DBModelConstructor } from "../model";
import { getTableInformation } from "../internal/meta-keys";
/** Adapts to a PostgreSQL database system */
export class PGAdapter implements DBAdapter {
......@@ -12,7 +12,8 @@ export class PGAdapter implements DBAdapter {
// Sub-Adapters
command: PGCommandAdapter;
schema: PGSchemaAdapter;
table = {};
table: PGTableAdapter;
column: PGColumnAdapter;
/** The connection pool for this adapter */
public readonly pool: Pool;
......@@ -22,6 +23,8 @@ export class PGAdapter implements DBAdapter {
this.pool = new Pool(options);
this.command = new PGCommandAdapter(this);
this.schema = new PGSchemaAdapter(this);
this.table = new PGTableAdapter(this);
this.column = new PGColumnAdapter(this);
}
......
import { DBTableAdapter } from "../adapter/table-adapter";
import { DBTableInformation } from "../decorators";
import { PGAdapter } from "./pg-database-adapter";
export class PGTableAdapter implements DBTableAdapter {
constructor(
private readonly db: PGAdapter
) { }
async list(schema: string) {
const res = await this.db.command.query(`
SELECT table_name
FROM information_schema
WHERE table_schema = $1
`);
return res.map(row => row.table_name);
}
async verify(table: DBTableInformation) {
await this.db.command.query(`
CREATE TABLE IF NOT EXISTS ${table.options.schema}.${table.name}()
`);
}
async drop(table: DBTableInformation) {
await this.db.command.query(`
DROP TABLE IF EXISTS ${table.options.schema}.${table.name} CASCADE
`);
}
}
import { DBType } from "./type";
export class DBChar implements DBType {
constructor(
private readonly length: number
) { }
create() {
return `CHARACTER(${this.length})`;
}
}
import { DBType } from "./type";
export class DBIdSequence implements DBType {
create() {
return `SERIAL`;
}
}
export * from "./type";
export * from "./varchar";
export * from "./char";
export * from "./text";
export * from "./integer";
export * from "./numeric";
export * from "./id-sequence";
\ No newline at end of file
import { DBType } from "./type";
export class DBInteger implements DBType {
create() {
return `INTEGER`;
}
}
import { DBType } from "./type";
export class DBNumeric implements DBType {
constructor(
private readonly precision?: number,
private readonly scale?: number
) { }
create() {
if(this.precision == null && this.scale == null) {
return `NUMERIC`;
}
else if(this.precision != null && this.scale == null) {
return `NUMERIC(${this.precision})`
}
else if(this.precision != null && this.scale != null) {
return `NUMERIC(${this.precision}, ${this.scale})`;
} else {
throw new Error(`A DBNumeric cannot have a non-null scale with a null precision`);
}
}
}
import { DBType } from "./type";
export class DBText implements DBType {
create() {
return `TEXT`;
}
}
/** Identifies a column data type and interface to parse/serialize it */
export interface DBType {
/** Parses a value of this type from the driver */
parse?(raw: any): any;
/** Serializes a value of this type from the driver */
serialize?(parsed: any): any;
/** Returns the SQL definition of this type for use in creating & modifying columns */
create(): string;
}
import { DBType } from "./type";
export class DBVarChar implements DBType {
constructor(
public readonly length: number
) { }
create() {
return `CHARACTER VARYING(${this.length})`;
}
}
import { DBTable, DBModel, DBField } from "@swirl/db";
import { DBTable, DBModel, DBField, DBVarChar, DBChar, DBIdSequence } from "@swirl/db";
@DBTable('account')
@DBTable('account', {
id: 'id'
})
export class Account extends DBModel {
@DBField('first_name')
firstName: string;
@DBField('id', new DBIdSequence())
id: number;
@DBField('name', new DBVarChar(80))
name: string;
}
\ No newline at end of file
......@@ -2,16 +2,16 @@ import { TEST_ADAPTER } from "./db/adapter";
import { Account } from "./db/account";
import { Database } from "@swirl/db";
describe('register()', function() {
describe('migrate()', function() {
let db = new Database(TEST_ADAPTER);
afterEach(async () => {
await db.delete();
// await db.delete();
await db.close();
});
it('Creates the schema', async () => {
await db.register(Account);
await db.register(Account).migrate();
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment