@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.
Files changed (90) hide show
  1. package/README.md +71 -14
  2. package/docs/README.md +2 -0
  3. package/docs/guide/auth.md +1 -1
  4. package/docs/guide/columns.md +29 -15
  5. package/docs/guide/errors.md +1 -1
  6. package/docs/guide/getting-started.md +16 -7
  7. package/docs/guide/headers.md +2 -2
  8. package/docs/guide/operation-ids.md +1 -1
  9. package/docs/guide/requests.md +145 -0
  10. package/docs/guide/scalars.md +288 -0
  11. package/docs/roadmap.md +22 -0
  12. package/docs/troubleshooting.md +95 -3
  13. package/lib/main.tsp +1 -0
  14. package/lib/requests.tsp +36 -0
  15. package/lib/scalars/color/hex-color-code.tsp +17 -0
  16. package/lib/scalars/color/hsl.tsp +18 -0
  17. package/lib/scalars/color/hsla.tsp +18 -0
  18. package/lib/scalars/color/rgb.tsp +18 -0
  19. package/lib/scalars/color/rgba.tsp +18 -0
  20. package/lib/scalars/color.tsp +6 -0
  21. package/lib/scalars/date-time/date-time.tsp +19 -0
  22. package/lib/scalars/date-time/date.tsp +17 -0
  23. package/lib/scalars/date-time/iso-duration.tsp +17 -0
  24. package/lib/scalars/date-time/local-date-time.tsp +15 -0
  25. package/lib/scalars/date-time/local-time.tsp +15 -0
  26. package/lib/scalars/date-time/time-zone.tsp +18 -0
  27. package/lib/scalars/date-time/time.tsp +17 -0
  28. package/lib/scalars/date-time/timestamp.tsp +15 -0
  29. package/lib/scalars/date-time/utc-offset.tsp +15 -0
  30. package/lib/scalars/date-time.tsp +10 -0
  31. package/lib/scalars/encoding/base64-url.tsp +17 -0
  32. package/lib/scalars/encoding/base64.tsp +18 -0
  33. package/lib/scalars/encoding/hexadecimal.tsp +15 -0
  34. package/lib/scalars/encoding/jwt.tsp +19 -0
  35. package/lib/scalars/encoding/sha256.tsp +16 -0
  36. package/lib/scalars/encoding/sha512.tsp +16 -0
  37. package/lib/scalars/encoding.tsp +7 -0
  38. package/lib/scalars/finance/currency.tsp +17 -0
  39. package/lib/scalars/finance/iban.tsp +19 -0
  40. package/lib/scalars/finance.tsp +3 -0
  41. package/lib/scalars/geo/latitude.tsp +14 -0
  42. package/lib/scalars/geo/longitude.tsp +14 -0
  43. package/lib/scalars/geo.tsp +3 -0
  44. package/lib/scalars/identifier/cuid2.tsp +17 -0
  45. package/lib/scalars/identifier/guid.tsp +12 -0
  46. package/lib/scalars/identifier/isbn.tsp +18 -0
  47. package/lib/scalars/identifier/ksuid.tsp +18 -0
  48. package/lib/scalars/identifier/nano-id.tsp +16 -0
  49. package/lib/scalars/identifier/object-id.tsp +16 -0
  50. package/lib/scalars/identifier/sem-ver.tsp +17 -0
  51. package/lib/scalars/identifier/ulid.tsp +17 -0
  52. package/lib/scalars/identifier/uuid-v4.tsp +16 -0
  53. package/lib/scalars/identifier/uuid-v7.tsp +16 -0
  54. package/lib/scalars/identifier/uuid.tsp +18 -0
  55. package/lib/scalars/identifier/xid.tsp +17 -0
  56. package/lib/scalars/identifier.tsp +13 -0
  57. package/lib/scalars/locale/country-code.tsp +17 -0
  58. package/lib/scalars/locale/locale.tsp +20 -0
  59. package/lib/scalars/locale.tsp +3 -0
  60. package/lib/scalars/network/cidr-v4.tsp +19 -0
  61. package/lib/scalars/network/cidr-v6.tsp +18 -0
  62. package/lib/scalars/network/email-address.tsp +20 -0
  63. package/lib/scalars/network/hostname.tsp +21 -0
  64. package/lib/scalars/network/http-url.tsp +21 -0
  65. package/lib/scalars/network/ip-v4.tsp +17 -0
  66. package/lib/scalars/network/ip-v6.tsp +18 -0
  67. package/lib/scalars/network/ip.tsp +17 -0
  68. package/lib/scalars/network/mac.tsp +16 -0
  69. package/lib/scalars/network/phone-number.tsp +18 -0
  70. package/lib/scalars/network.tsp +11 -0
  71. package/lib/scalars/number/big-int.tsp +15 -0
  72. package/lib/scalars/number/long.tsp +17 -0
  73. package/lib/scalars/number/negative-float.tsp +13 -0
  74. package/lib/scalars/number/negative-int.tsp +15 -0
  75. package/lib/scalars/number/non-negative-float.tsp +13 -0
  76. package/lib/scalars/number/non-negative-int.tsp +15 -0
  77. package/lib/scalars/number/non-positive-float.tsp +13 -0
  78. package/lib/scalars/number/non-positive-int.tsp +15 -0
  79. package/lib/scalars/number/port.tsp +14 -0
  80. package/lib/scalars/number/positive-float.tsp +13 -0
  81. package/lib/scalars/number/positive-int.tsp +15 -0
  82. package/lib/scalars/number/safe-int.tsp +16 -0
  83. package/lib/scalars/number.tsp +13 -0
  84. package/lib/scalars/string/emoji.tsp +21 -0
  85. package/lib/scalars/string/non-empty-string.tsp +15 -0
  86. package/lib/scalars/string.tsp +3 -0
  87. package/lib/scalars/value.tsp +2 -0
  88. package/lib/scalars.tsp +13 -12
  89. package/package.json +3 -3
  90. 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: Create<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
