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,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)
}
}