@nxgt/typespec 0.9.2 → 0.11.0
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/README.md +71 -14
- package/docs/README.md +2 -0
- package/docs/guide/auth.md +1 -1
- package/docs/guide/columns.md +29 -15
- package/docs/guide/errors.md +1 -1
- package/docs/guide/getting-started.md +16 -7
- package/docs/guide/headers.md +2 -2
- package/docs/guide/operation-ids.md +1 -1
- package/docs/guide/requests.md +145 -0
- package/docs/guide/scalars.md +288 -0
- package/docs/roadmap.md +22 -0
- package/docs/troubleshooting.md +95 -3
- package/lib/main.tsp +1 -0
- package/lib/requests.tsp +36 -0
- package/lib/scalars/color/hex-color-code.tsp +17 -0
- package/lib/scalars/color/hsl.tsp +18 -0
- package/lib/scalars/color/hsla.tsp +18 -0
- package/lib/scalars/color/rgb.tsp +18 -0
- package/lib/scalars/color/rgba.tsp +18 -0
- package/lib/scalars/color.tsp +6 -0
- package/lib/scalars/date-time/date-time.tsp +19 -0
- package/lib/scalars/date-time/date.tsp +17 -0
- package/lib/scalars/date-time/iso-duration.tsp +17 -0
- package/lib/scalars/date-time/local-date-time.tsp +15 -0
- package/lib/scalars/date-time/local-time.tsp +15 -0
- package/lib/scalars/date-time/time-zone.tsp +18 -0
- package/lib/scalars/date-time/time.tsp +17 -0
- package/lib/scalars/date-time/timestamp.tsp +15 -0
- package/lib/scalars/date-time/utc-offset.tsp +15 -0
- package/lib/scalars/date-time.tsp +10 -0
- package/lib/scalars/encoding/base64-url.tsp +17 -0
- package/lib/scalars/encoding/base64.tsp +18 -0
- package/lib/scalars/encoding/hexadecimal.tsp +15 -0
- package/lib/scalars/encoding/jwt.tsp +19 -0
- package/lib/scalars/encoding/sha256.tsp +16 -0
- package/lib/scalars/encoding/sha512.tsp +16 -0
- package/lib/scalars/encoding.tsp +7 -0
- package/lib/scalars/finance/currency.tsp +17 -0
- package/lib/scalars/finance/iban.tsp +19 -0
- package/lib/scalars/finance.tsp +3 -0
- package/lib/scalars/geo/latitude.tsp +14 -0
- package/lib/scalars/geo/longitude.tsp +14 -0
- package/lib/scalars/geo.tsp +3 -0
- package/lib/scalars/identifier/cuid2.tsp +17 -0
- package/lib/scalars/identifier/guid.tsp +12 -0
- package/lib/scalars/identifier/isbn.tsp +18 -0
- package/lib/scalars/identifier/ksuid.tsp +18 -0
- package/lib/scalars/identifier/nano-id.tsp +16 -0
- package/lib/scalars/identifier/object-id.tsp +16 -0
- package/lib/scalars/identifier/sem-ver.tsp +17 -0
- package/lib/scalars/identifier/ulid.tsp +17 -0
- package/lib/scalars/identifier/uuid-v4.tsp +16 -0
- package/lib/scalars/identifier/uuid-v7.tsp +16 -0
- package/lib/scalars/identifier/uuid.tsp +18 -0
- package/lib/scalars/identifier/xid.tsp +17 -0
- package/lib/scalars/identifier.tsp +13 -0
- package/lib/scalars/locale/country-code.tsp +17 -0
- package/lib/scalars/locale/locale.tsp +20 -0
- package/lib/scalars/locale.tsp +3 -0
- package/lib/scalars/network/cidr-v4.tsp +19 -0
- package/lib/scalars/network/cidr-v6.tsp +18 -0
- package/lib/scalars/network/email-address.tsp +20 -0
- package/lib/scalars/network/hostname.tsp +21 -0
- package/lib/scalars/network/http-url.tsp +21 -0
- package/lib/scalars/network/ip-v4.tsp +17 -0
- package/lib/scalars/network/ip-v6.tsp +18 -0
- package/lib/scalars/network/ip.tsp +17 -0
- package/lib/scalars/network/mac.tsp +16 -0
- package/lib/scalars/network/phone-number.tsp +18 -0
- package/lib/scalars/network.tsp +11 -0
- package/lib/scalars/number/big-int.tsp +15 -0
- package/lib/scalars/number/long.tsp +17 -0
- package/lib/scalars/number/negative-float.tsp +13 -0
- package/lib/scalars/number/negative-int.tsp +15 -0
- package/lib/scalars/number/non-negative-float.tsp +13 -0
- package/lib/scalars/number/non-negative-int.tsp +15 -0
- package/lib/scalars/number/non-positive-float.tsp +13 -0
- package/lib/scalars/number/non-positive-int.tsp +15 -0
- package/lib/scalars/number/port.tsp +14 -0
- package/lib/scalars/number/positive-float.tsp +13 -0
- package/lib/scalars/number/positive-int.tsp +15 -0
- package/lib/scalars/number/safe-int.tsp +16 -0
- package/lib/scalars/number.tsp +13 -0
- package/lib/scalars/string/emoji.tsp +21 -0
- package/lib/scalars/string/non-empty-string.tsp +15 -0
- package/lib/scalars/string.tsp +3 -0
- package/lib/scalars/value.tsp +2 -0
- package/lib/scalars.tsp +13 -12
- package/package.json +3 -3
- package/templates/nxgt/main.tsp +11 -4
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ namespace Blog;
|
|
|
125
125
|
@route("/posts")
|
|
126
126
|
interface Posts {
|
|
127
127
|
@get getPost(@path postId: string): Post | NotFound | Unauthorized;
|
|
128
|
-
@post createPost(@body post:
|
|
128
|
+
@post createPost(@body post: CreateRequest<Post>): {
|
|
129
129
|
@statusCode _: 201;
|
|
130
130
|
@body post: Post;
|
|
131
131
|
} | BadRequest | Conflict | ErrorResponse<503>;
|
|
@@ -175,8 +175,9 @@ an alias leaves out:
|
|
|
175
175
|
interface Users {
|
|
176
176
|
@get list(...PageParameters): Page<User> | ListErrors;
|
|
177
177
|
@get get(@path id: uuid): User | GetErrors;
|
|
178
|
-
@post create(@body user: User): User | CreateErrors | AuthErrors | TooManyRequests;
|
|
179
|
-
@
|
|
178
|
+
@post create(@body user: CreateRequest<User>): User | CreateErrors | AuthErrors | TooManyRequests;
|
|
179
|
+
@put update(@path id: uuid, @body user: UpdateRequest<User>): User | UpdateErrors;
|
|
180
|
+
@patch patch(@path id: uuid, @body user: PatchRequest<User>): User | UpdateErrors;
|
|
180
181
|
@delete delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
181
182
|
}
|
|
182
183
|
```
|
|
@@ -184,6 +185,26 @@ interface Users {
|
|
|
184
185
|
An alias is exactly its union, so the OpenAPI it emits is the same as
|
|
185
186
|
writing the responses out.
|
|
186
187
|
|
|
188
|
+
### Request bodies
|
|
189
|
+
|
|
190
|
+
`CreateRequest<T>`, `UpdateRequest<T>` and `PatchRequest<T>` are TypeSpec's own
|
|
191
|
+
`Create`, `Update` and `MergePatchUpdate`, named so the generated types and
|
|
192
|
+
schemas read `CreatePostRequest`, `UpdatePostRequest` and `PatchPostRequest`:
|
|
193
|
+
|
|
194
|
+
```tsp
|
|
195
|
+
@post createPost(@body post: CreateRequest<Post>): {
|
|
196
|
+
@statusCode _: 201;
|
|
197
|
+
@body post: Post;
|
|
198
|
+
} | BadRequest;
|
|
199
|
+
@put updatePost(@path postId: uuid, @body post: UpdateRequest<Post>): Post | BadRequest | NotFound;
|
|
200
|
+
@patch patchPost(@path postId: uuid, @body post: PatchRequest<Post>): Post | BadRequest | NotFound;
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A `PUT` body keeps each property as optional as the model declares it; a
|
|
204
|
+
`PATCH` body is a JSON Merge Patch, `application/merge-patch+json`, with
|
|
205
|
+
every property optional. `Create<Post, "New{name}">` names a body your own
|
|
206
|
+
way. See [Request bodies](docs/guide/requests.md).
|
|
207
|
+
|
|
187
208
|
### Pagination
|
|
188
209
|
|
|
189
210
|
Spread the query of a page into the operation's parameters, and answer the
|
|
@@ -291,15 +312,34 @@ model Post {
|
|
|
291
312
|
|
|
292
313
|
| Name | Emitted as | Generated | drizzle |
|
|
293
314
|
| --- | --- | --- | --- |
|
|
294
|
-
| `uuid` | the schema `
|
|
295
|
-
| `email` | the schema `
|
|
296
|
-
| `...Timestamps` | `createdAt`, `updatedAt`, read-only | left out of `
|
|
315
|
+
| `uuid` | the schema `UUID`, `format: uuid`, a pattern for versions 1–8 | `UUID`, `z.guid().regex(…)` | `id()` |
|
|
316
|
+
| `email` | `emailAddress`, the schema `EmailAddress`, `format: email` | `EmailAddress`, `z.string().regex(…)` | |
|
|
317
|
+
| `...Timestamps` | `createdAt`, `updatedAt`, read-only | left out of `CreateRequest<T>` and updates | `timestamps()` |
|
|
297
318
|
| `...SoftDelete` | `deletedAt: utcDateTime \| null`, read-only | | `softDelete()` |
|
|
298
|
-
| `...Versioned` | `version: integer`, read and update | kept in `
|
|
319
|
+
| `...Versioned` | `version: integer`, read and update | kept in `UpdateRequest<T>` and `PatchRequest<T>` | `version()` |
|
|
299
320
|
| `...Actors<Id = uuid>` | `createdBy`, `updatedBy`, `deletedBy`, `Id \| null`, read-only | | `actors()` |
|
|
300
321
|
|
|
301
|
-
|
|
302
|
-
|
|
322
|
+
Every scalar of [`@nxgt/graphql-scalars`](https://www.npmjs.com/package/@nxgt/graphql-scalars)
|
|
323
|
+
is declared too, as a camelCase scalar (`latitude`, `currency`, `positiveInt`,
|
|
324
|
+
`httpUrl`, `ipV4`, `isoDuration`):
|
|
325
|
+
|
|
326
|
+
```tsp
|
|
327
|
+
model Place {
|
|
328
|
+
location: { latitude: latitude; longitude: longitude };
|
|
329
|
+
currency: currency;
|
|
330
|
+
openedOn: date;
|
|
331
|
+
homepage?: httpUrl;
|
|
332
|
+
seats: positiveInt;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Each is an OpenAPI component named as in GraphQL (`Latitude`, `URL`), with a
|
|
337
|
+
`format`, a pattern or bounds approximating the rule, and `x-nxgt-scalar`
|
|
338
|
+
naming it. The exact rules (checksums, ISO and IANA lists) are outside the
|
|
339
|
+
pattern. TypeSpec's own `url` takes any scheme; `httpUrl` takes `http:` and
|
|
340
|
+
`https:`. Every scalar by category, in
|
|
341
|
+
[Scalars](docs/guide/scalars.md); the ids, the addresses and the columns, in
|
|
342
|
+
[Scalars and columns](docs/guide/columns.md).
|
|
303
343
|
|
|
304
344
|
### Headers
|
|
305
345
|
|
|
@@ -307,7 +347,7 @@ Spread the headers of an idempotent write and of a rate limit into an
|
|
|
307
347
|
operation and its replies:
|
|
308
348
|
|
|
309
349
|
```tsp
|
|
310
|
-
@post createPost(...IdempotencyKeyHeader, @body post:
|
|
350
|
+
@post createPost(...IdempotencyKeyHeader, @body post: CreateRequest<Post>): {
|
|
311
351
|
@statusCode _: 201;
|
|
312
352
|
...IdempotentReplayedHeader;
|
|
313
353
|
...RateLimitHeaders;
|
|
@@ -488,6 +528,9 @@ In the ruleset `@nxgt/typespec/recommended`; each is a warning, and named
|
|
|
488
528
|
| `Page<Item>` | `{ items: Item[], total, page, pageSize, pageCount }`, the schema `<Item>Page` |
|
|
489
529
|
| `CursorPageParameters` | the query `after` and `limit` |
|
|
490
530
|
| `CursorPage<Item>` | `{ items: Item[], nextCursor: string \| null }`, the schema `<Item>CursorPage` |
|
|
531
|
+
| `CreateRequest<T>` | `Create<T, "Create{name}Request">`: the body of a create, `CreatePostRequest` |
|
|
532
|
+
| `UpdateRequest<T>` | `Update<T, "Update{name}Request">`: the body of a `PUT`, `UpdatePostRequest` |
|
|
533
|
+
| `PatchRequest<T>` | `MergePatchUpdate<T, "Patch{name}Request">`: a merge patch, all optional, `PatchPostRequest` |
|
|
491
534
|
| `ValidationIssue` | `{ target: ValidationTarget, path: (string \| integer)[], code: string, message: string }` |
|
|
492
535
|
| `ValidationTarget` | `"param" \| "query" \| "header" \| "json" \| "form" \| "body" \| "response"` |
|
|
493
536
|
|
|
@@ -503,8 +546,18 @@ In the ruleset `@nxgt/typespec/recommended`; each is a warning, and named
|
|
|
503
546
|
|
|
504
547
|
| Name | What it is |
|
|
505
548
|
| --- | --- |
|
|
506
|
-
| `uuid` |
|
|
507
|
-
| `email` |
|
|
549
|
+
| `uuid` | an RFC 9562 UUID, `format: uuid` and a pattern; the schema `UUID` |
|
|
550
|
+
| `email` | an alias of `emailAddress`, `format: email` and a pattern; the schema `EmailAddress` |
|
|
551
|
+
| `guid`, `uuidV4`, `uuidV7` | a UUID of any shape, of version 4, of version 7; `GUID`, `UUIDv4`, `UUIDv7` |
|
|
552
|
+
| `ulid`, `cuid2`, `nanoId`, `ksuid`, `xid`, `objectId`, `isbn`, `semVer` | identifiers by shape: `ULID`, `Cuid2`, `NanoID`, `KSUID`, `XID`, `ObjectID`, `ISBN`, `SemVer` |
|
|
553
|
+
| `base64`, `base64Url`, `hexadecimal`, `jwt`, `sha256`, `sha512` | encodings and digests; `base64` has the format `byte` and is generated as `z.base64()` |
|
|
554
|
+
| `nonEmptyString`, `emoji` | a string with a visible character; one emoji |
|
|
555
|
+
| `positiveInt`, `nonNegativeInt`, `negativeInt`, `nonPositiveInt`, `safeInt`, `port`, `positiveFloat`, `nonNegativeFloat`, `negativeFloat`, `nonPositiveFloat`, `bigInt`, `long` | the numbers: `int32` bounds, JavaScript's safe integers, TCP ports, floats by sign, and 64-bit (`long`, `format: int64`) or larger (`bigInt`) integers as strings |
|
|
556
|
+
| `date`, `dateTime`, `time`, `localDateTime`, `localTime`, `isoDuration`, `utcOffset`, `timeZone`, `timestamp` | dates, times and durations as RFC 3339 and ISO 8601 write them |
|
|
557
|
+
| `latitude`, `longitude` | decimal degrees, as numbers |
|
|
558
|
+
| `hexColorCode`, `rgb`, `rgba`, `hsl`, `hsla` | CSS colors |
|
|
559
|
+
| `emailAddress`, `hostname`, `httpUrl`, `ipV4`, `ipV6`, `ip`, `cidrV4`, `cidrV6`, `mac`, `phoneNumber` | addresses; `httpUrl` is named so because `url` is TypeSpec's |
|
|
560
|
+
| `countryCode`, `locale`, `currency`, `iban` | ISO and BCP 47 codes, by shape |
|
|
508
561
|
| `Timestamps` | `createdAt`, `updatedAt: utcDateTime`, read-only |
|
|
509
562
|
| `SoftDelete` | `deletedAt: utcDateTime \| null`, read-only |
|
|
510
563
|
| `Versioned` | `version: integer`, read and update |
|
|
@@ -600,8 +653,8 @@ envelope and the rate limit's headers.
|
|
|
600
653
|
`3.1.0/openapi.yaml` and `3.2.0/openapi.yaml`: point the generator's input
|
|
601
654
|
at one of them.
|
|
602
655
|
- **The library's schema names are global.** `BadRequestBody` …
|
|
603
|
-
`InternalServerErrorBody`, `ValidationIssue`, `ValidationTarget`,
|
|
604
|
-
`
|
|
656
|
+
`InternalServerErrorBody`, `ValidationIssue`, `ValidationTarget`, the scalars' components
|
|
657
|
+
(`UUID`, `EmailAddress`, `Latitude`, …), and each `<Item>Page` and `<Item>CursorPage` are emitted under
|
|
605
658
|
those names, without a namespace, once the spec uses them. A model of the
|
|
606
659
|
same name in your spec then fails with `duplicate-type-name`
|
|
607
660
|
([troubleshooting](docs/troubleshooting.md)).
|
|
@@ -616,12 +669,16 @@ envelope and the rate limit's headers.
|
|
|
616
669
|
what a handler answers;
|
|
617
670
|
- [Authentication](docs/guide/auth.md): `JanusAuth`, and the guards'
|
|
618
671
|
replies without a body;
|
|
672
|
+
- [Scalars](docs/guide/scalars.md): every scalar of `@nxgt/graphql-scalars`
|
|
673
|
+
by category, with what each takes and what its pattern leaves out;
|
|
619
674
|
- [Scalars and columns](docs/guide/columns.md): `uuid`, `email`, and the
|
|
620
675
|
columns `@nxgt/drizzle` stamps;
|
|
621
676
|
- [Headers](docs/guide/headers.md): `Idempotency-Key`, the rate limit and
|
|
622
677
|
`Retry-After`;
|
|
623
678
|
- [Sorting](docs/guide/sorting.md): `orderBy` and `direction` beside a
|
|
624
679
|
list's filters, and the `@nxgt/drizzle` call they map to;
|
|
680
|
+
- [Request bodies](docs/guide/requests.md): `CreateRequest`, `UpdateRequest`
|
|
681
|
+
and `PatchRequest`, and naming your own;
|
|
625
682
|
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, each
|
|
626
683
|
operation named as written or a verb with its resource, the method each
|
|
627
684
|
verb is sent with, the options, and the ids it refuses;
|
package/docs/README.md
CHANGED
|
@@ -5,9 +5,11 @@ The [package README](../README.md) is the short version.
|
|
|
5
5
|
| Page | Read it when |
|
|
6
6
|
| --- | --- |
|
|
7
7
|
| [Getting started](guide/getting-started.md) | starting a new API project from the `tsp init` template, up to a Hono route |
|
|
8
|
+
| [Request bodies](guide/requests.md) | naming the body of a create, a `PUT` and a `PATCH`: `CreatePostRequest`, `UpdatePostRequest`, `PatchPostRequest` |
|
|
8
9
|
| [Error replies](guide/errors.md) | declaring what an operation answers when it fails |
|
|
9
10
|
| [Pagination](guide/pagination.md) | listing a collection a page at a time |
|
|
10
11
|
| [Authentication](guide/auth.md) | protecting an operation with a `@nxgt/janus` session |
|
|
12
|
+
| [Scalars](guide/scalars.md) | typing a latitude, a currency, an IP address or a port, and knowing what each pattern leaves out |
|
|
11
13
|
| [Scalars and columns](guide/columns.md) | typing ids and addresses, and the columns a row carries |
|
|
12
14
|
| [Headers](guide/headers.md) | making a write idempotent, or declaring a rate limit |
|
|
13
15
|
| [Sorting](guide/sorting.md) | letting a client filter and sort a list |
|
package/docs/guide/auth.md
CHANGED
|
@@ -91,7 +91,7 @@ operation can declare the guard's 401 and the handler's 403:
|
|
|
91
91
|
|
|
92
92
|
```tsp
|
|
93
93
|
@useAuth(JanusAuth)
|
|
94
|
-
@post createPost(@body post:
|
|
94
|
+
@post createPost(@body post: CreateRequest<Post>): {
|
|
95
95
|
@statusCode _: 201;
|
|
96
96
|
@body post: Post;
|
|
97
97
|
} | BadRequest | AuthenticationRequired | Forbidden;
|
package/docs/guide/columns.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Scalars and columns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scalars for the ids and addresses every API has, and the columns
|
|
4
4
|
[`@nxgt/drizzle`](https://www.npmjs.com/package/@nxgt/drizzle) stamps a row
|
|
5
|
-
with, as models to spread.
|
|
5
|
+
with, as models to spread. The other scalars, by category, are in
|
|
6
|
+
[Scalars](scalars.md).
|
|
6
7
|
|
|
7
8
|
## Scalars
|
|
8
9
|
|
|
@@ -18,7 +19,7 @@ namespace Blog;
|
|
|
18
19
|
model Author {
|
|
19
20
|
@visibility(Lifecycle.Read) id: uuid;
|
|
20
21
|
email: email;
|
|
21
|
-
site?:
|
|
22
|
+
site?: httpUrl;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
@route("/authors/{authorId}")
|
|
@@ -27,12 +28,25 @@ model Author {
|
|
|
27
28
|
|
|
28
29
|
| Scalar | Emitted as | Generated | Refused |
|
|
29
30
|
| --- | --- | --- | --- |
|
|
30
|
-
| `uuid` | `$ref` to the schema `
|
|
31
|
-
| `email` | `$ref` to the schema `
|
|
32
|
-
| `
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
the
|
|
31
|
+
| `uuid` | `$ref` to the schema `UUID`: `type: string`, `format: uuid`, a pattern, `x-nxgt-scalar: UUID` | `type UUID = string`, `zUUID = z.guid().regex(…)` | `42`, a UUID with no hyphens or of version 9, in a path, a query or a body |
|
|
32
|
+
| `email` | an alias of `emailAddress`: `$ref` to the schema `EmailAddress`, `format: email`, a pattern, `x-nxgt-scalar: EmailAddress` | `type EmailAddress = string`, `zEmailAddress = z.string().regex(…)` | `not an address`, `ada@localhost` |
|
|
33
|
+
| `httpUrl` | `$ref` to the schema `URL`: `format: uri`, a pattern for `http:` and `https:`, `x-nxgt-scalar: URL` | `type URL = string`, `zURL = z.string().regex(…)` | `not a url`, `javascript:alert(1)` |
|
|
34
|
+
|
|
35
|
+
TypeSpec's own `url` takes any scheme, inline, as `z.url()`; `httpUrl` is
|
|
36
|
+
the `http:` and `https:` page address. `httpUrl` and `email` are generated as
|
|
37
|
+
`z.string().regex(…)`: the pattern is the rule, as the generator leaves out
|
|
38
|
+
`z.url()`, which trims its input, and `z.email()`, which refuses
|
|
39
|
+
`ada@x.xn--p1ai`, an internationalised top-level domain `email` accepts.
|
|
40
|
+
|
|
41
|
+
`uuid` takes an RFC 9562 UUID, any version 1 to 8 with the RFC variant, or
|
|
42
|
+
the nil or the max UUID, in any case, as `@nxgt/graphql-scalars`' `UUID`
|
|
43
|
+
does: the pattern adds those checks to the shape `z.guid()` checks. `uuidV4`,
|
|
44
|
+
`uuidV7` and `guid` (any 8-4-4-4-12 hexadecimal) are the narrower and the
|
|
45
|
+
looser ones. `email` is `emailAddress`, under the name it had first.
|
|
46
|
+
|
|
47
|
+
The schema names changed in 0.10.0: `Uuid` and `Email` became `UUID` and
|
|
48
|
+
`EmailAddress`, and a UUID of no known version is refused. A spec that needs
|
|
49
|
+
the old shape-only id writes `guid`.
|
|
36
50
|
|
|
37
51
|
## Columns
|
|
38
52
|
|
|
@@ -66,9 +80,9 @@ model Post {
|
|
|
66
80
|
| `Versioned` | `version: integer`, at least 0 | read, update | `version()` |
|
|
67
81
|
| `Actors<Id = uuid>` | `createdBy`, `updatedBy`, `deletedBy: Id \| null` | read | `actors()` |
|
|
68
82
|
|
|
69
|
-
The server sets every read-only column, so `
|
|
70
|
-
`
|
|
71
|
-
`
|
|
83
|
+
The server sets every read-only column, so `CreateRequest<Post>` and
|
|
84
|
+
`PatchRequest<Post>` leave them out, and the generated `CreatePostRequest` and
|
|
85
|
+
`PatchPostRequest` have no `createdAt` a client could send.
|
|
72
86
|
`Actors<string>` or `Actors<integer>` match `actors('text')` or
|
|
73
87
|
`actors('integer')`.
|
|
74
88
|
|
|
@@ -80,7 +94,7 @@ that version, and throws `OptimisticLockError` otherwise. Declare the
|
|
|
80
94
|
`Conflict` it becomes:
|
|
81
95
|
|
|
82
96
|
```tsp
|
|
83
|
-
@patch
|
|
97
|
+
@patch patchPost(@path postId: uuid, @body post: PatchRequest<Post>):
|
|
84
98
|
Post | BadRequest | NotFound | Conflict;
|
|
85
99
|
```
|
|
86
100
|
|
|
@@ -112,6 +126,6 @@ without it is not checked against the version.
|
|
|
112
126
|
|
|
113
127
|
## Names
|
|
114
128
|
|
|
115
|
-
`
|
|
116
|
-
model named `
|
|
129
|
+
`UUID` and `EmailAddress` are schemas of their own, and their names are global: a
|
|
130
|
+
model named `EmailAddress` in a spec that also uses `email` collides with it
|
|
117
131
|
([troubleshooting](../troubleshooting.md)).
|
package/docs/guide/errors.md
CHANGED
|
@@ -143,7 +143,7 @@ A handler may send a 400 of its own, without `issues`. `BadRequestBody`
|
|
|
143
143
|
describes both, with `issues` optional:
|
|
144
144
|
|
|
145
145
|
```tsp
|
|
146
|
-
@patch
|
|
146
|
+
@patch patchPost(@path postId: string, @body post: PatchRequest<Post>): Post | BadRequest;
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
The generator declares the validators' 400 itself, as `ValidationErrorBody`,
|
|
@@ -99,13 +99,16 @@ interface Users {
|
|
|
99
99
|
get(@path id: uuid): User | GetErrors;
|
|
100
100
|
|
|
101
101
|
@post
|
|
102
|
-
create(@body user:
|
|
102
|
+
create(@body user: CreateRequest<User>): {
|
|
103
103
|
@statusCode _: 201;
|
|
104
104
|
@body user: User;
|
|
105
105
|
} | CreateErrors;
|
|
106
106
|
|
|
107
|
+
@put
|
|
108
|
+
update(@path id: uuid, @body user: UpdateRequest<User>): User | UpdateErrors;
|
|
109
|
+
|
|
107
110
|
@patch
|
|
108
|
-
|
|
111
|
+
patch(@path id: uuid, @body user: PatchRequest<User>): User | UpdateErrors;
|
|
109
112
|
|
|
110
113
|
@delete
|
|
111
114
|
delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
@@ -113,7 +116,10 @@ interface Users {
|
|
|
113
116
|
```
|
|
114
117
|
|
|
115
118
|
`@operationIds` names the operations `listUsers`, `getUser`, `createUser`,
|
|
116
|
-
`updateUser` and `deleteUser` ([Operation ids](operation-ids.md)).
|
|
119
|
+
`updateUser`, `patchUser` and `deleteUser` ([Operation ids](operation-ids.md)).
|
|
120
|
+
`update` replaces the whole user with a `PUT` (`UpdateRequest`), `patch`
|
|
121
|
+
changes some of it with a merge patch (`PatchRequest`); see
|
|
122
|
+
[Request bodies](requests.md).
|
|
117
123
|
|
|
118
124
|
```ts
|
|
119
125
|
// openapi-codegen.config.ts
|
|
@@ -250,13 +256,16 @@ interface Pets {
|
|
|
250
256
|
get(@path id: uuid): Pet | GetErrors;
|
|
251
257
|
|
|
252
258
|
@post
|
|
253
|
-
create(@body pet:
|
|
259
|
+
create(@body pet: CreateRequest<Pet>): {
|
|
254
260
|
@statusCode _: 201;
|
|
255
261
|
@body pet: Pet;
|
|
256
262
|
} | CreateErrors;
|
|
257
263
|
|
|
264
|
+
@put
|
|
265
|
+
update(@path id: uuid, @body pet: UpdateRequest<Pet>): Pet | UpdateErrors;
|
|
266
|
+
|
|
258
267
|
@patch
|
|
259
|
-
|
|
268
|
+
patch(@path id: uuid, @body pet: PatchRequest<Pet>): Pet | UpdateErrors;
|
|
260
269
|
|
|
261
270
|
@delete
|
|
262
271
|
delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
@@ -264,8 +273,8 @@ interface Pets {
|
|
|
264
273
|
```
|
|
265
274
|
|
|
266
275
|
The operation names stay; `@operationIds` takes the resource from the
|
|
267
|
-
interface, so the ids become `listPets`, `getPet`, `createPet`, `updatePet
|
|
268
|
-
and `deletePet`. Run `tsp compile .` and `nxgt-openapi generate` again, and
|
|
276
|
+
interface, so the ids become `listPets`, `getPet`, `createPet`, `updatePet`,
|
|
277
|
+
`patchPet` and `deletePet`. Run `tsp compile .` and `nxgt-openapi generate` again, and
|
|
269
278
|
the generated types, `Pet` and `PetPage`, follow. A name the singular rule
|
|
270
279
|
does not know takes `@operationIds(#{ singular: "…" })` on the interface
|
|
271
280
|
([Operation ids](operation-ids.md)).
|
package/docs/guide/headers.md
CHANGED
|
@@ -19,7 +19,7 @@ namespace Blog;
|
|
|
19
19
|
|
|
20
20
|
@route("/posts")
|
|
21
21
|
interface Posts {
|
|
22
|
-
@post createPost(...IdempotencyKeyHeader, @body post:
|
|
22
|
+
@post createPost(...IdempotencyKeyHeader, @body post: CreateRequest<Post>): {
|
|
23
23
|
@statusCode _: 201;
|
|
24
24
|
...IdempotentReplayedHeader;
|
|
25
25
|
@body post: Post;
|
|
@@ -48,7 +48,7 @@ lock, gets one 409 with the description of the first, and the library warns,
|
|
|
48
48
|
## A rate limit
|
|
49
49
|
|
|
50
50
|
```tsp
|
|
51
|
-
@post createPost(@body post:
|
|
51
|
+
@post createPost(@body post: CreateRequest<Post>): {
|
|
52
52
|
@statusCode _: 201;
|
|
53
53
|
...RateLimitHeaders;
|
|
54
54
|
@body post: Post;
|
|
@@ -147,7 +147,7 @@ Give the operation its verb's method, or a name that is not a verb, which
|
|
|
147
147
|
is then its id as written:
|
|
148
148
|
|
|
149
149
|
```tsp
|
|
150
|
-
@
|
|
150
|
+
@put @route("/{id}") update(@path id: uuid, @body user: User): User; // updateUser
|
|
151
151
|
@post @route("/lookup") lookUpUser(@body id: uuid): User; // lookUpUser
|
|
152
152
|
```
|
|
153
153
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Request bodies
|
|
2
|
+
|
|
3
|
+
`CreateRequest<T>`, `UpdateRequest<T>` and `PatchRequest<T>` are TypeSpec's own
|
|
4
|
+
`Create`, `Update` and `MergePatchUpdate` with a name template, so the body of
|
|
5
|
+
each write is a component with a name you can read: `CreatePostRequest`.
|
|
6
|
+
|
|
7
|
+
## Why names
|
|
8
|
+
|
|
9
|
+
TypeSpec's templates name what they emit from the model: `Create<Post>` is
|
|
10
|
+
the component `CreatePost` and `MergePatchUpdate<Post>` is
|
|
11
|
+
`PostMergePatchUpdate`. Two conventions in one spec, and a type in the
|
|
12
|
+
generated client that reads unlike the rest. The aliases give each body the
|
|
13
|
+
name `<Verb><Model>Request`, and `@nxgt/openapi-codegen` generates the types
|
|
14
|
+
and the Zod schemas under it.
|
|
15
|
+
|
|
16
|
+
## The smallest example
|
|
17
|
+
|
|
18
|
+
```tsp
|
|
19
|
+
import "@typespec/http";
|
|
20
|
+
import "@nxgt/typespec";
|
|
21
|
+
|
|
22
|
+
using Http;
|
|
23
|
+
using Nxgt;
|
|
24
|
+
|
|
25
|
+
namespace Blog;
|
|
26
|
+
|
|
27
|
+
model Post {
|
|
28
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
29
|
+
@minLength(1) title: string;
|
|
30
|
+
body: string;
|
|
31
|
+
tags?: string[];
|
|
32
|
+
...Timestamps;
|
|
33
|
+
...Versioned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@route("/posts")
|
|
37
|
+
interface Posts {
|
|
38
|
+
@post createPost(@body post: CreateRequest<Post>): {
|
|
39
|
+
@statusCode _: 201;
|
|
40
|
+
@body post: Post;
|
|
41
|
+
} | BadRequest;
|
|
42
|
+
|
|
43
|
+
@put updatePost(@path postId: uuid, @body post: UpdateRequest<Post>):
|
|
44
|
+
Post | BadRequest | NotFound | Conflict;
|
|
45
|
+
|
|
46
|
+
@patch patchPost(@path postId: uuid, @body post: PatchRequest<Post>):
|
|
47
|
+
Post | BadRequest | NotFound | Conflict;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Alias | Is | Component | Content type | Properties |
|
|
52
|
+
| --- | --- | --- | --- | --- |
|
|
53
|
+
| `CreateRequest<T>` | `Create<T, "Create{name}Request">` | `CreatePostRequest` | `application/json` | those visible on create, as `T` declares them |
|
|
54
|
+
| `UpdateRequest<T>` | `Update<T, "Update{name}Request">` | `UpdatePostRequest` | `application/json` | those visible on update, as `T` declares them |
|
|
55
|
+
| `PatchRequest<T>` | `MergePatchUpdate<T, "Patch{name}Request">` | `PatchPostRequest` | `application/merge-patch+json` | those visible on update, all optional |
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type {
|
|
59
|
+
CreatePostRequest,
|
|
60
|
+
PatchPostRequest,
|
|
61
|
+
UpdatePostRequest,
|
|
62
|
+
} from './generated/types';
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```tsp
|
|
66
|
+
alias CreateRequest<T extends Reflection.Model> = Create<T, "Create{name}Request">;
|
|
67
|
+
alias UpdateRequest<T extends Reflection.Model> = Update<T, "Update{name}Request">;
|
|
68
|
+
alias PatchRequest<T extends Reflection.Model> = MergePatchUpdate<T, "Patch{name}Request">;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Read-only properties (`id`, `Timestamps`, `Actors`) are in none of the three.
|
|
72
|
+
`version` from `Versioned` is read and update, so it is in
|
|
73
|
+
`UpdatePostRequest` and `PatchPostRequest`, and not in `CreatePostRequest`.
|
|
74
|
+
|
|
75
|
+
## `PUT` replaces, `PATCH` merges
|
|
76
|
+
|
|
77
|
+
`UpdateRequest<Post>` is the whole resource: each property is as optional as
|
|
78
|
+
`Post` declares it, so `title` is required and `tags?` is not. Send it with
|
|
79
|
+
`@put`.
|
|
80
|
+
|
|
81
|
+
`PatchRequest<Post>` is a JSON Merge Patch: every property is optional, and a
|
|
82
|
+
nullable one is sent as `null` to clear it. The body's content type is
|
|
83
|
+
`application/merge-patch+json`, where the others are `application/json`.
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
# openapi.yaml (excerpt)
|
|
87
|
+
PUT:
|
|
88
|
+
requestBody:
|
|
89
|
+
content:
|
|
90
|
+
application/json:
|
|
91
|
+
schema:
|
|
92
|
+
$ref: '#/components/schemas/UpdatePostRequest'
|
|
93
|
+
PATCH:
|
|
94
|
+
requestBody:
|
|
95
|
+
content:
|
|
96
|
+
application/merge-patch+json:
|
|
97
|
+
schema:
|
|
98
|
+
$ref: '#/components/schemas/PatchPostRequest'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A client sending a patch sets the header `Content-Type:
|
|
102
|
+
application/merge-patch+json`. The component `UpdatePostRequest` requires
|
|
103
|
+
`title`; `PatchPostRequest` requires nothing.
|
|
104
|
+
|
|
105
|
+
## Nested models
|
|
106
|
+
|
|
107
|
+
A nested model the template filters is named by the same template; one it
|
|
108
|
+
leaves whole keeps its name.
|
|
109
|
+
|
|
110
|
+
```tsp
|
|
111
|
+
model Address {
|
|
112
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
113
|
+
street: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
model Author {
|
|
117
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
118
|
+
name: string;
|
|
119
|
+
address: Address;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@post createAuthor(@body author: CreateRequest<Author>): Author;
|
|
123
|
+
// CreateAuthorRequest.address is CreateAddressRequest, without `id`
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
A model with no property the filter changes, such as a `Tag` with only
|
|
127
|
+
`name`, is referenced under its own name, `Tag`.
|
|
128
|
+
|
|
129
|
+
## Your own name template
|
|
130
|
+
|
|
131
|
+
The aliases fix the convention. Another one is the same template, inline, and
|
|
132
|
+
`Create<T>` and `MergePatchUpdate<T>` stay usable:
|
|
133
|
+
|
|
134
|
+
```tsp
|
|
135
|
+
@post createPost(@body post: Create<Post, "New{name}">): Post; // NewPost
|
|
136
|
+
@patch patchPost(@path postId: uuid, @body post: MergePatchUpdate<Post, "{name}Changes">): Post;
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`{name}` is the model's name. Without a template, TypeSpec names the
|
|
140
|
+
components `CreatePost` and `PostMergePatchUpdate`.
|
|
141
|
+
|
|
142
|
+
## Next
|
|
143
|
+
|
|
144
|
+
The columns a request leaves out are on [Scalars and columns](columns.md); the
|
|
145
|
+
errors a write answers on [Error replies](errors.md).
|