@nxgt/typespec 0.6.0 → 0.7.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 CHANGED
@@ -131,6 +131,24 @@ The generator also declares the validators' own 400, `ValidationErrorBody`,
131
131
  on every operation that takes a parameter or a body. With `BadRequest`, a
132
132
  400 reply is typed `BadRequestBody | ValidationErrorBody`.
133
133
 
134
+ Each verb has an alias for the errors it usually answers, so a resource's
135
+ operations do not repeat the same unions. Combine them, and add a response
136
+ an alias leaves out:
137
+
138
+ ```tsp
139
+ @route("/users")
140
+ interface Users {
141
+ @get list(...PageParameters): Page<User> | ListErrors;
142
+ @get get(@path id: uuid): User | GetErrors;
143
+ @post create(@body user: User): User | CreateErrors | AuthErrors | TooManyRequests;
144
+ @patch update(@path id: uuid, @body user: User): User | UpdateErrors;
145
+ @delete delete(@path id: uuid): NoContentResponse | DeleteErrors;
146
+ }
147
+ ```
148
+
149
+ An alias is exactly its union, so the OpenAPI it emits is the same as
150
+ writing the responses out.
151
+
134
152
  ### Pagination
135
153
 
136
154
  Spread the query of a page into the operation's parameters, and answer the
@@ -445,6 +463,15 @@ envelope and the rate limit's headers.
445
463
  `@error` model with its `@statusCode` and body, and
446
464
  `ErrorResponse<Status>` for any other status.
447
465
 
466
+ | Alias | What it is |
467
+ | --- | --- |
468
+ | `ListErrors` | `BadRequest` |
469
+ | `GetErrors` | `NotFound` |
470
+ | `CreateErrors` | `BadRequest \| Conflict` |
471
+ | `UpdateErrors` | `BadRequest \| NotFound \| Conflict` |
472
+ | `DeleteErrors` | `NotFound` |
473
+ | `AuthErrors` | `Unauthorized \| Forbidden`, with the envelope; a route that answers without a body declares `AuthenticationRequired \| AccessDenied` instead |
474
+
448
475
  ## Traps
449
476
 
450
477
  - **Do not declare a schema named `ValidationErrorBody`.** The generator
@@ -466,7 +493,8 @@ envelope and the rate limit's headers.
466
493
  - **One status, one reply.** `@typespec/openapi3` merges two replies of one
467
494
  status into one. With `AuthenticationRequired` and `Unauthorized`, the 401
468
495
  keeps the envelope and the reply without a body is lost: the library
469
- refuses it, `duplicate-status-reply`. With `Conflict` beside
496
+ refuses it, `duplicate-status-reply`. With `Conflict` (which
497
+ `CreateErrors` and `UpdateErrors` hold) beside
470
498
  `IdempotencyInProgress`, or `UnprocessableEntity` beside
471
499
  `IdempotencyKeyReused`, both bodies stay, under the first reply's
472
500
  description: the library warns, `merged-status-reply`. Declare the one the
@@ -503,8 +531,8 @@ envelope and the rate limit's headers.
503
531
 
504
532
  ## Documentation
505
533
 
506
- - [Error replies](docs/guide/errors.md): the envelope, each response, and
507
- what the generator makes of them;
534
+ - [Error replies](docs/guide/errors.md): the envelope, each response, the
535
+ aliases by verb, and what the generator makes of them;
508
536
  - [Pagination](docs/guide/pagination.md): offset and cursor pages, and
509
537
  what a handler answers;
510
538
  - [Authentication](docs/guide/auth.md): `JanusAuth`, and the guards'
@@ -173,6 +173,71 @@ Name them in an operation's return type, beside the success:
173
173
  @get getPost(@path postId: string): Post | NotFound | Unauthorized | Forbidden;
