@nxgt/typespec 0.1.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.
@@ -0,0 +1,138 @@
1
+ # Pagination
2
+
3
+ Two ways to list a collection a page at a time, with the query and the page
4
+ [`@nxgt/drizzle`](https://www.npmjs.com/package/@nxgt/drizzle) and
5
+ [`@nxgt/mongo`](https://www.npmjs.com/package/@nxgt/mongo) use:
6
+
7
+ | | Query | Page | Use it when |
8
+ | --- | --- | --- | --- |
9
+ | Offset | `...PageParameters` | `Page<Item>` | a client jumps to a page, and shows a total |
10
+ | Cursor | `...CursorPageParameters` | `CursorPage<Item>` | a client scrolls on, and the collection is large or changes under it |
11
+
12
+ ## Offset
13
+
14
+ ```tsp
15
+ import "@typespec/http";
16
+ import "@nxgt/typespec";
17
+
18
+ using Http;
19
+ using Nxgt;
20
+
21
+ namespace Blog;
22
+
23
+ @route("/posts")
24
+ interface Posts {
25
+ @get list(@query status?: PostStatus, ...PageParameters): Page<Post>;
26
+ }
27
+ ```
28
+
29
+ `...PageParameters` adds two query parameters:
30
+
31
+ - `page`: 1-based, 1 when absent;
32
+ - `pageSize`: 20 when absent.
33
+
34
+ Both must be integers of at least 1, or `@nxgt/openapi-hono` answers its
35
+ 400. `Page<Post>` is the schema `PostPage`:
36
+
37
+ ```json
38
+ { "items": [], "total": 42, "page": 3, "pageSize": 20, "pageCount": 3 }
39
+ ```
40
+
41
+ `pageCount` is `ceil(total / pageSize)`, 0 when nothing matches. A page past
42
+ the last one has no items and the same `total`.
43
+
44
+ ## Cursor
45
+
46
+ ```tsp
47
+ @route("/posts/{postId}/comments")
48
+ interface Comments {
49
+ @get list(
50
+ @path postId: string,
51
+ ...CursorPageParameters,
52
+ ): CursorPage<Comment> | BadRequest | NotFound;
53
+ }
54
+ ```
55
+
56
+ `...CursorPageParameters` adds:
57
+
58
+ - `after`: the previous page's `nextCursor`, absent for the first page;
59
+ - `limit`: 20 when absent, an integer of at least 1.
60
+
61
+ `CursorPage<Comment>` is the schema `CommentCursorPage`, with no total:
62
+
63
+ ```json
64
+ { "items": [], "nextCursor": "eyJpZCI6NDJ9" }
65
+ ```
66
+
67
+ `nextCursor` is `null` on the last page. It is opaque: a client sends it
68
+ back as it is.
69
+
70
+ ## Serving it
71
+
72
+ The generated query arrives checked, with its defaults filled in, so it
73
+ goes to `@nxgt/drizzle` as it is. The page it returns is the generated type,
74
+ when the table's rows are what the spec's `Post` describes:
75
+
76
+ ```ts
77
+ import { Hono } from 'hono';
78
+ import { createRoutes } from './generated/hono';
79
+ import type { PostPage } from './generated/types';
80
+
81
+ const app = new Hono();
82
+ createRoutes(app).get('/posts', async (c) => {
83
+ const { page, pageSize } = c.req.valid('query');
84
+ const body: PostPage = await posts.paginate({ page, pageSize });
85
+ return c.json(body, 200);
86
+ });
87
+ ```
88
+
89
+ A cursor that was not written by the server, or for another ordering, makes
90
+ `@nxgt/drizzle` throw `InvalidCursorError`. That is the client's input:
91
+ declare `BadRequest` on the operation, as above, and answer it:
92
+
93
+ ```ts
94
+ import { InvalidCursorError } from '@nxgt/drizzle';
95
+ import type { BadRequestBody, CommentCursorPage } from './generated/types';
96
+
97
+ createRoutes(app).get('/posts/{postId}/comments', async (c) => {
98
+ const { after, limit } = c.req.valid('query');
99
+ try {
100
+ const body: CommentCursorPage = await comments.paginateByCursor({ after, limit });
101
+ return c.json(body, 200);
102
+ } catch (error) {
103
+ if (!(error instanceof InvalidCursorError)) throw error;
104
+ const body: BadRequestBody = {
105
+ status: 400,
106
+ message: 'errors.invalid-cursor',
107
+ timestamp: new Date().toISOString(),
108
+ };
109
+ return c.json(body, 400);
110
+ }
111
+ });
112
+ ```
113
+
114
+ ## The largest page
115
+
116
+ Neither `pageSize` nor `limit` has a maximum in the spec. A server lowers one
117
+ above its own maximum to it, as `@nxgt/drizzle` does with 100 by default,
118
+ rather than refusing the request. A client reads the `pageSize` the page
119
+ reports, not the one it asked for.
120
+
121
+ ## Names
122
+
123
+ `Page<Item>` is named `<Item>Page`, and `CursorPage<Item>` `<Item>CursorPage`,
124
+ after the item's model: `PostPage`, `CommentCursorPage`. Those names are
125
+ global, as the library's other schemas are: a model named `PostPage` in the
126
+ spec collides with `Page<Post>`
127
+ ([troubleshooting](../troubleshooting.md)).
128
+
129
+ Give the item a name: a page of an anonymous type is named after what
130
+ TypeSpec calls it, `Page<Post[]>` gives `ArrayPage` and
131
+ `CursorPage<Cat | Dog>` gives `undefinedCursorPage`. Name the union, and the
132
+ page follows:
133
+
134
+ ```tsp
135
+ union Pet { cat: Cat, dog: Dog }
136
+
137
+ @get list(...PageParameters): Page<Pet>; // PetPage
138
+ ```
@@ -0,0 +1,112 @@
1
+ # Resources
2
+
3
+ `Resource` gives a resource its five operations in one line: list, read,
4
+ create, update and delete, with the replies each one can send.
5
+
6
+ ## One resource
7
+
8
+ ```tsp
9
+ import "@typespec/http";
10
+ import "@nxgt/typespec";
11
+
12
+ using Http;
13
+ using Nxgt;
14
+
15
+ namespace Blog;
16
+
17
+ model Author {
18
+ @visibility(Lifecycle.Read) id: uuid;
19
+ name: string;
20
+ email: email;
21
+ ...Timestamps;
22
+ }
23
+
24
+ model AuthorFilters {
25
+ /** Authors of this name. */
26
+ @query name?: string;
27
+ }
28
+
29
+ @route("/authors")
30
+ interface Authors extends Resource<Author, AuthorFilters, SortField = "name" | "createdAt"> {}
31
+ ```
32
+
33
+ | Operation | Route | Body | Answers |
34
+ | --- | --- | --- | --- |
35
+ | `listAuthors` | `GET /authors` | | `Page<Author>` |
36
+ | `readAuthors` | `GET /authors/{id}` | | `Author`, `NotFound` |
37
+ | `createAuthors` | `POST /authors` | `Create<Author>` | 201 `Author`, `BadRequest`, `Conflict` |
38
+ | `updateAuthors` | `PATCH /authors/{id}` | `MergePatchUpdate<Author>` | `Author`, `BadRequest`, `NotFound`, `Conflict` |
39
+ | `deleteAuthors` | `DELETE /authors/{id}` | | 204, `NotFound` |
40
+
41
+ The template carries `@operationIds`, so each operation is named after the
42
+ interface that extends it. The generator's own 400 is on every operation
43
+ that takes a parameter or a body, as always.
44
+
45
+ ## The template's arguments
46
+
47
+ `Resource<Item, Filters = {}, SortField = "id", Id = uuid>`. Name the ones
48
+ after `Filters`, as above, to skip those you keep:
49
+
50
+ - `Item`: the resource's model. `Create<Item>` and `MergePatchUpdate<Item>`
51
+ leave its read-only properties out, such as `id` and the `Timestamps`.
52
+ - `Filters`: a model of `@query` properties, spread into `list`. Each is
53
+ typed and checked, as any query parameter.
54
+ - `SortField`: the fields `list` may sort by, as a union of strings.
55
+ - `Id`: the type of `{id}`. `Id = integer` for a table whose id is
56
+ `@nxgt/drizzle`'s `id('identity')`.
57
+
58
+ ## Filters and sort
59
+
60
+ `list` takes the filters, the offset page's `page` and `pageSize`, and
61
+ `SortParameters<SortField>`:
62
+
63
+ | Query | Default | Checked |
64
+ | --- | --- | --- |
65
+ | `orderBy` | none: the server's order, the primary key with `@nxgt/drizzle` | one of the `SortField`s |
66
+ | `direction` | `asc` | `asc` or `desc` |
67
+
68
+ `?orderBy=email` on `Authors` is a 400: `email` is not a `SortField`. The
69
+ handler passes them to `@nxgt/drizzle`:
70
+
71
+ ```ts
72
+ import { Hono } from 'hono';
73
+ import { createRoutes } from './generated/hono';
74
+
75
+ const app = new Hono();
76
+ createRoutes(app).get('/authors', async (c) => {
77
+ const { name, page, pageSize, orderBy, direction } = c.req.valid('query');
78
+ const found = await authors.paginate({
79
+ where: name === undefined ? {} : { name },
80
+ page,
81
+ pageSize,
82
+ // paginate takes the sort as an object of directions by field
83
+ ...(orderBy === undefined ? {} : { orderBy: { [orderBy]: direction } }),
84
+ });
85
+ return c.json(found, 200);
86
+ });
87
+ ```
88
+
89
+ `paginateByCursor` takes them apart instead, `{ orderBy, direction }`, as
90
+ the query has them.
91
+
92
+ `SortParameters<Field>` works on its own too, in any list:
93
+
94
+ ```tsp
95
+ @get list(...PageParameters, ...SortParameters<"createdAt" | "title">): Page<Post>;
96
+ ```
97
+
98
+ ## Changing an operation
99
+
100
+ An interface that extends `Resource` can declare one of its operations
101
+ again, and its own replaces the template's. A list paged by cursor:
102
+
103
+ ```tsp
104
+ @route("/comments")
105
+ interface Comments extends Resource<Comment> {
106
+ @get list(...CursorPageParameters): CursorPage<Comment>;
107
+ }
108
+ ```
109
+
110
+ Add `@useAuth(JanusAuth)` on the interface to protect all five, and name the
111
+ guards' replies in the operations you declare again
112
+ ([Authentication](auth.md)).
@@ -0,0 +1,38 @@
1
+ # Roadmap
2
+
3
+ What `@nxgt/typespec` gives a spec, and what is coming. Each convention is the
4
+ shape the `@nxgt/*` packages already send, compiled to OpenAPI 3.1 and 3.2 and
5
+ generated by `@nxgt/openapi-codegen` in CI before it ships.
6
+
7
+ ## Now
8
+
9
+ - **Error replies.** The envelope `@nxgt/openapi-hono` sends, and the
10
+ 400, 401, 403, 404, 409, 422, 429 and 500 responses, with any other status
11
+ through `ErrorResponse<Status>`.
12
+ - **Pagination.** `PageParameters` and `Page<Item>` for an offset page,
13
+ `CursorPageParameters` and `CursorPage<Item>` for a cursor page, as
14
+ `@nxgt/drizzle` and `@nxgt/mongo` page.
15
+ - **Authentication.** `JanusAuth`, the bearer, header and cookie schemes
16
+ `@nxgt/janus` reads, and its guards' 401, 403 and 404 without a body.
17
+ - **Scalars and columns.** `uuid` and `email`, and `Timestamps`,
18
+ `SoftDelete`, `Versioned` and `Actors`, the columns `@nxgt/drizzle`
19
+ stamps a row with.
20
+ - **Headers.** `Idempotency-Key`, `Idempotent-Replayed`, the `RateLimit-*`
21
+ headers and `Retry-After`, and the idempotent write's 409 and 422.
22
+ - **Resources.** `Resource<Item, Filters, SortField, Id>`: list, read,
23
+ create, update and delete in one template, with typed filters and a sort
24
+ by `orderBy` and `direction`.
25
+ - **Operation ids.** `@operationIds` names an interface's operations
26
+ `listPosts`, `createPosts`, instead of `Posts_list`.
27
+
28
+ ## Next
29
+
30
+ - **One reply per status.** An error when an operation declares two replies
31
+ of one status, such as `AuthenticationRequired` and `Unauthorized`, which
32
+ the emitter merges without a warning today.
33
+
34
+ ## Later
35
+
36
+ - **`ETag` and conditional requests.** `ETag`, with `If-None-Match` and
37
+ `If-Match`, once the `@nxgt/*` packages send one the same way. The
38
+ optimistic lock is the body's `version` today (`Versioned`).
@@ -0,0 +1,109 @@
1
+ # Troubleshooting
2
+
3
+ ## `schema ValidationErrorBody and schema ValidationErrorBody would both generate ValidationErrorBody`
4
+
5
+ **When:** the spec declares a schema named `ValidationErrorBody`, for
6
+ instance to describe the 400 with its `issues`.
7
+
8
+ **Why:** `@nxgt/openapi-codegen` declares `ValidationErrorBody` itself, on
9
+ every operation that takes a parameter or a body, and refuses two schemas of
10
+ one name with `name_collision`.
11
+
12
+ **Fix:** answer `BadRequest`. Its `BadRequestBody` carries the `issues`,
13
+ optional, and the generator adds its own 400 beside it.
14
+
15
+ ```tsp
16
+ @patch update(@path postId: string, @body post: MergePatchUpdate<Post>): Post | BadRequest;
17
+ ```
18
+
19
+ ## `Couldn't resolve import "@nxgt/typespec"`
20
+
21
+ **When:** `tsp compile` runs on a spec that imports the library.
22
+
23
+ **Why:** the library is not installed where the spec is, or the project has
24
+ no `package.json` to resolve it from.
25
+
26
+ **Fix:** install it and its peers next to the spec's project, as
27
+ devDependencies, with the command in the
28
+ [README's Install section](../README.md#install).
29
+
30
+ ## `Duplicate type name: 'NotFoundBody'`
31
+
32
+ **When:** `tsp compile` runs on a spec that declares its own model named like
33
+ one of the library's: `BadRequestBody` … `InternalServerErrorBody`,
34
+ `ValidationIssue`, `ValidationTarget`, `Uuid` or `Email` when the spec uses
35
+ `uuid` or `email`, or the page of one of its models,
36
+ such as `PostPage` beside `Page<Post>`, or `CommentCursorPage` beside
37
+ `CursorPage<Comment>`.
38
+
39
+ ```text
40
+ error @typespec/openapi/duplicate-type-name: Duplicate type name: 'NotFoundBody'. Check @friendlyName decorators and overlap with types in TypeSpec or service namespace.
41
+ ```
42
+
43
+ **Why:** the library names its schemas with `@friendlyName`, without a
44
+ namespace, so they share the spec's schema names.
45
+
46
+ **Fix:** reuse the library's model, or rename yours:
47
+
48
+ ```tsp
49
+ @get read(@path postId: string): Post | NotFound;
50
+ ```
51
+
52
+ ## `Two operations are named listPets: an OpenAPI operation id must be unique`
53
+
54
+ **When:** an operation of an `@operationIds` interface has the id of another
55
+ operation:
56
+
57
+ - two interfaces of one name, in two namespaces, share an operation name:
58
+ `Store.Pets` and `Shelter.Pets` both give `listPets`;
59
+ - an interface `extends` another, whose operation has an `@operationId`:
60
+ `extends` copies it as is;
61
+ - an `@operationId` written elsewhere takes the id `@operationIds` gives;
62
+ - an operation declared in the service namespace is named after itself:
63
+ `op listPets()` takes the id of `Pets.list`.
64
+
65
+ ```text
66
+ error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets: an OpenAPI operation id must be unique.
67
+ ```
68
+
69
+ **Why:** `@operationIds` names each operation `<operation><Interface>`, without
70
+ the namespace. `@typespec/openapi3` would emit both ids without a word, and
71
+ the generator would then refuse the spec.
72
+
73
+ **Fix:** give one of them its own `@operationId`. An inherited operation takes
74
+ one by being declared again in the interface that extends:
75
+
76
+ ```tsp
77
+ import "@typespec/openapi";
78
+
79
+ using OpenAPI;
80
+
81
+ @route("/drafts")
82
+ interface Drafts extends Posts {
83
+ @get @operationId("getDraft") read(@path postId: string): Post | NotFound;
84
+ }
85
+ ```
86
+
87
+ ## `Unknown decorator @operationId`
88
+
89
+ **When:** a spec writes `@operationId` next to `@operationIds`.
90
+
91
+ **Why:** `@operationIds` comes from `@nxgt/typespec`, but `@operationId`
92
+ comes from `@typespec/openapi`, which the file does not import.
93
+
94
+ **Fix:** import it in that file:
95
+
96
+ ```tsp
97
+ import "@typespec/openapi";
98
+
99
+ using OpenAPI;
100
+ ```
101
+
102
+ ## `unsupported_version` from the generator
103
+
104
+ **When:** you generate from what `tsp compile` emitted.
105
+
106
+ **Why:** `@typespec/openapi3` emits OpenAPI 3.0 by default.
107
+
108
+ **Fix:** in `tspconfig.yaml`, set `openapi-versions: ['3.1.0']` or
109
+ `['3.2.0']` under the `@typespec/openapi3` options.
package/lib/auth.tsp ADDED
@@ -0,0 +1,43 @@
1
+ import "@typespec/http";
2
+
3
+ using Http;
4
+
5
+ namespace Nxgt;
6
+
7
+ /**
8
+ * A session token in the `X-Session-Token` header, as `@nxgt/janus` reads it
9
+ * after `Authorization: Bearer`.
10
+ */
11
+ model SessionTokenAuth is ApiKeyAuth<ApiKeyLocation.header, "X-Session-Token">;
12
+
13
+ /**
14
+ * A session token in the `janus-session` cookie, as `@nxgt/janus` sets it for
15
+ * a browser.
16
+ */
17
+ model SessionCookieAuth is ApiKeyAuth<ApiKeyLocation.cookie, "janus-session">;
18
+
19
+ /**
20
+ * Every way `@nxgt/janus` reads a session token, in its order:
21
+ * `Authorization: Bearer`, then `X-Session-Token`, then the `janus-session`
22
+ * cookie. Any one of them is enough. Use it with `@useAuth(JanusAuth)`.
23
+ */
24
+ alias JanusAuth = BearerAuth | SessionTokenAuth | SessionCookieAuth;
25
+
26
+ // An error reply without a body, as the @nxgt/janus guards send one:
27
+ // session(auth, { required: true }) and permission() answer 401, 403 or 404
28
+ // with no body.
29
+ /** The request was refused; the reply has no body. */
30
+ @error
31
+ model ErrorWithoutBody<Status extends integer> {
32
+ @statusCode _: Status;
33
+ }
34
+
35
+ // A handler's own 401, with the envelope, is Unauthorized.
36
+ /** The request carries no session, or one the server does not know. */
37
+ @error
38
+ model AuthenticationRequired is ErrorWithoutBody<401>;
39
+
40
+ // A handler's own 403, with the envelope, is Forbidden.
41
+ /** The session's user may not do this. */
42
+ @error
43
+ model AccessDenied is ErrorWithoutBody<403>;
@@ -0,0 +1,50 @@
1
+ namespace Nxgt;
2
+
3
+ /**
4
+ * `createdAt` and `updatedAt`, as `@nxgt/drizzle`'s `timestamps()` columns
5
+ * hold them. Set by the server: read-only. Spread it into a model.
6
+ */
7
+ model Timestamps {
8
+ @visibility(Lifecycle.Read)
9
+ createdAt: utcDateTime;
10
+
11
+ @visibility(Lifecycle.Read)
12
+ updatedAt: utcDateTime;
13
+ }
14
+
15
+ /**
16
+ * `deletedAt`, as `@nxgt/drizzle`'s `softDelete()` column holds it: `null`
17
+ * until the row is deleted. Set by the server: read-only.
18
+ */
19
+ model SoftDelete {
20
+ @visibility(Lifecycle.Read)
21
+ deletedAt: utcDateTime | null;
22
+ }
23
+
24
+ /**
25
+ * `version`, as `@nxgt/drizzle`'s `version()` column holds it: the optimistic
26
+ * lock. Read with the row, and sent back in an update, which the server
27
+ * refuses with a 409 once the row has moved on.
28
+ */
29
+ model Versioned {
30
+ @visibility(Lifecycle.Read, Lifecycle.Update)
31
+ @minValue(0)
32
+ version: integer;
33
+ }
34
+
35
+ /**
36
+ * `createdBy`, `updatedBy` and `deletedBy`, as `@nxgt/drizzle`'s `actors()`
37
+ * columns hold them: the id of who wrote the row, `null` when nobody is
38
+ * known. Set by the server: read-only. `Id` is `uuid` by default; `string`
39
+ * or `integer` for `actors('text')` or `actors('integer')`.
40
+ */
41
+ model Actors<Id = uuid> {
42
+ @visibility(Lifecycle.Read)
43
+ createdBy: Id | null;
44
+
45
+ @visibility(Lifecycle.Read)
46
+ updatedBy: Id | null;
47
+
48
+ @visibility(Lifecycle.Read)
49
+ deletedBy: Id | null;
50
+ }
package/lib/errors.tsp ADDED
@@ -0,0 +1,148 @@
1
+ import "@typespec/http";
2
+
3
+ using Http;
4
+
5
+ /**
6
+ * nxgt's HTTP conventions, as the `@nxgt/*` packages serve them.
7
+ */
8
+ namespace Nxgt;
9
+
10
+ /**
11
+ * Where `@nxgt/openapi-hono` found a request value wrong: a request location,
12
+ * or `response` for a reply the spec does not declare.
13
+ */
14
+ @friendlyName("ValidationTarget")
15
+ union ValidationTarget {
16
+ "param",
17
+ "query",
18
+ "header",
19
+ "json",
20
+ "form",
21
+ "body",
22
+ "response",
23
+ }
24
+
25
+ /** One value `@nxgt/openapi-hono` refused, and why. */
26
+ @friendlyName("ValidationIssue")
27
+ model ValidationIssue {
28
+ target: ValidationTarget;
29
+
30
+ /** Inside the target: `["items", 0, "name"]`, or `[]` for the whole of it. */
31
+ path: (string | integer)[];
32
+
33
+ /**
34
+ * Zod's issue code, or one of the engine's own: `invalid_json`,
35
+ * `invalid_form`, `invalid_content_type`, `missing_body`,
36
+ * `repeated_parameter`, `undeclared_status`.
37
+ */
38
+ code: string;
39
+
40
+ message: string;
41
+ }
42
+
43
+ /** The body of an error reply, as `@nxgt/openapi-hono` sends it. */
44
+ model ErrorBody<Status extends integer> {
45
+ /** The reply's status, repeated in the body. */
46
+ status: Status;
47
+
48
+ /** An i18n key, such as `errors.not-found`, not a sentence. */
49
+ message: string;
50
+
51
+ /** When the error was answered. */
52
+ timestamp: utcDateTime;
53
+ }
54
+
55
+ /**
56
+ * The 400. `@nxgt/openapi-hono` sends it with the `issues` when its
57
+ * validators refuse a request; a handler sends it without them.
58
+ */
59
+ @friendlyName("BadRequestBody")
60
+ model BadRequestBody {
61
+ ...ErrorBody<400>;
62
+
63
+ /** Every value the validators refused, from every target at once. */
64
+ issues?: ValidationIssue[];
65
+ }
66
+
67
+ @friendlyName("UnauthorizedBody")
68
+ model UnauthorizedBody is ErrorBody<401>;
69
+ @friendlyName("ForbiddenBody")
70
+ model ForbiddenBody is ErrorBody<403>;
71
+ @friendlyName("NotFoundBody")
72
+ model NotFoundBody is ErrorBody<404>;
73
+ @friendlyName("ConflictBody")
74
+ model ConflictBody is ErrorBody<409>;
75
+ @friendlyName("UnprocessableEntityBody")
76
+ model UnprocessableEntityBody is ErrorBody<422>;
77
+ @friendlyName("TooManyRequestsBody")
78
+ model TooManyRequestsBody is ErrorBody<429>;
79
+ @friendlyName("InternalServerErrorBody")
80
+ model InternalServerErrorBody is ErrorBody<500>;
81
+
82
+ /** The request is refused, by the validators or by a handler. */
83
+ @error
84
+ model BadRequest {
85
+ @statusCode _: 400;
86
+ @body body: BadRequestBody;
87
+ }
88
+
89
+ /** No credentials, or credentials that are not valid. */
90
+ @error
91
+ model Unauthorized {
92
+ @statusCode _: 401;
93
+ @body body: UnauthorizedBody;
94
+ }
95
+
96
+ /** Authenticated, but not allowed. */
97
+ @error
98
+ model Forbidden {
99
+ @statusCode _: 403;
100
+ @body body: ForbiddenBody;
101
+ }
102
+
103
+ /** Nothing is there. */
104
+ @error
105
+ model NotFound {
106
+ @statusCode _: 404;
107
+ @body body: NotFoundBody;
108
+ }
109
+
110
+ /** The request conflicts with the resource's state, such as a version that moved on. */
111
+ @error
112
+ model Conflict {
113
+ @statusCode _: 409;
114
+ @body body: ConflictBody;
115
+ }
116
+
117
+ /** Well-formed, but not something the server can act on. */
118
+ @error
119
+ model UnprocessableEntity {
120
+ @statusCode _: 422;
121
+ @body body: UnprocessableEntityBody;
122
+ }
123
+
124
+ /** Too many requests. Retry after `Retry-After`. */
125
+ @error
126
+ model TooManyRequests {
127
+ @statusCode _: 429;
128
+ ...RateLimitHeaders;
129
+ ...RetryAfterHeader;
130
+ @body body: TooManyRequestsBody;
131
+ }
132
+
133
+ /**
134
+ * The server failed. With `validateResponses`, `@nxgt/openapi-hono` answers
135
+ * it too when a reply breaks the spec, with `errors.response-validation-failed`.
136
+ */
137
+ @error
138
+ model InternalServerError {
139
+ @statusCode _: 500;
140
+ @body body: InternalServerErrorBody;
141
+ }
142
+
143
+ /** Any other status, with the same body. */
144
+ @error
145
+ model ErrorResponse<Status extends integer> {
146
+ @statusCode _: Status;
147
+ @body body: ErrorBody<Status>;
148
+ }