@typetron/framework 0.5.30 → 0.5.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +31 -0
- package/CLAUDE.md +1 -0
- package/Cache/Readme.md +63 -0
- package/Container/Readme.md +95 -0
- package/Database/Readme.md +139 -0
- package/Encryption/Readme.md +54 -0
- package/Forms/Readme.md +79 -0
- package/Framework/Readme.md +138 -0
- package/Frontend/Angular/Readme.md +68 -0
- package/Frontend/Websockets/Readme.md +96 -0
- package/Mail/Readme.md +99 -0
- package/Models/Readme.md +85 -0
- package/Router/Readme.md +145 -0
- package/Storage/Readme.md +83 -0
- package/Support/Readme.md +71 -0
- package/Testing/Readme.md +80 -0
- package/Validation/Readme.md +91 -0
- package/package.json +20 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Typetron Framework Guide
|
|
2
|
+
|
|
3
|
+
Read the documentation for the module you are changing before editing its source. Nested features are documented by their owning module; for
|
|
4
|
+
example, HTTP and WebSocket routing are covered by Router, while migrations and seeders are covered by Database.
|
|
5
|
+
|
|
6
|
+
## Module Documentation
|
|
7
|
+
|
|
8
|
+
- [Cache](Cache/Readme.md) - cache contracts and memory, file, and database stores.
|
|
9
|
+
- [Container](Container/Readme.md) - dependency injection, bindings, resolvers, and scopes.
|
|
10
|
+
- [Database](Database/Readme.md) - connections, queries, entities, relationships, migrations, and seeders.
|
|
11
|
+
- [Encryption](Encryption/Readme.md) - password hashing and JSON Web Tokens.
|
|
12
|
+
- [Forms](Forms/Readme.md) - typed input objects and field validation.
|
|
13
|
+
- [Framework](Framework/Readme.md) - application bootstrap, configuration, providers, authentication, and middleware.
|
|
14
|
+
- [Mail](Mail/Readme.md) - mailables, recipients, and transports.
|
|
15
|
+
- [Models](Models/Readme.md) - explicit response and data projections.
|
|
16
|
+
- [Router](Router/Readme.md) - controllers, HTTP routes, WebSocket actions, middleware, and servers.
|
|
17
|
+
- [Storage](Storage/Readme.md) - files, directories, and image data.
|
|
18
|
+
- [Support](Support/Readme.md) - shared types, utilities, and prototype helpers.
|
|
19
|
+
- [Testing](Testing/Readme.md) - application-aware HTTP and WebSocket test helpers.
|
|
20
|
+
- [Validation](Validation/Readme.md) - built-in and custom validation rules.
|
|
21
|
+
- [Angular](Frontend/Angular/Readme.md) - Angular form integration.
|
|
22
|
+
- [Browser WebSockets](Frontend/Websockets/Readme.md) - the Typetron WebSocket browser client.
|
|
23
|
+
|
|
24
|
+
## Working in This Repository
|
|
25
|
+
|
|
26
|
+
- Treat each capitalized source directory as a framework module.
|
|
27
|
+
- Use the nearest module barrel or the documented `@Typetron/<Module>` path for public imports.
|
|
28
|
+
- Look for corresponding behavior tests under `tests/<Module>` before changing an implementation.
|
|
29
|
+
- Do not edit `build/`; it is generated by `npm run build`.
|
|
30
|
+
- Keep module documentation aligned with public behavior. Update the relevant `Readme.md` when an API, default, error, or example changes.
|
|
31
|
+
- Keep documentation examples short and executable in shape. Do not document planned or commented-out features as available.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/Cache/Readme.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Cache
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Using a Cache Store](#using-a-cache-store)
|
|
5
|
+
- [Retrieving and Remembering Values](#retrieving-and-remembering-values)
|
|
6
|
+
- [Available Stores](#available-stores)
|
|
7
|
+
- [Removing Values](#removing-values)
|
|
8
|
+
|
|
9
|
+
## Introduction
|
|
10
|
+
|
|
11
|
+
Typetron provides one cache contract for values stored in memory, on disk, or in the configured database. Each store supports the same
|
|
12
|
+
asynchronous operations, so application code may depend on `Cache` without knowing where values are kept.
|
|
13
|
+
|
|
14
|
+
## Using a Cache Store
|
|
15
|
+
|
|
16
|
+
The `CacheProvider` binds the configured store to the `Cache` class. A store may also be constructed directly when no application container
|
|
17
|
+
is available:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { MemoryStore } from '@Typetron/Cache'
|
|
21
|
+
|
|
22
|
+
const cache = new MemoryStore()
|
|
23
|
+
|
|
24
|
+
await cache.set('user:1', {name: 'John'}, 60)
|
|
25
|
+
const user = await cache.get<{name: string}>('user:1')
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The optional duration passed to `set` is measured in seconds. Omitting it keeps the value until it is removed or the store is flushed.
|
|
29
|
+
|
|
30
|
+
## Retrieving and Remembering Values
|
|
31
|
+
|
|
32
|
+
The `get` method accepts either a value or a function as its fallback. Use `remember` when the fallback should also be stored:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const users = await cache.remember('users', async () => {
|
|
36
|
+
return [{id: 1, name: 'John'}]
|
|
37
|
+
}, 300)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The fallback is evaluated only when the key is unavailable. `get` returns `undefined` when no cached value or fallback is provided.
|
|
41
|
+
|
|
42
|
+
## Available Stores
|
|
43
|
+
|
|
44
|
+
`MemoryStore` keeps values in the current process. `FileStore` hashes each key and stores its serialized value below a configured directory.
|
|
45
|
+
`DatabaseStore` stores serialized values in a table and creates that table when it first writes a value.
|
|
46
|
+
|
|
47
|
+
The application-level `CacheConfig` supports the `memory`, `file`, and `database` drivers. The `CacheProvider` creates the selected
|
|
48
|
+
implementation.
|
|
49
|
+
|
|
50
|
+
> Note: Memory cache values disappear when the process exits. File and database stores persist values, but their values must be JSON
|
|
51
|
+
> serializable.
|
|
52
|
+
|
|
53
|
+
## Removing Values
|
|
54
|
+
|
|
55
|
+
Use `delete` to remove one key or `flush` to clear the entire store:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
await cache.delete('user:1')
|
|
59
|
+
await cache.flush()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Calling `has` checks whether a key is present. Expired memory values are removed during this check, while persisted stores enforce
|
|
63
|
+
expiration when values are read.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Service Container
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Resolving Classes](#resolving-classes)
|
|
5
|
+
- [Binding Values](#binding-values)
|
|
6
|
+
- [Dependency Injection](#dependency-injection)
|
|
7
|
+
- [Scopes and Child Containers](#scopes-and-child-containers)
|
|
8
|
+
- [Custom Resolvers](#custom-resolvers)
|
|
9
|
+
|
|
10
|
+
## Introduction
|
|
11
|
+
|
|
12
|
+
The service container constructs classes and resolves their dependencies. Typetron uses it for controllers, providers, middleware, and other
|
|
13
|
+
framework services, but it may also resolve application classes directly.
|
|
14
|
+
|
|
15
|
+
## Resolving Classes
|
|
16
|
+
|
|
17
|
+
Concrete classes require no manual registration. Pass the constructor to `get` and the container will build it:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Container } from '@Typetron/Container'
|
|
21
|
+
|
|
22
|
+
class UserRepository {}
|
|
23
|
+
|
|
24
|
+
const container = new Container()
|
|
25
|
+
const users = container.get(UserRepository)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Classes use singleton scope by default, so subsequent calls return the same resolved instance.
|
|
29
|
+
|
|
30
|
+
## Binding Values
|
|
31
|
+
|
|
32
|
+
Use `set` when an abstraction, symbol, or string should resolve to a particular value:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
abstract class StorageContract {}
|
|
36
|
+
|
|
37
|
+
class LocalStorage extends StorageContract {}
|
|
38
|
+
|
|
39
|
+
container.set(StorageContract, new LocalStorage())
|
|
40
|
+
container.set('appName', 'Typetron')
|
|
41
|
+
|
|
42
|
+
const storage = container.get(StorageContract)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A missing string binding throws `No instance found for '<name>'`.
|
|
46
|
+
|
|
47
|
+
## Dependency Injection
|
|
48
|
+
|
|
49
|
+
Constructor types are resolved automatically. The `@Inject` decorator provides property injection and may name an explicit binding:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { Inject, Injectable } from '@Typetron/Container'
|
|
53
|
+
|
|
54
|
+
@Injectable()
|
|
55
|
+
class UserService {
|
|
56
|
+
constructor(public users: UserRepository) {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class UserController {
|
|
60
|
+
@Inject()
|
|
61
|
+
users: UserService
|
|
62
|
+
|
|
63
|
+
@Inject('appName')
|
|
64
|
+
appName: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const controller = container.get(UserController)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Decorator-based injection requires TypeScript decorator metadata to be enabled, as it is in this repository's `tsconfig.json`.
|
|
71
|
+
|
|
72
|
+
## Scopes and Child Containers
|
|
73
|
+
|
|
74
|
+
Pass a `Scope` to `@Injectable` when the default singleton lifetime is not appropriate:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { Injectable, Scope } from '@Typetron/Container'
|
|
78
|
+
|
|
79
|
+
@Injectable(Scope.TRANSIENT)
|
|
80
|
+
class Operation {}
|
|
81
|
+
|
|
82
|
+
const first = container.get(Operation)
|
|
83
|
+
const second = container.get(Operation)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`first` and `second` are different instances. `REQUEST` services are isolated to request containers, while `CONNECTION` services may live in
|
|
87
|
+
a connection container and be reused by its request children. Create those boundaries with `createChildContainer('connection')` and
|
|
88
|
+
`createChildContainer('request')`.
|
|
89
|
+
|
|
90
|
+
## Custom Resolvers
|
|
91
|
+
|
|
92
|
+
Extend `BaseResolver` to resolve values the default class resolver cannot construct. Add the resolver near the beginning of
|
|
93
|
+
`container.resolvers` when it should take precedence, and implement `canResolve` and `resolve`.
|
|
94
|
+
|
|
95
|
+
> Note: A resolver may return a promise. In that case, callers must await the value returned by `container.get`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Database
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Configuration](#configuration)
|
|
5
|
+
- [Queries and Entities](#queries-and-entities)
|
|
6
|
+
- [Relationships](#relationships)
|
|
7
|
+
- [Migrations and Seeders](#migrations-and-seeders)
|
|
8
|
+
|
|
9
|
+
## Introduction
|
|
10
|
+
|
|
11
|
+
The Database module contains Typetron's connection layer, SQL query builder, schema drivers, and entity ORM. SQLite and MySQL use the same
|
|
12
|
+
`Query` and `Entity` APIs.
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
Applications configure the active driver through `DatabaseConfig` and register `DatabaseProvider`. Outside an application, assign
|
|
17
|
+
`Query.connection` before creating queries:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Connection, Query, SqliteDriver } from '@Typetron/Database'
|
|
21
|
+
import { DatabaseConfig } from '@Typetron/Framework'
|
|
22
|
+
|
|
23
|
+
export const config = new DatabaseConfig({
|
|
24
|
+
driver: 'sqlite',
|
|
25
|
+
migrationsDirectory: 'Database/migrations',
|
|
26
|
+
seedersDirectory: 'Database/seeders',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
Query.connection = new Connection(new SqliteDriver(':memory:'))
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The built-in SQLite driver reads `process.env.database` and otherwise uses `database.sqlite`. The MySQL driver reads the database host,
|
|
33
|
+
user, password, and database environment values. All queries and entities use the static `Query.connection`.
|
|
34
|
+
|
|
35
|
+
## Queries and Entities
|
|
36
|
+
|
|
37
|
+
Start a query with `Query.table`, or extend `Entity` and decorate mapped properties. The following example defines an entity and exercises
|
|
38
|
+
both APIs:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import {
|
|
42
|
+
Column,
|
|
43
|
+
CreatedAt,
|
|
44
|
+
Entity,
|
|
45
|
+
HasMany,
|
|
46
|
+
Options,
|
|
47
|
+
PrimaryColumn,
|
|
48
|
+
Query,
|
|
49
|
+
Relation,
|
|
50
|
+
UpdatedAt,
|
|
51
|
+
} from '@Typetron/Database'
|
|
52
|
+
import { Post } from '../Entities/Post'
|
|
53
|
+
|
|
54
|
+
@Options({table: 'users'})
|
|
55
|
+
class User extends Entity {
|
|
56
|
+
@PrimaryColumn()
|
|
57
|
+
id: number
|
|
58
|
+
|
|
59
|
+
@Column()
|
|
60
|
+
name: string
|
|
61
|
+
|
|
62
|
+
@Column()
|
|
63
|
+
email: string
|
|
64
|
+
|
|
65
|
+
@CreatedAt()
|
|
66
|
+
createdAt: Date
|
|
67
|
+
|
|
68
|
+
@UpdatedAt()
|
|
69
|
+
updatedAt: Date
|
|
70
|
+
|
|
71
|
+
@Relation(() => Post, 'author')
|
|
72
|
+
posts: HasMany<Post>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const id = await Query.table('users').insertOne({name: 'John'})
|
|
76
|
+
const user = await User.create({
|
|
77
|
+
name: 'Jane',
|
|
78
|
+
email: 'jane@example.com',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const users = await User
|
|
82
|
+
.whereLike('email', '%@example.com')
|
|
83
|
+
.orderBy('name', 'ASC')
|
|
84
|
+
.get()
|
|
85
|
+
|
|
86
|
+
user.name = 'Janet'
|
|
87
|
+
await user.save()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The query builder supports selection, joins, grouping, ordering, limits, aggregates, subqueries, and common `where` variants. Use `first`,
|
|
91
|
+
`insert`, `insertOne`, `update`, and `delete` to execute work; `insertOne` resolves to the identifier supplied by the active driver.
|
|
92
|
+
|
|
93
|
+
Without `@Options`, an entity table defaults to the lowercase class name. `@Column('column_name')` maps another column name, while
|
|
94
|
+
`@JSONColumn` serializes objects as JSON. Use `find` or `first` when absence is expected; `findOrFail` throws `EntityNotFoundError`.
|
|
95
|
+
`firstOrNew` returns an unsaved entity, while `firstOrCreate` persists it.
|
|
96
|
+
|
|
97
|
+
## Relationships
|
|
98
|
+
|
|
99
|
+
Typetron supports `BelongsTo`, `HasOne`, `HasMany`, and `BelongsToMany`. Declare both sides with `@Relation`, naming the inverse property:
|
|
100
|
+
|
|
101
|
+
The `User.posts` property above pairs with a `Post.author: BelongsTo<User>` relation whose inverse name is `posts`. Load the relation with
|
|
102
|
+
`User.with('posts').get()`, or use `User.withCount('posts').get()` to add a `postsCount` value.
|
|
103
|
+
|
|
104
|
+
`with` accepts nested relation paths and constrained relation queries. `withCount` places the loaded count on the corresponding
|
|
105
|
+
`<relation>Count` property.
|
|
106
|
+
|
|
107
|
+
## Migrations and Seeders
|
|
108
|
+
|
|
109
|
+
Migration classes implement `up` and `down` with the provided connection. Seeders implement `run` against the active connection:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { Query } from '@Typetron/Database'
|
|
113
|
+
import { Migration } from '@Typetron/Database/Migrations'
|
|
114
|
+
import { Seeder } from '@Typetron/Database/Seeders'
|
|
115
|
+
|
|
116
|
+
export class CreateUsersTable extends Migration {
|
|
117
|
+
async up() {
|
|
118
|
+
await this.connection.runRaw(
|
|
119
|
+
'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)'
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async down() {
|
|
124
|
+
await this.connection.runRaw('DROP TABLE users')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class UserSeeder extends Seeder {
|
|
129
|
+
async run() {
|
|
130
|
+
await Query.table('users').insert({name: 'John'})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`Migrator.migrate` discovers pending TypeScript files and records batches. `rollback(steps)` and `reset` invoke migration `down` methods.
|
|
136
|
+
`SeederManager.seed` discovers TypeScript seeder files and runs them.
|
|
137
|
+
|
|
138
|
+
> Warning: `reset` reverses migrations against the active database. `migrate({fresh: true})` clears only the migration history; it does not
|
|
139
|
+
> drop existing application tables before rerunning migrations.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Encryption
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Hashing Passwords](#hashing-passwords)
|
|
5
|
+
- [Signing Tokens](#signing-tokens)
|
|
6
|
+
- [Verifying Tokens](#verifying-tokens)
|
|
7
|
+
|
|
8
|
+
## Introduction
|
|
9
|
+
|
|
10
|
+
The Encryption module wraps password hashing and JSON Web Token operations in promise-based services that can be resolved through Typetron's
|
|
11
|
+
container.
|
|
12
|
+
|
|
13
|
+
## Hashing Passwords
|
|
14
|
+
|
|
15
|
+
Use `Crypt.hash` before storing a password and `Crypt.compare` when checking a submitted value:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Crypt } from '@Typetron/Encryption'
|
|
19
|
+
|
|
20
|
+
const crypt = new Crypt()
|
|
21
|
+
const password = await crypt.hash('secret', 12)
|
|
22
|
+
const matches = await crypt.compare('secret', password)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The second argument to `hash` is the bcrypt salt-round count. Both operations reject when bcrypt cannot process the supplied value.
|
|
26
|
+
|
|
27
|
+
## Signing Tokens
|
|
28
|
+
|
|
29
|
+
The `JWT` service exposes the asynchronous form of `jsonwebtoken.sign`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { JWT } from '@Typetron/Encryption'
|
|
33
|
+
|
|
34
|
+
const jwt = new JWT()
|
|
35
|
+
const token = await jwt.sign(
|
|
36
|
+
{sub: 1},
|
|
37
|
+
process.env.APP_SECRET!,
|
|
38
|
+
{expiresIn: '1h'}
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Payload and signing options follow the `jsonwebtoken` package.
|
|
43
|
+
|
|
44
|
+
## Verifying Tokens
|
|
45
|
+
|
|
46
|
+
Pass the token and the same public key or secret to `verify`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const payload = await jwt.verify<number>(token, process.env.APP_SECRET!)
|
|
50
|
+
|
|
51
|
+
console.log(payload.sub)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The promise resolves to the decoded payload, including `sub`, `iat`, and `exp`, or rejects when the token is invalid or expired.
|
package/Forms/Readme.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Defining Forms](#defining-forms)
|
|
5
|
+
- [Filling Forms](#filling-forms)
|
|
6
|
+
- [Validating Forms](#validating-forms)
|
|
7
|
+
- [Optional Fields](#optional-fields)
|
|
8
|
+
|
|
9
|
+
## Introduction
|
|
10
|
+
|
|
11
|
+
Forms describe the input an application accepts. A form declares its fields, applies Validation module rules, and exposes only those fields
|
|
12
|
+
when it is filled or serialized.
|
|
13
|
+
|
|
14
|
+
## Defining Forms
|
|
15
|
+
|
|
16
|
+
Extend `Form` and decorate each accepted property with `@Field`. Apply validation with `@Rules`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Field, Form, Rules } from '@Typetron/Forms'
|
|
20
|
+
import { Email, MinLength, Required } from '@Typetron/Validation'
|
|
21
|
+
|
|
22
|
+
class UserForm extends Form {
|
|
23
|
+
@Field()
|
|
24
|
+
@Rules(Required, MinLength(3))
|
|
25
|
+
name: string
|
|
26
|
+
|
|
27
|
+
@Field()
|
|
28
|
+
@Rules(Required, Email)
|
|
29
|
+
email: string
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`@Field('externalName')` may be used when the exposed field name differs from the property name.
|
|
34
|
+
|
|
35
|
+
## Filling Forms
|
|
36
|
+
|
|
37
|
+
The `fill` method copies decorated fields from an input object and ignores other properties:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const form = new UserForm()
|
|
41
|
+
|
|
42
|
+
form.fill({
|
|
43
|
+
name: 'John',
|
|
44
|
+
email: 'john@example.com',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const value = form.value()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`value` and `toJSON` return an object containing all declared fields. `validated` returns the truthy fields that do not currently have
|
|
51
|
+
validation errors.
|
|
52
|
+
|
|
53
|
+
## Validating Forms
|
|
54
|
+
|
|
55
|
+
Call `valid` after filling the form:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
if (!form.valid()) {
|
|
59
|
+
console.log(form.errors)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Errors are grouped first by field name and then by rule identifier, such as `required` or `email`.
|
|
64
|
+
|
|
65
|
+
## Optional Fields
|
|
66
|
+
|
|
67
|
+
Use `Optional` before other constraints when a missing value is allowed:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { MinLength, Optional } from '@Typetron/Validation'
|
|
71
|
+
|
|
72
|
+
class ProfileForm extends Form {
|
|
73
|
+
@Field()
|
|
74
|
+
@Rules(Optional, MinLength(10))
|
|
75
|
+
biography?: string
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A value of `undefined` or `null` skips all rules for an optional field. An empty string is still validated by the remaining rules.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Application Framework
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Creating an Application](#creating-an-application)
|
|
5
|
+
- [Configuration](#configuration)
|
|
6
|
+
- [Providers and Middleware](#providers-and-middleware)
|
|
7
|
+
- [Authentication](#authentication)
|
|
8
|
+
- [Automatic Parameter Resolution](#automatic-parameter-resolution)
|
|
9
|
+
- [Accessing Services](#accessing-services)
|
|
10
|
+
|
|
11
|
+
## Introduction
|
|
12
|
+
|
|
13
|
+
The Framework module bootstraps a Typetron application. It loads configuration, prepares the service container, registers providers and
|
|
14
|
+
parameter resolvers, and starts the selected HTTP and WebSocket servers.
|
|
15
|
+
|
|
16
|
+
## Creating an Application
|
|
17
|
+
|
|
18
|
+
Use `Application.create` with the application's root directory, then start its server:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
AppConfig,
|
|
23
|
+
Application,
|
|
24
|
+
AuthConfig,
|
|
25
|
+
CacheProvider,
|
|
26
|
+
DatabaseProvider,
|
|
27
|
+
MailProvider,
|
|
28
|
+
} from '@Typetron/Framework'
|
|
29
|
+
import { User } from './Entities/User'
|
|
30
|
+
|
|
31
|
+
export const appConfig = new AppConfig({
|
|
32
|
+
environment: 'development',
|
|
33
|
+
server: 'node',
|
|
34
|
+
port: 3000,
|
|
35
|
+
providers: [DatabaseProvider, CacheProvider, MailProvider],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const authConfig = new AuthConfig({
|
|
39
|
+
entity: User,
|
|
40
|
+
identityColumn: 'id',
|
|
41
|
+
signature: process.env.APP_SECRET!,
|
|
42
|
+
duration: 3600,
|
|
43
|
+
saltRounds: 12,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const app = await Application.create(process.cwd())
|
|
47
|
+
app.startServer()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
In a real application, each config instance is the default export of a TypeScript file in `config`. Configuration loads before
|
|
51
|
+
`Application.create` resolves. The second argument may select another config directory.
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
The module includes `AppConfig`, `AuthConfig`, `CacheConfig`, `DatabaseConfig`, and `MailConfig`. Constructor values replace defaults when
|
|
56
|
+
the application loads each file.
|
|
57
|
+
|
|
58
|
+
`AppConfig.server` selects the Node or uWebSockets HTTP server. A configured `websocketsPort` starts the separate WebSocket provider. Static
|
|
59
|
+
assets are served when `staticAssets` entries are present.
|
|
60
|
+
|
|
61
|
+
Authentication configuration must provide a signing `signature`. If it is missing, application creation throws an error stating that
|
|
62
|
+
`APP_SECRET` is not set up.
|
|
63
|
+
|
|
64
|
+
## Providers and Middleware
|
|
65
|
+
|
|
66
|
+
Providers bind application services. Middleware surrounds requests and may be global, HTTP-only, WebSocket-only, or attached by the Router:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { AppConfig, Provider } from '@Typetron/Framework'
|
|
70
|
+
import { RequestHandler } from '@Typetron/Router'
|
|
71
|
+
import { Request } from '@Typetron/Router/Http'
|
|
72
|
+
import { HttpMiddleware } from '@Typetron/Router/Http/Middleware'
|
|
73
|
+
|
|
74
|
+
class SearchService {}
|
|
75
|
+
|
|
76
|
+
class SearchProvider extends Provider {
|
|
77
|
+
register() {
|
|
78
|
+
this.app.set(SearchService, new SearchService())
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class ResponseHeader extends HttpMiddleware {
|
|
83
|
+
async handle(request: Request, next: RequestHandler) {
|
|
84
|
+
const response = await next(request)
|
|
85
|
+
response.setHeader('x-powered-by', 'Typetron')
|
|
86
|
+
return response
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const config = new AppConfig({
|
|
91
|
+
port: 3000,
|
|
92
|
+
providers: [SearchProvider],
|
|
93
|
+
middleware: {
|
|
94
|
+
global: [],
|
|
95
|
+
http: [ResponseHeader],
|
|
96
|
+
websocket: [],
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Typetron includes providers for database, cache, mail, and WebSockets, together with `AuthMiddleware`, `CorsMiddleware`, and
|
|
102
|
+
`StaticAssetsMiddleware`.
|
|
103
|
+
|
|
104
|
+
## Authentication
|
|
105
|
+
|
|
106
|
+
The `Auth` service signs and verifies user tokens. Your user may extend the provided `User` entity or implement `Authenticatable` by naming
|
|
107
|
+
its identity, username, and password columns.
|
|
108
|
+
|
|
109
|
+
Protect an HTTP controller with `AuthMiddleware`, then resolve the current user with `@AuthUser`:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { AuthUser, User } from '@Typetron/Framework/Auth'
|
|
113
|
+
import { AuthMiddleware } from '@Typetron/Framework/Middleware'
|
|
114
|
+
import { Controller, Get, Middleware } from '@Typetron/Router'
|
|
115
|
+
|
|
116
|
+
@Controller('account')
|
|
117
|
+
@Middleware(AuthMiddleware)
|
|
118
|
+
class AccountController {
|
|
119
|
+
@Get('me')
|
|
120
|
+
me(@AuthUser() user: User) {
|
|
121
|
+
return user
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The middleware accepts a bearer token from the `Authorization` header or a `token` field in the body. Invalid or expired tokens produce an
|
|
127
|
+
unauthenticated HTTP response.
|
|
128
|
+
|
|
129
|
+
## Automatic Parameter Resolution
|
|
130
|
+
|
|
131
|
+
Application bootstrap registers resolvers for forms, entities, and authenticated users. A controller parameter extending `Form` is filled
|
|
132
|
+
from the request body and files, then validated; failure produces status `422`. An entity parameter is loaded from a route parameter named
|
|
133
|
+
after its class.
|
|
134
|
+
|
|
135
|
+
## Accessing Services
|
|
136
|
+
|
|
137
|
+
An `Application` is also the root container. Resolve services with `app.get`, or use `App.get` after creation. For example, `App.get(Cache)`
|
|
138
|
+
returns the store registered by `CacheProvider`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Typetron Angular
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Installation](#installation)
|
|
5
|
+
- [Building Angular Forms](#building-angular-forms)
|
|
6
|
+
- [Validation](#validation)
|
|
7
|
+
|
|
8
|
+
## Introduction
|
|
9
|
+
|
|
10
|
+
The `@typetron/angular` package turns a Typetron `Form` class into an Angular reactive `FormGroup`. Backend field names, initial values, and
|
|
11
|
+
validation rules can therefore be reused by an Angular application.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install the package with Angular's reactive forms dependencies:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @typetron/angular @angular/forms
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The package expects compatible `@angular/core`, `@angular/common`, and `@angular/forms` packages, together with `@typetron/framework`.
|
|
22
|
+
|
|
23
|
+
## Building Angular Forms
|
|
24
|
+
|
|
25
|
+
Define the form with Typetron decorators, then pass its class to `FormBuilder.build`:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Component } from '@angular/core'
|
|
29
|
+
import { ReactiveFormsModule } from '@angular/forms'
|
|
30
|
+
import { FormBuilder } from '@typetron/angular'
|
|
31
|
+
import { Field, Form, Rules } from '@Typetron/Forms'
|
|
32
|
+
import { Email, Required } from '@Typetron/Validation'
|
|
33
|
+
|
|
34
|
+
class UserForm extends Form {
|
|
35
|
+
@Field()
|
|
36
|
+
@Rules(Required)
|
|
37
|
+
name: string
|
|
38
|
+
|
|
39
|
+
@Field()
|
|
40
|
+
@Rules(Required, Email)
|
|
41
|
+
email: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Component({
|
|
45
|
+
imports: [ReactiveFormsModule],
|
|
46
|
+
template: `<form [formGroup]="form"></form>`,
|
|
47
|
+
})
|
|
48
|
+
export class UserEditor {
|
|
49
|
+
form = FormBuilder.build(UserForm)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The resulting `FormGroup` contains one control for every `@Field` in `UserForm`.
|
|
54
|
+
|
|
55
|
+
## Validation
|
|
56
|
+
|
|
57
|
+
Each Angular control delegates validation to the corresponding Typetron `FormField`. Validation failures are returned using the Typetron
|
|
58
|
+
rule identifiers and messages.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const email = this.form.controls.email
|
|
62
|
+
|
|
63
|
+
email.setValue('invalid')
|
|
64
|
+
console.log(email.errors?.email)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
> Note: The builder creates a new Typetron form to obtain default field values. Set defaults on the form class before calling
|
|
68
|
+
`FormBuilder.build`.
|