@nxgt/typespec 0.8.0 → 0.9.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.
- package/README.md +40 -0
- package/docs/README.md +1 -0
- package/docs/guide/errors.md +2 -0
- package/docs/guide/getting-started.md +277 -0
- package/docs/roadmap.md +17 -6
- package/docs/troubleshooting.md +61 -0
- package/lib/errors.tsp +23 -0
- package/lib/headers.tsp +0 -23
- 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
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ Each shape here is one the `@nxgt/*` packages already send on the wire.
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
A new project needs no install of its own: `tsp init` with the package's
|
|
12
|
+
template installs everything ([Start a project](#start-a-project)). For an
|
|
13
|
+
existing project, install the library and its peers:
|
|
14
|
+
|
|
11
15
|
```sh
|
|
12
16
|
bun add -d @nxgt/typespec @typespec/compiler @typespec/http @typespec/openapi @typespec/openapi3 @nxgt/openapi-codegen
|
|
13
17
|
```
|
|
@@ -16,6 +20,8 @@ bun add -d @nxgt/typespec @typespec/compiler @typespec/http @typespec/openapi @t
|
|
|
16
20
|
are peer dependencies,
|
|
17
21
|
and `typescript` 6 too, as for every `@nxgt` package. `@typespec/openapi3`
|
|
18
22
|
compiles the spec, and `@nxgt/openapi-codegen` generates the code from it.
|
|
23
|
+
The generator's CLI, `nxgt-openapi`, runs on [Bun](https://bun.sh): it needs
|
|
24
|
+
`bun` on the PATH, even when called through `npx` or an npm script.
|
|
19
25
|
Emit OpenAPI 3.1 or 3.2 in `tspconfig.yaml`: `@typespec/openapi3` emits 3.0
|
|
20
26
|
by default, and the generator refuses it. Every convention here is compiled
|
|
21
27
|
to both in CI, and generates the same code from either.
|
|
@@ -33,6 +39,35 @@ options:
|
|
|
33
39
|
|
|
34
40
|
## Usage
|
|
35
41
|
|
|
42
|
+
### Start a project
|
|
43
|
+
|
|
44
|
+
`tsp init` with the package's template starts an API project with these
|
|
45
|
+
conventions in place. Run it in an empty directory, with
|
|
46
|
+
[Bun](https://bun.sh) on the PATH for the generator:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json \
|
|
50
|
+
--template nxgt -y --project-name petstore
|
|
51
|
+
npx tsp compile .
|
|
52
|
+
npx nxgt-openapi generate
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It installs the compiler, `@typespec/http`, `@typespec/openapi`,
|
|
56
|
+
`@typespec/openapi3`, this package and `@nxgt/openapi-codegen`, and writes:
|
|
57
|
+
|
|
58
|
+
- `tspconfig.yaml`: the linter's recommended rules, and OpenAPI 3.1 written
|
|
59
|
+
to `openapi/openapi.yaml`;
|
|
60
|
+
- `main.tsp`: the service in the namespace `Api`, with `@operationIds`, and a
|
|
61
|
+
`Users` resource with a paged, sorted list, `get`, `create`, `update` and
|
|
62
|
+
`delete`, and their error aliases;
|
|
63
|
+
- `openapi-codegen.config.ts`: the generator's input, and `src/generated`
|
|
64
|
+
with the Hono routes;
|
|
65
|
+
- `README.md`: the `api` and `api:check` scripts to add to `package.json`.
|
|
66
|
+
|
|
67
|
+
Drop `-y` and its options to be asked instead. The next steps, serving the
|
|
68
|
+
routes and renaming the resource, are in
|
|
69
|
+
[Getting started](docs/guide/getting-started.md).
|
|
70
|
+
|
|
36
71
|
### A spec split across files
|
|
37
72
|
|
|
38
73
|
A spec that grows is split by role: one file per resource's models, one per
|
|
@@ -513,6 +548,9 @@ envelope and the rate limit's headers.
|
|
|
513
548
|
|
|
514
549
|
## Traps
|
|
515
550
|
|
|
551
|
+
- **`nxgt-openapi` fails with `env: 'bun': No such file or directory`**
|
|
552
|
+
without Bun on the PATH: install it, `curl -fsSL https://bun.sh/install | bash`
|
|
553
|
+
([troubleshooting](docs/troubleshooting.md#env-bun-no-such-file-or-directory)).
|
|
516
554
|
- **Do not declare a schema named `ValidationErrorBody`.** The generator
|
|
517
555
|
declares it itself, and two schemas of that name fail with
|
|
518
556
|
`name_collision`. `BadRequestBody` carries the `issues` instead.
|
|
@@ -570,6 +608,8 @@ envelope and the rate limit's headers.
|
|
|
570
608
|
|
|
571
609
|
## Documentation
|
|
572
610
|
|
|
611
|
+
- [Getting started](docs/guide/getting-started.md): the `tsp init`
|
|
612
|
+
template, the files it writes, and the steps from the spec to a Hono route;
|
|
573
613
|
- [Error replies](docs/guide/errors.md): the envelope, each response, the
|
|
574
614
|
aliases by verb, and what the generator makes of them;
|
|
575
615
|
- [Pagination](docs/guide/pagination.md): offset and cursor pages, and
|
package/docs/README.md
CHANGED
|
@@ -4,6 +4,7 @@ The [package README](../README.md) is the short version.
|
|
|
4
4
|
|
|
5
5
|
| Page | Read it when |
|
|
6
6
|
| --- | --- |
|
|
7
|
+
| [Getting started](guide/getting-started.md) | starting a new API project from the `tsp init` template, up to a Hono route |
|
|
7
8
|
| [Error replies](guide/errors.md) | declaring what an operation answers when it fails |
|
|
8
9
|
| [Pagination](guide/pagination.md) | listing a collection a page at a time |
|
|
9
10
|
| [Authentication](guide/auth.md) | protecting an operation with a `@nxgt/janus` session |
|
package/docs/guide/errors.md
CHANGED
|
@@ -162,7 +162,9 @@ its own way, through `onValidationError`, turns that off with the generator's
|
|
|
162
162
|
| `Forbidden` | 403 | `ForbiddenBody` | authenticated, but not allowed; a `@nxgt/janus-hono` guard sends `AccessDenied` instead, and `janusErrors()` its own `{ code }` body ([Authentication](auth.md)) |
|
|
163
163
|
| `NotFound` | 404 | `NotFoundBody` | nothing is there |
|
|
164
164
|
| `Conflict` | 409 | `ConflictBody` | the request conflicts with the resource's state, such as a stale `version` |
|
|
165
|
+
| `IdempotencyInProgress` | 409 | `ConflictBody` | the first request with this `Idempotency-Key` still runs; with the `RateLimit-*` headers and `Retry-After` ([Headers](headers.md)) |
|
|
165
166
|
| `UnprocessableEntity` | 422 | `UnprocessableEntityBody` | well-formed, but not something the server can act on |
|
|
167
|
+
| `IdempotencyKeyReused` | 422 | `UnprocessableEntityBody` | an `Idempotency-Key` sent again with another body; with the `RateLimit-*` headers ([Headers](headers.md)) |
|
|
166
168
|
| `TooManyRequests` | 429 | `TooManyRequestsBody` | too many requests; with the `RateLimit-*` headers and `Retry-After` ([Headers](headers.md)) |
|
|
167
169
|
| `InternalServerError` | 500 | `InternalServerErrorBody` | the server failed; with `validateResponses`, `@nxgt/openapi-hono` sends it with `errors.response-validation-failed` when a reply breaks the spec |
|
|
168
170
|
| `ErrorResponse<Status>` | any | `ErrorBody<Status>` | any other status |
|
|
@@ -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).
|
package/docs/roadmap.md
CHANGED
|
@@ -6,12 +6,7 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
extends in `tspconfig.yaml`, warns of a list that returns an array
|
|
11
|
-
(`list-returns-page`), a service without `@operationIds`
|
|
12
|
-
(`service-operation-ids`), and an error reply without the envelope
|
|
13
|
-
(`error-body-shape`). Each rule can be turned off, for the spec or for one
|
|
14
|
-
operation.
|
|
9
|
+
Nothing in progress.
|
|
15
10
|
|
|
16
11
|
## Later
|
|
17
12
|
|
|
@@ -25,6 +20,22 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
25
20
|
|
|
26
21
|
## Shipped
|
|
27
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
|
+
|
|
28
39
|
### 0.7.0
|
|
29
40
|
|
|
30
41
|
- **Error aliases by verb.** `ListErrors`, `GetErrors`, `CreateErrors`,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -27,6 +27,67 @@ no `package.json` to resolve it from.
|
|
|
27
27
|
devDependencies, with the command in the
|
|
28
28
|
[README's Install section](../README.md#install).
|
|
29
29
|
|
|
30
|
+
## `init-template-invalid-json: Unable to parse …/scaffolding.json: Unexpected token … is not valid JSON`
|
|
31
|
+
|
|
32
|
+
**When:** `tsp init` with the package's template, before it writes anything.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
error init-template-invalid-json: Unable to parse https://unpkg.com/@nxgt/typespec@0.8.0/templates/scaffolding.json: Unexpected token 'N', "Not found:"... is not valid JSON. Check that the template URL is correct.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Why:** the URL names no template, so unpkg answers a page of text, `Not
|
|
39
|
+
found: …` or `Package version not found: …`, which `tsp init` cannot parse.
|
|
40
|
+
The template ships from 0.9.0 on: a version before it in the URL, a version
|
|
41
|
+
that is not published, or a typo in the path all answer that page.
|
|
42
|
+
|
|
43
|
+
**Fix:** drop the version, or name 0.9.0 or later, and keep the path as it is:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm view @nxgt/typespec version # 0.9.0 or later
|
|
47
|
+
npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## `init-template-download-failed: Failed to download template from …: fetch failed`
|
|
51
|
+
|
|
52
|
+
**When:** `tsp init` with the package's template, before it writes anything.
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
error init-template-download-failed: Failed to download template from https://unpkg.com/@nxgt/typespec/templates/scaffolding.json: fetch failed. Check that the template URL is correct.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Why:** `tsp init` could not reach the host: no network, a firewall or
|
|
59
|
+
proxy in the way, or a host name mistyped. Nothing answered, unlike the entry
|
|
60
|
+
above, where unpkg answers a page that is not the template.
|
|
61
|
+
|
|
62
|
+
**Fix:** check that the host answers from that machine, then run `tsp init`
|
|
63
|
+
again:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
curl -fsSI https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## `env: 'bun': No such file or directory`
|
|
70
|
+
|
|
71
|
+
**When:** `nxgt-openapi generate` runs, through `npx`, an npm script such as
|
|
72
|
+
the template's `api`, or a CI step, on a machine without Bun. A shell in
|
|
73
|
+
another locale quotes it `‘bun’`; macOS prints `env: bun: No such file or
|
|
74
|
+
directory`. The exit code is 127.
|
|
75
|
+
|
|
76
|
+
**Why:** `@nxgt/openapi-codegen`'s CLI, `nxgt-openapi`, runs on
|
|
77
|
+
[Bun](https://bun.sh): its first line is `#!/usr/bin/env bun`. `npx` and
|
|
78
|
+
`npm run` find the script, then the system looks for `bun` to run it.
|
|
79
|
+
`tsp compile` runs on Node and is not affected.
|
|
80
|
+
|
|
81
|
+
**Fix:** install Bun, and make sure `bun` is on the PATH of the shell or the
|
|
82
|
+
CI job that runs the generator:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
curl -fsSL https://bun.sh/install | bash
|
|
86
|
+
bunx nxgt-openapi generate
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
In GitHub Actions, add `oven-sh/setup-bun@v2` before the step.
|
|
90
|
+
|
|
30
91
|
## `Duplicate type name: 'NotFoundBody'`
|
|
31
92
|
|
|
32
93
|
**When:** `tsp compile` runs on a spec that declares its own model named like
|
package/lib/errors.tsp
CHANGED
|
@@ -130,6 +130,29 @@ model TooManyRequests {
|
|
|
130
130
|
@body body: TooManyRequestsBody;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* A request with this `Idempotency-Key` is still running. Retry it as it is,
|
|
135
|
+
* after `Retry-After`.
|
|
136
|
+
*/
|
|
137
|
+
@error
|
|
138
|
+
model IdempotencyInProgress {
|
|
139
|
+
@statusCode _: 409;
|
|
140
|
+
...RateLimitHeaders;
|
|
141
|
+
...RetryAfterHeader;
|
|
142
|
+
@body body: ConflictBody;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This `Idempotency-Key` was already used for a request with another body. A
|
|
147
|
+
* new request takes a new key.
|
|
148
|
+
*/
|
|
149
|
+
@error
|
|
150
|
+
model IdempotencyKeyReused {
|
|
151
|
+
@statusCode _: 422;
|
|
152
|
+
...RateLimitHeaders;
|
|
153
|
+
@body body: UnprocessableEntityBody;
|
|
154
|
+
}
|
|
155
|
+
|
|
133
156
|
/**
|
|
134
157
|
* The server failed. With `validateResponses`, `@nxgt/openapi-hono` answers
|
|
135
158
|
* it too when a reply breaks the spec, with `errors.response-validation-failed`.
|
package/lib/headers.tsp
CHANGED
|
@@ -51,26 +51,3 @@ model RetryAfterHeader {
|
|
|
51
51
|
@header("Retry-After")
|
|
52
52
|
retryAfter?: integer;
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* A request with this `Idempotency-Key` is still running. Retry it as it is,
|
|
57
|
-
* after `Retry-After`.
|
|
58
|
-
*/
|
|
59
|
-
@error
|
|
60
|
-
model IdempotencyInProgress {
|
|
61
|
-
@statusCode _: 409;
|
|
62
|
-
...RateLimitHeaders;
|
|
63
|
-
...RetryAfterHeader;
|
|
64
|
-
@body body: ConflictBody;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* This `Idempotency-Key` was already used for a request with another body. A
|
|
69
|
-
* new request takes a new key.
|
|
70
|
-
*/
|
|
71
|
-
@error
|
|
72
|
-
model IdempotencyKeyReused {
|
|
73
|
-
@statusCode _: 422;
|
|
74
|
-
...RateLimitHeaders;
|
|
75
|
-
@body body: UnprocessableEntityBody;
|
|
76
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/typespec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"lib",
|
|
12
|
+
"templates",
|
|
12
13
|
"docs",
|
|
13
14
|
"README.md",
|
|
14
15
|
"package.json",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
The API is written in TypeSpec, in `main.tsp`, with the
|
|
4
|
+
[`@nxgt/typespec`](https://www.npmjs.com/package/@nxgt/typespec) conventions.
|
|
5
|
+
|
|
6
|
+
Compile it to `openapi/openapi.yaml`, then generate the types, validators
|
|
7
|
+
and routes into `src/generated`. The generator, `nxgt-openapi`, runs on
|
|
8
|
+
[Bun](https://bun.sh): it needs `bun` on the PATH, even through `npx` or an
|
|
9
|
+
npm script.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx tsp compile .
|
|
13
|
+
npx nxgt-openapi generate
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Chain both in `package.json`, so the code never lags the spec:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"scripts": {
|
|
21
|
+
"api": "tsp compile . && nxgt-openapi generate",
|
|
22
|
+
"api:check": "tsp compile . && nxgt-openapi generate --check"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`tspconfig.yaml` turns on the linter's recommended rules, which warn of a
|
|
28
|
+
list without a page, a service without `@operationIds`, and an error reply
|
|
29
|
+
without the envelope.
|
|
30
|
+
|
|
31
|
+
The next steps, serving the routes with Hono and renaming the `Users`
|
|
32
|
+
resource, are in
|
|
33
|
+
[Getting started](https://github.com/softistx/nxgt-http/blob/develop/packages/typespec/docs/guide/getting-started.md).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The API's spec. `tsp compile .` writes openapi/openapi.yaml, and
|
|
2
|
+
// `nxgt-openapi generate` the types, validators and routes from it.
|
|
3
|
+
import "@typespec/http";
|
|
4
|
+
import "@nxgt/typespec";
|
|
5
|
+
|
|
6
|
+
using Http;
|
|
7
|
+
using Nxgt;
|
|
8
|
+
|
|
9
|
+
/** The {{name}} API. */
|
|
10
|
+
@service(#{ title: "{{name}}" })
|
|
11
|
+
@operationIds
|
|
12
|
+
namespace Api;
|
|
13
|
+
|
|
14
|
+
/** Someone who uses the API. */
|
|
15
|
+
model User {
|
|
16
|
+
@visibility(Lifecycle.Read)
|
|
17
|
+
id: uuid;
|
|
18
|
+
|
|
19
|
+
@minLength(1)
|
|
20
|
+
@maxLength(80)
|
|
21
|
+
name: string;
|
|
22
|
+
|
|
23
|
+
email: email;
|
|
24
|
+
|
|
25
|
+
...Timestamps;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What the user list filters on. */
|
|
29
|
+
model UserFilters {
|
|
30
|
+
/** Users of this name. */
|
|
31
|
+
@query
|
|
32
|
+
name?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Verbs @operationIds completes: list is listUsers, get is getUser.
|
|
36
|
+
@route("/users")
|
|
37
|
+
@tag("Users")
|
|
38
|
+
interface Users {
|
|
39
|
+
/** One page of the users, filtered and sorted. */
|
|
40
|
+
@get
|
|
41
|
+
list(
|
|
42
|
+
...UserFilters,
|
|
43
|
+
...PageParameters,
|
|
44
|
+
...SortParameters<"name" | "createdAt">,
|
|
45
|
+
): Page<User> | ListErrors;
|
|
46
|
+
|
|
47
|
+
/** One user. */
|
|
48
|
+
@get
|
|
49
|
+
get(@path id: uuid): User | GetErrors;
|
|
50
|
+
|
|
51
|
+
/** Creates a user. */
|
|
52
|
+
@post
|
|
53
|
+
create(@body user: Create<User>): {
|
|
54
|
+
@statusCode _: 201;
|
|
55
|
+
@body user: User;
|
|
56
|
+
} | CreateErrors;
|
|
57
|
+
|
|
58
|
+
/** Updates a user with a merge patch. */
|
|
59
|
+
@patch
|
|
60
|
+
update(
|
|
61
|
+
@path id: uuid,
|
|
62
|
+
@body user: MergePatchUpdate<User>,
|
|
63
|
+
): User | UpdateErrors;
|
|
64
|
+
|
|
65
|
+
/** Deletes a user. */
|
|
66
|
+
@delete
|
|
67
|
+
delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
68
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nxgt": {
|
|
3
|
+
"title": "nxgt REST API",
|
|
4
|
+
"description": "A REST API with the @nxgt conventions: ids as written, pages, the error envelope and the linter, generated with @nxgt/openapi-codegen.",
|
|
5
|
+
"compilerVersion": "1.16.0",
|
|
6
|
+
"libraries": [
|
|
7
|
+
"@typespec/http",
|
|
8
|
+
"@typespec/openapi",
|
|
9
|
+
"@nxgt/typespec",
|
|
10
|
+
"@nxgt/openapi-codegen"
|
|
11
|
+
],
|
|
12
|
+
"config": {
|
|
13
|
+
"linter": {
|
|
14
|
+
"extends": ["@nxgt/typespec/recommended"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"emitters": {
|
|
18
|
+
"@typespec/openapi3": {
|
|
19
|
+
"selected": true,
|
|
20
|
+
"label": "OpenAPI 3.1 document",
|
|
21
|
+
"options": {
|
|
22
|
+
"emitter-output-dir": "{project-root}/openapi",
|
|
23
|
+
"openapi-versions": ["3.1.0"],
|
|
24
|
+
"output-file": "openapi.yaml"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
{ "path": "nxgt/main.tsp", "destination": "main.tsp" },
|
|
30
|
+
{
|
|
31
|
+
"path": "nxgt/openapi-codegen.config.ts",
|
|
32
|
+
"destination": "openapi-codegen.config.ts"
|
|
33
|
+
},
|
|
34
|
+
{ "path": "nxgt/README.md", "destination": "README.md" }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|