Commit aa6ca895 authored by William Naslund's avatar William Naslund

Added query where filter to query interface

parent 53fd24fc
......@@ -6,6 +6,9 @@ export interface DBQueryAdapter {
/** Runs a query request, returning the query response */
run(req: DBQueryRequest): Promise<DBQueryResponse[]>;
/** Returns the table reference name for the given quer and identifier */
getRef(req: DBQueryRequest, identifier: string[]): string;
}
/** Contains a record that was part of a query */
......
import { DBCompoundCondition } from "./compound-condition";
import { DBModel } from "../model";
/** A condition where all contained conditions must be met */
export class DBAnd<T extends DBModel> extends DBCompoundCondition<T> {
joiningSQL() {
return 'AND';
}
}
import { DBCondition } from "./db-condition";
import { DBModel } from "../model";
/** A condition that combines other conditions */
export abstract class DBCompoundCondition<T extends DBModel> implements DBCondition<T> {
/** The parameters from each condition */
private conditionParameters: any[];
/** The conditions that are part of this compound condition */
private readonly conditions: DBCondition<T>[];
constructor(
...conditions: DBCondition<T>[]
) {
this.conditions = conditions || [];
}
/** Returns the SQL to be placed in between each condition */
protected abstract joiningSQL(): string;
sql() {
const conditionSQL: string[] = [];
this.conditionParameters = [];
for(const condition of this.conditions) {
conditionSQL.push( offsetPlaceholders(condition.sql(), this.conditionParameters.length) );
if(condition.parameters != null) {
for(const param of condition.parameters()) {
this.conditionParameters.push(param);
}
}
}
if(conditionSQL.length < 1) return '';
else return `(${conditionSQL.join(' ' + this.joiningSQL() + ' ')})`;
}
parameters() {
if(this.conditionParameters == null) {
this.sql();
}
return this.conditionParameters;
}
}
/** Updates all placeholder arguments ($X) by adding a number to them */
export function offsetPlaceholders(sql: string, offset: number): string {
if(sql == null) return null;
return sql.replace(/\$(\d+)/g, (_, numberText) => {
const originalNumber = parseInt(numberText, 10);
return `$${originalNumber + offset}`;
});
}
import { DBModel } from "../model";
/** Base interface for classes that are used to filter based on a certain condition */
export interface DBCondition<T extends DBModel> {
/** Returns the SQL for this condition, to be used in a the WHERE section of a SELECT */
sql(): string;
/** Returns any query parameters that need to be included in the query */
parameters?(): any[];
}
import { DBCondition } from "./db-condition";
import { DBModel, DBModelConstructor } from "../model";
import { DBFieldInformation } from "../decorators";
import { getFieldInformation } from "../internal/meta-keys";
/** A condition where a value is exactly equal */
export class DBEquals<T extends DBModel> implements DBCondition<T> {
private readonly field: DBFieldInformation;
constructor(
model: DBModelConstructor<T>, propertyKey: keyof T,
private readonly value: any
) {
this.field = getFieldInformation(model, propertyKey);
}
sql() {
return `${String(this.field.name)} = $1`;
}
parameters() {
return [ this.value ];
}
}
import { DBCondition } from "./db-condition";
import { DBModel, DBModelConstructor } from "../model";
import { getFieldInformation } from "../internal/meta-keys";
import { DBFieldInformation } from "../decorators";
/** A condition where a value is in a collection of values */
export class DBIn<T extends DBModel> implements DBCondition<T> {
/** The values for the condition */
private readonly values: any[];
/** The name of the field being queried */
private readonly field: DBFieldInformation;
constructor(model: DBModelConstructor<T>, propertyKey: keyof T, values: any[]);
constructor(model: DBModelConstructor<T>, propertyKey: keyof T, iterable: IterableIterator<any>);
constructor(model: DBModelConstructor<T>, propertyKey: keyof T, values: any) {
if(values == null) {
this.values = [];
}
else if(Array.isArray(values)) {
this.values = values;
}
else if(typeof values[Symbol.iterator] === 'function') {
this.values = [];
for(const val of values) {
this.values.push(val);
}
}
else {
throw new Error(`Unsupported argument for DBIn: ${values}`);
}
this.field = getFieldInformation(model, propertyKey);
}
sql() {
return `${this.field.name} IN $1`;
}
parameters() {
return [ this.values ];
}
}
export * from './db-condition';
export * from './equals';
export * from './in';
export * from './and';
export * from './or';
\ No newline at end of file
import { DBCompoundCondition } from "./compound-condition";
import { DBModel } from "../model";
/** A condition where any of the containing conditions is met */
export class DBOr<T extends DBModel> extends DBCompoundCondition<T> {
joiningSQL() {
return 'OR';
}
}
......@@ -8,4 +8,3 @@ export * from './decorators/index';
export * from './adapter/index';
export * from './types/index';
export * from './constraints/index';
export * from './conditions/index';
import "reflect-metadata";
import { DBQueryAdapter, DBQueryResponse } from "../adapter/query-adapter";
import { PGAdapter } from "./pg-database-adapter";
import { getTableInformation, getFieldInformation } from "../internal/meta-keys";
import { getTableInformation, getFieldInformation, LOOKUP_INFO } from "../internal/meta-keys";
import { DBQueryRequest, DBQueryParent } from "../query";
import { DBModelConstructor } from "../model";
import { DBLookupInformation } from "../decorators";
export class PGQueryAdapter implements DBQueryAdapter {
......@@ -26,8 +29,13 @@ export class PGQueryAdapter implements DBQueryAdapter {
}
sql += ' ';
// WHERE
if(req.where != null && req.where.trim() != '') {
sql += 'WHERE ' + req.where.trim();
}
const recordList: DBQueryResponse[] = [];
for(const row of await this.db.command.query(sql)) {
for(const row of await this.db.command.query(sql, req.whereArgs)) {
const record: DBQueryResponse = {
record: row,
parents: { },
......@@ -44,6 +52,35 @@ export class PGQueryAdapter implements DBQueryAdapter {
return recordList;
}
getRef(req: DBQueryRequest, identifier: string[]) {
let ref = '';
let currentModel: DBModelConstructor<any> = req.model;
for(const idPart of identifier) {
const lookup = Reflect.getMetadata(LOOKUP_INFO, currentModel, idPart) as DBLookupInformation;
if(lookup != null) {
ref += `__${lookup.name}`;
currentModel = lookup.model;
continue;
}
const field = getFieldInformation(currentModel, idPart);
if(field == null) {
throw new Error(`Unable to find field for key "${idPart}" in model ${currentModel.name}`);
}
ref += `.${field.name}`;
break;
}
if(identifier.length == 1) {
return '__' + ref;
} else {
return ref;
}
}
private getParentFields(lookupId: string, parent: DBQueryParent<any>): string[] {
const parentFields: string[] = [];
......
......@@ -4,7 +4,6 @@ import { Database } from './database';
import { getTableFields, TABLE_PARENT_LOOKUPS, getFieldInformation, LOOKUP_INFO } from './internal/meta-keys';
import { DBLookupInformation, DBTableInformation } from './decorators';
import { DBQueryResponse } from "./adapter";
import { DBCondition } from "./conditions";
export class DBQuery<T extends DBModel> {
......@@ -21,7 +20,7 @@ export class DBQuery<T extends DBModel> {
this.req = {
model: model,
fields: [ ],
condition: null,
where: null,
parents: { },
children: { }
};
......@@ -87,8 +86,9 @@ export class DBQuery<T extends DBModel> {
}
/** Adds the condition to this query */
public where(condition: DBCondition<T>): this {
this.req.condition = condition;
public where(sql: string, args?: any[]): this {
this.req.where = sql;
this.req.whereArgs = args;
return this;
}
......@@ -101,6 +101,11 @@ export class DBQuery<T extends DBModel> {
return parentLookup;
}
/** Returns the internal table name for a field or parent field */
public ref(...identifier: string[]): string {
return this.db.adapter.query.getRef(this.req, identifier);
}
/** Sets the fields on a model given its data */
private setFieldData(model: DBModelConstructor<any>, record: any, res: DBQueryResponse) {
const lookupFields = new Set<string>();
......@@ -142,7 +147,8 @@ export class DBQuery<T extends DBModel> {
export interface DBQueryRequest<T extends DBModel = any> {
model: DBModelConstructor<T>;
fields: any[];
condition: DBCondition<T>;
where: string;
whereArgs?: any[];
parents: { [name: string]: DBQueryParent<any>; }
children: { [name: string]: DBQueryRequest; }
}
......@@ -170,7 +176,7 @@ export class DBQueryParent<T extends DBModel> {
this.req = {
model: model,
fields: fieldsToSelect,
condition: null,
where: null,
parents: { },
children: { }
};
......
......@@ -91,6 +91,65 @@ describe('DBQuery', function() {
});
describe('where()', function() {
it('Adds a filter to the query', async () => {
const accA = new Account();
accA.name = 'Account A';
await accA.insert();
const accB = new Account();
accB.name = 'Account B';
await accB.insert();
const query = db.query(Account);
query.where(`
${query.ref('name')} = $1
`, [ accB.name ]);
const res = await query.getAll();
assert(res.length == 1);
assert(res[0].id == accB.id);
assert(res[0].name == accB.name);
});
it('Allows filtering on parent fields', async () => {
const accA = new Account();
accA.name = 'Account A';
await accA.insert();
const accB = new Account();
accB.name = 'Account B';
await accB.insert();
const contactA = new Contact();
contactA.account = accA;
contactA.firstName = 'Tommy';
contactA.lastName = 'Toaster';
await contactA.insert();
const contactB = new Contact();
contactB.account = accB;
contactB.firstName = 'Timmy';
contactB.lastName = 'Toaster';
await contactB.insert();
const query = db.query(Contact);
query.parent(Account, 'account');
query.where(`
${query.ref('account', 'name')} = ANY ($1)
AND ${query.ref('firstName')} = $2
`, [ [accB.name], contactB.firstName ]);
const res = await query.getAll();
assert(res.length == 1);
assert(res[0].id == contactB.id);
assert(res[0].account.id == accB.id);
});
});
afterEach('Reset the database', async () => {
await db.delete();
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