@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.
@@ -1,113 +1,202 @@
1
1
  # Operation ids
2
2
 
3
- An OpenAPI operation id is what `@nxgt/openapi-codegen` names everything
4
- after: the client's method, the `operations` entry, and the prefix of the
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
- ## What TypeSpec does alone
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
- Without an `@operationId`, `@typespec/openapi3` names an operation after its
10
- interface and its method, joined by `_`:
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 list(@query status?: PostStatus): Post[]; // Posts_list
16
- @post create(@body post: Create<Post>): Post; // Posts_create
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
- The client then calls `Posts_list`, and its types are `PostsListQuery`.
21
- Writing an `@operationId` on each operation fixes that, one line per
22
- operation.
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
- ## `@operationIds`
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
- Put it on the interface, once:
63
+ ## Where it goes
27
64
 
28
65
  ```tsp
29
- import "@typespec/http";
30
- import "@typespec/openapi";
31
- import "@nxgt/typespec";
66
+ extern dec operationIds(target: Interface | Namespace);
67
+ ```
32
68
 
33
- using Http;
34
- using OpenAPI;
35
- using Nxgt;
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 list(@query status?: PostStatus): Post[]; // listPosts
43
- @get @operationId("getPost") read(@path postId: string): Post | NotFound; // getPost
44
- @post create(@body post: Create<Post>): Post | BadRequest; // createPosts
45
- @patch update(@path postId: string, @body post: MergePatchUpdate<Post>): Post | NotFound; // updatePosts
46
- @delete delete(@path postId: string): NoContentResponse | NotFound; // deletePosts
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
- - Each operation is named `<operation><Interface>`: the method's name, then
51
- the interface's, as written.
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
- The generated `operations` then has one entry per id:
96
+ ## An operation's own `@operationId`
61
97
 
62
- ```ts
63
- import { operations } from './generated/operations';
98
+ An operation with its own `@operationId` keeps it, whether it is written
99
+ before or after `@operationIds`:
64
100
 
65
- Object.keys(operations);
66
- // ['listPosts', 'createPosts', 'getPost', 'updatePosts', 'deletePosts']
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
- On an interface template, `@operationIds` names the operations of each
72
- interface that extends an instance, after that interface:
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 Resource<Item> {
77
- @get list(): Item[];
123
+ interface Listable<Item> {
124
+ @get listAll(): Item[];
78
125
  }
79
126
 
80
127
  @route("/pets")
81
- interface Pets extends Resource<Pet> {} // listPets
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
- @route("/orders")
84
- interface Orders extends Resource<Order> {} // listOrders
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, and four cases break that:
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
- - the namespace is not part of the id, so `Store.Pets` and `Shelter.Pets`
92
- would both name their `list` `listPets`;
93
- - an `@operationId` is copied by `extends`, so `Drafts extends Posts` would
94
- have a second `getPost`;
95
- - an `@operationId` written on another operation, in any interface, takes
96
- the id `@operationIds` gives;
97
- - an operation declared in the service namespace is named after itself, so
98
- `op listPets()` takes the id of `Pets.list`.
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
- `@operationIds` refuses each with `duplicate-operation-id`, where
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` (`Posts_list`, the default), `fqn` (with the namespace)
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` only names the operations of the interfaces it marks: the
112
- strategy still names every other one. The check for two operations named
113
- alike reads their ids as the default strategy gives them.
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.
@@ -22,7 +22,7 @@ namespace Blog;
22
22
 
23
23
  @route("/posts")
24
24
  interface Posts {
25
- @get list(@query status?: PostStatus, ...PageParameters): Page<Post>;
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 list(
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 list(...PageParameters): Page<Pet>; // PetPage
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
- - **One reply per status.** An operation that declares a reply without a
10
- body beside one with a body, such as `AuthenticationRequired` and
11
- `Unauthorized`, is an error, `duplicate-status-reply`, and two replies with
12
- a body of one status code a warning, `merged-status-reply`, instead of
13
- replies the emitter merges without a word.
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
@@ -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 update(@path postId: string, @body post: MergePatchUpdate<Post>): Post | BadRequest;
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 read(@path postId: string): Post | NotFound;
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:** 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`.
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` 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.
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
- **Fix:** give one of them its own `@operationId`. An inherited operation takes
74
- one by being declared again in the interface that extends:
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") read(@path postId: string): Post | NotFound;
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 list(): Post[] | AuthenticationRequired | Unauthorized;
113
+ @get listPosts(): Post[] | AuthenticationRequired | Unauthorized;
94
114
  ```
95
115
 
96
116
  ```text
97
- error @nxgt/typespec/duplicate-status-reply: list 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.
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 list(): Post[] | AuthenticationRequired;
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 create(@body post: Post): Post | IdempotencyInProgress | Conflict;
139
+ @post createPost(@body post: Post): Post | IdempotencyInProgress | Conflict;
120
140
  ```
121
141
 
122
142
  ```text
123
- warning @nxgt/typespec/merged-status-reply: create 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.
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 create(@body post: Post): Post | IdempotencyInProgress;
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
@@ -8,4 +8,4 @@ import "./auth.tsp";
8
8
  import "./scalars.tsp";
9
9
  import "./columns.tsp";
10
10
  import "./headers.tsp";
11
- import "./resources.tsp";
11
+ import "./sorting.tsp";
@@ -5,18 +5,23 @@ using TypeSpec.Reflection;
5
5
  namespace Nxgt;
6
6
 
7
7
  /**
8
- * Names each operation of the interface after its method and the interface,
9
- * `<operation><Interface>`: `list` in `interface Pets` becomes `listPets`. An
10
- * operation with its own `@operationId` keeps it. An interface extending a
11
- * marked one, or an instance of a marked template, is named after itself. An
12
- * id another operation has is an error, `duplicate-operation-id`.
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
- * interface Pets {
18
- * @get list(): Pet[];
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);
@@ -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
+ }