@nxgt/typespec 0.2.0 → 0.4.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 +41 -47
- package/dist/index.js +48 -8
- package/dist/index.js.map +4 -4
- package/dist/lib.d.ts.map +1 -1
- package/dist/operation-ids.d.ts +15 -11
- package/dist/operation-ids.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide/auth.md +3 -3
- package/docs/guide/columns.md +1 -1
- package/docs/guide/errors.md +3 -3
- package/docs/guide/headers.md +2 -2
- package/docs/guide/operation-ids.md +152 -63
- package/docs/guide/pagination.md +3 -3
- package/docs/guide/sorting.md +131 -0
- package/docs/roadmap.md +20 -5
- package/docs/troubleshooting.md +44 -24
- package/lib/main.tsp +1 -1
- package/lib/operation-ids.tsp +13 -8
- package/lib/sorting.tsp +20 -0
- package/package.json +2 -2
- package/docs/guide/resources.md +0 -112
- package/lib/resources.tsp +0 -63
|
@@ -1,113 +1,202 @@
|
|
|
1
1
|
# Operation ids
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
operation's types (`ListPostsQuery`, `zGetPostParam`).
|
|
3
|
+
`@operationIds` makes each operation's OpenAPI id its name, exactly as
|
|
4
|
+
written, so the generated client's methods are the names in your spec.
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
An operation id is what `@nxgt/openapi-codegen` names everything after: the
|
|
7
|
+
client's method, the `operations` entry, and the prefix of the operation's
|
|
8
|
+
types (`ListPostsQuery`, `zGetPostParam`).
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
## On the service namespace
|
|
11
|
+
|
|
12
|
+
Mark the namespace once, and every operation in it is named as written:
|
|
11
13
|
|
|
12
14
|
```tsp
|
|
15
|
+
import "@typespec/http";
|
|
16
|
+
import "@nxgt/typespec";
|
|
17
|
+
|
|
18
|
+
using Http;
|
|
19
|
+
using Nxgt;
|
|
20
|
+
|
|
21
|
+
@service(#{ title: "Blog" })
|
|
22
|
+
@operationIds
|
|
23
|
+
namespace Blog;
|
|
24
|
+
|
|
25
|
+
model Post {
|
|
26
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
27
|
+
title: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
model Comment {
|
|
31
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
32
|
+
body: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
@route("/posts")
|
|
14
36
|
interface Posts {
|
|
15
|
-
@get
|
|
16
|
-
@
|
|
37
|
+
@get listPosts(...PageParameters): Page<Post>; // listPosts
|
|
38
|
+
@get getPost(@path postId: uuid): Post | NotFound; // getPost
|
|
39
|
+
@delete deletePost(@path postId: uuid): NoContentResponse | NotFound; // deletePost
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@route("/posts/{postId}/comments")
|
|
43
|
+
interface Comments {
|
|
44
|
+
@get findPostComments(@path postId: uuid, ...CursorPageParameters): CursorPage<Comment>; // findPostComments
|
|
17
45
|
}
|
|
18
46
|
```
|
|
19
47
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
48
|
+
Alone, `@typespec/openapi3` would write `Posts_listPosts` and
|
|
49
|
+
`Comments_findPostComments`. The generated `operations` has one entry per
|
|
50
|
+
id:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { operations } from './generated/operations';
|
|
54
|
+
|
|
55
|
+
Object.keys(operations);
|
|
56
|
+
// ['listPosts', 'getPost', 'deletePost', 'findPostComments']
|
|
57
|
+
```
|
|
23
58
|
|
|
24
|
-
|
|
59
|
+
Name each operation as the client should call it: `listPosts`, not `list`.
|
|
60
|
+
An operation named `list` gets the id `list`, and a second `list` in another
|
|
61
|
+
interface is an error ([below](#two-operations-named-alike)).
|
|
25
62
|
|
|
26
|
-
|
|
63
|
+
## Where it goes
|
|
27
64
|
|
|
28
65
|
```tsp
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
import "@nxgt/typespec";
|
|
66
|
+
extern dec operationIds(target: Interface | Namespace);
|
|
67
|
+
```
|
|
32
68
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
69
|
+
| Marked | Named as written |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| a namespace | every operation in it, however deep: in its interfaces, in the namespaces it contains, and declared in it with `op` |
|
|
72
|
+
| an interface | its operations, and those of each interface that `extends` it |
|
|
73
|
+
| neither | nothing: the emitter's `operation-id-strategy` names the operation |
|
|
74
|
+
|
|
75
|
+
On one interface, the others keep the emitter's names:
|
|
36
76
|
|
|
77
|
+
```tsp
|
|
78
|
+
@service
|
|
37
79
|
namespace Blog;
|
|
38
80
|
|
|
39
81
|
@route("/posts")
|
|
40
82
|
@operationIds
|
|
41
83
|
interface Posts {
|
|
42
|
-
@get
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
@get listPosts(): Post[]; // listPosts
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@route("/drafts")
|
|
88
|
+
interface Drafts {
|
|
89
|
+
@get listDrafts(): Post[]; // Drafts_listDrafts
|
|
47
90
|
}
|
|
48
91
|
```
|
|
49
92
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- An operation with its own `@operationId` keeps it, whether it is written
|
|
53
|
-
before or after `@operationIds`. `import "@typespec/openapi"` and
|
|
54
|
-
`using OpenAPI` are only needed for that `@operationId`.
|
|
55
|
-
- An interface that `extends` a marked one is marked too, and names the
|
|
56
|
-
operations it inherits after itself: `interface Drafts extends Posts {}`
|
|
57
|
-
gives `listDrafts`. An inherited `@operationId` is copied as is, though,
|
|
58
|
-
so `Drafts` would name its `read` `getPost` too: see below.
|
|
93
|
+
A namespace beside the marked one is not marked: `@operationIds` on `Blog`
|
|
94
|
+
does not reach a `namespace Legacy` declared next to it.
|
|
59
95
|
|
|
60
|
-
|
|
96
|
+
## An operation's own `@operationId`
|
|
61
97
|
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
An operation with its own `@operationId` keeps it, whether it is written
|
|
99
|
+
before or after `@operationIds`:
|
|
64
100
|
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
```tsp
|
|
102
|
+
import "@typespec/openapi";
|
|
103
|
+
|
|
104
|
+
using OpenAPI;
|
|
105
|
+
|
|
106
|
+
@route("/posts")
|
|
107
|
+
interface Posts {
|
|
108
|
+
@get @operationId("fetchPost") getPost(@path postId: uuid): Post | NotFound; // fetchPost
|
|
109
|
+
}
|
|
67
110
|
```
|
|
68
111
|
|
|
112
|
+
`import "@typespec/openapi"` and `using OpenAPI` are only needed for
|
|
113
|
+
`@operationId` ([troubleshooting](../troubleshooting.md#unknown-decorator-operationid)).
|
|
114
|
+
|
|
69
115
|
## On a template
|
|
70
116
|
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
An interface template is never emitted, only the interfaces that extend one
|
|
118
|
+
of its instances. When the template is marked, or the interface that extends
|
|
119
|
+
it is in a marked namespace, those are named as written:
|
|
73
120
|
|
|
74
121
|
```tsp
|
|
75
122
|
@operationIds
|
|
76
|
-
interface
|
|
77
|
-
@get
|
|
123
|
+
interface Listable<Item> {
|
|
124
|
+
@get listAll(): Item[];
|
|
78
125
|
}
|
|
79
126
|
|
|
80
127
|
@route("/pets")
|
|
81
|
-
interface Pets extends
|
|
128
|
+
interface Pets extends Listable<Pet> {} // listAll
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A second interface extending `Listable` would name its operation `listAll`
|
|
132
|
+
too, which is an error: see below.
|
|
82
133
|
|
|
83
|
-
|
|
84
|
-
|
|
134
|
+
An operation template is never emitted either, only the operations declared
|
|
135
|
+
from it. In a marked namespace, each is named as written:
|
|
136
|
+
|
|
137
|
+
```tsp
|
|
138
|
+
op Read<Item>(): Item;
|
|
139
|
+
|
|
140
|
+
@route("/pets/first") @get op readFirstPet is Read<Pet>; // readFirstPet
|
|
141
|
+
@route("/toys/first") @get op readFirstToy is Read<Toy>; // readFirstToy
|
|
85
142
|
```
|
|
86
143
|
|
|
87
144
|
## Two operations named alike
|
|
88
145
|
|
|
89
|
-
An id must be unique
|
|
146
|
+
An id must be unique in its service's document. Two `@service` namespaces
|
|
147
|
+
are two documents, and may each have a `health`. `@operationIds` refuses two
|
|
148
|
+
operations of one id, one of them named by it, with `duplicate-operation-id`,
|
|
149
|
+
where `@typespec/openapi3` would emit both without a word:
|
|
90
150
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
151
|
+
```text
|
|
152
|
+
error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets: an OpenAPI operation id must be unique.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
It happens when:
|
|
156
|
+
|
|
157
|
+
- one name is in two interfaces: neither the namespace nor the interface is
|
|
158
|
+
part of the id, so `Store.Pets` and `Shelter.Pets` both give `listPets`;
|
|
159
|
+
- an interface `extends` another: it copies its operations and their
|
|
160
|
+
names, so `Drafts extends Posts` gives a second `getPost`, and two
|
|
161
|
+
interfaces extending one template share every operation;
|
|
162
|
+
- an `@operationId` written on another operation is a name
|
|
163
|
+
`@operationIds` gives;
|
|
164
|
+
- an unmarked operation declared in the service namespace, which the
|
|
165
|
+
emitter names after itself, has a marked operation's name: `op listPets()`
|
|
166
|
+
beside `Pets.listPets`.
|
|
167
|
+
|
|
168
|
+
Declare distinct operations rather than extending:
|
|
169
|
+
|
|
170
|
+
```tsp
|
|
171
|
+
@route("/posts")
|
|
172
|
+
interface Posts {
|
|
173
|
+
@get getPost(@path postId: uuid): Post | NotFound;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@route("/drafts")
|
|
177
|
+
interface Drafts {
|
|
178
|
+
@get getDraft(@path draftId: uuid): Post | NotFound;
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Or keep `extends`, and declare each copied operation again with its own
|
|
183
|
+
`@operationId`:
|
|
184
|
+
|
|
185
|
+
```tsp
|
|
186
|
+
@route("/drafts")
|
|
187
|
+
interface Drafts extends Posts {
|
|
188
|
+
@get @operationId("getDraft") getPost(@path postId: uuid): Post | NotFound;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
99
191
|
|
|
100
|
-
|
|
101
|
-
`@typespec/openapi3` would emit both without a word. Give one of them its own
|
|
102
|
-
`@operationId`; an inherited operation takes one by being declared again in
|
|
103
|
-
the interface that extends ([troubleshooting](../troubleshooting.md)).
|
|
192
|
+
More in [troubleshooting](../troubleshooting.md#two-operations-are-named-listpets-an-openapi-operation-id-must-be-unique).
|
|
104
193
|
|
|
105
194
|
## Without the library
|
|
106
195
|
|
|
107
196
|
The emitter's own option, `operation-id-strategy`, only chooses between
|
|
108
|
-
`parent-container` (`
|
|
109
|
-
and `explicit-only`. None of them gives `listPosts`.
|
|
197
|
+
`parent-container` (`Posts_listPosts`, the default), `fqn` (with the
|
|
198
|
+
namespace) and `explicit-only`. None of them gives `listPosts`.
|
|
110
199
|
|
|
111
|
-
`@operationIds`
|
|
112
|
-
|
|
113
|
-
|
|
200
|
+
The operations `@operationIds` does not name keep that strategy. They are
|
|
201
|
+
checked against the ones it names by the id the default strategy gives
|
|
202
|
+
them.
|
package/docs/guide/pagination.md
CHANGED
|
@@ -22,7 +22,7 @@ namespace Blog;
|
|
|
22
22
|
|
|
23
23
|
@route("/posts")
|
|
24
24
|
interface Posts {
|
|
25
|
-
@get
|
|
25
|
+
@get listPosts(@query status?: PostStatus, ...PageParameters): Page<Post>;
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -46,7 +46,7 @@ the last one has no items and the same `total`.
|
|
|
46
46
|
```tsp
|
|
47
47
|
@route("/posts/{postId}/comments")
|
|
48
48
|
interface Comments {
|
|
49
|
-
@get
|
|
49
|
+
@get findPostComments(
|
|
50
50
|
@path postId: string,
|
|
51
51
|
...CursorPageParameters,
|
|
52
52
|
): CursorPage<Comment> | BadRequest | NotFound;
|
|
@@ -134,5 +134,5 @@ page follows:
|
|
|
134
134
|
```tsp
|
|
135
135
|
union Pet { cat: Cat, dog: Dog }
|
|
136
136
|
|
|
137
|
-
@get
|
|
137
|
+
@get listPets(...PageParameters): Page<Pet>; // PetPage
|
|
138
138
|
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Sorting
|
|
2
|
+
|
|
3
|
+
`SortParameters<Field>` gives a list the query `orderBy` and `direction`,
|
|
4
|
+
checked against the fields you name, as `@nxgt/drizzle` sorts a page.
|
|
5
|
+
|
|
6
|
+
## A filtered, sorted list
|
|
7
|
+
|
|
8
|
+
Write the filters as a model of `@query` properties, and spread it into the
|
|
9
|
+
list with `PageParameters` and `SortParameters`:
|
|
10
|
+
|
|
11
|
+
```tsp
|
|
12
|
+
import "@typespec/http";
|
|
13
|
+
import "@nxgt/typespec";
|
|
14
|
+
|
|
15
|
+
using Http;
|
|
16
|
+
using Nxgt;
|
|
17
|
+
|
|
18
|
+
@service(#{ title: "Blog" })
|
|
19
|
+
@operationIds
|
|
20
|
+
namespace Blog;
|
|
21
|
+
|
|
22
|
+
model Author {
|
|
23
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
24
|
+
name: string;
|
|
25
|
+
email: email;
|
|
26
|
+
...Timestamps;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What the author list filters on. */
|
|
30
|
+
model AuthorFilters {
|
|
31
|
+
/** Authors of this name. */
|
|
32
|
+
@query name?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@route("/authors")
|
|
36
|
+
interface Authors {
|
|
37
|
+
@get listAuthors(
|
|
38
|
+
...AuthorFilters,
|
|
39
|
+
...PageParameters,
|
|
40
|
+
...SortParameters<"name" | "createdAt">,
|
|
41
|
+
): Page<Author>;
|
|
42
|
+
|
|
43
|
+
@get getAuthor(@path id: uuid): Author | NotFound;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`GET /authors?name=Ada&orderBy=name&direction=desc` is one page of the
|
|
48
|
+
authors named Ada, by name, descending. The list is an operation like any
|
|
49
|
+
other, and `@operationIds` gives it the id `listAuthors`
|
|
50
|
+
([Operation ids](operation-ids.md)).
|
|
51
|
+
|
|
52
|
+
## The query
|
|
53
|
+
|
|
54
|
+
```tsp
|
|
55
|
+
model SortParameters<Field extends string> {
|
|
56
|
+
@query orderBy?: Field;
|
|
57
|
+
@query direction?: "asc" | "desc" = "asc";
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Query | Type | Default | Checked |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `orderBy` | `Field` | none: the server's order, the primary key with `@nxgt/drizzle` | one of the `Field`s |
|
|
64
|
+
| `direction` | `"asc" \| "desc"` | `asc` | `asc` or `desc` |
|
|
65
|
+
|
|
66
|
+
`?orderBy=email` on `Authors` is a 400: `email` is not one of the fields.
|
|
67
|
+
`?direction=up` is a 400 too. The validators fill in the default, so a
|
|
68
|
+
handler always reads a `direction`.
|
|
69
|
+
|
|
70
|
+
`Field` is a union of strings: the fields a client may sort by, not every
|
|
71
|
+
property of the model.
|
|
72
|
+
|
|
73
|
+
## Filters
|
|
74
|
+
|
|
75
|
+
Each property of the filters model is a query parameter, typed and checked
|
|
76
|
+
like any other. A filter that is not a string says so:
|
|
77
|
+
|
|
78
|
+
```tsp
|
|
79
|
+
enum PostStatus {
|
|
80
|
+
draft,
|
|
81
|
+
published,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
model PostFilters {
|
|
85
|
+
@query status?: PostStatus;
|
|
86
|
+
@query authorId?: uuid;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@route("/posts")
|
|
90
|
+
interface Posts {
|
|
91
|
+
@get listPosts(
|
|
92
|
+
...PostFilters,
|
|
93
|
+
...PageParameters,
|
|
94
|
+
...SortParameters<"createdAt" | "title">,
|
|
95
|
+
): Page<Post>;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Serving it with `@nxgt/drizzle`
|
|
100
|
+
|
|
101
|
+
The handler reads the query the generator checked, and passes it to the
|
|
102
|
+
repository:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Hono } from 'hono';
|
|
106
|
+
import { createRoutes } from './generated/hono';
|
|
107
|
+
|
|
108
|
+
const app = new Hono();
|
|
109
|
+
createRoutes(app).get('/authors', async (c) => {
|
|
110
|
+
const { name, page, pageSize, orderBy, direction } = c.req.valid('query');
|
|
111
|
+
const found = await authors.paginate({
|
|
112
|
+
where: name === undefined ? {} : { name },
|
|
113
|
+
page,
|
|
114
|
+
pageSize,
|
|
115
|
+
// paginate takes the sort as an object of directions by field
|
|
116
|
+
...(orderBy === undefined ? {} : { orderBy: { [orderBy]: direction } }),
|
|
117
|
+
});
|
|
118
|
+
return c.json(found, 200);
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- `authors` is the `@nxgt/drizzle` repository of the table.
|
|
123
|
+
- `where` is an exact equality per field: a filter that is absent is left
|
|
124
|
+
out, not passed as `undefined`.
|
|
125
|
+
- The sort is spread only when `orderBy` is there, which keeps the call
|
|
126
|
+
valid under `exactOptionalPropertyTypes`.
|
|
127
|
+
- `paginate` returns the `Page<Author>` the spec declares, as it is.
|
|
128
|
+
|
|
129
|
+
`paginateByCursor` takes the sort apart instead, `{ orderBy, direction }`, as
|
|
130
|
+
the query has them; spread `CursorPageParameters` in place of
|
|
131
|
+
`PageParameters` for it ([Pagination](pagination.md)).
|
package/docs/roadmap.md
CHANGED
|
@@ -6,11 +6,12 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
- **Operation ids as written.** `@operationIds` names each operation
|
|
10
|
+
exactly as the spec writes it, `findPostComments`, on an interface or on
|
|
11
|
+
the whole service namespace, instead of `<operation><Interface>`.
|
|
12
|
+
- **No `Resource` template.** A resource's operations are written by hand,
|
|
13
|
+
each with the name it has in the client; `SortParameters` stays, for the
|
|
14
|
+
sort of a list.
|
|
14
15
|
|
|
15
16
|
## Later
|
|
16
17
|
|
|
@@ -20,6 +21,20 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
20
21
|
|
|
21
22
|
## Shipped
|
|
22
23
|
|
|
24
|
+
### 0.3.0
|
|
25
|
+
|
|
26
|
+
- **Resource ids in the singular.** `Resource` named `read`, `create`,
|
|
27
|
+
`update` and `delete` after its item, `readPost`, and `list` after the
|
|
28
|
+
interface, `listPosts`.
|
|
29
|
+
|
|
30
|
+
### 0.2.0
|
|
31
|
+
|
|
32
|
+
- **One reply per status.** An operation that declares a reply without a
|
|
33
|
+
body beside one with a body, such as `AuthenticationRequired` and
|
|
34
|
+
`Unauthorized`, is an error, `duplicate-status-reply`, and two replies with
|
|
35
|
+
a body of one status code a warning, `merged-status-reply`, instead of
|
|
36
|
+
replies the emitter merges without a word.
|
|
37
|
+
|
|
23
38
|
### 0.1.0
|
|
24
39
|
|
|
25
40
|
- **Error replies.** The envelope `@nxgt/openapi-hono` sends, and the
|
package/docs/troubleshooting.md
CHANGED
|
@@ -13,7 +13,7 @@ one name with `name_collision`.
|
|
|
13
13
|
optional, and the generator adds its own 400 beside it.
|
|
14
14
|
|
|
15
15
|
```tsp
|
|
16
|
-
@patch
|
|
16
|
+
@patch updatePost(@path postId: string, @body post: MergePatchUpdate<Post>): Post | BadRequest;
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## `Couldn't resolve import "@nxgt/typespec"`
|
|
@@ -46,32 +46,52 @@ namespace, so they share the spec's schema names.
|
|
|
46
46
|
**Fix:** reuse the library's model, or rename yours:
|
|
47
47
|
|
|
48
48
|
```tsp
|
|
49
|
-
@get
|
|
49
|
+
@get getPost(@path postId: string): Post | NotFound;
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## `Two operations are named listPets: an OpenAPI operation id must be unique`
|
|
53
53
|
|
|
54
|
-
**When:**
|
|
55
|
-
operation
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
**When:** `tsp compile` runs on a spec where an operation that
|
|
55
|
+
`@operationIds` names has the id of another operation of the same service
|
|
56
|
+
(two `@service` namespaces are two documents, and may reuse a name):
|
|
57
|
+
|
|
58
|
+
- one name is in two interfaces: `Store.Pets` and `Shelter.Pets` both
|
|
59
|
+
declare `listPets`;
|
|
60
|
+
- an interface `extends` another, and so copies its operations and their
|
|
61
|
+
names: `Drafts extends Posts` gives a second `getPost`, and two interfaces
|
|
62
|
+
extending one template share every operation;
|
|
63
|
+
- an `@operationId` written on another operation is a name `@operationIds`
|
|
64
|
+
gives;
|
|
65
|
+
- an unmarked operation declared in the service namespace, which the
|
|
66
|
+
emitter names after itself, has a marked operation's name: `op listPets()`
|
|
67
|
+
beside `Pets.listPets`.
|
|
64
68
|
|
|
65
69
|
```text
|
|
66
70
|
error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets: an OpenAPI operation id must be unique.
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
**Why:** `@operationIds`
|
|
70
|
-
|
|
71
|
-
the generator would then refuse the spec.
|
|
73
|
+
**Why:** `@operationIds` makes each operation's id its name, as written,
|
|
74
|
+
without its interface or its namespace. `@typespec/openapi3` would emit both
|
|
75
|
+
ids without a word, and the generator would then refuse the spec.
|
|
76
|
+
|
|
77
|
+
**Fix:** give each operation a name of its own. Declare distinct operations
|
|
78
|
+
rather than extending:
|
|
79
|
+
|
|
80
|
+
```tsp
|
|
81
|
+
@route("/posts")
|
|
82
|
+
interface Posts {
|
|
83
|
+
@get getPost(@path postId: uuid): Post | NotFound;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@route("/drafts")
|
|
87
|
+
interface Drafts {
|
|
88
|
+
@get getDraft(@path draftId: uuid): Post | NotFound;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
Or give one of them its own `@operationId`; an inherited operation takes one
|
|
93
|
+
by being declared again in the interface that extends
|
|
94
|
+
([Operation ids](guide/operation-ids.md#two-operations-named-alike)):
|
|
75
95
|
|
|
76
96
|
```tsp
|
|
77
97
|
import "@typespec/openapi";
|
|
@@ -80,7 +100,7 @@ using OpenAPI;
|
|
|
80
100
|
|
|
81
101
|
@route("/drafts")
|
|
82
102
|
interface Drafts extends Posts {
|
|
83
|
-
@get @operationId("getDraft")
|
|
103
|
+
@get @operationId("getDraft") getPost(@path postId: uuid): Post | NotFound;
|
|
84
104
|
}
|
|
85
105
|
```
|
|
86
106
|
|
|
@@ -90,11 +110,11 @@ interface Drafts extends Posts {
|
|
|
90
110
|
one with a body, such as the guard's 401 and the handler's:
|
|
91
111
|
|
|
92
112
|
```tsp
|
|
93
|
-
@get
|
|
113
|
+
@get listPosts(): Post[] | AuthenticationRequired | Unauthorized;
|
|
94
114
|
```
|
|
95
115
|
|
|
96
116
|
```text
|
|
97
|
-
error @nxgt/typespec/duplicate-status-reply:
|
|
117
|
+
error @nxgt/typespec/duplicate-status-reply: listPosts declares a reply without a body and one with a body of status 401: the emitter merges them into one, and the reply without a body is lost. Declare the one the route sends.
|
|
98
118
|
```
|
|
99
119
|
|
|
100
120
|
**Why:** OpenAPI has one reply per status. `@typespec/openapi3` merges the two
|
|
@@ -106,7 +126,7 @@ guard's reply has no body.
|
|
|
106
126
|
answers with the envelope, `Unauthorized`:
|
|
107
127
|
|
|
108
128
|
```tsp
|
|
109
|
-
@get
|
|
129
|
+
@get listPosts(): Post[] | AuthenticationRequired;
|
|
110
130
|
```
|
|
111
131
|
|
|
112
132
|
## `declares two replies with a body of status …`
|
|
@@ -116,11 +136,11 @@ status code, such as the idempotent write's 409 beside the optimistic
|
|
|
116
136
|
lock's, or its 422, `IdempotencyKeyReused`, beside `UnprocessableEntity`:
|
|
117
137
|
|
|
118
138
|
```tsp
|
|
119
|
-
@post
|
|
139
|
+
@post createPost(@body post: Post): Post | IdempotencyInProgress | Conflict;
|
|
120
140
|
```
|
|
121
141
|
|
|
122
142
|
```text
|
|
123
|
-
warning @nxgt/typespec/merged-status-reply:
|
|
143
|
+
warning @nxgt/typespec/merged-status-reply: createPost declares two replies with a body of status 409: the emitter merges their bodies under the first one's description. Declare the one the route sends.
|
|
124
144
|
```
|
|
125
145
|
|
|
126
146
|
**Why:** `@typespec/openapi3` keeps both bodies, as one reply whose body is
|
|
@@ -131,7 +151,7 @@ header either reply declares required becomes optional.
|
|
|
131
151
|
`message` key tells them apart:
|
|
132
152
|
|
|
133
153
|
```tsp
|
|
134
|
-
@post
|
|
154
|
+
@post createPost(@body post: Post): Post | IdempotencyInProgress;
|
|
135
155
|
```
|
|
136
156
|
|
|
137
157
|
Two plain bodies, `Post | Draft`, are one reply whose body is either; two
|
package/lib/main.tsp
CHANGED
package/lib/operation-ids.tsp
CHANGED
|
@@ -5,18 +5,23 @@ using TypeSpec.Reflection;
|
|
|
5
5
|
namespace Nxgt;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* id another operation has is an
|
|
8
|
+
* The id of each operation is its name, as written: `findPostComments` is
|
|
9
|
+
* `findPostComments`, where `@typespec/openapi3` would write
|
|
10
|
+
* `Posts_findPostComments`. On an interface, its operations; on a namespace,
|
|
11
|
+
* every operation in it, however deep. An operation with its own
|
|
12
|
+
* `@operationId` keeps it. An id another operation of the same service has is an
|
|
13
|
+
* error, `duplicate-operation-id`.
|
|
13
14
|
*
|
|
14
15
|
* @example
|
|
15
16
|
* ```typespec
|
|
17
|
+
* @service
|
|
16
18
|
* @operationIds
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
+
* namespace Blog;
|
|
20
|
+
*
|
|
21
|
+
* @route("/posts")
|
|
22
|
+
* interface Posts {
|
|
23
|
+
* @get listPosts(): Post[];
|
|
19
24
|
* }
|
|
20
25
|
* ```
|
|
21
26
|
*/
|
|
22
|
-
extern dec operationIds(target: Interface);
|
|
27
|
+
extern dec operationIds(target: Interface | Namespace);
|
package/lib/sorting.tsp
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "@typespec/http";
|
|
2
|
+
|
|
3
|
+
using Http;
|
|
4
|
+
|
|
5
|
+
namespace Nxgt;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The sort of a list, as `@nxgt/drizzle` takes it: one field, and a
|
|
9
|
+
* direction. `Field` is the union of the fields a client may sort by.
|
|
10
|
+
* Spread it into an operation's parameters.
|
|
11
|
+
*/
|
|
12
|
+
model SortParameters<Field extends string> {
|
|
13
|
+
/** The field to sort by. */
|
|
14
|
+
@query
|
|
15
|
+
orderBy?: Field;
|
|
16
|
+
|
|
17
|
+
/** `asc` or `desc`. */
|
|
18
|
+
@query
|
|
19
|
+
direction?: "asc" | "desc" = "asc";
|
|
20
|
+
}
|