@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.
@@ -0,0 +1,96 @@
1
+ # Typetron WebSockets
2
+
3
+ - [Introduction](#introduction)
4
+ - [Installation](#installation)
5
+ - [Using the Connection](#using-the-connection)
6
+ - [Errors and Reconnection](#errors-and-reconnection)
7
+ - [Message Format](#message-format)
8
+
9
+ ## Introduction
10
+
11
+ The `@typetron/websockets` package is a browser client for Typetron WebSocket actions. It queues outgoing actions until the connection
12
+ opens, exposes responses as RxJS observables, and correlates requests by ID.
13
+
14
+ ## Installation
15
+
16
+ Install the package and RxJS:
17
+
18
+ ```bash
19
+ npm install @typetron/websockets rxjs
20
+ ```
21
+
22
+ The client requires the browser `WebSocket` API and a module-aware frontend build.
23
+
24
+ ## Using the Connection
25
+
26
+ Construct `Websocket` with the server URL. Use `on` for a response stream, `emit` for a one-way action, or `request` to await one correlated
27
+ response:
28
+
29
+ ```ts
30
+ import { Websocket } from '@typetron/websockets'
31
+
32
+ interface User {
33
+ id: number
34
+ name: string
35
+ }
36
+
37
+ const socket = new Websocket('ws://localhost:3001')
38
+
39
+ const subscription = socket.on<User>('users.updated').subscribe(user => {
40
+ console.log(user.name)
41
+ })
42
+
43
+ socket.emit('users.save', {
44
+ body: {name: 'John'},
45
+ })
46
+
47
+ const user = await socket.request<User>('users.save', {
48
+ body: {name: 'John'},
49
+ })
50
+
51
+ socket.onError({except: ['health.check']}).subscribe(response => {
52
+ console.error(response.action, response.message)
53
+ })
54
+
55
+ socket.onConnect(() => console.log('connected'))
56
+ socket.onDisconnect(() => console.log('disconnected'))
57
+ ```
58
+
59
+ The optional message may contain `body` and `parameters`. Actions emitted before the connection opens remain queued. `request` resolves with
60
+ the response's `message`, while an error response rejects it. Unsubscribe from RxJS listeners when they are no longer needed.
61
+
62
+ ## Errors and Reconnection
63
+
64
+ Action errors reach the corresponding listener and the shared `onError` stream. The optional `except` list suppresses action names from that
65
+ shared stream.
66
+
67
+ When the browser socket closes, the client invokes the disconnect callback, resets its outgoing queue, and attempts to reconnect after one
68
+ second. `isConnected` is `true` only while the underlying socket is open.
69
+
70
+ ## Message Format
71
+
72
+ Actions sent to the server use this shape:
73
+
74
+ ```json
75
+ {
76
+ "$id": "request-id",
77
+ "action": "users.save",
78
+ "message": {
79
+ "body": {"name": "John"},
80
+ "parameters": [1]
81
+ }
82
+ }
83
+ ```
84
+
85
+ Responses use the same ID and action name:
86
+
87
+ ```json
88
+ {
89
+ "$id": "request-id",
90
+ "action": "users.save",
91
+ "status": "OK",
92
+ "message": {"id": 1, "name": "John"}
93
+ }
94
+ ```
95
+
96
+ The status is `OK` or `Error`.
package/Mail/Readme.md ADDED
@@ -0,0 +1,99 @@
1
+ # Mail
2
+
3
+ - [Introduction](#introduction)
4
+ - [Configuration](#configuration)
5
+ - [Defining Mailables](#defining-mailables)
6
+ - [Sending Mail](#sending-mail)
7
+ - [Recipients and Message Details](#recipients-and-message-details)
8
+ - [Transports and Testing](#transports-and-testing)
9
+
10
+ ## Introduction
11
+
12
+ Typetron provides a fluent API for building and sending email. A `Mailer` collects recipients and message details, while a transport
13
+ performs the delivery.
14
+
15
+ ## Configuration
16
+
17
+ Applications normally register `MailProvider` and configure `MailConfig`. The `default` mailer may be `memory` or `SendGrid`, and `from`
18
+ supplies the default sender.
19
+
20
+ ```ts
21
+ import { MailConfig } from '@Typetron/Framework'
22
+
23
+ export default new MailConfig({
24
+ default: 'memory',
25
+ from: {
26
+ email: 'hello@example.com',
27
+ name: 'Typetron',
28
+ },
29
+ })
30
+ ```
31
+
32
+ The SendGrid transport also requires `mailers.SendGrid.key`. Constructing `Mailer` without a transport throws an error instructing the
33
+ application to register `MailProvider` or bind a mailer manually.
34
+
35
+ ## Defining Mailables
36
+
37
+ Represent a reusable message by extending `Mailable` and implementing `content`:
38
+
39
+ ```ts
40
+ import { Mailable } from '@Typetron/Mail'
41
+
42
+ class OrderShipped extends Mailable {
43
+ constructor(public orderId: number) {
44
+ super()
45
+ }
46
+
47
+ content() {
48
+ return {
49
+ html: `<h1>Order #${this.orderId} shipped</h1>`,
50
+ text: `Order #${this.orderId} shipped`,
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ `content` may return a string or an object containing both `html` and `text`.
57
+
58
+ ## Sending Mail
59
+
60
+ Begin a message with `Mailer.to`, configure it, and call `send`:
61
+
62
+ ```ts
63
+ import { Mailer, MemoryTransport } from '@Typetron/Mail'
64
+
65
+ const transport = new MemoryTransport()
66
+ const mail = new Mailer({email: 'hello@example.com'}, transport)
67
+
68
+ const sent = await mail
69
+ .to('john@example.com')
70
+ .subject('Your order has shipped')
71
+ .send(new OrderShipped(1234))
72
+ ```
73
+
74
+ `send` resolves to the transport's `SentMessage` after the transport accepts the message.
75
+
76
+ ## Recipients and Message Details
77
+
78
+ The pending message supports `from`, `to`, `replyTo`, `cc`, `bcc`, and `subject`. A plain string or `{html, text}` object may be sent
79
+ instead of a custom mailable:
80
+
81
+ ```ts
82
+ await mail
83
+ .to('john@example.com')
84
+ .replyTo('support@example.com')
85
+ .cc('manager@example.com')
86
+ .send('Welcome to Typetron')
87
+ ```
88
+
89
+ ## Transports and Testing
90
+
91
+ `MemoryTransport` stores sent messages in its public `messages` array and is suitable for tests and local development. `SendGridTransport`
92
+ sends through `@sendgrid/mail` and converts SendGrid failures into an HTTP error.
93
+
94
+ ```ts
95
+ const [message] = transport.messages
96
+
97
+ console.log(message.to)
98
+ console.log(message.body)
99
+ ```
@@ -0,0 +1,85 @@
1
+ # Models
2
+
3
+ - [Introduction](#introduction)
4
+ - [Defining Models](#defining-models)
5
+ - [Transforming Values](#transforming-values)
6
+ - [Nested Models](#nested-models)
7
+ - [Transforming Collections](#transforming-collections)
8
+
9
+ ## Introduction
10
+
11
+ Models create explicit data projections. They are useful for returning only selected fields from entities or plain objects without modifying
12
+ the original value.
13
+
14
+ ## Defining Models
15
+
16
+ Extend `Model` and mark every exposed property with `@Field`:
17
+
18
+ ```ts
19
+ import { Field, Model } from '@Typetron/Models'
20
+
21
+ class UserModel extends Model {
22
+ @Field()
23
+ id: number
24
+
25
+ @Field()
26
+ name: string
27
+ }
28
+ ```
29
+
30
+ Properties without `@Field` are omitted. Pass a source name to `@Field('user_name')` when the model property reads from a differently named
31
+ input field.
32
+
33
+ ## Transforming Values
34
+
35
+ Call the model's static `from` method with an object:
36
+
37
+ ```ts
38
+ const user = UserModel.from({
39
+ id: 1,
40
+ name: 'John',
41
+ password: 'secret',
42
+ })
43
+ ```
44
+
45
+ The result contains `id` and `name`; `password` is not included.
46
+
47
+ ## Nested Models
48
+
49
+ A property typed as another model is transformed recursively. Use `@FieldMany` for a collection:
50
+
51
+ ```ts
52
+ import { Field, FieldMany, Model } from '@Typetron/Models'
53
+
54
+ class PostModel extends Model {
55
+ @Field()
56
+ title: string
57
+ }
58
+
59
+ class AuthorModel extends Model {
60
+ @Field()
61
+ name: string
62
+
63
+ @Field()
64
+ @FieldMany(PostModel)
65
+ posts: PostModel[]
66
+ }
67
+ ```
68
+
69
+ Nested values with a `toJSON` method are serialized before they are projected.
70
+
71
+ ## Transforming Collections
72
+
73
+ `Model.from` accepts a single object, any iterable of objects, or a promise resolving to either form:
74
+
75
+ ```ts
76
+ const users = UserModel.from([
77
+ {id: 1, name: 'John'},
78
+ {id: 2, name: 'Jane'},
79
+ ])
80
+
81
+ const promisedUser = await UserModel.from(Promise.resolve({
82
+ id: 3,
83
+ name: 'Alex',
84
+ }))
85
+ ```
@@ -0,0 +1,145 @@
1
+ # Router
2
+
3
+ - [Introduction](#introduction)
4
+ - [HTTP Controllers](#http-controllers)
5
+ - [Request Data and Responses](#request-data-and-responses)
6
+ - [Middleware and Guards](#middleware-and-guards)
7
+ - [WebSocket Actions](#websocket-actions)
8
+ - [Loading Controllers and Starting Servers](#loading-controllers-and-starting-servers)
9
+
10
+ ## Introduction
11
+
12
+ The Router maps HTTP routes and WebSocket action names to controller methods. Both transports resolve controllers and typed parameters
13
+ through the service container, and both support middleware and guards.
14
+
15
+ ## HTTP Controllers
16
+
17
+ Decorate a class with `@Controller` and methods with `@Get`, `@Post`, `@Put`, `@Patch`, or `@Delete`:
18
+
19
+ ```ts
20
+ import {
21
+ Body,
22
+ Controller,
23
+ Get,
24
+ Post,
25
+ Query,
26
+ } from '@Typetron/Router'
27
+ import { Http, Response } from '@Typetron/Router/Http'
28
+ import { User } from '../Entities/User'
29
+
30
+ @Controller('users')
31
+ class UserController {
32
+ @Get('', 'index')
33
+ index(@Query('active') active?: string) {
34
+ return User.where('active', active === 'true').get()
35
+ }
36
+
37
+ @Get(':User', 'read')
38
+ read(user: User) {
39
+ return user
40
+ }
41
+
42
+ @Post('', 'create')
43
+ async create(@Body() data: {name: string}) {
44
+ const user = await User.create(data)
45
+ return new Response(user, Http.Status.CREATED)
46
+ }
47
+ }
48
+ ```
49
+
50
+ These routes are named `users.index`, `users.read`, and `users.create`. Without an explicit method name, the controller method name is used.
51
+ Two routes may share a path when their HTTP methods differ; registering the same path and method twice throws an error.
52
+
53
+ ## Request Data and Responses
54
+
55
+ Prefix a path segment with `:` to capture it. Primitive `string` and `number` parameters are resolved in route order. `@Body` returns the
56
+ body, while `@Query('name')` reads one query value and converts it to a number when the decorated parameter is typed as `number`.
57
+
58
+ An entity parameter is loaded when the route parameter uses its class name, such as `:User` above. The Framework resolver calls `User.find`
59
+ and throws `EntityNotFoundError` when no row exists.
60
+
61
+ Plain controller return values become a status `200` `Response`. Return `Response` directly to control status and headers; convenience
62
+ constructors include `ok`, `notFound`, and `badRequest`.
63
+
64
+ ## Middleware and Guards
65
+
66
+ Middleware surrounds the action, while guards decide whether resolved parameters may reach it:
67
+
68
+ ```ts
69
+ import {
70
+ AllowIf,
71
+ Controller,
72
+ Get,
73
+ Middleware,
74
+ RequestHandler,
75
+ } from '@Typetron/Router'
76
+ import { Guard } from '@Typetron/Router/Guard'
77
+ import { Request } from '@Typetron/Router/Http'
78
+ import { HttpMiddleware } from '@Typetron/Router/Http/Middleware'
79
+ import { User } from '../Entities/User'
80
+
81
+ class ResponseHeader extends HttpMiddleware {
82
+ async handle(request: Request, next: RequestHandler) {
83
+ const response = await next(request)
84
+ response.setHeader('x-powered-by', 'Typetron')
85
+ return response
86
+ }
87
+ }
88
+
89
+ class CanReadUser extends Guard {
90
+ async condition(user: User) {
91
+ return user.id > 0
92
+ }
93
+ }
94
+
95
+ @Controller('users')
96
+ @Middleware(ResponseHeader)
97
+ class UserController {
98
+ @Get(':User', 'read')
99
+ @AllowIf(CanReadUser)
100
+ read(user: User) {
101
+ return user
102
+ }
103
+ }
104
+ ```
105
+
106
+ Apply `@Middleware` and `@AllowIf` to a controller or method. The default `Guard.onFail` throws HTTP `401 Unauthorized`; override it when
107
+ another failure is required. Transport-specific middleware extends `HttpMiddleware` or `WebsocketMiddleware`, while `GlobalMiddleware` may
108
+ wrap both.
109
+
110
+ ## WebSocket Actions
111
+
112
+ Use `@Action` for controller methods called by an action name:
113
+
114
+ ```ts
115
+ import { Action, Body, Controller, OnClose, OnOpen } from '@Typetron/Router'
116
+
117
+ @Controller('users')
118
+ class UserActions {
119
+ @Action('save')
120
+ save(@Body() data: {name: string}) {
121
+ return {id: 1, ...data}
122
+ }
123
+
124
+ @OnOpen
125
+ opened() {}
126
+
127
+ @OnClose
128
+ closed() {}
129
+ }
130
+ ```
131
+
132
+ The action is registered as `users.save`. Results use the same `Response` wrapper as HTTP routes. Duplicate action names throw during
133
+ registration; `@OnOpen` and `@OnClose` register optional connection lifecycle handlers.
134
+
135
+ Messages sent or published through the injected `WebSocket` while handling an action retain the incoming request's `$id`. This allows
136
+ clients to correlate every streamed message with the action that initiated it, including when multiple actions run concurrently on one
137
+ connection. Messages initiated outside an action may omit `$id`.
138
+
139
+ ## Loading Controllers and Starting Servers
140
+
141
+ `Router.loadControllers(directory)` imports TypeScript controller files recursively. Importing decorated classes registers their routes and
142
+ actions.
143
+
144
+ The Framework starts the Node or uWebSockets HTTP server selected by `AppConfig.server`. Configuring `websocketsPort` starts the separate
145
+ WebSocket provider.
@@ -0,0 +1,83 @@
1
+ # Storage
2
+
3
+ - [Introduction](#introduction)
4
+ - [Writing and Reading Files](#writing-and-reading-files)
5
+ - [Working With File Objects](#working-with-file-objects)
6
+ - [Listing Files](#listing-files)
7
+ - [Directories and Deletion](#directories-and-deletion)
8
+ - [Image Data](#image-data)
9
+
10
+ ## Introduction
11
+
12
+ The Storage module provides a small filesystem abstraction used by uploads, migrations, cache files, and static assets. Its asynchronous
13
+ methods operate on paths while `File` carries a file's name and directory.
14
+
15
+ ## Writing and Reading Files
16
+
17
+ Use `put` to write content and receive a `File` describing the result:
18
+
19
+ ```ts
20
+ import { Storage } from '@Typetron/Storage'
21
+
22
+ const storage = new Storage()
23
+ const file = await storage.put('storage/users/1.txt', 'John')
24
+ const content = await storage.read(file.path)
25
+
26
+ console.log(content.toString())
27
+ ```
28
+
29
+ Missing parent directories are created automatically. `read` resolves to a `Buffer`.
30
+
31
+ ## Working With File Objects
32
+
33
+ A `File` derives its `path` and `extension` from its name and directory:
34
+
35
+ ```ts
36
+ import { File } from '@Typetron/Storage'
37
+
38
+ const upload = new File('avatar.png', '/tmp/uploads')
39
+
40
+ console.log(upload.path)
41
+ console.log(upload.extension)
42
+ ```
43
+
44
+ Use `save(file, directory)` to copy an existing file into another directory. The destination directory is created when needed.
45
+
46
+ ## Listing Files
47
+
48
+ `files(directory)` returns immediate files as `File` instances. Pass `true` as the second argument to include nested directories:
49
+
50
+ ```ts
51
+ const migrations = storage
52
+ .files('Database/migrations', true)
53
+ .where('extension', 'ts')
54
+ ```
55
+
56
+ File listing is synchronous internally even though most other storage operations are asynchronous.
57
+
58
+ ## Directories and Deletion
59
+
60
+ Use `exists`, `makeDirectory`, and `deleteDirectory` for directories. Use `delete` only for file paths:
61
+
62
+ ```ts
63
+ if (await storage.exists(file.path)) {
64
+ await storage.delete(file.path)
65
+ }
66
+ ```
67
+
68
+ Deleting a missing file has no effect. Passing a directory to `delete` throws an error; use `deleteDirectory` for recursive directory
69
+ removal.
70
+
71
+ ## Image Data
72
+
73
+ The `fromBase64` helper decodes an image data URL:
74
+
75
+ ```ts
76
+ import { fromBase64 } from '@Typetron/Storage'
77
+
78
+ const image = fromBase64('data:image/png;base64,iVBORw0KGgo=')
79
+
80
+ await storage.put(`storage/avatar.${image.extension}`, image.content)
81
+ ```
82
+
83
+ Invalid image data throws `InvalidImage` with the message `The image has an invalid data format`.
@@ -0,0 +1,71 @@
1
+ # Support
2
+
3
+ - [Introduction](#introduction)
4
+ - [Array Helpers](#array-helpers)
5
+ - [String Helpers](#string-helpers)
6
+ - [Number Helpers](#number-helpers)
7
+ - [JSON Parsing](#json-parsing)
8
+ - [Shared Types](#shared-types)
9
+
10
+ ## Introduction
11
+
12
+ The Support module contains shared TypeScript types and lightweight helpers used throughout Typetron. Importing `@Typetron/Support` also
13
+ installs the framework's array, string, and math prototype extensions.
14
+
15
+ ## Array Helpers
16
+
17
+ Arrays gain collection-style operations for common filtering and transformation tasks:
18
+
19
+ ```ts
20
+ import '@Typetron/Support'
21
+
22
+ const users = [
23
+ {id: 1, name: 'John', active: true},
24
+ {id: 2, name: 'Jane', active: false},
25
+ ]
26
+
27
+ const names = users.where('active', true).pluck('name')
28
+ const byId = users.groupBy('id')
29
+ ```
30
+
31
+ Other helpers include `first`, `last`, `findWhere`, `whereIn`, `whereNotIn`, `sortBy`, `unique`, `sum`, `mapAsync`, and `forEachAsync`.
32
+ Methods such as `sortBy` and `remove` mutate the original array; filtering and mapping helpers return new arrays.
33
+
34
+ ## String Helpers
35
+
36
+ Strings may be capitalized or limited, and the `String` constructor can create random values:
37
+
38
+ ```ts
39
+ const title = 'typetron'.capitalize()
40
+ const excerpt = 'A longer description'.limit(10)
41
+ const token = String.randomAlphaNum(24)
42
+ ```
43
+
44
+ `limit` uses `...` by default and counts that ending inside the requested length.
45
+
46
+ ## Number Helpers
47
+
48
+ `Math.randomInt(min, max)` returns an integer including both boundaries:
49
+
50
+ ```ts
51
+ const dice = Math.randomInt(1, 6)
52
+ ```
53
+
54
+ ## JSON Parsing
55
+
56
+ `JSONSafeParse` returns `undefined` instead of throwing when input is empty or invalid:
57
+
58
+ ```ts
59
+ import { JSONSafeParse } from '@Typetron/Support/utils'
60
+
61
+ const settings = JSONSafeParse<{theme: string}>('{"theme":"dark"}')
62
+ const invalid = JSONSafeParse('{')
63
+ ```
64
+
65
+ ## Shared Types
66
+
67
+ The module exports reusable types including `Type`, `Constructor`, `Abstract`, `AnyFunction`, `KeysOfType`, `ChildKeys`, and `ChildObject`.
68
+ These types describe constructors and derive child-specific keys or objects without adding runtime behavior.
69
+
70
+ > Note: Prototype helpers are global after the Support module is imported. Check for an existing helper before adding another extension with
71
+ > the same name.
@@ -0,0 +1,80 @@
1
+ # Testing
2
+
3
+ - [Introduction](#introduction)
4
+ - [Creating a Test Case](#creating-a-test-case)
5
+ - [Making HTTP Requests](#making-http-requests)
6
+ - [Authentication](#authentication)
7
+ - [Testing WebSocket Actions](#testing-websocket-actions)
8
+
9
+ ## Introduction
10
+
11
+ `TestCase` boots a Typetron application once, creates an isolated child container before each test, and provides helpers for exercising
12
+ named HTTP routes and WebSocket actions without starting a network server.
13
+
14
+ ## Creating a Test Case
15
+
16
+ Extend `TestCase` and implement `bootstrapApp`:
17
+
18
+ ```ts
19
+ import { Application } from '@Typetron/Framework'
20
+ import { TestCase } from '@Typetron/Testing/TestCase'
21
+
22
+ class UsersTest extends TestCase {
23
+ bootstrapApp() {
24
+ return Application.create(process.cwd())
25
+ }
26
+ }
27
+ ```
28
+
29
+ The base `before` hook re-registers `DatabaseProvider` and replaces password hashing with a deterministic test double. Call the inherited
30
+ lifecycle hook when a test framework override would otherwise bypass it.
31
+
32
+ ## Making HTTP Requests
33
+
34
+ HTTP helpers resolve the route by its registered name rather than by URL:
35
+
36
+ ```ts
37
+ const response = await this.get<{id: number, name: string}>('users.read')
38
+
39
+ console.log(response.status)
40
+ console.log(response.body)
41
+ ```
42
+
43
+ Use `post`, `patch`, `put`, or `delete` with a body and optional headers. For named routes with parameters, pass a tuple to those helpers:
44
+
45
+ ```ts
46
+ await this.put(
47
+ ['users.update', {User: 1}],
48
+ {name: 'John'},
49
+ )
50
+ ```
51
+
52
+ A route name that is not registered throws `Route '<name>' not found`. HTTP errors are converted to normal `Response` objects so assertions
53
+ may inspect their status and body.
54
+
55
+ ## Authentication
56
+
57
+ Use `actingAs` with a user entity or `loginById` with an identity. Subsequent HTTP helpers add the generated bearer token automatically:
58
+
59
+ ```ts
60
+ await this.actingAs(user)
61
+ const response = await this.get('users.me')
62
+ ```
63
+
64
+ The older `login(user)` helper is deprecated; prefer `actingAs` or `loginById`.
65
+
66
+ ## Testing WebSocket Actions
67
+
68
+ Call `action` with the registered action name and optional message data:
69
+
70
+ ```ts
71
+ this.on<{id: number}>('users.created', user => {
72
+ console.log(user.id)
73
+ })
74
+
75
+ const response = await this.action<{id: number}>('users.create', {
76
+ body: {name: 'John'},
77
+ })
78
+ ```
79
+
80
+ The WebSocket object is mocked. Calls to `send` and `publishAndSend` are delivered to callbacks registered with `on`.