@typetron/framework 0.5.30 → 0.5.32
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 +143 -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 +188 -0
- package/Support/Readme.md +71 -0
- package/Testing/Readme.md +80 -0
- package/Validation/Readme.md +91 -0
- package/build/Cache/Stores/FileStore.d.ts +3 -3
- package/build/Cache/Stores/FileStore.d.ts.map +1 -1
- package/build/Cache/Stores/FileStore.js.map +1 -1
- package/build/Database/List.d.ts +1 -1
- package/build/Database/Migrations/Migrator.d.ts +3 -3
- package/build/Database/Migrations/Migrator.d.ts.map +1 -1
- package/build/Database/Migrations/Migrator.js.map +1 -1
- package/build/Database/Seeders/SeederManager.d.ts +3 -3
- package/build/Database/Seeders/SeederManager.d.ts.map +1 -1
- package/build/Database/Seeders/SeederManager.js +3 -3
- package/build/Database/Seeders/SeederManager.js.map +1 -1
- package/build/Framework/Application.js +1 -1
- package/build/Framework/Application.js.map +1 -1
- package/build/Framework/Config/StorageConfig.d.ts +7 -0
- package/build/Framework/Config/StorageConfig.d.ts.map +1 -0
- package/build/Framework/Config/StorageConfig.js +15 -0
- package/build/Framework/Config/StorageConfig.js.map +1 -0
- package/build/Framework/Config/index.d.ts +1 -0
- package/build/Framework/Config/index.d.ts.map +1 -1
- package/build/Framework/Config/index.js +1 -0
- package/build/Framework/Config/index.js.map +1 -1
- package/build/Framework/Middleware/StaticAssetsMiddleware.d.ts +2 -2
- package/build/Framework/Middleware/StaticAssetsMiddleware.d.ts.map +1 -1
- package/build/Framework/Middleware/StaticAssetsMiddleware.js +1 -1
- package/build/Framework/Middleware/StaticAssetsMiddleware.js.map +1 -1
- package/build/Framework/Providers/CacheProvider.js +1 -1
- package/build/Framework/Providers/CacheProvider.js.map +1 -1
- package/build/Framework/Providers/DatabaseProvider.d.ts +2 -2
- package/build/Framework/Providers/DatabaseProvider.d.ts.map +1 -1
- package/build/Framework/Providers/DatabaseProvider.js +1 -1
- package/build/Framework/Providers/DatabaseProvider.js.map +1 -1
- package/build/Framework/Providers/StorageProvider.d.ts +7 -0
- package/build/Framework/Providers/StorageProvider.d.ts.map +1 -0
- package/build/Framework/Providers/StorageProvider.js +28 -0
- package/build/Framework/Providers/StorageProvider.js.map +1 -0
- package/build/Framework/Providers/WebsocketsProvider.d.ts.map +1 -1
- package/build/Framework/Providers/WebsocketsProvider.js +13 -0
- package/build/Framework/Providers/WebsocketsProvider.js.map +1 -1
- package/build/Framework/index.d.ts +1 -0
- package/build/Framework/index.d.ts.map +1 -1
- package/build/Framework/index.js +1 -0
- package/build/Framework/index.js.map +1 -1
- package/build/Router/Http/Request.d.ts.map +1 -1
- package/build/Router/Http/Request.js +3 -4
- package/build/Router/Http/Request.js.map +1 -1
- package/build/Router/Router.js +1 -1
- package/build/Router/Router.js.map +1 -1
- package/build/Router/Servers/uNetworking.d.ts.map +1 -1
- package/build/Router/Servers/uNetworking.js +3 -4
- package/build/Router/Servers/uNetworking.js.map +1 -1
- package/build/Storage/Drivers/LocalDriver.d.ts +26 -0
- package/build/Storage/Drivers/LocalDriver.d.ts.map +1 -0
- package/build/Storage/Drivers/LocalDriver.js +209 -0
- package/build/Storage/Drivers/LocalDriver.js.map +1 -0
- package/build/Storage/Drivers/MemoryDriver.d.ts +25 -0
- package/build/Storage/Drivers/MemoryDriver.d.ts.map +1 -0
- package/build/Storage/Drivers/MemoryDriver.js +108 -0
- package/build/Storage/Drivers/MemoryDriver.js.map +1 -0
- package/build/Storage/Drivers/ReadOnlyDriver.d.ts +22 -0
- package/build/Storage/Drivers/ReadOnlyDriver.d.ts.map +1 -0
- package/build/Storage/Drivers/ReadOnlyDriver.js +38 -0
- package/build/Storage/Drivers/ReadOnlyDriver.js.map +1 -0
- package/build/Storage/Drivers/S3Driver.d.ts +32 -0
- package/build/Storage/Drivers/S3Driver.d.ts.map +1 -0
- package/build/Storage/Drivers/S3Driver.js +248 -0
- package/build/Storage/Drivers/S3Driver.js.map +1 -0
- package/build/Storage/Drivers/ScopedDriver.d.ts +24 -0
- package/build/Storage/Drivers/ScopedDriver.d.ts.map +1 -0
- package/build/Storage/Drivers/ScopedDriver.js +50 -0
- package/build/Storage/Drivers/ScopedDriver.js.map +1 -0
- package/build/Storage/File.d.ts +14 -4
- package/build/Storage/File.d.ts.map +1 -1
- package/build/Storage/File.js +29 -47
- package/build/Storage/File.js.map +1 -1
- package/build/Storage/Filesystem.d.ts +31 -0
- package/build/Storage/Filesystem.d.ts.map +1 -0
- package/build/Storage/Filesystem.js +121 -0
- package/build/Storage/Filesystem.js.map +1 -0
- package/build/Storage/PathPrefixer.d.ts +9 -0
- package/build/Storage/PathPrefixer.d.ts.map +1 -0
- package/build/Storage/PathPrefixer.js +53 -0
- package/build/Storage/PathPrefixer.js.map +1 -0
- package/build/Storage/Storage.d.ts +37 -9
- package/build/Storage/Storage.d.ts.map +1 -1
- package/build/Storage/Storage.js +85 -101
- package/build/Storage/Storage.js.map +1 -1
- package/build/Storage/StorageDisk.d.ts +34 -0
- package/build/Storage/StorageDisk.d.ts.map +1 -0
- package/build/Storage/StorageDisk.js +104 -0
- package/build/Storage/StorageDisk.js.map +1 -0
- package/build/Storage/StorageDriver.d.ts +19 -0
- package/build/Storage/StorageDriver.d.ts.map +1 -0
- package/build/Storage/StorageDriver.js +7 -0
- package/build/Storage/StorageDriver.js.map +1 -0
- package/build/Storage/UploadedFile.d.ts +9 -0
- package/build/Storage/UploadedFile.d.ts.map +1 -0
- package/build/Storage/UploadedFile.js +22 -0
- package/build/Storage/UploadedFile.js.map +1 -0
- package/build/Storage/errors.d.ts +21 -0
- package/build/Storage/errors.d.ts.map +1 -0
- package/build/Storage/errors.js +43 -0
- package/build/Storage/errors.js.map +1 -0
- package/build/Storage/index.d.ts +12 -0
- package/build/Storage/index.d.ts.map +1 -1
- package/build/Storage/index.js +12 -0
- package/build/Storage/index.js.map +1 -1
- package/build/Storage/types.d.ts +73 -0
- package/build/Storage/types.d.ts.map +1 -0
- package/build/Storage/types.js +3 -0
- package/build/Storage/types.js.map +1 -0
- package/package.json +35 -3
|
@@ -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,188 @@
|
|
|
1
|
+
# Storage
|
|
2
|
+
|
|
3
|
+
- [Introduction](#introduction)
|
|
4
|
+
- [Configuring Disks](#configuring-disks)
|
|
5
|
+
- [Using Disks](#using-disks)
|
|
6
|
+
- [Writing and Reading](#writing-and-reading)
|
|
7
|
+
- [Listing Files](#listing-files)
|
|
8
|
+
- [Uploads and Metadata](#uploads-and-metadata)
|
|
9
|
+
- [URLs and Visibility](#urls-and-visibility)
|
|
10
|
+
- [Scoped and Read-Only Disks](#scoped-and-read-only-disks)
|
|
11
|
+
- [Testing](#testing)
|
|
12
|
+
- [Custom Drivers](#custom-drivers)
|
|
13
|
+
- [Framework Filesystem](#framework-filesystem)
|
|
14
|
+
- [Image Data](#image-data)
|
|
15
|
+
|
|
16
|
+
## Introduction
|
|
17
|
+
|
|
18
|
+
Storage provides one asynchronous API for local files, AWS S3, and S3-compatible object stores. Applications work with logical keys relative
|
|
19
|
+
to a configured disk rather than operating-system paths. `Storage` resolves named disks lazily and forwards methods to the default disk.
|
|
20
|
+
|
|
21
|
+
Each disk wraps a `StorageDriver`. Drivers translate portable operations to a backend while `StorageDisk` supplies shared conveniences such
|
|
22
|
+
as uploads, JSON decoding, URL generation, and testing assertions.
|
|
23
|
+
|
|
24
|
+
## Configuring Disks
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { StorageConfig } from '@Typetron/Framework'
|
|
28
|
+
|
|
29
|
+
export default new StorageConfig({
|
|
30
|
+
default: 'local',
|
|
31
|
+
disks: {
|
|
32
|
+
local: {
|
|
33
|
+
driver: 'local',
|
|
34
|
+
root: 'storage/app',
|
|
35
|
+
url: '/storage',
|
|
36
|
+
},
|
|
37
|
+
s3: {
|
|
38
|
+
driver: 's3',
|
|
39
|
+
bucket: process.env.AWS_BUCKET!,
|
|
40
|
+
region: process.env.AWS_REGION!,
|
|
41
|
+
},
|
|
42
|
+
objects: {
|
|
43
|
+
driver: 's3',
|
|
44
|
+
bucket: process.env.OBJECT_BUCKET!,
|
|
45
|
+
region: process.env.OBJECT_REGION!,
|
|
46
|
+
endpoint: process.env.OBJECT_ENDPOINT,
|
|
47
|
+
forcePathStyle: true,
|
|
48
|
+
credentials: {
|
|
49
|
+
accessKeyId: process.env.OBJECT_ACCESS_KEY!,
|
|
50
|
+
secretAccessKey: process.env.OBJECT_SECRET_KEY!,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
AWS disks use the SDK's default credential chain when `credentials` is omitted. The S3 driver is loaded only when an S3 disk is resolved;
|
|
58
|
+
applications using it must install `@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner`.
|
|
59
|
+
|
|
60
|
+
## Using Disks
|
|
61
|
+
|
|
62
|
+
Register `StorageProvider`, then inject or resolve `Storage`. Methods on `Storage` target the default disk; `disk` selects another
|
|
63
|
+
configured
|
|
64
|
+
disk. `build` creates an uncached on-demand disk.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
await storage.put('users/1.txt', 'John')
|
|
68
|
+
await storage.disk('s3').put('backups/data.json', '{}')
|
|
69
|
+
|
|
70
|
+
const temporary = storage.build({driver: 'local', root: '/tmp/exports'})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Named disks are constructed once and cached. Use `forget(name)` to discard a cached disk so its next lookup rebuilds it.
|
|
74
|
+
|
|
75
|
+
## Writing and Reading
|
|
76
|
+
|
|
77
|
+
`put` accepts a string, `Buffer`, or Node `Readable` and returns a lightweight `File`. `read` returns a `Buffer`; use `readStream` for large
|
|
78
|
+
objects and `json` for JSON content.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const file = await storage.put('users/1.json', JSON.stringify({name: 'John'}), {
|
|
82
|
+
mimeType: 'application/json',
|
|
83
|
+
visibility: 'private',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const user = await storage.json<{name: string}>(file.key)
|
|
87
|
+
const stream = await storage.readStream(file.key)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `copy` and `move` within one disk. Moving copies first and deletes the source only after a successful copy. Cross-disk copies are not
|
|
91
|
+
implicit; stream the source into the destination when that behavior is required.
|
|
92
|
+
|
|
93
|
+
## Listing Files
|
|
94
|
+
|
|
95
|
+
`list` is an `AsyncIterable`, so paginated object-store listings do not have to be retained in memory. `files` collects file entries into
|
|
96
|
+
`File` instances.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
for await (const entry of storage.list('users', {recursive: true})) {
|
|
100
|
+
console.log(entry.type, entry.key)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const files = await storage.files('users', {recursive: true})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Directories are logical prefixes. Writing `users/1/avatar.jpg` creates required local directories, while S3 stores only the object key.
|
|
107
|
+
`deleteDirectory('users/1')` removes that non-empty prefix recursively; an empty prefix is rejected.
|
|
108
|
+
|
|
109
|
+
## Uploads and Metadata
|
|
110
|
+
|
|
111
|
+
HTTP multipart parsing produces `UploadedFile` instances backed by temporary local files. `putFile` streams the upload to its destination.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const avatar = request.files.avatar as UploadedFile
|
|
115
|
+
await storage.disk('s3').putFile(`avatars/${user.id}.jpg`, avatar)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use `stat` for size, last modification, MIME type, ETag, and backend metadata. A `File` may contain metadata already returned by a listing
|
|
119
|
+
or
|
|
120
|
+
write, but Storage does not issue an extra metadata request merely to fill every optional field.
|
|
121
|
+
|
|
122
|
+
## URLs and Visibility
|
|
123
|
+
|
|
124
|
+
`url` returns a public URL when the disk supports or configures one. `temporaryUrl` creates an expiring S3 URL. Unsupported operations throw
|
|
125
|
+
`UnsupportedStorageOperation`.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const publicUrl = storage.disk('s3').url('avatars/1.jpg')
|
|
129
|
+
const privateUrl = await storage.disk('s3').temporaryUrl('reports/1.pdf', 300)
|
|
130
|
+
await storage.setVisibility('avatars/1.jpg', 'public')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Visibility is `public` or `private`. Local disks map it to file permissions; S3 maps it to object ACLs. Bucket policies may still forbid
|
|
134
|
+
public ACLs.
|
|
135
|
+
|
|
136
|
+
## Scoped and Read-Only Disks
|
|
137
|
+
|
|
138
|
+
Composition drivers wrap any named disk without changing its implementation:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
disks: {
|
|
142
|
+
s3: {driver: 's3', bucket: 'app', region: 'eu-central-1'},
|
|
143
|
+
avatars: {driver: 'scoped', disk: 's3', prefix: 'users/avatars'},
|
|
144
|
+
assets: {driver: 'read-only', disk: 's3'},
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A scoped disk exposes only keys below its prefix. Mutating a read-only disk throws `ReadOnlyStorage`.
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
`fake` replaces a named disk with an isolated in-memory driver:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const avatars = storage.fake('avatars')
|
|
156
|
+
await service.uploadAvatar(upload)
|
|
157
|
+
await avatars.assertExists('users/1.jpg')
|
|
158
|
+
await avatars.assertCount('users', 1, true)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Custom Drivers
|
|
162
|
+
|
|
163
|
+
Register a driver factory with `extend`. The factory receives its disk configuration, name, and Storage manager and returns a
|
|
164
|
+
`StorageDriver`. Drivers implement backend translation only; shared application conveniences belong on `StorageDisk`.
|
|
165
|
+
|
|
166
|
+
## Framework Filesystem
|
|
167
|
+
|
|
168
|
+
`Filesystem` is a separate local operating-system service used by Typetron to load configuration, controllers, entities, migrations,
|
|
169
|
+
seeders, static assets, and file cache data. It is available before `StorageConfig` is loaded, accepts real host paths, and keeps these
|
|
170
|
+
framework paths independent of the application's default object-storage disk.
|
|
171
|
+
|
|
172
|
+
`Filesystem` is framework-internal and is not the local-disk API intended for applications. Application code should use `Storage` or a
|
|
173
|
+
named disk such as `storage.disk('local')`. Its synchronous `files` method exists only because framework modules are currently discovered
|
|
174
|
+
synchronously during bootstrap; configured storage listings remain asynchronous and backend-neutral.
|
|
175
|
+
|
|
176
|
+
## Image Data
|
|
177
|
+
|
|
178
|
+
The `fromBase64` helper decodes an image data URL:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { fromBase64 } from '@Typetron/Storage'
|
|
182
|
+
|
|
183
|
+
const image = fromBase64('data:image/png;base64,iVBORw0KGgo=')
|
|
184
|
+
|
|
185
|
+
await storage.put(`storage/avatar.${image.extension}`, image.content)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
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.
|