Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e57819d318 | |||
| 0a9dd30909 | |||
| d92c8b5409 | |||
| 589cb7d579 | |||
| 3680ad1914 | |||
| 96e13d85fc | |||
| 5a9283ad85 | |||
| b1ea489ccb | |||
| c3f2779650 | |||
| 248b24e612 | |||
| b4a9057e2b | |||
| c078d695a8 | |||
| 55ffadc742 | |||
| 56574d43ce | |||
| e16f02ce12 | |||
| c34fad3502 | |||
| 156006053b | |||
| 22cf6aa953 | |||
| b35eb8d6a1 | |||
| 9ee4c42e43 |
@@ -22,7 +22,7 @@ steps:
|
||||
from_secret: docs_deploy_key
|
||||
port: 22
|
||||
source: extollo_api_documentation.tar.gz
|
||||
target: /var/nfs/general/static/sites/extollo
|
||||
target: /var/nfs/storage/static/sites/extollo
|
||||
when:
|
||||
event: promote
|
||||
target: docs
|
||||
@@ -38,7 +38,7 @@ steps:
|
||||
from_secret: docs_deploy_key
|
||||
port: 22
|
||||
script:
|
||||
- cd /var/nfs/general/static/sites/extollo
|
||||
- cd /var/nfs/storage/static/sites/extollo
|
||||
- rm -rf docs
|
||||
- tar xzf extollo_api_documentation.tar.gz
|
||||
- rm -rf extollo_api_documentation.tar.gz
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@extollo/lib",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.9",
|
||||
"description": "The framework library that lifts up your code.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
||||
@@ -169,8 +169,12 @@ export abstract class Directive extends AppClass {
|
||||
const optionValues = this.parseOptions(options, argv)
|
||||
this.setOptionValues(optionValues)
|
||||
await this.handle(argv)
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
if ( e instanceof Error ) {
|
||||
this.nativeOutput(e.message)
|
||||
this.error(e)
|
||||
}
|
||||
|
||||
if ( e instanceof OptionValidationError ) {
|
||||
// expecting, value, requirements
|
||||
if ( e.context.expecting ) {
|
||||
@@ -187,6 +191,7 @@ export abstract class Directive extends AppClass {
|
||||
this.nativeOutput(` - ${e.context.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
this.nativeOutput('\nUse --help for more info.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@ export * from './directive/options/PositionalOption'
|
||||
export * from './directive/ShellDirective'
|
||||
export * from './directive/TemplateDirective'
|
||||
export * from './directive/UsageDirective'
|
||||
|
||||
export * from './decorators'
|
||||
|
||||
37
src/di/InjectionAware.ts
Normal file
37
src/di/InjectionAware.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {Container} from './Container'
|
||||
import {TypedDependencyKey} from './types'
|
||||
|
||||
/**
|
||||
* Base class for Injection-aware classes that automatically
|
||||
* pass along their configured container to instances created
|
||||
* via their `make` method.
|
||||
*/
|
||||
export class InjectionAware {
|
||||
private ci: Container
|
||||
|
||||
constructor() {
|
||||
this.ci = Container.getContainer()
|
||||
}
|
||||
|
||||
/** Set the container for this instance. */
|
||||
public setContainer(ci: Container): this {
|
||||
this.ci = ci
|
||||
return this
|
||||
}
|
||||
|
||||
/** Get the container for this instance. */
|
||||
public getContainer(): Container {
|
||||
return this.ci
|
||||
}
|
||||
|
||||
/** Instantiate a new injectable using the container. */
|
||||
public make<T>(target: TypedDependencyKey<T>, ...parameters: any[]): T {
|
||||
const inst = this.ci.make<T>(target, ...parameters)
|
||||
|
||||
if ( inst instanceof InjectionAware ) {
|
||||
inst.setContainer(this.ci)
|
||||
}
|
||||
|
||||
return inst
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,4 @@ export * from './ScopedContainer'
|
||||
export * from './types'
|
||||
|
||||
export * from './decorator/injection'
|
||||
export * from './InjectionAware'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Base type for an API response format.
|
||||
*/
|
||||
export interface APIResponse {
|
||||
export interface APIResponse<T> {
|
||||
success: boolean,
|
||||
message?: string,
|
||||
data?: any,
|
||||
data?: T,
|
||||
error?: {
|
||||
name: string,
|
||||
message: string,
|
||||
@@ -17,7 +17,7 @@ export interface APIResponse {
|
||||
* @param {string} displayMessage
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function message(displayMessage: string): APIResponse {
|
||||
export function message(displayMessage: string): APIResponse<void> {
|
||||
return {
|
||||
success: true,
|
||||
message: displayMessage,
|
||||
@@ -29,7 +29,7 @@ export function message(displayMessage: string): APIResponse {
|
||||
* @param record
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function one(record: unknown): APIResponse {
|
||||
export function one<T>(record: T): APIResponse<T> {
|
||||
return {
|
||||
success: true,
|
||||
data: record,
|
||||
@@ -41,7 +41,7 @@ export function one(record: unknown): APIResponse {
|
||||
* @param {array} records
|
||||
* @return APIResponse
|
||||
*/
|
||||
export function many(records: any[]): APIResponse {
|
||||
export function many<T>(records: T[]): APIResponse<{records: T[], total: number}> {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
@@ -56,7 +56,7 @@ export function many(records: any[]): APIResponse {
|
||||
* @return APIResponse
|
||||
* @param thrownError
|
||||
*/
|
||||
export function error(thrownError: string | Error): APIResponse {
|
||||
export function error(thrownError: string | Error): APIResponse<void> {
|
||||
if ( typeof thrownError === 'string' ) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Application} from './Application'
|
||||
import {Container, DependencyKey, Injectable} from '../di'
|
||||
import {Container, Injectable, TypedDependencyKey} from '../di'
|
||||
|
||||
/**
|
||||
* Base type for a class that supports binding methods by string.
|
||||
@@ -43,7 +43,7 @@ export class AppClass {
|
||||
}
|
||||
|
||||
/** Call the `make()` method on the global container. */
|
||||
protected make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||
protected make<T>(target: TypedDependencyKey<T>, ...parameters: any[]): T {
|
||||
return this.container().make<T>(target, ...parameters)
|
||||
}
|
||||
|
||||
|
||||
@@ -259,9 +259,13 @@ export class Application extends Container {
|
||||
try {
|
||||
await this.up()
|
||||
await this.down()
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
if ( e instanceof Error ) {
|
||||
this.errorHandler(e)
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,10 +310,15 @@ export class Application extends Container {
|
||||
await unit.up()
|
||||
unit.status = UnitStatus.Started
|
||||
logging.info(`Started ${unit.constructor.name}.`)
|
||||
} catch (e) {
|
||||
} catch (e: unknown) {
|
||||
unit.status = UnitStatus.Error
|
||||
|
||||
if ( e instanceof Error ) {
|
||||
throw this.errorWrapContext(e, {unitName: unit.constructor.name})
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,7 +336,12 @@ export class Application extends Container {
|
||||
logging.info(`Stopped ${unit.constructor.name}.`)
|
||||
} catch (e) {
|
||||
unit.status = UnitStatus.Error
|
||||
|
||||
if ( e instanceof Error ) {
|
||||
throw this.errorWrapContext(e, {unitName: unit.constructor.name})
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +79,13 @@ ${contextDisplay}
|
||||
}
|
||||
|
||||
this.logging.error(errorString, true)
|
||||
} catch (displayError) {
|
||||
} catch (displayError: unknown) {
|
||||
if ( displayError instanceof Error ) {
|
||||
// The error display encountered an error...
|
||||
// just throw the original so it makes it out
|
||||
console.error('RunLevelErrorHandler encountered an error:', displayError.message) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
throw operativeError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {Inject, Injectable} from '../di'
|
||||
import {ConstraintType, DatabaseService, FieldType, Migration, Schema} from '../orm'
|
||||
|
||||
/**
|
||||
* Migration that creates the sessions table used by the ORMSession backend.
|
||||
*/
|
||||
@Injectable()
|
||||
export default class CreateSessionsTableMigration extends Migration {
|
||||
@Inject()
|
||||
protected readonly db!: DatabaseService
|
||||
|
||||
async up(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('sessions')
|
||||
|
||||
table.primaryKey('session_uuid', FieldType.varchar)
|
||||
.required()
|
||||
|
||||
table.column('session_data')
|
||||
.type(FieldType.json)
|
||||
.required()
|
||||
.default('{}')
|
||||
|
||||
table.constraint('session_uuid_ck')
|
||||
.type(ConstraintType.Check)
|
||||
.expression('LENGTH(session_uuid) > 0')
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('sessions')
|
||||
|
||||
table.dropIfExists()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {Inject, Injectable} from '../di'
|
||||
import {DatabaseService, FieldType, Migration, Schema} from '../orm'
|
||||
|
||||
/**
|
||||
* Migration that creates the users table used by @extollo/lib.auth.
|
||||
*/
|
||||
@Injectable()
|
||||
export default class CreateUsersTableMigration extends Migration {
|
||||
@Inject()
|
||||
protected readonly db!: DatabaseService
|
||||
|
||||
async up(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('users')
|
||||
|
||||
table.primaryKey('user_id')
|
||||
.required()
|
||||
|
||||
table.column('first_name')
|
||||
.type(FieldType.varchar)
|
||||
.nullable()
|
||||
|
||||
table.column('last_name')
|
||||
.type(FieldType.varchar)
|
||||
.nullable()
|
||||
|
||||
table.column('password_hash')
|
||||
.type(FieldType.text)
|
||||
.nullable()
|
||||
|
||||
table.column('username')
|
||||
.type(FieldType.varchar)
|
||||
.required()
|
||||
.unique()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
const schema: Schema = this.db.get().schema()
|
||||
const table = await schema.table('users')
|
||||
|
||||
table.dropIfExists()
|
||||
|
||||
await schema.commit(table)
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,13 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
*/
|
||||
public abstract getResultIterable(): AbstractResultIterable<T>
|
||||
|
||||
/**
|
||||
* Get a copy of this builder with its values finalized.
|
||||
*/
|
||||
public finalize(): AbstractBuilder<T> {
|
||||
return this.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone the current query to a new AbstractBuilder instance with the same properties.
|
||||
*/
|
||||
@@ -489,7 +496,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this.registeredConnection.dialect().renderUpdate(this, data)
|
||||
const query = this.registeredConnection.dialect().renderUpdate(this.finalize(), data)
|
||||
return this.registeredConnection.query(query)
|
||||
}
|
||||
|
||||
@@ -515,7 +522,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this.registeredConnection.dialect().renderDelete(this)
|
||||
const query = this.registeredConnection.dialect().renderDelete(this.finalize())
|
||||
return this.registeredConnection.query(query)
|
||||
}
|
||||
|
||||
@@ -548,7 +555,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this.registeredConnection.dialect().renderInsert(this, rowOrRows)
|
||||
const query = this.registeredConnection.dialect().renderInsert(this.finalize(), rowOrRows)
|
||||
return this.registeredConnection.query(query)
|
||||
}
|
||||
|
||||
@@ -560,7 +567,7 @@ export abstract class AbstractBuilder<T> extends AppClass {
|
||||
throw new ErrorWithContext(`No connection specified to execute update query.`)
|
||||
}
|
||||
|
||||
const query = this.registeredConnection.dialect().renderExistential(this)
|
||||
const query = this.registeredConnection.dialect().renderExistential(this.finalize())
|
||||
const result = await this.registeredConnection.query(query)
|
||||
return Boolean(result.rows.first())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ export class Builder extends AbstractBuilder<QueryRow> {
|
||||
throw new ErrorWithContext(`No connection specified to fetch iterator for query.`)
|
||||
}
|
||||
|
||||
return Container.getContainer().make<ResultIterable>(ResultIterable, this, this.registeredConnection)
|
||||
return Container.getContainer().make<ResultIterable>(ResultIterable, this.finalize(), this.registeredConnection)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,15 @@ export class PostgresConnection extends Connection {
|
||||
rowCount: result.rowCount,
|
||||
}
|
||||
} catch (e) {
|
||||
if ( e instanceof Error ) {
|
||||
throw this.app().errorWrapContext(e, {
|
||||
query,
|
||||
connection: this.name,
|
||||
})
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public async asTransaction<T>(closure: () => Awaitable<T>): Promise<T> {
|
||||
|
||||
@@ -230,7 +230,7 @@ export class PostgreSQLDialect extends SQLDialect {
|
||||
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(', ')})` : ''))
|
||||
+ (columns.length ? ` (${columns.map(x => `"${x}"`).join(', ')})` : ''))
|
||||
}
|
||||
|
||||
if ( Array.isArray(data) && !data.length ) {
|
||||
|
||||
@@ -18,6 +18,16 @@ export * from './model/ModelResultIterable'
|
||||
export * from './model/events'
|
||||
export * from './model/Model'
|
||||
|
||||
export * from './model/relation/RelationBuilder'
|
||||
export * from './model/relation/Relation'
|
||||
export * from './model/relation/HasOneOrMany'
|
||||
export * from './model/relation/HasOne'
|
||||
export * from './model/relation/HasMany'
|
||||
export * from './model/relation/decorators'
|
||||
|
||||
export * from './model/scope/Scope'
|
||||
export * from './model/scope/ActiveScope'
|
||||
|
||||
export * from './support/SessionModel'
|
||||
export * from './support/ORMSession'
|
||||
export * from './support/CacheModel'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {ModelKey, QueryRow, QuerySource} from '../types'
|
||||
import {Container, Inject, Instantiable, StaticClass} from '../../di'
|
||||
import {Container, Inject, Instantiable, isInstantiable, StaticClass} from '../../di'
|
||||
import {DatabaseService} from '../DatabaseService'
|
||||
import {ModelBuilder} from './ModelBuilder'
|
||||
import {getFieldsMeta, ModelField} from './Field'
|
||||
import {deepCopy, Pipe, Collection, Awaitable, uuid4} from '../../util'
|
||||
import {deepCopy, Pipe, Collection, Awaitable, uuid4, isKeyof} from '../../util'
|
||||
import {EscapeValueObject} from '../dialect/SQLDialect'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Logging} from '../../service/Logging'
|
||||
@@ -17,6 +17,11 @@ import {ModelUpdatedEvent} from './events/ModelUpdatedEvent'
|
||||
import {ModelCreatingEvent} from './events/ModelCreatingEvent'
|
||||
import {ModelCreatedEvent} from './events/ModelCreatedEvent'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {Relation, RelationValue} from './relation/Relation'
|
||||
import {HasOne} from './relation/HasOne'
|
||||
import {HasMany} from './relation/HasMany'
|
||||
import {HasOneOrMany} from './relation/HasOneOrMany'
|
||||
import {Scope, ScopeClosure} from './scope/Scope'
|
||||
|
||||
/**
|
||||
* Base for classes that are mapped to tables in a database.
|
||||
@@ -83,6 +88,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
*/
|
||||
protected static masks: string[] = []
|
||||
|
||||
/**
|
||||
* Relations that should be eager-loaded by default.
|
||||
* @protected
|
||||
*/
|
||||
protected with: (keyof T)[] = []
|
||||
|
||||
/**
|
||||
* The original row fetched from the database.
|
||||
* @protected
|
||||
@@ -95,6 +106,14 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
*/
|
||||
protected modelEventBusSubscribers: Collection<EventSubscriberEntry<any>> = new Collection<EventSubscriberEntry<any>>()
|
||||
|
||||
/**
|
||||
* Cache of relation instances by property accessor.
|
||||
* This is used by the `@Relation()` decorator to cache Relation instances.
|
||||
*/
|
||||
public relationCache: Collection<{ accessor: string | symbol, relation: Relation<T, any, any> }> = new Collection<{accessor: string | symbol; relation: Relation<T, any, any>}>()
|
||||
|
||||
protected scopes: Collection<{ accessor: string | Instantiable<Scope>, scope: ScopeClosure }> = new Collection<{accessor: string | Instantiable<Scope>; scope: ScopeClosure}>()
|
||||
|
||||
/**
|
||||
* Get the table name for this model.
|
||||
*/
|
||||
@@ -155,6 +174,14 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
builder.field(field.databaseKey)
|
||||
})
|
||||
|
||||
for ( const relation of this.prototype.with ) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
builder.with(relation)
|
||||
}
|
||||
|
||||
builder.withScopes(this.prototype.scopes)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
@@ -307,6 +334,12 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
builder.field(field.databaseKey)
|
||||
})
|
||||
|
||||
for ( const relation of this.with ) {
|
||||
builder.with(relation)
|
||||
}
|
||||
|
||||
builder.withScopes(this.scopes)
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
@@ -627,7 +660,7 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
|
||||
const data = result.rows.first()
|
||||
if ( data ) {
|
||||
await this.assumeFromSource(result)
|
||||
await this.assumeFromSource(data)
|
||||
}
|
||||
|
||||
await this.dispatch(new ModelCreatedEvent<T>(this as any))
|
||||
@@ -871,4 +904,208 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new one-to-one relation instance. Should be called from a method on the model:
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class MyModel extends Model<MyModel> {
|
||||
* @Related()
|
||||
* public otherModel() {
|
||||
* return this.hasOne(MyOtherModel)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param related
|
||||
* @param foreignKeyOverride
|
||||
* @param localKeyOverride
|
||||
*/
|
||||
public hasOne<T2 extends Model<T2>>(related: Instantiable<T2>, foreignKeyOverride?: keyof T & string, localKeyOverride?: keyof T2 & string): HasOne<T, T2> {
|
||||
return new HasOne<T, T2>(this as unknown as T, this.make(related), foreignKeyOverride, localKeyOverride)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new one-to-one relation instance. Should be called from a method on the model:
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class MyModel extends Model<MyModel> {
|
||||
* @Related()
|
||||
* public otherModels() {
|
||||
* return this.hasMany(MyOtherModel)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param related
|
||||
* @param foreignKeyOverride
|
||||
* @param localKeyOverride
|
||||
*/
|
||||
public hasMany<T2 extends Model<T2>>(related: Instantiable<T2>, foreignKeyOverride?: keyof T & string, localKeyOverride?: keyof T2 & string): HasMany<T, T2> {
|
||||
return new HasMany<T, T2>(this as unknown as T, this.make(related), foreignKeyOverride, localKeyOverride)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the inverse of a one-to-one relation. Should be called from a method on the model:
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class MyModel extends Model<MyModel> {
|
||||
* @Related()
|
||||
* public otherModel() {
|
||||
* return this.hasOne(MyOtherModel)
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class MyOtherModel extends Model<MyOtherModel> {
|
||||
* @Related()
|
||||
* public myModel() {
|
||||
* return this.belongsToOne(MyModel, 'otherModel')
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param related
|
||||
* @param relationName
|
||||
*/
|
||||
public belongsToOne<T2 extends Model<T2>>(related: Instantiable<T>, relationName: keyof T2): HasOne<T, T2> {
|
||||
const relatedInst = this.make(related) as T2
|
||||
const relation = relatedInst.getRelation(relationName)
|
||||
|
||||
if ( !(relation instanceof HasOneOrMany) ) {
|
||||
throw new TypeError(`Cannot create belongs to one relation. Inverse relation must be HasOneOrMany.`)
|
||||
}
|
||||
|
||||
const localKey = relation.localKey
|
||||
const foreignKey = relation.foreignKey
|
||||
|
||||
if ( !isKeyof(localKey, this as unknown as T) || !isKeyof(foreignKey, relatedInst) ) {
|
||||
throw new TypeError('Local or foreign keys do not exist on the base model.')
|
||||
}
|
||||
|
||||
return new HasOne<T, T2>(this as unknown as T, relatedInst, localKey, foreignKey)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create the inverse of a one-to-many relation. Should be called from a method on the model:
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class MyModel extends Model<MyModel> {
|
||||
* @Related()
|
||||
* public otherModels() {
|
||||
* return this.hasMany(MyOtherModel)
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class MyOtherModel extends Model<MyOtherModel> {
|
||||
* @Related()
|
||||
* public myModels() {
|
||||
* return this.belongsToMany(MyModel, 'otherModels')
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param related
|
||||
* @param relationName
|
||||
*/
|
||||
public belongsToMany<T2 extends Model<T2>>(related: Instantiable<T>, relationName: keyof T2): HasMany<T, T2> {
|
||||
const relatedInst = this.make(related) as T2
|
||||
const relation = relatedInst.getRelation(relationName)
|
||||
|
||||
if ( !(relation instanceof HasOneOrMany) ) {
|
||||
throw new TypeError(`Cannot create belongs to one relation. Inverse relation must be HasOneOrMany.`)
|
||||
}
|
||||
|
||||
const localKey = relation.localKey
|
||||
const foreignKey = relation.foreignKey
|
||||
|
||||
if ( !isKeyof(localKey, this as unknown as T) || !isKeyof(foreignKey, relatedInst) ) {
|
||||
throw new TypeError('Local or foreign keys do not exist on the base model.')
|
||||
}
|
||||
|
||||
return new HasMany<T, T2>(this as unknown as T, relatedInst, localKey, foreignKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation instance returned by a method on this model.
|
||||
* @param name
|
||||
* @protected
|
||||
*/
|
||||
public getRelation<T2 extends Model<T2>>(name: keyof this): Relation<T, T2, RelationValue<T2>> {
|
||||
const relFn = this[name]
|
||||
|
||||
if ( relFn instanceof Relation ) {
|
||||
return relFn
|
||||
}
|
||||
|
||||
if ( typeof relFn === 'function' ) {
|
||||
const rel = relFn.apply(relFn, this)
|
||||
if ( rel instanceof Relation ) {
|
||||
return rel
|
||||
}
|
||||
}
|
||||
|
||||
throw new TypeError(`Cannot get relation of name: ${name}. Method does not return a Relation.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a scope on the model.
|
||||
* @param scope
|
||||
* @protected
|
||||
*/
|
||||
protected scope(scope: Instantiable<Scope> | ScopeClosure): this {
|
||||
if ( isInstantiable(scope) ) {
|
||||
if ( !this.hasScope(scope) ) {
|
||||
this.scopes.push({
|
||||
accessor: scope,
|
||||
scope: builder => (this.make<Scope>(scope)).apply(builder),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.scopes.push({
|
||||
accessor: uuid4(),
|
||||
scope,
|
||||
})
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a scope on the model with a specific name.
|
||||
* @param name
|
||||
* @param scope
|
||||
* @protected
|
||||
*/
|
||||
protected namedScope(name: string, scope: Instantiable<Scope> | ScopeClosure): this {
|
||||
if ( isInstantiable(scope) ) {
|
||||
if ( !this.hasScope(scope) ) {
|
||||
this.scopes.push({
|
||||
accessor: name,
|
||||
scope: builder => (this.make<Scope>(scope)).apply(builder),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.scopes.push({
|
||||
accessor: name,
|
||||
scope,
|
||||
})
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current model has a scope with the given identifier.
|
||||
* @param name
|
||||
* @protected
|
||||
*/
|
||||
protected hasScope(name: string | Instantiable<Scope>): boolean {
|
||||
return Boolean(this.scopes.firstWhere('accessor', '=', name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,16 @@ import {ModelResultIterable} from './ModelResultIterable'
|
||||
import {Collection} from '../../util'
|
||||
import {ConstraintOperator, ModelKey, ModelKeys} from '../types'
|
||||
import {EscapeValue} from '../dialect/SQLDialect'
|
||||
import {Scope, ScopeClosure} from './scope/Scope'
|
||||
|
||||
/**
|
||||
* Implementation of the abstract builder whose results yield instances of a given Model, `T`.
|
||||
*/
|
||||
export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
|
||||
protected eagerLoadRelations: (keyof T)[] = []
|
||||
|
||||
protected appliedScopes: Collection<{ accessor: string | Instantiable<Scope>, scope: ScopeClosure }> = new Collection<{accessor: string | Instantiable<Scope>; scope: ScopeClosure}>()
|
||||
|
||||
constructor(
|
||||
/** The model class that is created for results of this query. */
|
||||
protected readonly ModelClass: StaticClass<T, typeof Model> & Instantiable<T>,
|
||||
@@ -18,12 +23,27 @@ export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
|
||||
super()
|
||||
}
|
||||
|
||||
public withScopes(scopes: Collection<{ accessor: string | Instantiable<Scope>, scope: ScopeClosure }>): this {
|
||||
this.appliedScopes = scopes.clone()
|
||||
return this
|
||||
}
|
||||
|
||||
public getNewInstance(): AbstractBuilder<T> {
|
||||
return this.app().make<ModelBuilder<T>>(ModelBuilder)
|
||||
return this.app().make<ModelBuilder<T>>(ModelBuilder, this.ModelClass)
|
||||
}
|
||||
|
||||
public getResultIterable(): AbstractResultIterable<T> {
|
||||
return this.app().make<ModelResultIterable<T>>(ModelResultIterable, this, this.registeredConnection, this.ModelClass)
|
||||
return this.app().make<ModelResultIterable<T>>(ModelResultIterable, this.finalize(), this.registeredConnection, this.ModelClass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of this builder with all of its values finalized.
|
||||
* @override to apply scopes
|
||||
*/
|
||||
public finalize(): AbstractBuilder<T> {
|
||||
const inst = super.finalize()
|
||||
this.appliedScopes.each(rec => rec.scope(inst))
|
||||
return inst
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +72,42 @@ export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a relation to be eager-loaded.
|
||||
* @param relationName
|
||||
*/
|
||||
public with(relationName: keyof T): this {
|
||||
if ( !this.eagerLoadRelations.includes(relationName) ) {
|
||||
// Try to load the Relation so we fail if the name is invalid
|
||||
this.make<T>(this.ModelClass).getRelation(relationName)
|
||||
this.eagerLoadRelations.push(relationName)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all global scopes from this query.
|
||||
*/
|
||||
public withoutGlobalScopes(): this {
|
||||
this.appliedScopes = new Collection<{accessor: string | Instantiable<Scope>; scope: ScopeClosure}>()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific scope from this query by its identifier.
|
||||
* @param name
|
||||
*/
|
||||
public withoutGlobalScope(name: string | Instantiable<Scope>): this {
|
||||
this.appliedScopes = this.appliedScopes.where('accessor', '=', name)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Get the list of relations to eager-load. */
|
||||
public getEagerLoadedRelations(): (keyof T)[] {
|
||||
return [...this.eagerLoadRelations]
|
||||
}
|
||||
|
||||
/**
|
||||
* Given some format of keys of the model, try to normalize them to a flat array.
|
||||
* @param keys
|
||||
@@ -66,4 +122,15 @@ export class ModelBuilder<T extends Model<T>> extends AbstractBuilder<T> {
|
||||
|
||||
return [keys]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of this builder.
|
||||
* @override to add implementation-specific pass-alongs.
|
||||
*/
|
||||
public clone(): ModelBuilder<T> {
|
||||
const inst = super.clone() as ModelBuilder<T>
|
||||
inst.eagerLoadRelations = [...this.eagerLoadRelations]
|
||||
inst.appliedScopes = this.appliedScopes.clone()
|
||||
return inst
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Connection} from '../connection/Connection'
|
||||
import {ModelBuilder} from './ModelBuilder'
|
||||
import {Container, Instantiable} from '../../di'
|
||||
import {QueryRow} from '../types'
|
||||
import {Collection} from '../../util'
|
||||
import {collect, Collection} from '../../util'
|
||||
|
||||
/**
|
||||
* Implementation of the result iterable that returns query results as instances of the defined model.
|
||||
@@ -28,13 +28,17 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
|
||||
const row = (await this.connection.query(query)).rows.first()
|
||||
|
||||
if ( row ) {
|
||||
return this.inflateRow(row)
|
||||
const inflated = await this.inflateRow(row)
|
||||
await this.processEagerLoads(collect([inflated]))
|
||||
return inflated
|
||||
}
|
||||
}
|
||||
|
||||
async range(start: number, end: number): Promise<Collection<T>> {
|
||||
const query = this.connection.dialect().renderRangedSelect(this.selectSQL, start, end)
|
||||
return (await this.connection.query(query)).rows.promiseMap<T>(row => this.inflateRow(row))
|
||||
const inflated = await (await this.connection.query(query)).rows.promiseMap<T>(row => this.inflateRow(row))
|
||||
await this.processEagerLoads(inflated)
|
||||
return inflated
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
@@ -45,7 +49,9 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
|
||||
|
||||
async all(): Promise<Collection<T>> {
|
||||
const result = await this.connection.query(this.selectSQL)
|
||||
return result.rows.promiseMap<T>(row => this.inflateRow(row))
|
||||
const inflated = await result.rows.promiseMap<T>(row => this.inflateRow(row))
|
||||
await this.processEagerLoads(inflated)
|
||||
return inflated
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +64,30 @@ export class ModelResultIterable<T extends Model<T>> extends AbstractResultItera
|
||||
.assumeFromSource(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* Eager-load eager-loaded relations for the models in the query result.
|
||||
* @param results
|
||||
* @protected
|
||||
*/
|
||||
protected async processEagerLoads(results: Collection<T>): Promise<void> {
|
||||
const eagers = this.builder.getEagerLoadedRelations()
|
||||
const model = this.make<T>(this.ModelClass)
|
||||
|
||||
for ( const name of eagers ) {
|
||||
// TODO support nested eager loads?
|
||||
|
||||
const relation = model.getRelation(name)
|
||||
const select = relation.buildEagerQuery(this.builder, results)
|
||||
|
||||
const allRelated = await select.get().collect()
|
||||
allRelated.each(result => {
|
||||
const resultRelation = result.getRelation(name as any)
|
||||
const resultRelated = resultRelation.matchResults(allRelated as any)
|
||||
resultRelation.setValue(resultRelated as any)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
clone(): ModelResultIterable<T> {
|
||||
return new ModelResultIterable(this.builder, this.connection, this.ModelClass)
|
||||
}
|
||||
|
||||
47
src/orm/model/relation/HasMany.ts
Normal file
47
src/orm/model/relation/HasMany.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {Model} from '../Model'
|
||||
import {HasOneOrMany} from './HasOneOrMany'
|
||||
import {Collection} from '../../../util'
|
||||
import {RelationNotLoadedError} from './Relation'
|
||||
|
||||
/**
|
||||
* One-to-many relation implementation.
|
||||
*/
|
||||
export class HasMany<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2, Collection<T2>> {
|
||||
protected cachedValue?: Collection<T2>
|
||||
|
||||
protected cachedLoaded = false
|
||||
|
||||
constructor(
|
||||
parent: T,
|
||||
related: T2,
|
||||
foreignKeyOverride?: keyof T & string,
|
||||
localKeyOverride?: keyof T2 & string,
|
||||
) {
|
||||
super(parent, related, foreignKeyOverride, localKeyOverride)
|
||||
}
|
||||
|
||||
/** Resolve the result of this relation. */
|
||||
public get(): Promise<Collection<T2>> {
|
||||
return this.fetch().collect()
|
||||
}
|
||||
|
||||
/** Set the value of this relation. */
|
||||
public setValue(related: Collection<T2>): void {
|
||||
this.cachedValue = related.clone()
|
||||
this.cachedLoaded = true
|
||||
}
|
||||
|
||||
/** Get the value of this relation. */
|
||||
public getValue(): Collection<T2> {
|
||||
if ( !this.cachedValue ) {
|
||||
throw new RelationNotLoadedError()
|
||||
}
|
||||
|
||||
return this.cachedValue
|
||||
}
|
||||
|
||||
/** Returns true if the relation has been loaded. */
|
||||
public isLoaded(): boolean {
|
||||
return this.cachedLoaded
|
||||
}
|
||||
}
|
||||
47
src/orm/model/relation/HasOne.ts
Normal file
47
src/orm/model/relation/HasOne.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {Model} from '../Model'
|
||||
import {HasOneOrMany} from './HasOneOrMany'
|
||||
import {RelationNotLoadedError} from './Relation'
|
||||
import {Maybe} from '../../../util'
|
||||
|
||||
/**
|
||||
* One-to-one relation implementation.
|
||||
*/
|
||||
export class HasOne<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2, Maybe<T2>> {
|
||||
protected cachedValue?: T2
|
||||
|
||||
protected cachedLoaded = false
|
||||
|
||||
constructor(
|
||||
parent: T,
|
||||
related: T2,
|
||||
foreignKeyOverride?: keyof T & string,
|
||||
localKeyOverride?: keyof T2 & string,
|
||||
) {
|
||||
super(parent, related, foreignKeyOverride, localKeyOverride)
|
||||
}
|
||||
|
||||
/** Resolve the result of this relation. */
|
||||
async get(): Promise<Maybe<T2>> {
|
||||
return this.fetch().first()
|
||||
}
|
||||
|
||||
/** Set the value of this relation. */
|
||||
public setValue(related: T2): void {
|
||||
this.cachedValue = related
|
||||
this.cachedLoaded = true
|
||||
}
|
||||
|
||||
/** Get the value of this relation. */
|
||||
public getValue(): Maybe<T2> {
|
||||
if ( !this.cachedLoaded ) {
|
||||
throw new RelationNotLoadedError()
|
||||
}
|
||||
|
||||
return this.cachedValue
|
||||
}
|
||||
|
||||
/** Returns true if the relation has been loaded. */
|
||||
public isLoaded(): boolean {
|
||||
return this.cachedLoaded
|
||||
}
|
||||
}
|
||||
80
src/orm/model/relation/HasOneOrMany.ts
Normal file
80
src/orm/model/relation/HasOneOrMany.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {Model} from '../Model'
|
||||
import {Relation, RelationValue} from './Relation'
|
||||
import {RelationBuilder} from './RelationBuilder'
|
||||
import {raw} from '../../dialect/SQLDialect'
|
||||
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||
import {ModelBuilder} from '../ModelBuilder'
|
||||
import {Collection, toString} from '../../../util'
|
||||
|
||||
/**
|
||||
* Base class for 1:1 and 1:M relations.
|
||||
*/
|
||||
export abstract class HasOneOrMany<T extends Model<T>, T2 extends Model<T2>, V extends RelationValue<T2>> extends Relation<T, T2, V> {
|
||||
protected constructor(
|
||||
parent: T,
|
||||
related: T2,
|
||||
|
||||
/** Override the foreign key property. */
|
||||
protected foreignKeyOverride?: keyof T & string,
|
||||
|
||||
/** Override the local key property. */
|
||||
protected localKeyOverride?: keyof T2 & string,
|
||||
) {
|
||||
super(parent, related)
|
||||
}
|
||||
|
||||
/** Get the name of the foreign key for this relation. */
|
||||
public get foreignKey(): string {
|
||||
return this.foreignKeyOverride || this.parent.keyName()
|
||||
}
|
||||
|
||||
/** Get the name of the local key for this relation. */
|
||||
public get localKey(): string {
|
||||
return this.localKeyOverride || this.foreignKey
|
||||
}
|
||||
|
||||
/** Get the fully-qualified name of the foreign key. */
|
||||
public get qualifiedForeignKey(): string {
|
||||
return this.related.qualify(this.foreignKey)
|
||||
}
|
||||
|
||||
/** Get the fully-qualified name of the local key. */
|
||||
public get qualifiedLocalKey(): string {
|
||||
return this.related.qualify(this.localKey)
|
||||
}
|
||||
|
||||
/** Get the value of the pivot for this relation from the parent model. */
|
||||
public get parentValue(): any {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return this.parent[this.localKey]
|
||||
}
|
||||
|
||||
/** Create a new query for this relation. */
|
||||
public query(): RelationBuilder<T2> {
|
||||
return this.builder().select(raw('*'))
|
||||
}
|
||||
|
||||
/** Apply the relation's constraints on a model query. */
|
||||
public applyScope(where: AbstractBuilder<T2>): void {
|
||||
where.where(subq => {
|
||||
subq.where(this.qualifiedForeignKey, '=', this.parentValue)
|
||||
.whereRaw(this.qualifiedForeignKey, 'IS NOT', 'NULL')
|
||||
})
|
||||
}
|
||||
|
||||
/** Create an eager-load query matching this relation's models. */
|
||||
public buildEagerQuery(parentQuery: ModelBuilder<T>, result: Collection<T>): ModelBuilder<T2> {
|
||||
const keys = result.pluck(this.localKey as keyof T)
|
||||
.map(toString)
|
||||
.all()
|
||||
|
||||
return this.related.query()
|
||||
.whereIn(this.foreignKey, keys)
|
||||
}
|
||||
|
||||
/** Given a collection of results, filter out those that are relevant to this relation. */
|
||||
public matchResults(possiblyRelated: Collection<T>): Collection<T> {
|
||||
return possiblyRelated.where(this.foreignKey as keyof T, '=', this.parentValue)
|
||||
}
|
||||
}
|
||||
111
src/orm/model/relation/Relation.ts
Normal file
111
src/orm/model/relation/Relation.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {Model} from '../Model'
|
||||
import {ModelBuilder} from '../ModelBuilder'
|
||||
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||
import {ResultCollection} from '../../builder/result/ResultCollection'
|
||||
import {Collection, ErrorWithContext, Maybe} from '../../../util'
|
||||
import {QuerySource} from '../../types'
|
||||
import {RelationBuilder} from './RelationBuilder'
|
||||
import {InjectionAware} from '../../../di'
|
||||
|
||||
/** Type alias for possible values of a relation. */
|
||||
export type RelationValue<T2> = Maybe<Collection<T2> | T2>
|
||||
|
||||
/** Error thrown when a relation result is accessed synchronously before it is loaded. */
|
||||
export class RelationNotLoadedError extends ErrorWithContext {
|
||||
constructor(
|
||||
context: {[key: string]: any} = {},
|
||||
) {
|
||||
super('Attempted to get value of relation that has not yet been loaded.', context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for inter-model relation implementations.
|
||||
*/
|
||||
export abstract class Relation<T extends Model<T>, T2 extends Model<T2>, V extends RelationValue<T2>> extends InjectionAware {
|
||||
protected constructor(
|
||||
/** The model related from. */
|
||||
protected parent: T,
|
||||
|
||||
/** The model related to. */
|
||||
public readonly related: T2,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/** Get the value of the key field from the parent model. */
|
||||
protected abstract get parentValue(): any
|
||||
|
||||
/** Create a new relation builder query for this relation instance. */
|
||||
public abstract query(): RelationBuilder<T2>
|
||||
|
||||
/** Limit the results of the builder to only this relation's rows. */
|
||||
public abstract applyScope(where: AbstractBuilder<T2>): void
|
||||
|
||||
/** Create a relation query that will eager-load the result of this relation for a set of models. */
|
||||
public abstract buildEagerQuery(parentQuery: ModelBuilder<T>, result: Collection<T>): ModelBuilder<T2>
|
||||
|
||||
/** Given a set of possibly-related instances, filter out the ones that are relevant to the parent. */
|
||||
public abstract matchResults(possiblyRelated: Collection<T>): Collection<T>
|
||||
|
||||
/** Set the value of the relation. */
|
||||
public abstract setValue(related: V): void
|
||||
|
||||
/** Get the value of the relation. */
|
||||
public abstract getValue(): V
|
||||
|
||||
/** Returns true if the relation has been loaded. */
|
||||
public abstract isLoaded(): boolean
|
||||
|
||||
/** Get a collection of the results of this relation. */
|
||||
public fetch(): ResultCollection<T2> {
|
||||
return this.query().get()
|
||||
}
|
||||
|
||||
/** Resolve the result of this relation. */
|
||||
public abstract get(): Promise<V>
|
||||
|
||||
/**
|
||||
* Makes the relation "thenable" so relation methods on models can be awaited
|
||||
* to yield the result of the relation.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const rows = await myModelInstance.myHasManyRelation() -- rows is a Collection
|
||||
* ```
|
||||
*
|
||||
* @param resolve
|
||||
* @param reject
|
||||
*/
|
||||
public then(resolve: (result: V) => unknown, reject: (e: Error) => unknown): void {
|
||||
if ( this.isLoaded() ) {
|
||||
resolve(this.getValue())
|
||||
} else {
|
||||
this.get()
|
||||
.then(result => {
|
||||
if ( result instanceof Collection ) {
|
||||
this.setValue(result)
|
||||
}
|
||||
|
||||
resolve(result)
|
||||
})
|
||||
.catch(reject)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the value of this relation. */
|
||||
public get value(): V {
|
||||
return this.getValue()
|
||||
}
|
||||
|
||||
/** Get the query source for the related model in this relation. */
|
||||
public get relatedQuerySource(): QuerySource {
|
||||
const related = this.related.constructor as typeof Model
|
||||
return related.querySource()
|
||||
}
|
||||
|
||||
/** Get a new builder instance for this relation. */
|
||||
public builder(): RelationBuilder<T2> {
|
||||
return this.make(RelationBuilder, this)
|
||||
}
|
||||
}
|
||||
14
src/orm/model/relation/RelationBuilder.ts
Normal file
14
src/orm/model/relation/RelationBuilder.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {Model} from '../Model'
|
||||
import {ModelBuilder} from '../ModelBuilder'
|
||||
import {Relation} from './Relation'
|
||||
|
||||
/**
|
||||
* ModelBuilder instance that queries the related model in a relation.
|
||||
*/
|
||||
export class RelationBuilder<T extends Model<T>> extends ModelBuilder<T> {
|
||||
constructor(
|
||||
protected relation: Relation<any, T, any>,
|
||||
) {
|
||||
super(relation.related.constructor as any)
|
||||
}
|
||||
}
|
||||
37
src/orm/model/relation/decorators.ts
Normal file
37
src/orm/model/relation/decorators.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {Model} from '../Model'
|
||||
|
||||
/**
|
||||
* Decorator for relation methods on a Model implementation.
|
||||
* Caches the relation instances between uses for the life of the model.
|
||||
* @constructor
|
||||
*/
|
||||
export function Related(): MethodDecorator {
|
||||
return (target, propertyKey, descriptor) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const original = descriptor.value
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
descriptor.value = function(...args) {
|
||||
const model = this as Model<any>
|
||||
const cache = model.relationCache
|
||||
|
||||
const existing = cache.firstWhere('accessor', '=', propertyKey)
|
||||
if ( existing ) {
|
||||
return existing.relation
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const value = original.apply(this, args)
|
||||
|
||||
cache.push({
|
||||
accessor: propertyKey,
|
||||
relation: value,
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/orm/model/scope/ActiveScope.ts
Normal file
11
src/orm/model/scope/ActiveScope.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {Scope} from './Scope'
|
||||
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||
|
||||
/**
|
||||
* A basic scope to limit results where `active` = true.
|
||||
*/
|
||||
export class ActiveScope extends Scope {
|
||||
apply(query: AbstractBuilder<any>): void {
|
||||
query.where('active', '=', true)
|
||||
}
|
||||
}
|
||||
18
src/orm/model/scope/Scope.ts
Normal file
18
src/orm/model/scope/Scope.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {Injectable, InjectionAware} from '../../../di'
|
||||
import {Awaitable} from '../../../util'
|
||||
import {AbstractBuilder} from '../../builder/AbstractBuilder'
|
||||
|
||||
/**
|
||||
* A closure that takes a query and applies some scope to it.
|
||||
*/
|
||||
export type ScopeClosure = (query: AbstractBuilder<any>) => Awaitable<void>
|
||||
|
||||
/**
|
||||
* Base class for scopes that can be applied to queries.
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class Scope extends InjectionAware {
|
||||
|
||||
abstract apply(query: AbstractBuilder<any>): Awaitable<void>
|
||||
|
||||
}
|
||||
@@ -131,6 +131,7 @@ export enum FieldType {
|
||||
json = 'json',
|
||||
line = 'line',
|
||||
lseg = 'lseg',
|
||||
ltree = 'ltree',
|
||||
macaddr = 'macaddr',
|
||||
money = 'money',
|
||||
numeric = 'numeric',
|
||||
@@ -189,6 +190,7 @@ export function inverseFieldType(type: FieldType): string {
|
||||
json: 'json',
|
||||
line: 'line',
|
||||
lseg: 'lseg',
|
||||
ltree: 'ltree',
|
||||
macaddr: 'macaddr',
|
||||
money: 'money',
|
||||
numeric: 'numeric',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Inject, Singleton} from '../di'
|
||||
import {HTTPStatus, withTimeout} from '../util'
|
||||
import {ErrorWithContext, HTTPStatus, withTimeout} from '../util'
|
||||
import {Unit} from '../lifecycle/Unit'
|
||||
import {createServer, IncomingMessage, RequestListener, Server, ServerResponse} from 'http'
|
||||
import {Logging} from './Logging'
|
||||
@@ -114,9 +114,13 @@ export class HTTPServer extends Unit {
|
||||
try {
|
||||
await this.kernel.handle(extolloReq)
|
||||
} catch (e) {
|
||||
if ( e instanceof Error ) {
|
||||
await error(e).write(extolloReq)
|
||||
}
|
||||
|
||||
await error(new ErrorWithContext('Unknown error occurred.', { e }))
|
||||
}
|
||||
|
||||
await extolloReq.response.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,11 @@ type ComparisonFunction<T> = (item: CollectionItem<T>, otherItem: CollectionItem
|
||||
import { WhereOperator, applyWhere, whereMatch } from './where'
|
||||
|
||||
const collect = <T>(items: CollectionItem<T>[]): Collection<T> => Collection.collect(items)
|
||||
const toString = (item: unknown): string => String(item)
|
||||
|
||||
export {
|
||||
collect,
|
||||
toString,
|
||||
Collection,
|
||||
|
||||
// Types
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Collection} from './Collection'
|
||||
import {InjectionAware} from '../../di'
|
||||
|
||||
export type MaybeIterationItem<T> = { done: boolean, value?: T }
|
||||
export type ChunkCallback<T> = (items: Collection<T>) => any
|
||||
@@ -9,7 +10,7 @@ export class StopIteration extends Error {}
|
||||
* Abstract class representing an iterable, lazy-loaded dataset.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Iterable<T> {
|
||||
export abstract class Iterable<T> extends InjectionAware {
|
||||
/**
|
||||
* The current index of the iterable.
|
||||
* @type number
|
||||
|
||||
@@ -137,7 +137,7 @@ export class BehaviorSubject<T> {
|
||||
} catch (e) {
|
||||
if ( e instanceof UnsubscribeError ) {
|
||||
this.subscribers = this.subscribers.filter(x => x !== subscriber)
|
||||
} else if (subscriber.error) {
|
||||
} else if (subscriber.error && e instanceof Error) {
|
||||
await subscriber.error(e)
|
||||
} else {
|
||||
throw e
|
||||
@@ -181,7 +181,7 @@ export class BehaviorSubject<T> {
|
||||
try {
|
||||
await subscriber.complete(finalValue)
|
||||
} catch (e) {
|
||||
if ( subscriber.error ) {
|
||||
if ( subscriber.error && e instanceof Error ) {
|
||||
await subscriber.error(e)
|
||||
} else {
|
||||
throw e
|
||||
|
||||
@@ -92,7 +92,7 @@ export class LocalFilesystem extends Filesystem {
|
||||
isFile: stat.isFile(),
|
||||
}
|
||||
} catch (e) {
|
||||
if ( e?.code === 'ENOENT' ) {
|
||||
if ( (e as any)?.code === 'ENOENT' ) {
|
||||
return {
|
||||
path: new UniversalPath(args.storePath, this),
|
||||
exists: false,
|
||||
|
||||
@@ -9,3 +9,12 @@ export type ParameterizedCallback<T> = ((arg: T) => any)
|
||||
|
||||
/** A key-value form of a given type. */
|
||||
export type KeyValue<T> = {key: string, value: T}
|
||||
|
||||
/** Simple helper method to verify that a key is a keyof some object. */
|
||||
export function isKeyof<T>(key: unknown, obj: T): key is keyof T {
|
||||
if ( typeof key !== 'string' && typeof key !== 'symbol' ) {
|
||||
return false
|
||||
}
|
||||
|
||||
return key in obj
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user