Setup eslint and enforce rules
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-06-02 22:36:25 -05:00
parent 82e7a1f299
commit 1d5056b753
149 changed files with 6104 additions and 3114 deletions

View File

@@ -1,8 +1,8 @@
import {Connection} from "./connection/Connection";
import {Inject, Singleton} from "../di";
import {ErrorWithContext, uuid_v4} from "../util";
import {AppClass} from "../lifecycle/AppClass";
import {Logging} from "../service/Logging";
import {Connection} from './connection/Connection'
import {Inject, Singleton} from '../di'
import {ErrorWithContext, uuid4} from '../util'
import {AppClass} from '../lifecycle/AppClass'
import {Logging} from '../service/Logging'
/**
* A singleton, non-unit service that stores and retrieves database connections by name.
@@ -20,20 +20,21 @@ export class DatabaseService extends AppClass {
* @param name
* @param connection
*/
register(name: string, connection: Connection) {
register(name: string, connection: Connection): this {
if ( this.connections[name] ) {
this.logging.warn(`Overriding duplicate connection: ${name}`)
}
this.connections[name] = connection
return this
}
/**
* Returns true if a connection is registered with the given name.
* @param name
*/
has(name: string) {
return !!this.connections[name]
has(name: string): boolean {
return Boolean(this.connections[name])
}
/**
@@ -57,10 +58,10 @@ export class DatabaseService extends AppClass {
/** Get a guaranteed-unique connection name. */
uniqueName(): string {
let name: string;
let name: string
do {
name = uuid_v4()
name = uuid4()
} while (this.has(name))
return name

View File

@@ -1,19 +1,19 @@
import {Inject} from "../../di";
import {DatabaseService} from "../DatabaseService";
import {Inject} from '../../di'
import {DatabaseService} from '../DatabaseService'
import {
Constraint, ConstraintConnectionOperator,
ConstraintOperator,
OrderDirection,
OrderStatement, QueryResult,
QuerySource,
SpecifiedField
} from "../types";
import {Connection} from "../connection/Connection";
import {deepCopy, ErrorWithContext} from "../../util";
import {EscapeValue, QuerySafeValue, raw} from "../dialect/SQLDialect";
import {ResultCollection} from "./result/ResultCollection";
import {AbstractResultIterable} from "./result/AbstractResultIterable";
import {AppClass} from "../../lifecycle/AppClass";
SpecifiedField,
} from '../types'
import {Connection} from '../connection/Connection'
import {deepCopy, ErrorWithContext} from '../../util'
import {EscapeValue, QuerySafeValue, raw} from '../dialect/SQLDialect'
import {ResultCollection} from './result/ResultCollection'
import {AbstractResultIterable} from './result/AbstractResultIterable'
import {AppClass} from '../../lifecycle/AppClass'
/**
* Type alias for a function that applies some constraints to a builder group.
@@ -35,25 +35,25 @@ export abstract class AbstractBuilder<T> extends AppClass {
protected source?: QuerySource
/** The fields to query from the table. */
protected _fields: SpecifiedField[] = []
protected registeredFields: SpecifiedField[] = []
/** The number of records to skip before the result set. */
protected _skip?: number
protected registeredSkip?: number
/** The max number of records to include in the result set. */
protected _take?: number
protected registeredTake?: number
/** If true, the query should refer to distinct records. */
protected _distinct: boolean = false
protected registeredDistinct = false
/** Array of SQL group-by clauses. */
protected _groupings: string[] = []
protected registeredGroupings: string[] = []
/** Array of SQL order-by clauses. */
protected _orders: OrderStatement[] = []
protected registeredOrders: OrderStatement[] = []
/** The connection on which the query should be executed. */
protected _connection?: Connection
protected registeredConnection?: Connection
/**
* Create a new, empty, instance of the current builder.
@@ -73,50 +73,53 @@ export abstract class AbstractBuilder<T> extends AppClass {
bldr.constraints = deepCopy(this.constraints)
bldr.source = deepCopy(this.source)
bldr._fields = deepCopy(this._fields)
bldr._skip = deepCopy(this._skip)
bldr._take = deepCopy(this._take)
bldr._distinct = deepCopy(this._distinct)
bldr._groupings = deepCopy(this._groupings)
bldr._orders = deepCopy(this._orders)
bldr._connection = this._connection
bldr.registeredFields = deepCopy(this.registeredFields)
bldr.registeredSkip = deepCopy(this.registeredSkip)
bldr.registeredTake = deepCopy(this.registeredTake)
bldr.registeredDistinct = deepCopy(this.registeredDistinct)
bldr.registeredGroupings = deepCopy(this.registeredGroupings)
bldr.registeredOrders = deepCopy(this.registeredOrders)
bldr.registeredConnection = this.registeredConnection
return bldr
}
/** Get the constraints applied to this query. */
public get appliedConstraints() {
public get appliedConstraints(): Constraint[] {
return deepCopy(this.constraints)
}
/** Get the fields that should be included in this query. */
public get appliedFields() {
return deepCopy(this._fields)
public get appliedFields(): SpecifiedField[] {
return deepCopy(this.registeredFields)
}
/** Get the skip/take values of this query. */
public get appliedPagination() {
return { skip: this._skip, take: this._take }
public get appliedPagination(): { skip: number | undefined, take: number | undefined} {
return { skip: this.registeredSkip,
take: this.registeredTake }
}
/** True if the query should be DISTINCT */
public get appliedDistinction() {
return this._distinct
public get appliedDistinction(): boolean {
return this.registeredDistinct
}
/** Get the SQL group-by clauses applied to this query. */
public get appliedGroupings() {
return deepCopy(this._groupings)
public get appliedGroupings(): string[] {
return deepCopy(this.registeredGroupings)
}
/** Get the SQL order-by clauses applied to this query. */
public get appliedOrder() {
return deepCopy(this._orders)
public get appliedOrder(): OrderStatement[] {
return deepCopy(this.registeredOrders)
}
/** Get the source table for this query. */
public get querySource() {
if ( this.source ) return deepCopy(this.source)
public get querySource(): QuerySource | undefined {
if ( this.source ) {
return deepCopy(this.source)
}
}
/**
@@ -124,9 +127,10 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param table
* @param alias
*/
from(table: string, alias?: string) {
from(table: string, alias?: string): this {
if ( alias ) {
this.source = { table, alias }
this.source = { table,
alias }
} else {
this.source = table
}
@@ -138,7 +142,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param table
* @param alias
*/
table(table: string, alias?: string) {
table(table: string, alias?: string): this {
return this.from(table, alias)
}
@@ -147,11 +151,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param field
* @param alias
*/
field(field: string | QuerySafeValue, alias?: string) {
field(field: string | QuerySafeValue, alias?: string): this {
if ( alias ) {
this._fields.push({ field, alias })
this.registeredFields.push({ field,
alias })
} else {
this._fields.push(field)
this.registeredFields.push(field)
}
return this
}
@@ -161,7 +166,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param fields
*/
fields(...fields: SpecifiedField[]): this {
this._fields = [...this._fields, ...fields]
this.registeredFields = [...this.registeredFields, ...fields]
return this
}
@@ -184,8 +189,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
/**
* Remove all selected fields from this query.
*/
clearFields() {
this._fields = []
clearFields(): this {
this.registeredFields = []
return this
}
@@ -195,7 +200,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operator
* @param operand
*/
where(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
where(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
this.createConstraint('AND', field, operator, operand)
return this
}
@@ -206,7 +211,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operator
* @param operand
*/
whereRaw(field: string, operator: ConstraintOperator, operand: string) {
whereRaw(field: string, operator: ConstraintOperator, operand: string): this {
this.createConstraint('AND', field, operator, raw(operand))
return this
}
@@ -217,7 +222,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operator
* @param operand
*/
whereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
whereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
this.createConstraint('AND NOT', field, operator, operand)
return this
}
@@ -228,7 +233,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operator
* @param operand
*/
orWhere(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
orWhere(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
this.createConstraint('OR', field, operator, operand)
return this
}
@@ -239,7 +244,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operator
* @param operand
*/
orWhereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue) {
orWhereNot(field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: EscapeValue): this {
this.createConstraint('OR NOT', field, operator, operand)
return this
}
@@ -250,7 +255,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operator
* @param operand
*/
orWhereRaw(field: string, operator: ConstraintOperator, operand: string) {
orWhereRaw(field: string, operator: ConstraintOperator, operand: string): this {
this.createConstraint('OR', field, operator, raw(operand))
return this
}
@@ -260,7 +265,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param field
* @param values
*/
whereIn(field: string, values: EscapeValue) {
whereIn(field: string, values: EscapeValue): this {
this.constraints.push({
field,
operator: 'IN',
@@ -275,7 +280,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param field
* @param values
*/
whereNotIn(field: string, values: EscapeValue) {
whereNotIn(field: string, values: EscapeValue): this {
this.constraints.push({
field,
operator: 'NOT IN',
@@ -290,12 +295,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param field
* @param values
*/
orWhereIn(field: string, values: EscapeValue) {
orWhereIn(field: string, values: EscapeValue): this {
this.constraints.push({
field,
operator: 'IN',
operand: values,
preop: 'OR'
preop: 'OR',
})
return this
}
@@ -305,12 +310,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param field
* @param values
*/
orWhereNotIn(field: string, values: EscapeValue) {
orWhereNotIn(field: string, values: EscapeValue): this {
this.constraints.push({
field,
operator: 'NOT IN',
operand: values,
preop: 'OR'
preop: 'OR',
})
return this
}
@@ -319,8 +324,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Limit the query to a maximum number of rows.
* @param rows
*/
limit(rows: number) {
this._take = rows
limit(rows: number): this {
this.registeredTake = rows
return this
}
@@ -328,7 +333,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Alias of `limit()`.
* @param rows
*/
take(rows: number) {
take(rows: number): this {
return this.limit(rows)
}
@@ -336,8 +341,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Skip the first `rows` many rows in the result set.
* @param rows
*/
skip(rows: number) {
this._skip = rows
skip(rows: number): this {
this.registeredSkip = rows
return this
}
@@ -345,23 +350,23 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Alias of `skip()`.
* @param rows
*/
offset(rows: number) {
offset(rows: number): this {
return this.skip(rows)
}
/**
* Make the query return only distinct rows.
*/
distinct() {
this._distinct = true
distinct(): this {
this.registeredDistinct = true
return this
}
/**
* Allow the query to return non-distinct rows. (Undoes `distinct()`.)
*/
notDistinct() {
this._distinct = false
notDistinct(): this {
this.registeredDistinct = false
return this
}
@@ -371,7 +376,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param pageNum
* @param pageSize
*/
page(pageNum: number = 1, pageSize: number = 20) {
page(pageNum = 1, pageSize = 20): this {
this.skip(pageSize * (pageNum - 1))
this.take(pageSize)
return this
@@ -381,8 +386,8 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Apply one or more GROUP-BY clauses to the query.
* @param groupings
*/
groupBy(...groupings: string[]) {
this._groupings = groupings
groupBy(...groupings: string[]): this {
this.registeredGroupings = groupings
return this
}
@@ -391,8 +396,9 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param field
* @param direction
*/
orderBy(field: string, direction: OrderDirection = 'ASC') {
this._orders.push({ field, direction })
orderBy(field: string, direction: OrderDirection = 'ASC'): this {
this.registeredOrders.push({ field,
direction })
return this
}
@@ -400,7 +406,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Order the query by the given field, ascending.
* @param field
*/
orderByAscending(field: string) {
orderByAscending(field: string): this {
return this.orderBy(field, 'ASC')
}
@@ -408,7 +414,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Order the query by the given field, descending.
* @param field
*/
orderByDescending(field: string) {
orderByDescending(field: string): this {
return this.orderBy(field, 'DESC')
}
@@ -416,11 +422,11 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Specify the connection name or instance to execute the query on.
* @param nameOrInstance
*/
connection(nameOrInstance: string | Connection) {
connection(nameOrInstance: string | Connection): this {
if ( nameOrInstance instanceof Connection ) {
this._connection = nameOrInstance
this.registeredConnection = nameOrInstance
} else {
this._connection = this.databaseService.get(nameOrInstance)
this.registeredConnection = this.databaseService.get(nameOrInstance)
}
return this
@@ -430,11 +436,11 @@ export abstract class AbstractBuilder<T> extends AppClass {
* Get a result iterable for the rows of this query.
*/
iterator(): AbstractResultIterable<T> {
if ( !this._connection ) {
if ( !this.registeredConnection ) {
throw new ErrorWithContext(`No connection specified to fetch iterator for query.`)
}
return this.getResultIterable();
return this.getResultIterable()
}
/**
@@ -469,12 +475,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param data
*/
async update(data: {[key: string]: EscapeValue}): Promise<QueryResult> {
if ( !this._connection ) {
if ( !this.registeredConnection ) {
throw new ErrorWithContext(`No connection specified to execute update query.`)
}
const query = this._connection.dialect().renderUpdate(this, data)
return this._connection.query(query)
const query = this.registeredConnection.dialect().renderUpdate(this, data)
return this.registeredConnection.query(query)
}
/**
@@ -495,12 +501,12 @@ export abstract class AbstractBuilder<T> extends AppClass {
*
*/
async delete(): Promise<QueryResult> {
if ( !this._connection ) {
if ( !this.registeredConnection ) {
throw new ErrorWithContext(`No connection specified to execute update query.`)
}
const query = this._connection.dialect().renderDelete(this)
return this._connection.query(query)
const query = this.registeredConnection.dialect().renderDelete(this)
return this.registeredConnection.query(query)
}
/**
@@ -527,26 +533,26 @@ export abstract class AbstractBuilder<T> extends AppClass {
*
* @param rowOrRows
*/
async insert(rowOrRows: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[]) {
if ( !this._connection ) {
async insert(rowOrRows: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[]): Promise<QueryResult> {
if ( !this.registeredConnection ) {
throw new ErrorWithContext(`No connection specified to execute update query.`)
}
const query = this._connection.dialect().renderInsert(this, rowOrRows)
return this._connection.query(query)
const query = this.registeredConnection.dialect().renderInsert(this, rowOrRows)
return this.registeredConnection.query(query)
}
/**
* Returns true if at least one row matches the current query.
*/
async exists() {
if ( !this._connection ) {
async exists(): Promise<boolean> {
if ( !this.registeredConnection ) {
throw new ErrorWithContext(`No connection specified to execute update query.`)
}
const query = this._connection.dialect().renderExistential(this)
const result = await this._connection.query(query)
return !!result.rows.first()
const query = this.registeredConnection.dialect().renderExistential(this)
const result = await this.registeredConnection.query(query)
return Boolean(result.rows.first())
}
/**
@@ -557,17 +563,20 @@ export abstract class AbstractBuilder<T> extends AppClass {
* @param operand
* @private
*/
private createConstraint(preop: ConstraintConnectionOperator, field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: any) {
private createConstraint(preop: ConstraintConnectionOperator, field: string | ConstraintGroupClosure<T>, operator?: ConstraintOperator, operand?: any): void {
if ( typeof field === 'function' ) {
const builder = this.getNewInstance()
field(builder)
this.constraints.push({
preop,
items: builder.appliedConstraints
items: builder.appliedConstraints,
})
} else if ( field && operator && typeof operand !== 'undefined' ) {
this.constraints.push({
field, operator, operand, preop, // FIXME escape operand
field,
operator,
operand,
preop, // FIXME escape operand
})
}
}

View File

@@ -1,23 +1,23 @@
import {ErrorWithContext} from "../../util";
import {Container} from "../../di";
import {ResultIterable} from "./result/ResultIterable";
import {QueryRow} from "../types";
import {AbstractBuilder} from "./AbstractBuilder";
import {AbstractResultIterable} from "./result/AbstractResultIterable";
import {ErrorWithContext} from '../../util'
import {Container} from '../../di'
import {ResultIterable} from './result/ResultIterable'
import {QueryRow} from '../types'
import {AbstractBuilder} from './AbstractBuilder'
import {AbstractResultIterable} from './result/AbstractResultIterable'
/**
* Implementation of the abstract builder class that returns simple QueryRow objects.
*/
export class Builder extends AbstractBuilder<QueryRow> {
public getNewInstance(): AbstractBuilder<QueryRow> {
return Container.getContainer().make<Builder>(Builder);
return Container.getContainer().make<Builder>(Builder)
}
public getResultIterable(): AbstractResultIterable<QueryRow> {
if ( !this._connection ) {
if ( !this.registeredConnection ) {
throw new ErrorWithContext(`No connection specified to fetch iterator for query.`)
}
return Container.getContainer().make<ResultIterable>(ResultIterable, this, this._connection)
return Container.getContainer().make<ResultIterable>(ResultIterable, this, this.registeredConnection)
}
}

View File

@@ -1,6 +1,6 @@
import {Collection, Iterable} from "../../../util"
import {Connection} from "../../connection/Connection";
import {AbstractBuilder} from "../AbstractBuilder";
import {Collection, Iterable} from '../../../util'
import {Connection} from '../../connection/Connection'
import {AbstractBuilder} from '../AbstractBuilder'
/**
* Base Iterable class that generates the results of a Builder query.
@@ -12,7 +12,9 @@ export abstract class AbstractResultIterable<T> extends Iterable<T> {
/** The connection on which to execute the builder. */
public readonly connection: Connection,
) { super() }
) {
super()
}
/**
* Get the SQL string for the SELECT query for this iterable.

View File

@@ -1,5 +1,5 @@
import {AsyncCollection} from "../../../util";
import {AbstractResultIterable} from "./AbstractResultIterable";
import {AsyncCollection} from '../../../util'
import {AbstractResultIterable} from './AbstractResultIterable'
/**
* Async collection class that iterates AbstractResultIterables in chunks.
@@ -10,7 +10,7 @@ export class ResultCollection<T> extends AsyncCollection<T> {
iterator: AbstractResultIterable<T>,
/** The max number of records to request per-query, by default. */
chunkSize: number = 500
chunkSize = 500,
) {
super(iterator, chunkSize)
}

View File

@@ -1,8 +1,8 @@
import {QueryRow} from "../../types";
import {Builder} from "../Builder";
import {Connection} from "../../connection/Connection";
import {AbstractResultIterable} from "./AbstractResultIterable";
import {Collection} from "../../../util";
import {QueryRow} from '../../types'
import {Builder} from '../Builder'
import {Connection} from '../../connection/Connection'
import {AbstractResultIterable} from './AbstractResultIterable'
import {Collection} from '../../../util'
/**
* Implementation of AbstractResultIterable that yields simple QueryRow instances (objects).
@@ -11,9 +11,11 @@ export class ResultIterable extends AbstractResultIterable<QueryRow> {
constructor(
public readonly builder: Builder,
public readonly connection: Connection,
) { super(builder, connection) }
) {
super(builder, connection)
}
public get selectSQL() {
public get selectSQL(): string {
return this.connection.dialect().renderSelect(this.builder)
}
@@ -27,7 +29,7 @@ export class ResultIterable extends AbstractResultIterable<QueryRow> {
return (await this.connection.query(query)).rows
}
async count() {
async count(): Promise<number> {
const query = this.connection.dialect().renderCount(this.selectSQL)
const result = (await this.connection.query(query)).rows.first()
return result?.extollo_render_count ?? 0
@@ -38,7 +40,7 @@ export class ResultIterable extends AbstractResultIterable<QueryRow> {
return result.rows
}
clone() {
clone(): ResultIterable {
return new ResultIterable(this.builder, this.connection)
}
}

View File

@@ -1,7 +1,7 @@
import {Collection, ErrorWithContext} from "../../util";
import {QueryResult} from "../types";
import {SQLDialect} from "../dialect/SQLDialect";
import {AppClass} from "../../lifecycle/AppClass";
import {ErrorWithContext} from '../../util'
import {QueryResult} from '../types'
import {SQLDialect} from '../dialect/SQLDialect'
import {AppClass} from '../../lifecycle/AppClass'
/**
* Error thrown when a connection is used before it is ready.
@@ -30,7 +30,9 @@ export abstract class Connection extends AppClass {
* This connection's config object
*/
public readonly config: any = {},
) { super() }
) {
super()
}
public abstract dialect(): SQLDialect

View File

@@ -1,11 +1,11 @@
import {Connection, ConnectionNotReadyError} from "./Connection";
import {Client} from "pg";
import {Inject} from "../../di";
import {QueryResult} from "../types";
import {collect} from "../../util";
import {SQLDialect} from "../dialect/SQLDialect";
import {PostgreSQLDialect} from "../dialect/PostgreSQLDialect";
import {Logging} from "../../service/Logging";
import {Connection, ConnectionNotReadyError} from './Connection'
import {Client} from 'pg'
import {Inject} from '../../di'
import {QueryResult} from '../types'
import {collect} from '../../util'
import {SQLDialect} from '../dialect/SQLDialect'
import {PostgreSQLDialect} from '../dialect/PostgreSQLDialect'
import {Logging} from '../../service/Logging'
/**
* Type interface representing the config for a PostgreSQL connection.
@@ -32,13 +32,13 @@ export class PostgresConnection extends Connection {
return <PostgreSQLDialect> this.app().make(PostgreSQLDialect)
}
public async init() {
public async init(): Promise<void> {
this.logging.debug(`Initializing PostgreSQL connection ${this.name}...`)
this.client = new Client(this.config)
await this.client.connect()
}
public async close() {
public async close(): Promise<void> {
this.logging.debug(`Closing PostgreSQL connection ${this.name}...`)
if ( this.client ) {
await this.client.end()
@@ -46,8 +46,11 @@ export class PostgresConnection extends Connection {
}
public async query(query: string): Promise<QueryResult> {
if ( !this.client ) throw new ConnectionNotReadyError(this.name, { config: JSON.stringify(this.config) })
this.logging.verbose(`Executing query in connection ${this.name}: \n${query.split('\n').map(x => ' ' + x).join('\n')}`)
if ( !this.client ) {
throw new ConnectionNotReadyError(this.name, { config: JSON.stringify(this.config) })
}
this.logging.verbose(`Executing query in connection ${this.name}: \n${query.split('\n').map(x => ' ' + x)
.join('\n')}`)
try {
const result = await this.client.query(query)

View File

@@ -1,6 +1,6 @@
import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect';
import {Constraint, isConstraintGroup, isConstraintItem, SpecifiedField} from "../types";
import {AbstractBuilder} from "../builder/AbstractBuilder";
import {EscapeValue, QuerySafeValue, raw, SQLDialect} from './SQLDialect'
import {Constraint, isConstraintGroup, isConstraintItem, SpecifiedField} from '../types'
import {AbstractBuilder} from '../builder/AbstractBuilder'
/**
* An implementation of the SQLDialect specific to PostgreSQL.
@@ -8,8 +8,9 @@ import {AbstractBuilder} from "../builder/AbstractBuilder";
export class PostgreSQLDialect extends SQLDialect {
public escape(value: EscapeValue): QuerySafeValue {
if ( value instanceof QuerySafeValue ) return value
else if ( Array.isArray(value) ) {
if ( value instanceof QuerySafeValue ) {
return value
} else if ( Array.isArray(value) ) {
return new QuerySafeValue(value, `(${value.map(v => this.escape(v)).join(',')})`)
} else if ( String(value).toLowerCase() === 'true' || value === true ) {
return new QuerySafeValue(value, 'TRUE')
@@ -34,7 +35,7 @@ export class PostgreSQLDialect extends SQLDialect {
} else if ( value === null || typeof value === 'undefined' ) {
return new QuerySafeValue(value, 'NULL')
} else {
const escaped = value.replace(/'/g, '\\\'') //.replace(/"/g, '\\"').replace(/`/g, '\\`')
const escaped = value.replace(/'/g, '\\\'') // .replace(/"/g, '\\"').replace(/`/g, '\\`')
return new QuerySafeValue(value, `'${escaped}'`)
}
}
@@ -44,7 +45,7 @@ export class PostgreSQLDialect extends SQLDialect {
'SELECT COUNT(*) AS "extollo_render_count"',
'FROM (',
...query.split('\n').map(x => ` ${x}`),
') AS extollo_target_query'
') AS extollo_target_query',
].join('\n')
}
@@ -54,35 +55,46 @@ export class PostgreSQLDialect extends SQLDialect {
'FROM (',
...query.split('\n').map(x => ` ${x}`),
') AS extollo_target_query',
`OFFSET ${start} LIMIT ${(end - start) + 1}`
`OFFSET ${start} LIMIT ${(end - start) + 1}`,
].join('\n')
}
/** Render the fields from the builder class to PostgreSQL syntax. */
protected renderFields(builder: AbstractBuilder<any>) {
protected renderFields(builder: AbstractBuilder<any>): string[] {
return builder.appliedFields.map((field: SpecifiedField) => {
let columnString: string
if ( typeof field === 'string' ) columnString = field.split('.').map(x => `"${x}"`).join('.')
else if ( field instanceof QuerySafeValue ) columnString = field.toString()
else if ( typeof field.field === 'string' ) columnString = field.field.split('.').map(x => `"${x}"`).join('.')
else columnString = field.field.toString()
if ( typeof field === 'string' ) {
columnString = field.split('.').map(x => `"${x}"`)
.join('.')
} else if ( field instanceof QuerySafeValue ) {
columnString = field.toString()
} else if ( typeof field.field === 'string' ) {
columnString = field.field.split('.').map(x => `"${x}"`)
.join('.')
} else {
columnString = field.field.toString()
}
let aliasString = ''
if ( typeof field !== 'string' && !(field instanceof QuerySafeValue) ) aliasString = ` AS "${field.alias}"`
if ( typeof field !== 'string' && !(field instanceof QuerySafeValue) ) {
aliasString = ` AS "${field.alias}"`
}
return `${columnString}${aliasString}`
})
}
public renderSelect(builder: AbstractBuilder<any>): string {
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
const indent = (item: string, level = 1) => Array(level + 1).fill('')
.join(' ') + item
const queryLines = [
`SELECT${builder.appliedDistinction ? ' DISTINCT' : ''}`
`SELECT${builder.appliedDistinction ? ' DISTINCT' : ''}`,
]
// Add fields
// FIXME error if no fields
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
const fields = this.renderFields(builder).map(x => indent(x))
.join(',\n')
queryLines.push(fields)
@@ -91,7 +103,8 @@ export class PostgreSQLDialect extends SQLDialect {
const source = builder.querySource
if ( source ) {
const tableString = typeof source === 'string' ? source : source.table
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
const table: string = tableString.split('.').map(x => `"${x}"`)
.join('.')
queryLines.push('FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
}
@@ -105,7 +118,8 @@ export class PostgreSQLDialect extends SQLDialect {
// Add group by
if ( builder.appliedGroupings?.length ) {
const grouping = builder.appliedGroupings.map(group => {
return indent(group.split('.').map(x => `"${x}"`).join('.'))
return indent(group.split('.').map(x => `"${x}"`)
.join('.'))
}).join(',\n')
queryLines.push('GROUP BY')
@@ -114,7 +128,8 @@ export class PostgreSQLDialect extends SQLDialect {
// Add order by
if ( builder.appliedOrder?.length ) {
const ordering = builder.appliedOrder.map(x => indent(`${x.field.split('.').map(x => '"' + x + '"').join('.')} ${x.direction}`)).join(',\n')
const ordering = builder.appliedOrder.map(x => indent(`${x.field.split('.').map(y => '"' + y + '"')
.join('.')} ${x.direction}`)).join(',\n')
queryLines.push('ORDER BY')
queryLines.push(ordering)
}
@@ -132,14 +147,14 @@ export class PostgreSQLDialect extends SQLDialect {
// TODO support FROM, RETURNING
public renderUpdate(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}): string {
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
const queryLines: string[] = []
// Add table source
const source = builder.querySource
if ( source ) {
const tableString = typeof source === 'string' ? source : source.table
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
const table: string = tableString.split('.').map(x => `"${x}"`)
.join('.')
queryLines.push('UPDATE ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
}
@@ -166,17 +181,21 @@ export class PostgreSQLDialect extends SQLDialect {
// FIXME: subquery support here and with select
public renderInsert(builder: AbstractBuilder<any>, data: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[] = []): string {
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
const indent = (item: string, level = 1) => Array(level + 1).fill('')
.join(' ') + item
const queryLines: string[] = []
if ( !Array.isArray(data) ) data = [data]
if ( !Array.isArray(data) ) {
data = [data]
}
const columns = Object.keys(data[0])
// Add table source
const source = builder.querySource
if ( source ) {
const tableString = typeof source === 'string' ? source : source.table
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
const table: string = tableString.split('.').map(x => `"${x}"`)
.join('.')
queryLines.push('INSERT INTO ' + (typeof source === 'string' ? table : `${table} AS "${source.alias}"`)
+ (columns.length ? ` (${columns.join(', ')})` : ''))
}
@@ -187,9 +206,9 @@ export class PostgreSQLDialect extends SQLDialect {
queryLines.push('VALUES')
const valueString = data.map(row => {
const values = columns.map(x => this.escape(row[x]))
return indent(`(${values.join(', ')})`)
})
const values = columns.map(x => this.escape(row[x]))
return indent(`(${values.join(', ')})`)
})
.join(',\n')
queryLines.push(valueString)
@@ -198,7 +217,8 @@ export class PostgreSQLDialect extends SQLDialect {
// Add return fields
if ( builder.appliedFields?.length ) {
queryLines.push('RETURNING')
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
const fields = this.renderFields(builder).map(x => indent(x))
.join(',\n')
queryLines.push(fields)
}
@@ -207,14 +227,16 @@ export class PostgreSQLDialect extends SQLDialect {
}
public renderDelete(builder: AbstractBuilder<any>): string {
const indent = (item: string, level = 1) => Array(level + 1).fill('').join(' ') + item
const indent = (item: string, level = 1) => Array(level + 1).fill('')
.join(' ') + item
const queryLines: string[] = []
// Add table source
const source = builder.querySource
if ( source ) {
const tableString = typeof source === 'string' ? source : source.table
const table: string = tableString.split('.').map(x => `"${x}"`).join('.')
const table: string = tableString.split('.').map(x => `"${x}"`)
.join('.')
queryLines.push('DELETE FROM ' + (typeof source === 'string' ? table : `${table} "${source.alias}"`))
}
@@ -229,7 +251,8 @@ export class PostgreSQLDialect extends SQLDialect {
if ( builder.appliedFields?.length ) {
queryLines.push('RETURNING')
const fields = this.renderFields(builder).map(x => indent(x)).join(',\n')
const fields = this.renderFields(builder).map(x => indent(x))
.join(',\n')
queryLines.push(fields)
}
@@ -237,16 +260,18 @@ export class PostgreSQLDialect extends SQLDialect {
return queryLines.join('\n')
}
public renderConstraints(constraints: Constraint[]): string {
public renderConstraints(allConstraints: Constraint[]): string {
const constraintsToSql = (constraints: Constraint[], level = 1): string => {
const indent = Array(level * 2).fill(' ').join('')
let statements = []
const indent = Array(level * 2).fill(' ')
.join('')
const statements = []
for ( const constraint of constraints ) {
if ( isConstraintGroup(constraint) ) {
statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}(\n${constraintsToSql(constraint.items, level + 1)}\n${indent})`)
} else if ( isConstraintItem(constraint) ) {
const field: string = constraint.field.split('.').map(x => `"${x}"`).join('.')
const field: string = constraint.field.split('.').map(x => `"${x}"`)
.join('.')
statements.push(`${indent}${statements.length < 1 ? '' : constraint.preop + ' '}${field} ${constraint.operator} ${this.escape(constraint.operand).value}`)
}
}
@@ -254,13 +279,15 @@ export class PostgreSQLDialect extends SQLDialect {
return statements.filter(Boolean).join('\n')
}
return constraintsToSql(constraints)
return constraintsToSql(allConstraints)
}
public renderUpdateSet(data: {[key: string]: EscapeValue}) {
public renderUpdateSet(data: {[key: string]: EscapeValue}): string {
const sets = []
for ( const key in data ) {
if ( !data.hasOwnProperty(key) ) continue
if ( !Object.prototype.hasOwnProperty.call(data, key) ) {
continue
}
sets.push(` "${key}" = ${this.escape(data[key])}`)
}

View File

@@ -1,6 +1,6 @@
import {Constraint} from "../types";
import {AbstractBuilder} from "../builder/AbstractBuilder";
import {AppClass} from "../../lifecycle/AppClass";
import {Constraint} from '../types'
import {AbstractBuilder} from '../builder/AbstractBuilder'
import {AppClass} from '../../lifecycle/AppClass'
/**
* A value which can be escaped to be interpolated into an SQL query.
@@ -18,15 +18,15 @@ export type EscapeValueObject = { [field: string]: EscapeValue }
export class QuerySafeValue {
constructor(
/** The unescaped value. */
public readonly originalValue: any,
public readonly originalValue: unknown,
/** The query-safe sanitized value. */
public readonly value: any,
public readonly value: unknown,
) { }
/** Cast the value to a query-safe string. */
toString() {
return this.value
toString(): string {
return String(this.value)
}
}
@@ -35,7 +35,7 @@ export class QuerySafeValue {
* This is dangerous and should NEVER be used to wrap user input.
* @param value
*/
export function raw(value: any) {
export function raw(value: unknown): QuerySafeValue {
return new QuerySafeValue(value, value)
}

View File

@@ -1,5 +1,5 @@
import {Collection} from "../../util";
import {FieldType} from "../types";
import {Collection} from '../../util'
import {FieldType} from '../types'
/** The reflection metadata key containing information about the model's fields. */
export const EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY = 'extollo:orm:Field.ts'
@@ -17,8 +17,8 @@ export interface ModelField {
* Retrieve a collection of ModelField metadata from the given model.
* @param model
*/
export function getFieldsMeta(model: any): Collection<ModelField> {
const fields = Reflect.getMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, model.constructor)
export function getFieldsMeta(model: unknown): Collection<ModelField> {
const fields = Reflect.getMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, (model as any).constructor)
if ( !(fields instanceof Collection) ) {
return new Collection<ModelField>()
}
@@ -31,8 +31,8 @@ export function getFieldsMeta(model: any): Collection<ModelField> {
* @param model
* @param fields
*/
export function setFieldsMeta(model: any, fields: Collection<ModelField>) {
Reflect.defineMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, fields, model.constructor)
export function setFieldsMeta(model: unknown, fields: Collection<ModelField>): void {
Reflect.defineMetadata(EXTOLLO_ORM_MODEL_FIELDS_METADATA_KEY, fields, (model as any).constructor)
}
/**
@@ -57,7 +57,9 @@ export function setFieldsMeta(model: any, fields: Collection<ModelField>) {
*/
export function Field(type: FieldType, databaseKey?: string): PropertyDecorator {
return (target, modelKey) => {
if ( !databaseKey ) databaseKey = String(modelKey)
if ( !databaseKey ) {
databaseKey = String(modelKey)
}
const fields = getFieldsMeta(target)
const existingField = fields.firstWhere('modelKey', '=', modelKey)

View File

@@ -1,12 +1,13 @@
import {ModelKey, QueryRow, QuerySource} from "../types";
import {Container, Inject} from "../../di";
import {DatabaseService} from "../DatabaseService";
import {ModelBuilder} from "./ModelBuilder";
import {getFieldsMeta, ModelField} from "./Field";
import {deepCopy, BehaviorSubject, Pipe, Collection} from "../../util";
import {EscapeValueObject} from "../dialect/SQLDialect";
import {AppClass} from "../../lifecycle/AppClass";
import {Logging} from "../../service/Logging";
import {ModelKey, QueryRow, QuerySource} from '../types'
import {Container, Inject} from '../../di'
import {DatabaseService} from '../DatabaseService'
import {ModelBuilder} from './ModelBuilder'
import {getFieldsMeta, ModelField} from './Field'
import {deepCopy, BehaviorSubject, Pipe, Collection} from '../../util'
import {EscapeValueObject} from '../dialect/SQLDialect'
import {AppClass} from '../../lifecycle/AppClass'
import {Logging} from '../../service/Logging'
import {Connection} from '../connection/Connection'
/**
* Base for classes that are mapped to tables in a database.
@@ -19,7 +20,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* The name of the connection this model should run through.
* @type string
*/
protected static connection: string = 'default'
protected static connection = 'default'
/**
* The name of the table this model is stored in.
@@ -36,7 +37,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
/**
* If false (default), the primary key will be excluded from INSERTs.
*/
protected static populateKeyOnInsert: boolean = false
protected static populateKeyOnInsert = false
/**
* Optionally, the timestamp field set on creation.
@@ -74,7 +75,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* The original row fetched from the database.
* @protected
*/
protected _original?: QueryRow
protected originalSourceRow?: QueryRow
/**
* Behavior subject that fires after the model is populated.
@@ -124,7 +125,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
/**
* Get the table name for this model.
*/
public static tableName() {
public static tableName(): string {
return this.table
}
@@ -144,15 +145,16 @@ export abstract class Model<T extends Model<T>> extends AppClass {
/**
* Get the name of the connection where this model's table is found.
*/
public static connectionName() {
public static connectionName(): string {
return this.connection
}
/**
* Get the database connection instance for this model's connection.
*/
public static getConnection() {
return Container.getContainer().make<DatabaseService>(DatabaseService).get(this.connectionName());
public static getConnection(): Connection {
return Container.getContainer().make<DatabaseService>(DatabaseService)
.get(this.connectionName())
}
/**
@@ -164,14 +166,17 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* const user = await UserModel.query<UserModel>().where('name', 'LIKE', 'John Doe').first()
* ```
*/
public static query<T2 extends Model<T2>>() {
public static query<T2 extends Model<T2>>(): ModelBuilder<T2> {
const builder = <ModelBuilder<T2>> Container.getContainer().make<ModelBuilder<T2>>(ModelBuilder, this)
const source: QuerySource = this.querySource()
builder.connection(this.getConnection())
if ( typeof source === 'string' ) builder.from(source)
else builder.from(source.table, source.alias)
if ( typeof source === 'string' ) {
builder.from(source)
} else {
builder.from(source.table, source.alias)
}
getFieldsMeta(this.prototype).each(field => {
builder.field(field.databaseKey)
@@ -185,7 +190,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* Pre-fill the model's properties from the given values.
* Calls `boot()` under the hood.
*/
values?: {[key: string]: any}
values?: {[key: string]: any},
) {
super()
this.boot(values)
@@ -199,7 +204,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* @param values
*/
public boot(values?: any) {
public boot(values?: {[key: string]: unknown}): void {
if ( values ) {
getFieldsMeta(this).each(field => {
this.setFieldFromObject(field.modelKey, String(field.modelKey), values)
@@ -216,8 +221,8 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* @param row
*/
public async assumeFromSource(row: QueryRow) {
this._original = row
public async assumeFromSource(row: QueryRow): Promise<this> {
this.originalSourceRow = row
getFieldsMeta(this).each(field => {
this.setFieldFromObject(field.modelKey, field.databaseKey, row)
@@ -236,7 +241,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* @param object
*/
public async assume(object: { [key: string]: any }) {
public async assume(object: { [key: string]: any }): Promise<this> {
getFieldsMeta(this).each(field => {
if ( field.modelKey in object ) {
this.setFieldFromObject(field.modelKey, String(field.modelKey), object)
@@ -249,26 +254,24 @@ export abstract class Model<T extends Model<T>> extends AppClass {
/**
* Get the value of the primary key of this model, if it exists.
*/
public key() {
public key(): string {
const ctor = this.constructor as typeof Model
const field = getFieldsMeta(this)
.firstWhere('databaseKey', '=', ctor.key)
if ( field ) {
// @ts-ignore
return this[field.modelKey]
return (this as any)[field.modelKey]
}
// @ts-ignore
return this[ctor.key]
return (this as any)[ctor.key]
}
/**
* Returns true if this instance's record has been persisted into the database.
*/
public exists() {
return !!this._original && !!this.key()
public exists(): boolean {
return Boolean(this.originalSourceRow) && Boolean(this.key())
}
/**
@@ -284,11 +287,13 @@ export abstract class Model<T extends Model<T>> extends AppClass {
const timestamps: { updated?: Date, created?: Date } = {}
if ( ctor.timestamps ) {
// @ts-ignore
if ( ctor.CREATED_AT ) timestamps.created = this[ctor.CREATED_AT]
if ( ctor.CREATED_AT ) {
timestamps.created = (this as any)[ctor.CREATED_AT]
}
// @ts-ignore
if ( ctor.UPDATED_AT ) timestamps.updated = this[ctor.UPDATED_AT]
if ( ctor.UPDATED_AT ) {
timestamps.updated = (this as any)[ctor.UPDATED_AT]
}
}
return timestamps
@@ -312,8 +317,11 @@ export abstract class Model<T extends Model<T>> extends AppClass {
builder.connection(ModelClass.getConnection())
if ( typeof source === 'string' ) builder.from(source)
else builder.from(source.table, source.alias)
if ( typeof source === 'string' ) {
builder.from(source)
} else {
builder.from(source.table, source.alias)
}
getFieldsMeta(this).each(field => {
builder.field(field.databaseKey)
@@ -343,15 +351,17 @@ export abstract class Model<T extends Model<T>> extends AppClass {
/**
* Get an array of all instances of this model.
*/
public async all() {
return this.query().get().all()
public async all(): Promise<T[]> {
return this.query().get()
.all()
}
/**
* Count all instances of this model in the database.
*/
public async count(): Promise<number> {
return this.query().get().count()
return this.query().get()
.count()
}
/**
@@ -365,7 +375,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* @param column
*/
public qualify(column: string) {
public qualify(column: string): string {
const ctor = this.constructor as typeof Model
return `${ctor.tableName()}.${column}`
}
@@ -384,7 +394,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* a.qualifyKey() // => 'table_a.a_id'
* ```
*/
public qualifyKey() {
public qualifyKey(): string {
const ctor = this.constructor as typeof Model
return this.qualify(ctor.key)
}
@@ -400,7 +410,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* @param column
*/
public static qualify(column: string) {
public static qualify(column: string): string {
return `${this.tableName()}.${column}`
}
@@ -417,7 +427,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* A.qualifyKey() // => 'table_a.a_id'
* ```
*/
public static qualifyKey() {
public static qualifyKey(): string {
return this.qualify(this.key)
}
@@ -426,7 +436,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* return the unqualified name of the database column it corresponds to.
* @param modelKey
*/
public static propertyToColumn(modelKey: string) {
public static propertyToColumn(modelKey: string): string {
return getFieldsMeta(this)
.firstWhere('modelKey', '=', modelKey)?.databaseKey || modelKey
}
@@ -434,7 +444,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
/**
* Get the unqualified name of the column corresponding to the primary key of this model.
*/
public keyName() {
public keyName(): string {
const ctor = this.constructor as typeof Model
return ctor.key
}
@@ -446,11 +456,10 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* Only fields with `@Field()` annotations will be included.
*/
public toQueryRow(): QueryRow {
const row = {}
const row: QueryRow = {}
getFieldsMeta(this).each(field => {
// @ts-ignore
row[field.databaseKey] = this[field.modelKey]
row[field.databaseKey] = (this as any)[field.modelKey]
})
return row
@@ -462,13 +471,12 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* the record was fetched from the database or created.
*/
public dirtyToQueryRow(): QueryRow {
const row = {}
const row: QueryRow = {}
getFieldsMeta(this)
.filter(this._isDirty)
.filter(this.isDirtyCheck)
.each(field => {
// @ts-ignore
row[field.databaseKey] = this[field.modelKey]
row[field.databaseKey] = (this as any)[field.modelKey]
})
return row
@@ -478,13 +486,13 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* Get an object of the database field => value mapping that was originally
* fetched from the database. Excludes changes to model properties.
*/
public getOriginalValues() {
return deepCopy(this._original)
public getOriginalValues(): QueryRow | undefined {
return deepCopy(this.originalSourceRow)
}
/**
* Return an object of only the given properties on this model.
*
*
* @example
* Assume `a` is an instance of some model `A` with the given fields.
* ```typescript
@@ -492,15 +500,14 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* a.only('field1', 'id) // => {field1: 'field1 value', id: 123}
* ```
*
*
* @param fields
*/
public only(...fields: string[]) {
const row = {}
public only(...fields: string[]): QueryRow {
const row: QueryRow = {}
for ( const field of fields ) {
// @ts-ignore
row[field] = this[field]
row[field] = (this as any)[field]
}
return row
@@ -512,8 +519,8 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* Only fields with `@Field()` annotations are checked.
*/
public isDirty() {
return getFieldsMeta(this).some(this._isDirty)
public isDirty(): boolean {
return getFieldsMeta(this).some(this.isDirtyCheck)
}
@@ -523,7 +530,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* Only fields with `@Field()` annotations are checked.
*/
public isClean() {
public isClean(): boolean {
return !this.isDirty()
}
@@ -532,18 +539,25 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* the database, or if the given field never existed in the database.
* @param field
*/
public wasChanged(field: string) {
// @ts-ignore
return getFieldsMeta(this).pluck('modelKey').includes(field) && this[field] !== this._original[field]
public wasChanged(field: string): boolean {
return (
getFieldsMeta(this)
.pluck('modelKey')
.includes(field)
&& (
!this.originalSourceRow
|| (this as any)[field] !== this.originalSourceRow[field]
)
)
}
/**
* Returns an array of MODEL fields that have been modified since this record
* was fetched from the database or created.
*/
public getDirtyFields() {
public getDirtyFields(): string[] {
return getFieldsMeta(this)
.filter(this._isDirty)
.filter(this.isDirtyCheck)
.pluck('modelKey')
.toArray()
}
@@ -554,17 +568,15 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* If the model doesn't yet exist, set the CREATED_AT date. Always
* sets the UPDATED_AT date.
*/
public touch() {
public touch(): this {
const constructor = (this.constructor as typeof Model)
if ( constructor.timestamps ) {
if ( constructor.UPDATED_AT ) {
// @ts-ignore
this[constructor.UPDATED_AT] = new Date()
(this as any)[constructor.UPDATED_AT] = new Date()
}
if ( !this.exists() && constructor.CREATED_AT ) {
// @ts-ignore
this[constructor.CREATED_AT] = new Date()
(this as any)[constructor.CREATED_AT] = new Date()
}
}
return this
@@ -587,8 +599,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
await this.updating$.next(this)
if ( !withoutTimestamps && ctor.timestamps && ctor.UPDATED_AT ) {
// @ts-ignore
this[ctor.UPDATED_AT] = new Date()
(this as any)[ctor.UPDATED_AT] = new Date()
}
const result = await this.query()
@@ -602,7 +613,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
}
const data = result.rows.firstWhere(this.keyName(), '=', this.key())
if ( data ) await this.assumeFromSource(data)
if ( data ) {
await this.assumeFromSource(data)
}
await this.updated$.next(this)
} else if ( !this.exists() ) {
@@ -610,17 +623,15 @@ export abstract class Model<T extends Model<T>> extends AppClass {
if ( !withoutTimestamps ) {
if ( ctor.timestamps && ctor.CREATED_AT ) {
// @ts-ignore
this[ctor.CREATED_AT] = new Date()
(this as any)[ctor.CREATED_AT] = new Date()
}
if ( ctor.timestamps && ctor.UPDATED_AT ) {
// @ts-ignore
this[ctor.UPDATED_AT] = new Date()
(this as any)[ctor.UPDATED_AT] = new Date()
}
}
const row = this._buildInsertFieldObject()
const row = this.buildInsertFieldObject()
const returnable = new Collection<string>([this.keyName(), ...Object.keys(row)])
const result = await this.query()
@@ -633,7 +644,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
}
const data = result.rows.first()
if ( data ) await this.assumeFromSource(result)
if ( data ) {
await this.assumeFromSource(result)
}
await this.created$.next(this)
}
@@ -646,22 +659,19 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* Only fields with `@Field()` annotations are included.
*/
public toObject(): { [key: string]: any } {
public toObject(): QueryRow {
const ctor = this.constructor as typeof Model
const obj = {}
const obj: QueryRow = {}
getFieldsMeta(this).each(field => {
// @ts-ignore
obj[field.modelKey] = this[field.modelKey]
obj[String(field.modelKey)] = (this as any)[field.modelKey]
})
ctor.appends.forEach(field => {
// @ts-ignore
obj[field] = this[field]
obj[field] = (this as any)[field]
})
ctor.masks.forEach(field => {
// @ts-ignore
delete obj[field]
})
@@ -673,8 +683,8 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* Only fields with `@Field()` annotations are included.
*/
public toJSON(): string {
return JSON.stringify(this.toObject())
public toJSON(): QueryRow {
return this.toObject()
}
/**
@@ -696,7 +706,7 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* Overwrites any un-persisted changes in the current instance.
*/
public async refresh() {
public async refresh(): Promise<void> {
const results = this.query()
.clearFields()
.fields(...this.getLoadedDatabaseFields())
@@ -705,7 +715,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
.get()
const row = await results.first()
if ( row ) await this.assumeFromSource(row)
if ( row ) {
await this.assumeFromSource(row)
}
}
/**
@@ -766,10 +778,9 @@ export abstract class Model<T extends Model<T>> extends AppClass {
*
* @protected
*/
protected get _isDirty() {
protected get isDirtyCheck(): (field: ModelField) => boolean {
return (field: ModelField) => {
// @ts-ignore
return this[field.modelKey] !== this._original[field.databaseKey]
return !this.originalSourceRow || (this as any)[field.modelKey] !== this.originalSourceRow[field.databaseKey]
}
}
@@ -778,15 +789,18 @@ export abstract class Model<T extends Model<T>> extends AppClass {
* @protected
*/
protected getLoadedDatabaseFields(): string[] {
if ( !this._original ) return []
return Object.keys(this._original).map(String)
if ( !this.originalSourceRow ) {
return []
}
return Object.keys(this.originalSourceRow).map(String)
}
/**
* Build an object mapping database fields to the values that should be inserted for them.
* @private
*/
private _buildInsertFieldObject(): EscapeValueObject {
private buildInsertFieldObject(): EscapeValueObject {
const ctor = this.constructor as typeof Model
return getFieldsMeta(this)
@@ -795,19 +809,17 @@ export abstract class Model<T extends Model<T>> extends AppClass {
return fields.where('modelKey', '!=', this.keyName())
})
.get()
// @ts-ignore
.keyMap('databaseKey', inst => this[inst.modelKey])
.keyMap('databaseKey', inst => (this as any)[inst.modelKey])
}
/**
* Sets a property on `this` to the value of a given property in `object`.
* @param this_field_name
* @param object_field_name
* @param thisFieldName
* @param objectFieldName
* @param object
* @protected
*/
protected setFieldFromObject(this_field_name: string | symbol, object_field_name: string, object: { [key: string]: any }) {
// @ts-ignore
this[this_field_name] = object[object_field_name]
protected setFieldFromObject(thisFieldName: string | symbol, objectFieldName: string, object: QueryRow): void {
(this as any)[thisFieldName] = object[objectFieldName]
}
}

View File

@@ -1,8 +1,8 @@
import {Model} from "./Model";
import {AbstractBuilder} from "../builder/AbstractBuilder";
import {AbstractResultIterable} from "../builder/result/AbstractResultIterable";
import {Instantiable} from "../../di";
import {ModelResultIterable} from "./ModelResultIterable";
import {Model} from './Model'
import {AbstractBuilder} from '../builder/AbstractBuilder'
import {AbstractResultIterable} from '../builder/result/AbstractResultIterable'
import {Instantiable} from '../../di'
import {ModelResultIterable} from './ModelResultIterable'
/**
* Implementation of the abstract builder whose results yield instances of a given Model, `T`.
@@ -10,16 +10,16 @@ import {ModelResultIterable} from "./ModelResultIterable";
export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
constructor(
/** The model class that is created for results of this query. */
protected readonly ModelClass: Instantiable<T>
protected readonly ModelClass: Instantiable<T>,
) {
super()
}
public getNewInstance(): AbstractBuilder<T> {
return this.app().make<ModelBuilder<T>>(ModelBuilder)
}
public getResultIterable(): AbstractResultIterable<T> {
return this.app().make<ModelResultIterable<T>>(ModelResultIterable, this, this._connection, this.ModelClass)
return this.app().make<ModelResultIterable<T>>(ModelResultIterable, this, this.registeredConnection, this.ModelClass)
}
}

View File

@@ -1,10 +1,10 @@
import {Model} from "./Model";
import {AbstractResultIterable} from "../builder/result/AbstractResultIterable";
import {Connection} from "../connection/Connection";
import {ModelBuilder} from "./ModelBuilder";
import {Container, Instantiable} from "../../di";
import {QueryRow} from "../types";
import {Collection} from "../../util";
import {Model} from './Model'
import {AbstractResultIterable} from '../builder/result/AbstractResultIterable'
import {Connection} from '../connection/Connection'
import {ModelBuilder} from './ModelBuilder'
import {Container, Instantiable} from '../../di'
import {QueryRow} from '../types'
import {Collection} from '../../util'
/**
* Implementation of the result iterable that returns query results as instances of the defined model.
@@ -14,14 +14,16 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
public readonly builder: ModelBuilder<T>,
public readonly connection: Connection,
/** The model that should be instantiated for each row. */
protected readonly ModelClass: Instantiable<T>
) { super(builder, connection) }
protected readonly ModelClass: Instantiable<T>,
) {
super(builder, connection)
}
public get selectSQL() {
public get selectSQL(): string {
return this.connection.dialect().renderSelect(this.builder)
}
async at(i: number) {
async at(i: number): Promise<T | undefined> {
const query = this.connection.dialect().renderRangedSelect(this.selectSQL, i, i + 1)
const row = (await this.connection.query(query)).rows.first()
@@ -35,7 +37,7 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
return (await this.connection.query(query)).rows.promiseMap<T>(row => this.inflateRow(row))
}
async count() {
async count(): Promise<number> {
const query = this.connection.dialect().renderCount(this.selectSQL)
const result = (await this.connection.query(query)).rows.first()
return result?.extollo_render_count ?? 0
@@ -52,10 +54,11 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
* @protected
*/
protected async inflateRow(row: QueryRow): Promise<T> {
return Container.getContainer().make<T>(this.ModelClass).assumeFromSource(row)
return Container.getContainer().make<T>(this.ModelClass)
.assumeFromSource(row)
}
clone() {
clone(): ModelResultIterable<T> {
return new ModelResultIterable(this.builder, this.connection, this.ModelClass)
}
}

View File

@@ -1,10 +1,10 @@
import {Inject, Singleton} from "../../di";
import {DatabaseService} from "../DatabaseService";
import {PostgresConnection} from "../connection/PostgresConnection";
import {ErrorWithContext} from "../../util";
import {Unit} from "../../lifecycle/Unit";
import {Config} from "../../service/Config";
import {Logging} from "../../service/Logging";
import {Inject, Singleton} from '../../di'
import {DatabaseService} from '../DatabaseService'
import {PostgresConnection} from '../connection/PostgresConnection'
import {ErrorWithContext} from '../../util'
import {Unit} from '../../lifecycle/Unit'
import {Config} from '../../service/Config'
import {Logging} from '../../service/Logging'
/**
* Application unit responsible for loading and creating database connections from config.
@@ -24,12 +24,15 @@ export class Database extends Unit {
* Load the `database.connections` config and register Connection instances for each config.
* Automatically initializes the connections.
*/
public async up() {
public async up(): Promise<void> {
const connections = this.config.get('database.connections')
const promises = []
for ( const key in connections ) {
if ( !connections.hasOwnProperty(key) ) continue
if ( !Object.prototype.hasOwnProperty.call(connections, key) ) {
continue
}
const config = connections[key]
this.logging.info(`Initializing database connection: ${key}`)
@@ -55,7 +58,7 @@ export class Database extends Unit {
/**
* Close the configured connections cleanly before exit.
*/
public async down() {
public async down(): Promise<void> {
await Promise.all(this.dbService.names()
.map(name => this.dbService.get(name).close()))
}

View File

@@ -1,9 +1,9 @@
import {Model} from "../model/Model";
import {Instantiable, Singleton, Inject} from "../../di";
import {CommandLine} from "../../cli";
import {model_template} from "../template/model";
import {CanonicalStatic} from "../../service/CanonicalStatic";
import {CanonicalDefinition} from "../../service/Canonical";
import {Model} from '../model/Model'
import {Instantiable, Singleton, Inject} from '../../di'
import {CommandLine} from '../../cli'
import {templateModel} from '../template/model'
import {CanonicalStatic} from '../../service/CanonicalStatic'
import {CanonicalDefinition} from '../../service/Canonical'
/**
* Canonical unit responsible for loading the model classes defined by the application.
@@ -14,12 +14,14 @@ export class Models extends CanonicalStatic<Model<any>, Instantiable<Model<any>>
protected readonly cli!: CommandLine
protected appPath = ['models']
protected canonicalItem = 'model'
protected suffix = '.model.js'
public async up() {
public async up(): Promise<void> {
await super.up()
this.cli.registerTemplate(model_template)
this.cli.registerTemplate(templateModel)
}
public async initCanonicalItem(definition: CanonicalDefinition): Promise<Instantiable<Model<any>>> {

View File

@@ -1,12 +1,13 @@
import {Model} from "../model/Model";
import {Field} from "../model/Field";
import {FieldType} from "../types";
import {Model} from '../model/Model'
import {Field} from '../model/Field'
import {FieldType} from '../types'
/**
* A model instance which stores records from the ORMCache driver.
*/
export class CacheModel extends Model<CacheModel> {
protected static table = 'caches'; // FIXME allow configuring
protected static key = 'cache_key';
@Field(FieldType.varchar, 'cache_key')

View File

@@ -1,6 +1,6 @@
import {Container} from "../../di"
import {Cache} from "../../util"
import {CacheModel} from "./CacheModel"
import {Container} from '../../di'
import {Cache} from '../../util'
import {CacheModel} from './CacheModel'
/**
* A cache driver whose records are stored in a database table using the CacheModel.

View File

@@ -1,13 +1,15 @@
import {SessionModel} from "./SessionModel"
import {Container} from "../../di"
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from "../../http/session/Session";
import {SessionModel} from './SessionModel'
import {Container} from '../../di'
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from '../../http/session/Session'
/**
* An implementation of the Session driver whose records are stored in a database table.
*/
export class ORMSession extends Session {
protected key?: string
protected data?: SessionData
protected session?: SessionModel
public getKey(): string {
@@ -22,7 +24,7 @@ export class ORMSession extends Session {
this.key = key
}
public async load() {
public async load(): Promise<void> {
if ( !this.key ) {
throw new NoSessionKeyError()
}
@@ -38,31 +40,41 @@ export class ORMSession extends Session {
}
}
public async persist() {
if ( !this.key ) throw new NoSessionKeyError()
if ( !this.data || !this.session ) throw new SessionNotLoadedError()
public async persist(): Promise<void> {
if ( !this.key ) {
throw new NoSessionKeyError()
}
if ( !this.data || !this.session ) {
throw new SessionNotLoadedError()
}
this.session.uuid = this.key
this.session.json = JSON.stringify(this.data)
await this.session.save()
}
public getData() {
if ( !this.data ) throw new SessionNotLoadedError()
public getData(): SessionData {
if ( !this.data ) {
throw new SessionNotLoadedError()
}
return this.data
}
public setData(data: SessionData) {
public setData(data: SessionData): void {
this.data = data
}
public get(key: string, fallback?: any): any {
if ( !this.data ) throw new SessionNotLoadedError()
public get(key: string, fallback?: unknown): any {
if ( !this.data ) {
throw new SessionNotLoadedError()
}
return this.data[key] ?? fallback
}
public set(key: string, value: any) {
if ( !this.data ) throw new SessionNotLoadedError()
public set(key: string, value: unknown): void {
if ( !this.data ) {
throw new SessionNotLoadedError()
}
this.data[key] = value
}
}

View File

@@ -1,12 +1,13 @@
import {Model} from "../model/Model";
import {Field} from "../model/Field";
import {FieldType} from "../types";
import {Model} from '../model/Model'
import {Field} from '../model/Field'
import {FieldType} from '../types'
/**
* Model used to fetch & store sessions from the ORMSession driver.
*/
export class SessionModel extends Model<SessionModel> {
protected static table = 'sessions'; // FIXME allow configuring
protected static key = 'session_uuid';
@Field(FieldType.varchar, 'session_uuid')

View File

@@ -1,17 +1,15 @@
import {Template} from "../../cli"
import {UniversalPath} from "../../util"
import {Template} from '../../cli'
/**
* Template for creating new database model classes in app/models.
*/
const model_template: Template = {
const templateModel: Template = {
name: 'model',
fileSuffix: '.model.ts',
baseAppPath: ['models'],
description: 'Create a new class that represents a record in a database',
render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => {
return `import {Model} from "@extollo/orm"
import {Injectable} from "@extollo/di"
render: (name: string) => {
return `import {Injectable, Model} from "@extollo/lib"
/**
* ${name} Model
@@ -24,7 +22,7 @@ export class ${name} extends Model<${name}> {
protected static key = '${name.toLowerCase()}_id';
}
`
}
},
}
export { model_template }
export { templateModel }

View File

@@ -1,5 +1,5 @@
import { Collection } from '../util';
import {EscapeValue, QuerySafeValue} from "./dialect/SQLDialect";
import { Collection } from '../util'
import {EscapeValue, QuerySafeValue} from './dialect/SQLDialect'
/**
* A single query row, as an object.
@@ -51,16 +51,22 @@ export interface ConstraintGroup {
* Returns true if the given object is a valid ConstraintGroup.
* @param what
*/
export function isConstraintGroup(what: any): what is ConstraintGroup {
return typeof what === 'object' && Array.isArray(what.items) && what.preop
export function isConstraintGroup(what: unknown): what is ConstraintGroup {
return typeof what === 'object' && Array.isArray((what as any).items) && (what as any).preop
}
/**
* Returns true if the given object is a valid ConstraintItem
* @param what
*/
export function isConstraintItem(what: any): what is ConstraintItem {
return typeof what === 'object' && what.field && what.operator && what.operand && what.preop
export function isConstraintItem(what: unknown): what is ConstraintItem {
return (
typeof what === 'object'
&& (what as any).field
&& (what as any).operator
&& (what as any).operand
&& (what as any).preop
)
}
/**
@@ -97,7 +103,7 @@ export enum FieldType {
bigserial = 'bigserial',
serial8 = 'bigserial',
bit = 'bit',
bit_varying = 'bit varying',
bitVarying = 'bit varying',
varbit = 'bit varying',
boolean = 'boolean',
bool = 'boolean',
@@ -105,12 +111,12 @@ export enum FieldType {
bytea = 'bytea',
character = 'character',
char = 'character',
character_varying = 'character varying',
characterVarying = 'character varying',
varchar = 'character varying',
cidr = 'cidr',
circle = 'circle',
date = 'date',
double_precision = 'double precision',
doublePrecision = 'double precision',
float8 = 'double precision',
inet = 'inet',
integer = 'integer',
@@ -140,7 +146,7 @@ export enum FieldType {
timestamp = 'timestamp',
tsquery = 'tsquery',
tsvector = 'tsvector',
txid_snapshot = 'txid_snapshot',
txidSnapshot = 'txidSnapshot',
uuid = 'uuid',
xml = 'xml',
other = 'other',