@nxgt/typespec 0.3.0 → 0.4.1

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,120 +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:
23
51
 
24
- ## `@operationIds`
52
+ ```ts
53
+ import { operations } from './generated/operations';
25
54
 
26
- Put it on the interface, once:
55
+ Object.keys(operations);
56
+ // ['listPosts', 'getPost', 'deletePost', 'findPostComments']
57
+ ```
58
+
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)).
62
+
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 |
36
74
 
75
+ On one interface, the others keep the emitter's names:
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
82
-
83
- @route("/orders")
84
- interface Orders extends Resource<Order> {} // listOrders
128
+ interface Pets extends Listable<Pet> {} // listAll
85
129
  ```
86
130
 
87
- The library's own `Resource` goes one step further: `list` is named after
88
- the interface, and `read`, `create`, `update` and `delete` after the item,
89
- `readPet` ([Resources](resources.md)).
131
+ A second interface extending `Listable` would name its operation `listAll`
132
+ too, which is an error: see below.
133
+
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
142
+ ```
90
143
 
91
144
  ## Two operations named alike
92
145
 
93
- An id must be unique, and five cases break that:
94
-
95
- - the namespace is not part of the id, so `Store.Pets` and `Shelter.Pets`
96
- would both name their `list` `listPets`;
97
- - an `@operationId` is copied by `extends`, so `Drafts extends Posts` would
98
- have a second `getPost`;
99
- - an `@operationId` written on another operation, in any interface, takes
100
- the id `@operationIds` gives;
101
- - an operation declared in the service namespace is named after itself, so
102
- `op listPets()` takes the id of `Pets.list`;
103
- - two interfaces extend `Resource` with one item, so `Authors` and
104
- `ArchivedAuthors` would both name their `read` `readAuthor`
105
- ([Resources](resources.md#two-resources-of-one-item)).
106
-
107
- `@operationIds` refuses each with `duplicate-operation-id`, where
108
- `@typespec/openapi3` would emit both without a word. Give one of them its own
109
- `@operationId`; an inherited operation takes one by being declared again in
110
- the interface that extends ([troubleshooting](../troubleshooting.md)).
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:
150
+
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
+ ```
191
+
192
+ More in [troubleshooting](../troubleshooting.md#two-operations-are-named-listpets-an-openapi-operation-id-must-be-unique).
111
193
 
112
194
  ## Without the library
113
195
 
114
196
  The emitter's own option, `operation-id-strategy`, only chooses between
115
- `parent-container` (`Posts_list`, the default), `fqn` (with the namespace)
116
- 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`.
117
199
 
118
- `@operationIds` only names the operations of the interfaces it marks: the
119
- strategy still names every other one. The check for two operations named
120
- 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,9 +6,7 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
6
6
 
7
7
  ## Now
8
8
 
9
- - **Resource ids in the singular.** `Resource` names `read`, `create`,
10
- `update` and `delete` after its item, `readPost`, and `list` after the
11
- interface, `listPosts`, instead of `readPosts`.
9
+ Nothing in progress.
12
10
 
13
11
  ## Later
14
12
 
@@ -18,6 +16,22 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
18
16
 
19
17
  ## Shipped
20
18
 
19
+ ### 0.4.0
20
+
21
+ - **Operation ids as written.** `@operationIds` names each operation
22
+ exactly as the spec writes it, `findPostComments`, on an interface, or on a
23
+ namespace to cover every operation in it, instead of
24
+ `<operation><Interface>`. Ids are unique per service document.
25
+ - **No `Resource` template.** A resource's operations are written by hand,
26
+ each with the name it has in the client; `SortParameters` stays, for the
27
+ sort of a list.
28
+
29
+ ### 0.3.0
30
+
31
+ - **Resource ids in the singular.** `Resource` named `read`, `create`,
32
+ `update` and `delete` after its item, `readPost`, and `list` after the
33
+ interface, `listPosts`.
34
+
21
35
  ### 0.2.0
22
36
 
23
37
  - **One reply per status.** An operation that declares a reply without a
@@ -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,37 +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`;
64
- - two interfaces extend `Resource` with one item: `Authors` and
65
- `ArchivedAuthors` both give `readAuthor`, as `Resource` names `read`,
66
- `create`, `update` and `delete` after the item.
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`.
67
68
 
68
69
  ```text
69
70
  error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets: an OpenAPI operation id must be unique.
70
71
  ```
71
72
 
72
- **Why:** `@operationIds` names each operation `<operation><Interface>`
73
- (`<operation><Item>` for `Resource`'s `read`, `create`, `update` and
74
- `delete`), without the namespace. `@typespec/openapi3` would emit both ids without a word, and
75
- 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
+ ```
76
91
 
77
- **Fix:** give one of them its own `@operationId`, for a `Resource` in the
78
- interface that extends it ([Resources](guide/resources.md#two-resources-of-one-item)). An inherited operation takes
79
- 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)):
80
95
 
81
96
  ```tsp
82
97
  import "@typespec/openapi";
@@ -85,7 +100,7 @@ using OpenAPI;
85
100
 
86
101
  @route("/drafts")
87
102
  interface Drafts extends Posts {
88
- @get @operationId("getDraft") read(@path postId: string): Post | NotFound;
103
+ @get @operationId("getDraft") getPost(@path postId: uuid): Post | NotFound;
89
104
  }
90
105
  ```
91
106
 
@@ -95,11 +110,11 @@ interface Drafts extends Posts {
95
110
  one with a body, such as the guard's 401 and the handler's:
96
111
 
97
112
  ```tsp
98
- @get list(): Post[] | AuthenticationRequired | Unauthorized;
113
+ @get listPosts(): Post[] | AuthenticationRequired | Unauthorized;
99
114
  ```
100
115
 
101
116
  ```text
102
- 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.
103
118
  ```
104
119
 
105
120
  **Why:** OpenAPI has one reply per status. `@typespec/openapi3` merges the two
@@ -111,7 +126,7 @@ guard's reply has no body.
111
126
  answers with the envelope, `Unauthorized`:
112
127
 
113
128
  ```tsp
114
- @get list(): Post[] | AuthenticationRequired;
129
+ @get listPosts(): Post[] | AuthenticationRequired;
115
130
  ```
116
131
 
117
132
  ## `declares two replies with a body of status …`
@@ -121,11 +136,11 @@ status code, such as the idempotent write's 409 beside the optimistic
121
136
  lock's, or its 422, `IdempotencyKeyReused`, beside `UnprocessableEntity`:
122
137
 
123
138
  ```tsp
124
- @post create(@body post: Post): Post | IdempotencyInProgress | Conflict;
139
+ @post createPost(@body post: Post): Post | IdempotencyInProgress | Conflict;
125
140
  ```
126
141
 
127
142
  ```text
128
- 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.
129
144
  ```
130
145
 
131
146
  **Why:** `@typespec/openapi3` keeps both bodies, as one reply whose body is
@@ -136,7 +151,7 @@ header either reply declares required becomes optional.
136
151
  `message` key tells them apart:
137
152
 
138
153
  ```tsp
139
- @post create(@body post: Post): Post | IdempotencyInProgress;
154
+ @post createPost(@body post: Post): Post | IdempotencyInProgress;
140
155
  ```
141
156
 
142
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,26 +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);
23
-
24
- namespace Private {
25
- /**
26
- * Names the operation after `item`, `<operation><Item>`, where
27
- * `@operationIds` names it. `Resource`'s alone: not public.
28
- */
29
- extern dec itemOperation(target: Operation, item: Model);
30
- }
27
+ extern dec operationIds(target: Interface | Namespace);