Models unit; session model; generalize session classes/interfaces

This commit is contained in:
garrettmills 2020-07-27 09:41:04 -05:00
parent 878de025d8
commit 25a37cf1a2
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E
27 changed files with 331 additions and 37 deletions

View File

@ -4,3 +4,4 @@ export { default as ControllerUnit } from '../../lib/src/unit/Controllers.ts'
export { default as MiddlewareUnit } from '../../lib/src/unit/Middlewares.ts' export { default as MiddlewareUnit } from '../../lib/src/unit/Middlewares.ts'
export { default as RoutesUnit } from '../../lib/src/unit/Routes.ts' export { default as RoutesUnit } from '../../lib/src/unit/Routes.ts'
export { default as HttpKernelUnit } from '../../lib/src/unit/HttpKernel.ts' export { default as HttpKernelUnit } from '../../lib/src/unit/HttpKernel.ts'
export { default as ModelsUnit } from '../../orm/src/ModelsUnit.ts'

View File

@ -6,4 +6,9 @@ export default {
enable: true, enable: true,
text: 'Daton', text: 'Daton',
}, },
session: {
driver: 'database', // memory | database
model: 'http:Session', // required for database
}
} }

View File

@ -0,0 +1,20 @@
import {Field} from '../../../orm/src/model/Field.ts'
import {Type} from '../../../orm/src/db/types.ts'
import {SessionModel} from '../../../lib/src/module.ts'
export default class Session extends SessionModel {
protected static table = 'sessions'
protected static key = 'session_id'
protected static readonly CREATED_AT = 'start_time'
protected static readonly UPDATED_AT = null // No updated at
@Field(Type.int)
protected session_id!: number
@Field(Type.int)
protected user_id?: number
@Field(Type.timestamp)
protected start_time!: Date
}

View File

@ -1,15 +1,11 @@
import { import {
ConfigUnit, ConfigUnit, DatabaseUnit, ControllerUnit, MiddlewareUnit, RoutesUnit, HttpKernelUnit, ModelsUnit
DatabaseUnit,
ControllerUnit,
MiddlewareUnit,
RoutesUnit,
HttpKernelUnit
} from './bundle/daton_units.ts' } from './bundle/daton_units.ts'
export default [ export default [
ConfigUnit, ConfigUnit,
DatabaseUnit, DatabaseUnit,
ModelsUnit,
HttpKernelUnit, HttpKernelUnit,
MiddlewareUnit, MiddlewareUnit,
ControllerUnit, ControllerUnit,

View File

@ -1,5 +1,5 @@
import Instantiable from './Instantiable.ts' import Instantiable from './Instantiable.ts'
import {StaticClass} from './StaticClass.ts' import {StaticClass} from './StaticClass.ts'
const DEPENDENCY_KEYS_METADATA_KEY = 'daton:di:dependencyKeys.ts' const DEPENDENCY_KEYS_METADATA_KEY = 'daton:di:dependencyKeys.ts'
type DependencyKey = Instantiable<any> | StaticClass<any> | string type DependencyKey = Instantiable<any> | StaticClass<any, any> | string
export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY } export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY }

View File

@ -2,7 +2,7 @@ export default interface Instantiable<T> {
new(...args: any[]): T new(...args: any[]): T
} }
const isInstantiable = (what: any): what is Instantiable<any> => { const isInstantiable = <T>(what: any): what is Instantiable<T> => {
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function' return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
} }

View File

@ -1,5 +1,5 @@
export type StaticClass<T> = Function & {prototype: T} export type StaticClass<T, T2> = Function & {prototype: T} & T2
export function isStaticClass<T>(something: any): something is StaticClass<T> { export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
return typeof something === 'function' && typeof something.prototype !== 'undefined' return typeof something === 'function' && typeof something.prototype !== 'undefined'
} }

View File

@ -1,6 +1,7 @@
import Session, { SessionData } from './Session.ts' import Session from './Session.ts'
import SessionInterface, { SessionData } from './SessionInterface.ts'
export default class MemorySession extends Session { export default class MemorySession extends Session implements SessionInterface {
private _key!: string private _key!: string
private _data: SessionData = {} private _data: SessionData = {}
@ -29,4 +30,5 @@ export default class MemorySession extends Session {
} }
public async persist() {} public async persist() {}
public async init_session(): Promise<void> {}
} }

View File

@ -0,0 +1,9 @@
import SessionFactory from './SessionFactory.ts'
import MemorySession from './MemorySession.ts'
import SessionInterface from './SessionInterface.ts'
export default class MemorySessionFactory extends SessionFactory {
produce(dependencies: any[], parameters: any[]): SessionInterface {
return new MemorySession()
}
}

