diff --git a/app/bundle/daton_units.ts b/app/bundle/daton_units.ts index 1057d93..e46aa13 100644 --- a/app/bundle/daton_units.ts +++ b/app/bundle/daton_units.ts @@ -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 RoutesUnit } from '../../lib/src/unit/Routes.ts' export { default as HttpKernelUnit } from '../../lib/src/unit/HttpKernel.ts' +export { default as ModelsUnit } from '../../orm/src/ModelsUnit.ts' diff --git a/app/configs/server.config.ts b/app/configs/server.config.ts index a10b6bb..a954d04 100644 --- a/app/configs/server.config.ts +++ b/app/configs/server.config.ts @@ -6,4 +6,9 @@ export default { enable: true, text: 'Daton', }, + + session: { + driver: 'database', // memory | database + model: 'http:Session', // required for database + } } diff --git a/app/models/http/Session.model.ts b/app/models/http/Session.model.ts new file mode 100644 index 0000000..246a34b --- /dev/null +++ b/app/models/http/Session.model.ts @@ -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 +} diff --git a/app/units.ts b/app/units.ts index 36a5bd3..587794a 100644 --- a/app/units.ts +++ b/app/units.ts @@ -1,15 +1,11 @@ import { - ConfigUnit, - DatabaseUnit, - ControllerUnit, - MiddlewareUnit, - RoutesUnit, - HttpKernelUnit + ConfigUnit, DatabaseUnit, ControllerUnit, MiddlewareUnit, RoutesUnit, HttpKernelUnit, ModelsUnit } from './bundle/daton_units.ts' export default [ ConfigUnit, DatabaseUnit, + ModelsUnit, HttpKernelUnit, MiddlewareUnit, ControllerUnit, diff --git a/di/src/type/DependencyKey.ts b/di/src/type/DependencyKey.ts index 06a26ae..9e794e2 100644 --- a/di/src/type/DependencyKey.ts +++ b/di/src/type/DependencyKey.ts @@ -1,5 +1,5 @@ import Instantiable from './Instantiable.ts' import {StaticClass} from './StaticClass.ts' const DEPENDENCY_KEYS_METADATA_KEY = 'daton:di:dependencyKeys.ts' -type DependencyKey = Instantiable | StaticClass | string +type DependencyKey = Instantiable | StaticClass | string export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY } diff --git a/di/src/type/Instantiable.ts b/di/src/type/Instantiable.ts index 74ddb33..0f75b87 100644 --- a/di/src/type/Instantiable.ts +++ b/di/src/type/Instantiable.ts @@ -2,7 +2,7 @@ export default interface Instantiable { new(...args: any[]): T } -const isInstantiable = (what: any): what is Instantiable => { +const isInstantiable = (what: any): what is Instantiable => { return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function' } diff --git a/di/src/type/StaticClass.ts b/di/src/type/StaticClass.ts index c7bb961..4ee0019 100644 --- a/di/src/type/StaticClass.ts +++ b/di/src/type/StaticClass.ts @@ -1,5 +1,5 @@ -export type StaticClass = Function & {prototype: T} +export type StaticClass = Function & {prototype: T} & T2 -export function isStaticClass(something: any): something is StaticClass { +export function isStaticClass(something: any): something is StaticClass { return typeof something === 'function' && typeof something.prototype !== 'undefined' } diff --git a/lib/src/http/session/MemorySession.ts b/lib/src/http/session/MemorySession.ts index 75f2ad0..86a165c 100644 --- a/lib/src/http/session/MemorySession.ts +++ b/lib/src/http/session/MemorySession.ts @@ -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 _data: SessionData = {} @@ -29,4 +30,5 @@ export default class MemorySession extends Session { } public async persist() {} + public async init_session(): Promise {} } diff --git a/lib/src/http/session/MemorySessionFactory.ts b/lib/src/http/session/MemorySessionFactory.ts new file mode 100644 index 0000000..be2d4e1 --- /dev/null +++ b/lib/src/http/session/MemorySessionFactory.ts @@ -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() + } +} diff --git a/lib/src/http/session/MemorySessionManager.ts b/lib/src/http/session/MemorySessionManager.ts index 5501073..a717414 100644 --- a/lib/src/http/session/MemorySessionManager.ts +++ b/lib/src/http/session/MemorySessionManager.ts @@ -3,8 +3,9 @@ import {Collection} from '../../collection/Collection.ts' import Session from './Session.ts' import SessionManager, {InvalidSessionKeyError} from './SessionManager.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() export default class MemorySessionManager extends SessionManager { @@ -14,7 +15,7 @@ export default class MemorySessionManager extends SessionManager { return !!this._sessions.firstWhere('key', '=', key) } - public async get_session(key?: string): Promise { + public async get_session(key?: string): Promise { if ( !key ) { const utility: Utility = this.make(Utility) const session_key: string = key || utility.uuid() diff --git a/lib/src/http/session/MemorySessionManagerFactory.ts b/lib/src/http/session/MemorySessionManagerFactory.ts new file mode 100644 index 0000000..097e8cf --- /dev/null +++ b/lib/src/http/session/MemorySessionManagerFactory.ts @@ -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() + } +} diff --git a/lib/src/http/session/ModelSessionFactory.ts b/lib/src/http/session/ModelSessionFactory.ts new file mode 100644 index 0000000..ff118e0 --- /dev/null +++ b/lib/src/http/session/ModelSessionFactory.ts @@ -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, + ) { + super() + } + + produce(dependencies: any[], parameters: any[]): SessionInterface { + if ( isInstantiable(this.ModelClass) ) + return new this.ModelClass() as SessionInterface + else + throw new TypeError(`Session model class ${this.ModelClass} is not instantiable.`) + } +} diff --git a/lib/src/http/session/ModelSessionManager.ts b/lib/src/http/session/ModelSessionManager.ts new file mode 100644 index 0000000..fe881a6 --- /dev/null +++ b/lib/src/http/session/ModelSessionManager.ts @@ -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, + ) { + super() + } + + public async get_session(key?: string): Promise { + 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 { + 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 { + 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() + } +} diff --git a/lib/src/http/session/ModelSessionManagerFactory.ts b/lib/src/http/session/ModelSessionManagerFactory.ts new file mode 100644 index 0000000..de5fbfe --- /dev/null +++ b/lib/src/http/session/ModelSessionManagerFactory.ts @@ -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, + ) { + super() + } + + produce(dependencies: any[], parameters: any[]): any { + return new ModelSessionManager(this.ModelClass) + } +} diff --git a/lib/src/http/session/Session.ts b/lib/src/http/session/Session.ts index 849243f..a3f0585 100644 --- a/lib/src/http/session/Session.ts +++ b/lib/src/http/session/Session.ts @@ -1,8 +1,7 @@ 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 { +export default abstract class Session extends AppClass implements SessionInterface { public abstract get_key(): string public abstract set_key(key: string): void public abstract async persist(): Promise @@ -10,4 +9,5 @@ export default abstract class Session extends AppClass { public abstract set_data(data: SessionData): void public abstract get_attribute(key: string): any public abstract set_attribute(key: string, value: any): void + public abstract async init_session(): Promise } diff --git a/lib/src/http/session/SessionFactory.ts b/lib/src/http/session/SessionFactory.ts index 28deec2..04e11c3 100644 --- a/lib/src/http/session/SessionFactory.ts +++ b/lib/src/http/session/SessionFactory.ts @@ -3,6 +3,7 @@ import MemorySession from './MemorySession.ts' import Session from './Session.ts' import {DependencyRequirement} from '../../../../di/src/type/DependencyRequirement.ts' import {Collection} from '../../collection/Collection.ts' +import SessionInterface from './SessionInterface.ts' // TODO support configurable session backends @@ -11,7 +12,7 @@ export default class SessionFactory extends AbstractFactory { super({}) } - produce(dependencies: any[], parameters: any[]): any { + produce(dependencies: any[], parameters: any[]): SessionInterface { return new MemorySession() } diff --git a/lib/src/http/session/SessionInterface.ts b/lib/src/http/session/SessionInterface.ts new file mode 100644 index 0000000..d18bfae --- /dev/null +++ b/lib/src/http/session/SessionInterface.ts @@ -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 + get_data(): SessionData + set_data(data: SessionData): void + get_attribute(key: string): any + set_attribute(key: string, value: any): void + init_session(): Promise +} + +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 +} diff --git a/lib/src/http/session/SessionManager.ts b/lib/src/http/session/SessionManager.ts index aa28896..878703d 100644 --- a/lib/src/http/session/SessionManager.ts +++ b/lib/src/http/session/SessionManager.ts @@ -1,5 +1,5 @@ import AppClass from '../../lifecycle/AppClass.ts' -import Session from './Session.ts' +import SessionInterface from './SessionInterface.ts' export class InvalidSessionKeyError extends Error { constructor(key: any) { @@ -9,7 +9,7 @@ export class InvalidSessionKeyError extends Error { export default abstract class SessionManager extends AppClass { - public abstract async get_session(key?: string): Promise + public abstract async get_session(key?: string): Promise public abstract async has_session(key: string): Promise public abstract async purge(key?: string): Promise diff --git a/lib/src/http/session/SessionModel.ts b/lib/src/http/session/SessionModel.ts new file mode 100644 index 0000000..55a4702 --- /dev/null +++ b/lib/src/http/session/SessionModel.ts @@ -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 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 { + 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 { + this.data = JSON.stringify({}) + await this.save() + } +} diff --git a/lib/src/module.ts b/lib/src/module.ts index cd932e1..4d67f9d 100644 --- a/lib/src/module.ts +++ b/lib/src/module.ts @@ -1,2 +1,3 @@ export { default as Scaffolding } from './unit/Scaffolding.ts' export { default as Application } from './lifecycle/Application.ts' +export { default as SessionModel } from './http/session/SessionModel.ts' \ No newline at end of file diff --git a/lib/src/unit/HttpKernel.ts b/lib/src/unit/HttpKernel.ts index 5b3400c..635e352 100644 --- a/lib/src/unit/HttpKernel.ts +++ b/lib/src/unit/HttpKernel.ts @@ -1,17 +1,29 @@ -import LifecycleUnit from "../lifecycle/Unit.ts"; -import {Unit} from "../lifecycle/decorators.ts"; -import Kernel from "../http/kernel/Kernel.ts"; -import PrepareRequest from "../http/kernel/module/PrepareRequest.ts"; -import SetSessionCookie from "../http/kernel/module/SetSessionCookie.ts"; -import Config from "./Config.ts"; -import SetDatonHeaders from "../http/kernel/module/SetDatonHeaders.ts"; +import LifecycleUnit from '../lifecycle/Unit.ts' +import {Unit} from '../lifecycle/decorators.ts' +import Kernel from '../http/kernel/Kernel.ts' +import PrepareRequest from '../http/kernel/module/PrepareRequest.ts' +import SetSessionCookie from '../http/kernel/module/SetSessionCookie.ts' +import Config from './Config.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() export default class HttpKernel extends LifecycleUnit { - constructor( protected readonly kernel: Kernel, protected readonly config: Config, + protected readonly logger: Logging, + protected readonly injector: Container, + protected readonly models: ModelsUnit, ) { super() } @@ -23,6 +35,33 @@ export default class HttpKernel extends LifecycleUnit { if ( this.config.get('server.powered_by.enable') ) { 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 | 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)) + } + } } diff --git a/lib/src/unit/Scaffolding.ts b/lib/src/unit/Scaffolding.ts index 4da3517..469f64d 100644 --- a/lib/src/unit/Scaffolding.ts +++ b/lib/src/unit/Scaffolding.ts @@ -56,9 +56,5 @@ export default class Scaffolding extends LifecycleUnit { public register_factories() { this.logger.verbose('Adding the cache production factory to the container...') 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()) } } diff --git a/lib/src/unit/StaticCanonical.ts b/lib/src/unit/StaticCanonical.ts new file mode 100644 index 0000000..00cf525 --- /dev/null +++ b/lib/src/unit/StaticCanonical.ts @@ -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 extends Canonical> { + public async init_canonical_item(def: CanonicalDefinition): Promise> { + if ( isStaticClass(def.imported.default) ) { + return def.imported.default + } + + throw new InvalidCanonicalExportError(def.original_name) + } +} diff --git a/orm/src/ModelsUnit.ts b/orm/src/ModelsUnit.ts new file mode 100644 index 0000000..f51b377 --- /dev/null +++ b/orm/src/ModelsUnit.ts @@ -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, 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 + } +} diff --git a/orm/src/builder/type/ConnectionExecutable.ts b/orm/src/builder/type/ConnectionExecutable.ts index 161aa8b..6db7144 100644 --- a/orm/src/builder/type/ConnectionExecutable.ts +++ b/orm/src/builder/type/ConnectionExecutable.ts @@ -82,6 +82,10 @@ export default abstract class ConnectionExecutable { return 0 } + async exists(): Promise { + return (await this.count()) > 0 + } + async execute_in_connection(connection: string | Connection): Promise { const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection diff --git a/orm/src/db/PostgresConnection.ts b/orm/src/db/PostgresConnection.ts index 3a1fab2..059588e 100644 --- a/orm/src/db/PostgresConnection.ts +++ b/orm/src/db/PostgresConnection.ts @@ -19,7 +19,7 @@ export default class PostgresConnection extends Connection { const result = await this._client.query(query) 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 base_i += 1 return col diff --git a/orm/src/model/Model.ts b/orm/src/model/Model.ts index 919b967..ef9ff66 100644 --- a/orm/src/model/Model.ts +++ b/orm/src/model/Model.ts @@ -24,7 +24,7 @@ export abstract class Model> extends Builder { * The name of the connection this model should run through. * @type string */ - protected static connection: string + protected static connection: string = 'default' /** * The name of the table this model is stored in. @@ -42,13 +42,13 @@ export abstract class Model> extends Builder { * Optionally, the timestamp field set on creation. * @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. * @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. @@ -569,7 +569,7 @@ export abstract class Model> extends Builder { * @param without_timestamps - if true, the UPDATED_AT/CREATED_AT timestamps will not be touched * @return Promise */ - public async save({ without_timestamps = false }): Promise> { + public async save({ without_timestamps = false } = {}): Promise> { await this.saving$.next(this) const constructor = (this.constructor as typeof Model)