Commit bc690c9b authored by William Naslund's avatar William Naslund

Began refactor of the adapter

parent fdd6d2ed
{
"name": "@swirl/db",
"version": "0.1.0",
"main": "dist/module/main.js",
"main": "dist/module/index.js",
"scripts": {
"start": "concurrently npm:watch:module npm:watch:tests",
"test": "mocha --require module-alias/register --recursive dist/tests",
......@@ -13,7 +13,8 @@
},
"dependencies": {
"pg": "^7.8.0",
"pg-cursor": "^2.0.0"
"pg-cursor": "^2.0.0",
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
......
import { TableInfo, ColumnInfo } from "./decorators";
import { ConstraintInfo } from "./decorators/constraint";
import { Model } from "./model";
/** The interface that databases use to access the underlying database system */
export interface DatabaseAdapter {
/** Called to setup the database connection and other setup tasks */
setup(): Promise<void>;
/** Called to close the the database connection */
shutdown?(): Promise<void>;
/** Deletes all the resources from the database permanently */
delete?(): Promise<void>;
/** Runs a SQL query */
runQuery<T = any>(sql: string, args?: any[]): Promise<T[]>;
/** Returns true if a table exists */
tableExists(tableInfo: TableInfo): Promise<boolean>;
/** Returns the SQL to create a table */
createTable(tableInfo: TableInfo): Promise<string>;
/** Checks if a table column exists */
columnExists(table: TableInfo, column: ColumnInfo): Promise<boolean>;
/** Return the SQL to create a column */
createColumn(table: TableInfo, column: ColumnInfo): Promise<string>;
/** Updates a column */
updateColumn(table: TableInfo, column: ColumnInfo): Promise<void>;
/** Returns true if a constraint exists on the given table */
constraintExists(table: TableInfo, column: ColumnInfo, constraint: ConstraintInfo): Promise<boolean>;
/** Returns the SQL to create a constraint */
createConstraint(table: TableInfo, column: ColumnInfo, constraint: ConstraintInfo): Promise<string>;
/** Updates a constraint */
updateConstraint(table: TableInfo, column: ColumnInfo, constraint: ConstraintInfo): Promise<void>;
/** Sets up the checksum storage */
setupChecksumStorage(): Promise<void>;
/** Returns the checksum of a table */
getTableChecksum(table: TableInfo): Promise<Buffer>;
/** Returns the checksum of a column */
getColumnChecksum(table: TableInfo, column: ColumnInfo): Promise<Buffer>;
/** Returns the stored checksum of a constraint */
getConstraintChecksum(table: TableInfo, column: ColumnInfo, constraint: ConstraintInfo): Promise<Buffer>;
/** Sets the checksum of a table */
setTableChecksum(table: TableInfo, checksum: Buffer): Promise<void>;
/** Sets the checksum of a column */
setColumnChecksum(table: TableInfo, column: ColumnInfo, checksum: Buffer): Promise<void>;
/** Sets the checksum of a constraint */
setConstraintChecksum(table: TableInfo, column: ColumnInfo, constraint: ConstraintInfo, checksum: Buffer): Promise<void>;
/** Inserts a record into a table */
insert<T extends Model>(record: T): Promise<string | number>;
/** Gets a cursor for a query */
getCursor(sql: string, args: any[]): Promise<QueryCursor>;
}
/** A cursor for rows of data */
export interface QueryCursor {
available(): Promise<boolean>;
next(): Promise<any>;
close(): Promise<void>;
}
/** Provides a means to run database commands */
export interface DBCommandAdapter {
/** Runs a single query */
query<T = any>(sql: string, args?: any[]): Promise<T[]>;
/** Starts a transaction */
run(handler: (transaction: DBTransaction) => Promise<void>): Promise<void>;
}
/** An interface to run a series of commands in one transaction */
export interface DBTransaction {
/** Runs a query in the transaction */
query<T = any>(sql: string, args?: any[]): Promise<T[]>;
}
import { DBSchemaAdapter } from "./schema-adapter";
import { DBTableAdapter } from "./table-adapter";
import { DBCommandAdapter } from "./command-adapter";
import { DBModelConstructor } from "../model";
/** A collection of adapters to interface with a database system */
export interface DBAdapter {
// Sub-Adapters
command: DBCommandAdapter;
schema: DBSchemaAdapter;
table: DBTableAdapter;
/** 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
/** Manges schemas in a database */
export interface DBSchemaAdapter {
/** Creates a schema if it does not exist */
verifySchema(name: string): Promise<void>;
/** Drops a schema recursively */
dropSchema(name: string): Promise<void>;
}
/** A table adapter manages tables in a database */
export interface DBTableAdapter {
}
export * from "./postgres";
\ No newline at end of file
This diff is collapsed.
import * as crypo from "crypto";
import { DatabaseAdapter } from "./adapter";
import { ModelConstructor } from "./model";
import { getTableInformation, getColumnInformation, getConstraintInfo, getColumnConstraintInfo } from "./decorators";
import { QueryResult } from "./result";
import { DBAdapter } from "./adapter/database-adapter";
import { DBModelConstructor } from "./model";
import { getTableInformation } from "./internal/meta-keys";
/** Maps models to the database they belong to */
const MODEL_DATABASES = new Map<Function, Database>();
/** The main entrypoint to setup a database and its structure */
export class Database {
/** Tables in this database */
private readonly tables: DBModelConstructor<any>[] = [];
constructor(
public readonly config: DatabaseConfiguration
private readonly adapter: DBAdapter
) { }
/** Sets up the database adapter */
public async setup(): Promise<void> {
await this.config.adapter.setup();
await this.config.adapter.setupChecksumStorage();
// Create Tables
for(const constructor of this.config.tables || []) {
const tableInfo = getTableInformation(constructor);
const tableSQL = await this.config.adapter.createTable(tableInfo);
const tableChecksum = await generateChecksum(tableSQL);
if(await this.config.adapter.tableExists(tableInfo) != true) {
await this.config.adapter.runQuery(tableSQL);
await this.config.adapter.setTableChecksum(tableInfo, tableChecksum);
} else {
const storedHash = await this.config.adapter.getTableChecksum(tableInfo);
if(!storedHash.equals(tableChecksum)) {
throw new Error('Table update not supported');
}
}
// Create Columns
for(const column of getColumnInformation(constructor) || []) {
const columnSQL = await this.config.adapter.createColumn(tableInfo, column);
const columnChecksum = await generateChecksum(columnSQL);
if(await this.config.adapter.columnExists(tableInfo, column) != true) {
await this.config.adapter.runQuery(columnSQL);
await this.config.adapter.setColumnChecksum(tableInfo, column, columnChecksum);
} else {
const storedHash = await this.config.adapter.getColumnChecksum(tableInfo, column);
if(!storedHash.equals(columnChecksum)) {
await this.config.adapter.updateColumn(tableInfo, column);
await this.config.adapter.setColumnChecksum(tableInfo, column, columnChecksum);
}
}
/** 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);
// Create Column Constraints
for(const constraint of getColumnConstraintInfo(constructor, column.propertyKey) || []) {
const constraintSQL = await this.config.adapter.createConstraint(tableInfo, column, constraint);
const constraintChecksum = await generateChecksum(constraintSQL);
if(await this.config.adapter.constraintExists(tableInfo, column, constraint) != true) {
await this.config.adapter.runQuery(constraintSQL);
await this.config.adapter.setConstraintChecksum(tableInfo, column, constraint, constraintChecksum);
} else {
const storedHash = await this.config.adapter.getConstraintChecksum(tableInfo, column, constraint);
if(!storedHash.equals(constraintChecksum)) {
await this.config.adapter.updateConstraint(tableInfo, column, constraint);
await this.config.adapter.setConstraintChecksum(tableInfo, column, constraint, constraintChecksum);
}
}
}
}
// Create Table Constraints
for(const constraint of getConstraintInfo(constructor) || []) {
const constraintSQL = await this.config.adapter.createConstraint(tableInfo, null, constraint);
const constraintChecksum = await generateChecksum(constraintSQL);
if(await this.config.adapter.constraintExists(tableInfo, null, constraint) != true) {
await this.config.adapter.runQuery(constraintSQL);
await this.config.adapter.setConstraintChecksum(tableInfo, null, constraint, constraintChecksum);
} else {
const storedHash = await this.config.adapter.getConstraintChecksum(tableInfo, null, constraint);
if(!storedHash.equals(constraintChecksum)) {
await this.config.adapter.updateConstraint(tableInfo, null, constraint);
await this.config.adapter.setConstraintChecksum(tableInfo, null, constraint, constraintChecksum);
}
}
}
MODEL_DATABASES.set(constructor, this);
}
this.tables.push(modelType);
}
/** Shuts down the connection to the database */
public async shutdown(): Promise<void> {
if(this.config.adapter.shutdown) {
await this.config.adapter.shutdown();
}
}
/** Drops all registered schemas */
async delete() {
const deletedSchemas = new Set<string>();
for(const table of this.tables) {
const tableInfo = getTableInformation(table);
if(deletedSchemas.has(tableInfo.options.schema)) {
continue;
}
/** Deletes the database permanently */
public async delete(): Promise<void> {
if(this.config.adapter.delete) {
await this.config.adapter.delete();
await this.adapter.schema.dropSchema(tableInfo.options.schema);
deletedSchemas.add(tableInfo.options.schema);
}
await this.shutdown();
}
/** Runs a query in the database, returning the results as an array of objects */
public async query<T = any>(sql: string, args?: any[]): Promise<T[]> {
return await this.config.adapter.runQuery<T>(sql, args);
}
/** Loads models using a SQL query */
public async load(sql: string, args?: any[]): Promise<QueryResult> {
const cursor = await this.config.adapter.getCursor(sql, args);
return new QueryResult(cursor);
}
/** Returns the database for a model */
public static getDatabase(constructor: Function): Database {
return MODEL_DATABASES.get(constructor);
/** Closes the database connection */
async close() {
await this.adapter.close();
}
}
/** Generates a checksum given an object */
async function generateChecksum(object: any): Promise<Buffer> {
const hash = crypo.createHash('sha512');
hash.update(String(object));
return hash.digest();
}
/** The configuration for a database, including the definition of the database */
export interface DatabaseConfiguration {
/** The database adapter to use */
adapter: DatabaseAdapter;
/** The tables that are in this database */
tables: ModelConstructor[];
}
import { getOriginalTable } from "./table";
import { ModelConstructor } from "../model";
const COLUMN_REGISTRY = new Map<ModelConstructor, ColumnInfo[]>();
export function getColumnInformation(constructor: ModelConstructor): ColumnInfo[] {
return COLUMN_REGISTRY.get(getOriginalTable(constructor));
}
export function Column(columnName: string, typeName: string) {
return function(target: any, propertyKey: string | symbol) {
const newInfo: ColumnInfo = {
name: columnName,
typeName: typeName,
propertyKey: propertyKey
};
if(COLUMN_REGISTRY.has(target.constructor)) {
COLUMN_REGISTRY.get(target.constructor).push(newInfo);
} else {
COLUMN_REGISTRY.set(target.constructor, [ newInfo ]);
}
}
}
export interface ColumnInfo {
name: string;
typeName: string;
propertyKey: string | symbol;
}
import { ModelConstructor } from "../model";
import { getOriginalTable } from "./table";
export interface ConstraintInfo {
name: string;
sql: string;
}
const TABLE_CONSTRAINTS = new Map<ModelConstructor, ConstraintInfo[]>();
const COLUMN_CONSTRAINTS = new Map<ModelConstructor, Map<string | symbol, ConstraintInfo[]>>();
export function getConstraintInfo(constructor: ModelConstructor): ConstraintInfo[] {
return TABLE_CONSTRAINTS.get(getOriginalTable(constructor));
}
export function getColumnConstraintInfo(constructor: ModelConstructor, propertyKey: string | symbol): ConstraintInfo[] {
const originalConstructor = getOriginalTable(constructor);
if(COLUMN_CONSTRAINTS.has(originalConstructor)) {
return COLUMN_CONSTRAINTS.get(originalConstructor).get(propertyKey);
} else {
return null;
}
}
export function Constraint(constraintName: string, sql: string) {
return function(target: any, propertyKey?: string | symbol) {
const constraintInfo: ConstraintInfo = {
name: constraintName,
sql: sql
};
// Table Constraint
if(propertyKey == null) {
if(TABLE_CONSTRAINTS.has(target)) {
TABLE_CONSTRAINTS.get(target).push(constraintInfo);
} else {
TABLE_CONSTRAINTS.set(target, [ constraintInfo ]);
}
}
// Column Constraing
else {
const constructor = target.constructor;
if(!COLUMN_CONSTRAINTS.has(constructor)) {
COLUMN_CONSTRAINTS.set(constructor, new Map());
}
const tableMap = COLUMN_CONSTRAINTS.get(constructor);
if(tableMap.has(propertyKey)) {
tableMap.get(propertyKey).push(constraintInfo);
} else {
tableMap.set(propertyKey, [ constraintInfo ]);
}
}
}
}
import "reflect-metadata";
import { DBModel } from "../model";
import { TABLE_FIELDS } from "../internal/meta-keys";
export interface DBFieldInformation {
readonly name: string;
}
/** Registers a field on a model */
export function DBField(name: string) {
return function(proto: DBModel, propertyKey: string | symbol) {
let fieldSet: DBFieldInformation[];
if(Reflect.hasMetadata(TABLE_FIELDS, proto.constructor)) {
fieldSet = Reflect.getMetadata(TABLE_FIELDS, proto.constructor);
} else {
fieldSet = [];
Reflect.defineMetadata(TABLE_FIELDS, proto.constructor, fieldSet);
}
fieldSet.push({
name: name
});
};
}
import "reflect-metadata";
import { DBModel, DBModelConstructor } from "../model";
import { TABLE_INFO } from "../internal/meta-keys";
/** Registered table information */
export interface DBTableInformation {
readonly name: string;
readonly options: Partial<DBTableOptions>;
}
/** Table options */
export interface DBTableOptions {
schema: string;
}
/** Registers a model as a table in the database */
export function DBTable(name: string, options?: Partial<DBTableOptions>) {
return function<T extends DBModel>(constructor: DBModelConstructor<T>): DBModelConstructor<T> {
const info: DBTableInformation = {
name: name,
options: options || {}
};
info.options.schema = info.options.schema || 'public';
Reflect.defineMetadata(TABLE_INFO, info, constructor);
return constructor;
};
}
import { getOriginalTable } from "./table";
import { ModelConstructor } from "../model";
const PRIMARY_KEY_REGISTRY = new Map<ModelConstructor, string | symbol>();
export function getPrimaryKey(constructor: ModelConstructor): string | symbol {
return PRIMARY_KEY_REGISTRY.get(getOriginalTable(constructor));
}
export function PrimaryKey() {
return function(target: any, propertyKey: string | symbol) {
PRIMARY_KEY_REGISTRY.set(target.constructor, propertyKey);
}
}
export * from "./table";
export * from "./column";
export * from "./constraint";
export * from "./id";
\ No newline at end of file
export * from './db-table';
export * from './db-field';
\ No newline at end of file
import { Model, ModelConstructor } from "../model";
const TABLE_REGISTRY = new Map<ModelConstructor, TableInfo>();
const TABLE_EXTENSIONS = new Map<ModelConstructor, ModelConstructor>();
/** Loads the registered table information */
export function getTableInformation(constructor: ModelConstructor): TableInfo {
return TABLE_REGISTRY.get(constructor);
}
/** Loads the original constructor for the extended constructor */
export function getOriginalTable(constructor: ModelConstructor): ModelConstructor {
return TABLE_EXTENSIONS.get(constructor);
}
/** Identifies a class as a model, mapping to a table in the database */
export function Table<T extends ModelConstructor>(tableName: string) {
return function(constructor: T) {
const extendedConstructor = class extends constructor {
};
Object.defineProperty(extendedConstructor, 'name', {
value: constructor.name,
writable: false
});
TABLE_REGISTRY.set(extendedConstructor, {
name: tableName
});
TABLE_EXTENSIONS.set(extendedConstructor, constructor);
return extendedConstructor;
};
}
/** Information about a table in a database */
export interface TableInfo {
name: string;
}
/**
SWIRL PostgreSQL Adapter
export * from './model';
export * from './database';
Main Entry Point
*/
export * from './postgres/pg-database-adapter';
export * from "./database";
export * from "./model";
export * from "./result";
export * from "./adapters/index";
export * from "./decorators/index";
export * from './decorators/index';
export * from './adapter/index';
\ No newline at end of file
import { DBModelConstructor } from "../model";
import { DBTableInformation } from "../decorators";
/** Identifier for table information */
export const TABLE_INFO = Symbol();
/** Identifier for table field lists */
export const TABLE_FIELDS = Symbol();
/** Loads the table information for a constructor */
export function getTableInformation(modelType: DBModelConstructor<any>): DBTableInformation {
return Reflect.getMetadata(TABLE_INFO, modelType);
}
import { Database } from "./database";
import { getPrimaryKey } from "./decorators";
import { DBTableInformation } from "./decorators";
/** A database model, backed by a record in a table */
export class Model {
public async save(): Promise<void> {
const db = Database.getDatabase(this.constructor as any);
const primaryKey = String(getPrimaryKey(this.constructor as any));
const id = this[primaryKey as any];
if(id != null) {
throw new Error('Update not supported');
} else {
const newId = await db.config.adapter.insert(this);
this[primaryKey] = newId;
}
}
/** A record that is backed by a table in the database */
export class DBModel {
}
export interface DBModel {
export interface Model {
[fieldName: string]: any;
}
constructor: DBModelConstructor<this>;
}
/** A constructor for a model */
export interface ModelConstructor<T extends Model = any> {
export interface DBModelConstructor<T extends DBModel> {
new(...args: any[]): T;
}
import { DBCommandAdapter, DBTransaction } from "../adapter/command-adapter";
import { PGAdapter } from "./pg-database-adapter";
import { PoolClient } from "pg";
export class PGCommandAdapter implements DBCommandAdapter {
constructor(
private readonly db: PGAdapter
) { }
async query<T = any>(sql: string, args?: any[]): Promise<T[]> {
const res = await this.db.pool.query(sql, args);
return res.rows;
}
async run(handler: (transaction: DBTransaction) => Promise<void>): Promise<void> {
let client: PoolClient;
try {
client = await this.db.pool.connect();