toiljs 0.0.85 → 0.0.87
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/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +303 -293
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +1 -0
- package/build/compiler/docs.js +8 -24
- package/build/compiler/generate.js +4 -2
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +69 -6
- package/build/compiler/toil-docs.generated.js +64 -22
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/analytics/index.js +7 -3
- package/build/devserver/db/database.d.ts +4 -0
- package/build/devserver/db/database.js +43 -1
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/types.d.ts +3 -0
- package/build/devserver/db/types.js +2 -0
- package/build/devserver/runtime/module.js +4 -1
- package/docs/README.md +104 -65
- package/docs/auth/README.md +102 -0
- package/docs/auth/configuration.md +94 -0
- package/docs/auth/extending.md +202 -0
- package/docs/auth/how-it-works.md +138 -0
- package/docs/auth/usage.md +188 -0
- package/docs/backend/README.md +143 -0
- package/docs/backend/data.md +351 -0
- package/docs/backend/rest.md +402 -0
- package/docs/backend/rpc.md +226 -0
- package/docs/background/README.md +114 -0
- package/docs/background/daemons.md +230 -0
- package/docs/background/derive.md +179 -0
- package/docs/cli/README.md +377 -0
- package/docs/concepts/config.md +416 -0
- package/docs/concepts/decorators.md +127 -0
- package/docs/concepts/security.md +108 -0
- package/docs/concepts/tiers.md +166 -0
- package/docs/concepts/types.md +216 -0
- package/docs/database/README.md +143 -0
- package/docs/database/capacity.md +350 -0
- package/docs/database/counters.md +174 -0
- package/docs/database/documents.md +255 -0
- package/docs/database/events.md +307 -0
- package/docs/database/membership.md +246 -0
- package/docs/database/setup.md +155 -0
- package/docs/database/unique.md +216 -0
- package/docs/database/views.md +246 -0
- package/docs/frontend/README.md +101 -0
- package/docs/frontend/data-fetching.md +243 -0
- package/docs/frontend/images.md +148 -0
- package/docs/frontend/metadata.md +344 -0
- package/docs/frontend/rendering.md +236 -0
- package/docs/frontend/routing.md +344 -0
- package/docs/frontend/scripts.md +118 -0
- package/docs/frontend/search.md +191 -0
- package/docs/frontend/styling.md +147 -0
- package/docs/getting-started/README.md +81 -0
- package/docs/getting-started/create-project.md +131 -0
- package/docs/getting-started/deploy.md +101 -0
- package/docs/getting-started/first-app.md +215 -0
- package/docs/getting-started/installation.md +106 -0
- package/docs/getting-started/migrating.md +125 -0
- package/docs/getting-started/project-structure.md +163 -0
- package/docs/introduction/README.md +41 -0
- package/docs/introduction/design-principles.md +55 -0
- package/docs/introduction/distributed.md +74 -0
- package/docs/introduction/how-it-works.md +74 -0
- package/docs/introduction/hyperscale.md +51 -0
- package/docs/introduction/modern-stack.md +36 -0
- package/docs/introduction/vs-other-frameworks.md +48 -0
- package/docs/introduction/why-toil.md +93 -0
- package/docs/llms.txt +90 -0
- package/docs/realtime/README.md +102 -0
- package/docs/realtime/channels.md +211 -0
- package/docs/realtime/streams.md +369 -0
- package/docs/services/README.md +60 -0
- package/docs/services/analytics.md +268 -0
- package/docs/services/caching.md +175 -0
- package/docs/services/cookies.md +235 -0
- package/docs/services/crypto.md +209 -0
- package/docs/services/email.md +289 -0
- package/docs/services/environment.md +141 -0
- package/docs/services/ratelimit.md +174 -0
- package/docs/services/time.md +85 -0
- package/examples/basic/client/routes/analytics.tsx +13 -12
- package/examples/basic/server/models/SiteAnalytics.ts +29 -17
- package/examples/basic/server/routes/Analytics.ts +15 -17
- package/examples/basic/server/routes/UserId.ts +22 -0
- package/package.json +15 -11
- package/scripts/gen-toil-docs.mjs +24 -35
- package/server/auth/AuthController.ts +336 -0
- package/server/auth/AuthUser.ts +23 -0
- package/server/auth/index.ts +16 -0
- package/server/globals/auth.ts +31 -0
- package/server/globals/userid.ts +107 -0
- package/src/compiler/config.ts +13 -0
- package/src/compiler/docs.ts +16 -33
- package/src/compiler/generate.ts +6 -2
- package/src/compiler/index.ts +114 -6
- package/src/compiler/toil-docs.generated.ts +64 -22
- package/src/devserver/analytics/index.ts +10 -4
- package/src/devserver/db/database.ts +67 -1
- package/src/devserver/db/index.ts +1 -0
- package/src/devserver/db/types.ts +13 -0
- package/src/devserver/runtime/module.ts +7 -0
- package/test/analytics-dev.test.ts +2 -1
- package/test/devserver-database.test.ts +113 -0
- package/docs/auth-todo.md +0 -149
- package/docs/auth.md +0 -322
- package/docs/caching.md +0 -115
- package/docs/cli.md +0 -17
- package/docs/client.md +0 -39
- package/docs/cookies.md +0 -457
- package/docs/crypto.md +0 -130
- package/docs/daemon.md +0 -123
- package/docs/data.md +0 -131
- package/docs/derive.md +0 -159
- package/docs/email.md +0 -326
- package/docs/environment.md +0 -97
- package/docs/getting-started.md +0 -128
- package/docs/index.md +0 -30
- package/docs/ratelimit.md +0 -95
- package/docs/routing.md +0 -259
- package/docs/rpc.md +0 -149
- package/docs/server.md +0 -61
- package/docs/ssr.md +0 -632
- package/docs/streams.md +0 -178
- package/docs/styling.md +0 -22
- package/docs/tiers.md +0 -133
- package/docs/time.md +0 -43
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Data types (`@data`)
|
|
2
|
+
|
|
3
|
+
`@data` turns a plain class into a typed value that can travel safely between your frontend and your WASM backend, and in and out of the database, with both a binary and a JSON codec generated for you.
|
|
4
|
+
|
|
5
|
+
## What `@data` is
|
|
6
|
+
|
|
7
|
+
A **codec** is a pair of functions: one that turns a value into bytes (encode), and one that turns those bytes back into a value (decode). Any time data crosses a boundary (browser to server, server to database, one WASM call to another) it has to become bytes and back. Writing that by hand is tedious and easy to get wrong.
|
|
8
|
+
|
|
9
|
+
`@data` writes it for you. You declare a class with typed fields, tag it `@data`, and the compiler synthesizes a deterministic binary codec and a JSON codec on the class. The exact same class is also generated into your `shared/server.ts`, so the browser and the WASM backend agree on the format down to the byte.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
@data
|
|
13
|
+
class Player {
|
|
14
|
+
username: string = '';
|
|
15
|
+
admin: bool = false;
|
|
16
|
+
score: u64 = 0;
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is a complete, serializable type. Note every field has a **default value**; that is required (the generated decoder and the client constructor use it).
|
|
21
|
+
|
|
22
|
+
## Why and when
|
|
23
|
+
|
|
24
|
+
`@data` is the backbone of almost everything that moves in a toiljs app:
|
|
25
|
+
|
|
26
|
+
- **Request and response bodies** for [`@rest`](./rest.md) routes.
|
|
27
|
+
- **Arguments and return values** for [`@service` / `@remote`](./rpc.md) calls.
|
|
28
|
+
- **Values stored in [ToilDB](../database/README.md)**.
|
|
29
|
+
- **Sessions**, [stream](../realtime/README.md) messages, and any custom payload you design.
|
|
30
|
+
|
|
31
|
+
Whenever a route, an RPC method, or the database needs a structured value, that value is a `@data` class. You will define these constantly.
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
flowchart LR
|
|
35
|
+
D["@data class<br/>(one definition)"] --> R["REST route body"]
|
|
36
|
+
D --> P["RPC argument / result"]
|
|
37
|
+
D --> B[("ToilDB value")]
|
|
38
|
+
D --> S["Stream message"]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What the compiler generates
|
|
42
|
+
|
|
43
|
+
From your `@data` class, the compiler adds these members:
|
|
44
|
+
|
|
45
|
+
| Member | What it does |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `encode(): Uint8Array` | Serialize to bytes, with a 4-byte type-id prefix. |
|
|
48
|
+
| `static decode(buf): T` | Rebuild a value from bytes produced by `encode()`. |
|
|
49
|
+
| `encodeInto(w: DataWriter)` | Serialize without the type-id frame, for nesting inside another value. |
|
|
50
|
+
| `static decodeFrom(r: DataReader): T` | The matching read, for nested values. |
|
|
51
|
+
| `toJSON()` / `static fromJSON(v)` | The JSON codec (large integers become decimal strings, so `JSON.parse` keeps them exact). |
|
|
52
|
+
| `static dataId(): u32` | A stable hash (FNV-1a) of the class name, written as the type-id prefix. |
|
|
53
|
+
|
|
54
|
+
You mostly do not call these directly; routes, RPC, and the database call them for you. But they are there when you need them (for example `value.toJSON().toString()` to build a `Response.json`).
|
|
55
|
+
|
|
56
|
+
## Supported field types
|
|
57
|
+
|
|
58
|
+
A `@data` field may be:
|
|
59
|
+
|
|
60
|
+
- a scalar: `u8` through `u256`, `i8` through `i256`, `f32`, `f64`, `bool`;
|
|
61
|
+
- a `string`;
|
|
62
|
+
- a `Uint8Array` (a raw byte buffer);
|
|
63
|
+
- a nested `@data` class;
|
|
64
|
+
- an array `T[]` of any of the above.
|
|
65
|
+
|
|
66
|
+
For the number types (why `u64` is a `bigint` on the client, when to reach for `u256`, and so on), see [Types](../concepts/types.md).
|
|
67
|
+
|
|
68
|
+
Give every field a default. The layout is exactly the field declaration order (this matters, see [gotchas](#gotchas)).
|
|
69
|
+
|
|
70
|
+
## Nested `@data`, arrays, and bytes
|
|
71
|
+
|
|
72
|
+
`@data` classes compose. A field can be another `@data` class, an array, or a byte buffer, and it all encodes and decodes as one value:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
@data
|
|
76
|
+
class Tag {
|
|
77
|
+
label: string = '';
|
|
78
|
+
weight: f32 = 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@data
|
|
82
|
+
class Document {
|
|
83
|
+
id: u64 = 0;
|
|
84
|
+
title: string = '';
|
|
85
|
+
tags: Tag[] = []; // array of nested @data
|
|
86
|
+
authors: string[] = []; // array of strings
|
|
87
|
+
thumbnail: Uint8Array = new Uint8Array(0); // raw bytes
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`Document.encode()` walks the whole tree: it writes `id`, `title`, each `Tag` (via `encodeInto`), each author string, and the raw `thumbnail` bytes, in field order. `Document.decode(bytes)` reads them back in the same order. On the JSON side, a `Uint8Array` field becomes a JSON array of byte numbers, and nested `@data` fields become nested JSON objects.
|
|
92
|
+
|
|
93
|
+
## Using `@data` in a route
|
|
94
|
+
|
|
95
|
+
A route's body parameter and its return value are `@data` values. Which codec runs depends on the route's stream mode (see [REST bodies](./rest.md#request-and-response-bodies)):
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// JSON route (the default): body from JSON.parse, result via toJSON()
|
|
99
|
+
@post('/')
|
|
100
|
+
public create(input: NewPlayer): Player { /* ... */ }
|
|
101
|
+
|
|
102
|
+
// Binary route: body via decode(), result via encode()
|
|
103
|
+
@route({ method: Methods.POST, path: '/blob', stream: DataStream.Binary })
|
|
104
|
+
public blob(input: FileData): FileResult { /* ... */ }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- In a **JSON** route, the incoming body is `JSON.parse`d and revived with the type's `fromJSON`, and the returned value is serialized with `toJSON()`.
|
|
108
|
+
- In a **Binary** route, the incoming body is `decode`d and the returned value is `encode`d.
|
|
109
|
+
|
|
110
|
+
You do not call the codec yourself in either case; you just declare the types.
|
|
111
|
+
|
|
112
|
+
## The raw codec: `DataWriter` and `DataReader`
|
|
113
|
+
|
|
114
|
+
Sometimes you want to lay out bytes by hand: a custom body, a session token, a challenge message, a wire format someone else defined. For that, use the codec directly. It lives in the `data` module:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { DataWriter, DataReader } from 'data';
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is the same codec `@data` classes are built from, and it has a byte-for-byte identical TypeScript version in `toiljs/io` (`src/io/codec.ts`), so your browser code can read and write the exact same bytes the WASM backend does.
|
|
121
|
+
|
|
122
|
+
### `DataWriter`
|
|
123
|
+
|
|
124
|
+
Every write method returns the writer, so calls chain.
|
|
125
|
+
|
|
126
|
+
| Method | Signature | Wire format |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `writeU8` / `writeI8` | `(v): DataWriter` | 1 byte |
|
|
129
|
+
| `writeU16` / `writeI16` | `(v): DataWriter` | 2 bytes, little-endian |
|
|
130
|
+
| `writeU32` / `writeI32` | `(v): DataWriter` | 4 bytes, LE |
|
|
131
|
+
| `writeU64` / `writeI64` | `(v): DataWriter` | 8 bytes, LE |
|
|
132
|
+
| `writeF32` / `writeF64` | `(v): DataWriter` | 4 / 8 bytes, IEEE-754 LE |
|
|
133
|
+
| `writeBool` | `(v): DataWriter` | 1 byte (`1` / `0`) |
|
|
134
|
+
| `writeBytes` | `(b: Uint8Array): DataWriter` | `u32` length (LE) + the raw bytes |
|
|
135
|
+
| `writeString` | `(s: string): DataWriter` | `u32` length (LE) + UTF-8 bytes |
|
|
136
|
+
| `writeU128` / `writeI128` | `(v): DataWriter` | two `u64` limbs (lo, hi) |
|
|
137
|
+
| `writeU256` / `writeI256` | `(v): DataWriter` | four `u64` limbs |
|
|
138
|
+
| `length` | `(): i32` | bytes written so far |
|
|
139
|
+
| `toBytes` | `(): Uint8Array` | an exact-length copy of the buffer |
|
|
140
|
+
|
|
141
|
+
### `DataReader`
|
|
142
|
+
|
|
143
|
+
Reads are **bounds-safe**: reading past the end of the buffer never crashes. Instead the read returns a zero or empty default and flips the reader's public `ok` flag to `false`. Check `ok` after a sequence of reads to catch a truncated or malformed buffer.
|
|
144
|
+
|
|
145
|
+
| Method | Signature | On over-read |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `readU8` / `readI8` | `(): integer` | `0` |
|
|
148
|
+
| `readU16`..`readU64`, `readI16`..`readI64` | `(): integer` | `0` |
|
|
149
|
+
| `readF32` / `readF64` | `(): float` | `0` |
|
|
150
|
+
| `readBool` | `(): bool` | `false` |
|
|
151
|
+
| `readBytes` | `(): Uint8Array` | empty array |
|
|
152
|
+
| `readString` | `(): string` | `""` |
|
|
153
|
+
| `readU128` / `readI128` / `readU256` / `readI256` | `(): bignum` | `0` |
|
|
154
|
+
| `remaining` | `(): i32` | bytes left unread |
|
|
155
|
+
| `ok` | `bool` (field) | `false` once any read over-ran |
|
|
156
|
+
|
|
157
|
+
### Encode and decode, both directions
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { DataWriter, DataReader } from 'data';
|
|
161
|
+
|
|
162
|
+
// Encode: a version byte, a name, a score, then a blob.
|
|
163
|
+
const out = new DataWriter()
|
|
164
|
+
.writeU8(1)
|
|
165
|
+
.writeString('alice')
|
|
166
|
+
.writeU64(1234)
|
|
167
|
+
.writeBytes(payload)
|
|
168
|
+
.toBytes();
|
|
169
|
+
|
|
170
|
+
// Decode: read the fields back in the exact same order.
|
|
171
|
+
const r = new DataReader(out);
|
|
172
|
+
const version = r.readU8();
|
|
173
|
+
const name = r.readString();
|
|
174
|
+
const score = r.readU64();
|
|
175
|
+
const blob = r.readBytes();
|
|
176
|
+
if (!r.ok) return Response.badRequest('truncated');
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The order of reads must match the order of writes exactly. The layout is the format.
|
|
180
|
+
|
|
181
|
+
## JSON vs binary: which to use
|
|
182
|
+
|
|
183
|
+
You usually pick this at the route level (see [REST bodies](./rest.md#request-and-response-bodies)), but the trade-off is the same everywhere:
|
|
184
|
+
|
|
185
|
+
- **JSON** is human-readable and understood by every tool. Pick it for endpoints a browser or a third party calls directly. Large integers ride as decimal strings so they stay exact.
|
|
186
|
+
- **Binary** is smaller, faster, and lossless for big numbers. Pick it for app-to-app traffic and anything performance sensitive. RPC always uses binary under the hood.
|
|
187
|
+
|
|
188
|
+
## Evolving a `@data` format: `@migrate`
|
|
189
|
+
|
|
190
|
+
Once records are stored, changing a `@data` **value** type is a problem: as the [gotchas](#gotchas) explain, the binary layout *is* the field order, so adding or changing a field means the bytes already on disk no longer match the new shape and would fail to decode. `@migrate` is how you evolve a stored type without a backfill and without downtime.
|
|
191
|
+
|
|
192
|
+
**What it is.** `@migrate` marks a plain (free) function that upgrades one record written under an **old** version of a value type into the **current** shape. The compiler weaves that function into the value type's decoder as a version dispatch: every stored record carries the schema version it was written under, so when a read hits an old record, the decoder decodes it as its old shape and runs your `@migrate` forward to today's shape. This happens **lazily**, per row, only when a record is actually read (nothing rewrites your whole collection up front).
|
|
193
|
+
|
|
194
|
+
**When to use it.** Any time you change a `@data` type that is already stored in ToilDB: you added a field, renamed one, changed a type. New writes use the new layout; old rows keep working because the migration upgrades them on the way out.
|
|
195
|
+
|
|
196
|
+
Here is the whole story. You shipped this value type:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// server/models/GuestEntry.ts (version 1: what you shipped first)
|
|
200
|
+
@data
|
|
201
|
+
export class GuestEntry {
|
|
202
|
+
author: string = '';
|
|
203
|
+
message: string = '';
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Later you add an `at` timestamp:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// server/models/GuestEntry.ts (version 2: gained an `at` field)
|
|
211
|
+
@data
|
|
212
|
+
export class GuestEntry {
|
|
213
|
+
author: string = '';
|
|
214
|
+
message: string = '';
|
|
215
|
+
at: u64 = 0; // NEW field
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Every entry already stored was written without `at`, so its bytes cannot decode as the new `GuestEntry`. You add a migration file to bridge them:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// server/migrations/GuestEntry.migration.ts
|
|
223
|
+
import { GuestEntry } from '../models/GuestEntry';
|
|
224
|
+
|
|
225
|
+
// Keep the ORIGINAL layout as its own class so old rows still decode.
|
|
226
|
+
// One kept class per past version.
|
|
227
|
+
@data
|
|
228
|
+
export class GuestEntryV1 {
|
|
229
|
+
author: string = '';
|
|
230
|
+
message: string = '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Upgrade a v1 row to the current GuestEntry. This is the DELTA form:
|
|
234
|
+
// (old, into). The compiler pre-copies the fields the two layouts share
|
|
235
|
+
// (author, message), so your body fills only what is new.
|
|
236
|
+
@migrate
|
|
237
|
+
export function up(old: GuestEntryV1, into: GuestEntry): void {
|
|
238
|
+
into.at = 0; // unknown for entries written before the timestamp existed
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
That is it. Old entries now surface as fully-formed `GuestEntry` values with `at = 0`; new entries carry a real timestamp; the same read path serves both.
|
|
243
|
+
|
|
244
|
+
A `@migrate` function comes in two shapes, and you pick whichever reads better:
|
|
245
|
+
|
|
246
|
+
- **Delta form** `up(old: OldType, into: NewType): void` (used above). The compiler copies every field the two layouts share by name and type, and your body fills only the changed or new fields. Least to write when most fields carry over.
|
|
247
|
+
- **Full form** `up(old: OldType): NewType`. Your body builds and returns the whole new value itself. Use it when the transform is not a simple field-for-field copy.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
// The same migration written in the full form.
|
|
251
|
+
@migrate
|
|
252
|
+
export function up(old: GuestEntryV1): GuestEntry {
|
|
253
|
+
const e = new GuestEntry();
|
|
254
|
+
e.author = old.author;
|
|
255
|
+
e.message = old.message;
|
|
256
|
+
e.at = 0;
|
|
257
|
+
return e;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Gotchas specific to `@migrate`:
|
|
262
|
+
|
|
263
|
+
- **Location is enforced.** Every `@migrate` must live in a `migrations/<Type>.migration.ts` file (a `*.migration.ts` file under a `migrations/` folder). The build auto-discovers it (nothing imports it), and a `@migrate` placed anywhere else is a hard compile error, because it would silently never run.
|
|
264
|
+
- **A migration is a pure value transform.** It may not touch the database or any host service; it only turns old fields into new ones. Trying to read or write ToilDB from inside a `@migrate` is a compile error.
|
|
265
|
+
- **Migrations chain.** If you evolve a type more than once, keep one old class and one `@migrate` per step (`V0 -> V1`, `V1 -> V2`). The compiler walks the chain, so a row written under the oldest layout is carried all the way forward to the current shape, shortest path first.
|
|
266
|
+
- **When it actually runs.** On any read that decodes an old row (`get`, `getMany`, a view read, events `latest`). It also runs when a [`@derive`](../background/derive.md) rebuilds a view on box load and re-reads old stored events (see [Views](../database/views.md)).
|
|
267
|
+
|
|
268
|
+
## Dynamic JSON on the server: the `JSON` value tree
|
|
269
|
+
|
|
270
|
+
`@data` is for shapes you know ahead of time. Sometimes you do not: a webhook whose body varies, a third-party payload with optional fields, or a response you assemble on the fly. For those, toiljs gives you a `JSON` **value tree**: an in-memory value that can be a null, a bool, a number, a string, an array, or an object, which you read and build at runtime.
|
|
271
|
+
|
|
272
|
+
**What it is.** `JSON` is an ambient global class (no import needed). It represents one dynamic JSON value. `JSON.parse(text)` turns JSON text into one of these trees, and a `@data` class's `toJSON()` actually returns one too. It is the **untyped** counterpart to `@data`'s typed, fixed-shape codec: reach for `@data` when the shape is known and you want type safety, and for `JSON` when the shape is dynamic.
|
|
273
|
+
|
|
274
|
+
The statics that make a value:
|
|
275
|
+
|
|
276
|
+
| Static | What it does |
|
|
277
|
+
| --- | --- |
|
|
278
|
+
| `JSON.parse(text: string): JSON` | Parse JSON text into a value tree (returns an error value on malformed input). |
|
|
279
|
+
| `JSON.obj(): JSON` | A new empty object; fill it with `.set(key, value)`. |
|
|
280
|
+
| `JSON.arr(): JSON` | A new empty array; fill it with `.push(value)`. |
|
|
281
|
+
| `JSON.of<T>(value: T): JSON` | Wrap a scalar, string, bool, or array as a JSON value. |
|
|
282
|
+
| `JSON.nul(): JSON` | A JSON null. |
|
|
283
|
+
| `JSON.stringify<T>(value: T): string` | Serialize a scalar / string / bool / array value straight to a JSON string. |
|
|
284
|
+
|
|
285
|
+
The instance methods that read and build a value:
|
|
286
|
+
|
|
287
|
+
| Method | What it does |
|
|
288
|
+
| --- | --- |
|
|
289
|
+
| `.isObject()` / `.isArray()` / `.isString()` / `.isNumber()` / `.isBool()` / `.isNull(): bool` | Test the value's type before you read it. |
|
|
290
|
+
| `.has(key: string): bool` | Whether an object has `key`. |
|
|
291
|
+
| `.get(key: string): JSON` | The value for `key` on an object. |
|
|
292
|
+
| `.objectKeys(): Array<string>` | The keys of an object. |
|
|
293
|
+
| `.at(index: i32): JSON` | The element at `index` of an array. |
|
|
294
|
+
| `.length(): i32` | The element count of an array (0 otherwise). |
|
|
295
|
+
| `.asString(): string` | Read the value as a string. |
|
|
296
|
+
| `.asF64(): f64` / `.asI64(): i64` / `.asU64(): u64` | Read the value as a number. |
|
|
297
|
+
| `.asBool(): bool` | Read the value as a bool. |
|
|
298
|
+
| `.set(key: string, value: JSON): JSON` | Set a key on an object; returns `this`, so calls chain. |
|
|
299
|
+
| `.push(value: JSON): JSON` | Append to an array; returns `this`, so calls chain. |
|
|
300
|
+
| `.toString(): string` | Serialize this tree back to a JSON string. |
|
|
301
|
+
|
|
302
|
+
Reading an untyped body and building a reply, together:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
306
|
+
|
|
307
|
+
@rest('echo')
|
|
308
|
+
class Echo {
|
|
309
|
+
// POST /echo with an arbitrary JSON body we do not model as a @data class.
|
|
310
|
+
@post('/')
|
|
311
|
+
public handle(ctx: RouteContext): Response {
|
|
312
|
+
const body = JSON.parse(ctx.text()); // ctx.text() is the raw body as text
|
|
313
|
+
if (!body.isObject() || !body.has('name')) {
|
|
314
|
+
return Response.badRequest('expected an object with a "name" field');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Read fields out of the tree, guarding types and optional keys.
|
|
318
|
+
const name = body.get('name').asString();
|
|
319
|
+
const age = body.has('age') ? body.get('age').asI64() : 0;
|
|
320
|
+
|
|
321
|
+
// Build a fresh JSON object to send back (chaining .set).
|
|
322
|
+
const out = JSON.obj()
|
|
323
|
+
.set('greeting', JSON.of<string>('hello, ' + name))
|
|
324
|
+
.set('age', JSON.of<i64>(age));
|
|
325
|
+
return Response.json(out.toString());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Gotchas specific to `JSON`:
|
|
331
|
+
|
|
332
|
+
- **Check the type before you read.** `JSON.parse` never throws; a malformed input or a wrong-typed field yields an error or default value rather than a crash. Use `.isObject()` / `.has(...)` / `.isString()` and friends to validate untrusted input before you trust it.
|
|
333
|
+
- **It is a value tree, not a typed struct.** You get no compile-time field checking. When a shape is stable, prefer a `@data` class so the compiler catches typos and the client gets a typed type for free.
|
|
334
|
+
- **Big-integer care still applies.** As with any JSON, integers above 2^53 are best carried as strings; read them with `.asString()` when exactness matters.
|
|
335
|
+
|
|
336
|
+
## Gotchas
|
|
337
|
+
|
|
338
|
+
- **Field order is the format.** The binary layout is exactly your field declaration order. Reordering fields, or changing a field's type, is a breaking change: old bytes will decode wrong. To evolve a format safely, add new fields at the **end** (and, for hand-rolled payloads, bump a leading version byte).
|
|
339
|
+
- **Every field needs a default.** The generated decoder and the client constructor rely on it. A field with no default will not compile as `@data`.
|
|
340
|
+
- **`encode()` carries a type id; `encodeInto` does not.** The 4-byte `dataId()` prefix lets a decoder confirm it is reading the type it expected. When nesting one `@data` inside another, `encodeInto` / `decodeFrom` skip that frame (the outer type already identifies the whole value).
|
|
341
|
+
- **Endianness.** The WASM codec is little-endian. The `toiljs/io` codec defaults to little-endian too, and also accepts a per-call big-endian flag for network formats. Keep both ends on the same setting.
|
|
342
|
+
- **Plain JSON numbers lose precision above 2^53.** That is why `@data` sends 64-bit-and-larger integers as decimal strings over JSON. If you hand-build JSON, do the same, or use the binary codec.
|
|
343
|
+
- **`DataReader` never throws; check `ok`.** An over-read returns a default and sets `ok = false`. Always check `ok` after decoding untrusted bytes.
|
|
344
|
+
|
|
345
|
+
## Related
|
|
346
|
+
|
|
347
|
+
- [Types](../concepts/types.md): `u64`, `u256`, `f64`, and how each maps to `number` or `bigint`.
|
|
348
|
+
- [HTTP routes (`@rest`)](./rest.md): where `@data` bodies and return values are used, and the JSON vs binary route modes.
|
|
349
|
+
- [Typed RPC](./rpc.md): `@data` as RPC arguments and results, and the generated client classes.
|
|
350
|
+
- [The database](../database/README.md): storing `@data` values in ToilDB.
|
|
351
|
+
- [Backend overview](./README.md): where `@data` fits in the request lifecycle.
|