├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .mocharc.json ├── CONTRIBUTING.md ├── Cache ├── Cache.ts ├── Stores │ ├── DatabaseStore.ts │ ├── FileStore.ts │ └── MemoryStore.ts ├── index.ts └── types.ts ├── Container ├── ClassResolver.ts ├── Container.ts ├── Contracts.ts ├── Decorators.ts ├── Metadata.ts ├── Resolver.ts └── index.ts ├── Database ├── Clauses │ └── Where.ts ├── Connection.ts ├── Decorators.ts ├── Drivers │ ├── DatabaseDriver.ts │ ├── MySQL │ │ ├── Alter │ │ │ └── index.ts │ │ ├── ColumnDefinition.ts │ │ ├── Create.ts │ │ ├── MysqlDriver.ts │ │ ├── Schema.ts │ │ └── index.ts │ ├── SQL │ │ ├── Schema.ts │ │ ├── Statements │ │ │ ├── Alter.ts │ │ │ ├── BaseStatement.ts │ │ │ ├── Delete.ts │ │ │ ├── Insert.ts │ │ │ ├── Select.ts │ │ │ └── Update.ts │ │ └── index.ts │ ├── SQLite │ │ ├── Create.ts │ │ ├── Schema.ts │ │ ├── SqliteDriver.ts │ │ └── index.ts │ ├── SchemaContract.ts │ ├── Statement.ts │ └── index.ts ├── Entity.ts ├── EntityNotFoundError.ts ├── EntityProxyHandler.ts ├── EntityQuery.ts ├── Expression.ts ├── Fields.ts ├── Helpers.ts ├── List.ts ├── Migrations │ ├── Migration.ts │ ├── MigrationHistory.ts │ ├── Migrator.ts │ └── index.ts ├── ORM │ └── BaseRelationship.ts ├── Query.ts ├── Seeders │ ├── Seeder.ts │ ├── SeederManager.ts │ └── index.ts ├── StringExpression.ts ├── Types.ts └── index.ts ├── Encryption └── index.ts ├── Forms ├── Decorators.ts ├── Form.ts ├── FormFields.ts └── index.ts ├── Framework ├── App.ts ├── Application.ts ├── Auth │ ├── Auth.ts │ ├── Authenticatable.ts │ ├── User.ts │ └── index.ts ├── Config │ ├── AppConfig.ts │ ├── AuthConfig.ts │ ├── BaseConfig.ts │ ├── CacheConfig.ts │ ├── DatabaseConfig.ts │ ├── MailConfig.ts │ └── index.ts ├── Middleware │ ├── AuthMiddleware.ts │ ├── CorsMiddleware.ts │ ├── StaticAssetsMiddleware.ts │ └── index.ts ├── Provider.ts ├── Providers │ ├── CacheProvider.ts │ ├── DatabaseProvider.ts │ ├── MailProvider.ts │ └── WebsocketsProvider.ts ├── Resolvers │ ├── AuthResolver.ts │ ├── EntityResolver.ts │ └── FormResolver.ts ├── RootDir.ts └── index.ts ├── Frontend ├── Angular │ ├── Readme.md │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json └── Websockets │ ├── Readme.md │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── LICENSE.md ├── Mail ├── MailAddress.ts ├── MailTransport.ts ├── Mailable.ts ├── Mailer.ts ├── PendingMail.ts ├── SentMessage.ts ├── Transports │ ├── MemoryTransport.ts │ └── SendGridTransport.ts └── index.ts ├── Models ├── Model.ts └── index.ts ├── Readme.md ├── Router ├── BaseController.ts ├── Decorators.ts ├── Guard.ts ├── Http │ ├── Contracts.ts │ ├── ErrorHandler.ts │ ├── ErrorHandlerInterface.ts │ ├── Errors │ │ └── HttpError.ts │ ├── Handler.ts │ ├── HttpRoute.ts │ ├── Middleware.ts │ ├── Request.ts │ ├── Response.ts │ └── index.ts ├── Metadata.ts ├── Middleware.ts ├── Request.ts ├── Route.ts ├── RouteNotFoundError.ts ├── Router.ts ├── Servers │ ├── index.ts │ ├── node.ts │ └── uNetworking.ts ├── Websockets │ ├── Handler.ts │ ├── Middleware.ts │ ├── Request.ts │ ├── WebsocketRoute.ts │ ├── index.ts │ └── types.ts └── index.ts ├── Storage ├── File.ts ├── Image.ts ├── InvalidImage.ts ├── Storage.ts ├── index.ts └── utils.ts ├── Support ├── Array.ts ├── Math.ts ├── Metadata.ts ├── String.ts ├── index.ts └── utils.ts ├── Testing └── TestCase.ts ├── Validation ├── Rule.ts ├── RuleInterface.ts ├── Rules │ ├── Email.ts │ ├── FileExtension.ts │ ├── MaxLength.ts │ ├── MinLength.ts │ ├── Optional.ts │ └── Required.ts ├── Validator.ts └── index.ts ├── benchmark.ts ├── package-lock.json ├── package.json ├── tests ├── .nycrc.json ├── Cache │ ├── DatabaseStoreTest.ts │ ├── FileStoreTest.ts │ └── MemoryStoreTest.ts ├── Container │ └── ContainerTest.ts ├── Database │ ├── Drivers │ │ ├── MySQL │ │ │ ├── AlterTest.ts │ │ │ └── CreateTest.ts │ │ ├── MysqlDriverTest.ts │ │ └── SqliteDriverTest.ts │ ├── Entities │ │ ├── Article.ts │ │ ├── Profile.ts │ │ ├── Role.ts │ │ └── User.ts │ ├── EntityRelationshipsTest.ts │ ├── EntityTest.ts │ ├── Migrations │ │ ├── RunnerTest.ts │ │ └── migrations │ │ │ ├── 1.createUserTable.ts │ │ │ └── 2.createArticlesTable.ts │ ├── QueryTest.ts │ ├── SchemaTest.ts │ ├── Seeders │ │ ├── RunnerTest.ts │ │ └── seeders │ │ │ └── RandomSeeder.ts │ └── utils.ts ├── Forms │ └── FormsTest.ts ├── Framework │ ├── Middleware │ │ ├── StaticAssetsMiddlewareTest.ts │ │ ├── articles │ │ │ ├── my-article.html │ │ │ └── some-directory │ │ │ │ └── index.html │ │ └── assets │ │ │ └── styles.css │ ├── Resolvers │ │ └── EntityResolverTest.ts │ └── Router │ │ └── RouterTest.ts ├── Mail │ └── MailerTest.ts ├── Model │ └── ModelTest.ts ├── Router │ ├── HttpHandlerTest.ts │ ├── RequestTest.ts │ └── WebSocketsHandlerTest.ts ├── Storage │ └── StorageTest.ts ├── Support │ ├── ArrayTest.ts │ └── StringTest.ts ├── Validation │ └── RulesTest.ts └── tsconfig.json ├── tsconfig.json ├── tslint.json └── types.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 140 10 | tab_width = 4 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : [ 3 | "eslint:recommended", "plugin:@typescript-eslint/recommended" 4 | ], 5 | "parser" : "@typescript-eslint/parser", 6 | "plugins" : [ 7 | "@typescript-eslint" 8 | ], 9 | "root" : true, 10 | "overrides": [ 11 | { 12 | "files": [ 13 | "./tests/**/*.ts" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/no-unused-vars": "off" 17 | } 18 | } 19 | ], 20 | "rules" : { 21 | "max-len": [ 22 | "warn", 23 | { 24 | "code": 140, 25 | "tabWidth": 4 26 | } 27 | ], 28 | "@typescript-eslint/semi": ["error", "never", { "beforeStatementContinuationChars": "always"}], // doesn't apply to TS interfaces 29 | "@typescript-eslint/no-explicit-any" : "off", 30 | "no-async-promise-executor" : "off", 31 | "@typescript-eslint/ban-ts-comment": "warn", 32 | "@typescript-eslint/no-unused-vars": "warn" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | *.js 4 | !.eslintrc.json 5 | *.js.map 6 | *.ts.map 7 | tsconfig.tsbuildinfo 8 | .nyc_output 9 | build 10 | tests/Storage/.storage 11 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | "ts" 4 | ], 5 | "require": [ 6 | "tsconfig-paths/register", 7 | "ts-node/register", 8 | "source-map-support/register", 9 | "reflect-metadata", 10 | "./Support" 11 | ], 12 | "spec": "tests/**/*Test.ts" 13 | } 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | _In progress_ 2 | 3 | ---- 4 | 5 | Publishing: 6 | 7 | ``` 8 | cp Readme.md dist/Readme.md && cp package.json dist/package.json && cp package-lock.json dist/package-lock.json && cd dist && npm publish 9 | ``` 10 | -------------------------------------------------------------------------------- /Cache/Cache.ts: -------------------------------------------------------------------------------- 1 | export abstract class Cache { 2 | abstract has(name: string): Promise 3 | 4 | abstract get(name: string, defaultValue?: T | (() => T | Promise)): Promise 5 | 6 | abstract remember(name: string, defaultValue?: T | (() => T | Promise), duration?: number): Promise 7 | 8 | abstract set(name: string, data: any, duration?: number): Promise 9 | 10 | abstract delete(name: string): Promise 11 | 12 | abstract flush(): Promise 13 | } 14 | -------------------------------------------------------------------------------- /Cache/Stores/FileStore.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@Typetron/Cache' 2 | import { Storage } from '@Typetron/Storage' 3 | import { createHash } from 'node:crypto' 4 | import path from 'node:path' 5 | import { JSONSafeParse } from '@Typetron/Support/utils' 6 | import { CacheItem } from '@Typetron/Cache/types' 7 | 8 | export class FileStore extends Cache { 9 | constructor(public storage: Storage, public directory: string) { 10 | super() 11 | } 12 | 13 | async has(name: string): Promise { 14 | return await this.storage.exists(this.path(name)) 15 | } 16 | 17 | async get(name: string, defaultValue?: T | (() => T | Promise)): Promise { 18 | const item: CacheItem = await this.storage.read(this.path(name)) 19 | .then(async (data) => JSONSafeParse>(data.toString()) ?? {value: await this.getDefaultValue(defaultValue)}) 20 | .catch(async () => ({value: await this.getDefaultValue(defaultValue)})) 21 | 22 | if (item['date'] && item['date'] < new Date().getTime()) { 23 | await this.delete(name) 24 | return this.getDefaultValue(defaultValue) 25 | } 26 | 27 | return item['value'] 28 | } 29 | 30 | async remember(name: string, defaultValue?: T | (() => T | Promise), durationInSeconds?: number): Promise { 31 | const data = await this.get(name) 32 | 33 | if (data !== undefined) { 34 | return data 35 | } 36 | 37 | const value = await this.getDefaultValue(defaultValue) 38 | await this.set(name, value, durationInSeconds) 39 | return value 40 | } 41 | 42 | async set(name: string, value: any, durationInSeconds?: number): Promise { 43 | await this.storage.put(this.path(name), JSON.stringify({ 44 | value, 45 | date: durationInSeconds ? Date.now() + durationInSeconds * 1000 : undefined 46 | } as CacheItem)) 47 | } 48 | 49 | async delete(name: string): Promise { 50 | await this.storage.delete(this.path(name)) 51 | } 52 | 53 | async flush(): Promise { 54 | await this.storage.deleteDirectory(this.directory) 55 | } 56 | 57 | private path(key: string): string { 58 | const hashKey = createHash('sha1').update(key).digest('hex') 59 | return path.join(this.directory, '/', hashKey) 60 | } 61 | 62 | private async getDefaultValue(defaultValue?: (() => (Promise | T)) | T) { 63 | const valueFunction = typeof defaultValue === 'function' 64 | ? defaultValue as () => Promise | T 65 | : () => defaultValue 66 | 67 | return valueFunction() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Cache/Stores/MemoryStore.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '@Typetron/Cache' 2 | import { CacheItem } from '@Typetron/Cache/types' 3 | 4 | export class MemoryStore extends Cache { 5 | public cache: Map> = new Map() 6 | 7 | async has(name: string): Promise { 8 | const cacheItem = this.cache.get(name) 9 | 10 | if (!cacheItem) { 11 | return false 12 | } 13 | 14 | const currentTime = Date.now() 15 | 16 | if (cacheItem.date && cacheItem.date < currentTime) { 17 | this.cache.delete(name) 18 | return false 19 | } 20 | 21 | return true 22 | } 23 | 24 | async get(name: string, defaultValue?: T | (() => T | Promise)): Promise { 25 | const cacheItem = this.cache.get(name) 26 | 27 | if (!await this.has(name)) { 28 | return this.getDefaultValue(defaultValue) 29 | } 30 | 31 | return cacheItem!.value as T 32 | } 33 | 34 | // return (cacheItem as CacheItem).value as T 35 | async remember(name: string, defaultValue?: T | (() => T | Promise), durationInSeconds?: number): Promise { 36 | const cacheItem = this.cache.get(name) 37 | 38 | if (await this.has(name)) { 39 | return cacheItem!.value as T 40 | } 41 | 42 | const value = await this.getDefaultValue(defaultValue) 43 | await this.set(name, value, durationInSeconds) 44 | return value 45 | } 46 | 47 | async set(name: string, value: any, durationInSeconds?: number): Promise { 48 | const expirationTime = durationInSeconds ? Date.now() + durationInSeconds * 1000 : Infinity 49 | 50 | this.cache.set(name, { 51 | value: value, 52 | date: expirationTime 53 | }) 54 | } 55 | 56 | async delete(name: string): Promise { 57 | this.cache.delete(name) 58 | } 59 | 60 | async flush(): Promise { 61 | this.cache.clear() 62 | } 63 | 64 | private async getDefaultValue(defaultValue?: (() => (Promise | T)) | T) { 65 | const valueFunction = typeof defaultValue === 'function' 66 | ? defaultValue as () => Promise | T 67 | : () => defaultValue 68 | 69 | return valueFunction() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './Cache' 3 | export * from './Stores/MemoryStore' 4 | export * from './Stores/FileStore' 5 | export * from './Stores/DatabaseStore' 6 | -------------------------------------------------------------------------------- /Cache/types.ts: -------------------------------------------------------------------------------- 1 | export interface CacheItem{ 2 | value?: T 3 | date?: number 4 | } 5 | -------------------------------------------------------------------------------- /Container/Contracts.ts: -------------------------------------------------------------------------------- 1 | import { Abstract, Constructor } from '../Support' 2 | 3 | export type ServiceIdentifier = string | Constructor | Abstract | symbol; 4 | -------------------------------------------------------------------------------- /Container/Decorators.ts: -------------------------------------------------------------------------------- 1 | import { ServiceIdentifier } from './Contracts' 2 | import { InjectableMetadata, Scope } from './Metadata' 3 | 4 | export function Injectable(scope: Scope = Scope.SINGLETON) { 5 | return function (target: T) { 6 | const metadata = InjectableMetadata.get(target) 7 | metadata.scope = scope 8 | InjectableMetadata.set(metadata, target) 9 | } 10 | } 11 | 12 | export function Inject(abstract?: ServiceIdentifier) { 13 | return function (target: T, targetKey: string, index?: number) { 14 | const fieldType = Reflect.getMetadata('design:type', target, targetKey) as ServiceIdentifier 15 | const metadata = InjectableMetadata.get(target.constructor) 16 | metadata.dependencies[targetKey] = abstract ?? fieldType 17 | InjectableMetadata.set(metadata, target.constructor) 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Container/Metadata.ts: -------------------------------------------------------------------------------- 1 | import { Resolver } from './Resolver' 2 | import { ServiceIdentifier } from './Contracts' 3 | import { MetadataKey } from '../Support/Metadata' 4 | 5 | export enum Scope { 6 | TRANSIENT = 'TRANSIENT', 7 | SINGLETON = 'SINGLETON', 8 | REQUEST = 'REQUEST', 9 | } 10 | 11 | export class InjectableMetadata extends MetadataKey('injectable') { 12 | resolver?: Resolver 13 | 14 | scope: Scope = Scope.SINGLETON 15 | dependencies: {[key: string]: ServiceIdentifier<{}>} = {} 16 | } 17 | -------------------------------------------------------------------------------- /Container/Resolver.ts: -------------------------------------------------------------------------------- 1 | import { Container } from './Container' 2 | import { ServiceIdentifier } from './Contracts' 3 | 4 | export interface Resolver { 5 | container: Container; 6 | 7 | canResolve(abstract: ServiceIdentifier): boolean; 8 | 9 | resolve(abstract: ServiceIdentifier, parameters: object[]): T | Promise; 10 | 11 | reload(abstract: ServiceIdentifier, concrete: T, container?: Container): T; 12 | } 13 | 14 | export abstract class BaseResolver implements Resolver { 15 | constructor(public container: Container) {} 16 | 17 | abstract canResolve(abstract: ServiceIdentifier): boolean; 18 | 19 | abstract resolve(abstract: ServiceIdentifier, parameters: object[]): T | Promise; 20 | 21 | /** 22 | * Used to update the dependencies that have the REQUEST scope 23 | */ 24 | reload(abstract: ServiceIdentifier, concrete: T, container?: Container): T { 25 | return concrete 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Container/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Decorators' 2 | export * from './Container' 3 | export * from './ClassResolver' 4 | export * from './Resolver' 5 | export * from './Metadata' 6 | -------------------------------------------------------------------------------- /Database/Clauses/Where.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typetron/framework/7459162acf362f0854c218893e8c5eba04816ec5/Database/Clauses/Where.ts -------------------------------------------------------------------------------- /Database/Connection.ts: -------------------------------------------------------------------------------- 1 | import { Query } from './Query' 2 | import { DatabaseDriver } from './Drivers/DatabaseDriver' 3 | import { SqlValue } from './Types' 4 | 5 | export class Connection { 6 | 7 | constructor(public driver: DatabaseDriver) {} 8 | 9 | async run(query: Query): Promise { 10 | return this.driver.run(query.toSQL(), query.getBindings()) 11 | } 12 | 13 | async insertOne(query: Query): Promise { 14 | return this.driver.insertOne(query.toSQL(), query.getBindings()) 15 | } 16 | 17 | truncate(table: string) { 18 | return this.driver.truncate(table) 19 | } 20 | 21 | async get(query: Query): Promise { 22 | return await this.driver.get(query.toSQL(), query.getBindings()) as T[] 23 | } 24 | 25 | async first(query: Query): Promise { 26 | return await this.driver.first(query.toSQL(), query.getBindings()) as T 27 | } 28 | 29 | async getRaw(rawQuery: string, params: SqlValue[] = []): Promise { 30 | return await this.driver.get(rawQuery, params) as T[] 31 | } 32 | 33 | async firstRaw(rawQuery: string, params: SqlValue[] = []): Promise { 34 | return await this.driver.first(rawQuery, params) as T 35 | } 36 | 37 | async runRaw(rawQuery: string, params: SqlValue[] = []): Promise { 38 | return this.driver.run(rawQuery, params) 39 | } 40 | 41 | async tables() { 42 | return this.driver.tables() 43 | } 44 | 45 | async tableExists(table: string) { 46 | return this.driver.tableExists(table) 47 | } 48 | 49 | async tableColumns(table: string) { 50 | return this.driver.tableColumns(table) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Database/Drivers/DatabaseDriver.ts: -------------------------------------------------------------------------------- 1 | import { SqlValue } from '../Types' 2 | import { Statement } from './Statement' 3 | import { Constructor } from '@Typetron/Support' 4 | import { SchemaContract } from './SchemaContract' 5 | import { ColumnDefinitionOptions } from '@Typetron/Database/Drivers/SQL' 6 | 7 | interface StatementsList { 8 | create: Constructor, 9 | select: Constructor, 10 | insert: Constructor, 11 | delete: Constructor, 12 | update: Constructor, 13 | alter: Constructor, 14 | } 15 | 16 | export abstract class DatabaseDriver { 17 | 18 | abstract statements: StatementsList 19 | 20 | abstract schema: SchemaContract 21 | 22 | abstract run(query: string, params?: SqlValue[]): Promise 23 | 24 | abstract truncate(table: string): Promise 25 | 26 | abstract insertOne(query: string, params: SqlValue[]): Promise 27 | 28 | abstract get(query: string, params: SqlValue[]): Promise 29 | 30 | abstract first(query: string, params: SqlValue[]): Promise 31 | 32 | abstract tables(): Promise<{name: string}[]> 33 | 34 | abstract tableExists(table: string): Promise 35 | 36 | abstract tableColumns(table: string): Promise 37 | } 38 | -------------------------------------------------------------------------------- /Database/Drivers/MySQL/Alter/index.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitionOptions } from '../../../Drivers/SQL' 2 | import { wrap } from '../../../Helpers' 3 | import { ColumnDefinition } from '@Typetron/Database/Drivers/MySQL/ColumnDefinition' 4 | import { Expression } from '@Typetron/Database' 5 | 6 | export enum Constraints { 7 | PrimaryKey = 'PRIMARY KEY', 8 | ForeignKey = 'FOREIGN KEY', 9 | } 10 | 11 | export abstract class AlterOption extends Expression { 12 | constructor(public column: ColumnDefinitionOptions) {super()} 13 | 14 | abstract toSQL(): string 15 | } 16 | 17 | export class AddColumn extends AlterOption { 18 | toSQL(): string { 19 | return `ADD ${new ColumnDefinition(this.column).toSQL()}` 20 | } 21 | } 22 | 23 | export class AddConstraint extends AlterOption { 24 | 25 | constructor(column: ColumnDefinitionOptions, public type: Constraints) {super(column)} 26 | 27 | toSQL(): string { 28 | return `ADD CONSTRAINT ${this.type} (${this.column.name})` 29 | } 30 | } 31 | 32 | export class ModifyColumn extends AlterOption { 33 | toSQL(): string { 34 | return `MODIFY ${new ColumnDefinition(this.column).toSQL()}` 35 | } 36 | } 37 | 38 | export class DropColumn extends Expression { 39 | constructor(public column: string) {super()} 40 | 41 | toSQL(): string { 42 | return `DROP COLUMN ${wrap(this.column)}` 43 | } 44 | } 45 | 46 | export class Alter extends Expression { 47 | constructor(public table: string, public alterOptions: Expression[]) {super()} 48 | 49 | toSQL() { 50 | return ` 51 | ALTER TABLE ${wrap(this.table)} 52 | ${this.alterOptions.map(option => option.toSQL()).join(', \n')} 53 | ` 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Database/Drivers/MySQL/ColumnDefinition.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitionOptions } from '../../Drivers/SQL' 2 | import { wrap } from '../../Helpers' 3 | 4 | export class ColumnDefinition { 5 | constructor(public options: ColumnDefinitionOptions) {} 6 | 7 | toSQL() { 8 | const sqlParts = [wrap(this.options.name), this.options.type] 9 | 10 | if (this.options.autoIncrement) { 11 | sqlParts.push('AUTO_INCREMENT') 12 | } 13 | 14 | return sqlParts.join(' ') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Database/Drivers/MySQL/Create.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from '../SQL/Statements/BaseStatement' 2 | import { ColumnDefinitionOptions } from '../SQL' 3 | import { wrap } from '../../Helpers' 4 | 5 | export class Create extends BaseStatement { 6 | 7 | constructor(table: string, public columns: ColumnDefinitionOptions[] = []) { 8 | super({ 9 | table, 10 | columns: [], 11 | joins: [], 12 | wheres: [], 13 | groups: [], 14 | orders: [], 15 | having: [] 16 | }) 17 | } 18 | 19 | toSQL() { 20 | const definitions: string[] = this.getColumnsSQL(this.columns) 21 | 22 | this.columns.where('primaryKey').whenNotEmpty(primaryColumns => { 23 | definitions.push(`PRIMARY KEY (${primaryColumns.pluck('name').join(', ')})`) 24 | }) 25 | 26 | return ` 27 | CREATE TABLE ${this.table} 28 | ( 29 | ${definitions.join(', \n')} 30 | ) 31 | ` 32 | } 33 | 34 | private getColumnsSQL(columns: ColumnDefinitionOptions[]) { 35 | return columns.map(this.getColumnSQL) 36 | } 37 | 38 | private getColumnSQL(column: ColumnDefinitionOptions) { 39 | const sqlParts = [wrap(column.name), column.type] 40 | 41 | if (column.autoIncrement) { 42 | sqlParts.push('AUTO_INCREMENT') 43 | } 44 | 45 | return sqlParts.join(' ') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Database/Drivers/MySQL/Schema.ts: -------------------------------------------------------------------------------- 1 | import { ColumnField, Entity, Expression, PrimaryField } from '../..' 2 | import { Schema as BaseSchema } from '../SQL/Schema' 3 | import { Create } from './Create' 4 | import { typesMatches } from '.' 5 | import { ColumnDefinitionOptions } from '../SQL' 6 | import { AddColumn, AddConstraint, Alter, Constraints, DropColumn, ModifyColumn } from './Alter' 7 | 8 | export class Schema extends BaseSchema { 9 | 10 | columnMetadataToColumnDefinition(metadata: ColumnField) { 11 | return { 12 | name: metadata.column, 13 | primaryKey: metadata.constructor === PrimaryField, 14 | autoIncrement: Number(metadata.constructor === PrimaryField), 15 | type: this.getColumnTypeBasedOnTypescriptType(metadata), 16 | default: undefined, 17 | nullable: true 18 | } 19 | } 20 | 21 | async create(table: string, columns: ColumnField[]) { 22 | const columnsDefinitions: ColumnDefinitionOptions[] = columns.map(column => this.columnMetadataToColumnDefinition(column)) 23 | const createStatement = new Create(table, columnsDefinitions) 24 | 25 | await this.driver.run(createStatement.toSQL()) 26 | } 27 | 28 | protected async syncTableColumns(table: string, columnsMetadata: ColumnField[]) { 29 | const columns = columnsMetadata.map(metadata => this.columnMetadataToColumnDefinition(metadata)) 30 | 31 | const tableColumns = await this.driver.tableColumns(table) 32 | 33 | let alters: Expression[] = [] 34 | 35 | alters = alters.concat( 36 | columns 37 | .whereNotIn('name', tableColumns.pluck('name')) 38 | .map(column => new AddColumn({...column, autoIncrement: undefined})) 39 | ) 40 | 41 | columns.whereIn('name', tableColumns.pluck('name')) 42 | .forEach(column => { 43 | const typeInDB = tableColumns.findWhere('name', column.name)?.type 44 | if (typeInDB !== column.type) { 45 | alters.push(new ModifyColumn(column)) 46 | } 47 | }) 48 | 49 | alters = alters.concat( 50 | tableColumns 51 | .whereNotIn('name', columns.pluck('name')) 52 | .map(tableColumn => new DropColumn(tableColumn.name)) 53 | ) 54 | 55 | await this.driver.run(new Alter(table, alters).toSQL()) 56 | 57 | const additionalAlters: Expression[] = [] 58 | columns.forEach(column => { 59 | const dbInfo = tableColumns.findWhere('name', column.name) 60 | if (column.primaryKey && !dbInfo?.primaryKey) { 61 | additionalAlters.push(new AddConstraint(column, Constraints.PrimaryKey)) 62 | } 63 | if (column.autoIncrement && !dbInfo?.autoIncrement) { 64 | additionalAlters.push(new ModifyColumn(column)) 65 | } 66 | }) 67 | 68 | if (!additionalAlters.empty()) { 69 | await this.driver.run(new Alter(table, additionalAlters).toSQL()) 70 | } 71 | } 72 | 73 | private getColumnTypeBasedOnTypescriptType(column: ColumnField) { 74 | const typePrototype = Array.from(typesMatches.keys()) 75 | .find(key => column.type().prototype instanceof key) || String 76 | 77 | return typesMatches.get(column.type()) 78 | ?? typesMatches.get(column.constructor) 79 | ?? typesMatches.get(typePrototype) 80 | ?? typesMatches.get(String) as string 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Database/Drivers/MySQL/index.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../Entity' 2 | import { JSONField, PrimaryField } from '../../Fields' 3 | 4 | // tslint:disable-next-line:no-any 5 | export const typesMatches = new Map([ 6 | [Entity, 'integer'], 7 | [PrimaryField, `integer`], 8 | [Number, 'integer'], 9 | [String, 'varchar(255)'], 10 | [Date, 'datetime'], 11 | [JSONField, 'text'], 12 | [Boolean, 'integer'], 13 | ]) 14 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Schema.ts: -------------------------------------------------------------------------------- 1 | import { SchemaContract } from '../SchemaContract' 2 | import { BelongsToManyField, ColumnField, Entity, EntityConstructor, EntityMetadata } from '../..' 3 | 4 | export abstract class Schema extends SchemaContract { 5 | 6 | // async create(table: string, columns: ColumnField[]) { 7 | // const columnsSQLs = columns 8 | // .filter(column => column.column) 9 | // .map(columnMetadata => this.getColumnSql(columnMetadata)) 10 | // 11 | // // @ts-ignore 12 | // const createStatement = new Create({ 13 | // table, 14 | // columns: columnsSQLs 15 | // }) 16 | // 17 | // await this.driver.run(createStatement.toSql()) 18 | // } 19 | 20 | // async addColumn(table: string, column: ColumnField) { 21 | // await this.driver.run(`ALTER TABLE ${table} 22 | // ADD ${this.getColumnSql(column)}(6)`) 23 | // } 24 | 25 | async synchronize(entitiesMetadata: EntityMetadata[]) { 26 | const pivotTables = new Map[]>() 27 | for await(const metadata of entitiesMetadata) { 28 | const table = metadata.table as string 29 | const belongsToManyFields = Object.values(metadata.inverseRelationships) 30 | .filter(field => field instanceof BelongsToManyField) as BelongsToManyField[] 31 | 32 | belongsToManyFields.forEach(field => { 33 | const entity = Entity as EntityConstructor 34 | const pivotTableColumns: ColumnField[] = [ 35 | // new ColumnField(entity, 'id', () => PrimaryField, 'id'), 36 | ...[field.getParentForeignKey(), field.getRelatedForeignKey()].sort().map(columnName => { 37 | return new ColumnField(entity, columnName, () => Number, columnName) 38 | }) 39 | ] 40 | pivotTables.set(field.getPivotTable(), pivotTableColumns) 41 | }) 42 | const tableInfo = await this.driver.tableExists(table) 43 | if (!tableInfo) { 44 | await this.create(table, Object.values({...metadata.columns, ...metadata.relationships})) 45 | continue 46 | } 47 | await this.syncTableColumns(table, Object.values({...metadata.columns, ...metadata.relationships})) 48 | } 49 | 50 | for await (const [name, columns] of pivotTables) { 51 | const tableInfo = await this.driver.tableExists(name) 52 | if (!tableInfo) { 53 | await this.create(name, columns) 54 | continue 55 | } 56 | await this.syncTableColumns(name, columns) 57 | } 58 | } 59 | 60 | protected abstract syncTableColumns(table: string, columns: ColumnField[]): Promise; 61 | 62 | // protected getColumnSql(columnMetadata: ColumnField): string { 63 | // const columnType = columnMetadata.type() 64 | // const type = Array.from(this.typesMatches.keys()) 65 | // .find(key => 66 | // key === columnMetadata.type() || columnType.prototype instanceof key || columnMetadata instanceof key 67 | // ) || String 68 | // 69 | // const columnInfo = this.typesMatches.get(type) 70 | // 71 | // return `${columnMetadata.column} ${columnInfo}` 72 | // } 73 | } 74 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Statements/Alter.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from './BaseStatement' 2 | import { ColumnField, Entity } from '../../..' 3 | 4 | export class Alter extends BaseStatement { 5 | 6 | columns: ColumnField[] = [] 7 | 8 | getColumns() { 9 | return '' 10 | } 11 | 12 | toSQL() { 13 | return ` 14 | ALTER 15 | TABLE 16 | ${this.table} 17 | ( 18 | ${this.getColumns()} 19 | ) 20 | ` 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Statements/BaseStatement.ts: -------------------------------------------------------------------------------- 1 | import { wrap } from '../../../Helpers' 2 | import { Statement } from '../../Statement' 3 | 4 | export abstract class BaseStatement extends Statement { 5 | 6 | get table() { 7 | return wrap(this.components.table || '') 8 | } 9 | 10 | get wheres() { 11 | const wheres = this.components.wheres || [] 12 | 13 | return wheres.map(where => { 14 | this.bindings = this.bindings.concat(where.getValues()) 15 | return where.toSql() 16 | }).join(' ').replace(/^(and |or )/i, '') 17 | } 18 | 19 | get limit() { 20 | const limit = this.components.limit 21 | return limit ? `LIMIT ${limit.from}` + (limit.count ? `, ${limit.count}` : '') : '' 22 | } 23 | 24 | get joins() { 25 | return this.components.joins.map(join => { 26 | return `${join.type} JOIN ${wrap(join.table)} ON ${join.first} ${join.operator} ${join.second}` 27 | }).join(' ') 28 | } 29 | 30 | get groups() { 31 | if (!this.components.groups.length) { 32 | return '' 33 | } 34 | 35 | return `GROUP BY ` + this.components.groups.map(column => wrap(column)).join(', ') 36 | } 37 | 38 | get having() { 39 | return '' 40 | } 41 | 42 | get orders() { 43 | if (!this.components.orders?.length) { 44 | return '' 45 | } 46 | 47 | return 'ORDER BY ' + this.components.orders.map(order => `${wrap(order[0])} ${order[1]}`).join(', ') 48 | } 49 | 50 | abstract toSQL(): string; 51 | 52 | toString() { 53 | return this.toSQL() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Statements/Delete.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from './BaseStatement' 2 | 3 | export class Delete extends BaseStatement { 4 | 5 | get wheres() { 6 | if (!this.components.wheres.length) { 7 | return '' 8 | } 9 | 10 | return `WHERE ` + super.wheres 11 | } 12 | 13 | toSQL() { 14 | return ` 15 | DELETE 16 | FROM ${this.table} ${this.wheres} ${this.orders} ${this.limit} 17 | ` 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Statements/Insert.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from './BaseStatement' 2 | import { wrap } from '../../../Helpers' 3 | 4 | export class Insert extends BaseStatement { 5 | 6 | get columns() { 7 | const values = this.components.insert || [] 8 | 9 | return wrap(Object.keys(values.first() ?? {})) 10 | } 11 | 12 | get values() { 13 | return (this.components.insert || []).map(valuesMap => { 14 | const values = Object.values(valuesMap) 15 | this.bindings = this.bindings.concat(values) 16 | return '(' + [...Array(values.length)].map(() => '?').join(', ') + ')' 17 | }).join(', ') 18 | } 19 | 20 | toSQL() { 21 | return `INSERT INTO ${this.table} (${this.columns}) 22 | VALUES ${this.values} ` 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Statements/Select.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from './BaseStatement' 2 | import { StringExpression } from '../../..' 3 | import { wrap } from '../../../Helpers' 4 | 5 | export class Select extends BaseStatement { 6 | get distinct() { 7 | return this.components.distinct ? 'DISTINCT ' : '' 8 | } 9 | 10 | get columns() { 11 | let columns = this.components.columns || [] 12 | columns = columns.map(column => { 13 | return column instanceof StringExpression ? column.value : wrap(column) 14 | }) 15 | 16 | const aggregate = this.components.aggregate 17 | if (aggregate) { 18 | columns.push(`${aggregate.function}(${wrap(aggregate.columns) || '*'}) as aggregate`) 19 | } 20 | 21 | if (!columns.length) { 22 | return '*' 23 | } 24 | 25 | return columns.join(', ') 26 | } 27 | 28 | get wheres() { 29 | if (!this.components.wheres.length) { 30 | return '' 31 | } 32 | 33 | return `WHERE ${super.wheres}` 34 | } 35 | 36 | toSQL() { 37 | return ` 38 | SELECT ${this.distinct}${this.columns} 39 | FROM ${this.table} ${this.joins} ${this.wheres} ${this.groups} 40 | ${this.having} 41 | ${this.orders} 42 | ${this.limit} 43 | ` 44 | } 45 | } 46 | 47 | /* 48 | const query = `SELECT * 49 | FROM users 50 | where name = 'john' -- where basic 51 | or (age in (1, 2, 3) and age > 2) -- where nested 52 | or age in (1, 2, 3) -- where in 53 | or age in (SELECT age from users) -- where subSelect 54 | `; 55 | */ 56 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/Statements/Update.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from './BaseStatement' 2 | 3 | export class Update extends BaseStatement { 4 | 5 | get wheres() { 6 | if (!this.components.wheres.length) { 7 | return '' 8 | } 9 | 10 | return 'WHERE ' + super.wheres 11 | } 12 | 13 | get columns() { 14 | const columns = [] 15 | const values = this.components.update || {} 16 | for (const column in values) { 17 | if (values.hasOwnProperty(column)) { 18 | this.bindings.push(values[column]) 19 | columns.push(`${column} = ?`) 20 | } 21 | } 22 | return columns.join(', ') 23 | } 24 | 25 | toSQL() { 26 | return ` 27 | UPDATE 28 | ${this.table} 29 | SET ${this.columns} 30 | ${this.wheres} ${this.orders} ${this.limit} 31 | ` 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Database/Drivers/SQL/index.ts: -------------------------------------------------------------------------------- 1 | export interface ColumnDefinitionOptions { 2 | name: string 3 | type: string 4 | nullable?: boolean 5 | default?: string | number 6 | autoIncrement?: number 7 | primaryKey?: boolean 8 | } 9 | -------------------------------------------------------------------------------- /Database/Drivers/SQLite/Create.ts: -------------------------------------------------------------------------------- 1 | import { BaseStatement } from '../SQL/Statements/BaseStatement' 2 | import { ColumnDefinitionOptions } from '../SQL' 3 | 4 | export class Create extends BaseStatement { 5 | 6 | constructor(table: string, public columns: ColumnDefinitionOptions[] = []) { 7 | super({ 8 | table, 9 | columns: [], 10 | joins: [], 11 | wheres: [], 12 | groups: [], 13 | orders: [], 14 | having: [] 15 | }) 16 | } 17 | 18 | toSQL() { 19 | const definitions: string[] = this.getColumnsSQL(this.columns) 20 | 21 | return ` 22 | CREATE TABLE ${this.table} 23 | ( 24 | ${definitions.join(', \n')} 25 | ) 26 | ` 27 | } 28 | 29 | private getColumnsSQL(columns: ColumnDefinitionOptions[]) { 30 | return columns.map(this.getColumnSQL) 31 | } 32 | 33 | private getColumnSQL(column: ColumnDefinitionOptions) { 34 | const sqlParts = [column.name, column.type] 35 | 36 | if (column.primaryKey) { 37 | sqlParts.push('PRIMARY KEY') 38 | } 39 | if (column.autoIncrement) { 40 | sqlParts.push('AUTOINCREMENT') 41 | } 42 | 43 | return sqlParts.join(' ') 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Database/Drivers/SQLite/Schema.ts: -------------------------------------------------------------------------------- 1 | import { ColumnField, Entity, PrimaryField } from '../..' 2 | import { Schema as BaseSchema } from '../SQL/Schema' 3 | import { Create } from './Create' 4 | import { typesMatches } from '.' 5 | import { ColumnDefinitionOptions } from '../SQL' 6 | import { wrap } from '../../Helpers' 7 | 8 | export class Schema extends BaseSchema { 9 | 10 | columnMetadataToColumnDefinition(metadata: ColumnField) { 11 | return { 12 | name: metadata.column, 13 | primaryKey: metadata.constructor === PrimaryField, 14 | autoIncrement: Number(metadata.constructor === PrimaryField), 15 | type: this.getColumnTypeBasedOnTypescriptType(metadata), 16 | default: undefined, 17 | nullable: true 18 | } 19 | } 20 | 21 | async create(table: string, columns: ColumnField[]) { 22 | const columnsDefinitions: ColumnDefinitionOptions[] = columns.map(column => this.columnMetadataToColumnDefinition(column)) 23 | const createStatement = new Create(table, columnsDefinitions) 24 | 25 | await this.driver.run(createStatement.toSQL()) 26 | } 27 | 28 | protected async syncTableColumns(table: string, columnsMetadata: ColumnField[]) { 29 | const columns = columnsMetadata.map(metadata => this.columnMetadataToColumnDefinition(metadata)) 30 | const tableColumns = await this.driver.tableColumns(table) 31 | 32 | if (tableColumns.length) { 33 | const temporaryTableName = table + '_alter_tmp' 34 | await this.create(temporaryTableName, columnsMetadata) 35 | const columnList = wrap(columns.whereIn('name', tableColumns.pluck('name')).pluck('name')) 36 | await this.driver.run(`INSERT INTO ${temporaryTableName}(${columnList}) 37 | SELECT ${columnList} 38 | FROM ${table}`) 39 | await this.driver.run(`DROP TABLE ${table}`) 40 | await this.driver.run(`ALTER TABLE ${temporaryTableName} RENAME TO ${table}`) 41 | } 42 | } 43 | 44 | private getColumnTypeBasedOnTypescriptType(column: ColumnField) { 45 | const typePrototype = Array.from(typesMatches.keys()) 46 | .find(key => column.type().prototype instanceof key) || String 47 | 48 | return typesMatches.get(column.type()) 49 | ?? typesMatches.get(column.constructor) 50 | ?? typesMatches.get(typePrototype) 51 | ?? typesMatches.get(String) as string 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Database/Drivers/SQLite/index.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../../Entity' 2 | import { JSONField, PrimaryField } from '../../Fields' 3 | 4 | export * from './SqliteDriver' 5 | 6 | // tslint:disable-next-line:no-any 7 | export const typesMatches = new Map([ 8 | [Entity, 'integer'], 9 | [PrimaryField, `integer`], 10 | [Number, 'integer'], 11 | [String, 'varchar(255)'], 12 | [Date, 'datetime'], 13 | [JSONField, 'text'], 14 | [Boolean, 'integer'], 15 | ]) 16 | -------------------------------------------------------------------------------- /Database/Drivers/SchemaContract.ts: -------------------------------------------------------------------------------- 1 | import { ColumnField, Entity, EntityMetadata } from '..' 2 | import { DatabaseDriver } from './DatabaseDriver' 3 | 4 | export abstract class SchemaContract { 5 | constructor(public driver: DatabaseDriver) {} 6 | 7 | abstract create(table: string, columns: ColumnField[]): Promise 8 | 9 | abstract synchronize(entitiesMetadata: EntityMetadata[]): Promise 10 | 11 | } 12 | -------------------------------------------------------------------------------- /Database/Drivers/Statement.ts: -------------------------------------------------------------------------------- 1 | import { Components, SqlValue } from '../Types' 2 | import { StringExpression } from '../StringExpression' 3 | 4 | export abstract class Statement extends StringExpression { 5 | bindings: SqlValue[] = [] 6 | 7 | constructor(public components: Components) { 8 | super('') 9 | } 10 | 11 | abstract toSQL(): string; 12 | 13 | toString() { 14 | return this.toSQL() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Database/Drivers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SQLite/SqliteDriver' 2 | export * from './MySQL/MysqlDriver' 3 | export * from './Statement' 4 | export * from './SchemaContract' 5 | -------------------------------------------------------------------------------- /Database/EntityNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from './Entity' 2 | 3 | export class EntityNotFoundError extends Error { 4 | constructor(public entity: typeof Entity, message: string) {super(message)} 5 | } 6 | -------------------------------------------------------------------------------- /Database/EntityProxyHandler.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from './Entity' 2 | import { EntityQuery } from './EntityQuery' 3 | import { EntityConstructor } from './index' 4 | 5 | export class EntityProxyHandler { 6 | 7 | set(target: T, property: string, value: T[K]) { 8 | target[property as keyof T] = value 9 | return true 10 | } 11 | 12 | get(target: T, property: string) { 13 | if (!(property in target)) { 14 | const targetConstructor = target.constructor as EntityConstructor 15 | const query = new EntityQuery(targetConstructor) 16 | const queryProperty = query[property as keyof EntityQuery] as Function 17 | if (typeof queryProperty === 'function') { 18 | // @ts-ignore 19 | query.table(target.constructor.getTable()) 20 | return (...args: object[]) => { 21 | return queryProperty.apply(query, args) 22 | } 23 | } 24 | } 25 | 26 | return target[property as keyof T] 27 | // if (typeof value === 'function') { 28 | // return value.bind(target); 29 | // } 30 | // return value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Database/Expression.ts: -------------------------------------------------------------------------------- 1 | export abstract class Expression { 2 | abstract toSQL(): string 3 | } 4 | -------------------------------------------------------------------------------- /Database/Helpers.ts: -------------------------------------------------------------------------------- 1 | export function wrap(strings: string | string[]) { 2 | if (typeof strings === 'string') { 3 | strings = [strings] 4 | } 5 | return strings.map(column => { 6 | // // Only add backticks for columns that contain characters outside the following set 7 | // if (/^[0-9,a-z,A-Z$_]+$/.test(column)) { 8 | // return column 9 | // } 10 | return '`' + column + '`' 11 | }).join(', ') 12 | } 13 | -------------------------------------------------------------------------------- /Database/List.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from './Entity' 2 | import { EntityKeys } from './index' 3 | import { EntityQuery } from './EntityQuery' 4 | import { BooleanOperator, Operator, WhereValue } from './Types' 5 | import { BelongsToManyField, HasManyField } from './Fields' 6 | import { BaseRelationship as Relationship } from './ORM/BaseRelationship' 7 | 8 | export abstract class List extends Relationship implements Iterable, ArrayLike { 9 | 10 | items: E[] = [] 11 | 12 | public relationship: HasManyField | BelongsToManyField 13 | 14 | protected constructor( 15 | relationship: HasManyField | BelongsToManyField, 16 | parent: P 17 | ) { 18 | super(relationship, parent) 19 | return new Proxy(this, new ListProxyHandler()) 20 | } 21 | 22 | get length() { 23 | return this.items.length 24 | } 25 | 26 | [Symbol.iterator]() { 27 | return this.items[Symbol.iterator]() 28 | } 29 | 30 | readonly [n: number]: E; 31 | 32 | async load() { 33 | await this.parent.load(this.relationship.property) 34 | return this 35 | } 36 | 37 | async get() { 38 | await this.load() 39 | return this.items 40 | } 41 | 42 | newQuery(): EntityQuery { 43 | return this.relationship.getQuery(this.parent) 44 | } 45 | 46 | where>( 47 | column: EntityKeys, 48 | operator: Operator | WhereValue | E[K], 49 | value?: WhereValue | E[K], 50 | boolean?: BooleanOperator 51 | ): EntityQuery { 52 | return this.relationship.getQuery(this.parent).where(column, operator, value, boolean) 53 | } 54 | 55 | findWhere(name: string, value: string): E | undefined { 56 | return undefined 57 | } 58 | 59 | toJSON() { 60 | return this.items 61 | } 62 | } 63 | 64 | export class ListProxyHandler { 65 | constructor() { 66 | } 67 | 68 | get(target: List, property: string | number) { 69 | if (Number.isInteger(Number(property.toString()))) { 70 | return target.items[property as number] 71 | } 72 | return target[property as keyof List] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Database/Migrations/Migration.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '../Connection' 2 | 3 | export abstract class Migration { 4 | 5 | constructor(protected connection: Connection) {} 6 | 7 | abstract up(): void | Promise; 8 | 9 | abstract down(): void | Promise; 10 | } 11 | -------------------------------------------------------------------------------- /Database/Migrations/MigrationHistory.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreatedAt, ID, Options, PrimaryColumn } from '../Decorators' 2 | import { Entity } from '../Entity' 3 | 4 | @Options({ 5 | table: 'migration_history' 6 | }) 7 | export class MigrationHistory extends Entity { 8 | 9 | @PrimaryColumn() 10 | id: ID 11 | 12 | @Column() 13 | name: string 14 | 15 | @Column() 16 | batch: number 17 | 18 | @CreatedAt() 19 | createdAt: Date 20 | } 21 | -------------------------------------------------------------------------------- /Database/Migrations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Migrator' 2 | export * from './Migration' 3 | export * from './MigrationHistory' 4 | -------------------------------------------------------------------------------- /Database/ORM/BaseRelationship.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from '../Entity' 2 | import { InverseRelationship, RelationshipField } from '../Fields' 3 | 4 | export abstract class BaseRelationship { 5 | constructor( 6 | public relationship: RelationshipField | InverseRelationship, 7 | public parent: P 8 | ) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Database/Seeders/Seeder.ts: -------------------------------------------------------------------------------- 1 | export abstract class Seeder { 2 | public abstract run():void 3 | } -------------------------------------------------------------------------------- /Database/Seeders/SeederManager.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from "@Typetron/Storage" 2 | import { Constructor } from "@Typetron/Support" 3 | import { Seeder } from "./Seeder" 4 | 5 | export class SeederManager { 6 | constructor( 7 | public storage: Storage, 8 | public directory: string 9 | ) {} 10 | 11 | async seed() { 12 | const files = await this.storage 13 | .files(this.directory, true) 14 | .where("extension", "ts") 15 | .whenEmpty(() => console.log('Nothing to seed!')) 16 | 17 | files.forEachAsync(async file => { 18 | await this.getSeed(file.path).run() 19 | }) 20 | } 21 | 22 | private getSeed(path: string): Seeder { 23 | const module: Record> = require(path) 24 | const Class = Object.values(module)[0] 25 | return new Class() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Database/Seeders/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Seeder' 2 | export * from './SeederManager' -------------------------------------------------------------------------------- /Database/StringExpression.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '@Typetron/Database/Expression' 2 | 3 | export class StringExpression extends Expression { 4 | constructor(public value: string) {super()} 5 | 6 | toSQL() { 7 | return this.value 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Database/index.ts: -------------------------------------------------------------------------------- 1 | import { ChildKeys, ChildObject, Constructor } from '@Typetron/Support' 2 | import { Entity } from './Entity' 3 | import { BaseRelationship } from './ORM/BaseRelationship' 4 | 5 | export type EntityKeys = ChildKeys 6 | export type EntityColumns = { [P in EntityKeys]: T[P] extends Function ? never : P }[ EntityKeys] 7 | 8 | export type EntityObject = ChildObject<{ 9 | [P in keyof T]: T[P] extends BaseRelationship ? U | number : T[P] 10 | }, Entity> 11 | export type EntityConstructor = typeof Entity & Constructor 12 | // export type EntityConstructor = {[key in keyof typeof Entity]: (typeof Entity)[key]} & Constructor; 13 | export type DotNotationProperties = string 14 | 15 | export * from './Query' 16 | export * from './Entity' 17 | export * from './Decorators' 18 | export * from './StringExpression' 19 | export * from './Expression' 20 | export * from './Drivers' 21 | export * from './Fields' 22 | export * from './Connection' 23 | 24 | -------------------------------------------------------------------------------- /Encryption/index.ts: -------------------------------------------------------------------------------- 1 | import { compare, hash } from 'bcryptjs' 2 | import * as jwt from 'jsonwebtoken' 3 | import { GetPublicKeyOrSecret, Secret, SignOptions, VerifyErrors, VerifyOptions } from 'jsonwebtoken' 4 | 5 | export class Crypt { 6 | hash(value: string, saltRounds: number): Promise { 7 | return hash(value, saltRounds) 8 | } 9 | 10 | compare(value: string, encrypted: string): Promise { 11 | return compare(value, encrypted) 12 | } 13 | } 14 | 15 | export interface JWToken { 16 | sub: T 17 | iat: number 18 | exp: number 19 | } 20 | 21 | export class JWT { 22 | sign(payload: string | Buffer | object, secretOrPrivateKey: Secret, options?: SignOptions) { 23 | return new Promise((resolve, reject) => { 24 | jwt.sign( 25 | payload, 26 | secretOrPrivateKey, 27 | options ?? {}, 28 | (error: Error | null, encoded?: string) => { 29 | error ? reject(error) : resolve(encoded as string) 30 | } 31 | ) 32 | }) 33 | } 34 | 35 | verify(token: string, secretOrPrivateKey: Secret | GetPublicKeyOrSecret, options?: VerifyOptions) { 36 | return new Promise>((resolve, reject) => { 37 | jwt.verify( 38 | token, 39 | secretOrPrivateKey, 40 | { 41 | ...options, 42 | complete: true 43 | }, 44 | (error: VerifyErrors | null, decoded) => { 45 | error ? reject(error) : resolve(decoded?.payload as any as JWToken) 46 | } 47 | ) 48 | }) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Forms/Decorators.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface } from '../Validation' 2 | import { Type } from '../Support' 3 | import { FormField } from './FormFields' 4 | import { Form } from './Form' 5 | 6 | export const FormMetadataKey = 'form:fields' 7 | 8 | export function Field(name?: string) { 9 | return function(target: T, property: string) { 10 | const fields: {[key: string]: FormField} = Reflect.getMetadata(FormMetadataKey, target.constructor) || {} 11 | const type = Reflect.getMetadata('design:type', target, property) 12 | const field = fields[property] || new FormField(name || property, type) 13 | field.name = name || property 14 | fields[property] = field 15 | Reflect.defineMetadata(FormMetadataKey, fields, target.constructor) 16 | } 17 | } 18 | 19 | // tslint:disable-next-line:no-any 20 | export function Rules(...rules: (Type | ((...args: any[]) => Type))[]) { 21 | return function(target: Object, property: string) { 22 | const fields: {[key: string]: FormField} = Reflect.getMetadata(FormMetadataKey, target.constructor) || {} 23 | const type = Reflect.getMetadata('design:type', target, property) 24 | const field = fields[property] || new FormField(property, type) 25 | field.rules = rules 26 | fields[property] = field 27 | Reflect.defineMetadata(FormMetadataKey, fields, target.constructor) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Forms/Form.ts: -------------------------------------------------------------------------------- 1 | import { FormMetadataKey } from './Decorators' 2 | import { RuleValue } from '../Validation' 3 | import { ChildKeys, ChildObject, Constructor } from '../Support' 4 | import { FormField } from './FormFields' 5 | import { Injectable, Scope } from '../Container' 6 | 7 | export type FormFields = ChildObject; 8 | 9 | @Injectable(Scope.TRANSIENT) 10 | export abstract class Form { 11 | 12 | readonly errors: {[key: string]: Record} = {} 13 | 14 | static fields(this: Constructor): Record, FormField> { 15 | return Reflect.getMetadata(FormMetadataKey, this) 16 | } 17 | 18 | fields() { 19 | return (this.constructor as (Constructor
& typeof Form)).fields() 20 | } 21 | 22 | valid() { 23 | const fields = Object.values(this.fields()) as FormField[] 24 | fields.forEach(field => { 25 | const errors = field.validate(this[field.name as keyof FormFields]) 26 | if (errors) { 27 | this.errors[field.name] = errors 28 | } 29 | }) 30 | 31 | return !Object.keys(this.errors).length 32 | } 33 | 34 | validated() { 35 | const fields = Object.values(this.fields()) as FormField[] 36 | return fields.filter(field => !this.errors[field.name]) 37 | .reduce((obj, field) => { 38 | const value = this[field.name as keyof this] 39 | if (value) { 40 | obj[field.name] = value 41 | } 42 | return obj 43 | }, <{[key: string]: RuleValue}>{}) 44 | } 45 | 46 | value() { 47 | const fields = Object.values(this.fields()) as FormField[] 48 | // tslint:disable-next-line:no-any 49 | const value: Record = {} 50 | fields.forEach(field => { 51 | value[field.name] = this[field.name as keyof FormFields] 52 | }) 53 | 54 | return value 55 | } 56 | 57 | fill(data: Partial>) { 58 | const fields = Object.values(this.fields()) as FormField[] 59 | fields.forEach(field => { 60 | if (field.name in data) { 61 | // @ts-ignore 62 | this[field.name] = data[field.name] 63 | } 64 | }) 65 | } 66 | 67 | toJSON() { 68 | return this.value() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Forms/FormFields.ts: -------------------------------------------------------------------------------- 1 | import { Constructor, Type } from '../Support' 2 | import { Optional, RuleInterface } from '../Validation' 3 | 4 | export class FormField { 5 | constructor( 6 | public name: string, 7 | // tslint:disable-next-line:no-any 8 | public type: any, 9 | // tslint:disable-next-line:no-any 10 | public rules: (Type | ((...args: any[]) => Type)) [] = [] 11 | ) { 12 | } 13 | 14 | // tslint:disable-next-line:no-any 15 | validate(value: any): Record | undefined { 16 | const errors: Record = {} 17 | let hasErrors = false 18 | 19 | if (this.rules.includes(Optional) && (value === undefined || value === null)) { 20 | return 21 | } 22 | 23 | this.rules.forEach(rule => { 24 | if (!rule.prototype.passes) { 25 | rule = (rule as Function)() 26 | } 27 | const instance = new (rule as Constructor) 28 | if (!instance.passes(this.name, value)) { 29 | hasErrors = true 30 | errors[instance.identifier] = instance.message(this.name, value) 31 | } 32 | }) 33 | 34 | return hasErrors ? errors : undefined 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Forms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Form' 2 | export * from './Decorators' 3 | export * from './FormFields' 4 | -------------------------------------------------------------------------------- /Framework/App.ts: -------------------------------------------------------------------------------- 1 | import { Application } from './Application' 2 | import { ServiceIdentifier } from '../Container/Contracts' 3 | 4 | export class App { 5 | static instance: Application 6 | 7 | static get(service: ServiceIdentifier | string): T { 8 | return App.instance.get(service) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Framework/Application.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '../Container' 2 | import { Type } from '../Support' 3 | import { App } from './App' 4 | import { Provider } from './Provider' 5 | import { AppConfig, AuthConfig, BaseConfig } from './Config' 6 | import { FormResolver } from './Resolvers/FormResolver' 7 | import { EntityResolver } from './Resolvers/EntityResolver' 8 | import { RootDir } from './RootDir' 9 | import { StaticAssetsMiddleware } from './Middleware/StaticAssetsMiddleware' 10 | import { ErrorHandler, ErrorHandlerInterface, Handler as HttpHandler, } from '../Router/Http' 11 | import { Storage } from '../Storage' 12 | import { AuthResolver } from './Resolvers/AuthResolver' 13 | import fileSystem from 'fs' 14 | import path from 'path' 15 | import { WebsocketsProvider } from './Providers/WebsocketsProvider' 16 | 17 | export class Application extends Container { 18 | static defaultConfigDirectory = 'config' 19 | 20 | constructor( 21 | public directory: string, 22 | public configDirectory = Application.defaultConfigDirectory 23 | ) { 24 | super() 25 | Application.setInstance(this) 26 | this.set(Application, (App.instance = this)) 27 | this.set(Container, this) 28 | this.set(RootDir, directory) 29 | } 30 | 31 | static async create( 32 | directory: string, 33 | configDirectory = Application.defaultConfigDirectory 34 | ) { 35 | const app = new this( 36 | fileSystem.realpathSync.native(directory), 37 | configDirectory 38 | ) 39 | await app.bootstrap() 40 | return app 41 | } 42 | 43 | async startServer() { 44 | const httpHandler = this.get(HttpHandler) 45 | 46 | const appConfig = this.get(AppConfig) 47 | 48 | if (appConfig.websocketsPort) { 49 | await this.registerProviders([WebsocketsProvider]) 50 | } 51 | 52 | return httpHandler.startServer(this) 53 | } 54 | 55 | public async registerProviders(providers: Type[]) { 56 | await Promise.all( 57 | providers.map((provider) => { 58 | const instance = this.get(provider) 59 | return instance.register() 60 | }) 61 | ) 62 | } 63 | 64 | private async loadConfig(configDirectory: string) { 65 | const configsPath = path.join(this.directory, configDirectory) 66 | 67 | const storage = this.get(Storage) 68 | 69 | if (!(await storage.exists(configsPath))) { 70 | console.warn( 71 | `Config path '${configsPath}' does not exist. Running with default config.` 72 | ) 73 | } 74 | 75 | storage 76 | .files(configsPath) 77 | .whereIn('extension', ['ts']) 78 | .forEach((file) => { 79 | const configItem = require(file.path).default as BaseConfig<{}> 80 | if (configItem && configItem.constructor) { 81 | configItem.applyNewValues() 82 | this.set(configItem.constructor, configItem) 83 | } 84 | }) 85 | } 86 | 87 | private registerResolvers() { 88 | this.resolvers.unshift(new FormResolver(this)) 89 | this.resolvers.unshift(new EntityResolver(this)) 90 | this.resolvers.unshift(new AuthResolver(this)) 91 | } 92 | 93 | private async bootstrap() { 94 | await this.loadConfig(this.configDirectory) 95 | 96 | this.set(ErrorHandlerInterface, this.get(ErrorHandler)) 97 | 98 | const appConfig = this.get(AppConfig) 99 | await this.checkAppSecret() 100 | 101 | if (appConfig.staticAssets) { 102 | appConfig.middleware.http.unshift(StaticAssetsMiddleware) 103 | } 104 | 105 | this.registerResolvers() 106 | 107 | const providers = appConfig.providers || [] 108 | 109 | await this.registerProviders(providers) 110 | } 111 | 112 | private async checkAppSecret() { 113 | const authConfig = this.get(AuthConfig) 114 | if (!authConfig.signature) { 115 | throw new Error(`APP_SECRET is not setup in your '.env' file.`) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Framework/Auth/Authenticatable.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityColumns } from '../../Database' 2 | 3 | export interface Authenticatable { 4 | getId(): EntityColumns; 5 | 6 | getUsername(): EntityColumns; 7 | 8 | getPassword(): EntityColumns; 9 | } 10 | -------------------------------------------------------------------------------- /Framework/Auth/User.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, EntityColumns, ID, PrimaryColumn } from '../../Database' 2 | import { Authenticatable } from './Authenticatable' 3 | 4 | export class User extends Entity implements Authenticatable { 5 | 6 | @PrimaryColumn() 7 | id: ID 8 | 9 | @Column() 10 | email: string 11 | 12 | @Column() 13 | password: string 14 | 15 | getId(): EntityColumns { 16 | return 'id' 17 | } 18 | 19 | getPassword(): EntityColumns { 20 | return 'password' 21 | } 22 | 23 | getUsername(): EntityColumns { 24 | return 'email' 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Framework/Auth/index.ts: -------------------------------------------------------------------------------- 1 | import { Container, InjectableMetadata, Scope } from '../../Container' 2 | import { ControllerMetadata, MethodMetadata } from '../../Router/Metadata' 3 | 4 | export * from './Auth' 5 | export * from './User' 6 | export * from './Authenticatable' 7 | 8 | export const AuthUserIdentifier = Symbol('framework.auth:userIdentifier') 9 | 10 | export function AuthUser() { 11 | return function(target: object, property: string, parameterIndex?: number) { 12 | if (parameterIndex === undefined) { 13 | const metadata = InjectableMetadata.get(target.constructor) 14 | metadata.dependencies[property] = AuthUserIdentifier 15 | metadata.scope = Scope.REQUEST 16 | InjectableMetadata.set(metadata, target.constructor) 17 | } else { 18 | const metadata = ControllerMetadata.get(target.constructor) 19 | 20 | const methodMetadata = metadata.methods[property] || new MethodMetadata() 21 | methodMetadata.parametersOverrides[parameterIndex] = async function(container: Container) { 22 | return await container.get(AuthUserIdentifier) 23 | } 24 | metadata.methods[property] = methodMetadata 25 | 26 | ControllerMetadata.set(metadata, target.constructor) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Framework/Config/AppConfig.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from './BaseConfig' 2 | import { GlobalMiddleware } from '../../Router' 3 | import { Abstract, Type } from '../../Support' 4 | import { Provider } from '../Provider' 5 | import { HttpMiddleware } from '@Typetron/Router/Http/Middleware' 6 | import { WebsocketMiddleware } from '@Typetron/Router/Websockets/Middleware' 7 | 8 | export class AppConfig extends BaseConfig { 9 | environment: string 10 | server: 'node' | 'uNetworking' = 'node' 11 | debug = true 12 | port: number 13 | websocketsPort?: number 14 | middleware: { 15 | global: Abstract[], 16 | http: Abstract[], 17 | websocket: Abstract[], 18 | } = { 19 | global: [], 20 | http: [], 21 | websocket: [], 22 | } 23 | providers: Type [] 24 | staticAssets?: {url: string, path: string, basePath?: boolean, indexFile?: string}[] 25 | } 26 | -------------------------------------------------------------------------------- /Framework/Config/AuthConfig.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../Auth' 2 | import { BaseConfig } from './BaseConfig' 3 | 4 | export class AuthConfig extends BaseConfig { 5 | entity: typeof User 6 | signature: string 7 | duration: number 8 | saltRounds: number 9 | } 10 | -------------------------------------------------------------------------------- /Framework/Config/BaseConfig.ts: -------------------------------------------------------------------------------- 1 | import { ChildObject } from '../../Support' 2 | 3 | export class BaseConfig { 4 | constructor(private newValues: Partial>>) {} 5 | 6 | applyNewValues() { 7 | Object.assign(this, this.newValues) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Framework/Config/CacheConfig.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from './BaseConfig' 2 | 3 | export type CacheStoreKey = keyof CacheConfig['drivers'] 4 | 5 | export class CacheConfig extends BaseConfig { 6 | default: CacheStoreKey = process.env?.CACHE_DRIVER as CacheStoreKey ?? 'file' 7 | 8 | drivers = { 9 | file: { 10 | path: 'cache/data', 11 | }, 12 | 13 | memory: {}, 14 | 15 | database: { 16 | table: 'cache', 17 | connection: null, 18 | }, 19 | // 20 | // redis: { 21 | // connection: 'cache', 22 | // lock_connection: 'default', 23 | // }, 24 | // 25 | // memcached: { 26 | // persistent_id: process.env.MEMCACHED_PERSISTENT_ID, 27 | // sasl: [process.env.MEMCACHED_USERNAME, process.env.MEMCACHED_PASSWORD], 28 | // options: { 29 | // // Memcached::OPT_CONNECT_TIMEOUT : 2000, 30 | // }, 31 | // servers: { 32 | // host: process.env.MEMCACHED_HOST ?? '127.0.0.1', 33 | // port: process.env.MEMCACHED_PORT ?? '11211', 34 | // weight: 100, 35 | // }, 36 | // }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Framework/Config/DatabaseConfig.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from './BaseConfig' 2 | import { MysqlDriver, SqliteDriver } from '@Typetron/Database' 3 | import { DatabaseDriver } from '@Typetron/Database/Drivers/DatabaseDriver' 4 | 5 | export class DatabaseConfig extends BaseConfig { 6 | synchronizeSchema = false 7 | entities: string 8 | migrationsDirectory = 'Database/migrations' 9 | seedersDirectory = 'Database/seeders' 10 | driver: keyof this['drivers'] = process.env.databaseDriver as keyof this['drivers'] ?? 'sqlite' 11 | 12 | drivers: Record DatabaseDriver> = { 13 | sqlite: () => new SqliteDriver(process.env.database ?? 'database.sqlite'), 14 | mysql: () => new MysqlDriver({ 15 | host: process.env.databaseHost, 16 | user: process.env.databaseUser, 17 | password: process.env.databasePassword, 18 | database: process.env.database, 19 | }), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Framework/Config/MailConfig.ts: -------------------------------------------------------------------------------- 1 | import { BaseConfig } from './BaseConfig' 2 | 3 | export type Mailers = keyof MailConfig['mailers'] 4 | 5 | export class MailConfig extends BaseConfig { 6 | default: Mailers = process.env?.MAILER as Mailers ?? 'memory' 7 | 8 | from: { 9 | email: string, 10 | name?: string, 11 | } 12 | 13 | mailers: { 14 | memory: any, 15 | 16 | SendGrid?: { 17 | key: string, 18 | }, 19 | SES?: { 20 | // TODO 21 | }, 22 | Mailgun?: { 23 | // TODO 24 | }, 25 | Postmark?: { 26 | // TODO 27 | }, 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Framework/Config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseConfig' 2 | export * from './AppConfig' 3 | export * from './AuthConfig' 4 | export * from './DatabaseConfig' 5 | export * from './CacheConfig' 6 | export * from './MailConfig' 7 | -------------------------------------------------------------------------------- /Framework/Middleware/AuthMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '../../Container' 2 | import { RequestHandler } from '../../Router' 3 | import { Http, HttpError, Request } from '../../Router/Http' 4 | import * as jwt from 'jsonwebtoken' 5 | import { Auth } from '../Auth' 6 | import { HttpMiddleware } from '@Typetron/Router/Http/Middleware' 7 | 8 | @Injectable() 9 | export class AuthMiddleware implements HttpMiddleware { 10 | 11 | @Inject() 12 | auth: Auth 13 | 14 | async handle(request: Request, next: RequestHandler) { 15 | if (this.auth.id && this.auth.expiresAt > new Date()) { 16 | return next(request) 17 | } 18 | const authHeader = request.headers.authorization || (request.body as Record)?.token || '' 19 | const token = authHeader.replace('Bearer ', '') 20 | try { 21 | await this.auth.verify(token) 22 | return next(request) 23 | } catch (error) { 24 | if (error instanceof jwt.TokenExpiredError || error instanceof jwt.JsonWebTokenError) { 25 | throw new HttpError('Unauthenticated', Http.Status.UNAUTHORIZED) 26 | } 27 | throw new HttpError(error.message, Http.Status.BAD_REQUEST) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Framework/Middleware/CorsMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '../../Container' 2 | import { RequestHandler } from '../../Router' 3 | import { ErrorHandlerInterface, Request, Response } from '../../Router/Http' 4 | import { HttpMiddleware } from '@Typetron/Router/Http/Middleware' 5 | 6 | @Injectable() 7 | export class CorsMiddleware extends HttpMiddleware { 8 | 9 | @Inject() 10 | errorHandler: ErrorHandlerInterface 11 | 12 | async handle(request: Request, next: RequestHandler) { 13 | let response: Response 14 | if (request.method.toLowerCase() === 'options') { 15 | response = new Response(undefined) 16 | response.setHeaders({ 17 | 'Access-Control-Allow-Methods': 'GET, PUT, PATCH, POST, DELETE', 18 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization' 19 | }) 20 | } else { 21 | try { 22 | response = await next(request) 23 | } catch (error) { 24 | response = await this.errorHandler.handle(error, request) 25 | } 26 | } 27 | 28 | response.setHeader('Access-Control-Allow-Origin', '*') 29 | return response 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Framework/Middleware/StaticAssetsMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '../../Container' 2 | import { RequestHandler, RouteNotFoundError } from '../../Router' 3 | import { Storage } from '../../Storage' 4 | import { Http, Request, Response } from '../../Router/Http' 5 | import { AppConfig } from '../Config' 6 | import * as path from 'path' 7 | import { HttpMiddleware } from '@Typetron/Router/Http/Middleware' 8 | 9 | @Injectable() 10 | export class StaticAssetsMiddleware extends HttpMiddleware { 11 | 12 | @Inject() 13 | appConfig: AppConfig 14 | 15 | @Inject() 16 | storage: Storage 17 | 18 | mimeTypes: {[key: string]: string} = { 19 | 'txt': 'text/plain', 20 | 'js': 'text/javascript', 21 | 'html': 'text/html', 22 | 'css': 'text/css', 23 | 'jpg': 'image/jpeg', 24 | 'jpeg': 'image/jpeg', 25 | 'png': 'image/png', 26 | 'webp': 'image/webp', 27 | 'gif': 'image/gif', 28 | } 29 | 30 | async handle(request: Request, next: RequestHandler) { 31 | try { 32 | return await next(request) 33 | } catch (error) { 34 | if (request.method.toLowerCase() === Http.Method.GET.toLowerCase() && error instanceof RouteNotFoundError) { 35 | return this.loadStaticAsset(error, request) 36 | } 37 | throw error 38 | } 39 | } 40 | 41 | async loadStaticAsset(error: RouteNotFoundError, request: Request): Promise { 42 | const configs = this.appConfig.staticAssets ?? [] 43 | for (const config of configs) { 44 | if (!request.uri.match(config.url)) { 45 | continue 46 | } 47 | 48 | let realPath = path.join(config.path, request.uri) 49 | let extension = path.extname(realPath).substring(1) 50 | if (!extension) { 51 | realPath = path.join(realPath, 'index.html') 52 | extension = 'html' 53 | } 54 | if (!await this.storage.exists(realPath)) { 55 | if (config.basePath) { 56 | const basePath = path.join(config.path, config.indexFile ?? 'index.html') 57 | 58 | return this.getResponse(basePath) 59 | } 60 | 61 | continue 62 | } 63 | return this.getResponse(realPath, extension) 64 | } 65 | 66 | throw error 67 | } 68 | 69 | private async getResponse(filePath: string, extension?: string) { 70 | extension = extension ?? path.extname(filePath).substring(1) 71 | const file = await this.storage.read(filePath) 72 | const contentType = this.mimeTypes[extension || 'application/octet-stream'] || this.mimeTypes.txt 73 | return new Response(file, Http.Status.OK, {'Content-type': contentType}) 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /Framework/Middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthMiddleware' 2 | export * from './CorsMiddleware' 3 | -------------------------------------------------------------------------------- /Framework/Provider.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject } from '@Typetron/Container' 2 | 3 | export abstract class Provider { 4 | 5 | @Inject() 6 | public app: Container 7 | 8 | abstract register(): void | Promise; 9 | } 10 | -------------------------------------------------------------------------------- /Framework/Providers/CacheProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Container, Inject } from '@Typetron/Container' 4 | import { Cache, DatabaseStore, FileStore, MemoryStore } from '@Typetron/Cache' 5 | import { Storage } from '@Typetron/Storage' 6 | import { Provider } from '../Provider' 7 | import { CacheConfig, CacheStoreKey } from '../Config' 8 | 9 | const cacheStores: Record Cache> = { 10 | file: (app: Container, config: CacheConfig) => { 11 | return new FileStore(app.get(Storage), config.drivers.file.path) 12 | }, 13 | memory: () => { 14 | return new MemoryStore() 15 | }, 16 | database: (app: Container, config: CacheConfig) => { 17 | return new DatabaseStore(config.drivers.database.table) 18 | }, 19 | } 20 | 21 | export class CacheProvider extends Provider { 22 | @Inject() 23 | config: CacheConfig 24 | 25 | public register() { 26 | this.app.set(Cache, cacheStores[this.config.default](this.app, this.config)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Framework/Providers/DatabaseProvider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '../Provider' 2 | import { DatabaseConfig } from '../Config' 3 | import { Inject } from '../../Container' 4 | import { Connection, Entity, Query } from '@Typetron/Database' 5 | import { Storage } from '../../Storage' 6 | 7 | export class DatabaseProvider extends Provider { 8 | 9 | @Inject() 10 | databaseConfig: DatabaseConfig 11 | 12 | @Inject() 13 | storage: Storage 14 | 15 | async register() { 16 | const driver = this.databaseConfig.drivers[this.databaseConfig.driver] 17 | if (!driver) { 18 | throw new Error(`Driver '${this.databaseConfig.driver}' is not defined in the 'database.drivers' config`) 19 | } 20 | Query.connection = new Connection(driver()) 21 | if (this.databaseConfig.synchronizeSchema) { 22 | await this.synchronize(Query.connection) 23 | } 24 | } 25 | 26 | private async synchronize(connection: Connection) { 27 | const entityFiles = await this.storage.files(this.databaseConfig.entities, true) 28 | const entitiesImports: (typeof Entity)[] = await Promise.all( 29 | entityFiles.map(file => import(file.path)) 30 | ) 31 | const entitiesMetadata = entitiesImports.flatMap(item => Object.values(item).pluck('metadata')) 32 | 33 | await connection.driver.schema.synchronize(entitiesMetadata) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Framework/Providers/MailProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Container, Inject } from '@Typetron/Container' 4 | import { Provider } from '../Provider' 5 | import { MailConfig, Mailers } from '../Config' 6 | import { Mailer, MemoryTransport } from '@Typetron/Mail' 7 | import { MailTransport } from '@Typetron/Mail/MailTransport' 8 | import { SendGridTransport } from '@Typetron/Mail/Transports/SendGridTransport' 9 | 10 | const mailers: Partial MailTransport>> = { 11 | memory: (app: Container, config: MailConfig) => { 12 | return new MemoryTransport() 13 | }, 14 | SendGrid: (app: Container, config: MailConfig) => { 15 | if (!config.mailers.SendGrid?.key) { 16 | throw new Error('SendGrid key is not set') 17 | } 18 | 19 | return new SendGridTransport(config.mailers.SendGrid?.key) 20 | }, 21 | } 22 | 23 | export class MailProvider extends Provider { 24 | @Inject() 25 | config: MailConfig 26 | 27 | public register() { 28 | const transport = mailers[this.config.default]?.(this.app, this.config) 29 | this.app.set(Mailer, new Mailer(this.config.from, transport)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Framework/Resolvers/AuthResolver.ts: -------------------------------------------------------------------------------- 1 | import { BaseResolver } from '../../Container' 2 | import { Auth, AuthUserIdentifier } from '../Auth' 3 | 4 | export class AuthResolver extends BaseResolver { 5 | 6 | async resolve(abstract: symbol, parameters: object[]): Promise { 7 | const auth = this.container.get(Auth) 8 | if (!auth.id) { 9 | return undefined as unknown as T 10 | } 11 | return await auth.user() as unknown as T 12 | } 13 | 14 | canResolve(abstract: symbol): boolean { 15 | return abstract === AuthUserIdentifier 16 | } 17 | 18 | // private setScopeToRequest() { 19 | // const metadata = InjectableMetadata.get(User); 20 | // metadata.scope = Scope.REQUEST; 21 | // InjectableMetadata.set(metadata, User); 22 | // } 23 | } 24 | -------------------------------------------------------------------------------- /Framework/Resolvers/EntityResolver.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../../Support' 2 | import { Entity, EntityConstructor } from '../../Database' 3 | import { Request } from '../../Router/Request' 4 | import { BaseResolver, Container, InjectableMetadata, Scope } from '../../Container' 5 | import { EntityNotFoundError } from '../../Database/EntityNotFoundError' 6 | 7 | export class EntityResolver extends BaseResolver { 8 | 9 | constructor(container: Container) { 10 | super(container) 11 | this.setEntityScopeToRequest() 12 | } 13 | 14 | async resolve(abstract: EntityConstructor & typeof Entity, parametersValues: object[]): Promise { 15 | let entity: Entity | undefined 16 | const request = this.container.get(Request) 17 | const requestParameterName = abstract.name 18 | const parameter = request.parameters[requestParameterName] ?? Number(parametersValues[0]) 19 | if (parameter) { 20 | entity = await abstract.find(parameter) 21 | if (!entity) { 22 | throw new EntityNotFoundError(abstract, `Entity '${requestParameterName}' with ${abstract.getPrimaryKey()} '${parameter}' not found`) 23 | } 24 | } else { 25 | throw new Error(`No parameter found that can be used as an entity identifier for the '${requestParameterName}' entity. Did you forget to add the '{${requestParameterName}}' parameter on the route?`) 26 | } 27 | 28 | // @ts-ignore 29 | return entity 30 | } 31 | 32 | canResolve(abstract: Constructor): boolean { 33 | return abstract.prototype instanceof Entity 34 | } 35 | 36 | private setEntityScopeToRequest() { 37 | const metadata = InjectableMetadata.get(Entity) 38 | metadata.scope = Scope.TRANSIENT 39 | InjectableMetadata.set(metadata, Entity) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Framework/Resolvers/FormResolver.ts: -------------------------------------------------------------------------------- 1 | import { BaseResolver, ClassResolver, Container } from '../../Container' 2 | import { Constructor } from '../../Support' 3 | import { Form, FormFields } from '../../Forms' 4 | import { Http, HttpError } from '../../Router/Http' 5 | import { Request } from '../../Router/Request' 6 | 7 | export class FormResolver extends BaseResolver { 8 | 9 | constructor(container: Container) { 10 | super(container) 11 | this.setFormScopeToRequest() 12 | } 13 | 14 | async resolve(abstract: Constructor, parameters: object[]) { 15 | const request = this.container.get(Request) 16 | const classResolver = new ClassResolver(this.container) 17 | const form = await classResolver.resolve(abstract, parameters) 18 | form.fill({...request.body as object, ...request.files} as FormFields) 19 | 20 | if (!form.valid()) { 21 | throw new HttpError(form.errors, Http.Status.UNPROCESSABLE_ENTITY) 22 | } 23 | 24 | return form 25 | } 26 | 27 | canResolve(abstract: Constructor): boolean { 28 | return abstract.prototype instanceof Form 29 | } 30 | 31 | private setFormScopeToRequest() { 32 | // const metadata = InjectableMetadata.get(Form) 33 | // 34 | // metadata.scope = Scope.TRANSIENT 35 | // InjectableMetadata.set(metadata, Form) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Framework/RootDir.ts: -------------------------------------------------------------------------------- 1 | export class RootDir extends String {} 2 | -------------------------------------------------------------------------------- /Framework/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Application' 2 | export * from './Provider' 3 | export * from './App' 4 | export * from './RootDir' 5 | export * from './Config' 6 | export * from './Providers/DatabaseProvider' 7 | export * from './Providers/CacheProvider' 8 | export * from './Providers/MailProvider' 9 | -------------------------------------------------------------------------------- /Frontend/Angular/Readme.md: -------------------------------------------------------------------------------- 1 | # Typetron Angular utilities 2 | 3 | **[Typetron](https://typetron.org)** is a **modern web framework** built for Node.js, written in **Typescript**, that 4 | allows you to build fully featured web applications. 5 | 6 | This package contains a set of utilities used in Angular apps when working with Typetron in order to speed up the 7 | development process. More about it at [https://typetron.org](https://typetron.org/frontend/angular) 8 | -------------------------------------------------------------------------------- /Frontend/Angular/index.ts: -------------------------------------------------------------------------------- 1 | import type { AbstractControl, ValidatorFn } from '@angular/forms' 2 | import { FormControl, FormGroup } from '@angular/forms' 3 | import type { Form, FormField } from '@Typetron/Forms' 4 | import { Constructor } from '@Typetron/Support' 5 | 6 | export class FormBuilder { 7 | static build(form: typeof Form & Constructor): FormGroup { 8 | const controls: Record = {} 9 | const formFields = Object.values(form.fields()) as FormField[] 10 | const instance = new (form as unknown as Constructor)() 11 | Object.values(formFields).forEach(field => { 12 | controls[field.name] = new FormControl( 13 | instance[field.name as keyof Form], 14 | {validators: this.getValidators(field)} 15 | ) 16 | }) 17 | return new FormGroup(controls) 18 | } 19 | 20 | private static getValidators(field: FormField): ValidatorFn { 21 | return control => field.validate(control.value) as unknown as ValidatorFn 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Frontend/Angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typetron/angular", 3 | "description": "Frontend tools for Angular from Typetron", 4 | "license": "MIT", 5 | "version": "16.2.4", 6 | "author": { 7 | "email": "ionel@typetron.org", 8 | "name": "Ionel Lupu" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "version": "npm run build" 13 | }, 14 | "engines": { 15 | "node": ">= 18.12.0" 16 | }, 17 | "dependencies": { 18 | "@typetron/framework": "^0.4.0-rc9" 19 | }, 20 | "peerDependencies": { 21 | "@angular/common": "^16.2.4", 22 | "@angular/core": "^16.2.4", 23 | "@angular/forms": "^16.2.4" 24 | }, 25 | "devDependencies": { 26 | "typescript": "5.2.2" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/typetron/angular.git" 31 | }, 32 | "files": [ 33 | "**/*.ts", 34 | "**/*.js", 35 | "**/*.ts.map", 36 | "**/*.js.map" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /Frontend/Angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "outDir": "build", 5 | "baseUrl": ".", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@Typetron/*": [ 9 | "node_modules/@typetron/framework/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "dom" 14 | ] 15 | }, 16 | "files": [ 17 | "index.ts" 18 | ] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Frontend/Websockets/Readme.md: -------------------------------------------------------------------------------- 1 | # Typetron WebSockets 2 | 3 | This library is used to easily listen and emit WebSockets action from the browser client to a Typetron app. 4 | 5 | #### Installation 6 | 7 | ```bash 8 | $ npm install @typetron/websockets 9 | ``` 10 | 11 | Note: you need to have a module loader in your app for this to work (webpack, rollup, babel, parcel, etc.) 12 | 13 | #### Listening for actions 14 | 15 | You can listen to a WebSocket action sent from the server using the _on('Action name')_ method like this on the socket 16 | connection: 17 | 18 | ```ts 19 | socket.on('actionName').subscribe(response => { 20 | console.log('Backend response', response) 21 | }) 22 | socket.on('user.update').subscribe(user => { 23 | console.log('User updated', user) 24 | }) 25 | ``` 26 | 27 | The _on_ method will return an observable (see [RxJS](https://rxjs.dev/) for more details) that you can use to subscribe 28 | to. 29 | 30 | #### Emitting actions 31 | 32 | If you want to signal the server with an action or when you want to send some data to it, you can use the _emit('action 33 | name', data)_ method: 34 | 35 | ```ts 36 | socket.emit('actionName'); 37 | socket.emit('actionName', "my message here"); 38 | socket.emit('actionName', {my: {message: "here"}}); 39 | ``` 40 | 41 | Be aware that if you are expecting a response from the backend, you need to subscribe to the same action (or the action 42 | the server is emitting to) using the _.on_ method. 43 | 44 | #### Emitting and listening for server response 45 | 46 | If you want to make a single "request" to the server, meaning that you want to emit and wait for a response at the same 47 | time, you can use the _request('action name', data?)_ method. This will essentially make an _emit_ and listen to its 48 | response using the _on_ method for you: 49 | 50 | ```ts 51 | const users = await socket.request('users.list') 52 | const savedUser = await socket.request('users.save', {name: 'John'}) 53 | ``` 54 | 55 | #### Message format of the WebSocket actions 56 | 57 | The Typetron WebSocket server uses a specific message format when exchanging information between it and the clients. 58 | These message have the following format: 59 | _When sending a message:_ 60 | 61 | ```json 62 | { 63 | "action": "action name", 64 | "message": { 65 | // optional 66 | "body": "content sent to the controllers", 67 | "parameters": [ 68 | "param1", 69 | "param1" 70 | ] 71 | // controller method parameters (optional) 72 | } 73 | } 74 | ``` 75 | 76 | _When receiving a message:_ 77 | 78 | ```json 79 | { 80 | "action": "action name", 81 | "status": "OK", 82 | // or "Error", 83 | "message": "backend response" 84 | // optional 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /Frontend/Websockets/index.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of, ReplaySubject, Subject, throwError } from 'rxjs' 2 | import { filter, map, switchMap, take } from 'rxjs/operators' 3 | import { ActionErrorResponse, ActionRequest, ActionResponse, WebsocketMessageStatus } from '@typetron/framework/Router/Websockets/types' 4 | 5 | export class Websocket { 6 | socket?: WebSocket 7 | 8 | actionMessages$ = new Subject>() 9 | queuedActions$ = new ReplaySubject() 10 | errors$ = new Subject() 11 | onConnectCallback: () => void 12 | 13 | constructor(public url: string, public protocols?: string | string[]) { 14 | this.connect(url, protocols) 15 | } 16 | 17 | get isConnected() { 18 | return this.socket?.readyState === WebSocket.OPEN 19 | } 20 | 21 | connect(url?: string, protocols?: string | string[]): void { 22 | const socket = new WebSocket(url ?? this.url, protocols ?? this.protocols) 23 | this.socket = socket 24 | 25 | socket.onmessage = (action) => { 26 | const message = JSON.parse(action.data) 27 | this.actionMessages$.next(message) 28 | } 29 | socket.onopen = () => { 30 | this.onConnectCallback?.() 31 | this.queuedActions$.subscribe(message => { 32 | socket.send(JSON.stringify(message)) 33 | }) 34 | } 35 | 36 | socket.onclose = (action) => { 37 | this.queuedActions$ = new ReplaySubject() 38 | this.reconnect() 39 | } 40 | } 41 | 42 | onConnect(callback: () => void) { 43 | this.onConnectCallback = callback 44 | } 45 | 46 | reconnect(): void { 47 | if (this.socket?.readyState === WebSocket.CLOSED) { 48 | window.setTimeout(() => { 49 | this.connect(this.url, this.protocols) 50 | }, 1000) 51 | } 52 | } 53 | 54 | emit(action: string, request?: ActionRequest['message']): void { 55 | const actionMessage: ActionRequest = {action, message: request} 56 | this.queuedActions$.next(actionMessage) 57 | } 58 | 59 | on(action: string): Observable { 60 | return this.actionMessages$.pipe( 61 | filter(actionMessage => actionMessage.action === action), 62 | switchMap(actionMessage => { 63 | if (actionMessage.status === WebsocketMessageStatus.Error) { 64 | this.errors$.next(actionMessage as ActionErrorResponse) 65 | return throwError(actionMessage) 66 | } 67 | return of(actionMessage) 68 | }), 69 | map(actionMessage => actionMessage.message) 70 | ) as Observable 71 | } 72 | 73 | onError({except}: {except?: string[]}): Observable> { 74 | return this.errors$.pipe(filter(actionMessage => !(except || []).includes(actionMessage.action))) 75 | } 76 | 77 | async request(action: string, message?: ActionRequest['message']): Promise { 78 | this.emit(action, message) 79 | return new Promise((resolve, reject) => { 80 | this.on(action).pipe(take(1)).subscribe({ 81 | next: resolve, 82 | error: reject 83 | }) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Frontend/Websockets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typetron/websockets", 3 | "description": "Typetron Websockets library", 4 | "keywords": [ 5 | "websockets", 6 | "typetron", 7 | "nodejs", 8 | " typescript", 9 | "real-time", 10 | "realtime" 11 | ], 12 | "license": "MIT", 13 | "version": "0.4.0-rc9", 14 | "author": { 15 | "email": "ionel@typetron.org", 16 | "name": "Ionel Lupu" 17 | }, 18 | "scripts": { 19 | "build": "tsc" 20 | }, 21 | "engines": { 22 | "node": ">= 12.13.0" 23 | }, 24 | "peerDependencies": { 25 | "rxjs": "~7.8.1" 26 | }, 27 | "dependencies": { 28 | "@typetron/framework": "0.4.0-rc9" 29 | }, 30 | "devDependencies": { 31 | "typescript": "5.2.2" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/typetron/websockets.git" 36 | }, 37 | "files": [ 38 | "**/*.ts", 39 | "**/*.js", 40 | "**/*.ts.map", 41 | "**/*.js.map" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /Frontend/Websockets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "build", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@Typetron/*": [ 8 | "node_modules/@typetron/framework/*" 9 | ] 10 | }, 11 | "lib": [ 12 | "dom" 13 | ] 14 | }, 15 | "include": [ 16 | "./*.ts" 17 | ], 18 | "exclude": [] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2019] [Ionel-Cristian Lupu] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Mail/MailAddress.ts: -------------------------------------------------------------------------------- 1 | export interface MailAddress { 2 | email: string 3 | name?: string 4 | } 5 | -------------------------------------------------------------------------------- /Mail/MailTransport.ts: -------------------------------------------------------------------------------- 1 | import { Mailable } from './Mailable' 2 | import { SentMessage } from './SentMessage' 3 | 4 | export abstract class MailTransport { 5 | abstract send(message: Mailable): Promise 6 | } 7 | -------------------------------------------------------------------------------- /Mail/Mailable.ts: -------------------------------------------------------------------------------- 1 | import { MailAddress } from '@Typetron/Mail/MailAddress' 2 | 3 | export class Mailable { 4 | 5 | from: string | MailAddress 6 | subject?: string 7 | body?: string | {html: string, text: string} 8 | to?: string | MailAddress | (string | MailAddress)[] 9 | replyTo?: string | MailAddress | (string | MailAddress)[] 10 | cc?: string | MailAddress | (string | MailAddress)[] 11 | bcc?: string | MailAddress | (string | MailAddress)[] 12 | 13 | content(): string | {html: string, text: string} | undefined { 14 | return 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mail/Mailer.ts: -------------------------------------------------------------------------------- 1 | import { MailTransport } from './MailTransport' 2 | import { PendingMail } from './PendingMail' 3 | import { Mailable } from './Mailable' 4 | import { MailAddress } from '@Typetron/Mail/MailAddress' 5 | 6 | export class Mailer { 7 | transport: MailTransport 8 | 9 | constructor(public from: MailAddress, transport?: MailTransport) { 10 | if (!transport) { 11 | throw new Error('Please add the MailProvider in your app in order to use the Mailing feature, ' + 12 | 'or manually add a Mailer instance in the app container ') 13 | } 14 | this.transport = transport 15 | } 16 | 17 | to(email: string) { 18 | const mail = new PendingMail(this) 19 | mail.from(this.from) 20 | mail.to(email) 21 | return mail 22 | } 23 | 24 | async send(message: Mailable) { 25 | return await this.transport.send(message) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Mail/PendingMail.ts: -------------------------------------------------------------------------------- 1 | import { Mailer } from './index' 2 | import { Mailable } from './Mailable' 3 | import { MailAddress } from '@Typetron/Mail/MailAddress' 4 | 5 | export class PendingMail { 6 | 7 | details: { 8 | subject?: string 9 | from: string | MailAddress 10 | to?: string | MailAddress 11 | replyTo?: string | MailAddress 12 | cc?: string | MailAddress 13 | bcc?: string | MailAddress 14 | body?: string | {html: string, text: string} 15 | } = {from: ''} 16 | 17 | constructor(private mailer: Mailer) { 18 | } 19 | 20 | subject(subject: string) { 21 | this.details.subject = subject 22 | return this 23 | } 24 | 25 | from(email: string | MailAddress) { 26 | this.details.from = email 27 | return this 28 | } 29 | 30 | to(email: string | MailAddress) { 31 | this.details.to = email 32 | return this 33 | } 34 | 35 | replyTo(email: string | MailAddress) { 36 | this.details.replyTo = email 37 | return this 38 | } 39 | 40 | cc(email: string | MailAddress) { 41 | this.details.cc = email 42 | return this 43 | } 44 | 45 | bcc(email: string | MailAddress) { 46 | this.details.bcc = email 47 | return this 48 | } 49 | 50 | async send(body: string | {html: string, text: string} | Mailable) { 51 | let mail 52 | if (body instanceof Mailable) { 53 | mail = body 54 | } else { 55 | mail = new Mailable() 56 | mail.body = body 57 | } 58 | 59 | mail.subject = this.details.subject 60 | mail.from = this.details.from 61 | mail.to = this.details.to 62 | mail.replyTo = this.details.replyTo 63 | mail.cc = this.details.cc 64 | mail.bcc = this.details.bcc 65 | 66 | return this.mailer.send(mail) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Mail/SentMessage.ts: -------------------------------------------------------------------------------- 1 | import { MailAddress } from './MailAddress' 2 | import { Mailable } from '@Typetron/Mail/Mailable' 3 | 4 | export class SentMessage { 5 | readonly from: string | MailAddress 6 | readonly subject?: string 7 | readonly body?: string | {html: string, text: string} 8 | readonly to?: string | MailAddress | (string | MailAddress)[] 9 | readonly replyTo?: string | MailAddress | (string | MailAddress)[] 10 | readonly cc?: string | MailAddress | (string | MailAddress)[] 11 | readonly bcc?: string | MailAddress | (string | MailAddress)[] 12 | 13 | constructor(mail: Mailable) { 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | const {content, ...mailDetails} = mail 16 | Object.assign(this, mailDetails) 17 | this.body = mail.content() ?? mail.body 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Mail/Transports/MemoryTransport.ts: -------------------------------------------------------------------------------- 1 | import { MailTransport } from '../MailTransport' 2 | import { SentMessage } from '../SentMessage' 3 | import { Mailable } from '../Mailable' 4 | 5 | export class MemoryTransport extends MailTransport { 6 | messages: SentMessage[] = [] 7 | 8 | async send(message: Mailable) { 9 | const sentMessage = new SentMessage(message) 10 | this.messages.push(sentMessage) 11 | return sentMessage 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Mail/Transports/SendGridTransport.ts: -------------------------------------------------------------------------------- 1 | import { MailTransport } from '../MailTransport' 2 | import { SentMessage } from '../SentMessage' 3 | import { Mailable } from '../Mailable' 4 | import { Inject } from '../../Container' 5 | import { MailConfig } from '../../Framework' 6 | import client from '@sendgrid/mail' 7 | import { HttpError } from '@Typetron/Router/Http' 8 | 9 | export class SendGridTransport extends MailTransport { 10 | messages: SentMessage[] = [] 11 | 12 | @Inject() 13 | config: MailConfig 14 | 15 | constructor(key: string) { 16 | super() 17 | 18 | client.setApiKey(key) 19 | } 20 | 21 | async send(message: Mailable) { 22 | const sentMessage = new SentMessage(message) 23 | 24 | const sendGridMessage = { 25 | personalizations: [ 26 | { 27 | to: sentMessage.to, 28 | cc: sentMessage.cc, 29 | bcc: sentMessage.bcc 30 | }, 31 | ], 32 | from: sentMessage.from, 33 | replyTo: sentMessage.replyTo, 34 | subject: sentMessage.subject, 35 | content: [ 36 | { 37 | type: 'text/html', 38 | value: sentMessage.body 39 | } 40 | ], 41 | // attachments: [ 42 | // { 43 | // content: 'PCFET0NUdGxlPkRvY3VtZW50PC90aXRsZT4KICAgIDwvaGVhZD4KCiAgICA8Ym9keT4KCiAgICA8L2JvZHk+Cgo8L2h0bWw+Cg==', 44 | // filename: 'index.html', 45 | // type: 'text/html', 46 | // disposition: 'attachment' 47 | // } 48 | // ], 49 | // categories: [ 50 | // 'cake', 51 | // 'pie', 52 | // 'baking' 53 | // ], 54 | // sendAt: 1617260400, 55 | // batchId: 'AsdFgHjklQweRTYuIopzXcVBNm0aSDfGHjklmZcVbNMqWert1znmOP2asDFjkl', 56 | // asm: { 57 | // groupId: 12345, 58 | // groupsToDisplay: [ 59 | // 12345 60 | // ] 61 | // }, 62 | // ipPoolName: 'transactional email', 63 | // mailSettings: { 64 | // bypassListManagement: { 65 | // enable: false 66 | // }, 67 | // footer: { 68 | // enable: false 69 | // }, 70 | // sandboxMode: { 71 | // enable: false 72 | // } 73 | // }, 74 | // trackingSettings: { 75 | // clickTracking: { 76 | // enable: true, 77 | // enableText: false 78 | // }, 79 | // openTracking: { 80 | // enable: true, 81 | // substitutionTag: '%open-track%' 82 | // }, 83 | // subscriptionTracking: { 84 | // enable: false 85 | // } 86 | // } 87 | } 88 | 89 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 90 | /* @ts-ignore */ // SendGrid has TS issues 91 | await client.send(sendGridMessage).catch(error => {throw new HttpError(error.response.body.errors, error.code)}) 92 | 93 | return sentMessage 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Mail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Transports/MemoryTransport' 2 | export * from './Mailer' 3 | export * from './Mailable' 4 | -------------------------------------------------------------------------------- /Models/Model.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from '../Support' 2 | import { ModelField, ModelMetadataKey, ModelTypeInterface } from './index' 3 | 4 | export class Model { 5 | static async from(this: Constructor & typeof Model, entity: Promise>): Promise; 6 | static async from(this: Constructor & typeof Model, entity: Promise): Promise; 7 | static from(this: Constructor & typeof Model, entity: Iterable): T[]; 8 | static from(this: Constructor & typeof Model, entity: Q): T; 9 | static from( 10 | this: Constructor & typeof Model, 11 | entity: Q | Iterable | Promise> 12 | ): T | T[] | Promise { 13 | if (entity instanceof Promise) { 14 | return entity.then(value => this.from(value)) 15 | } 16 | if (Symbol.iterator in Object(entity)) { 17 | return Array.from(entity as Iterable).map(item => this.from(item)) 18 | } 19 | const fields: Record = Reflect.getMetadata(ModelMetadataKey, this) || {} 20 | 21 | return this.transform(fields, entity as Q) 22 | } 23 | 24 | protected static transform(fields: Record, entity: Q): T { 25 | const data: Partial> = new this 26 | Object.values(fields).forEach(field => { 27 | if (!entity || !entity.hasOwnProperty(field.name)) { 28 | return 29 | } 30 | const value = entity[field.name as keyof Q] 31 | if (field.type instanceof ModelTypeInterface && value) { 32 | // @ts-ignore 33 | const jsonValue = value['toJSON' as keyof object] ? value?.toJSON() : value 34 | data[field.name as keyof T] = jsonValue ? field.type.transform(jsonValue) as Q[keyof Q] : undefined 35 | } else { 36 | data[field.name as keyof T] = value 37 | } 38 | }) 39 | return data as T 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Models/index.ts: -------------------------------------------------------------------------------- 1 | import { Model } from './Model' 2 | import { Constructor } from '../Support' 3 | 4 | export const ModelMetadataKey = 'model:fields' 5 | 6 | export interface ModelField { 7 | name: string; 8 | type: Function | ModelTypeInterface<{}>; 9 | } 10 | 11 | export function Field(name?: string) { 12 | return function(target: Object, property: string) { 13 | const fields: Record = Reflect.getMetadata(ModelMetadataKey, target.constructor) || {} 14 | let type = Reflect.getMetadata('design:type', target, property) 15 | if (type.prototype instanceof Model) { 16 | type = new ModelType(type) 17 | } 18 | fields[property] = fields[property] || {name: name || property, type} as ModelField 19 | Reflect.defineMetadata(ModelMetadataKey, fields, target.constructor) 20 | } 21 | } 22 | 23 | export abstract class ModelTypeInterface { 24 | constructor(public model: Constructor & typeof Model) {} 25 | 26 | abstract transform(value: Q): Q; 27 | } 28 | 29 | export class ModelType extends ModelTypeInterface { 30 | 31 | transform(value: T) { 32 | return this.model.from(value) 33 | } 34 | } 35 | 36 | export class ArrayType extends ModelTypeInterface { 37 | transform(value: T[]) { 38 | return Array.from(value).map(item => { 39 | return this.model.from(item) 40 | }) 41 | } 42 | } 43 | 44 | export function FieldMany(type: typeof Model) { 45 | return function(target: Object, property: string) { 46 | const fields: Record = Reflect.getMetadata(ModelMetadataKey, target.constructor) || {} 47 | const field = fields[property] || {name: property, type: new ArrayType(type)} as ModelField 48 | field.type = new ArrayType(type) 49 | fields[property] = field 50 | Reflect.defineMetadata(ModelMetadataKey, fields, target.constructor) 51 | } 52 | } 53 | 54 | export * from './Model' 55 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Typetron 2 | > **Note:** This project is a prototype in heavy development and not ready for production. 3 | 4 | **[Typetron](https://typetron.org)** is a **modern web framework** built for Node.js, written in **Typescript**, that 5 | allows you to build fully featured web applications. 6 | Typetron is best suited for small sized to enterprise level apps. 7 | Most of the core packages it uses were built from scratch in order to preserve the performance of the framework. 8 | 9 | _(check [this tutorial](https://typetron.org/tutorials/blog) on how to get started with Typetron)_ 10 | 11 | ### Prerequisites 12 | - [NodeJs >=12 LTS](https://nodejs.org) 13 | 14 | #### Features 15 | Typetron aims to have all the features necessary for building any web app without the need for you 16 | to search the internet for a package to use. Almost all the packages it has were built from scratch and are 17 | available in the framework. 18 | This was done to ensure that all the features you are using benefit from the latest language features. 19 | Also, every package can be tuned for performance or updated in no time if needed. 20 | 21 | ##### Features List 22 | 23 | [typetron.org](https://typetron.org/docs/) 24 | 25 | ##### Performance 26 | Being built with packages created from scratch using the latest features of the language, Typetron comes with 27 | excellent performance out of the box compared to other available frameworks. 28 | 29 | ##### Developer experience 30 | Typetron's source code is built around developer's expectations: it is modern, clean and beautiful. 31 | Also, the tools Typetron is providing are everything a developer needs to build his next awesome project. 32 | 33 | ##### Code sharing 34 | A few years ago we wrote websites. Nowadays we write web applications. The web evolved alongside the tools we are 35 | using. A typical web application is composed of at least two parts: a backend app and a frontend app. 36 | This separation led to two different camps that have a very distinct line between them. Typetron provides tools to make 37 | these two camps work together. 38 | 39 | #### Source code examples 40 | 41 | ##### Entities 42 | ```ts 43 | export class User extends Entity { 44 | 45 | @PrimaryColumn() 46 | id: number; 47 | 48 | @Column() 49 | email: string; 50 | 51 | @Column() 52 | name: string; 53 | 54 | @Relation(() => Post, 'author') 55 | posts: HasMany = []; 56 | 57 | @Relation(() => Group, 'users') 58 | group: BelongsTo; 59 | } 60 | ``` 61 | ##### Forms 62 | ```ts 63 | export class UserForm extends Form { 64 | @Field() 65 | @Rules( 66 | Required, 67 | ) 68 | email: string; 69 | 70 | @Field() 71 | @Rules( 72 | Required, 73 | MinLength(5), 74 | ) 75 | name: string; 76 | 77 | @Field() 78 | dateOfBirth: Date; 79 | } 80 | ``` 81 | 82 | ##### Controllers 83 | 84 | ```ts 85 | @Controller('users') 86 | export class UserController { 87 | 88 | @Inject() 89 | storage: Storage; 90 | 91 | @AuthUser() 92 | user: User; 93 | 94 | @Get('me') 95 | async me() { 96 | return this.auth.user; 97 | } 98 | 99 | @Get() 100 | async browse() { 101 | return UserModel.from(User.get()); 102 | } 103 | 104 | @Get(':User') 105 | read(user: User) { 106 | return user; 107 | } 108 | 109 | @Put(':User') 110 | update(user: User, userForm: UserForm) { 111 | return user.fill(userForm).save(); 112 | } 113 | 114 | @Post() 115 | create(userForm: UserForm) { 116 | return User.create(userForm); 117 | } 118 | } 119 | 120 | ``` 121 | 122 | ## License 123 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 124 | -------------------------------------------------------------------------------- /Router/BaseController.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject } from '../Container' 2 | 3 | export class BaseController { 4 | 5 | @Inject() 6 | protected app: Container 7 | } 8 | -------------------------------------------------------------------------------- /Router/Guard.ts: -------------------------------------------------------------------------------- 1 | import { Http, HttpError } from './Http' 2 | 3 | export abstract class Guard { 4 | // tslint:disable-next-line:no-any 5 | abstract condition(...args: any[]): Promise 6 | 7 | onFail(): void { 8 | throw new HttpError('Unauthorized', Http.Status.UNAUTHORIZED) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Router/Http/Contracts.ts: -------------------------------------------------------------------------------- 1 | export interface Parameters { 2 | [key: string]: string | undefined; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /Router/Http/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './Response' 2 | import { ErrorHandlerInterface } from './ErrorHandlerInterface' 3 | import { HttpError } from './Errors/HttpError' 4 | import { Http } from './index' 5 | import { Request } from './Request' 6 | import { Inject } from '../../Container' 7 | import { AppConfig } from '../../Framework/Config' 8 | 9 | export class ErrorHandler implements ErrorHandlerInterface { 10 | 11 | @Inject() 12 | appConfig: AppConfig 13 | 14 | handle(error: Error, request?: Request) { 15 | if (this.appConfig.debug) { 16 | return this.handleDevelopmentError(error, request) 17 | } 18 | 19 | if (error instanceof HttpError) { 20 | return new Response(error.content, error.status) 21 | } 22 | return new Response(error.message, Http.Status.BAD_REQUEST) 23 | } 24 | 25 | private handleDevelopmentError(error: Error, request?: Request) { 26 | let code = Http.Status.BAD_REQUEST, message: string | object | number = '', stack: string[] 27 | 28 | stack = error.stack ? error.stack.replace(/\n|\s{2,}/g, '').split('at ') : [] 29 | if (error instanceof HttpError) { 30 | code = error.status 31 | message = error.content 32 | if (request) { 33 | stack.unshift(`Route: ${request.method} ${request.uri}`) 34 | } 35 | } else { 36 | message = error.message 37 | } 38 | 39 | const errorInfo: {message: string | object | number, stack?: string[]} = { 40 | message, stack 41 | } 42 | 43 | return new Response(errorInfo, code) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Router/Http/ErrorHandlerInterface.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './Response' 2 | import { Request } from './Request' 3 | 4 | export class ErrorHandlerInterface { 5 | handle: (error: Error, request?: Request) => Promise | Response 6 | } 7 | -------------------------------------------------------------------------------- /Router/Http/Errors/HttpError.ts: -------------------------------------------------------------------------------- 1 | import { Http } from '../index' 2 | 3 | export class HttpError extends Error { 4 | 5 | constructor( 6 | public content: string | object, 7 | public status: Http.Status = Http.Status.BAD_REQUEST 8 | ) { 9 | super(typeof content === 'string' ? content : 'HTTP Error') 10 | } 11 | 12 | getMessage() { 13 | return this.content 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Router/Http/Middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '@Typetron/Router/Request' 2 | import { Response } from '.' 3 | import { GlobalMiddleware, RequestHandler } from '..' 4 | 5 | export abstract class HttpMiddleware extends GlobalMiddleware { 6 | abstract handle(request: Request, next: RequestHandler): Promise 7 | } 8 | -------------------------------------------------------------------------------- /Router/Http/Response.ts: -------------------------------------------------------------------------------- 1 | import { OutgoingHttpHeaders } from 'http' 2 | import { Http } from '.' 3 | import { Buffer } from 'buffer' 4 | 5 | type Content = unknown | undefined | number | string | object | Buffer 6 | 7 | export class Response { 8 | 9 | constructor( 10 | public body: T, 11 | public status: Http.Status = Http.Status.OK, 12 | public headers: OutgoingHttpHeaders = {} 13 | ) { 14 | } 15 | 16 | static ok(content: string | object) { 17 | return new Response(content, Http.Status.OK) 18 | } 19 | 20 | static notFound(content: string | object) { 21 | return new Response(content, Http.Status.NOT_FOUND) 22 | } 23 | 24 | static badRequest(content: string | object) { 25 | return new Response(content, Http.Status.BAD_REQUEST) 26 | } 27 | 28 | setHeader(name: string, value: string) { 29 | this.headers[name] = value 30 | } 31 | 32 | setHeaders(headers: OutgoingHttpHeaders) { 33 | this.headers = headers 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Router/Http/index.ts: -------------------------------------------------------------------------------- 1 | export namespace Http { 2 | export enum Method { 3 | GET = 'GET', 4 | HEAD = 'HEAD', 5 | POST = 'POST', 6 | PUT = 'PUT', 7 | PATCH = 'PATCH', 8 | DELETE = 'DELETE', 9 | OPTIONS = 'OPTIONS', 10 | } 11 | 12 | export enum Status { 13 | CONTINUE = 100, 14 | SWITCHING_PROTOCOLS = 101, 15 | PROCESSING = 102, // RFC2518 16 | EARLY_HINTS = 103, // RFC8297 17 | OK = 200, 18 | CREATED = 201, 19 | ACCEPTED = 202, 20 | NON_AUTHORITATIVE_INFORMATION = 203, 21 | NO_CONTENT = 204, 22 | RESET_CONTENT = 205, 23 | PARTIAL_CONTENT = 206, 24 | MULTI_STATUS = 207, // RFC4918 25 | ALREADY_REPORTED = 208, // RFC5842 26 | IM_USED = 226, // RFC3229 27 | MULTIPLE_CHOICES = 300, 28 | MOVED_PERMANENTLY = 301, 29 | FOUND = 302, 30 | SEE_OTHER = 303, 31 | NOT_MODIFIED = 304, 32 | USE_PROXY = 305, 33 | RESERVED = 306, 34 | TEMPORARY_REDIRECT = 307, 35 | PERMANENTLY_REDIRECT = 308, // RFC7238 36 | BAD_REQUEST = 400, 37 | UNAUTHORIZED = 401, 38 | PAYMENT_REQUIRED = 402, 39 | FORBIDDEN = 403, 40 | NOT_FOUND = 404, 41 | METHOD_NOT_ALLOWED = 405, 42 | NOT_ACCEPTABLE = 406, 43 | PROXY_AUTHENTICATION_REQUIRED = 407, 44 | REQUEST_TIMEOUT = 408, 45 | CONFLICT = 409, 46 | GONE = 410, 47 | LENGTH_REQUIRED = 411, 48 | PRECONDITION_FAILED = 412, 49 | REQUEST_ENTITY_TOO_LARGE = 413, 50 | REQUEST_URI_TOO_LONG = 414, 51 | UNSUPPORTED_MEDIA_TYPE = 415, 52 | REQUESTED_RANGE_NOT_SATISFIABLE = 416, 53 | EXPECTATION_FAILED = 417, 54 | I_AM_A_TEAPOT = 418, // RFC2324 55 | MISDIRECTED_REQUEST = 421, // RFC7540 56 | UNPROCESSABLE_ENTITY = 422, // RFC4918 57 | LOCKED = 423, // RFC4918 58 | FAILED_DEPENDENCY = 424, // RFC4918 59 | RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425, // RFC2817 60 | TOO_EARLY = 425, // RFC-ietf-httpbis-replay-04 61 | UPGRADE_REQUIRED = 426, // RFC2817 62 | PRECONDITION_REQUIRED = 428, // RFC6585 63 | TOO_MANY_REQUESTS = 429, // RFC6585 64 | REQUEST_HEADER_FIELDS_TOO_LARGE = 431, // RFC6585 65 | UNAVAILABLE_FOR_LEGAL_REASONS = 451, 66 | INTERNAL_SERVER_ERROR = 500, 67 | NOT_IMPLEMENTED = 501, 68 | BAD_GATEWAY = 502, 69 | SERVICE_UNAVAILABLE = 503, 70 | GATEWAY_TIMEOUT = 504, 71 | VERSION_NOT_SUPPORTED = 505, 72 | VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506, // RFC2295 73 | INSUFFICIENT_STORAGE = 507, // RFC4918 74 | LOOP_DETECTED = 508, // RFC5842 75 | NOT_EXTENDED = 510, // RFC2774 76 | NETWORK_AUTHENTICATION_REQUIRED = 511, // RFC6585 77 | } 78 | } 79 | 80 | export * from './ErrorHandler' 81 | export * from './ErrorHandlerInterface' 82 | export * from './Handler' 83 | export * from './Request' 84 | export * from './Response' 85 | export * from './Errors/HttpError' 86 | -------------------------------------------------------------------------------- /Router/Metadata.ts: -------------------------------------------------------------------------------- 1 | import { MetadataKey } from '../Support/Metadata' 2 | import { Abstract, Type } from '../Support' 3 | import { MiddlewareInterface } from './Middleware' 4 | import { Http } from './Http' 5 | import { Guard } from './Guard' 6 | import { Container } from '@Typetron/Container' 7 | 8 | export class MethodMetadata { 9 | middleware: Abstract[] = [] 10 | parametersTypes: (Type<(...args: any[]) => any> | FunctionConstructor)[] 11 | name: string 12 | parametersOverrides: ((container: Container) => any)[] = [] 13 | guards: typeof Guard[] = [] 14 | } 15 | 16 | export class ActionMetadata extends MethodMetadata { 17 | } 18 | 19 | export class RouteMetadata extends MethodMetadata { 20 | path: string 21 | method: Http.Method 22 | } 23 | 24 | export class ControllerOptions { 25 | prefix?: string 26 | } 27 | 28 | export class ControllerMetadata extends MetadataKey('framework:controller') { 29 | middleware: Abstract[] = [] 30 | routes: {[key: string]: RouteMetadata} = {} 31 | actions: {[key: string]: ActionMetadata} = {} 32 | methods: {[key: string]: MethodMetadata} = {} 33 | guards: typeof Guard[] = [] 34 | } 35 | -------------------------------------------------------------------------------- /Router/Middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './Http' 2 | import { Request } from '@Typetron/Router/Request' 3 | 4 | export type RequestHandler = (request: Request) => Promise 5 | 6 | /** 7 | * @deprecated Please use the Middleware, HttpMiddleware or WebsocketMiddleware classes instead 8 | */ 9 | export type MiddlewareInterface = { 10 | handle(request: Request, next: RequestHandler): Promise 11 | } 12 | 13 | export abstract class GlobalMiddleware { 14 | abstract handle(request: Request, next: RequestHandler): Promise 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Router/Request.ts: -------------------------------------------------------------------------------- 1 | import { File } from '../Storage' 2 | 3 | export class Request { 4 | public parameters: Record = {} 5 | 6 | protected raw: { 7 | name: string, 8 | body?: string | object, 9 | cookies?: Record | string, 10 | files?: Record, 11 | } 12 | 13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 14 | // @ts-ignore 15 | protected details: { 16 | name: string, 17 | body: string | object | undefined, 18 | cookies: Record, 19 | files: Record, 20 | } = {} 21 | 22 | constructor( 23 | name: string, 24 | body?: string | object, 25 | cookies?: string | Record, 26 | files?: Record, 27 | ) { 28 | this.raw = { 29 | name, 30 | cookies, 31 | body, 32 | files, 33 | } 34 | } 35 | 36 | get name(): string { 37 | return this.details.name ?? (() => { 38 | return this.details.name = this.raw.name 39 | })() 40 | } 41 | 42 | get body() { 43 | return this.details.body ?? (() => { 44 | return this.details.body = this.raw.body 45 | })() 46 | } 47 | 48 | set body(body: string | object | undefined) { 49 | this.details.body = body 50 | } 51 | 52 | get cookies(): Record { 53 | return this.details.cookies ?? (() => { 54 | return {todo: 'parse cookies'} 55 | })() 56 | } 57 | 58 | get files() { 59 | return this.raw.files ?? {} 60 | } 61 | 62 | set files(files: Record) { 63 | this.raw.files = files 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Router/Route.ts: -------------------------------------------------------------------------------- 1 | import { Abstract, AnyFunction, Constructor, Type } from '../Support' 2 | import { GlobalMiddleware } from './Middleware' 3 | import { Guard } from './Guard' 4 | 5 | export abstract class Route { 6 | guards: (typeof Guard)[] = [] 7 | 8 | constructor( 9 | public name = '', 10 | public controller: Constructor, 11 | public controllerMethod: string, 12 | public parametersTypes: (Type | FunctionConstructor)[] = [], 13 | public middleware: Abstract[] = [] 14 | ) { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Router/RouteNotFoundError.ts: -------------------------------------------------------------------------------- 1 | export class RouteNotFoundError extends Error { 2 | constructor(path: string) { 3 | super(`Route '${path}' not found`) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Router/Router.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject } from '../Container' 2 | import { Storage } from '../Storage' 3 | import { Abstract } from '../Support' 4 | import { GlobalMiddleware } from './Middleware' 5 | import { WebsocketRoute } from './Websockets/WebsocketRoute' 6 | import { HttpRoute } from '@Typetron/Router/Http/HttpRoute' 7 | import { HttpMiddleware } from '@Typetron/Router/Http/Middleware' 8 | import { WebsocketMiddleware } from '@Typetron/Router/Websockets/Middleware' 9 | 10 | export class Router { 11 | 12 | @Inject() 13 | app: Container 14 | 15 | routes: HttpRoute[] = [] 16 | actions = new Map() 17 | 18 | middleware: { 19 | global: Abstract[], 20 | http: Abstract[], 21 | websocket: Abstract[], 22 | } = { 23 | global: [], 24 | http: [], 25 | websocket: [], 26 | } 27 | 28 | public loadControllers(directory: string) { 29 | this.app.get(Storage) 30 | .files(directory, true) 31 | .whereIn('extension', ['ts']) 32 | .forEach(file => require(file.path)) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Router/Servers/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from '@Typetron/Router/Http' 2 | 3 | export * from './node' 4 | export * from './uNetworking' 5 | 6 | export type AppServer = (port: number, handler: (request: Request) => Promise) => void 7 | -------------------------------------------------------------------------------- /Router/Servers/node.ts: -------------------------------------------------------------------------------- 1 | import { Http, Request, Response } from '@Typetron/Router/Http' 2 | import { createServer, IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http' 3 | import { Buffer } from 'buffer' 4 | 5 | export function nodeServer(port: number, handler: (request: Request) => Promise) { 6 | const server = createServer(async (incomingMessage: IncomingMessage, serverResponse: ServerResponse) => { 7 | const request = new Request( 8 | incomingMessage.url ?? '', 9 | incomingMessage.method as Http.Method || Http.Method.GET 10 | ) 11 | 12 | request.setHeadersLoader(() => incomingMessage.headers) 13 | request.getHeader = (name: keyof IncomingHttpHeaders | string): T => { 14 | return incomingMessage.headers[String(name).toLowerCase()] as T 15 | } 16 | 17 | if (request.method.toLowerCase() !== Http.Method.GET.toLowerCase()) { 18 | if (request.isMultipartRequest()) { 19 | [request.body, request.files] = await Request.loadMultipartContent(incomingMessage) 20 | } else { 21 | request.body = await Request.loadSimpleContent(incomingMessage) 22 | } 23 | 24 | // const overwrittenMethod = (request.body as Record)[Request.methodField] || '' 25 | // request.method = Http.Method[overwrittenMethod.toUpperCase() as Http.Method] || request.method 26 | } 27 | 28 | 29 | const response = await handler(request) 30 | 31 | let content = response.body 32 | 33 | if (!(content instanceof Buffer)) { 34 | if (content instanceof Object) { 35 | content = JSON.stringify(content) 36 | serverResponse.setHeader('Content-Type', 'application/json') 37 | } 38 | content = String(content ?? '') 39 | } 40 | 41 | for (const header in response.headers) { 42 | serverResponse.setHeader(header, response.headers[header] || '') 43 | } 44 | 45 | serverResponse.statusCode = response.status 46 | serverResponse.end(content) 47 | }) 48 | 49 | server.listen(port) 50 | } 51 | -------------------------------------------------------------------------------- /Router/Websockets/Handler.ts: -------------------------------------------------------------------------------- 1 | import { Container, Inject } from '../../Container' 2 | import { RequestHandler, Router } from '../../Router' 3 | import { Response } from '../Http' 4 | import { Request as BaseRequest } from '../Request' 5 | import { Abstract, Constructor, Type } from '../../Support' 6 | import { WebsocketRoute } from './WebsocketRoute' 7 | import { Request } from '@Typetron/Router/Websockets/Request' 8 | import { WebsocketMiddleware } from '@Typetron/Router/Websockets/Middleware' 9 | 10 | export class Handler { 11 | @Inject() 12 | router: Router 13 | 14 | onOpen?: WebsocketRoute 15 | onClose?: WebsocketRoute 16 | 17 | addAction( 18 | name: string, 19 | controller: Constructor, 20 | actionName: string, 21 | parametersTypes: (Type<(...args: any[]) => any> | FunctionConstructor)[] = [], 22 | middleware: Abstract[] = [] 23 | ) { 24 | const action = new WebsocketRoute( 25 | name, 26 | controller, 27 | actionName, 28 | parametersTypes, 29 | middleware 30 | ) 31 | if (this.router.actions.has(name)) { 32 | throw new Error(`There is already an action with the same name: '${name}'`) 33 | } 34 | this.router.actions.set(name, action) 35 | return action 36 | } 37 | 38 | async handle(container: Container, request: Request): Promise> { 39 | 40 | container.set(BaseRequest, request) 41 | container.set(Request, request) 42 | 43 | let stack: RequestHandler = async () => { 44 | const route = this.router.actions.get(request.name) 45 | 46 | if (!route) { 47 | throw new Error(`Action '${request.name}' not found`) 48 | } 49 | 50 | container.set(WebsocketRoute, route) 51 | 52 | let routeStack: RequestHandler = async () => { 53 | const content = await route.run(container, request.parameters) 54 | 55 | if (content instanceof Response) { 56 | return content 57 | } 58 | 59 | return Response.ok(content) 60 | } 61 | 62 | route.middleware.forEach(middlewareClass => { 63 | const middleware = container.get(middlewareClass) 64 | routeStack = middleware.handle.bind(middleware, request, routeStack) 65 | }) 66 | 67 | return await routeStack(request) 68 | } 69 | 70 | this.router.middleware.global?.forEach(middlewareClass => { 71 | const middleware = container.get(middlewareClass) 72 | stack = middleware.handle.bind(middleware, request, stack) 73 | }) 74 | 75 | this.router.middleware.websocket?.forEach(middlewareClass => { 76 | const middleware = container.get(middlewareClass) 77 | stack = middleware.handle.bind(middleware, request, stack) 78 | }) 79 | 80 | return await stack(request) as Response 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Router/Websockets/Middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request } from '@Typetron/Router/Websockets/Request' 2 | import { GlobalMiddleware, RequestHandler } from '@Typetron/Router' 3 | import { Response } from '@Typetron/Router/Http' // TODO remove any Http reference from websockets 4 | 5 | export abstract class WebsocketMiddleware extends GlobalMiddleware { 6 | abstract handle(request: Request, next: RequestHandler): Promise 7 | } 8 | -------------------------------------------------------------------------------- /Router/Websockets/Request.ts: -------------------------------------------------------------------------------- 1 | import { Request as BaseRequest } from '@Typetron/Router/Request' 2 | 3 | export class Request extends BaseRequest {} 4 | -------------------------------------------------------------------------------- /Router/Websockets/WebsocketRoute.ts: -------------------------------------------------------------------------------- 1 | import { Container } from '../../Container' 2 | import { AnyFunction, Constructor, Type } from '../../Support' 3 | import { ControllerMetadata, MethodMetadata } from '../Metadata' 4 | import { Route } from '@Typetron/Router' 5 | 6 | export class WebsocketRoute extends Route { 7 | 8 | async run(container: Container, requestParameters: Record): Promise { 9 | const controller = await container.get(this.controller) 10 | 11 | try { 12 | const metadata: MethodMetadata | undefined = ControllerMetadata.get(this.controller).actions[this.controllerMethod] 13 | const parameters = await this.resolveParameters( 14 | requestParameters, 15 | this.parametersTypes, 16 | metadata?.parametersOverrides ?? [], 17 | container 18 | ) 19 | 20 | for await (const guardClass of this.guards) { 21 | const guard = container.get(guardClass) 22 | if (!await guard.condition(...parameters)) { 23 | guard.onFail() 24 | } 25 | } 26 | 27 | return (controller[this.controllerMethod as keyof Constructor] as AnyFunction).apply(controller, parameters) 28 | } catch (error) { 29 | error.stack = `Controller: ${controller.constructor.name}.${this.controllerMethod} \n at ` + error.stack 30 | throw error 31 | } 32 | } 33 | 34 | private async resolveParameters( 35 | requestParameters: Record, 36 | parametersTypes: (Type | FunctionConstructor)[], 37 | overrides: AnyFunction[], 38 | container: Container 39 | ) { 40 | let parameterIndex = 0 41 | // tslint:disable-next-line:no-any 42 | const routeParameters: any[] = Object.values(requestParameters) 43 | return parametersTypes.mapAsync(async (parameter, index) => { 44 | const newValueFunction = overrides[index] 45 | if (newValueFunction) { 46 | return await newValueFunction.call(undefined, container) 47 | } 48 | if (parameter.name === 'String') { 49 | return routeParameters[parameterIndex++] 50 | } 51 | if (parameter.name === 'Number') { 52 | const value = routeParameters[parameterIndex++] 53 | return value === undefined || value === 'undefined' ? undefined : Number(value) 54 | } 55 | return container.get(parameter, [routeParameters[parameterIndex++]]) 56 | }) 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /Router/Websockets/index.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket as uWebSocket } from 'uWebSockets.js' 2 | import { ActionResponse, WebsocketMessageStatus } from './types' 3 | import { Container } from '@Typetron/Container' 4 | 5 | export * from './Handler' 6 | export * from './types' 7 | 8 | export class WebSocket { 9 | 10 | id?: number | string 11 | 12 | constructor(public connection: uWebSocket, public container: Container) { 13 | this.reset(container) 14 | } 15 | 16 | subscribe(topic: string) { 17 | this.connection.subscribe(topic) 18 | } 19 | 20 | unsubscribe(topic: string) { 21 | this.connection.unsubscribe(topic) 22 | } 23 | 24 | reset(container?: Container) { 25 | this.connection.container = container || this.connection.container.parent.createChildContainer() 26 | this.connection.container.set(WebSocket, this) 27 | } 28 | 29 | // tslint:disable-next-line:no-any 30 | publish(topic: string, action: string, body?: unknown) { 31 | const sentResponse: ActionResponse = { 32 | action, 33 | message: body, 34 | status: WebsocketMessageStatus.OK, 35 | } 36 | this.connection.publish(topic, JSON.stringify(sentResponse), false, true) 37 | } 38 | 39 | // tslint:disable-next-line:no-any 40 | publishAndSend(topic: string, action: string, body?: unknown) { 41 | this.publish(topic, action, body) 42 | this.send(action, body) 43 | } 44 | 45 | // tslint:disable-next-line:no-any 46 | send(action: string, body?: unknown) { 47 | const sentResponse: ActionResponse = { 48 | action, 49 | message: body, 50 | status: WebsocketMessageStatus.OK, 51 | } 52 | this.connection.send(JSON.stringify(sentResponse), false, true) 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Router/Websockets/types.ts: -------------------------------------------------------------------------------- 1 | export enum WebsocketMessageStatus { 2 | OK = 'OK', 3 | Error = 'Error', 4 | } 5 | 6 | export interface Action { 7 | action: string 8 | message?: T 9 | } 10 | 11 | export interface ActionRequestMessage { 12 | parameters?: (string | number)[] 13 | // tslint:disable-next-line:no-any 14 | body?: any 15 | } 16 | 17 | export type ActionRequest = Action 18 | 19 | export interface ActionResponse extends Action { 20 | status: WebsocketMessageStatus 21 | } 22 | 23 | export type ActionErrorResponse = ActionResponse<{message: T, stack: string}> 24 | -------------------------------------------------------------------------------- /Router/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Route' 2 | export * from './Router' 3 | export * from './Middleware' 4 | export * from './Decorators' 5 | export * from './RouteNotFoundError' 6 | export * from './BaseController' 7 | -------------------------------------------------------------------------------- /Storage/File.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | export class File { 4 | // content?: string | Buffer 5 | originalName?: string 6 | 7 | saved = false 8 | 9 | constructor(public name: string, public directory?: string) { 10 | } 11 | 12 | get extension() { 13 | return this.guessExtension() 14 | } 15 | 16 | get path() { 17 | return path.join(this.directory || '', this.name) 18 | } 19 | 20 | guessExtension() { 21 | const fileParts = this.name.split('.') 22 | if (fileParts.length >= 1) { 23 | return fileParts.pop() 24 | } 25 | } 26 | 27 | toString() { 28 | return this.name 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Storage/Image.ts: -------------------------------------------------------------------------------- 1 | import { File } from './File' 2 | 3 | export class Image extends File { 4 | // static fromBase64(base64: string): Image { 5 | // const matches = base64.match(/data:image\/([a-zA-Z]*);base64,([^\\"]*)/) 6 | // if (!matches) { 7 | // throw new InvalidImage() 8 | // } 9 | // if (matches.length !== 3) { 10 | // throw InvalidImage 11 | // } 12 | // const extension = matches[1] 13 | // const data = matches[2] 14 | // 15 | // const image = new this('.' + extension) 16 | // image.content = Buffer.from(data, 'base64') 17 | // return image 18 | // } 19 | } 20 | -------------------------------------------------------------------------------- /Storage/InvalidImage.ts: -------------------------------------------------------------------------------- 1 | export class InvalidImage extends Error { 2 | constructor() {super('The image has an invalid data format') } 3 | } 4 | -------------------------------------------------------------------------------- /Storage/Storage.ts: -------------------------------------------------------------------------------- 1 | import * as fileSystemDeprecated from 'fs' 2 | import { File } from './File' 3 | import * as path from 'path' 4 | import * as fileSystem from 'node:fs/promises' 5 | 6 | export class Storage { 7 | files(directory: string, recursively = false): File[] { 8 | return fileSystemDeprecated 9 | .readdirSync(directory) // TODO make this async 10 | .reduce((files, name) => { 11 | const filePath = path.join(directory, name) 12 | const stats = fileSystemDeprecated.statSync(filePath) 13 | if (stats.isFile()) { 14 | const file = new File(name) 15 | file.directory = directory 16 | files.push(file) 17 | } 18 | 19 | if (recursively && stats.isDirectory()) { 20 | files = files.concat(this.files(filePath)) 21 | } 22 | return files 23 | }, [] as File[]) 24 | } 25 | 26 | async read(filePath: string): Promise { 27 | return fileSystem.readFile(filePath) 28 | } 29 | 30 | async exists(filePath: string): Promise { 31 | try { 32 | await fileSystem.access(filePath) 33 | return true 34 | } catch (error) { 35 | return false 36 | } 37 | } 38 | 39 | async save(file: File, directory: string = '', name?: string | number): Promise { 40 | const newPath = path.join(directory, file.name) 41 | if (!file.name) { 42 | file.name = String(name || this.generateRandomFileName() + '.' + file.extension) 43 | } 44 | if (directory && !(await this.exists(directory))) { 45 | await this.makeDirectory(directory) 46 | } 47 | return new Promise(async (resolve, reject) => { 48 | fileSystemDeprecated 49 | .createReadStream(file.path) 50 | .on('error', reject) 51 | .pipe(fileSystemDeprecated.createWriteStream(newPath)) 52 | .on('finish', () => resolve(file)) 53 | .on('error', reject) 54 | }) 55 | } 56 | 57 | async put(filePath: string, content: string | Buffer, options?: any): Promise { 58 | const file = new File(path.basename(filePath)) 59 | file.directory = path.dirname(filePath) 60 | 61 | if (file.directory && !(await this.exists(file.directory))) { 62 | await this.makeDirectory(file.directory) 63 | } 64 | 65 | await fileSystem.writeFile(filePath, content, options) 66 | return file 67 | } 68 | 69 | async makeDirectory(directory: string): Promise { 70 | await fileSystem.mkdir(directory, {recursive: true}) 71 | } 72 | 73 | async deleteDirectory(directory: string): Promise { 74 | if (await this.exists(directory)) { 75 | await fileSystem.rm(directory, {recursive: true}) 76 | } 77 | } 78 | 79 | async delete(filePath: string): Promise { 80 | if (!(await this.exists(filePath))) { 81 | return 82 | } 83 | 84 | if ((await fileSystem.stat(filePath)).isDirectory()) { 85 | throw new Error('Can not delete because this path leads to a directory and not a file.') 86 | } 87 | 88 | await fileSystem.unlink(filePath) 89 | } 90 | 91 | private generateRandomFileName(): string { 92 | const stringDomain = 93 | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 94 | return ( 95 | String.random(13, stringDomain) + '-' + new Date().getTime().toString() 96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './File' 2 | export * from './Storage' 3 | export * from './Image' 4 | export * from './utils' 5 | -------------------------------------------------------------------------------- /Storage/utils.ts: -------------------------------------------------------------------------------- 1 | import { InvalidImage } from '@Typetron/Storage/InvalidImage' 2 | 3 | export function fromBase64(base64: string): {extension: string, content: Buffer} { 4 | const matches = base64.match(/data:image\/([a-zA-Z]*);base64,([^\\"]*)/) 5 | if (!matches) { 6 | throw new InvalidImage() 7 | } 8 | if (matches.length !== 3) { 9 | throw InvalidImage 10 | } 11 | const extension = matches[1] 12 | const data = matches[2] 13 | 14 | return { 15 | content: Buffer.from(data, 'base64'), 16 | extension: extension 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Support/Math.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | interface Math { 4 | randomInt(min?: number, max?: number): number; 5 | } 6 | } 7 | 8 | Math.randomInt = function (min: number = Number.MIN_SAFE_INTEGER, max: number = Number.MAX_SAFE_INTEGER) { 9 | min = Math.ceil(min) 10 | max = Math.floor(max) 11 | return Math.floor(Math.random() * (max - min + 1)) + min 12 | } 13 | -------------------------------------------------------------------------------- /Support/Metadata.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from './index' 2 | 3 | export function MetadataKey(key: string) { 4 | return class Metadata { 5 | // tslint:disable-next-line:no-any 6 | static get(this: typeof Metadata & Constructor, target: any, defaultValue?: T): T { 7 | // This handles the copy of dependencies from the parent to the child. 8 | // It's hacky because `newInjectable.dependencies` is a property of InjectableMetadata 9 | if (target.__proto__._metadata_?.['injectable'] && target.__proto__._metadata_ === target._metadata_) { 10 | const newInjectable = new this 11 | Object.assign(newInjectable, target.__proto__._metadata_['injectable']) 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 13 | // @ts-ignore 14 | newInjectable.dependencies = {...target.__proto__._metadata_['injectable'].dependencies} 15 | target['_metadata_'] = {injectable: newInjectable} 16 | return newInjectable 17 | } 18 | return (target['_metadata_'] || (target['_metadata_'] = {[key]: new this}))[key] 19 | || (target['_metadata_'][key] = defaultValue ?? new this) 20 | } 21 | 22 | static set(this: typeof Metadata & Constructor, metadata: T, target: any) { 23 | const metadataBag = target['_metadata_'] || (target['_metadata_'] = {}) 24 | return metadataBag[key] = metadata 25 | } 26 | } 27 | } 28 | 29 | export class ParametersTypesMetadata extends MetadataKey('design:paramtypes') { 30 | parameters: T 31 | } 32 | -------------------------------------------------------------------------------- /Support/String.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | interface StringConstructor { 4 | random(length?: number, stringDomain?: string): string; 5 | 6 | randomAlphaNum(length?: number): string; 7 | } 8 | 9 | interface String { 10 | capitalize(): this; 11 | 12 | limit(length: number, end?: string): this; 13 | } 14 | } 15 | 16 | const string = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~' 17 | String.random = function(length = Math.randomInt(1, 15), stringDomain = string) { 18 | const characters = stringDomain.split('') 19 | let word = '' 20 | for (let i = 0; i < length; i++) { 21 | word += characters.random() 22 | } 23 | return word 24 | } 25 | String.randomAlphaNum = function(length: number) { 26 | return this.random(length, 'abcdefghijklmnopqrstuvwxyz1234567890') 27 | } 28 | 29 | String.prototype.capitalize = function () { 30 | return this.charAt(0).toUpperCase() + this.slice(1) 31 | } 32 | 33 | String.prototype.limit = function (limit: number, end = '...') { 34 | if (limit < this.length) { 35 | return this.substring(0, limit - end.length) + end 36 | } 37 | return this 38 | } 39 | -------------------------------------------------------------------------------- /Support/index.ts: -------------------------------------------------------------------------------- 1 | import './Array' 2 | import './String' 3 | import './Math' 4 | import './utils' 5 | 6 | export type KeysOfType = { [P in keyof T]: T[P] extends Condition ? P : never }[keyof T] 7 | export type ChildKeys = Exclude 8 | export type ChildObject = Omit 9 | 10 | // tslint:disable-next-line:no-any 11 | export type Type = new(...args: any[]) => T 12 | export type AnyFunction = (...args: any[]) => T 13 | 14 | export interface Abstract { 15 | prototype: T; 16 | } 17 | 18 | // tslint:disable-next-line:no-any 19 | export type Constructor = new(...args: any[]) => T 20 | -------------------------------------------------------------------------------- /Support/utils.ts: -------------------------------------------------------------------------------- 1 | export function JSONSafeParse(text?: string) { 2 | try { 3 | return text ? JSON.parse(text) as T : undefined 4 | } catch (error) { 5 | return undefined 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Validation/Rule.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface } from './RuleInterface' 2 | 3 | export abstract class Rule implements RuleInterface { 4 | abstract identifier: string 5 | 6 | abstract message(attribute: string, value: string | number | object | boolean | undefined): string; 7 | 8 | abstract passes(attribute: string, value: string | number | object | boolean | undefined): boolean ; 9 | } 10 | -------------------------------------------------------------------------------- /Validation/RuleInterface.ts: -------------------------------------------------------------------------------- 1 | import { RuleValue } from '.' 2 | 3 | export interface RuleInterface { 4 | identifier: string; 5 | 6 | passes(attribute: string, value?: RuleValue): boolean; 7 | 8 | message(attribute: string, value: RuleValue): string; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /Validation/Rules/Email.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface, RuleValue } from '..' 2 | import { Rule } from '../Rule' 3 | import { Type } from '@Typetron/Support' 4 | 5 | export function Email(message?: string): Type { 6 | return class extends Rule { 7 | identifier = 'email' 8 | 9 | private regex = /^(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])$/ 10 | 11 | passes(attribute: string, value: RuleValue): boolean { 12 | return this.regex.test(String(value).trim()) 13 | } 14 | 15 | message(attribute: string): string { 16 | return message ?? `The ${attribute} must be a valid email address` 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Validation/Rules/FileExtension.ts: -------------------------------------------------------------------------------- 1 | import { Rule, RuleInterface } from '@Typetron/Validation' 2 | import { Type } from '@Typetron/Support' 3 | import { File } from '@Typetron/Storage' 4 | 5 | export function FileExtension(extensions: string[], message?: string): Type { 6 | return class extends Rule { 7 | identifier = 'fileExtension' 8 | 9 | passes(attribute: string, value?: File): boolean { 10 | return value?.extension ? extensions.includes(value.extension) : false 11 | } 12 | 13 | message(attribute: string, value?: File): string { 14 | const messagePart = extensions.length === 1 15 | ? `the '.${extensions.first()}' extension` 16 | : `these extensions: ${extensions.map(extension => `.${extension}`).join(', ')}` 17 | 18 | return message ?? `The ${attribute} must have ${messagePart}` 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Validation/Rules/MaxLength.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface, RuleValue } from '..' 2 | import { Rule } from '../Rule' 3 | import { Type } from '../../Support' 4 | 5 | export function MaxLength(max: number, message?: string): Type { 6 | return class extends Rule { 7 | identifier = 'maxLength' 8 | 9 | passes(attribute: string, value: RuleValue): boolean { 10 | return Boolean(value && String(value).length <= max) 11 | } 12 | 13 | message(attribute: string): string { 14 | return message ?? `The ${attribute} must have at most ${max} characters` 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Validation/Rules/MinLength.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface } from '../RuleInterface' 2 | import { RuleValue } from '..' 3 | import { Type } from '../../Support' 4 | import { Rule } from '../Rule' 5 | 6 | export function MinLength(min: number, message?: string): Type { 7 | return class extends Rule { 8 | identifier = 'minLength' 9 | 10 | passes(attribute: string, value: RuleValue): boolean { 11 | return Boolean(value && String(value).length >= min) 12 | } 13 | 14 | message(attribute: string): string { 15 | return message ?? `The ${attribute} must have at least ${min} characters` 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Validation/Rules/Optional.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface, RuleValue } from '..' 2 | import { Rule } from '../Rule' 3 | import { Type } from '@Typetron/Support' 4 | 5 | export function Optional(): Type { 6 | return class extends Rule { 7 | identifier = 'optional' 8 | 9 | passes(attribute: string, value: RuleValue): boolean { 10 | return true // TODO 11 | } 12 | 13 | message(attribute: string): string { 14 | return '' 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Validation/Rules/Required.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface, RuleValue } from '..' 2 | import { Type } from '@Typetron/Support' 3 | 4 | export function Required(message?: string): Type { 5 | return class { 6 | identifier = 'required' 7 | 8 | passes(attribute: string, value: RuleValue): boolean { 9 | 10 | if (value === undefined || value === null) { 11 | return false 12 | } 13 | 14 | if (typeof value === 'string' && value.trim() === '') { 15 | return false 16 | } 17 | 18 | if (value.constructor === Array && value.length === 0) { 19 | return false 20 | } 21 | 22 | return true 23 | } 24 | 25 | message(attribute: string): string { 26 | return message ?? `The ${attribute} is required` 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Validation/Validator.ts: -------------------------------------------------------------------------------- 1 | import { RuleInterface } from './RuleInterface' 2 | import { Type } from '../Support' 3 | 4 | export abstract class Validator { 5 | protected constructor(public data: T, public rules: { [key in keyof T]?: (RuleInterface | Type)[] }) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Validation/index.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-any 2 | export type RuleValue = any 3 | export * from './RuleInterface' 4 | export * from './Validator' 5 | export * from './Rule' 6 | export * from './Rules/MaxLength' 7 | export * from './Rules/MinLength' 8 | export * from './Rules/FileExtension' 9 | export * from './Rules/Required' 10 | export * from './Rules/Email' 11 | export * from './Rules/Optional' 12 | // export * from './Rules/FileExtension' 13 | -------------------------------------------------------------------------------- /benchmark.ts: -------------------------------------------------------------------------------- 1 | // clinic flame -- node -r ts-node/register -r tsconfig-paths/register benchmark.js 2 | 3 | import 'reflect-metadata' 4 | import '@Typetron/Support' 5 | import { Handler, Http, Request } from './Router/Http' 6 | import { Container } from './Container' 7 | import { Get } from './Router' 8 | import Method = Http.Method 9 | 10 | class Controller { 11 | @Get() 12 | read() { 13 | return 'Hello world!!!' 14 | } 15 | } 16 | 17 | async function main() { 18 | 19 | const app = new Container() 20 | const handler = app.get(Handler) 21 | 22 | handler.addRoute('', Method.GET, Controller, 'read', 'read') 23 | 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-ignore 26 | // const request = await Request.capture({ 27 | // url: 'http://localhost:3000', 28 | // method: 'GET', 29 | // headers: {} 30 | // }) 31 | // await handler.handle(app, request) 32 | 33 | console.time('test') 34 | for (let i = 0; i < 1_000_000_0; i++) { 35 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 36 | // @ts-ignore 37 | const request = await Request.capture({ 38 | url: 'http://localhost:3000', 39 | method: 'GET', 40 | headers: {} 41 | }) 42 | await handler.handle(app, request) 43 | } 44 | console.timeEnd('test') 45 | } 46 | 47 | main() 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@typetron/framework", 3 | "description": "Modern Node.js framework for modern apps written in Typescript", 4 | "license": "MIT", 5 | "version": "0.4.0-rc13", 6 | "keywords": [ 7 | "typetron", 8 | "framework", 9 | "web", 10 | "backend", 11 | "typescript" 12 | ], 13 | "author": { 14 | "email": "ionel@typetron.org", 15 | "name": "Ionel Lupu" 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "dev": "tsc --watch", 20 | "test": "mocha", 21 | "test:watch": "mocha --watch", 22 | "preversion": "npm test", 23 | "lint": "eslint -c .eslintrc.json --ext .ts ." 24 | }, 25 | "engines": { 26 | "node": ">= 12.13.0" 27 | }, 28 | "dependencies": { 29 | "@sendgrid/mail": "^7.7.0", 30 | "bcryptjs": "^2.4.3", 31 | "busboy": "^1.6.0", 32 | "fast-url-parser": "^1.1.3", 33 | "jsonwebtoken": "^9.0.0", 34 | "mysql": "^2.18.1", 35 | "sqlite3": "^5.0.2", 36 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.31.0", 37 | "reflect-metadata": "^0.1.13" 38 | }, 39 | "devDependencies": { 40 | "@testdeck/mocha": "^0.1.2", 41 | "@types/bcryptjs": "^2.4.2", 42 | "@types/busboy": "^1.5.1", 43 | "@types/chai": "^4.2.21", 44 | "@types/jsonwebtoken": "^8.5.5", 45 | "@types/mocha": "^9.0.0", 46 | "@types/mysql": "^2.15.19", 47 | "@types/node": "^16.9.3", 48 | "@types/sqlite3": "^3.1.7", 49 | "@typescript-eslint/eslint-plugin": "^6.2.1", 50 | "@typescript-eslint/parser": "^6.2.1", 51 | "chai": "^4.3.4", 52 | "eslint": "^8.46.0", 53 | "mocha": "^9.1.1", 54 | "ts-mockito": "^2.6.1", 55 | "nyc": "^15.1.0", 56 | "source-map-support": "^0.5.20", 57 | "ts-node": "^10.2.1", 58 | "tsconfig-paths": "^3.11.0", 59 | "tslint": "^6.1.3", 60 | "typescript": "4.1.6" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/typetron/framework.git" 65 | }, 66 | "files": [ 67 | "**/*.ts", 68 | "**/*.js", 69 | "**/*.ts.map", 70 | "**/*.js.map" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /tests/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "lines": 80, 4 | "statements": 80, 5 | "functions": 80, 6 | "branches": 80, 7 | "include": [ 8 | "./**/*.ts" 9 | ], 10 | "exclude": [ 11 | "./tests/**/*.ts", 12 | "./dist/**" 13 | ], 14 | "reporter": [ 15 | "lcov", 16 | "text-summary" 17 | ], 18 | "all": true 19 | } 20 | -------------------------------------------------------------------------------- /tests/Cache/DatabaseStoreTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { CacheItem, DatabaseStore } from '@Typetron/Cache' 3 | import { expect } from 'chai' 4 | import { createHash } from 'crypto' 5 | import { Connection, Query, SqliteDriver } from '@Typetron/Database' 6 | import { Storage } from '@Typetron/Storage' 7 | 8 | const tableName = 'cache' 9 | const databaseFilePath = './tests/Cache/database.sqlite' 10 | 11 | @suite 12 | class DatabaseStoreTest { 13 | 14 | databaseStore = new DatabaseStore(tableName) 15 | 16 | static async before() { 17 | Query.connection = new Connection(new SqliteDriver(databaseFilePath)) 18 | } 19 | 20 | static async after() { 21 | await new Storage().deleteDirectory(databaseFilePath) 22 | } 23 | 24 | private getCacheRecord(name: string) { 25 | const cacheKeyHash = createHash('sha1').update(name).digest('hex') 26 | return Query.table>(tableName).where('name', cacheKeyHash).first() 27 | } 28 | 29 | @test 30 | async returnsUndefinedIfRecordDoesntExist() { 31 | const value = await this.databaseStore.get('tests') 32 | expect(value).to.be.undefined 33 | } 34 | 35 | @test 36 | async testPutCreatesRecord() { 37 | const contents = 'test' 38 | 39 | await this.databaseStore.set('foo', contents) 40 | 41 | const cacheRecord = await this.getCacheRecord('foo') 42 | expect(cacheRecord).to.exist 43 | expect((cacheRecord as CacheItem).value).to.be.equal(JSON.stringify(contents)) 44 | } 45 | 46 | @test 47 | async testExpirationValueIsWrittenInCacheRecord() { 48 | const durationInSeconds = 60 49 | const expirationTime = Date.now() + durationInSeconds * 1000 50 | 51 | await this.databaseStore.set('expireTest', 'value', durationInSeconds) 52 | const cacheRecord = await this.getCacheRecord('expireTest') 53 | 54 | expect(cacheRecord).to.exist 55 | expect(new Date((cacheRecord as CacheItem).date ?? Infinity).getTime()).to.be.closeTo(expirationTime, 1000) // Allowing a 1-second difference 56 | } 57 | 58 | @test 59 | async testCacheValueIsUndefinedIfExpired() { 60 | const durationInSeconds = -60 // Setting a past expiration 61 | 62 | await this.databaseStore.set('anotherExpiredTest', 'value', durationInSeconds) 63 | 64 | const value = await this.databaseStore.get('anotherExpiredTest') 65 | expect(value).to.be.undefined 66 | 67 | const cacheRecord = await this.getCacheRecord('anotherExpiredTest') 68 | expect(cacheRecord).to.be.undefined 69 | } 70 | 71 | @test 72 | async testDeleteMethodDeletesRecordFromDatabase() { 73 | await this.databaseStore.set('deleteTest', 'value') 74 | 75 | await this.databaseStore.delete('deleteTest') 76 | 77 | const cacheRecord = await this.getCacheRecord('deleteTest') 78 | expect(cacheRecord).to.be.undefined 79 | } 80 | 81 | @test 82 | async testFlushMethodDeletesAllRecords() { 83 | await this.databaseStore.set('flushTest', 'value') 84 | 85 | await this.databaseStore.flush() 86 | 87 | const allRecords = await Query.table(tableName).get() 88 | expect(allRecords.length).to.be.equal(0) 89 | } 90 | 91 | @test 92 | async testRememberMethodCreatesRecordIfNotExists() { 93 | const value = await this.databaseStore.remember('rememberTest', 'defaultValue', 60) 94 | 95 | expect(value).to.equal('defaultValue') 96 | 97 | const cacheRecord = await this.getCacheRecord('rememberTest') 98 | expect(cacheRecord).to.exist 99 | expect((cacheRecord as CacheItem).value).to.be.equal(JSON.stringify('defaultValue')) 100 | } 101 | 102 | @test 103 | async testRememberMethodReturnsExistingValue() { 104 | await this.databaseStore.set('rememberTest2', 'existingValue') 105 | 106 | const value = await this.databaseStore.remember('rememberTest2', 'defaultValue', 60) 107 | 108 | expect(value).to.equal('existingValue') 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Cache/FileStoreTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { FileStore } from '@Typetron/Cache' 3 | import { Storage } from '@Typetron/Storage' 4 | import { expect } from 'chai' 5 | 6 | const directory = './tests/Cache/.cache' 7 | 8 | @suite 9 | class FileStoreTest { 10 | storage = new Storage() 11 | fileStore = new FileStore(this.storage, directory) 12 | 13 | static async after() { 14 | const storage = new Storage() 15 | if (await storage.exists(directory)) { 16 | await storage.deleteDirectory(directory) 17 | } 18 | } 19 | 20 | @test 21 | async returnsUndefinedIfKeyDoesNotExist() { 22 | const value = await this.fileStore.get('nonexistentKey') 23 | expect(value).to.be.undefined 24 | } 25 | 26 | @test 27 | async canStoreAndRetrieveData() { 28 | await this.fileStore.set('testKey', 'testValue') 29 | const value = await this.fileStore.get('testKey') 30 | expect(value).to.equal('testValue') 31 | } 32 | 33 | @test 34 | async canDeleteData() { 35 | await this.fileStore.set('testKey', 'testValue') 36 | await this.fileStore.delete('testKey') 37 | const value = await this.fileStore.get('testKey') 38 | expect(value).to.be.undefined 39 | } 40 | 41 | @test 42 | async canFlushAllData() { 43 | await this.fileStore.set('key1', 'value1') 44 | await this.fileStore.set('key2', 'value2') 45 | await this.fileStore.flush() 46 | const value1 = await this.fileStore.get('key1') 47 | const value2 = await this.fileStore.get('key2') 48 | expect(value1).to.be.undefined 49 | expect(value2).to.be.undefined 50 | } 51 | 52 | @test 53 | async respectsExpiration() { 54 | await this.fileStore.set('expiringKey', 'expiringValue', 0.01) 55 | await new Promise(resolve => setTimeout(resolve, 11)) 56 | const value = await this.fileStore.get('expiringKey') 57 | expect(value).to.be.undefined 58 | } 59 | 60 | @test 61 | async canCheckIfKeyExists() { 62 | await this.fileStore.set('testKey', 'testValue') 63 | const exists = await this.fileStore.has('testKey') 64 | expect(exists).to.be.true 65 | const nonExistent = await this.fileStore.has('nonexistentKey') 66 | expect(nonExistent).to.be.false 67 | } 68 | 69 | @test 70 | async canRememberValue() { 71 | const value = await this.fileStore.remember('rememberKey', 'rememberValue', 0.01) 72 | expect(value).to.equal('rememberValue') 73 | await new Promise(resolve => setTimeout(resolve, 11)) 74 | const expiredValue = await this.fileStore.get('rememberKey') 75 | expect(expiredValue).to.be.undefined 76 | } 77 | 78 | @test 79 | async getMethodCanAcceptNonFunctionAndFunctionAsDefaultValue() { 80 | const value1 = await this.fileStore.get('nonexistentKey', 'defaultValue') 81 | expect(value1).to.equal('defaultValue') 82 | const value2 = await this.fileStore.get('nonexistentKey', () => 'defaultValue') 83 | expect(value2).to.equal('defaultValue') 84 | } 85 | 86 | @test 87 | async rememberMethodCanAcceptNonFunctionAndFunctionAsDefaultValue() { 88 | const value1 = await this.fileStore.remember('nonexistentKey', 'defaultValue', 0.01) 89 | expect(value1).to.equal('defaultValue') 90 | const value2 = await this.fileStore.remember('nonexistentKey2', () => 'defaultValue', 0.01) 91 | expect(value2).to.equal('defaultValue') 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Cache/MemoryStoreTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { MemoryStore } from '@Typetron/Cache' 3 | import { expect } from 'chai' 4 | 5 | @suite 6 | class MemoryStoreTest { 7 | memoryStore = new MemoryStore() 8 | 9 | @test 10 | async returnsUndefinedIfKeyDoesNotExist() { 11 | const value = await this.memoryStore.get('nonexistentKey') 12 | expect(value).to.be.undefined 13 | } 14 | 15 | @test 16 | async canStoreAndRetrieveData() { 17 | await this.memoryStore.set('testKey', 'testValue') 18 | expect(this.memoryStore.cache.has('testKey')).to.be.true 19 | const value = await this.memoryStore.get('testKey') 20 | expect(value).to.equal('testValue') 21 | } 22 | 23 | @test 24 | async canDeleteData() { 25 | await this.memoryStore.set('testKey', 'testValue') 26 | await this.memoryStore.delete('testKey') 27 | const value = await this.memoryStore.get('testKey') 28 | expect(value).to.be.undefined 29 | } 30 | 31 | @test 32 | async canFlushAllData() { 33 | await this.memoryStore.set('key1', 'value1') 34 | await this.memoryStore.set('key2', 'value2') 35 | await this.memoryStore.flush() 36 | const value1 = await this.memoryStore.get('key1') 37 | const value2 = await this.memoryStore.get('key2') 38 | expect(value1).to.be.undefined 39 | expect(value2).to.be.undefined 40 | } 41 | 42 | @test 43 | async respectsExpiration() { 44 | await this.memoryStore.set('expiringKey', 'expiringValue', 0.01) 45 | await new Promise(resolve => setTimeout(resolve, 11)) 46 | const value = await this.memoryStore.get('expiringKey') 47 | expect(value).to.be.undefined 48 | } 49 | 50 | @test 51 | async canCheckIfKeyExists() { 52 | await this.memoryStore.set('testKey', 'testValue') 53 | const exists = await this.memoryStore.has('testKey') 54 | expect(exists).to.be.true 55 | const nonExistent = await this.memoryStore.has('nonexistentKey') 56 | expect(nonExistent).to.be.false 57 | } 58 | 59 | @test 60 | async canRememberValue() { 61 | const value = await this.memoryStore.remember('rememberKey', 'rememberValue', 0.01) 62 | expect(value).to.equal('rememberValue') 63 | expect(this.memoryStore.cache.has('rememberKey')).to.be.true 64 | await new Promise(resolve => setTimeout(resolve, 11)) 65 | const expiredValue = await this.memoryStore.get('rememberKey') 66 | expect(expiredValue).to.be.undefined 67 | } 68 | 69 | @test 70 | async getMethodCanAcceptNonFunctionAndFunctionAsDefaultValue() { 71 | // Non-function default value 72 | const value1 = await this.memoryStore.get('nonexistentKey', 'defaultValue') 73 | expect(value1).to.equal('defaultValue') 74 | 75 | // Function default value 76 | const value2 = await this.memoryStore.get('nonexistentKey', () => 'defaultValue') 77 | expect(value2).to.equal('defaultValue') 78 | } 79 | 80 | @test 81 | async rememberMethodCanAcceptNonFunctionAndFunctionAsDefaultValue() { 82 | // Non-function default value 83 | const value1 = await this.memoryStore.remember('nonexistentKey', 'defaultValue', 0.01) 84 | expect(value1).to.equal('defaultValue') 85 | expect(this.memoryStore.cache.has('nonexistentKey')).to.be.true 86 | 87 | // Function default value 88 | const value2 = await this.memoryStore.remember('nonexistentKey2', () => 'defaultValue', 0.01) 89 | expect(value2).to.equal('defaultValue') 90 | expect(this.memoryStore.cache.has('nonexistentKey2')).to.be.true 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Database/Drivers/MySQL/AlterTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { expectQuery } from '@Typetron/tests/Database/utils' 3 | import { AddColumn, AddConstraint, Alter, Constraints, DropColumn, ModifyColumn } from '@Typetron/Database/Drivers/MySQL/Alter' 4 | 5 | @suite 6 | class AlterTest { 7 | 8 | @test 9 | async addColumn() { 10 | const addAge = new AddColumn({ 11 | name: 'age', 12 | type: 'integer', 13 | }) 14 | const statement = new Alter('test', [addAge]) 15 | expectQuery(statement).toEqual('ALTER TABLE test ADD age integer') 16 | } 17 | 18 | @test 19 | async addPrimaryKeyColumn() { 20 | const addAge = new AddColumn({ 21 | name: 'age', 22 | type: 'integer', 23 | }) 24 | const statement = new Alter('test', [addAge]) 25 | expectQuery(statement).toEqual('ALTER TABLE test ADD age integer') 26 | } 27 | 28 | @test 29 | async dropColumn() { 30 | const age = new DropColumn('age') 31 | const statement = new Alter('test', [age]) 32 | expectQuery(statement).toEqual('ALTER TABLE test DROP COLUMN age') 33 | } 34 | 35 | @test 36 | async ModifyColumn() { 37 | const age = new ModifyColumn({ 38 | name: 'age', 39 | type: 'integer', 40 | }) 41 | const statement = new Alter('test', [age]) 42 | expectQuery(statement).toEqual('ALTER TABLE test MODIFY age integer') 43 | } 44 | 45 | @test 46 | async ModifyColumnWithAutoIncrement() { 47 | const age = new ModifyColumn({ 48 | name: 'age', 49 | type: 'integer', 50 | autoIncrement: 1 51 | }) 52 | const statement = new Alter('test', [age]) 53 | expectQuery(statement).toEqual('ALTER TABLE test MODIFY age integer AUTO_INCREMENT') 54 | } 55 | 56 | @test 57 | async addPrimaryKeyConstraint() { 58 | const age = new AddConstraint({ 59 | name: 'age', 60 | type: 'integer', 61 | }, Constraints.PrimaryKey) 62 | const statement = new Alter('test', [age]) 63 | expectQuery(statement).toEqual('ALTER TABLE test ADD CONSTRAINT PRIMARY KEY (age)') 64 | } 65 | 66 | @test 67 | async addForeignKeyConstraint() { 68 | const age = new AddConstraint({ 69 | name: 'age', 70 | type: 'integer', 71 | }, Constraints.ForeignKey) 72 | const statement = new Alter('test', [age]) 73 | expectQuery(statement).toEqual('ALTER TABLE test ADD CONSTRAINT FOREIGN KEY (age)') 74 | } 75 | 76 | @test 77 | async altersMultipleColumns() { 78 | const addAge = new AddColumn({ 79 | name: 'age', 80 | type: 'integer', 81 | }) 82 | const dropName = new DropColumn('age') 83 | const dropTitle = new DropColumn('title') 84 | 85 | const addAddress = new AddColumn({ 86 | name: 'address', 87 | type: 'varchar(255)', 88 | }) 89 | const statement = new Alter('test', [addAge, dropName, dropTitle, addAddress]) 90 | expectQuery(statement).toEqual(` 91 | ALTER TABLE test 92 | ADD age integer, 93 | DROP 94 | COLUMN age, 95 | DROP 96 | COLUMN title, 97 | ADD address varchar(255) 98 | `) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/Database/Drivers/MySQL/CreateTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { Create } from '@Typetron/Database/Drivers/MySQL/Create' 3 | import { expectQuery } from '@Typetron/tests/Database/utils' 4 | 5 | @suite 6 | class CreateTest { 7 | 8 | @test 9 | async createsEmptyTable() { 10 | const statement = new Create('test') 11 | expectQuery(statement).toEqual('CREATE TABLE test ( )') 12 | } 13 | 14 | @test 15 | async createsTableWithIntegerColumn() { 16 | const statement = new Create('test', [{ 17 | name: 'age', 18 | type: 'integer', 19 | }]) 20 | expectQuery(statement).toEqual('CREATE TABLE test ( age integer )') 21 | } 22 | 23 | @test 24 | async createsTableWithPrimaryAutoIncrementColumn() { 25 | const statement = new Create('test', [{ 26 | name: 'id', 27 | type: 'integer', 28 | primaryKey: true, 29 | autoIncrement: 1 30 | }]) 31 | expectQuery(statement).toEqual(` 32 | CREATE TABLE test 33 | ( 34 | id integer AUTO_INCREMENT, 35 | PRIMARY KEY (id) 36 | ) 37 | `) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Database/Drivers/SqliteDriverTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { BelongsTo, Column, Entity, HasOne, ID, Options, PrimaryColumn, Relation, SqliteDriver } from '@Typetron/Database' 3 | import { anyString, instance, mock, when } from '@Typetron/node_modules/ts-mockito' 4 | import { Schema } from '@Typetron/Database/Drivers/SQLite/Schema' 5 | import { expectQuery } from '@Typetron/tests/Database/utils' 6 | import { expect } from 'chai' 7 | import { DatabaseDriver } from '@Typetron/Database/Drivers/DatabaseDriver' 8 | 9 | @suite 10 | class SqliteDriverTest { 11 | 12 | driver: DatabaseDriver 13 | schema: Schema 14 | query: string 15 | 16 | async before() { 17 | this.driver = mock(SqliteDriver) 18 | this.schema = new Schema(instance(this.driver)) 19 | when(this.driver.run(anyString())).thenCall((query: string) => this.query = query) 20 | } 21 | 22 | @test 23 | async createsEmptyTable() { 24 | @Options() 25 | class Test extends Entity {} 26 | 27 | await this.schema.synchronize([Test].pluck('metadata')) 28 | expectQuery(this.query).toEqual('CREATE TABLE test ( )') 29 | } 30 | 31 | @test 32 | async createsTableWithIntegerColumn() { 33 | class Test extends Entity { 34 | @Column() 35 | age: number 36 | } 37 | 38 | await this.schema.synchronize([Test].pluck('metadata')) 39 | expectQuery(this.query).toEqual('CREATE TABLE test ( age integer )') 40 | } 41 | 42 | @test 43 | async createsTableWithPrimaryAutoIncrementColumn() { 44 | class Test extends Entity { 45 | @PrimaryColumn() 46 | id: ID 47 | } 48 | 49 | await this.schema.synchronize([Test].pluck('metadata')) 50 | expectQuery(this.query).toEqual(` 51 | CREATE TABLE test 52 | ( 53 | id integer PRIMARY KEY AUTOINCREMENT 54 | ) 55 | `) 56 | } 57 | 58 | @test 59 | async createsTableWithForeignColumn() { 60 | class User extends Entity { 61 | @Column() 62 | @Relation(() => Image, 'user') 63 | image: HasOne 64 | } 65 | 66 | class Image extends Entity { 67 | @Column() 68 | @Relation(() => User, 'image') 69 | user: BelongsTo 70 | } 71 | 72 | await this.schema.synchronize([Image].pluck('metadata')) 73 | expectQuery(this.query).toEqual(` 74 | CREATE TABLE image 75 | ( 76 | userId integer 77 | ) 78 | `) 79 | } 80 | 81 | @test 82 | async altersTable() { 83 | when(this.driver.tableExists(anyString())).thenReturn(Promise.resolve(true)) 84 | when(this.driver.tableColumns(anyString())).thenReturn(Promise.resolve([ 85 | { 86 | name: 'column2', 87 | type: 'integer' 88 | }, 89 | { 90 | name: 'column3', 91 | type: 'integer' 92 | }, 93 | ])) 94 | 95 | const queries: string[] = [] 96 | when(this.driver.run(anyString())).thenCall((query: string) => queries.push(query)) 97 | 98 | class User extends Entity { 99 | @Column() 100 | column1: number 101 | 102 | @Column() 103 | column2: string 104 | } 105 | 106 | await this.schema.synchronize([User].pluck('metadata')) 107 | 108 | expect(queries).to.have.length(4) 109 | expectQuery(queries[0]).toEqual(` 110 | CREATE TABLE user_alter_tmp 111 | ( 112 | column1 integer, 113 | column2 varchar(255) 114 | ) 115 | `) 116 | 117 | expectQuery(queries[1]).toEqual(` 118 | INSERT INTO user_alter_tmp(column2) 119 | SELECT column2 120 | FROM user 121 | `) 122 | 123 | expectQuery(queries[2]).toEqual(`DROP TABLE user`) 124 | expectQuery(queries[3]).toEqual(`ALTER TABLE user_alter_tmp RENAME TO user`) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Database/Entities/Article.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn, Relation } from '../../../Database' 2 | import { User } from './User' 3 | import { BelongsTo } from '../../../Database/Fields' 4 | 5 | export class Article extends Entity { 6 | 7 | @PrimaryColumn() 8 | id: number 9 | 10 | @Column() 11 | title: string 12 | 13 | @Column() 14 | content: string 15 | 16 | @Column() 17 | published: boolean = false 18 | 19 | @Relation(() => User, 'articles') 20 | author: BelongsTo 21 | } 22 | -------------------------------------------------------------------------------- /tests/Database/Entities/Profile.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreatedAt, Entity, PrimaryColumn, Relation, UpdatedAt } from '../../../Database' 2 | import { User } from './User' 3 | import { BelongsTo } from '../../../Database/Fields' 4 | 5 | export class Profile extends Entity { 6 | 7 | @PrimaryColumn() 8 | id: number 9 | 10 | @Relation(() => User, 'profile') 11 | user: BelongsTo 12 | 13 | @Column() 14 | address: string 15 | 16 | @CreatedAt() 17 | createdAt: Date 18 | 19 | @UpdatedAt() 20 | updatedAt: Date 21 | } 22 | -------------------------------------------------------------------------------- /tests/Database/Entities/Role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn, Relation } from '../../../Database' 2 | import { User } from './User' 3 | import { BelongsToMany } from '../../../Database/Fields' 4 | 5 | export class Role extends Entity { 6 | 7 | @PrimaryColumn() 8 | id: number 9 | 10 | @Column() 11 | name: string 12 | 13 | @Relation(() => User, 'roles') 14 | users: BelongsToMany 15 | } 16 | -------------------------------------------------------------------------------- /tests/Database/Entities/User.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreatedAt, Entity, Options, PrimaryColumn, Relation, UpdatedAt } from '../../../Database' 2 | import { Article } from './Article' 3 | import { Role } from './Role' 4 | import { Profile } from './Profile' 5 | import { BelongsToMany, HasMany, HasOne } from '../../../Database/Fields' 6 | 7 | @Options({ 8 | table: 'users' 9 | }) 10 | export class User extends Entity { 11 | 12 | @PrimaryColumn() 13 | id: number 14 | 15 | @Column() 16 | name: string 17 | 18 | @Column() 19 | email: string 20 | 21 | @Relation(() => Profile, 'user') 22 | profile: HasOne 23 | 24 | @Relation(() => Article, 'author') 25 | articles: HasMany
26 | 27 | @Relation(() => Role, 'users') 28 | roles: BelongsToMany 29 | 30 | @CreatedAt() 31 | createdAt: Date 32 | 33 | @UpdatedAt() 34 | updatedAt: Date 35 | } 36 | -------------------------------------------------------------------------------- /tests/Database/Migrations/RunnerTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { expect } from 'chai' 3 | import { File, Storage } from '../../../Storage' 4 | import { Connection, Query, SqliteDriver } from '../../../Database' 5 | import { MigrationHistory, Migrator } from '../../../Database/Migrations' 6 | import { anything, instance, mock, when } from 'ts-mockito' 7 | 8 | @suite 9 | class RunnerTest { 10 | private runner: Migrator 11 | private migrationsPath = './tests/Database/Migrations/migrations' 12 | private migrationFiles = ['1.createUserTable.ts', '2.createArticlesTable.ts'] 13 | private tableNames = ['migration_test_users', 'migration_test_articles'] 14 | 15 | async before() { 16 | Query.connection = new Connection(new SqliteDriver(':memory:')) 17 | // Query.connection = new Connection(new MysqlDriver({ 18 | // host: 'localhost', user: 'root', password: 'root', database: 'typetron_test' 19 | // })) 20 | this.runner = new Migrator(new Storage(), Query.connection, this.migrationsPath) 21 | } 22 | 23 | async after() { 24 | if (await Query.connection.tableExists(MigrationHistory.getTable())) { 25 | await this.runner.reset() 26 | await Query.connection.runRaw(`DROP TABLE IF EXISTS ${MigrationHistory.getTable()}`) 27 | } 28 | } 29 | 30 | @test 31 | async getFilesByPath() { 32 | const migrationFiles = await this.runner.files() 33 | 34 | expect(migrationFiles).to.have.length(2) 35 | } 36 | 37 | @test 38 | async shouldMigrate() { 39 | const migrated = await this.runner.migrate() 40 | 41 | expect(migrated).equals(true) 42 | 43 | const tables = await Query.connection.tables() 44 | 45 | expect(tables.pluck('name')).to.include.members(this.tableNames) 46 | } 47 | 48 | @test 49 | async shouldRollbackOnce() { 50 | await this.migrateInBatches() 51 | 52 | const rollback = await this.runner.rollback() 53 | 54 | expect(rollback).equals(true) 55 | 56 | const tables = await Query.connection.tables() 57 | 58 | expect(tables.pluck('name')).to.include(this.tableNames[0]) 59 | expect(tables.pluck('name')).to.not.include(this.tableNames[1]) 60 | } 61 | 62 | @test 63 | async shouldRollbackBySteps() { 64 | await this.migrateInBatches() 65 | const rollback = await this.runner.rollback(2) 66 | 67 | expect(rollback).equals(true) 68 | 69 | const tables = await Query.connection.tables() 70 | 71 | expect(tables.pluck('name')).to.not.include.members(this.tableNames) 72 | } 73 | 74 | @test 75 | async shouldReset() { 76 | await this.migrateInBatches() 77 | const rollback = await this.runner.reset() 78 | 79 | expect(rollback).equals(true) 80 | 81 | const tables = await Query.connection.tables() 82 | 83 | expect(tables.pluck('name')).to.not.include.members(this.tableNames) 84 | } 85 | 86 | private async migrateInBatches() { 87 | const mockStorage = mock(Storage) 88 | when(mockStorage.files(anything(), anything())) 89 | .thenReturn([new File(this.migrationFiles[0], this.migrationsPath)]) 90 | .thenReturn([new File(this.migrationFiles[1], this.migrationsPath)]) 91 | 92 | this.runner.storage = instance(mockStorage) 93 | await this.runner.migrate() 94 | await this.runner.migrate() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Database/Migrations/migrations/1.createUserTable.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '../../../../Database/Migrations' 2 | 3 | export class CreateUserTable extends Migration { 4 | 5 | async up() { 6 | await this.connection.runRaw('CREATE TABLE migration_test_users (id INTEGER PRIMARY KEY, name TEXT, password TEXT)') 7 | } 8 | 9 | async down() { 10 | await this.connection.runRaw('DROP TABLE migration_test_users') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Database/Migrations/migrations/2.createArticlesTable.ts: -------------------------------------------------------------------------------- 1 | import { Migration } from '../../../../Database/Migrations' 2 | 3 | export class CreateArticlesTable extends Migration { 4 | 5 | async up() { 6 | await this.connection.runRaw('CREATE TABLE migration_test_articles (id INTEGER PRIMARY KEY, title TEXT, content TEXT)') 7 | } 8 | 9 | async down() { 10 | await this.connection.runRaw('DROP TABLE migration_test_articles') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/Database/SchemaTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { expect } from 'chai' 3 | import { Connection, Query } from '../../Database' 4 | import { User } from './Entities/User' 5 | import { Role } from './Entities/Role' 6 | import { SqliteDriver } from '@Typetron/Database/Drivers' 7 | 8 | @suite 9 | class SchemaTest { 10 | 11 | async before() { 12 | Query.connection = new Connection(new SqliteDriver(':memory:')) 13 | // Query.connection = new Connection(new MysqlDriver({ 14 | // host: 'localhost', user: 'root', password: 'root', database: 'typetron_test' 15 | // })) 16 | } 17 | 18 | @test 19 | async createsPivotTable() { 20 | await Query.connection.driver.schema.synchronize([User, Role].pluck('metadata')) 21 | 22 | const tableName = [User.getTable(), Role.getTable()].sort().join('_') 23 | const table = await Query.connection.tableExists(tableName) 24 | expect(Boolean(table)).to.be.equal(true, 'Pivot table was not created') 25 | const tableColumns = await Query.connection.tableColumns(tableName) as {name: string, type: string}[] 26 | expect(tableColumns).to.have.length(2) 27 | // expect(tableColumns[0]).to.deep.include({name: 'id', type: 'integer'}); 28 | expect(tableColumns[0]).to.deep.include({name: 'roleId'}) 29 | expect(tableColumns[1]).to.deep.include({name: 'userId'}) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/Database/Seeders/RunnerTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { SeederManager } from '@Typetron/Database/Seeders' 3 | import { expect } from 'chai' 4 | import { Connection, Query, SqliteDriver } from '../../../Database' 5 | import { Storage } from '../../../Storage' 6 | 7 | @suite 8 | class RunnerTest { 9 | private runner: SeederManager 10 | private seedsPath = './tests/Database/Seeders/seeders' 11 | 12 | async before() { 13 | Query.connection = new Connection(new SqliteDriver(':memory:')) 14 | // Query.connection = new Connection(new MysqlDriver({ 15 | // host: 'localhost', user: 'root', password: 'root', database: 'typetron_test' 16 | // })) 17 | await Query.connection.runRaw('CREATE TABLE random_table (col1 varchar)') 18 | 19 | this.runner = new SeederManager(new Storage(), this.seedsPath) 20 | } 21 | 22 | @test 23 | async shouldSeed() { 24 | await this.runner.seed() 25 | 26 | const result = await Query.table<{ col1: string }>('random_table').first() 27 | 28 | expect(result?.col1).to.be.equal('val1') 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Database/Seeders/seeders/RandomSeeder.ts: -------------------------------------------------------------------------------- 1 | import { Query } from "@Typetron/Database"; 2 | import { Seeder } from "@Typetron/Database/Seeders"; 3 | 4 | export class RandomSeeder extends Seeder{ 5 | public run(): void { 6 | Query.table('random_table').insertOne({ 7 | col1: 'val1' 8 | }) 9 | } 10 | } -------------------------------------------------------------------------------- /tests/Database/utils.ts: -------------------------------------------------------------------------------- 1 | import { Expression } from '@Typetron/Database' 2 | import { expect } from 'chai' 3 | 4 | export function expectQuery(expression: Expression | string) { 5 | const sql = expression instanceof Expression ? expression.toSQL() : expression 6 | 7 | return { 8 | toEqual: (expected: string) => { 9 | expect(trimSQL(sql)).to.be.equal(trimSQL(expected)) 10 | } 11 | } 12 | } 13 | 14 | export function trimSQL(sql: string) { 15 | return sql.replace(/(\r\n|\n|\r)/gm, ' ').replace(/`/gm, '').replace(/ +/g, ' ').trim() 16 | } 17 | -------------------------------------------------------------------------------- /tests/Forms/FormsTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { MinLength, Required } from '../../Validation' 3 | import { Field, Form, Rules } from '@Typetron/Forms' 4 | import { expect } from 'chai' 5 | import { Optional } from '@Typetron/Validation/Rules/Optional' 6 | 7 | @suite 8 | class FormsTest { 9 | 10 | @test 11 | validatesFormBasedOnValidators() { 12 | 13 | class UserForm extends Form { 14 | @Field() 15 | @Rules(Required) 16 | name: string 17 | } 18 | 19 | const formData = { 20 | name: 'John' 21 | } 22 | const form = new UserForm() 23 | form.fill(formData) 24 | 25 | expect(form.valid()).to.be.equal(true) 26 | } 27 | 28 | @test 29 | showsErrorWhenFormIsInvalid() { 30 | class UserForm extends Form { 31 | @Field() 32 | @Rules(Required) 33 | name: string 34 | } 35 | 36 | const form = new UserForm() 37 | 38 | expect(form.valid()).to.be.equal(false) 39 | } 40 | 41 | @test 42 | doesNotGiveErrorForOptionalFieldsThatHaveValidatorsAlready() { 43 | class UserForm extends Form { 44 | @Field() 45 | @Rules(Optional, MinLength(10)) 46 | name?: string 47 | } 48 | 49 | const validForm = new UserForm() 50 | 51 | expect(validForm.valid()).to.be.equal(true) 52 | 53 | const invalidForm = new UserForm() 54 | invalidForm.fill({name: ''} as object) 55 | 56 | expect(invalidForm.valid()).to.be.equal(false) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/Framework/Middleware/articles/my-article.html: -------------------------------------------------------------------------------- 1 | article 2 | -------------------------------------------------------------------------------- /tests/Framework/Middleware/articles/some-directory/index.html: -------------------------------------------------------------------------------- 1 | index file 2 | -------------------------------------------------------------------------------- /tests/Framework/Middleware/assets/styles.css: -------------------------------------------------------------------------------- 1 | .test { 2 | color : purple; 3 | } 4 | -------------------------------------------------------------------------------- /tests/Framework/Resolvers/EntityResolverTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { Container } from '@Typetron/Container' 3 | import { Column, Connection, Entity, ID, Query, SqliteDriver } from '@Typetron/Database' 4 | import { Handler } from '@Typetron/Router/Websockets' 5 | import { AnyFunction, Type } from '@Typetron/Support' 6 | import { Http, Request, Response } from '@Typetron/Router/Http' 7 | import { anyOfClass, instance, mock, when } from 'ts-mockito' 8 | import { EntityResolver } from '@Typetron/Framework/Resolvers/EntityResolver' 9 | import { expect } from 'chai' 10 | 11 | @suite 12 | class EntityResolverTest { 13 | 14 | @test 15 | async 'resolves entity parameters'() { 16 | const connection = mock(Connection) 17 | Query.connection = instance(connection) 18 | Query.connection.driver = new SqliteDriver(':memory:') 19 | when(connection.first(anyOfClass(Query))).thenResolve({id: 2}, {id: 3}) 20 | 21 | const container = new Container() 22 | container.resolvers.unshift(new EntityResolver(container)) 23 | 24 | class User extends Entity { 25 | @Column() 26 | id: ID 27 | } 28 | 29 | class Controller { 30 | read(thisUser: User, thatUser: User) { 31 | return [thisUser.id, thatUser.id] 32 | } 33 | } 34 | 35 | const handler = container.get(Handler) 36 | handler.addAction('read', Controller, 'read', [User as unknown as Type, User as unknown as Type]) 37 | 38 | const request = new Request('read', Http.Method.GET) 39 | request.parameters = { 40 | 0: 2, 41 | 1: 3 42 | } 43 | const response: Response<[ID, ID]> = await handler.handle(container, request) 44 | 45 | expect(response.body[0]).to.be.equal(2) 46 | expect(response.body[1]).to.be.equal(3) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Framework/Router/RouterTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { expect } from 'chai' 3 | import { Action, Body, Controller, Get, Router } from '../../../Router' 4 | import { App, Application } from '@Typetron/Framework' 5 | import { AuthUser } from '../../../Framework/Auth' 6 | 7 | @suite 8 | class RouterTest { 9 | 10 | @test 11 | 'registers http routes and websocket actions'() { 12 | App.instance = new Application('./tests') 13 | 14 | @Controller() 15 | class MyController { 16 | 17 | @Action() 18 | create(@AuthUser() user: string) {} 19 | 20 | @Get() 21 | list(@Body() content: number) {} 22 | 23 | } 24 | 25 | const router = App.instance.get(Router) 26 | 27 | expect(router.routes).to.have.length(1) 28 | expect(router.actions).to.have.length(1) 29 | expect(router.routes[0]).to.deep.include({name: 'list'}) 30 | expect(router.actions.get('create')).to.deep.include({name: 'create'}) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Mail/MailerTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { Mailable, Mailer, MemoryTransport } from '@Typetron/Mail' 3 | import { expect } from 'chai' 4 | 5 | class Order { 6 | constructor(public id: number) { 7 | } 8 | } 9 | 10 | class OrderShipped extends Mailable { 11 | constructor(public order: Order) { 12 | super() 13 | } 14 | 15 | content() { 16 | return { 17 | html: `

Order #${this.order.id} shipped

`, 18 | text: `Order #${this.order.id} shipped` 19 | } 20 | } 21 | } 22 | 23 | @suite 24 | class MailerTest { 25 | mail = new Mailer({email: 'tester@test.com'}, new MemoryTransport()) 26 | 27 | @test 28 | async canSendSimpleMessage() { 29 | const sentMessage = await this.mail.to('john@example.com') 30 | .from('from@example.com') 31 | .replyTo('replyTo@example.com') 32 | .subject('test') 33 | .cc('moreUsers@example.com') 34 | .bcc('evenMoreUsers@example.com') 35 | .send('Order #123 shipped') 36 | 37 | expect(sentMessage.replyTo).to.deep.equal('replyTo@example.com') 38 | expect(sentMessage.subject).to.deep.equal('test') 39 | expect(sentMessage.from).to.deep.equal('from@example.com') 40 | expect(sentMessage.to).to.deep.equal('john@example.com') 41 | expect(sentMessage.cc).to.equal('moreUsers@example.com') 42 | expect(sentMessage.bcc).to.equal('evenMoreUsers@example.com') 43 | expect(sentMessage.body).to.equal('Order #123 shipped') 44 | } 45 | 46 | @test 47 | async canSendHtmlMessage() { 48 | const sentMessage = await this.mail.to('john@example.com') 49 | .cc('moreUsers@example.com') 50 | .bcc('evenMoreUsers@example.com') 51 | .send({html: '

Order #1234 shipped

', text: 'Order #1234 shipped'}) 52 | 53 | expect(sentMessage.to).to.equal('john@example.com') 54 | expect(sentMessage.cc).to.equal('moreUsers@example.com') 55 | expect(sentMessage.bcc).to.equal('evenMoreUsers@example.com') 56 | expect(sentMessage.body).to.deep.equal({html: '

Order #1234 shipped

', text: 'Order #1234 shipped'}) 57 | } 58 | 59 | @test 60 | async canSendMailable() { 61 | const order = new Order(1234) 62 | 63 | const sentMessage = await this.mail.to('john@example.com') 64 | .cc('moreUsers@example.com') 65 | .bcc('evenMoreUsers@example.com') 66 | .send(new OrderShipped(order)) 67 | 68 | expect(sentMessage.to).to.equal('john@example.com') 69 | expect(sentMessage.cc).to.equal('moreUsers@example.com') 70 | expect(sentMessage.bcc).to.equal('evenMoreUsers@example.com') 71 | expect(sentMessage.body).to.deep.equal({ 72 | html: `

Order #${order.id} shipped

`, 73 | text: `Order #${order.id} shipped` 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Router/HttpHandlerTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { Handler, Http, Request } from '../../Router/Http' 3 | import { expect } from 'chai' 4 | import { Container } from '@Typetron/Container' 5 | import { Body, Controller, Get } from '../../Router' 6 | 7 | @suite 8 | class HttpHandlerTest { 9 | private container: Container 10 | 11 | async before() { 12 | this.container = new Container() 13 | Container.setInstance(this.container) 14 | } 15 | 16 | @test 17 | 'gives error when two routes have the same path'() { 18 | 19 | class MyController { 20 | index() {} 21 | } 22 | 23 | const handler = this.container.get(Handler) 24 | handler.addRoute('', Http.Method.GET, MyController, 'index', 'index') 25 | 26 | expect(() => handler.addRoute('', Http.Method.POST, MyController, 'index', 'index')) 27 | .to.not.throw(`There is already a route with the same url: [GET] ''`) 28 | expect(() => handler.addRoute('', Http.Method.GET, MyController, 'index', 'index')) 29 | .to.throw(`There is already a route with the same url: [GET] ''`) 30 | } 31 | 32 | @test 33 | async 'returns the value from the @Body'() { 34 | @Controller() 35 | class MyController { 36 | @Get('list') 37 | list(@Body() value: string) { 38 | return value 39 | } 40 | 41 | @Get('create') 42 | create(@Body() value: string) { 43 | return value 44 | } 45 | } 46 | 47 | const router = this.container.get(Handler) 48 | 49 | const listContent = await router.handle(this.container, new Request('list', Http.Method.GET, undefined, {}, 'test')) 50 | expect(listContent.body).to.be.equal('test') 51 | 52 | const createContent = await router.handle(this.container, new Request('create', Http.Method.GET)) 53 | expect(createContent.body).to.be.equal(undefined) 54 | } 55 | 56 | // @test 57 | // async 'caches route urls'(done: Function) { 58 | // class Controller { 59 | // index() {} 60 | // } 61 | // 62 | // const router = new Router() 63 | // router.add('', Http.Method.GET, Controller, 'index', 'index') 64 | // 65 | // expect(router.cachedRoutes).to.be.have.length(0) 66 | // 67 | // const app = new Container() 68 | // const request = new Request('', Http.Method.GET) 69 | // await router.handle(app as Application, request) 70 | // expect(router.findRouteIndex).to.be.called.once // ??? 71 | // } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Router/RequestTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { expect } from 'chai' 3 | import { Http, Request } from '../../Router/Http' 4 | import { IncomingHttpHeaders } from 'http' 5 | import { File } from '../../Storage' 6 | 7 | @suite 8 | class RequestTest { 9 | 10 | private request: Request 11 | 12 | before() { 13 | this.request = new Request('http://example.com/test?name=value', Http.Method.GET) 14 | this.request.setHeadersLoader(() => { 15 | return {'content-type': 'text/html'} 16 | }) 17 | this.request.getHeader = (name: keyof IncomingHttpHeaders | string): T => { 18 | return 'text/html' as T 19 | } 20 | } 21 | 22 | @test 23 | 'uri getter should return correct uri'() { 24 | expect(this.request.uri).to.equal('/test') 25 | } 26 | 27 | @test 28 | 'method getter should return correct method'() { 29 | expect(this.request.method).to.equal(Http.Method.GET) 30 | } 31 | 32 | // Assuming that the commented url getter is uncommented in your code 33 | @test 34 | 'url getter should return correct url'() { 35 | expect(this.request.url).to.equal('http://example.com/test?name=value') 36 | } 37 | 38 | @test 39 | 'query getter should parse and return correct query parameters'() { 40 | expect(this.request.query).to.deep.equal({name: 'value'}) 41 | } 42 | 43 | @test 44 | 'cookies getter should parse and return correct cookies'() { 45 | expect(this.request.cookies).to.deep.equal({todo: 'parse cookies'}) 46 | } 47 | 48 | @test 49 | 'body getter and setter should return and set body correctly'() { 50 | this.request.body = {test: 'data'} 51 | expect(this.request.body).to.deep.equal({test: 'data'}) 52 | } 53 | 54 | @test 55 | 'files getter and setter should return and set files correctly'() { 56 | const dummyFile = { // Just a mock structure based on what you provided 57 | originalName: 'test.txt', 58 | directory: '/tmp', 59 | saved: true, 60 | path: '/tmp/test.txt' 61 | } as File 62 | 63 | this.request.files = {testFile: dummyFile} 64 | expect(this.request.files).to.deep.equal({testFile: dummyFile}) 65 | } 66 | 67 | @test 68 | 'headers getter should return correct headers'() { 69 | expect(this.request.headers).to.deep.equal({'content-type': 'text/html'}) 70 | } 71 | 72 | @test 73 | 'header value is returned'() { 74 | expect(this.request.getHeader('random header')).to.deep.equal('text/html') 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /tests/Router/WebSocketsHandlerTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { Handler } from '../../Router/Websockets' 3 | import { expect } from 'chai' 4 | import { Container } from '@Typetron/Container' 5 | import { Action, Body, Controller } from '../../Router' 6 | import { Request } from '../../Router/Request' 7 | 8 | @suite 9 | class WebSocketsHandlerTest { 10 | 11 | @test 12 | 'gives error when two actions have the same path'() { 13 | const container = new Container() 14 | 15 | class Controller { 16 | list() {} 17 | } 18 | 19 | const router = container.get(Handler) 20 | router.addAction('list', Controller, 'list') 21 | 22 | expect(() => router.addAction('list', Controller, 'index')).to.throw(`There is already an action with the same name: 'list'`) 23 | } 24 | 25 | @test 26 | async 'returns the value from the @Body'() { 27 | const container = Container.getInstance() 28 | 29 | @Controller() 30 | class MyController { 31 | @Action() 32 | list(@Body() value: string) { 33 | return value 34 | } 35 | 36 | @Action() 37 | create(@Body() value: string) { 38 | return value 39 | } 40 | } 41 | 42 | const router = container.get(Handler) 43 | 44 | const listContent = await router.handle(container, new Request('list', 'test')) 45 | expect(listContent.body).to.be.equal('test') 46 | 47 | const createContent = await router.handle(container, new Request('create')) 48 | expect(createContent.body).to.be.equal(undefined) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/Storage/StorageTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { File, Storage } from '../../Storage' 3 | import { expect } from 'chai' 4 | import * as path from 'path' 5 | 6 | const directory = './tests/Storage/.storage' 7 | 8 | @suite 9 | class StorageTest { 10 | storage = new Storage() 11 | 12 | @test 13 | async canReadFiles() { 14 | const file = new File('testFile.txt') 15 | file.directory = directory 16 | // Create the file before trying to save it 17 | await this.storage.put(path.join(directory, file.name), 'test content') 18 | 19 | const content = await this.storage.read(path.join(directory, file.name)) 20 | expect(content).to.be.instanceOf(Buffer) 21 | expect(content.toString()).to.equal('test content') 22 | } 23 | 24 | @test 25 | async canCheckIfFileExists() { 26 | const file = new File('testFile.txt') 27 | file.directory = directory 28 | // Create the file before trying to save it 29 | await this.storage.put(path.join(directory, file.name), 'test content') 30 | 31 | const exists = await this.storage.exists(path.join(directory, file.name)) 32 | expect(exists).to.be.true 33 | } 34 | 35 | @test 36 | async canSaveFiles() { 37 | const file = new File('testFile.txt') 38 | file.directory = directory 39 | // Create the file before trying to save it 40 | await this.storage.put(path.join(directory, file.name), 'test content') 41 | const savedFile = await this.storage.save(file, './tests/Storage/.storage/alternative.txt') 42 | expect(savedFile).to.be.instanceOf(File) 43 | expect(savedFile.name).to.equal(file.name) 44 | expect(await this.storage.exists(savedFile.path)).to.be.true 45 | } 46 | 47 | @test 48 | async canWriteToFile() { 49 | const filePath = path.join(directory, 'testFile.txt') 50 | const file = await this.storage.put(filePath, 'test content') 51 | expect(file).to.be.instanceOf(File) 52 | expect(file.name).to.equal('testFile.txt') 53 | const content = await this.storage.read(path.join(directory, file.name)) 54 | expect(content).to.be.instanceOf(Buffer) 55 | expect(content.toString()).to.equal('test content') 56 | } 57 | 58 | @test 59 | async canCreateDirectory() { 60 | await this.storage.makeDirectory(directory) 61 | const exists = await this.storage.exists(directory) 62 | expect(exists).to.be.true 63 | } 64 | 65 | @test 66 | async canDeleteDirectory() { 67 | await this.storage.makeDirectory(directory) 68 | await this.storage.deleteDirectory(directory) 69 | const exists = await this.storage.exists(directory) 70 | expect(exists).to.be.false 71 | } 72 | 73 | @test 74 | async canDeleteFile() { 75 | const file = new File('testFile.txt') 76 | file.directory = directory 77 | // Create the file before trying to save it 78 | await this.storage.put(path.join(directory, file.name), 'test content') 79 | 80 | await this.storage.delete(path.join(directory, file.name)) 81 | const exists = await this.storage.exists(path.join(directory, file.name)) 82 | expect(exists).to.be.false 83 | } 84 | 85 | @test 86 | async cannotDeleteWithoutFileName() { 87 | try { 88 | // Attempt to delete a directory instead of a file 89 | await this.storage.delete(directory) 90 | throw new Error('Expected an error to be thrown, but none was thrown.') 91 | } catch (error) { 92 | expect(error).to.be.instanceOf(Error) 93 | expect(error.message).to.equal('Can not delete because this path leads to a directory and not a file.') 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Support/StringTest.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { suite, test } from '@testdeck/mocha' 3 | 4 | @suite 5 | class StringTest { 6 | @test 7 | shouldLimitStringAddingThreeDotsAtTheEnd() { 8 | expect('text'.limit(3)).to.be.equal('...') 9 | expect('texts'.limit(4)).to.be.equal('t...') 10 | expect('text'.limit(5)).to.be.equal('text') 11 | } 12 | 13 | @test 14 | shouldLimitStringWithCustomEnding() { 15 | expect('Testing this text'.limit(12, ' More...')).to.be.equal('Test More...') 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tests/Validation/RulesTest.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from '@testdeck/mocha' 2 | import { Email } from '../../Validation' 3 | import { expect } from 'chai' 4 | 5 | @suite 6 | class RulesTest { 7 | @test 8 | shouldLimitStringAddingThreeDotsAtTheEnd() { 9 | const validEmails = [ 10 | 'email@example.com', 11 | 'firstname.lastname@example.com', 12 | 'email@subdomain.example.com', 13 | 'firstname+lastname@example.com', 14 | 'email@123.123.123.123', 15 | 'email@[123.123.123.123]', 16 | '"email"@example.com', 17 | '1234567890@example.com', 18 | 'email@example-one.com', 19 | '_______@example.com', 20 | 'email@example.name', 21 | 'email@example.museum', 22 | 'email@example.co.jp', 23 | 'firstname-lastname@example.com', 24 | ] 25 | const invalidEmails = [ 26 | 'plainaddress', 27 | '#@%^%#$@#$@#.com', 28 | '@example.com', 29 | 'Joe Smith ', 30 | 'email.example.com', 31 | 'email@example@example.com', 32 | '.email@example.com', 33 | 'email.@example.com', 34 | 'email..email@example.com', 35 | 'あいうえお@example.com', 36 | 'email@example.com (Joe Smith)', 37 | 'email@example', 38 | 'email@-example.com', 39 | 'email@example..com', 40 | 'Abc..123@example.com', 41 | ] 42 | 43 | const rule = new (Email()) 44 | 45 | validEmails.forEach(email => { 46 | expect(rule.passes('', email), email).to.be.equal(true) 47 | }) 48 | 49 | invalidEmails.forEach(email => { 50 | expect(rule.passes('', email), email).to.be.equal(false) 51 | }) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": [] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "experimentalDecorators": true, 6 | "emitDecoratorMetadata": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "strictPropertyInitialization": false, 12 | "forceConsistentCasingInFileNames": true, 13 | // "useDefineForClassFields": true, // TODO some tests fail when using this flag 14 | "declarationMap": true, 15 | // "noUnusedLocals": true, 16 | // "noUnusedParameters": true, 17 | "outDir": "./build", 18 | "noImplicitAny": true, 19 | // "noUncheckedIndexedAccess": true, 20 | "moduleResolution": "node", 21 | "lib": [ 22 | "esnext" 23 | ], 24 | "types": [ 25 | "reflect-metadata", 26 | "@types/node" 27 | ], 28 | "baseUrl": ".", 29 | "paths": { 30 | "@Typetron/*": [ 31 | "*" 32 | ] 33 | } 34 | }, 35 | "files": [ 36 | "types.d.ts" 37 | ], 38 | "include": [ 39 | "Cache", 40 | "Container", 41 | "Database", 42 | "Encryption", 43 | "Forms", 44 | "Framework", 45 | "Models", 46 | "Mail", 47 | "Router", 48 | "Storage", 49 | "Support", 50 | "Testing" 51 | ] 52 | } 53 | 54 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-return-shorthand": true, 4 | "callable-types": true, 5 | "class-name": true, 6 | "comment-format": [ 7 | true, 8 | "check-space" 9 | ], 10 | "curly": true, 11 | "deprecation": { 12 | "severity": "warn" 13 | }, 14 | "eofline": false, 15 | "forin": false, 16 | "ban-types": true, 17 | "import-blacklist": [ 18 | true, 19 | "rxjs/Rx" 20 | ], 21 | "import-spacing": true, 22 | "no-any": true, 23 | "no-unsafe-any": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "label-position": true, 29 | "max-line-length": [ 30 | true, 31 | 140 32 | ], 33 | "member-access": false, 34 | "no-arg": true, 35 | "no-bitwise": true, 36 | "no-console": [ 37 | true, 38 | "debug", 39 | "info", 40 | "time", 41 | "timeEnd", 42 | "trace" 43 | ], 44 | "no-construct": true, 45 | "no-debugger": true, 46 | "no-duplicate-super": true, 47 | "no-empty": false, 48 | "no-empty-interface": true, 49 | "no-eval": true, 50 | "no-inferrable-types": [ 51 | false 52 | ], 53 | "no-misused-new": true, 54 | "no-non-null-assertion": true, 55 | "no-null-keyword": true, 56 | "no-redundant-jsdoc": true, 57 | "no-shadowed-variable": true, 58 | "no-string-literal": false, 59 | "no-string-throw": true, 60 | "no-switch-case-fall-through": true, 61 | "no-trailing-whitespace": false, 62 | "no-unnecessary-initializer": true, 63 | "no-unused-expression": true, 64 | "no-use-before-declare": true, 65 | "no-var-keyword": true, 66 | "object-literal-sort-keys": false, 67 | "one-line": [ 68 | true, 69 | "check-open-brace", 70 | "check-catch", 71 | "check-else", 72 | "check-whitespace" 73 | ], 74 | "prefer-const": true, 75 | "quotemark": [ 76 | true, 77 | "single" 78 | ], 79 | "radix": true, 80 | "semicolon": [ 81 | false, 82 | "always" 83 | ], 84 | "triple-equals": [ 85 | true, 86 | "allow-null-check" 87 | ], 88 | "typedef-whitespace": [ 89 | true, 90 | { 91 | "call-signature": "nospace", 92 | "index-signature": "nospace", 93 | "parameter": "nospace", 94 | "property-declaration": "nospace", 95 | "variable-declaration": "nospace" 96 | } 97 | ], 98 | "unified-signatures": true, 99 | "variable-name": false, 100 | "whitespace": [ 101 | true, 102 | "check-branch", 103 | "check-decl", 104 | "check-operator", 105 | "check-separator", 106 | "check-type" 107 | ], 108 | "no-output-on-prefix": true, 109 | "use-input-property-decorator": true, 110 | "use-output-property-decorator": true, 111 | "use-host-property-decorator": true, 112 | "no-input-rename": true, 113 | "no-output-rename": true, 114 | "use-life-cycle-interface": true, 115 | "use-pipe-transform-interface": true, 116 | "component-class-suffix": true, 117 | "directive-class-suffix": true, 118 | "no-default-export": true 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'fast-url-parser' { 2 | export function parse(url: string): Url 3 | 4 | type Url = URL & { 5 | format(): string 6 | query: string 7 | } 8 | } 9 | --------------------------------------------------------------------------------