Commit 8acba1df authored by William Naslund's avatar William Naslund

Added non-recursive parent lookup query ability

parent 8d846b7d
import { DBModelConstructor } from "../model";
import { DBQueryRequest } from "../query";
/** Provides a means to run SQL queries given a query request */
export interface DBQueryAdapter {
......@@ -8,19 +8,11 @@ export interface DBQueryAdapter {
}
/** 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[]; }
record: DBQueryRow;
parents: { [name: string]: DBQueryResponse; };
children: { [name: string]: DBQueryResponse[]; }
}
/** A single record from the database */
......
import { DBQueryAdapter, DBQueryRequest, DBQueryResponse } from "../adapter/query-adapter";
import { DBQueryAdapter, DBQueryResponse } from "../adapter/query-adapter";
import { PGAdapter } from "./pg-database-adapter";
import { getTableInformation } from "../internal/meta-keys";
import { getTableInformation, getFieldInformation } from "../internal/meta-keys";
import { DBQueryRequest, DBQueryParent } from "../query";
export class PGQueryAdapter implements DBQueryAdapter {
......@@ -11,21 +12,80 @@ export class PGQueryAdapter implements DBQueryAdapter {
async run(req: DBQueryRequest): Promise<DBQueryResponse[]> {
const baseInfo = getTableInformation(req.model);
let sql = `SELECT ${req.fields.join(', ')} `;
sql += `FROM ${baseInfo.name}`;
// SELECT
let sql = `SELECT ${req.fields.map(field => '__.' + field).join(', ')}`;
for(const parentField in req.parents) {
sql += ', ' + this.getParentFields(`__${parentField}`, req.parents[parentField]).join(', ');
}
sql += ' ';
// FROM
sql += `FROM ${baseInfo.name} AS __`;
for(const parentField in req.parents) {
sql += ' ' + this.getParentJoins(`__${parentField}`, parentField, '', req.parents[parentField]).join(' ');
}
sql += ' ';
const parsedList: DBQueryResponse[] = [];
const recordList: DBQueryResponse[] = [];
for(const row of await this.db.command.query(sql)) {
const parsed: DBQueryResponse = {
baseRecord: row,
const record: DBQueryResponse = {
record: row,
parents: { },
children: { }
};
parsedList.push(parsed);
for(const parentField in req.parents) {
record.parents[parentField] = this.getParentValues(row, `__${parentField}`, req.parents[parentField]);
}
recordList.push(record);
}
return recordList;
}
private getParentFields(lookupId: string, parent: DBQueryParent<any>): string[] {
const parentFields: string[] = [];
for(const field of parent.req.fields) {
parentFields.push(`${lookupId}.${field} AS ${lookupId}___${field}`);
}
return parentFields;
}
private getParentJoins(lookupId: string, lookupField: string, currentId: string, parent: DBQueryParent<any>): string[] {
const parentInfo = getTableInformation(parent.req.model);
const idInfo = getFieldInformation(parent.req.model, parentInfo.options.id);
const joins: string[] = [
`LEFT OUTER JOIN ${parentInfo.options.schema}.${parentInfo.name} `
+ `AS ${lookupId} `
+ `ON ${currentId != null && currentId.trim() != '' ? currentId + '.' : ''}${lookupField} = ${lookupId}.${idInfo.name}`
];
return joins;
}
private getParentValues(record: any, lookupId: string, parent: DBQueryParent<any>): DBQueryResponse {
const parentData: DBQueryResponse = {
record: { },
parents: { },
children: { }
};
const parentPattern = new RegExp('^' + lookupId.replace(/\_/g, '\\_') + '\\_\\_\\_(.+?)$');
for(const recordField of Object.keys(record)) {
const match = parentPattern.exec(recordField);
if(match == null) {
continue;
}
parentData.record[match[1]] = record[recordField];
delete record[recordField];
}
return parsedList;
return parentData;
}
}
import "reflect-metadata";
import { DBModel, DBModelConstructor } from './model';
import { Database } from './database';
import { DBQueryRequest } from './adapter';
import { getTableFields, TABLE_PARENT_LOOKUPS } from './internal/meta-keys';
import { DBLookupInformation } from './decorators';
import { getTableFields, TABLE_PARENT_LOOKUPS, getFieldInformation, LOOKUP_INFO } from './internal/meta-keys';
import { DBLookupInformation, DBTableInformation } from './decorators';
import { DBQueryResponse } from "./adapter";
export class DBQuery<T extends DBModel> {
......@@ -23,13 +23,13 @@ export class DBQuery<T extends DBModel> {
}
/** Returns the first row from the query */
async getOne(): Promise<T> {
public async getOne(): Promise<T> {
const res = await this.getAll();
return res[0];
}
/** Returns all the rows from the query */
async getAll(): Promise<T[]> {
public async getAll(): Promise<T[]> {
if(this.req.fields == null) {
this.req.fields = [ '*' ];
}
......@@ -48,12 +48,16 @@ export class DBQuery<T extends DBModel> {
continue;
}
const fieldValue = res.baseRecord[field.name];
const fieldValue = res.record[field.name];
if(fieldValue !== undefined) {
queried[field.propertyKey] = fieldValue;
}
}
for(const lookup of Reflect.getMetadata(TABLE_PARENT_LOOKUPS, this.req.model) as DBLookupInformation[] || []) {
this.setParent(lookup, queried, res);
}
parsedModels.push(queried);
}
......@@ -62,9 +66,87 @@ export class DBQuery<T extends DBModel> {
}
/** Adds fields on the base model to be selected */
select(...fields: (keyof T)[]): this {
public select(...fields: (keyof T)[]): this {
this.req.fields = fields;
return this;
}
/** Adds a related record to be queried */
public parent<P extends DBModel>(model: DBModelConstructor<P>, lookupProperty: keyof T, ...fields: (keyof P)[]): DBQueryParent<P> {
const parentLookup = new DBQueryParent(model, lookupProperty, fields);
const lookupInfo = Reflect.getMetadata(LOOKUP_INFO, this.req.model, lookupProperty as any);
this.req.parents[lookupInfo.name] = parentLookup;
return parentLookup;
}
/** Sets the fields on a model given its data */
private setFieldData(model: DBModelConstructor<any>, record: any, res: DBQueryResponse) {
const lookupFields = new Set<string>();
for(const lookup of Reflect.getMetadata(TABLE_PARENT_LOOKUPS, model) as DBLookupInformation[] || []) {
lookupFields.add(lookup.name);
}
for(const field of getTableFields(model)) {
if(lookupFields.has(field.name)) {
continue;
}
const fieldValue = res.record[field.name];
if(fieldValue !== undefined) {
record[field.propertyKey] = fieldValue;
}
}
}
/** Sets the parent to the given value */
private setParent(lookup: DBLookupInformation, model: any, res: DBQueryResponse) {
const lookupData = res.parents[lookup.name];
if(lookupData != null) {
const parentModel = new lookup.model();
this.setFieldData(lookup.model, parentModel, lookupData);
model[lookup.propertyKey] = parentModel;
}
}
}
/** Identifies a table, and any related parent and child tables, that should be queried */
export interface DBQueryRequest<T extends DBModel = any> {
model: DBModelConstructor<T>;
fields: any[];
parents: { [name: string]: DBQueryParent<any>; }
children: { [name: string]: DBQueryRequest; }
}
/** Contains a parent relationship that is part of a query */
export class DBQueryParent<T extends DBModel> {
public readonly req: DBQueryRequest<T>;
public readonly lookupProperty: any;
constructor(model: DBModelConstructor<T>, lookupField: any, fields?: (keyof T)[]) {
this.lookupProperty = lookupField;
this.req = {
model: model,
fields: fields != null && fields.length > 0 ? fields : [ '*' ],
parents: { },
children: { }
};
}
/** Adds a related parent record to this parent record */
public parent<P extends DBModel>(model: DBModelConstructor<P>, lookupProperty: keyof T, ...fields: (keyof P)[]): DBQueryParent<P> {
const parentLookup = new DBQueryParent(model, lookupProperty, fields);
const lookupInfo = getFieldInformation(model, lookupProperty as any);
this.req.parents[lookupInfo.name] = parentLookup;
return parentLookup;
}
}
......@@ -46,6 +46,35 @@ describe('DBQuery', function() {
});
describe('selectParent()', function() {
let account: Account;
let contact1: Contact;
beforeEach('Setup test record', async () => {
account = new Account();
account.name = 'Test Account';
await account.insert();
contact1 = new Contact();
contact1.firstName = 'Tommy';
contact1.lastName = 'Toaster (1)';
contact1.account = account;
await contact1.insert();
});
it('Loads the related parent record', async () => {
const query = db.query(Contact);
query.select('id');
query.parent(Account, 'account', 'name');
const queried = await query.getOne();
assert(queried.account != null);
assert.equal(queried.account.name, 'Test Account');
});
});
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