@nxgt/typespec 0.1.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/LICENSE +21 -0
- package/README.md +458 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +12 -0
- package/dist/lib.d.ts +6 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/operation-ids.d.ts +20 -0
- package/dist/operation-ids.d.ts.map +1 -0
- package/docs/README.md +15 -0
- package/docs/guide/auth.md +103 -0
- package/docs/guide/columns.md +117 -0
- package/docs/guide/errors.md +190 -0
- package/docs/guide/headers.md +96 -0
- package/docs/guide/operation-ids.md +113 -0
- package/docs/guide/pagination.md +138 -0
- package/docs/guide/resources.md +112 -0
- package/docs/roadmap.md +38 -0
- package/docs/troubleshooting.md +109 -0
- package/lib/auth.tsp +43 -0
- package/lib/columns.tsp +50 -0
- package/lib/errors.tsp +148 -0
- package/lib/headers.tsp +76 -0
- package/lib/main.tsp +11 -0
- package/lib/operation-ids.tsp +22 -0
- package/lib/pagination.tsp +62 -0
- package/lib/resources.tsp +63 -0
- package/lib/scalars.tsp +12 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Tsala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# @nxgt/typespec
|
|
2
|
+
|
|
3
|
+
nxgt's HTTP conventions as a [TypeSpec](https://typespec.io) library, so an
|
|
4
|
+
API spec states them in one word instead of rewriting them. Compile the spec to
|
|
5
|
+
OpenAPI 3.1 or 3.2 with `@typespec/openapi3`, then generate the code with
|
|
6
|
+
[`@nxgt/openapi-codegen`](https://github.com/softistx/nxgt-http/blob/develop/packages/openapi-codegen/README.md).
|
|
7
|
+
Each shape here is one the `@nxgt/*` packages already send on the wire.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add -d @nxgt/typespec @typespec/compiler @typespec/http @typespec/openapi @typespec/openapi3 @nxgt/openapi-codegen
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`@typespec/compiler`, `@typespec/http` and `@typespec/openapi` 1.16 or later
|
|
16
|
+
are peer dependencies,
|
|
17
|
+
and `typescript` 6 too, as for every `@nxgt` package. `@typespec/openapi3`
|
|
18
|
+
compiles the spec, and `@nxgt/openapi-codegen` generates the code from it.
|
|
19
|
+
Emit OpenAPI 3.1 or 3.2 in `tspconfig.yaml`: `@typespec/openapi3` emits 3.0
|
|
20
|
+
by default, and the generator refuses it. Every convention here is compiled
|
|
21
|
+
to both in CI, and generates the same code from either.
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
# api/tspconfig.yaml
|
|
25
|
+
emit:
|
|
26
|
+
- '@typespec/openapi3'
|
|
27
|
+
options:
|
|
28
|
+
'@typespec/openapi3':
|
|
29
|
+
openapi-versions: ['3.1.0']
|
|
30
|
+
emitter-output-dir: '{project-root}/../openapi'
|
|
31
|
+
output-file: openapi.yaml
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### A spec split across files
|
|
37
|
+
|
|
38
|
+
A spec that grows is split by role: one file per resource's models, one per
|
|
39
|
+
resource's routes, and a `main.tsp` that only assembles them. Each file opens
|
|
40
|
+
the service's namespace again:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
api/
|
|
44
|
+
main.tsp imports every file below, and declares the service
|
|
45
|
+
tspconfig.yaml
|
|
46
|
+
models/
|
|
47
|
+
author.tsp post.tsp comment.tsp
|
|
48
|
+
routes/
|
|
49
|
+
authors.tsp posts.tsp comments.tsp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```tsp
|
|
53
|
+
// api/main.tsp
|
|
54
|
+
import "@typespec/http";
|
|
55
|
+
import "./models/post.tsp";
|
|
56
|
+
import "./routes/posts.tsp";
|
|
57
|
+
|
|
58
|
+
using Http;
|
|
59
|
+
|
|
60
|
+
@service(#{ title: "Blog" })
|
|
61
|
+
namespace Blog;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```tsp
|
|
65
|
+
// api/models/post.tsp
|
|
66
|
+
namespace Blog;
|
|
67
|
+
|
|
68
|
+
model Post {
|
|
69
|
+
@visibility(Lifecycle.Read) id: string;
|
|
70
|
+
@minLength(1) title: string;
|
|
71
|
+
body: string;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Error replies
|
|
76
|
+
|
|
77
|
+
Import the library where the routes are, and name the errors each operation
|
|
78
|
+
can answer:
|
|
79
|
+
|
|
80
|
+
```tsp
|
|
81
|
+
// api/routes/posts.tsp
|
|
82
|
+
import "@typespec/http";
|
|
83
|
+
import "@nxgt/typespec";
|
|
84
|
+
|
|
85
|
+
using Http;
|
|
86
|
+
using Nxgt;
|
|
87
|
+
|
|
88
|
+
namespace Blog;
|
|
89
|
+
|
|
90
|
+
@route("/posts")
|
|
91
|
+
interface Posts {
|
|
92
|
+
@get read(@path postId: string): Post | NotFound | Unauthorized;
|
|
93
|
+
@post create(@body post: Create<Post>): {
|
|
94
|
+
@statusCode _: 201;
|
|
95
|
+
@body post: Post;
|
|
96
|
+
} | BadRequest | Conflict | ErrorResponse<503>;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
bunx --no-install tsp compile api && bunx nxgt-openapi generate -i openapi/openapi.yaml -o src/generated
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Every error body has the envelope of `@nxgt/openapi-hono`'s own replies, its
|
|
105
|
+
400 and its 500, and a handler answers with the same envelope: `status`,
|
|
106
|
+
`message`, an i18n key rather than a sentence, and `timestamp`. The generated
|
|
107
|
+
types require all three:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// src/generated/types.ts (excerpt)
|
|
111
|
+
export interface NotFoundBody {
|
|
112
|
+
status: 404;
|
|
113
|
+
message: string;
|
|
114
|
+
timestamp: string;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Response | Status | Body |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| `BadRequest` | 400 | `BadRequestBody`: the envelope, and `issues` when the validators refused the request |
|
|
121
|
+
| `Unauthorized` | 401 | `UnauthorizedBody` |
|
|
122
|
+
| `Forbidden` | 403 | `ForbiddenBody` |
|
|
123
|
+
| `NotFound` | 404 | `NotFoundBody` |
|
|
124
|
+
| `Conflict` | 409 | `ConflictBody` |
|
|
125
|
+
| `UnprocessableEntity` | 422 | `UnprocessableEntityBody` |
|
|
126
|
+
| `TooManyRequests` | 429 | `TooManyRequestsBody`, with the `RateLimit-*` headers and `Retry-After` |
|
|
127
|
+
| `InternalServerError` | 500 | `InternalServerErrorBody` |
|
|
128
|
+
| `ErrorResponse<Status>` | any | `ErrorBody<Status>` |
|
|
129
|
+
|
|
130
|
+
The generator also declares the validators' own 400, `ValidationErrorBody`,
|
|
131
|
+
on every operation that takes a parameter or a body. With `BadRequest`, a
|
|
132
|
+
400 reply is typed `BadRequestBody | ValidationErrorBody`.
|
|
133
|
+
|
|
134
|
+
### Pagination
|
|
135
|
+
|
|
136
|
+
Spread the query of a page into the operation's parameters, and answer the
|
|
137
|
+
page:
|
|
138
|
+
|
|
139
|
+
```tsp
|
|
140
|
+
@route("/posts")
|
|
141
|
+
interface Posts {
|
|
142
|
+
@get list(@query status?: PostStatus, ...PageParameters): Page<Post>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@route("/posts/{postId}/comments")
|
|
146
|
+
interface Comments {
|
|
147
|
+
@get list(@path postId: string, ...CursorPageParameters): CursorPage<Comment> | BadRequest | NotFound;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The shapes are the ones
|
|
152
|
+
[`@nxgt/drizzle`](https://www.npmjs.com/package/@nxgt/drizzle) and
|
|
153
|
+
[`@nxgt/mongo`](https://www.npmjs.com/package/@nxgt/mongo) return, so a
|
|
154
|
+
handler answers their page as it is:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// src/generated/types.ts (excerpt)
|
|
158
|
+
export interface PostPage {
|
|
159
|
+
items: Post[];
|
|
160
|
+
total: number;
|
|
161
|
+
page: number;
|
|
162
|
+
pageSize: number;
|
|
163
|
+
pageCount: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface CommentCursorPage {
|
|
167
|
+
items: Comment[];
|
|
168
|
+
nextCursor: string | null;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| Query | Default | Checked |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| `page` | 1 | an integer, at least 1 |
|
|
175
|
+
| `pageSize` | 20 | an integer, at least 1 |
|
|
176
|
+
| `after` | none: the first page | a string, the previous page's `nextCursor` |
|
|
177
|
+
| `limit` | 20 | an integer, at least 1 |
|
|
178
|
+
|
|
179
|
+
A `pageSize` or `limit` above the server's maximum is not refused: the
|
|
180
|
+
server lowers it, as `@nxgt/drizzle` does. A cursor the server did not write
|
|
181
|
+
makes `@nxgt/drizzle` throw `InvalidCursorError`: answer it with the
|
|
182
|
+
`BadRequest` declared above
|
|
183
|
+
([Serving it](docs/guide/pagination.md#serving-it)).
|
|
184
|
+
|
|
185
|
+
### Authentication
|
|
186
|
+
|
|
187
|
+
Name how a request authenticates with `@useAuth`, and the guards' refusals
|
|
188
|
+
beside the success:
|
|
189
|
+
|
|
190
|
+
```tsp
|
|
191
|
+
@route("/posts/{postId}/comments/{commentId}")
|
|
192
|
+
interface Comments {
|
|
193
|
+
@useAuth(JanusAuth)
|
|
194
|
+
@delete delete(@path postId: string, @path commentId: string):
|
|
195
|
+
NoContentResponse | AuthenticationRequired | AccessDenied | ErrorWithoutBody<404>;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`JanusAuth` is every way [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus)
|
|
200
|
+
reads a session token, and any one of them is enough:
|
|
201
|
+
|
|
202
|
+
| Scheme | Where the token is |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `BearerAuth` | `Authorization: Bearer <token>` |
|
|
205
|
+
| `SessionTokenAuth` | the `X-Session-Token` header |
|
|
206
|
+
| `SessionCookieAuth` | the `janus-session` cookie |
|
|
207
|
+
|
|
208
|
+
The guards of [`@nxgt/janus-hono`](https://www.npmjs.com/package/@nxgt/janus-hono),
|
|
209
|
+
`session(auth, { required: true })` and `permission()`, refuse without a body:
|
|
210
|
+
`AuthenticationRequired` is their 401 and `AccessDenied` their 403, and
|
|
211
|
+
`ErrorWithoutBody<404>` the 404 of `permission()`. A handler's own refusal,
|
|
212
|
+
with the envelope, stays `Unauthorized` or `Forbidden`. The generated
|
|
213
|
+
`hono.ts` types the guards' replies as `c.body(null, 401)`. More in
|
|
214
|
+
[Authentication](docs/guide/auth.md).
|
|
215
|
+
|
|
216
|
+
### Scalars and columns
|
|
217
|
+
|
|
218
|
+
Type an id or an address with a scalar, and spread the columns
|
|
219
|
+
[`@nxgt/drizzle`](https://www.npmjs.com/package/@nxgt/drizzle) stamps a row
|
|
220
|
+
with:
|
|
221
|
+
|
|
222
|
+
```tsp
|
|
223
|
+
import "@nxgt/typespec";
|
|
224
|
+
|
|
225
|
+
using Nxgt;
|
|
226
|
+
|
|
227
|
+
namespace Blog;
|
|
228
|
+
|
|
229
|
+
model Post {
|
|
230
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
231
|
+
title: string;
|
|
232
|
+
contact?: email;
|
|
233
|
+
...Timestamps; // createdAt, updatedAt: read-only
|
|
234
|
+
...Versioned; // version: read, and sent back in an update
|
|
235
|
+
...Actors; // createdBy, updatedBy, deletedBy: read-only
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
| Name | Emitted as | Generated | drizzle |
|
|
240
|
+
| --- | --- | --- | --- |
|
|
241
|
+
| `uuid` | the schema `Uuid`, `format: uuid` | `Uuid`, `z.guid()` | `id()` |
|
|
242
|
+
| `email` | the schema `Email`, `format: email` | `Email`, `z.email()` | |
|
|
243
|
+
| `...Timestamps` | `createdAt`, `updatedAt`, read-only | left out of `Create<T>` and updates | `timestamps()` |
|
|
244
|
+
| `...SoftDelete` | `deletedAt: utcDateTime \| null`, read-only | | `softDelete()` |
|
|
245
|
+
| `...Versioned` | `version: integer`, read and update | kept in `MergePatchUpdate<T>` | `version()` |
|
|
246
|
+
| `...Actors<Id = uuid>` | `createdBy`, `updatedBy`, `deletedBy`, `Id \| null`, read-only | | `actors()` |
|
|
247
|
+
|
|
248
|
+
TypeSpec's own `url` is the address of a page, `format: uri`, generated as
|
|
249
|
+
`z.url()`. More in [Scalars and columns](docs/guide/columns.md).
|
|
250
|
+
|
|
251
|
+
### Headers
|
|
252
|
+
|
|
253
|
+
Spread the headers of an idempotent write and of a rate limit into an
|
|
254
|
+
operation and its replies:
|
|
255
|
+
|
|
256
|
+
```tsp
|
|
257
|
+
@post create(...IdempotencyKeyHeader, @body post: Create<Post>): {
|
|
258
|
+
@statusCode _: 201;
|
|
259
|
+
...IdempotentReplayedHeader;
|
|
260
|
+
...RateLimitHeaders;
|
|
261
|
+
@body post: Post;
|
|
262
|
+
} | BadRequest | IdempotencyInProgress | IdempotencyKeyReused | TooManyRequests;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
| Name | Header | Where |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| `IdempotencyKeyHeader` | `Idempotency-Key`, 1 to 255 characters, optional | the request: checked |
|
|
268
|
+
| `IdempotentReplayedHeader` | `Idempotent-Replayed: true` | a reply that replays an earlier one |
|
|
269
|
+
| `RateLimitHeaders` | `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` | a rate-limited route's replies |
|
|
270
|
+
| `RetryAfterHeader` | `Retry-After`, in seconds | a 409 or a 429 |
|
|
271
|
+
| `IdempotencyInProgress` | 409, `ConflictBody`, `RateLimit-*`, `Retry-After` | the key's first request still runs |
|
|
272
|
+
| `IdempotencyKeyReused` | 422, `UnprocessableEntityBody`, `RateLimit-*` | the key was used with another body |
|
|
273
|
+
|
|
274
|
+
`TooManyRequests` carries the `RateLimit-*` headers and `Retry-After` too. The
|
|
275
|
+
generator checks the request's `Idempotency-Key`; a reply's headers are
|
|
276
|
+
documented, not checked. More in [Headers](docs/guide/headers.md).
|
|
277
|
+
|
|
278
|
+
### Resources
|
|
279
|
+
|
|
280
|
+
One line gives a resource its five operations, with `@operationIds` names:
|
|
281
|
+
|
|
282
|
+
```tsp
|
|
283
|
+
model PostFilters {
|
|
284
|
+
@query status?: PostStatus;
|
|
285
|
+
@query authorId?: uuid;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@route("/posts")
|
|
289
|
+
interface Posts extends Resource<Post, PostFilters, SortField = "createdAt" | "title"> {}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
| Operation | Route | Answers |
|
|
293
|
+
| --- | --- | --- |
|
|
294
|
+
| `listPosts` | `GET /posts`, with the filters, `page`, `pageSize`, `orderBy`, `direction` | `Page<Post>` |
|
|
295
|
+
| `readPosts` | `GET /posts/{id}` | `Post \| NotFound` |
|
|
296
|
+
| `createPosts` | `POST /posts`, `Create<Post>` | 201 `Post \| BadRequest \| Conflict` |
|
|
297
|
+
| `updatePosts` | `PATCH /posts/{id}`, `MergePatchUpdate<Post>` | `Post \| BadRequest \| NotFound \| Conflict` |
|
|
298
|
+
| `deletePosts` | `DELETE /posts/{id}` | 204, `NotFound` |
|
|
299
|
+
|
|
300
|
+
`orderBy` takes one of the `SortField`s (`SortField` is `"id"` by default), and `direction`
|
|
301
|
+
`asc` (the default) or `desc`, as `@nxgt/drizzle` sorts a page. `{id}`
|
|
302
|
+
is a `uuid`, or the `Id` you name. More in
|
|
303
|
+
[Resources](docs/guide/resources.md).
|
|
304
|
+
|
|
305
|
+
### Operation ids
|
|
306
|
+
|
|
307
|
+
TypeSpec names an operation after its interface, `Posts_list`, and the
|
|
308
|
+
generator turns that id into the client's method and its types' prefix. Put
|
|
309
|
+
`@operationIds` on the interface to name each operation after its method and
|
|
310
|
+
the interface instead:
|
|
311
|
+
|
|
312
|
+
```tsp
|
|
313
|
+
import "@typespec/openapi"; // for @operationId
|
|
314
|
+
|
|
315
|
+
using OpenAPI;
|
|
316
|
+
|
|
317
|
+
@route("/posts")
|
|
318
|
+
@operationIds
|
|
319
|
+
interface Posts {
|
|
320
|
+
@get list(): Post[]; // listPosts
|
|
321
|
+
@get @operationId("getPost") read(@path postId: string): Post | NotFound; // getPost
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
An operation's own `@operationId` wins. An interface that extends a marked
|
|
326
|
+
one, or an instance of a marked template, is named after itself. An operation
|
|
327
|
+
of a marked interface that has another operation's id is an error,
|
|
328
|
+
`duplicate-operation-id`. More in
|
|
329
|
+
[Operation ids](docs/guide/operation-ids.md).
|
|
330
|
+
|
|
331
|
+
## API
|
|
332
|
+
|
|
333
|
+
### Decorators
|
|
334
|
+
|
|
335
|
+
| Decorator | On | What it does |
|
|
336
|
+
| --- | --- | --- |
|
|
337
|
+
| `@operationIds` | an interface | names each operation `<operation><Interface>`, unless it has an `@operationId` |
|
|
338
|
+
|
|
339
|
+
### Models
|
|
340
|
+
|
|
341
|
+
| Model | What it is |
|
|
342
|
+
| --- | --- |
|
|
343
|
+
| `ErrorBody<Status>` | `{ status: Status, message: string, timestamp: utcDateTime }` |
|
|
344
|
+
| `BadRequestBody` | `ErrorBody<400>` and `issues?: ValidationIssue[]` |
|
|
345
|
+
| `UnauthorizedBody` … `InternalServerErrorBody` | `ErrorBody<401>` … `ErrorBody<500>`, one per response above |
|
|
346
|
+
| `PageParameters` | the query `page` and `pageSize`, to spread into an operation's parameters |
|
|
347
|
+
| `Page<Item>` | `{ items: Item[], total, page, pageSize, pageCount }`, the schema `<Item>Page` |
|
|
348
|
+
| `CursorPageParameters` | the query `after` and `limit` |
|
|
349
|
+
| `CursorPage<Item>` | `{ items: Item[], nextCursor: string \| null }`, the schema `<Item>CursorPage` |
|
|
350
|
+
| `ValidationIssue` | `{ target: ValidationTarget, path: (string \| integer)[], code: string, message: string }` |
|
|
351
|
+
| `ValidationTarget` | `"param" \| "query" \| "header" \| "json" \| "form" \| "body" \| "response"` |
|
|
352
|
+
|
|
353
|
+
### Authentication
|
|
354
|
+
|
|
355
|
+
| Name | What it is |
|
|
356
|
+
| --- | --- |
|
|
357
|
+
| `JanusAuth` | `BearerAuth \| SessionTokenAuth \| SessionCookieAuth`, for `@useAuth` |
|
|
358
|
+
| `SessionTokenAuth` | an API key in the `X-Session-Token` header |
|
|
359
|
+
| `SessionCookieAuth` | an API key in the `janus-session` cookie |
|
|
360
|
+
|
|
361
|
+
### Scalars and columns
|
|
362
|
+
|
|
363
|
+
| Name | What it is |
|
|
364
|
+
| --- | --- |
|
|
365
|
+
| `uuid` | a string, `format: uuid`; the schema `Uuid` |
|
|
366
|
+
| `email` | a string, `format: email`; the schema `Email` |
|
|
367
|
+
| `Timestamps` | `createdAt`, `updatedAt: utcDateTime`, read-only |
|
|
368
|
+
| `SoftDelete` | `deletedAt: utcDateTime \| null`, read-only |
|
|
369
|
+
| `Versioned` | `version: integer`, read and update |
|
|
370
|
+
| `Actors<Id = uuid>` | `createdBy`, `updatedBy`, `deletedBy: Id \| null`, read-only |
|
|
371
|
+
|
|
372
|
+
### Headers
|
|
373
|
+
|
|
374
|
+
| Model | What it is |
|
|
375
|
+
| --- | --- |
|
|
376
|
+
| `IdempotencyKeyHeader` | `Idempotency-Key`, 1 to 255 characters, optional: to spread into parameters |
|
|
377
|
+
| `IdempotentReplayedHeader` | `Idempotent-Replayed: "true"`, optional: to spread into a reply |
|
|
378
|
+
| `RateLimitHeaders` | `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, integers, optional |
|
|
379
|
+
| `RetryAfterHeader` | `Retry-After`, seconds, optional |
|
|
380
|
+
|
|
381
|
+
### Resources
|
|
382
|
+
|
|
383
|
+
| Name | What it is |
|
|
384
|
+
| --- | --- |
|
|
385
|
+
| `Resource<Item, Filters = {}, SortField = "id", Id = uuid>` | an interface template: `list`, `read`, `create`, `update`, `delete` |
|
|
386
|
+
| `SortParameters<Field>` | the query `orderBy?: Field` and `direction?: "asc" \| "desc" = "asc"` |
|
|
387
|
+
|
|
388
|
+
### Responses
|
|
389
|
+
|
|
390
|
+
`AuthenticationRequired` (401) and `AccessDenied` (403), without a body, and
|
|
391
|
+
`ErrorWithoutBody<Status>` for any other status without a body.
|
|
392
|
+
`IdempotencyInProgress` (409) and `IdempotencyKeyReused` (422), with the
|
|
393
|
+
envelope and the rate limit's headers.
|
|
394
|
+
|
|
395
|
+
`BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `Conflict`,
|
|
396
|
+
`UnprocessableEntity`, `TooManyRequests`, `InternalServerError`, each an
|
|
397
|
+
`@error` model with its `@statusCode` and body, and
|
|
398
|
+
`ErrorResponse<Status>` for any other status.
|
|
399
|
+
|
|
400
|
+
## Traps
|
|
401
|
+
|
|
402
|
+
- **Do not declare a schema named `ValidationErrorBody`.** The generator
|
|
403
|
+
declares it itself, and two schemas of that name fail with
|
|
404
|
+
`name_collision`. `BadRequestBody` carries the `issues` instead.
|
|
405
|
+
- **`ErrorResponse<503>`'s body is emitted inline**, and generated as
|
|
406
|
+
`<Operation>503Response`. For a named schema, name the body and the
|
|
407
|
+
response:
|
|
408
|
+
|
|
409
|
+
```tsp
|
|
410
|
+
model ServiceUnavailableBody is ErrorBody<503>;
|
|
411
|
+
|
|
412
|
+
@error
|
|
413
|
+
model ServiceUnavailable {
|
|
414
|
+
@statusCode _: 503;
|
|
415
|
+
@body body: ServiceUnavailableBody;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
- **One status, one reply.** An operation that declares both
|
|
419
|
+
`AuthenticationRequired` and `Unauthorized` gets a single 401, with the
|
|
420
|
+
envelope: the reply without a body is lost, without a warning. Declare the
|
|
421
|
+
one the route sends: the guard's, or the handler's. The same goes for
|
|
422
|
+
`Conflict` beside `IdempotencyInProgress`, and `UnprocessableEntity`
|
|
423
|
+
beside `IdempotencyKeyReused`: both keep one body, but the reply's
|
|
424
|
+
description is the first one's, and the body a union of the envelope with
|
|
425
|
+
itself. Declare one of each pair: the `message` key tells them apart.
|
|
426
|
+
- **Both versions at once write two folders.** With
|
|
427
|
+
`openapi-versions: ['3.1.0', '3.2.0']`, the emitter writes
|
|
428
|
+
`3.1.0/openapi.yaml` and `3.2.0/openapi.yaml`: point the generator's input
|
|
429
|
+
at one of them.
|
|
430
|
+
- **The library's schema names are global.** `BadRequestBody` …
|
|
431
|
+
`InternalServerErrorBody`, `ValidationIssue`, `ValidationTarget`, `Uuid`,
|
|
432
|
+
`Email`, and each `<Item>Page` and `<Item>CursorPage` are emitted under
|
|
433
|
+
those names, without a namespace, once the spec uses them. A model of the
|
|
434
|
+
same name in your spec then fails with `duplicate-type-name`
|
|
435
|
+
([troubleshooting](docs/troubleshooting.md)).
|
|
436
|
+
|
|
437
|
+
## Documentation
|
|
438
|
+
|
|
439
|
+
- [Error replies](docs/guide/errors.md): the envelope, each response, and
|
|
440
|
+
what the generator makes of them;
|
|
441
|
+
- [Pagination](docs/guide/pagination.md): offset and cursor pages, and
|
|
442
|
+
what a handler answers;
|
|
443
|
+
- [Authentication](docs/guide/auth.md): `JanusAuth`, and the guards'
|
|
444
|
+
replies without a body;
|
|
445
|
+
- [Scalars and columns](docs/guide/columns.md): `uuid`, `email`, and the
|
|
446
|
+
columns `@nxgt/drizzle` stamps;
|
|
447
|
+
- [Headers](docs/guide/headers.md): `Idempotency-Key`, the rate limit and
|
|
448
|
+
`Retry-After`;
|
|
449
|
+
- [Resources](docs/guide/resources.md): the five operations of a resource
|
|
450
|
+
in one line, with its filters and sort;
|
|
451
|
+
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, and what
|
|
452
|
+
the generated client calls each operation;
|
|
453
|
+
- [troubleshooting](docs/troubleshooting.md);
|
|
454
|
+
- [the roadmap](docs/roadmap.md): what is coming.
|
|
455
|
+
|
|
456
|
+
## License
|
|
457
|
+
|
|
458
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JavaScript side of the `@nxgt/typespec` library, which `lib/main.tsp`
|
|
3
|
+
* imports from `dist/`: its name and diagnostics, and the decorators a
|
|
4
|
+
* template cannot express. The conventions themselves are in `lib/`.
|
|
5
|
+
*
|
|
6
|
+
* A decorator is exported only through `$decorators`: a top-level `$name`
|
|
7
|
+
* export would declare it a second time, in the global namespace.
|
|
8
|
+
*/
|
|
9
|
+
import type { Program } from '@typespec/compiler';
|
|
10
|
+
import { operationIds } from './operation-ids';
|
|
11
|
+
export { $lib } from './lib';
|
|
12
|
+
export declare function $onValidate(program: Program): void;
|
|
13
|
+
export declare const $decorators: {
|
|
14
|
+
Nxgt: {
|
|
15
|
+
operationIds: typeof operationIds;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAwB,MAAM,iBAAiB,CAAC;AAErE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC;AAE7B,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAElD;AAED,eAAO,MAAM,WAAW;;;;CAEvB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/operation-ids.ts
|
|
2
|
+
import {
|
|
3
|
+
isTemplateInstance,
|
|
4
|
+
navigateProgram
|
|
5
|
+
} from "@typespec/compiler";
|
|
6
|
+
import {
|
|
7
|
+
getOperationId,
|
|
8
|
+
resolveOperationId,
|
|
9
|
+
setOperationId
|
|
10
|
+
} from "@typespec/openapi";
|
|
11
|
+
|
|
12
|
+
// src/lib.ts
|
|
13
|
+
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
|
|
14
|
+
var $lib = createTypeSpecLibrary({
|
|
15
|
+
name: "@nxgt/typespec",
|
|
16
|
+
diagnostics: {
|
|
17
|
+
"duplicate-operation-id": {
|
|
18
|
+
severity: "error",
|
|
19
|
+
messages: {
|
|
20
|
+
default: paramMessage`Two operations are named ${"id"}: an OpenAPI operation id must be unique.`
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
state: {
|
|
25
|
+
operationIds: { description: "The interfaces marked with @operationIds" }
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// src/operation-ids.ts
|
|
30
|
+
function operationIds(context, target) {
|
|
31
|
+
context.program.stateSet($lib.stateKeys.operationIds).add(target);
|
|
32
|
+
}
|
|
33
|
+
function isMarked(program, target) {
|
|
34
|
+
return program.stateSet($lib.stateKeys.operationIds).has(target) || target.sourceInterfaces.some((source) => isMarked(program, source));
|
|
35
|
+
}
|
|
36
|
+
function isNamed(program, operation) {
|
|
37
|
+
const container = operation.interface;
|
|
38
|
+
return container !== undefined && !isTemplateInstance(container) && isMarked(program, container);
|
|
39
|
+
}
|
|
40
|
+
function validateOperationIds(program) {
|
|
41
|
+
const seen = new Map;
|
|
42
|
+
navigateProgram(program, {
|
|
43
|
+
operation(operation) {
|
|
44
|
+
const named = isNamed(program, operation);
|
|
45
|
+
const own = getOperationId(program, operation);
|
|
46
|
+
const id = own ?? (named ? `${operation.name}${operation.interface?.name}` : resolveOperationId(program, operation));
|
|
47
|
+
if (seen.has(id) && (named || seen.get(id))) {
|
|
48
|
+
$lib.reportDiagnostic(program, {
|
|
49
|
+
code: "duplicate-operation-id",
|
|
50
|
+
format: { id },
|
|
51
|
+
target: operation
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
seen.set(id, named || (seen.get(id) ?? false));
|
|
55
|
+
if (named && own === undefined)
|
|
56
|
+
setOperationId(program, operation, id);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/index.ts
|
|
62
|
+
function $onValidate(program) {
|
|
63
|
+
validateOperationIds(program);
|
|
64
|
+
}
|
|
65
|
+
var $decorators = {
|
|
66
|
+
Nxgt: { operationIds }
|
|
67
|
+
};
|
|
68
|
+
export {
|
|
69
|
+
$decorators,
|
|
70
|
+
$lib,
|
|
71
|
+
$onValidate
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//# debugId=2D92CF45DEECB16A64756E2164756E21
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/operation-ids.ts", "../src/lib.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `@operationIds`: names each operation of an interface after its method and\n * the interface, `<operation><Interface>`, such as `listPets`.\n */\nimport {\n\ttype DecoratorContext,\n\ttype Interface,\n\tisTemplateInstance,\n\tnavigateProgram,\n\ttype Operation,\n\ttype Program,\n} from '@typespec/compiler';\nimport {\n\tgetOperationId,\n\tresolveOperationId,\n\tsetOperationId,\n} from '@typespec/openapi';\nimport { $lib } from './lib';\n\n/**\n * Marks the interface; its ids are set once the program is checked. On a\n * template, TypeSpec runs it on each instance, which `isMarked` follows to\n * the interfaces extending it.\n */\nexport function operationIds(\n\tcontext: DecoratorContext,\n\ttarget: Interface,\n): void {\n\tcontext.program.stateSet($lib.stateKeys.operationIds).add(target);\n}\n\n/** Marked itself, or extending a marked interface: an instance of a marked template included. */\nfunction isMarked(program: Program, target: Interface): boolean {\n\treturn (\n\t\tprogram.stateSet($lib.stateKeys.operationIds).has(target) ||\n\t\ttarget.sourceInterfaces.some((source) => isMarked(program, source))\n\t);\n}\n\n/**\n * Whether `@operationIds` names the operation's id, or checks it: its\n * interface is marked, and is not a template instance, which is never\n * emitted; only the interfaces extending it are.\n */\nfunction isNamed(program: Program, operation: Operation): boolean {\n\tconst container = operation.interface;\n\treturn (\n\t\tcontainer !== undefined &&\n\t\t!isTemplateInstance(container) &&\n\t\tisMarked(program, container)\n\t);\n}\n\n/**\n * Sets the ids, after every decorator, so the order of `@operationId` and\n * `@operationIds` in the source does not matter. Two operations with one id,\n * one of them in a marked interface, are an error: two interfaces of one name\n * in two namespaces, an `@operationId` that `extends` copied, one written\n * elsewhere, or an operation in the service namespace, named after itself. `@typespec/openapi3` would emit both without a word.\n */\nexport function validateOperationIds(program: Program): void {\n\tconst seen = new Map<string, boolean>();\n\tnavigateProgram(program, {\n\t\toperation(operation) {\n\t\t\tconst named = isNamed(program, operation);\n\t\t\tconst own = getOperationId(program, operation);\n\t\t\t// Elsewhere, only checked: the id `@typespec/openapi3` gives under its\n\t\t\t// default strategy; an operation in the service namespace is named\n\t\t\t// after itself.\n\t\t\tconst id =\n\t\t\t\town ??\n\t\t\t\t(named\n\t\t\t\t\t? `${operation.name}${operation.interface?.name}`\n\t\t\t\t\t: resolveOperationId(program, operation));\n\t\t\tif (seen.has(id) && (named || seen.get(id))) {\n\t\t\t\t$lib.reportDiagnostic(program, {\n\t\t\t\t\tcode: 'duplicate-operation-id',\n\t\t\t\t\tformat: { id },\n\t\t\t\t\ttarget: operation,\n\t\t\t\t});\n\t\t\t}\n\t\t\tseen.set(id, named || (seen.get(id) ?? false));\n\t\t\t// Only the ids it gives: an explicit id on any other operation would\n\t\t\t// override the emitter's `operation-id-strategy`.\n\t\t\tif (named && own === undefined) setOperationId(program, operation, id);\n\t\t},\n\t});\n}\n",
|
|
6
|
+
"/** The library's definition: its name, diagnostics and state keys. */\nimport { createTypeSpecLibrary, paramMessage } from '@typespec/compiler';\n\nexport const $lib = createTypeSpecLibrary({\n\tname: '@nxgt/typespec',\n\tdiagnostics: {\n\t\t'duplicate-operation-id': {\n\t\t\tseverity: 'error',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`Two operations are named ${'id'}: an OpenAPI operation id must be unique.`,\n\t\t\t},\n\t\t},\n\t},\n\tstate: {\n\t\toperationIds: { description: 'The interfaces marked with @operationIds' },\n\t},\n});\n",
|
|
7
|
+
"/**\n * The JavaScript side of the `@nxgt/typespec` library, which `lib/main.tsp`\n * imports from `dist/`: its name and diagnostics, and the decorators a\n * template cannot express. The conventions themselves are in `lib/`.\n *\n * A decorator is exported only through `$decorators`: a top-level `$name`\n * export would declare it a second time, in the global namespace.\n */\nimport type { Program } from '@typespec/compiler';\nimport { operationIds, validateOperationIds } from './operation-ids';\n\nexport { $lib } from './lib';\n\nexport function $onValidate(program: Program): void {\n\tvalidateOperationIds(program);\n}\n\nexport const $decorators = {\n\tNxgt: { operationIds },\n};\n"
|
|
8
|
+
],
|
|
9
|
+
"mappings": ";AAIA;AAAA;AAAA;AAAA;AAQA;AAAA;AAAA;AAAA;AAAA;;;ACXA;AAEO,IAAM,OAAO,sBAAsB;AAAA,EACzC,MAAM;AAAA,EACN,aAAa;AAAA,IACZ,0BAA0B;AAAA,MACzB,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SAAS,wCAAwC;AAAA,MAClD;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA,IACN,cAAc,EAAE,aAAa,2CAA2C;AAAA,EACzE;AACD,CAAC;;;ADQM,SAAS,YAAY,CAC3B,SACA,QACO;AAAA,EACP,QAAQ,QAAQ,SAAS,KAAK,UAAU,YAAY,EAAE,IAAI,MAAM;AAAA;AAIjE,SAAS,QAAQ,CAAC,SAAkB,QAA4B;AAAA,EAC/D,OACC,QAAQ,SAAS,KAAK,UAAU,YAAY,EAAE,IAAI,MAAM,KACxD,OAAO,iBAAiB,KAAK,CAAC,WAAW,SAAS,SAAS,MAAM,CAAC;AAAA;AASpE,SAAS,OAAO,CAAC,SAAkB,WAA+B;AAAA,EACjE,MAAM,YAAY,UAAU;AAAA,EAC5B,OACC,cAAc,aACd,CAAC,mBAAmB,SAAS,KAC7B,SAAS,SAAS,SAAS;AAAA;AAWtB,SAAS,oBAAoB,CAAC,SAAwB;AAAA,EAC5D,MAAM,OAAO,IAAI;AAAA,EACjB,gBAAgB,SAAS;AAAA,IACxB,SAAS,CAAC,WAAW;AAAA,MACpB,MAAM,QAAQ,QAAQ,SAAS,SAAS;AAAA,MACxC,MAAM,MAAM,eAAe,SAAS,SAAS;AAAA,MAI7C,MAAM,KACL,QACC,QACE,GAAG,UAAU,OAAO,UAAU,WAAW,SACzC,mBAAmB,SAAS,SAAS;AAAA,MACzC,IAAI,KAAK,IAAI,EAAE,MAAM,SAAS,KAAK,IAAI,EAAE,IAAI;AAAA,QAC5C,KAAK,iBAAiB,SAAS;AAAA,UAC9B,MAAM;AAAA,UACN,QAAQ,EAAE,GAAG;AAAA,UACb,QAAQ;AAAA,QACT,CAAC;AAAA,MACF;AAAA,MACA,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI,EAAE,KAAK,MAAM;AAAA,MAG7C,IAAI,SAAS,QAAQ;AAAA,QAAW,eAAe,SAAS,WAAW,EAAE;AAAA;AAAA,EAEvE,CAAC;AAAA;;;AEzEK,SAAS,WAAW,CAAC,SAAwB;AAAA,EACnD,qBAAqB,OAAO;AAAA;AAGtB,IAAM,cAAc;AAAA,EAC1B,MAAM,EAAE,aAAa;AACtB;",
|
|
10
|
+
"debugId": "2D92CF45DEECB16A64756E2164756E21",
|
|
11
|
+
"names": []
|
|
12
|
+
}
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,IAAI;;;;uCAaf,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@operationIds`: names each operation of an interface after its method and
|
|
3
|
+
* the interface, `<operation><Interface>`, such as `listPets`.
|
|
4
|
+
*/
|
|
5
|
+
import { type DecoratorContext, type Interface, type Program } from '@typespec/compiler';
|
|
6
|
+
/**
|
|
7
|
+
* Marks the interface; its ids are set once the program is checked. On a
|
|
8
|
+
* template, TypeSpec runs it on each instance, which `isMarked` follows to
|
|
9
|
+
* the interfaces extending it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function operationIds(context: DecoratorContext, target: Interface): void;
|
|
12
|
+
/**
|
|
13
|
+
* Sets the ids, after every decorator, so the order of `@operationId` and
|
|
14
|
+
* `@operationIds` in the source does not matter. Two operations with one id,
|
|
15
|
+
* one of them in a marked interface, are an error: two interfaces of one name
|
|
16
|
+
* in two namespaces, an `@operationId` that `extends` copied, one written
|
|
17
|
+
* elsewhere, or an operation in the service namespace, named after itself. `@typespec/openapi3` would emit both without a word.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateOperationIds(program: Program): void;
|
|
20
|
+
//# sourceMappingURL=operation-ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,SAAS,EAId,KAAK,OAAO,EACZ,MAAM,oBAAoB,CAAC;AAQ5B;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,GACf,IAAI,CAEN;AAwBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2B3D"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @nxgt/typespec documentation
|
|
2
|
+
|
|
3
|
+
The [package README](../README.md) is the short version.
|
|
4
|
+
|
|
5
|
+
| Page | Read it when |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| [Error replies](guide/errors.md) | declaring what an operation answers when it fails |
|
|
8
|
+
| [Pagination](guide/pagination.md) | listing a collection a page at a time |
|
|
9
|
+
| [Authentication](guide/auth.md) | protecting an operation with a `@nxgt/janus` session |
|
|
10
|
+
| [Scalars and columns](guide/columns.md) | typing ids and addresses, and the columns a row carries |
|
|
11
|
+
| [Headers](guide/headers.md) | making a write idempotent, or declaring a rate limit |
|
|
12
|
+
| [Resources](guide/resources.md) | declaring a resource's five operations at once |
|
|
13
|
+
| [Operation ids](guide/operation-ids.md) | naming the generated client's methods after the spec's |
|
|
14
|
+
| [Troubleshooting](troubleshooting.md) | the compiler or the generator refused the spec |
|
|
15
|
+
| [Roadmap](roadmap.md) | wondering what is coming |
|