174
174
  ```
175
175
 
176
+ ## Aliases by verb
177
+
178
+ Most operations of a resource answer the same errors. Each verb has an alias
179
+ for them:
180
+
181
+ | Alias | Responses | Statuses |
182
+ | --- | --- | --- |
183
+ | `ListErrors` | `BadRequest` | 400 |
184
+ | `GetErrors` | `NotFound` | 404 |
185
+ | `CreateErrors` | `BadRequest \| Conflict` | 400, 409 |
186
+ | `UpdateErrors` | `BadRequest \| NotFound \| Conflict` | 400, 404, 409 |
187
+ | `DeleteErrors` | `NotFound` | 404 |
188
+ | `AuthErrors` | `Unauthorized \| Forbidden` | 401, 403 |
189
+
190
+ ```tsp
191
+ import "@typespec/http";
192
+ import "@nxgt/typespec";
193
+
194
+ using Http;
195
+ using Nxgt;
196
+
197
+ @service
198
+ namespace Shop;
199
+
200
+ model User {
201
+ id: uuid;
202
+ }
203
+
204
+ @route("/users")
205
+ interface Users {
206
+ @get list(...PageParameters): Page<User> | ListErrors;
207
+ @get get(@path id: uuid): User | GetErrors;
208
+ @post create(@body user: User): User | CreateErrors;
209
+ @patch update(@path id: uuid, @body user: User): User | UpdateErrors;
210
+ @delete delete(@path id: uuid): NoContentResponse | DeleteErrors;
211
+ @get @route("/me") me(): User | AuthErrors;
212
+ }
213
+ ```
214
+
215
+ An alias is a union, and nothing more: it emits the same OpenAPI as the
216
+ responses written out. Combine aliases, and add a response an alias leaves
217
+ out:
218
+
219
+ ```tsp
220
+ @post create(@body user: User): User | CreateErrors | AuthErrors | TooManyRequests;
221
+ ```
222
+
223
+ A union of your own names the errors your routes share:
224
+
225
+ ```tsp
226
+ alias WriteErrors = CreateErrors | AuthErrors | TooManyRequests;
227
+ ```
228
+
229
+ Do not add a response the alias already holds. `UpdateErrors | NotFound`
230
+ declares the 404 twice, and the library warns, `merged-status-reply`. A 409
231
+ beside `CreateErrors` or `UpdateErrors`, such as `IdempotencyInProgress`,
232
+ warns too: declare the responses one by one there
233
+ ([Headers](headers.md), [troubleshooting](../troubleshooting.md)).
234
+
235
+ `AuthErrors` carries the envelope, for a handler's own refusal. A route
236
+ whose guard answers the 401 and 403 without a body, as `@nxgt/janus-hono`'s
237
+ do, declares `AuthenticationRequired | AccessDenied` instead
238
+ ([Authentication](auth.md)). The two in one operation are an error,
239
+ `duplicate-status-reply`.
240
+
176
241
  ## Another status, named
177
242
 
178
243
  `ErrorResponse<503>` works, but its body is emitted inline and generated as
package/docs/roadmap.md CHANGED
@@ -6,11 +6,10 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
6
6
 
7
7
  ## Now
8
8
 
9
- - **Verbs name their methods.** An operation named after one of the
10
- library's verbs and sent with another method is a warning,
11
- `verb-method-mismatch`: `create` is a `POST`, `list` a `GET` or a `HEAD`,
12
- `search` a `GET` or a `POST`. `#suppress` silences one operation.
13
- - **`query`, a verb.** `query` in `Users` is `queryUsers`, like `search`.
9
+ - **Error aliases by verb.** `ListErrors`, `GetErrors`, `CreateErrors`,
10
+ `UpdateErrors`, `DeleteErrors` and `AuthErrors` name the errors each verb
11
+ usually answers, so an operation writes `User | GetErrors` instead of the
12
+ union. Each is exactly its union, and they combine.
14
13
 
15
14
  ## Later
16
15
 
@@ -24,6 +23,14 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
24
23
 
25
24
  ## Shipped
26
25
 
26
+ ### 0.6.0
27
+
28
+ - **Verbs name their methods.** An operation named after one of the
29
+ library's verbs and sent with another method is a warning,
30
+ `verb-method-mismatch`: `create` is a `POST`, `list` a `GET` or a `HEAD`,
31
+ `search` a `GET` or a `POST`. `#suppress` silences one operation.
32
+ - **`query`, a verb.** `query` in `Users` is `queryUsers`, like `search`.
33
+
27
34
  ### 0.5.0
28
35
 
29
36
  - **Verbs in operation ids.** In an interface `@operationIds` marks, a known
@@ -281,6 +281,15 @@ header either reply declares required becomes optional.
281
281
  @post createPost(@body post: Post): Post | IdempotencyInProgress;
282
282
  ```
283
283
 
284
+ An alias can hide the duplicate: `CreateErrors` holds a `Conflict`, so
285
+ `Post | CreateErrors | IdempotencyInProgress` declares two 409s, and
286
+ `UpdateErrors | NotFound` two 404s. Spell the alias's other errors out
287
+ instead:
288
+
289
+ ```tsp
290
+ @post createPost(@body post: Post): Post | BadRequest | IdempotencyInProgress;
291
+ ```
292
+
284
293
  Two plain bodies, `Post | Draft`, are one reply whose body is either; two
285
294
  `@error` models without a `@statusCode` share `default`, and one
286
295
  description; bodies of different content types are negotiated. All three
package/lib/errors.tsp CHANGED
@@ -146,3 +146,31 @@ model ErrorResponse<Status extends integer> {
146
146
  @statusCode _: Status;
147
147
  @body body: ErrorBody<Status>;
148
148
  }
149
+
150
+ /**
151
+ * The errors a list declares: a query its validators refuse.
152
+ *
153
+ * ```tsp
154
+ * @get list(...PageParameters): Page<User> | ListErrors;
155
+ * ```
156
+ */
157
+ alias ListErrors = BadRequest;
158
+
159
+ /** The errors a read of one declares: nothing at that id. */
160
+ alias GetErrors = NotFound;
161
+
162
+ /** The errors a create declares: a body refused, or one that conflicts. */
163
+ alias CreateErrors = BadRequest | Conflict;
164
+
165
+ /** The errors an update declares: a body refused, nothing at that id, or a conflict. */
166
+ alias UpdateErrors = BadRequest | NotFound | Conflict;
167
+
168
+ /** The errors a delete declares: nothing at that id. */
169
+ alias DeleteErrors = NotFound;
170
+
171
+ /**
172
+ * The errors of an operation behind authentication, with the nxgt envelope.
173
+ * A route that answers them without a body, as Janus does, declares
174
+ * `AuthenticationRequired | AccessDenied` instead.
175
+ */
176
+ alias AuthErrors = Unauthorized | Forbidden;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/typespec",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",