Commit d2f3ae91 authored by William Naslund's avatar William Naslund

Added column constraints

parent dfaf4417
import { DBTableInformation } from "../decorators";
import { DBConstraint, DBConstraintConstructor } from "../constraints";
import { DBTableInformation, DBFieldInformation } from "../decorators";
import { DBConstraint, DBConstraintConstructor, DBColumnConstraintObject, DBColumnConstraintConstructor } from "../constraints";
/** Manages table constriants */
export interface DBConstraintAdapter {
......@@ -13,6 +13,15 @@ export interface DBConstraintAdapter {
/** Drops a constraint on a table */
drop(table: DBTableInformation, name: string): Promise<void>;
/** Returns the constraints currently enabled for the column */
listFieldConstraints(table: DBTableInformation, field: DBFieldInformation): Promise<DBColumnConstraintObject[]>;
/** Creates a column constraint */
createFieldConstraint(table: DBTableInformation, field: DBFieldInformation, constraint: DBColumnConstraintObject): Promise<void>;
/** Drops a column constraint */
dropFieldConstraint(table: DBTableInformation, field: DBFieldInformation, constraint: DBColumnConstraintObject): Promise<void>;
}
export interface DBConstraintId {
......
......@@ -5,12 +5,6 @@ export interface DBConstraint {
/** Returns the SQL necessary to create the constraint */
create(): Promise<string>;
/** If this is a column constraint, this is the SQL to create and drop the constraint */
columnModifySQL?(): {
create: string;
drop: string;
};
/** Returns the type identifer of this constraint (used in the constraint name) */
getTypeId(): string;
......@@ -20,3 +14,19 @@ export interface DBConstraint {
export interface DBConstraintConstructor {
new(...args: any[]): DBConstraint;
}
/** A column level constraint */
export interface DBColumnConstraintObject {
/** Returns the SQL to add the constraint to a column */
create(): Promise<string>;
/** Returns the SQL to drop the constraint from a column */
drop(): Promise<string>;
}
/** A column constraint constructor */
export interface DBColumnConstraintConstructor {
new(...args: any[]): DBColumnConstraintObject;
}
export * from './constraint';
export * from './foreign-key-constraint';
export * from './unique-constraint';
export * from './primary-key-constraint';
\ No newline at end of file
export * from './primary-key-constraint';
export * from './not-null-constraint';
export * from './sql-default-constraint';
\ No newline at end of file
import { DBColumnConstraintObject } from "./constraint";
export class DBNotNullConstraint implements DBColumnConstraintObject {
async create() {
return 'SET NOT NULL';
}
async drop() {
return 'DROP NOT NULL';
}
}
import { DBColumnConstraintObject } from "./constraint";
export class DBSQLDefaultConstraint implements DBColumnConstraintObject {
constructor(
private readonly defaultSQL: string
) { }
async create() {
return `SET DEFAULT ${this.defaultSQL}`;
}
async drop() {
return `DROP DEFAULT`;
}
}
......@@ -2,9 +2,13 @@ import { DBConstraint } from "./constraint";
export class DBUniqueConstraint implements DBConstraint {
private readonly fields: string[];
constructor(
private readonly fields: string[]
) { }
...fields: string[]
) {
this.fields = fields;
}
async create() {
return `
......
import { DBAdapter } from "./adapter/database-adapter";
import { DBModelConstructor, DBModel } from "./model";
import { getTableInformation, getTableFields, getParentLookups, getTableConstraints } from "./internal/meta-keys";
import { DBConstraintConstructor, DBForeignKeyConstraint, DBPrimaryKeyConstraint, DBConstraint } from "./constraints";
import { getTableInformation, getTableFields, getParentLookups, getTableConstraints, getColumnConstraints } from "./internal/meta-keys";
import { DBConstraintConstructor, DBForeignKeyConstraint, DBPrimaryKeyConstraint, DBConstraint, DBColumnConstraintObject, DBNotNullConstraint, DBSQLDefaultConstraint, DBColumnConstraintConstructor } from "./constraints";
import { DBTableInformation, DBFieldInformation } from "./decorators";
import { DBQuery } from "./query";
import { MODEL_DATABASE } from "./internal/model-registry";
import { DBIdSequence } from "./types";
export class Database {
......@@ -114,6 +115,62 @@ export class Database {
}
}
// Setup Column Constraints
for(const modelType of this.tables) {
const table = getTableInformation(modelType);
for(const field of getTableFields(modelType) || []) {
const fieldConstraints = await this.adapter.constraint.listFieldConstraints(table, field) || [];
const constraintsToKeep = new Set<DBColumnConstraintConstructor>();
for(const constraint of getColumnConstraints(modelType, field.propertyKey) || []) {
constraintsToKeep.add(constraint.constraint.constructor as any);
let existingConstraint: DBColumnConstraintObject = null;
for(const existingConstraintCandidate of fieldConstraints) {
if(existingConstraintCandidate instanceof constraint.constraint.constructor) {
existingConstraint = existingConstraintCandidate;
break;
}
}
let insertNewConstraint = true;
if(existingConstraint != null) {
switch(existingConstraint.constructor) {
case DBNotNullConstraint:
insertNewConstraint = false;
break;
case DBSQLDefaultConstraint:
await this.adapter.constraint.dropFieldConstraint(table, field, existingConstraint);
break;
}
}
if(insertNewConstraint) {
await this.adapter.constraint.createFieldConstraint(table, field, constraint.constraint);
}
}
// Make sure we don't delete not null and default for the primary key
if(table.options.id && field.propertyKey == table.options.id) {
if(field.type instanceof DBIdSequence) {
constraintsToKeep.add(DBSQLDefaultConstraint);
}
constraintsToKeep.add(DBNotNullConstraint);
}
for(const existingConstraint of fieldConstraints) {
if(!constraintsToKeep.has(existingConstraint.constructor as any)) {
await this.adapter.constraint.dropFieldConstraint(table, field, existingConstraint);
}
}
}
}
}
......
import "reflect-metadata";
import { DBConstraint } from "../constraints/constraint";
import { DBConstraint, DBColumnConstraintObject } from "../constraints/constraint";
import { DBModelConstructor, DBModel } from "../model";
import { TABLE_CONSTRAINTS, DBRegisteredTableConstraint, COLUMN_CONSTRAINTS, DBRegisteredColumnConstraint } from "../internal/meta-keys";
......@@ -24,7 +24,7 @@ export function DBTableConstraint(name: string, constraint: DBConstraint) {
}
/** Adds a constraint to a column */
export function DBColumnConstraint(name: string, constraint: DBConstraint) {
export function DBColumnConstraint(constraint: DBColumnConstraintObject) {
return function<T extends DBModel>(proto: T, propertyKey: string | symbol) {
let constraintList = Reflect.getMetadata(COLUMN_CONSTRAINTS, proto.constructor, propertyKey) as DBRegisteredColumnConstraint[];
if(constraintList == null) {
......@@ -33,7 +33,6 @@ export function DBColumnConstraint(name: string, constraint: DBConstraint) {
constraintList.push({
table: proto.constructor,
name: name,
propertyKey: propertyKey,
constraint: constraint
});
......
import { DBModelConstructor, DBModel } from "../model";
import { DBTableInformation, DBFieldInformation, DBLookupInformation } from "../decorators";
import { DBConstraint } from "../constraints";
import { DBConstraint, DBColumnConstraintObject } from "../constraints";
/** Identifier for table information */
export const TABLE_INFO = Symbol();
......@@ -98,6 +98,8 @@ export interface DBRegisteredTableConstraint {
constraint: DBConstraint;
}
export interface DBRegisteredColumnConstraint extends DBRegisteredTableConstraint {
export interface DBRegisteredColumnConstraint {
propertyKey: any;
table: DBModelConstructor<any>;
constraint: DBColumnConstraintObject;
}
import { DBConstraintAdapter, DBConstraintId } from "../adapter/constraint-adapter";
import { DBTableInformation } from "../decorators";
import { DBConstraint, DBUniqueConstraint, DBForeignKeyConstraint, DBPrimaryKeyConstraint } from "../constraints";
import { DBTableInformation, DBFieldInformation } from "../decorators";
import { DBConstraint, DBUniqueConstraint, DBForeignKeyConstraint, DBPrimaryKeyConstraint, DBColumnConstraintConstructor, DBNotNullConstraint, DBSQLDefaultConstraint, DBColumnConstraintObject } from "../constraints";
import { PGAdapter } from "./pg-database-adapter";
/** Constraint adapter for a PostgreSQL database */
......@@ -47,6 +47,31 @@ export class PGConstraintAdapter implements DBConstraintAdapter {
return constraints;
}
async listFieldConstraints(table: DBTableInformation, field: DBFieldInformation) {
const fieldInfoRes = await this.db.pool.query(`
SELECT is_nullable, column_default
FROM information_schema.columns
WHERE
table_schema = $1
AND table_name = $2
AND column_name = $3
`, [ table.options.schema, table.name, field.name ]);
const constraints: DBColumnConstraintObject[] = [];
if(fieldInfoRes.rows.length > 0) {
const fieldInfo = fieldInfoRes.rows[0];
if(fieldInfo.is_nullable == false) {
constraints.push(new DBNotNullConstraint());
}
if(fieldInfo.column_default != null && fieldInfo.column_default.trim() != '') {
constraints.push(new DBSQLDefaultConstraint(fieldInfo.column_default));
}
}
return constraints;
}
async create(table: DBTableInformation, name: string, constraint: DBConstraint) {
await this.db.pool.query(`
ALTER TABLE ${table.options.schema}.${table.name}
......@@ -55,6 +80,14 @@ export class PGConstraintAdapter implements DBConstraintAdapter {
`);
}
async createFieldConstraint(table: DBTableInformation, field: DBFieldInformation, constraint: DBColumnConstraintObject) {
await this.db.pool.query(`
ALTER TABLE ${table.options.schema}.${table.name}
ALTER COLUMN ${field.name}
${await constraint.create()}
`);
}
async drop(table: DBTableInformation, name: string) {
await this.db.pool.query(`
ALTER TABLE ${table.options.schema}.${table.name}
......@@ -62,4 +95,12 @@ export class PGConstraintAdapter implements DBConstraintAdapter {
`);
}
async dropFieldConstraint(table: DBTableInformation, field: DBFieldInformation, constraint: DBColumnConstraintObject) {
await this.db.pool.query(`
ALTER TABLE ${table.options.schema}.${table.name}
ALTER COLUMN ${field.name}
${await constraint.drop()}
`);
}
}
......@@ -23,13 +23,23 @@ export class PGModelAdapter implements DBModelAdapter {
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}.${modelInfo.name}
(${fieldNames.join(', ')})
VALUES (${placeholders.join(', ')})
${idInfo ? 'RETURNING ' + idInfo.name : ''}
`, fieldValues);
let insertSQL: string;
if(fieldNames.length > 0) {
insertSQL = `
INSERT INTO ${modelInfo.options.schema}.${modelInfo.name}
(${fieldNames.join(', ')})
VALUES (${placeholders.join(', ')})
${idInfo ? 'RETURNING ' + idInfo.name : ''}
`;
} else {
insertSQL = `
INSERT INTO ${modelInfo.options.schema}.${modelInfo.name}
DEFAULT VALUES
${idInfo ? 'RETURNING ' + idInfo.name : ''}
`;
}
const insertRes = await this.db.command.query(insertSQL, fieldValues);
if(idInfo) {
return insertRes[0][idInfo.name];
} else {
......
import * as assert from "assert";
import { Database, DBTableConstraint, DBUniqueConstraint, DBColumnConstraint, DBNotNullConstraint, DBSQLDefaultConstraint } from "@swirl/db";
import { getTestAdapter } from "./db/adapter";
import { Account } from "./db/account";
import { Contact } from "./db/contact";
import { Organization } from "./db/org";
describe('Constraints', function() {
let db: Database;
beforeEach('Setup the database', async () => {
db = new Database(getTestAdapter());
});
describe('DBUniqueConstraint', function() {
it('Prevents duplicate values in a single field', async () => {
DBTableConstraint('unique_name', new DBUniqueConstraint('name'))(Account);
await db.register(Account).register(Contact).register(Organization).migrate();
const accA = new Account();
accA.name = 'Test';
await accA.insert();
const accB = new Account();
accB.name = 'Test';
let dupErr = null;
try {
await accB.insert();
} catch(err) {
dupErr = err;
}
assert(dupErr != null);
const queried = await db.adapter.command.query(`
SELECT id, name
FROM account
`);
assert(queried.length == 1);
assert(queried[0].id == accA.id);
});
});
describe('DBNotNullConstraint', function() {
beforeEach('migrate', async () => {
await db.register(Account).register(Contact).register(Organization).migrate();
});
it('Prevents null values in a column', async () => {
const acc = new Account();
let nullErr = null;
try {
await acc.insert();
} catch(err) {
nullErr = err;
}
assert(nullErr != null);
});
});
describe('DBSQLDefaultConstraint', function() {
it('Sets the default value of a column', async () => {
DBColumnConstraint(new DBSQLDefaultConstraint(`'joey'`))(Account.prototype, 'name');
await db.register(Account).register(Contact).register(Organization).migrate();
const acc = new Account();
await acc.insert();
const res = await db.adapter.command.query(`
SELECT id, name
FROM account
`);
assert(res.length == 1);
assert(res[0].id == acc.id);
assert(res[0].name == 'joey');
});
});
afterEach('Reset the database', async () => {
await db.delete();
await db.close();
db = null;
});
});
import { DBTable, DBModel, DBField, DBVarChar, DBChar, DBIdSequence, DBLookup } from "@swirl/db";
import { DBTable, DBModel, DBField, DBVarChar, DBChar, DBIdSequence, DBLookup, DBColumnConstraint, DBNotNullConstraint } from "@swirl/db";
import { Organization } from "./org";
@DBTable('account', {
......@@ -10,6 +10,7 @@ export class Account extends DBModel {
id: number;
@DBField('name', new DBVarChar(80))
@DBColumnConstraint(new DBNotNullConstraint())
name: string;
@DBLookup('org', Organization)
......
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