View File

@ -3,8 +3,9 @@ import {Collection} from '../../collection/Collection.ts'
import Session from './Session.ts' import Session from './Session.ts'
import SessionManager, {InvalidSessionKeyError} from './SessionManager.ts' import SessionManager, {InvalidSessionKeyError} from './SessionManager.ts'
import Utility from '../../service/utility/Utility.ts' import Utility from '../../service/utility/Utility.ts'
import SessionInterface from './SessionInterface.ts'
export type SessionRegistrant = { key: string, session: Session } export type SessionRegistrant = { key: string, session: SessionInterface }
@Service() @Service()
export default class MemorySessionManager extends SessionManager { export default class MemorySessionManager extends SessionManager {
@ -14,7 +15,7 @@ export default class MemorySessionManager extends SessionManager {
return !!this._sessions.firstWhere('key', '=', key) return !!this._sessions.firstWhere('key', '=', key)
} }
public async get_session(key?: string): Promise<Session> { public async get_session(key?: string): Promise<SessionInterface> {
if ( !key ) { if ( !key ) {
const utility: Utility = this.make(Utility) const utility: Utility = this.make(Utility)
const session_key: string = key || utility.uuid() const session_key: string = key || utility.uuid()

View File

@ -0,0 +1,8 @@
import SessionManagerFactory from "./SessionManagerFactory.ts";
import MemorySessionManager from "./MemorySessionManager.ts";
export default class MemorySessionManagerFactory extends SessionManagerFactory {
produce(dependencies: any[], parameters: any[]): any {
return new MemorySessionManager()
}
}

View File

@ -0,0 +1,20 @@
import SessionFactory from './SessionFactory.ts'
import SessionInterface from './SessionInterface.ts'
import {Model} from '../../../../orm/src/model/Model.ts'
import {StaticClass} from '../../../../di/src/type/StaticClass.ts'
import {isInstantiable} from '../../../../di/src/type/Instantiable.ts'
export default class ModelSessionFactory extends SessionFactory {
constructor(
protected readonly ModelClass: StaticClass<SessionInterface, typeof Model>,
) {
super()
}
produce(dependencies: any[], parameters: any[]): SessionInterface {
if ( isInstantiable<SessionInterface>(this.ModelClass) )
return new this.ModelClass() as SessionInterface
else
throw new TypeError(`Session model class ${this.ModelClass} is not instantiable.`)
}
}

View File

@ -0,0 +1,52 @@
import SessionManager, {InvalidSessionKeyError} from './SessionManager.ts'
import {Model} from '../../../../orm/src/model/Model.ts'
import SessionInterface, {isSessionInterface} from './SessionInterface.ts'
import {StaticClass} from '../../../../di/src/type/StaticClass.ts'
export default class ModelSessionManager extends SessionManager {
constructor(
protected readonly ModelClass: StaticClass<SessionInterface, typeof Model>,
) {
super()
}
public async get_session(key?: string): Promise<SessionInterface> {
const ModelClass: typeof Model = this.ModelClass as typeof Model
if ( !key ) {
const session = this.make(ModelClass)
await session.init_session()
if ( isSessionInterface(session) )
return session as SessionInterface
throw new TypeError(`Session model improperly implements the required SessionInterface.`)
}
const session = await ModelClass.find_by_key(key)
if ( !session ) throw new InvalidSessionKeyError(key)
if ( isSessionInterface(session) )
return session as SessionInterface
throw new TypeError(`Session model improperly implements the required SessionInterface.`)
}
public async has_session(key: string): Promise<boolean> {
const ModelClass: typeof Model = this.ModelClass as typeof Model
return ModelClass.select(ModelClass.qualified_key_name())
.where(ModelClass.qualified_key_name(), '=', key)
.exists()
}
public async purge(key?: string): Promise<void> {
const ModelClass: typeof Model = this.ModelClass as typeof Model
const mutable = ModelClass.delete()
if ( key ) {
mutable.where(ModelClass.qualified_key_name(), '=', key)
}
await mutable.execute()
}
}

View File

@ -0,0 +1,17 @@
import SessionManagerFactory from './SessionManagerFactory.ts'
import ModelSessionManager from './ModelSessionManager.ts'
import {Model} from '../../../../orm/src/model/Model.ts'
import {StaticClass} from '../../../../di/src/type/StaticClass.ts'
import SessionInterface from './SessionInterface.ts'
export default class MemorySessionManagerFactory extends SessionManagerFactory {
constructor(
protected readonly ModelClass: StaticClass<SessionInterface, typeof Model>,
) {
super()
}
produce(dependencies: any[], parameters: any[]): any {
return new ModelSessionManager(this.ModelClass)
}
}

View File

@ -1,8 +1,7 @@
import AppClass from '../../lifecycle/AppClass.ts' import AppClass from '../../lifecycle/AppClass.ts'
import SessionInterface, {SessionData} from './SessionInterface.ts'
export type SessionData = { [key: string]: any } export default abstract class Session extends AppClass implements SessionInterface {
export default abstract class Session extends AppClass {
public abstract get_key(): string public abstract get_key(): string
public abstract set_key(key: string): void public abstract set_key(key: string): void
public abstract async persist(): Promise<void> public abstract async persist(): Promise<void>
@ -10,4 +9,5 @@ export default abstract class Session extends AppClass {
public abstract set_data(data: SessionData): void public abstract set_data(data: SessionData): void
public abstract get_attribute(key: string): any public abstract get_attribute(key: string): any
public abstract set_attribute(key: string, value: any): void public abstract set_attribute(key: string, value: any): void
public abstract async init_session(): Promise<void>
} }

View File

@ -3,6 +3,7 @@ import MemorySession from './MemorySession.ts'
import Session from './Session.ts' import Session from './Session.ts'
import {DependencyRequirement} from '../../../../di/src/type/DependencyRequirement.ts' import {DependencyRequirement} from '../../../../di/src/type/DependencyRequirement.ts'
import {Collection} from '../../collection/Collection.ts' import {Collection} from '../../collection/Collection.ts'
import SessionInterface from './SessionInterface.ts'
// TODO support configurable session backends // TODO support configurable session backends
@ -11,7 +12,7 @@ export default class SessionFactory extends AbstractFactory {
super({}) super({})
} }
produce(dependencies: any[], parameters: any[]): any { produce(dependencies: any[], parameters: any[]): SessionInterface {
return new MemorySession() return new MemorySession()
} }

View File

@ -0,0 +1,42 @@
import {logger} from "../../service/logging/global.ts";
export type SessionData = { [key: string]: any }
export default interface SessionInterface {
get_key(): string
set_key(key: string): void
persist(): Promise<void>
get_data(): SessionData
set_data(data: SessionData): void
get_attribute(key: string): any
set_attribute(key: string, value: any): void
init_session(): Promise<void>
}
export function isSessionInterface(what: any): what is SessionInterface {
const name_length_checks = [
{ name: 'get_key', length: 0 },
{ name: 'set_key', length: 1 },
{ name: 'persist', length: 0 },
{ name: 'get_data', length: 0 },
{ name: 'set_data', length: 1 },
{ name: 'get_attribute', length: 1 },
{ name: 'set_attribute', length: 2 },
{ name: 'init_session', length: 0 },
]
for ( const check of name_length_checks ) {
const { name, length } = check
if ( !(typeof what[name] === 'function') ) {
logger.debug(`Invalid session interface: typeof ${name} is not a function.`)
return false
}
if ( what[name].length !== length ) {
logger.debug(`Invalid session interface: method ${name} should expect ${length} arguments, ${what[name].length} actual.`)
return false
}
}
return true
}

View File

@ -1,5 +1,5 @@
import AppClass from '../../lifecycle/AppClass.ts' import AppClass from '../../lifecycle/AppClass.ts'
import Session from './Session.ts' import SessionInterface from './SessionInterface.ts'
export class InvalidSessionKeyError extends Error { export class InvalidSessionKeyError extends Error {
constructor(key: any) { constructor(key: any) {
@ -9,7 +9,7 @@ export class InvalidSessionKeyError extends Error {
export default abstract class SessionManager extends AppClass { export default abstract class SessionManager extends AppClass {
public abstract async get_session(key?: string): Promise<Session> public abstract async get_session(key?: string): Promise<SessionInterface>
public abstract async has_session(key: string): Promise<boolean> public abstract async has_session(key: string): Promise<boolean>
public abstract async purge(key?: string): Promise<void> public abstract async purge(key?: string): Promise<void>

View File

@ -0,0 +1,47 @@
import {Model} from '../../../../orm/src/model/Model.ts'
import SessionInterface, {SessionData} from './SessionInterface.ts'
import {Field} from '../../../../orm/src/model/Field.ts'
import {Type} from '../../../../orm/src/db/types.ts'
export default class SessionModel extends Model<SessionModel> implements SessionInterface {
@Field(Type.json)
protected data?: string
public get_key(): string {
return String(this.key())
}
public set_key(key: string) {
// @ts-ignore
this[this.key_name()] = parseInt(key)
}
public async persist(): Promise<void> {
await this.save()
}
public get_data(): SessionData {
return this.data ? JSON.parse(this.data) : undefined
}
public set_data(data: SessionData) {
this.data = JSON.stringify(data)
}
public get_attribute(key: string): any {
const data = this.get_data()
if ( data ) return data[key]
}
public set_attribute(key: string, value: any) {
const data = this.get_data()
data[key] = value
this.set_data(data)
}
public async init_session(): Promise<void> {
this.data = JSON.stringify({})
await this.save()
}
}

View File

@ -1,2 +1,3 @@
export { default as Scaffolding } from './unit/Scaffolding.ts' export { default as Scaffolding } from './unit/Scaffolding.ts'
export { default as Application } from './lifecycle/Application.ts' export { default as Application } from './lifecycle/Application.ts'
export { default as SessionModel } from './http/session/SessionModel.ts'

View File

@ -1,17 +1,29 @@
import LifecycleUnit from "../lifecycle/Unit.ts"; import LifecycleUnit from '../lifecycle/Unit.ts'
import {Unit} from "../lifecycle/decorators.ts"; import {Unit} from '../lifecycle/decorators.ts'
import Kernel from "../http/kernel/Kernel.ts"; import Kernel from '../http/kernel/Kernel.ts'
import PrepareRequest from "../http/kernel/module/PrepareRequest.ts"; import PrepareRequest from '../http/kernel/module/PrepareRequest.ts'
import SetSessionCookie from "../http/kernel/module/SetSessionCookie.ts"; import SetSessionCookie from '../http/kernel/module/SetSessionCookie.ts'
import Config from "./Config.ts"; import Config from './Config.ts'
import SetDatonHeaders from "../http/kernel/module/SetDatonHeaders.ts"; import SetDatonHeaders from '../http/kernel/module/SetDatonHeaders.ts'
import {Logging} from '../service/logging/Logging.ts'
import {Container} from '../../../di/src/Container.ts'
import MemorySessionFactory from '../http/session/MemorySessionFactory.ts'
import MemorySessionManagerFactory from '../http/session/MemorySessionManagerFactory.ts'
import ModelsUnit from '../../../orm/src/ModelsUnit.ts'
import {Model} from '../../../orm/src/model/Model.ts'
import {StaticClass} from '../../../di/src/type/StaticClass.ts'
import ModelSessionFactory from '../http/session/ModelSessionFactory.ts'
import ModelSessionManagerFactory from '../http/session/ModelSessionManagerFactory.ts'
import SessionInterface from '../http/session/SessionInterface.ts'
@Unit() @Unit()
export default class HttpKernel extends LifecycleUnit { export default class HttpKernel extends LifecycleUnit {
constructor( constructor(
protected readonly kernel: Kernel, protected readonly kernel: Kernel,
protected readonly config: Config, protected readonly config: Config,
protected readonly logger: Logging,
protected readonly injector: Container,
protected readonly models: ModelsUnit,
) { ) {
super() super()
} }
@ -23,6 +35,33 @@ export default class HttpKernel extends LifecycleUnit {
if ( this.config.get('server.powered_by.enable') ) { if ( this.config.get('server.powered_by.enable') ) {
SetDatonHeaders.register(this.kernel) SetDatonHeaders.register(this.kernel)
} }
this.determine_session_provider()
} }
protected determine_session_provider() {
const driver = this.config.get('server.session.driver')
if ( driver === 'memory' ) {
this.logger.verbose('Adding the memory session production factories to the container...')
this.injector.register_factory(new MemorySessionFactory())
this.injector.register_factory(new MemorySessionManagerFactory())
} else if ( driver === 'database' ) {
const model_key = this.config.get('server.session.model')
if ( !model_key ) {
this.logger.error('Please specify the canonical model name to use for the HTTP session.')
throw new Error('Missing required config property: server.session.model')
}
const ModelClass: StaticClass<SessionInterface, typeof Model> | undefined = this.models.get(model_key)
if ( !ModelClass ) {
this.logger.error(`Unable to find HTTP session model with name: ${model_key}`)
throw new Error(`Unable to find HTTP session model with name: ${model_key}`)
}
this.logger.info('Adding the model session production factories to the container...')
this.injector.register_factory(new ModelSessionFactory(ModelClass))
this.injector.register_factory(new ModelSessionManagerFactory(ModelClass))
}
}
} }

View File

@ -56,9 +56,5 @@ export default class Scaffolding extends LifecycleUnit {
public register_factories() { public register_factories() {
this.logger.verbose('Adding the cache production factory to the container...') this.logger.verbose('Adding the cache production factory to the container...')
this.injector.register_factory(new CacheFactory()) this.injector.register_factory(new CacheFactory())
this.logger.verbose('Adding the session production factories to the container...')
this.injector.register_factory(new SessionFactory())
this.injector.register_factory(new SessionManagerFactory())
} }
} }

View File

@ -0,0 +1,13 @@
import {Canonical, CanonicalDefinition} from './Canonical.ts'
import {InvalidCanonicalExportError} from './InstantiableCanonical.ts'
import {isStaticClass, StaticClass} from '../../../di/src/type/StaticClass.ts'
export class StaticCanonical<T, T2> extends Canonical<StaticClass<T, T2>> {
public async init_canonical_item(def: CanonicalDefinition): Promise<StaticClass<T, T2>> {
if ( isStaticClass(def.imported.default) ) {
return def.imported.default
}
throw new InvalidCanonicalExportError(def.original_name)
}
}

20
orm/src/ModelsUnit.ts Normal file
View File

@ -0,0 +1,20 @@
import {CanonicalDefinition} from '../../lib/src/unit/Canonical.ts'
import {Model} from './model/Model.ts'
import {Unit} from '../../lib/src/lifecycle/decorators.ts'
import {StaticCanonical} from '../../lib/src/unit/StaticCanonical.ts'
@Unit()
export default class ModelsUnit extends StaticCanonical<Model<any>, typeof Model> {
protected base_path = './app/models'
protected canonical_item = 'model'
protected suffix = '.model.ts'
public async init_canonical_item(def: CanonicalDefinition) {
const item = await super.init_canonical_item(def)
if ( !(item.prototype instanceof Model) ) {
throw new TypeError(`Invalid model definition: ${def.original_name}. Models must extend from Daton ORM's base Model class.`)
}
return item
}
}

View File

@ -82,6 +82,10 @@ export default abstract class ConnectionExecutable<T> {
return 0 return 0
} }
async exists(): Promise<boolean> {
return (await this.count()) > 0
}
async execute_in_connection(connection: string | Connection): Promise<QueryResult> { async execute_in_connection(connection: string | Connection): Promise<QueryResult> {
const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection

View File

@ -19,7 +19,7 @@ export default class PostgresConnection extends Connection {
const result = await this._client.query(query) const result = await this._client.query(query)
let base_i = 0 let base_i = 0
const cols = collect(result?.rowDescription?.columns || []).sortBy('index').map(col => { const cols = collect(result?.rowDescription?.columns || []).map(col => {
col.index = base_i col.index = base_i
base_i += 1 base_i += 1
return col return col

View File

@ -24,7 +24,7 @@ export abstract class Model<T extends Model<T>> extends Builder<T> {
* The name of the connection this model should run through. * The name of the connection this model should run through.
* @type string * @type string
*/ */
protected static connection: string protected static connection: string = 'default'
/** /**
* The name of the table this model is stored in. * The name of the table this model is stored in.
@ -42,13 +42,13 @@ export abstract class Model<T extends Model<T>> extends Builder<T> {
* Optionally, the timestamp field set on creation. * Optionally, the timestamp field set on creation.
* @type string * @type string
*/ */
protected static readonly CREATED_AT = 'created_at' protected static readonly CREATED_AT: string | null = 'created_at'
/** /**
* Optionally, the timestamp field set op update. * Optionally, the timestamp field set op update.
* @type string * @type string
*/ */
protected static readonly UPDATED_AT = 'updated_at' protected static readonly UPDATED_AT: string | null = 'updated_at'
/** /**
* If true, the CREATED_AT and UPDATED_AT columns will be automatically set. * If true, the CREATED_AT and UPDATED_AT columns will be automatically set.
@ -569,7 +569,7 @@ export abstract class Model<T extends Model<T>> extends Builder<T> {
* @param without_timestamps - if true, the UPDATED_AT/CREATED_AT timestamps will not be touched * @param without_timestamps - if true, the UPDATED_AT/CREATED_AT timestamps will not be touched
* @return Promise<Model> * @return Promise<Model>
*/ */
public async save({ without_timestamps = false }): Promise<Model<T>> { public async save({ without_timestamps = false } = {}): Promise<Model<T>> {
await this.saving$.next(this) await this.saving$.next(this)
const constructor = (this.constructor as typeof Model) const constructor = (this.constructor as typeof Model)