- @patch update(@path id: uuid, @body user: User): User | UpdateErrors;
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 `Uuid`, `format: uuid` | `Uuid`, `z.guid()` | `id()` |
295
- | `email` | the schema `Email`, `format: email` | `Email`, `z.email()` | |
296
- | `...Timestamps` | `createdAt`, `updatedAt`, read-only | left out of `Create<T>` and updates | `timestamps()` |
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 `MergePatchUpdate<T>` | `version()` |
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
- TypeSpec's own `url` is the address of a page, `format: uri`, generated as
302
- `z.url()`. More in [Scalars and columns](docs/guide/columns.md).
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: Create<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` | a string, `format: uuid`; the schema `Uuid` |
507
- | `email` | a string, `format: email`; the schema `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`, `Uuid`,
604
- `Email`, and each `<Item>Page` and `<Item>CursorPage` are emitted under
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 |
@@ -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: Create<Post>): {
94
+ @post createPost(@body post: CreateRequest<Post>): {
95
95
  @statusCode _: 201;
96
96
  @body post: Post;
97
97
  } | BadRequest | AuthenticationRequired | Forbidden;
@@ -1,8 +1,9 @@
1
1
  # Scalars and columns
2
2
 
3
- Two scalars for the strings every API has, and the columns
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?: url;
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 `Uuid`: `type: string`, `format: uuid` | `type Uuid = string`, `zUuid = z.guid()` | `42`, in a path, a query or a body |
31
- | `email` | `$ref` to the schema `Email`: `format: email` | `type Email = string`, `zEmail = z.email()` | `not an address` |
32
- | `url` | TypeSpec's own: `format: uri`, inline | `z.url()` | `not a url` |
33
-
34
- `z.guid()` checks the shape of a UUID, as JSON Schema's `uuid` does, not
35
- the version and variant bits `z.uuid()` also checks.
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 `Create<Post>` and
70
- `MergePatchUpdate<Post>` leave them out, and the generated `CreatePost` and
71
- `PostMergePatchUpdate` have no `createdAt` a client could send.
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 updatePost(@path postId: uuid, @body post: MergePatchUpdate<Post>):
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
- `Uuid` and `Email` are schemas of their own, and their names are global: a
116
- model named `Email` in a spec that also uses `email` collides with it
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)).
@@ -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 updatePost(@path postId: string, @body post: MergePatchUpdate<Post>): Post | BadRequest;
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: Create<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
- update(@path id: uuid, @body user: MergePatchUpdate<User>): User | UpdateErrors;
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: Create<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
- update(@path id: uuid, @body pet: MergePatchUpdate<Pet>): Pet | UpdateErrors;
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)).
@@ -19,7 +19,7 @@ namespace Blog;
19
19
 
20
20
  @route("/posts")
21
21
  interface Posts {
22
- @post createPost(...IdempotencyKeyHeader, @body post: Create<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: Create<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
- @patch @route("/{id}") update(@path id: uuid, @body user: User): User; // updateUser
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).