@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.
@@ -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.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.0",
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",
@@ -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
- }