@typetron/framework 0.5.29 → 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/build/Framework/Providers/WebsocketsProvider.js +2 -2
- package/build/Framework/Providers/WebsocketsProvider.js.map +1 -1
- package/build/Router/Websockets/Handler.d.ts.map +1 -1
- package/build/Router/Websockets/Handler.js +5 -0
- package/build/Router/Websockets/Handler.js.map +1 -1
- package/build/Router/Websockets/Request.d.ts +2 -0
- package/build/Router/Websockets/Request.d.ts.map +1 -1
- package/build/Router/Websockets/Request.js +5 -0
- package/build/Router/Websockets/Request.js.map +1 -1
- package/build/Router/Websockets/WebSocket.d.ts +18 -0
- package/build/Router/Websockets/WebSocket.d.ts.map +1 -0
- package/build/Router/Websockets/WebSocket.js +55 -0
- package/build/Router/Websockets/WebSocket.js.map +1 -0
- package/build/Router/Websockets/index.d.ts +1 -16
- package/build/Router/Websockets/index.d.ts.map +1 -1
- package/build/Router/Websockets/index.js +1 -49
- package/build/Router/Websockets/index.js.map +1 -1
- package/build/Router/Websockets/types.d.ts +4 -2
- package/build/Router/Websockets/types.d.ts.map +1 -1
- package/build/Testing/TestCase.d.ts.map +1 -1
- package/build/Testing/TestCase.js +2 -1
- package/build/Testing/TestCase.js.map +1 -1
- package/package.json +20 -2
|
@@ -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
|
+
```
|
package/Models/Readme.md
ADDED
|
@@ -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
|
+
```
|
package/Router/Readme.md
ADDED
|
@@ -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`.
|