Commit 3fb26280 authored by William Naslund's avatar William Naslund

Began implementing model operations

parent 80a1f2a9
......@@ -5,6 +5,7 @@
"scripts": {
"start": "concurrently npm:watch:module npm:watch:tests",
"test": "mocha --require module-alias/register --recursive dist/tests",
"test:one": "mocha --require module-alias/register $1",
"watch:module": "tsc -w --preserveWatchOutput",
"watch:tests": "tsc -p tests/ -w --preserveWatchOutput"
},
......
import { DBSchemaAdapter } from "./schema-adapter";
import { DBTableAdapter } from "./table-adapter";
import { DBCommandAdapter } from "./command-adapter";
import { DBModelConstructor } from "../model";
import { DBColumnAdapter } from "./column-adapter";
import { DBConstraintAdapter } from "./constraint-adapter";
import { DBQueryAdapter } from "./query-adapter";
import { DBModelAdapter } from "./model-adapter";
/** A collection of adapters to interface with a database system */
export interface DBAdapter {
......@@ -14,6 +15,8 @@ export interface DBAdapter {
table: DBTableAdapter;
column: DBColumnAdapter;
constraint: DBConstraintAdapter;
query: DBQueryAdapter;
model: DBModelAdapter;
/** Closes the database connection */
close(): Promise<void>;
......
......@@ -3,4 +3,6 @@ export * from './command-adapter';
export * from './schema-adapter';
export * from './table-adapter';
export * from './column-adapter';
export * from './constraint-adapter';
\ No newline at end of file
export * from './constraint-adapter';
export * from './query-adapter';
export * from './model-adapter';
\ No newline at end of file
import { DBModelConstructor } from "../model";
export interface DBModelAdapter {
/** Inserts a models values, returning its id if there is one */
insert(model: DBModelConstructor<any>, values: { [field: string]: any }): Promise<any>;
}
import { DBModelConstructor } from "../model";
/** Provides a means to run SQL queries given a query request */
export interface DBQueryAdapter {
/** Runs a query request, returning the query response */
run(req: DBQueryRequest): Promise<DBQueryResponse[]>;
}
/** Identifies a table, and any related parent and child tables, that should be queried */
export interface DBQueryRequest {
model: DBModelConstructor<any>;
fields: any[];
parents: { [name: string]: DBQueryRequest; }
children: { [name: string]: DBQueryRequest; }
}
/** Contains a record that was part of a query */
export interface DBQueryResponse {
baseRecord: DBQueryRow;
parents: { [name: string]: DBQueryRow; };
children: { [name: string]: DBQueryRow[]; }
}
/** A single record from the database */
export interface DBQueryRow {
[name: string]: any;
_model: DBModelConstructor<any>;
}
import { DBAdapter } from "./adapter/database-adapter";
import { DBModelConstructor } from "./model";
import { DBModelConstructor, DBModel } from "./model";
import { getTableInformation, getTableFields, getParentLookups } from "./internal/meta-keys";
import { DBUniqueConstraint, DBConstraintConstructor, DBForeignKeyConstraint, DBPrimaryKeyConstraint } from "./constraints";
import { DBConstraintConstructor, DBForeignKeyConstraint, DBPrimaryKeyConstraint } from "./constraints";
import { DBTableInformation } from "./decorators";
import { DBQuery } from "./query";
import { MODEL_DATABASE } from "./internal/model-registry";
export class Database {
......@@ -10,12 +12,13 @@ export class Database {
private readonly tables = new Set<DBModelConstructor<any>>();
constructor(
private readonly adapter: DBAdapter
public readonly adapter: DBAdapter
) { }
/** Registers and migrates a table in the database */
register(modelType: DBModelConstructor<any>) {
Reflect.defineMetadata(MODEL_DATABASE, this, modelType);
this.tables.add(modelType);
return this;
}
......@@ -89,6 +92,12 @@ export class Database {
}
/** Creates a query for models */
query<T extends DBModel>(model: DBModelConstructor<T>): DBQuery<T> {
return new DBQuery(model, this);
}
/** Drops all registered schemas */
async delete() {
const deletedSchemas = new Set<string>();
......
......@@ -11,7 +11,7 @@ export interface DBFieldInformation {
/** Registers a field on a model */
export function DBField(name: string, type: DBType) {
return function(proto: DBModel, propertyKey: string | symbol) {
return function(proto: DBModel, propertyKey: string | symbol): any {
let fieldSet: DBFieldInformation[] = Reflect.getMetadata(TABLE_FIELDS, proto.constructor) || [];
const fieldInfo: DBFieldInformation = {
......@@ -23,5 +23,27 @@ export function DBField(name: string, type: DBType) {
Reflect.defineMetadata(TABLE_FIELDS, fieldSet, proto.constructor);
Reflect.defineMetadata(FIELD_INFO, fieldInfo, proto.constructor, propertyKey);
return {
enumerable: true,
configurable: false,
get() {
if(fieldInfo.type.parse) {
return fieldInfo.type.parse((this as DBModel)._contents[fieldInfo.name]);
} else {
return (this as DBModel)._contents[fieldInfo.name];
}
},
set(val: any) {
if(fieldInfo.type.serialize) {
(this as DBModel)._contents[fieldInfo.name] = fieldInfo.type.serialize(val);
} else {
(this as DBModel)._contents[fieldInfo.name] = val;
}
}
}
};
}
......@@ -25,7 +25,6 @@ export function DBTable(name: string, options?: Partial<DBTableOptions>) {
info.options.schema = info.options.schema || 'public';
Reflect.defineMetadata(TABLE_INFO, info, constructor);
return constructor;
};
}
export * from './model';
export * from './database';
export * from './query';
export * from './postgres/pg-database-adapter';
......
export const MODEL_DATABASE = Symbol();
import { DBTableInformation } from "./decorators";
import "reflect-metadata";
import * as os from "os";
import { MODEL_DATABASE } from "./internal/model-registry";
import { Database } from "./database";
import { getTableInformation, getTableFields } from "./internal/meta-keys";
/** A record that is backed by a table in the database */
export class DBModel {
public readonly _contents: any = {};
/** Inserts this record into the database */
async insert() {
const db = Reflect.getMetadata(MODEL_DATABASE, this.constructor) as Database;
if(db == null) {
throw new Error(`This model (${this.constructor}) has not been registered with a database`);
}
const tableInfo = getTableInformation(this.constructor);
const newId = await db.adapter.model.insert(this.constructor, this._contents);
if(tableInfo.options.id) {
this[tableInfo.options.id] = newId;
}
}
toString(): string {
let str = `${this.constructor.name} {`;
const fieldStrings: string[] = [];
for(const field in this._contents) {
fieldStrings.push(`${field}: ${this._contents[field]}`);
}
if(fieldStrings.length > 0) {
str += os.EOL + ' ' + fieldStrings.join(os.EOL + ' ');
}
return str + ' }';
}
}
export interface DBModel {
......@@ -10,6 +44,7 @@ export interface DBModel {
}
/** A constructor of a user defined model */
export interface DBModelConstructor<T extends DBModel> {
new(...args: any[]): T;
......
......@@ -5,6 +5,8 @@ import { PGCommandAdapter } from "./pg-command-adapter";
import { PGTableAdapter } from "./pg-table-adapter";
import { PGColumnAdapter } from "./pg-column-adapter";
import { PGConstraintAdapter } from "./pg-constraint-adapter";
import { PGQueryAdapter } from "./pg-query-adapter";
import { PGModelAdapter } from "./pg-model-adapter";
import { Pool } from "pg";
/** Adapts to a PostgreSQL database system */
......@@ -16,6 +18,8 @@ export class PGAdapter implements DBAdapter {
table: PGTableAdapter;
column: PGColumnAdapter;
constraint: PGConstraintAdapter;
query: PGQueryAdapter;
model: PGModelAdapter;
/** The connection pool for this adapter */
public readonly pool: Pool;
......@@ -28,6 +32,8 @@ export class PGAdapter implements DBAdapter {
this.table = new PGTableAdapter(this);
this.column = new PGColumnAdapter(this);
this.constraint = new PGConstraintAdapter(this);
this.query = new PGQueryAdapter(this);
this.model = new PGModelAdapter(this);
}
......
import { DBModelAdapter } from "../adapter/model-adapter";
import { DBModelConstructor } from "../model";
import { getTableInformation, getFieldInformation } from "../internal/meta-keys";
import { PGAdapter } from "./pg-database-adapter";
export class PGModelAdapter implements DBModelAdapter {
constructor(
private readonly db: PGAdapter
) { }
async insert(model: DBModelConstructor<any>, values: { [field: string]: any }) {
const fieldNames: string[] = [];
const placeholders: string[] = [];
const fieldValues = [];
for(const fieldName in values) {
fieldNames.push(fieldName);
fieldValues.push(values[fieldName]);
placeholders.push(`$${fieldValues.length}`);
}
const modelInfo = getTableInformation(model);
const idInfo = modelInfo.options.id ? getFieldInformation(model, modelInfo.options.id) : null;
const insertRes = await this.db.command.query(`
INSERT INTO ${modelInfo.options.schema}.${model.name}
(${fieldNames.join(', ')})
VALUES (${placeholders.join(', ')})
${idInfo ? 'RETURNING ' + idInfo.name : ''}
`, fieldValues);
if(idInfo) {
return insertRes[0][idInfo.name];
} else {
return null;
}
}
}
import { DBQueryAdapter, DBQueryRequest, DBQueryResponse } from "../adapter/query-adapter";
import { PGAdapter } from "./pg-database-adapter";
import { getTableInformation } from "../internal/meta-keys";
export class PGQueryAdapter implements DBQueryAdapter {
constructor(
private readonly db: PGAdapter
) { }
async run(req: DBQueryRequest): Promise<DBQueryResponse[]> {
const baseInfo = getTableInformation(req.model);
let sql = `SELECT ${req.fields.join(', ')} `;
sql += `FROM ${baseInfo.name}`;
const parsedList: DBQueryResponse[] = [];
for(const row of await this.db.command.query(sql)) {
const parsed: DBQueryResponse = {
baseRecord: row,
parents: { },
children: { }
};
parsedList.push(parsed);
}
return parsedList;
}
}
import { DBModel, DBModelConstructor } from './model';
import { Database } from './database';
import { DBQueryRequest } from './adapter';
export class DBQuery<T extends DBModel> {
/** The base table that is being selected */
private readonly req: DBQueryRequest;
constructor(
model: DBModelConstructor<T>,
private readonly db: Database
) {
this.req = {
model: model,
fields: null,
parents: { },
children: { }
};
}
/** Returns the first row from the query */
async getOne(): Promise<T> {
const res = await this.getAll();
return res[0];
}
/** Returns all the rows from the query */
async getAll(): Promise<T[]> {
if(this.req.fields == null) {
this.req.fields.push('*');
}
const res = await this.db.adapter.query.run(this.req);
console.log(res);
return [];
}
/** Adds fields on the base model to be selected */
select(...fields: (keyof T)[]): this {
this.req.fields = fields;
return this;
}
}
import { getTestAdapter } from "./db/adapter";
import { Account } from "./db/account";
import { Database } from "@swirl/db";
import { Contact } from "./db/contact";
describe('Database', function() {
describe('migrate()', function() {
let db: Database;
beforeEach(async () => {
db = new Database(getTestAdapter());
});
it('Creates the schema for a brand new database', async () => {
await db.register(Account).register(Contact).migrate();
});
it('Does not change anything when updating the existing schema', async () => {
await db.register(Account).register(Contact).migrate();
await db.migrate();
});
afterEach(async () => {
await db.delete();
await db.close();
});
});
});
import { PGAdapter } from "@swirl/db";
export const TEST_ADAPTER = new PGAdapter({
host: 'localhost',
user: 'admin',
password: 'admin',
database: 'test'
});
export function getTestAdapter() {
return new PGAdapter({
host: 'localhost',
user: 'admin',
password: 'admin',
database: 'test'
});
}
import { Database } from "@swirl/db";
import { getTestAdapter } from "./db/adapter";
import { Account } from "./db/account";
import { Contact } from "./db/contact";
describe('DBQuery', function() {
let db: Database;
beforeEach('Setup the database', async () => {
db = new Database(getTestAdapter());
await db.register(Account).register(Contact).migrate();
});
describe('select()', function() {
it('Loads the selected fields into the model', async () => {
const test = new Account();
test.name = 'Test Account';
await test.insert();
const person = new Contact();
person.firstName = 'Tommy';
person.lastName = 'Toaster';
person.account = test;
await person.insert();
console.log(String(person));
const res = await db.query(Account)
.select('id', 'name')
.getOne();
console.log(res);
});
});
afterEach('Reset the database', async () => {
await db.delete();
await db.close();
db = null;
});
});
import { TEST_ADAPTER } from "./db/adapter";
import { Account } from "./db/account";
import { Database } from "@swirl/db";
import { Contact } from "./db/contact";
describe('migrate()', function() {
let db: Database;
beforeEach(async () => {
db = new Database(TEST_ADAPTER);
});
it('Creates the schema for a brand new database', async () => {
await db.register(Account).register(Contact).migrate();
});
it('Does not change anything when updating the existing schema', async () => {
await db.register(Account).register(Contact).migrate();
await db.migrate();
});
it('Testing 123', async function() {
});
afterEach(async () => {
await db.delete();
});
after(async () => {
await db.close();
});
});
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