@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.
- package/README.md +41 -51
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -20
- package/dist/index.js.map +5 -5
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/operation-ids.d.ts +15 -18
- package/dist/operation-ids.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide/auth.md +3 -3
- package/docs/guide/columns.md +1 -1
- package/docs/guide/errors.md +3 -3
- package/docs/guide/headers.md +2 -2
- package/docs/guide/operation-ids.md +154 -72
- package/docs/guide/pagination.md +3 -3
- package/docs/guide/sorting.md +131 -0
- package/docs/roadmap.md +17 -3
- package/docs/troubleshooting.md +44 -29
- package/lib/main.tsp +1 -1
- package/lib/operation-ids.tsp +13 -16
- package/lib/sorting.tsp +20 -0
- package/package.json +2 -2
- package/docs/guide/resources.md +0 -157
- package/lib/resources.tsp +0 -68
package/lib/sorting.tsp
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import "@typespec/http";
|
|
2
|
+
|
|
3
|
+
using Http;
|
|
4
|
+
|
|
5
|
+
namespace Nxgt;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The sort of a list, as `@nxgt/drizzle` takes it: one field, and a
|
|
9
|
+
* direction. `Field` is the union of the fields a client may sort by.
|
|
10
|
+
* Spread it into an operation's parameters.
|
|
11
|
+
*/
|
|
12
|
+
model SortParameters<Field extends string> {
|
|
13
|
+
/** The field to sort by. */
|
|
14
|
+
@query
|
|
15
|
+
orderBy?: Field;
|
|
16
|
+
|
|
17
|
+
/** `asc` or `desc`. */
|
|
18
|
+
@query
|
|
19
|
+
direction?: "asc" | "desc" = "asc";
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/typespec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"typescript": "^6.0.3"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@nxgt/openapi-codegen": "^0.5.
|
|
54
|
+
"@nxgt/openapi-codegen": "^0.5.1",
|
|
55
55
|
"@nxgt/openapi-hono": "^0.3.0",
|
|
56
56
|
"@types/bun": "^1.4.0",
|
|
57
57
|
"@typespec/compiler": "1.16.0",
|
package/docs/guide/resources.md
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# Resources
|
|
2
|
-
|
|
3
|
-
`Resource` gives a resource its five operations in one line: list, read,
|
|
4
|
-
create, update and delete, with the replies each one can send.
|
|
5
|
-
|
|
6
|
-
## One resource
|
|
7
|
-
|
|
8
|
-
```tsp
|
|
9
|
-
import "@typespec/http";
|
|
10
|
-
import "@nxgt/typespec";
|
|
11
|
-
|
|
12
|
-
using Http;
|
|
13
|
-
using Nxgt;
|
|
14
|
-
|
|
15
|
-
namespace Blog;
|
|
16
|
-
|
|
17
|
-
model Author {
|
|
18
|
-
@visibility(Lifecycle.Read) id: uuid;
|
|
19
|
-
name: string;
|
|
20
|
-
email: email;
|
|
21
|
-
...Timestamps;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
model AuthorFilters {
|
|
25
|
-
/** Authors of this name. */
|
|
26
|
-
@query name?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
@route("/authors")
|
|
30
|
-
interface Authors extends Resource<Author, AuthorFilters, SortField = "name" | "createdAt"> {}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
| Operation | Route | Body | Answers |
|
|
34
|
-
| --- | --- | --- | --- |
|
|
35
|
-
| `listAuthors` | `GET /authors` | | `Page<Author>` |
|
|
36
|
-
| `readAuthor` | `GET /authors/{id}` | | `Author`, `NotFound` |
|
|
37
|
-
| `createAuthor` | `POST /authors` | `Create<Author>` | 201 `Author`, `BadRequest`, `Conflict` |
|
|
38
|
-
| `updateAuthor` | `PATCH /authors/{id}` | `MergePatchUpdate<Author>` | `Author`, `BadRequest`, `NotFound`, `Conflict` |
|
|
39
|
-
| `deleteAuthor` | `DELETE /authors/{id}` | | 204, `NotFound` |
|
|
40
|
-
|
|
41
|
-
The template carries `@operationIds`. `list` is named after the interface
|
|
42
|
-
that extends it, which holds the many, and `read`, `create`, `update` and
|
|
43
|
-
`delete` after `Item`, the one: `listAuthors`, `readAuthor`. The item's
|
|
44
|
-
name is its `@friendlyName`, if it has one. An item without a name of its
|
|
45
|
-
own, an anonymous model or an instance such as `Draft<Author>`, falls back to
|
|
46
|
-
the interface: `readDrafts`. An operation the extending interface declares
|
|
47
|
-
again is named after the interface too, unless it has its own `@operationId`. The generated `operations` has
|
|
48
|
-
one entry per id:
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
import { operations } from './generated/operations';
|
|
52
|
-
|
|
53
|
-
Object.keys(operations);
|
|
54
|
-
// ['listAuthors', 'createAuthor', 'readAuthor', 'updateAuthor', 'deleteAuthor']
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The generator's own 400 is on every operation that takes a parameter or a
|
|
58
|
-
body, as always.
|
|
59
|
-
|
|
60
|
-
## Two resources of one item
|
|
61
|
-
|
|
62
|
-
Two interfaces extending `Resource` with one `Item` share `readAuthor`,
|
|
63
|
-
`createAuthor`, `updateAuthor` and `deleteAuthor`, which is an error,
|
|
64
|
-
`duplicate-operation-id`. Declare the operations of the second again, each
|
|
65
|
-
with its own `@operationId`, as an interface that extends overrides the
|
|
66
|
-
template's. Use the resource's `Id` in place of `uuid`, if it names one:
|
|
67
|
-
|
|
68
|
-
```tsp
|
|
69
|
-
import "@typespec/openapi";
|
|
70
|
-
|
|
71
|
-
using OpenAPI;
|
|
72
|
-
|
|
73
|
-
@route("/archived-authors")
|
|
74
|
-
interface ArchivedAuthors extends Resource<Author> {
|
|
75
|
-
@get @operationId("readArchivedAuthor") read(@path id: uuid): Author | NotFound;
|
|
76
|
-
@post @operationId("createArchivedAuthor") create(@body item: Create<Author>): {
|
|
77
|
-
@statusCode _: 201;
|
|
78
|
-
@body item: Author;
|
|
79
|
-
} | BadRequest | Conflict;
|
|
80
|
-
@patch @operationId("updateArchivedAuthor")
|
|
81
|
-
update(@path id: uuid, @body item: MergePatchUpdate<Author>): Author | BadRequest | NotFound | Conflict;
|
|
82
|
-
@delete @operationId("deleteArchivedAuthor") delete(@path id: uuid): NoContentResponse | NotFound;
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## The template's arguments
|
|
87
|
-
|
|
88
|
-
`Resource<Item, Filters = {}, SortField = "id", Id = uuid>`. Name the ones
|
|
89
|
-
after `Filters`, as above, to skip those you keep:
|
|
90
|
-
|
|
91
|
-
- `Item`: the resource's model. `Create<Item>` and `MergePatchUpdate<Item>`
|
|
92
|
-
leave its read-only properties out, such as `id` and the `Timestamps`.
|
|
93
|
-
- `Filters`: a model of `@query` properties, spread into `list`. Each is
|
|
94
|
-
typed and checked, as any query parameter.
|
|
95
|
-
- `SortField`: the fields `list` may sort by, as a union of strings.
|
|
96
|
-
- `Id`: the type of `{id}`. `Id = integer` for a table whose id is
|
|
97
|
-
`@nxgt/drizzle`'s `id('identity')`.
|
|
98
|
-
|
|
99
|
-
## Filters and sort
|
|
100
|
-
|
|
101
|
-
`list` takes the filters, the offset page's `page` and `pageSize`, and
|
|
102
|
-
`SortParameters<SortField>`:
|
|
103
|
-
|
|
104
|
-
| Query | Default | Checked |
|
|
105
|
-
| --- | --- | --- |
|
|
106
|
-
| `orderBy` | none: the server's order, the primary key with `@nxgt/drizzle` | one of the `SortField`s |
|
|
107
|
-
| `direction` | `asc` | `asc` or `desc` |
|
|
108
|
-
|
|
109
|
-
`?orderBy=email` on `Authors` is a 400: `email` is not a `SortField`. The
|
|
110
|
-
handler passes them to `@nxgt/drizzle`:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { Hono } from 'hono';
|
|
114
|
-
import { createRoutes } from './generated/hono';
|
|
115
|
-
|
|
116
|
-
const app = new Hono();
|
|
117
|
-
createRoutes(app).get('/authors', async (c) => {
|
|
118
|
-
const { name, page, pageSize, orderBy, direction } = c.req.valid('query');
|
|
119
|
-
const found = await authors.paginate({
|
|
120
|
-
where: name === undefined ? {} : { name },
|
|
121
|
-
page,
|
|
122
|
-
pageSize,
|
|
123
|
-
// paginate takes the sort as an object of directions by field
|
|
124
|
-
...(orderBy === undefined ? {} : { orderBy: { [orderBy]: direction } }),
|
|
125
|
-
});
|
|
126
|
-
return c.json(found, 200);
|
|
127
|
-
});
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
`paginateByCursor` takes them apart instead, `{ orderBy, direction }`, as
|
|
131
|
-
the query has them.
|
|
132
|
-
|
|
133
|
-
`SortParameters<Field>` works on its own too, in any list:
|
|
134
|
-
|
|
135
|
-
```tsp
|
|
136
|
-
@get list(...PageParameters, ...SortParameters<"createdAt" | "title">): Page<Post>;
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Changing an operation
|
|
140
|
-
|
|
141
|
-
An interface that extends `Resource` can declare one of its operations
|
|
142
|
-
again, and its own replaces the template's. A list paged by cursor:
|
|
143
|
-
|
|
144
|
-
```tsp
|
|
145
|
-
@route("/comments")
|
|
146
|
-
interface Comments extends Resource<Comment> {
|
|
147
|
-
@get list(...CursorPageParameters): CursorPage<Comment>;
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Add `@useAuth(JanusAuth)` on the interface to protect all five, and name the
|
|
152
|
-
guards' replies in the operations you declare again
|
|
153
|
-
([Authentication](auth.md)).
|
|
154
|
-
|
|
155
|
-
A `read`, `create`, `update` or `delete` declared again is named after the
|
|
156
|
-
interface, `readComments`, not after the item: give it
|
|
157
|
-
`@operationId("readComment")` to keep the item's name.
|
package/lib/resources.tsp
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import "@typespec/http";
|
|
2
|
-
|
|
3
|
-
using Http;
|
|
4
|
-
|
|
5
|
-
namespace Nxgt;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The sort of a list, as `@nxgt/drizzle` takes it: one field, and a
|
|
9
|
-
* direction. `Field` is the union of the fields a client may sort by.
|
|
10
|
-
* Spread it into an operation's parameters.
|
|
11
|
-
*/
|
|
12
|
-
model SortParameters<Field extends string> {
|
|
13
|
-
/** The field to sort by. */
|
|
14
|
-
@query
|
|
15
|
-
orderBy?: Field;
|
|
16
|
-
|
|
17
|
-
/** `asc` or `desc`. */
|
|
18
|
-
@query
|
|
19
|
-
direction?: "asc" | "desc" = "asc";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The five operations of a resource, at `/<route>` and `/<route>/{id}`:
|
|
24
|
-
* `list` pages by offset, with the `Filters` the spec writes and a sort by
|
|
25
|
-
* `SortField`; `read`, `create`, `update` (a merge patch) and `delete`.
|
|
26
|
-
* Extend it with `@route`; `@operationIds` names `list` after the interface
|
|
27
|
-
* that extends it, and the others after `Item`: `listPosts`, `readPost`,
|
|
28
|
-
* `createPost`, `updatePost`, `deletePost`.
|
|
29
|
-
*
|
|
30
|
-
* @template Item The resource's model.
|
|
31
|
-
* @template Filters A model of `@query` properties, spread into `list`.
|
|
32
|
-
* @template SortField The fields `list` may sort by; `"id"` by default.
|
|
33
|
-
* @template Id The type of `{id}`; `uuid` by default.
|
|
34
|
-
*/
|
|
35
|
-
@operationIds
|
|
36
|
-
interface Resource<
|
|
37
|
-
Item extends {},
|
|
38
|
-
Filters extends {} = {},
|
|
39
|
-
SortField extends string = "id",
|
|
40
|
-
Id = uuid
|
|
41
|
-
> {
|
|
42
|
-
/** One page of the resources, filtered and sorted. */
|
|
43
|
-
@get
|
|
44
|
-
list(...Filters, ...PageParameters, ...SortParameters<SortField>): Page<Item>;
|
|
45
|
-
|
|
46
|
-
/** One resource. */
|
|
47
|
-
@Private.itemOperation(Item)
|
|
48
|
-
@get
|
|
49
|
-
read(@path id: Id): Item | NotFound;
|
|
50
|
-
|
|
51
|
-
/** Creates a resource. */
|
|
52
|
-
@Private.itemOperation(Item)
|
|
53
|
-
@post
|
|
54
|
-
create(@body item: Create<Item>): {
|
|
55
|
-
@statusCode _: 201;
|
|
56
|
-
@body item: Item;
|
|
57
|
-
} | BadRequest | Conflict;
|
|
58
|
-
|
|
59
|
-
/** Updates a resource with a merge patch. */
|
|
60
|
-
@Private.itemOperation(Item)
|
|
61
|
-
@patch
|
|
62
|
-
update(@path id: Id, @body item: MergePatchUpdate<Item>): Item | BadRequest | NotFound | Conflict;
|
|
63
|
-
|
|
64
|
-
/** Deletes a resource. */
|
|
65
|
-
@Private.itemOperation(Item)
|
|
66
|
-
@delete
|
|
67
|
-
delete(@path id: Id): NoContentResponse | NotFound;
|
|
68
|
-
}
|