Commit 416ca3a5 authored by William Naslund's avatar William Naslund

Setup record inserting

parent 5b9e1815
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 {
......@@ -15,7 +16,7 @@ export interface DatabaseAdapter {
/** Runs a SQL query */
runQuery<T = any>(sql: string): Promise<T[]>;
runQuery<T = any>(sql: string, args?: any[]): Promise<T[]>;
/** Returns true if a table exists */
......@@ -66,4 +67,8 @@ export interface DatabaseAdapter {
/** 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>;
}
import { DatabaseAdapter } from "../adapter";
import { Pool, PoolClient } from "pg";
import { TableInfo, ColumnInfo } from "../decorators";
import { TableInfo, ColumnInfo, getColumnInformation, getTableInformation, getPrimaryKey } from "../decorators";
import { ConstraintInfo } from "../decorators/constraint";
import { Model } from "../model";
/** Connects to a PostgreSQL database */
export class PostgresAdapter implements DatabaseAdapter {
......@@ -56,8 +57,8 @@ export class PostgresAdapter implements DatabaseAdapter {
// Query Interface
async runQuery<T = any>(sql: string): Promise<T[]> {
const res = await this.pool.query(sql);
async runQuery<T = any>(sql: string, args?: any[]): Promise<T[]> {
const res = await this.pool.query(sql, args);
return res.rows;
}
......@@ -337,6 +338,33 @@ export class PostgresAdapter implements DatabaseAdapter {
}
}
// Model Interface
async insert<T extends Model>(record: T): Promise<string | number> {
const table = getTableInformation(record.constructor as any);
const id = String(getPrimaryKey(record.constructor as any));
const fields: string[] = [];
const values: any[] = [];
const placeholders: string[] = [];
for(const field of getColumnInformation(record.constructor as any) || []) {
const fieldValue = record[field.propertyKey as any];
if(fieldValue == null) continue;
fields.push(field.name);
values.push(fieldValue);
placeholders.push(`$${values.length}`);
}
const res = await this.pool.query(`
INSERT INTO public.${table.name} (${fields.join(', ')})
VALUES (${placeholders.join(', ')})
RETURNING ${id}
`, values);
return res.rows[0][id];
}
}
......
......@@ -3,11 +3,14 @@ import { ModelConstructor } from "./model";
import { getTableInformation, getColumnInformation, getConstraintInfo, getColumnConstraintInfo } from "./decorators";
import * as crypo from "crypto";
/** 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 {
constructor(
private readonly config: DatabaseConfiguration
public readonly config: DatabaseConfiguration
) { }
......@@ -88,6 +91,7 @@ export class Database {
}
}
MODEL_DATABASES.set(constructor, this);
}
}
......@@ -108,8 +112,14 @@ export class Database {
/** Runs a query in the database, returning the results as an array of objects */
public async query<T = any>(sql: string): Promise<T[]> {
return await this.config.adapter.runQuery<T>(sql);
public async query<T = any>(sql: string, args?: any[]): Promise<T[]> {
return await this.config.adapter.runQuery<T>(sql, args);
}
/** Returns the database for a model */
public static getDatabase(constructor: Function): Database {
return MODEL_DATABASES.get(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";
\ No newline at end of file
export * from "./constraint";
export * from "./id";
\ No newline at end of file
import { Database } from "./database";
import { getPrimaryKey } from "./decorators";
/** A database model, backed by a record in a table */
export class Model {
public async save(): Promise<void> {
console.log('SAVING');
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;
}
}
}
......
......@@ -12,7 +12,7 @@ describe('Database', function() {
tables: [
Contact
]
})
});
});
afterEach('Delete the database', async function() {
......@@ -35,13 +35,6 @@ describe('Database', function() {
table_schema = 'public'
AND table_name = 'contacts'
`)).length == 1);
console.log(await db.query(`
SELECT *
FROM information_schema.columns
WHERE
table_name = 'contacts'
`));
});
});
......
import { Table, Model, Column, Constraint } from "@swirl/db";
import { Table, Model, Column, Constraint, PrimaryKey } from "@swirl/db";
@Table('contacts')
@Constraint('first_name_not_andy', 'CHECK (first_name != \'andy\' AND first_name != \'johnston\')')
@Constraint('contact_primary_key', 'PRIMARY KEY(id)')
export class Contact extends Model {
@Column('id', 'serial')
@Constraint('id_not_null', 'NOT NULL')
@PrimaryKey()
id?: number;
@Column('first_name', 'varchar(90)')
@Constraint('not_null', 'NOT NULL')
@Constraint('default_name', 'DEFAULT(\'franky\')')
......
import * as assert from "assert";
import { Database } from "@swirl/db";
import { TEST_ADAPTER } from "./db/adapter";
import { Contact } from "./db/contact";
describe('Model', function() {
let db: Database;
beforeEach('Setup the database', async function() {
db = new Database({
adapter: TEST_ADAPTER,
tables: [
Contact
]
});
await db.setup();
});
afterEach('Delete the database', async function() {
if(db) {
await db.delete();
db = null;
}
});
describe('save()', async function() {
it('Sets up the record id', async function() {
const con = new Contact();
con.firstName = 'Tommy';
await con.save();
assert(con.id != null);
const res = await db.query(`SELECT * FROM contacts WHERE id = $1`, [ con.id ]);
assert(res.length == 1);
assert(res[0].first_name = 'Tommy');
});
});
});
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