@nxgt/typespec 0.7.0 → 0.9.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 +81 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +123 -1
- package/dist/index.js.map +10 -5
- package/dist/linter.d.ts +2 -0
- package/dist/linter.d.ts.map +1 -0
- package/dist/operation-ids.d.ts +9 -0
- package/dist/operation-ids.d.ts.map +1 -1
- package/dist/rules/error-body-shape.d.ts +4 -0
- package/dist/rules/error-body-shape.d.ts.map +1 -0
- package/dist/rules/list-returns-page.d.ts +4 -0
- package/dist/rules/list-returns-page.d.ts.map +1 -0
- package/dist/rules/service-operation-ids.d.ts +4 -0
- package/dist/rules/service-operation-ids.d.ts.map +1 -0
- package/dist/rules/written.d.ts +8 -0
- package/dist/rules/written.d.ts.map +1 -0
- package/docs/README.md +2 -0
- package/docs/guide/getting-started.md +277 -0
- package/docs/guide/linter.md +257 -0
- package/docs/roadmap.md +24 -4
- package/docs/troubleshooting.md +194 -0
- package/package.json +2 -1
- package/templates/nxgt/README.md +33 -0
- package/templates/nxgt/main.tsp +68 -0
- package/templates/nxgt/openapi-codegen.config.ts +7 -0
- package/templates/scaffolding.json +37 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
This page starts a new API project from the package's `tsp init` template,
|
|
4
|
+
then takes it from the spec to a running Hono route.
|
|
5
|
+
|
|
6
|
+
## Start a project
|
|
7
|
+
|
|
8
|
+
You need Node.js for `tsp`, and [Bun](https://bun.sh) on the PATH for the
|
|
9
|
+
generator, `nxgt-openapi`, which runs on Bun
|
|
10
|
+
([Compile and generate](#compile-and-generate)).
|
|
11
|
+
|
|
12
|
+
Run `tsp init` with the template, in an empty directory:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
mkdir petstore && cd petstore
|
|
16
|
+
npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`tsp init` asks for the template, the project's name and the emitters. To
|
|
20
|
+
answer them on the command line instead:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json \
|
|
24
|
+
--template nxgt -y --project-name petstore
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`--package=@typespec/compiler` matters: in a directory without the compiler,
|
|
28
|
+
a bare `npx tsp` runs `tsp`, an unrelated npm package. `bunx
|
|
29
|
+
--package=@typespec/compiler tsp init …` works too.
|
|
30
|
+
|
|
31
|
+
`tsp init` writes into the current directory, then runs `npm install`, so it
|
|
32
|
+
needs the network. The template ships from 0.9.0 on; an older version, or no
|
|
33
|
+
network, fails before anything is written
|
|
34
|
+
([troubleshooting](../troubleshooting.md#init-template-invalid-json-unable-to-parse-scaffoldingjson-unexpected-token--is-not-valid-json)).
|
|
35
|
+
|
|
36
|
+
## What it writes
|
|
37
|
+
|
|
38
|
+
| File | What it holds |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `package.json` | the compiler, `@typespec/http`, `@typespec/openapi`, `@typespec/openapi3`, `@nxgt/typespec` and `@nxgt/openapi-codegen`, at their latest versions |
|
|
41
|
+
| `tspconfig.yaml` | the linter's recommended rules, and OpenAPI 3.1 written to `openapi/openapi.yaml` |
|
|
42
|
+
| `main.tsp` | the service, marked with `@operationIds`, and a `Users` resource |
|
|
43
|
+
| `openapi-codegen.config.ts` | the generator's input and output, with the Hono routes on |
|
|
44
|
+
| `README.md` | the commands below, and the scripts to add |
|
|
45
|
+
| `.gitignore` | `node_modules/`, and TypeSpec's default output |
|
|
46
|
+
|
|
47
|
+
`tspconfig.yaml` turns the linter on and emits OpenAPI 3.1, which
|
|
48
|
+
`@nxgt/openapi-codegen` reads (it refuses 3.0, the emitter's default):
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# tspconfig.yaml
|
|
52
|
+
linter:
|
|
53
|
+
extends:
|
|
54
|
+
- "@nxgt/typespec/recommended"
|
|
55
|
+
emit:
|
|
56
|
+
- "@typespec/openapi3"
|
|
57
|
+
options:
|
|
58
|
+
"@typespec/openapi3":
|
|
59
|
+
emitter-output-dir: "{project-root}/openapi"
|
|
60
|
+
openapi-versions:
|
|
61
|
+
- 3.1.0
|
|
62
|
+
output-file: openapi.yaml
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`main.tsp` declares the service in the namespace `Api`, titled after the
|
|
66
|
+
project, and one resource with each convention in place: a paged, filtered and sorted
|
|
67
|
+
list, and the error aliases of each verb.
|
|
68
|
+
|
|
69
|
+
```tsp
|
|
70
|
+
// main.tsp (excerpt)
|
|
71
|
+
@service(#{ title: "petstore" })
|
|
72
|
+
@operationIds
|
|
73
|
+
namespace Api;
|
|
74
|
+
|
|
75
|
+
model User {
|
|
76
|
+
@visibility(Lifecycle.Read)
|
|
77
|
+
id: uuid;
|
|
78
|
+
|
|
79
|
+
@minLength(1)
|
|
80
|
+
@maxLength(80)
|
|
81
|
+
name: string;
|
|
82
|
+
|
|
83
|
+
email: email;
|
|
84
|
+
|
|
85
|
+
...Timestamps;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@route("/users")
|
|
89
|
+
@tag("Users")
|
|
90
|
+
interface Users {
|
|
91
|
+
@get
|
|
92
|
+
list(
|
|
93
|
+
...UserFilters,
|
|
94
|
+
...PageParameters,
|
|
95
|
+
...SortParameters<"name" | "createdAt">,
|
|
96
|
+
): Page<User> | ListErrors;
|
|
97
|
+
|
|
98
|
+
@get
|
|
99
|
+
get(@path id: uuid): User | GetErrors;
|
|
100
|
+
|
|
101
|
+
@post
|
|
102
|
+
create(@body user: Create<User>): {
|
|
103
|
+
@statusCode _: 201;
|
|
104
|
+
@body user: User;
|
|
105
|
+
} | CreateErrors;
|
|
106
|
+
|
|
107
|
+
@patch
|
|
108
|
+
update(@path id: uuid, @body user: MergePatchUpdate<User>): User | UpdateErrors;
|
|
109
|
+
|
|
110
|
+
@delete
|
|
111
|
+
delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`@operationIds` names the operations `listUsers`, `getUser`, `createUser`,
|
|
116
|
+
`updateUser` and `deleteUser` ([Operation ids](operation-ids.md)).
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// openapi-codegen.config.ts
|
|
120
|
+
import { defineConfig } from '@nxgt/openapi-codegen';
|
|
121
|
+
|
|
122
|
+
export default defineConfig({
|
|
123
|
+
input: 'openapi/openapi.yaml',
|
|
124
|
+
output: 'src/generated',
|
|
125
|
+
hono: true,
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Compile and generate
|
|
130
|
+
|
|
131
|
+
Compile the spec to `openapi/openapi.yaml`, then generate the code from it.
|
|
132
|
+
`nxgt-openapi` runs on Bun: it needs `bun` on the PATH, even through `npx` or
|
|
133
|
+
an npm script, and fails with `env: 'bun': No such file or directory`
|
|
134
|
+
without it ([troubleshooting](../troubleshooting.md#env-bun-no-such-file-or-directory)).
|
|
135
|
+
`bunx nxgt-openapi generate` does the same as the `npx` line.
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npx tsp compile .
|
|
139
|
+
npx nxgt-openapi generate
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
openapi/openapi.yaml → src/generated: 5 written, 0 unchanged
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`src/generated` then holds `types.ts`, `zod.ts`, `operations.ts`, `paths.ts`
|
|
147
|
+
and `hono.ts`. The linter runs in `tsp compile`; the template's spec passes
|
|
148
|
+
it.
|
|
149
|
+
|
|
150
|
+
## Add the scripts
|
|
151
|
+
|
|
152
|
+
`tsp init` writes its own `package.json`, so the template cannot add
|
|
153
|
+
scripts. Add these two, so the code never lags the spec:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"scripts": {
|
|
158
|
+
"api": "tsp compile . && nxgt-openapi generate",
|
|
159
|
+
"api:check": "tsp compile . && nxgt-openapi generate --check"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`api:check` exits with 1 and lists the files when the generated code is out
|
|
165
|
+
of date: run it in CI.
|
|
166
|
+
|
|
167
|
+
## Serve it
|
|
168
|
+
|
|
169
|
+
The generated `hono.ts` binds the routes to the spec through
|
|
170
|
+
[`@nxgt/openapi-hono`](https://www.npmjs.com/package/@nxgt/openapi-hono).
|
|
171
|
+
Install it, Hono, and zod, which the generated validators import, with the
|
|
172
|
+
package manager `tsp init` used:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
npm install @nxgt/openapi-hono hono zod
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Then register a handler per operation. Each one reads its validated input,
|
|
179
|
+
and may only answer the replies the spec declares:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
// src/app.ts
|
|
183
|
+
import { Hono } from 'hono';
|
|
184
|
+
import { createRoutes } from './generated/hono';
|
|
185
|
+
import type { NotFoundBody } from './generated/types';
|
|
186
|
+
|
|
187
|
+
const app = new Hono();
|
|
188
|
+
const routes = createRoutes(app);
|
|
189
|
+
|
|
190
|
+
routes.operation('getUser', async (c) => {
|
|
191
|
+
const { id } = c.req.valid('param');
|
|
192
|
+
const user = await users.find(id);
|
|
193
|
+
if (!user) {
|
|
194
|
+
const body: NotFoundBody = {
|
|
195
|
+
status: 404,
|
|
196
|
+
message: 'errors.not-found',
|
|
197
|
+
timestamp: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
return c.json(body, 404);
|
|
200
|
+
}
|
|
201
|
+
return c.json(user, 200);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
routes.operation('listUsers', async (c) => {
|
|
205
|
+
const { name, page = 1, pageSize = 20, orderBy, direction } = c.req.valid('query');
|
|
206
|
+
return c.json(await users.page({ name, page, pageSize, orderBy, direction }), 200);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
export default app;
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`users` stands for your data layer. Annotate an error body with its generated
|
|
213
|
+
type, as `NotFoundBody` above: an object literal passed straight to
|
|
214
|
+
`c.json(…, 404)` is inferred beside the handler's other returns, its fields
|
|
215
|
+
turn optional, and the handler fails with `TS2769: No overload matches this
|
|
216
|
+
call`. A `418`,
|
|
217
|
+
or a 404 without `timestamp`, does not compile either. More on the routes in the
|
|
218
|
+
[`@nxgt/openapi-hono` README](https://www.npmjs.com/package/@nxgt/openapi-hono),
|
|
219
|
+
and on serving a page in [Pagination](pagination.md#serving-it).
|
|
220
|
+
|
|
221
|
+
## Rename the resource
|
|
222
|
+
|
|
223
|
+
`Users` is an example. To make it your own resource, rename the model, its
|
|
224
|
+
filters, the interface, the route and the tag in `main.tsp`, and the body
|
|
225
|
+
parameters with them:
|
|
226
|
+
|
|
227
|
+
```tsp
|
|
228
|
+
model Pet {
|
|
229
|
+
@visibility(Lifecycle.Read)
|
|
230
|
+
id: uuid;
|
|
231
|
+
|
|
232
|
+
@minLength(1)
|
|
233
|
+
name: string;
|
|
234
|
+
|
|
235
|
+
...Timestamps;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
model PetFilters {
|
|
239
|
+
@query
|
|
240
|
+
name?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@route("/pets")
|
|
244
|
+
@tag("Pets")
|
|
245
|
+
interface Pets {
|
|
246
|
+
@get
|
|
247
|
+
list(...PetFilters, ...PageParameters, ...SortParameters<"name" | "createdAt">): Page<Pet> | ListErrors;
|
|
248
|
+
|
|
249
|
+
@get
|
|
250
|
+
get(@path id: uuid): Pet | GetErrors;
|
|
251
|
+
|
|
252
|
+
@post
|
|
253
|
+
create(@body pet: Create<Pet>): {
|
|
254
|
+
@statusCode _: 201;
|
|
255
|
+
@body pet: Pet;
|
|
256
|
+
} | CreateErrors;
|
|
257
|
+
|
|
258
|
+
@patch
|
|
259
|
+
update(@path id: uuid, @body pet: MergePatchUpdate<Pet>): Pet | UpdateErrors;
|
|
260
|
+
|
|
261
|
+
@delete
|
|
262
|
+
delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The operation names stay; `@operationIds` takes the resource from the
|
|
267
|
+
interface, so the ids become `listPets`, `getPet`, `createPet`, `updatePet`
|
|
268
|
+
and `deletePet`. Run `tsp compile .` and `nxgt-openapi generate` again, and
|
|
269
|
+
the generated types, `Pet` and `PetPage`, follow. A name the singular rule
|
|
270
|
+
does not know takes `@operationIds(#{ singular: "…" })` on the interface
|
|
271
|
+
([Operation ids](operation-ids.md)).
|
|
272
|
+
|
|
273
|
+
A second resource is a second model and interface, in the same namespace or
|
|
274
|
+
in files of their own ([README](../README.md#a-spec-split-across-files)).
|
|
275
|
+
|
|
276
|
+
Next: what each error alias holds, in [Error replies](errors.md), and the
|
|
277
|
+
rules the linter checks, in [Linter](linter.md).
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Linter
|
|
2
|
+
|
|
3
|
+
The linter warns when a spec strays from the conventions the library gives
|
|
4
|
+
it: a list that returns an array, a service without `@operationIds`, an error
|
|
5
|
+
reply without the envelope.
|
|
6
|
+
|
|
7
|
+
## Turn it on
|
|
8
|
+
|
|
9
|
+
Extend the ruleset in `tspconfig.yaml`, beside the emitter:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# api/tspconfig.yaml
|
|
13
|
+
emit:
|
|
14
|
+
- '@typespec/openapi3'
|
|
15
|
+
linter:
|
|
16
|
+
extends:
|
|
17
|
+
- '@nxgt/typespec/recommended'
|
|
18
|
+
options:
|
|
19
|
+
'@typespec/openapi3':
|
|
20
|
+
openapi-versions: ['3.1.0']
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Nothing runs without it: importing `@nxgt/typespec` does not lint the spec.
|
|
24
|
+
Each rule is a warning, so `tsp compile` still emits:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
warning @nxgt/typespec/list-returns-page: listAll returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`tsp compile api --warn-as-error` fails on any of them, for CI.
|
|
31
|
+
|
|
32
|
+
| Rule | Warns when |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| [`list-returns-page`](#list-returns-page) | a `list`, `search` or `query` returns an array |
|
|
35
|
+
| [`service-operation-ids`](#service-operation-ids) | a `@service` namespace has no `@operationIds` |
|
|
36
|
+
| [`error-body-shape`](#error-body-shape) | an error reply's body is not `status`, `message` and `timestamp` |
|
|
37
|
+
|
|
38
|
+
The library's own checks, such as `verb-method-mismatch` and
|
|
39
|
+
`duplicate-operation-id`, run whether or not the linter is on
|
|
40
|
+
([Operation ids](operation-ids.md)).
|
|
41
|
+
|
|
42
|
+
## `list-returns-page`
|
|
43
|
+
|
|
44
|
+
**Checks:** an operation whose name is `list`, `search` or `query`, alone or
|
|
45
|
+
followed by a capital letter (`listUsers`, `searchPosts`), with a success
|
|
46
|
+
reply whose body is an array. `listing` and `queryable` are not lists, and
|
|
47
|
+
are not checked. The name is checked as written, in an interface or not.
|
|
48
|
+
|
|
49
|
+
Only the operations the emitter writes are checked, by this rule and
|
|
50
|
+
`error-body-shape`: a template's declaration, its instance (`Read<Pet>`) and
|
|
51
|
+
the operations of an interface template are not, while an interface that
|
|
52
|
+
extends a template, `interface Users extends Crud<User> {}`, has its copied
|
|
53
|
+
operations checked.
|
|
54
|
+
|
|
55
|
+
**Why:** an array cannot grow a `total` or a `nextCursor` later without
|
|
56
|
+
breaking every client. `Page<Item>` and `CursorPage<Item>` can, and they are
|
|
57
|
+
the pages `@nxgt/drizzle` and `@nxgt/mongo` return.
|
|
58
|
+
|
|
59
|
+
Fails:
|
|
60
|
+
|
|
61
|
+
```tsp
|
|
62
|
+
@route("/users")
|
|
63
|
+
interface Users {
|
|
64
|
+
@get list(): User[];
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
warning @nxgt/typespec/list-returns-page: list returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Passes:
|
|
73
|
+
|
|
74
|
+
```tsp
|
|
75
|
+
@route("/users")
|
|
76
|
+
interface Users {
|
|
77
|
+
@get list(...PageParameters): Page<User>;
|
|
78
|
+
@get @route("/search") search(...CursorPageParameters): CursorPage<User>;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
More on both pages in [Pagination](pagination.md).
|
|
83
|
+
|
|
84
|
+
## `service-operation-ids`
|
|
85
|
+
|
|
86
|
+
**Checks:** a `@service` namespace that is not marked with `@operationIds`,
|
|
87
|
+
neither itself nor a namespace around it.
|
|
88
|
+
|
|
89
|
+
**Why:** without it, the emitter names each operation after its interface,
|
|
90
|
+
`Users_list`, and the generated client's methods and types take that name.
|
|
91
|
+
`@operationIds` gives `listUsers` ([Operation ids](operation-ids.md)).
|
|
92
|
+
|
|
93
|
+
Fails:
|
|
94
|
+
|
|
95
|
+
```tsp
|
|
96
|
+
@service
|
|
97
|
+
namespace Shop {
|
|
98
|
+
@route("/health") @get op health(): void;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
warning @nxgt/typespec/service-operation-ids: The service Shop has no @operationIds: its ids are the emitter's, such as Users_list. Mark the namespace with @operationIds.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Passes, marked on the service or on a namespace around it:
|
|
107
|
+
|
|
108
|
+
```tsp
|
|
109
|
+
@service
|
|
110
|
+
@operationIds
|
|
111
|
+
namespace Shop {
|
|
112
|
+
@route("/health") @get op health(): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@operationIds
|
|
116
|
+
namespace Admin {
|
|
117
|
+
@service
|
|
118
|
+
namespace Billing {
|
|
119
|
+
@route("/invoices") @get op invoices(): void;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`@operationIds` on an interface alone does not count: the rule asks for the
|
|
125
|
+
namespace.
|
|
126
|
+
|
|
127
|
+
## `error-body-shape`
|
|
128
|
+
|
|
129
|
+
**Checks:** each reply of status 400 or above, of a range that starts at 400
|
|
130
|
+
or above (`5XX`), or of no status (`default`, an `@error` model without
|
|
131
|
+
`@statusCode`), that has a body. The body must be a model with a `status`, a
|
|
132
|
+
`message` and a `timestamp` property. A reply without a body passes, such as
|
|
133
|
+
`AuthenticationRequired`, `AccessDenied` or `ErrorWithoutBody<404>`.
|
|
134
|
+
|
|
135
|
+
**Why:** `@nxgt/openapi-hono` answers its own 400 and 500 with that envelope,
|
|
136
|
+
and a client written against it reads `message` from every error. A body of
|
|
137
|
+
another shape is one more case each client must handle.
|
|
138
|
+
|
|
139
|
+
Fails:
|
|
140
|
+
|
|
141
|
+
```tsp
|
|
142
|
+
model Teapot {
|
|
143
|
+
@statusCode _: 418;
|
|
144
|
+
@body body: { reason: string };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@route("/users")
|
|
148
|
+
interface Users {
|
|
149
|
+
@post create(@body user: User): User | Teapot;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
warning @nxgt/typespec/error-body-shape: create answers 418 with a body that is not the nxgt envelope: declare BadRequest, NotFound or another of the library's errors, or a body that spreads or extends ErrorBody<Status>.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A range is named by its bounds, `500-599`, and a reply without a status as
|
|
158
|
+
`*`.
|
|
159
|
+
|
|
160
|
+
Passes: one of the library's errors, `ErrorResponse<Status>`, or a body that
|
|
161
|
+
spreads or extends `ErrorBody<Status>` and adds its own fields
|
|
162
|
+
(`model TeapotBody extends ErrorBody<418> { reason: string; }` passes too):
|
|
163
|
+
|
|
164
|
+
```tsp
|
|
165
|
+
model TeapotBody {
|
|
166
|
+
...ErrorBody<418>;
|
|
167
|
+
reason: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@error
|
|
171
|
+
model Teapot {
|
|
172
|
+
@statusCode _: 418;
|
|
173
|
+
@body body: TeapotBody;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@route("/users")
|
|
177
|
+
interface Users {
|
|
178
|
+
@get @route("/{id}") get(@path id: uuid): User | NotFound | AuthenticationRequired;
|
|
179
|
+
@post create(@body user: User): User | BadRequest | Teapot | ErrorResponse<503>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The rule checks the property names, not their types. Each error is in
|
|
184
|
+
[Error replies](errors.md).
|
|
185
|
+
|
|
186
|
+
## Turn a rule off
|
|
187
|
+
|
|
188
|
+
For the whole spec, in `tspconfig.yaml`, with the reason:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
linter:
|
|
192
|
+
extends:
|
|
193
|
+
- '@nxgt/typespec/recommended'
|
|
194
|
+
disable:
|
|
195
|
+
'@nxgt/typespec/error-body-shape': 'the legacy routes answer their own errors'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For one operation or one namespace, a `#suppress` line above it:
|
|
199
|
+
|
|
200
|
+
```tsp
|
|
201
|
+
@route("/users")
|
|
202
|
+
interface Users {
|
|
203
|
+
#suppress "@nxgt/typespec/list-returns-page" "a fixed set of ten"
|
|
204
|
+
@get @route("/top") listTop(): User[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#suppress "@nxgt/typespec/service-operation-ids" "its clients already call Health_check"
|
|
208
|
+
@service
|
|
209
|
+
namespace Health {
|
|
210
|
+
@route("/health") @get op check(): void;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or turn on only the rules you want, without the set:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
linter:
|
|
218
|
+
enable:
|
|
219
|
+
'@nxgt/typespec/list-returns-page': true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## A whole spec
|
|
223
|
+
|
|
224
|
+
The rules pass on a spec that uses the library throughout:
|
|
225
|
+
|
|
226
|
+
```tsp
|
|
227
|
+
// api/main.tsp
|
|
228
|
+
import "@typespec/http";
|
|
229
|
+
import "@nxgt/typespec";
|
|
230
|
+
|
|
231
|
+
using Http;
|
|
232
|
+
using Nxgt;
|
|
233
|
+
|
|
234
|
+
@service(#{ title: "Shop" })
|
|
235
|
+
@operationIds
|
|
236
|
+
namespace Shop;
|
|
237
|
+
|
|
238
|
+
model User {
|
|
239
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
240
|
+
name: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@route("/users")
|
|
244
|
+
interface Users {
|
|
245
|
+
@get list(...PageParameters): Page<User> | BadRequest; // listUsers
|
|
246
|
+
@get @route("/search") search(...CursorPageParameters): CursorPage<User>; // searchUsers
|
|
247
|
+
@get @route("/{id}") get(@path id: uuid): User | NotFound | AuthenticationRequired; // getUser
|
|
248
|
+
@post create(@body user: User): User | BadRequest | Conflict; // createUser
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
bunx --no-install tsp compile api --warn-as-error
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The messages, with their fixes, are also in
|
|
257
|
+
[troubleshooting](../troubleshooting.md).
|
package/docs/roadmap.md
CHANGED
|
@@ -6,10 +6,7 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
`UpdateErrors`, `DeleteErrors` and `AuthErrors` name the errors each verb
|
|
11
|
-
usually answers, so an operation writes `User | GetErrors` instead of the
|
|
12
|
-
union. Each is exactly its union, and they combine.
|
|
9
|
+
Nothing in progress.
|
|
13
10
|
|
|
14
11
|
## Later
|
|
15
12
|
|
|
@@ -23,6 +20,29 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
23
20
|
|
|
24
21
|
## Shipped
|
|
25
22
|
|
|
23
|
+
### 0.9.0
|
|
24
|
+
|
|
25
|
+
- **A `tsp init` template.** `tsp init` with the package's template
|
|
26
|
+
starts a project with the conventions in place: `@operationIds`, a paged
|
|
27
|
+
resource with its error aliases, the linter on, and the OpenAPI that
|
|
28
|
+
`@nxgt/openapi-codegen` generates from.
|
|
29
|
+
|
|
30
|
+
### 0.8.0
|
|
31
|
+
|
|
32
|
+
- **A linter.** The ruleset `@nxgt/typespec/recommended`, which a spec
|
|
33
|
+
extends in `tspconfig.yaml`, warns of a list that returns an array
|
|
34
|
+
(`list-returns-page`), a service without `@operationIds`
|
|
35
|
+
(`service-operation-ids`), and an error reply without the envelope
|
|
36
|
+
(`error-body-shape`). Each rule can be turned off, for the spec or for one
|
|
37
|
+
operation.
|
|
38
|
+
|
|
39
|
+
### 0.7.0
|
|
40
|
+
|
|
41
|
+
- **Error aliases by verb.** `ListErrors`, `GetErrors`, `CreateErrors`,
|
|
42
|
+
`UpdateErrors`, `DeleteErrors` and `AuthErrors` name the errors each verb
|
|
43
|
+
usually answers, so an operation writes `User | GetErrors` instead of the
|
|
44
|
+
union. Each is exactly its union, and they combine.
|
|
45
|
+
|
|
26
46
|
### 0.6.0
|
|
27
47
|
|
|
28
48
|
- **Verbs name their methods.** An operation named after one of the
|