@nxgt/typespec 0.2.0 → 0.4.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 +41 -47
- package/dist/index.js +48 -8
- package/dist/index.js.map +4 -4
- package/dist/lib.d.ts.map +1 -1
- package/dist/operation-ids.d.ts +15 -11
- 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 +152 -63
- package/docs/guide/pagination.md +3 -3
- package/docs/guide/sorting.md +131 -0
- package/docs/roadmap.md +20 -5
- package/docs/troubleshooting.md +44 -24
- package/lib/main.tsp +1 -1
- package/lib/operation-ids.tsp +13 -8
- package/lib/sorting.tsp +20 -0
- package/package.json +2 -2
- package/docs/guide/resources.md +0 -112
- package/lib/resources.tsp +0 -63
package/README.md
CHANGED
|
@@ -89,8 +89,8 @@ namespace Blog;
|
|
|
89
89
|
|
|
90
90
|
@route("/posts")
|
|
91
91
|
interface Posts {
|
|
92
|
-
@get
|
|
93
|
-
@post
|
|
92
|
+
@get getPost(@path postId: string): Post | NotFound | Unauthorized;
|
|
93
|
+
@post createPost(@body post: Create<Post>): {
|
|
94
94
|
@statusCode _: 201;
|
|
95
95
|
@body post: Post;
|
|
96
96
|
} | BadRequest | Conflict | ErrorResponse<503>;
|
|
@@ -139,12 +139,12 @@ page:
|
|
|
139
139
|
```tsp
|
|
140
140
|
@route("/posts")
|
|
141
141
|
interface Posts {
|
|
142
|
-
@get
|
|
142
|
+
@get listPosts(@query status?: PostStatus, ...PageParameters): Page<Post>;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
@route("/posts/{postId}/comments")
|
|
146
146
|
interface Comments {
|
|
147
|
-
@get
|
|
147
|
+
@get findPostComments(@path postId: string, ...CursorPageParameters): CursorPage<Comment> | BadRequest | NotFound;
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -191,7 +191,7 @@ beside the success:
|
|
|
191
191
|
@route("/posts/{postId}/comments/{commentId}")
|
|
192
192
|
interface Comments {
|
|
193
193
|
@useAuth(JanusAuth)
|
|
194
|
-
@delete
|
|
194
|
+
@delete deletePostComment(@path postId: string, @path commentId: string):
|
|
195
195
|
NoContentResponse | AuthenticationRequired | AccessDenied | ErrorWithoutBody<404>;
|
|
196
196
|
}
|
|
197
197
|
```
|
|
@@ -254,7 +254,7 @@ Spread the headers of an idempotent write and of a rate limit into an
|
|
|
254
254
|
operation and its replies:
|
|
255
255
|
|
|
256
256
|
```tsp
|
|
257
|
-
@post
|
|
257
|
+
@post createPost(...IdempotencyKeyHeader, @body post: Create<Post>): {
|
|
258
258
|
@statusCode _: 201;
|
|
259
259
|
...IdempotentReplayedHeader;
|
|
260
260
|
...RateLimitHeaders;
|
|
@@ -275,57 +275,52 @@ operation and its replies:
|
|
|
275
275
|
generator checks the request's `Idempotency-Key`; a reply's headers are
|
|
276
276
|
documented, not checked. More in [Headers](docs/guide/headers.md).
|
|
277
277
|
|
|
278
|
-
###
|
|
278
|
+
### Sorting
|
|
279
279
|
|
|
280
|
-
|
|
280
|
+
Spread the sort into a list, beside its filters and its page:
|
|
281
281
|
|
|
282
282
|
```tsp
|
|
283
|
-
model
|
|
284
|
-
@query
|
|
285
|
-
@query authorId?: uuid;
|
|
283
|
+
model AuthorFilters {
|
|
284
|
+
@query name?: string;
|
|
286
285
|
}
|
|
287
286
|
|
|
288
|
-
@route("/
|
|
289
|
-
interface
|
|
287
|
+
@route("/authors")
|
|
288
|
+
interface Authors {
|
|
289
|
+
@get listAuthors(
|
|
290
|
+
...AuthorFilters,
|
|
291
|
+
...PageParameters,
|
|
292
|
+
...SortParameters<"name" | "createdAt">,
|
|
293
|
+
): Page<Author>;
|
|
294
|
+
}
|
|
290
295
|
```
|
|
291
296
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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).
|
|
297
|
+
`orderBy` takes one of the fields named, and `direction` `asc` (the default)
|
|
298
|
+
or `desc`, as `@nxgt/drizzle` sorts a page: `?orderBy=email` is a 400. More,
|
|
299
|
+
with the handler, in [Sorting](docs/guide/sorting.md).
|
|
304
300
|
|
|
305
301
|
### Operation ids
|
|
306
302
|
|
|
307
|
-
TypeSpec names an operation after its interface, `
|
|
303
|
+
TypeSpec names an operation after its interface, `Posts_listPosts`, and the
|
|
308
304
|
generator turns that id into the client's method and its types' prefix. Put
|
|
309
|
-
`@operationIds` on the
|
|
310
|
-
|
|
305
|
+
`@operationIds` on the service namespace, and each operation's id is its
|
|
306
|
+
name, as written:
|
|
311
307
|
|
|
312
308
|
```tsp
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
309
|
+
@service(#{ title: "Blog" })
|
|
310
|
+
@operationIds
|
|
311
|
+
namespace Blog;
|
|
316
312
|
|
|
317
313
|
@route("/posts")
|
|
318
|
-
@operationIds
|
|
319
314
|
interface Posts {
|
|
320
|
-
@get
|
|
321
|
-
@get
|
|
315
|
+
@get listPosts(...PageParameters): Page<Post>; // listPosts
|
|
316
|
+
@get getPost(@path postId: uuid): Post | NotFound; // getPost
|
|
322
317
|
}
|
|
323
318
|
```
|
|
324
319
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
`duplicate-operation-id`. More in
|
|
320
|
+
On an interface, it names that interface's operations, and those of the
|
|
321
|
+
interfaces extending it. An operation's own `@operationId` wins; the others
|
|
322
|
+
keep the emitter's names. Two operations of one name in one service, in two
|
|
323
|
+
interfaces or copied by `extends`, are an error, `duplicate-operation-id`. More in
|
|
329
324
|
[Operation ids](docs/guide/operation-ids.md).
|
|
330
325
|
|
|
331
326
|
## API
|
|
@@ -334,13 +329,13 @@ of a marked interface that has another operation's id is an error,
|
|
|
334
329
|
|
|
335
330
|
| Decorator | On | What it does |
|
|
336
331
|
| --- | --- | --- |
|
|
337
|
-
| `@operationIds` | an interface |
|
|
332
|
+
| `@operationIds` | a namespace or an interface | makes each operation's id its name, as written, in the namespace however deep or in the interface, unless it has an `@operationId` |
|
|
338
333
|
|
|
339
334
|
### Diagnostics
|
|
340
335
|
|
|
341
336
|
| Code | Reported when |
|
|
342
337
|
| --- | --- |
|
|
343
|
-
| `duplicate-operation-id` | error: an operation
|
|
338
|
+
| `duplicate-operation-id` | error: an operation `@operationIds` names has the id of another operation in its service's document |
|
|
344
339
|
| `duplicate-status-reply` | error: an operation declares a reply without a body and one with a body of one status, which the emitter merges, losing the one without a body |
|
|
345
340
|
| `merged-status-reply` | warning: an operation declares two replies with a body of one status code, which the emitter merges under the first one's description |
|
|
346
341
|
|
|
@@ -386,11 +381,10 @@ of a marked interface that has another operation's id is an error,
|
|
|
386
381
|
| `RateLimitHeaders` | `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, integers, optional |
|
|
387
382
|
| `RetryAfterHeader` | `Retry-After`, seconds, optional |
|
|
388
383
|
|
|
389
|
-
###
|
|
384
|
+
### Sorting
|
|
390
385
|
|
|
391
|
-
|
|
|
386
|
+
| Model | What it is |
|
|
392
387
|
| --- | --- |
|
|
393
|
-
| `Resource<Item, Filters = {}, SortField = "id", Id = uuid>` | an interface template: `list`, `read`, `create`, `update`, `delete` |
|
|
394
388
|
| `SortParameters<Field>` | the query `orderBy?: Field` and `direction?: "asc" \| "desc" = "asc"` |
|
|
395
389
|
|
|
396
390
|
### Responses
|
|
@@ -457,10 +451,10 @@ envelope and the rate limit's headers.
|
|
|
457
451
|
columns `@nxgt/drizzle` stamps;
|
|
458
452
|
- [Headers](docs/guide/headers.md): `Idempotency-Key`, the rate limit and
|
|
459
453
|
`Retry-After`;
|
|
460
|
-
- [
|
|
461
|
-
|
|
462
|
-
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`,
|
|
463
|
-
|
|
454
|
+
- [Sorting](docs/guide/sorting.md): `orderBy` and `direction` beside a
|
|
455
|
+
list's filters, and the `@nxgt/drizzle` call they map to;
|
|
456
|
+
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, each
|
|
457
|
+
operation named as written, and the ids it refuses;
|
|
464
458
|
- [troubleshooting](docs/troubleshooting.md);
|
|
465
459
|
- [the roadmap](docs/roadmap.md): what is coming.
|
|
466
460
|
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,9 @@ var $lib = createTypeSpecLibrary({
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
state: {
|
|
32
|
-
operationIds: {
|
|
32
|
+
operationIds: {
|
|
33
|
+
description: "The interfaces and namespaces marked with @operationIds"
|
|
34
|
+
}
|
|
33
35
|
}
|
|
34
36
|
});
|
|
35
37
|
|
|
@@ -84,7 +86,9 @@ function validateOneReplyPerStatus(program) {
|
|
|
84
86
|
|
|
85
87
|
// src/operation-ids.ts
|
|
86
88
|
import {
|
|
89
|
+
isService,
|
|
87
90
|
isTemplateInstance,
|
|
91
|
+
listServices,
|
|
88
92
|
navigateProgram
|
|
89
93
|
} from "@typespec/compiler";
|
|
90
94
|
import {
|
|
@@ -95,28 +99,64 @@ import {
|
|
|
95
99
|
function operationIds(context, target) {
|
|
96
100
|
context.program.stateSet($lib.stateKeys.operationIds).add(target);
|
|
97
101
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
102
|
+
function isMarkedNamespace(program, target) {
|
|
103
|
+
for (let at = target;at !== undefined; at = at.namespace) {
|
|
104
|
+
if (program.stateSet($lib.stateKeys.operationIds).has(at))
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function isMarkedInterface(program, target) {
|
|
110
|
+
return program.stateSet($lib.stateKeys.operationIds).has(target) || target.sourceInterfaces.some((source) => isMarkedInterface(program, source));
|
|
100
111
|
}
|
|
101
112
|
function isNamed(program, operation) {
|
|
102
113
|
const container = operation.interface;
|
|
103
|
-
|
|
114
|
+
if (container === undefined) {
|
|
115
|
+
return isMarkedNamespace(program, operation.namespace);
|
|
116
|
+
}
|
|
117
|
+
return !isTemplateInstance(container) && (isMarkedInterface(program, container) || isMarkedNamespace(program, container.namespace));
|
|
118
|
+
}
|
|
119
|
+
function isEmitted(operation) {
|
|
120
|
+
const container = operation.interface;
|
|
121
|
+
if (container === undefined)
|
|
122
|
+
return !isTemplateInstance(operation);
|
|
123
|
+
return !isTemplateInstance(container);
|
|
124
|
+
}
|
|
125
|
+
function servicesOf(program, operation) {
|
|
126
|
+
const services = [];
|
|
127
|
+
for (let at = operation.interface?.namespace ?? operation.namespace;at !== undefined; at = at.namespace) {
|
|
128
|
+
if (isService(program, at))
|
|
129
|
+
services.push(at);
|
|
130
|
+
}
|
|
131
|
+
return services;
|
|
104
132
|
}
|
|
105
133
|
function validateOperationIds(program) {
|
|
106
134
|
const seen = new Map;
|
|
135
|
+
const hasServices = listServices(program).length > 0;
|
|
107
136
|
navigateProgram(program, {
|
|
108
137
|
operation(operation) {
|
|
138
|
+
if (!isEmitted(operation))
|
|
139
|
+
return;
|
|
109
140
|
const named = isNamed(program, operation);
|
|
110
141
|
const own = getOperationId(program, operation);
|
|
111
|
-
const id = own ?? (named ?
|
|
112
|
-
|
|
142
|
+
const id = own ?? (named ? operation.name : resolveOperationId(program, operation));
|
|
143
|
+
const services = servicesOf(program, operation);
|
|
144
|
+
const emitted = services.length > 0 || !hasServices;
|
|
145
|
+
let duplicate = false;
|
|
146
|
+
for (const service of services.length > 0 ? services : [undefined]) {
|
|
147
|
+
const ids = seen.get(service) ?? new Map;
|
|
148
|
+
seen.set(service, ids);
|
|
149
|
+
if (emitted && ids.has(id) && (named || ids.get(id)))
|
|
150
|
+
duplicate = true;
|
|
151
|
+
ids.set(id, named || (ids.get(id) ?? false));
|
|
152
|
+
}
|
|
153
|
+
if (duplicate) {
|
|
113
154
|
$lib.reportDiagnostic(program, {
|
|
114
155
|
code: "duplicate-operation-id",
|
|
115
156
|
format: { id },
|
|
116
157
|
target: operation
|
|
117
158
|
});
|
|
118
159
|
}
|
|
119
|
-
seen.set(id, named || (seen.get(id) ?? false));
|
|
120
160
|
if (named && own === undefined)
|
|
121
161
|
setOperationId(program, operation, id);
|
|
122
162
|
}
|
|
@@ -137,5 +177,5 @@ export {
|
|
|
137
177
|
$onValidate
|
|
138
178
|
};
|
|
139
179
|
|
|
140
|
-
//# debugId=
|
|
180
|
+
//# debugId=EF940BB8E619B41E64756E2164756E21
|
|
141
181
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
"sources": ["../src/one-reply-per-status.ts", "../src/lib.ts", "../src/operation-ids.ts", "../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * One reply per status: `@typespec/openapi3` merges two replies of one\n * status into one without a word. A reply without a body beside one with a\n * body is lost in the merge, an error; a `void` body is none, as the emitter\n * sees it. Two bodies of one content type are merged under the first reply's\n * description, a warning where the status is a code. Under `*` or a range,\n * where undocumented `@error` models share one description, they pass.\n * Bodies of different content types are one reply, negotiated, and pass.\n */\nimport { isVoidType, type Operation, type Program } from '@typespec/compiler';\nimport {\n\tgetAllHttpServices,\n\ttype HttpOperationResponse,\n\ttype HttpStatusCodesEntry,\n} from '@typespec/http';\nimport { $lib } from './lib';\n\nfunction statusOf(code: HttpStatusCodesEntry): string {\n\treturn typeof code === 'object' ? `${code.start}-${code.end}` : `${code}`;\n}\n\n/** What the emitter's merge costs: a reply, a description, or nothing. */\nfunction mergeOf({\n\tstatusCodes,\n\tresponses,\n}: HttpOperationResponse):\n\t| 'duplicate-status-reply'\n\t| 'merged-status-reply'\n\t| undefined {\n\tconst bodies = responses.flatMap(({ body }) =>\n\t\tbody && !isVoidType(body.type) ? [body] : [],\n\t);\n\tif (bodies.length > 0 && bodies.length < responses.length) {\n\t\treturn 'duplicate-status-reply';\n\t}\n\tif (typeof statusCodes !== 'number') return undefined;\n\tconst seen = new Set<string>();\n\tfor (const body of bodies) {\n\t\tif (body.contentTypes.some((type) => seen.has(type))) {\n\t\t\treturn 'merged-status-reply';\n\t\t}\n\t\tfor (const type of body.contentTypes) seen.add(type);\n\t}\n\treturn undefined;\n}\n\n/**\n * Checks each operation of each service, as the emitter sees it, once: a\n * service nested in another is listed in both. The services' own\n * diagnostics are the emitter's to report.\n */\nexport function validateOneReplyPerStatus(program: Program): void {\n\tconst [services] = getAllHttpServices(program);\n\tconst checked = new Set<Operation>();\n\tfor (const service of services) {\n\t\tfor (const operation of service.operations) {\n\t\t\tif (checked.has(operation.operation)) continue;\n\t\t\tchecked.add(operation.operation);\n\t\t\tfor (const response of operation.responses) {\n\t\t\t\tconst code = mergeOf(response);\n\t\t\t\tif (code === undefined) continue;\n\t\t\t\t$lib.reportDiagnostic(program, {\n\t\t\t\t\tcode,\n\t\t\t\t\tformat: {\n\t\t\t\t\t\toperation: operation.operation.name,\n\t\t\t\t\t\tstatus: statusOf(response.statusCodes),\n\t\t\t\t\t},\n\t\t\t\t\ttarget: operation.operation,\n\t\t\t\t});\n\t\t\t}\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\t'duplicate-status-reply': {\n\t\t\tseverity: 'error',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} declares a reply without a body and one with a body of status ${'status'}: the emitter merges them into one, and the reply without a body is lost. Declare the one the route sends.`,\n\t\t\t},\n\t\t},\n\t\t'merged-status-reply': {\n\t\t\tseverity: 'warning',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} declares two replies with a body of status ${'status'}: the emitter merges their bodies under the first one's description. Declare the one the route sends.`,\n\t\t\t},\n\t\t},\n\t},\n\tstate: {\n\t\toperationIds: {
|
|
7
|
-
"/**\n * `@operationIds`:
|
|
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\t'duplicate-status-reply': {\n\t\t\tseverity: 'error',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} declares a reply without a body and one with a body of status ${'status'}: the emitter merges them into one, and the reply without a body is lost. Declare the one the route sends.`,\n\t\t\t},\n\t\t},\n\t\t'merged-status-reply': {\n\t\t\tseverity: 'warning',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} declares two replies with a body of status ${'status'}: the emitter merges their bodies under the first one's description. Declare the one the route sends.`,\n\t\t\t},\n\t\t},\n\t},\n\tstate: {\n\t\toperationIds: {\n\t\t\tdescription: 'The interfaces and namespaces marked with @operationIds',\n\t\t},\n\t},\n});\n",
|
|
7
|
+
"/**\n * `@operationIds`: the id of each operation is its name, as written:\n * `@get findPostComments()` is `findPostComments`, where `@typespec/openapi3`\n * would write `Posts_findPostComments`.\n */\nimport {\n\ttype DecoratorContext,\n\ttype Interface,\n\tisService,\n\tisTemplateInstance,\n\tlistServices,\n\ttype Namespace,\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 or the namespace; its ids are set once the program is\n * checked. On a template, TypeSpec runs it on each instance, which\n * `isMarkedInterface` follows to the interfaces extending it.\n */\nexport function operationIds(\n\tcontext: DecoratorContext,\n\ttarget: Interface | Namespace,\n): void {\n\tcontext.program.stateSet($lib.stateKeys.operationIds).add(target);\n}\n\n/** Marked itself, or in a marked namespace, however deep. */\nfunction isMarkedNamespace(\n\tprogram: Program,\n\ttarget: Namespace | undefined,\n): boolean {\n\tfor (let at = target; at !== undefined; at = at.namespace) {\n\t\tif (program.stateSet($lib.stateKeys.operationIds).has(at)) return true;\n\t}\n\treturn false;\n}\n\n/** Marked itself, or extending a marked interface: an instance of a marked template included. */\nfunction isMarkedInterface(program: Program, target: Interface): boolean {\n\treturn (\n\t\tprogram.stateSet($lib.stateKeys.operationIds).has(target) ||\n\t\ttarget.sourceInterfaces.some((source) => isMarkedInterface(program, source))\n\t);\n}\n\n/**\n * Whether `@operationIds` names the operation's id, or checks it: its\n * interface or one of its namespaces is marked. An interface template's\n * instance is never emitted; only the interfaces extending it are.\n */\nfunction isNamed(program: Program, operation: Operation): boolean {\n\tconst container = operation.interface;\n\tif (container === undefined) {\n\t\treturn isMarkedNamespace(program, operation.namespace);\n\t}\n\treturn (\n\t\t!isTemplateInstance(container) &&\n\t\t(isMarkedInterface(program, container) ||\n\t\t\tisMarkedNamespace(program, container.namespace))\n\t);\n}\n\n/**\n * Whether the emitter writes the operation: not an instance of an operation\n * template, `Read<Pet>`, nor an operation of an interface template's\n * instance. An interface extending an instance copies its operations, which\n * are instances too, and emitted.\n */\nfunction isEmitted(operation: Operation): boolean {\n\tconst container = operation.interface;\n\tif (container === undefined) return !isTemplateInstance(operation);\n\treturn !isTemplateInstance(container);\n}\n\n/**\n * The services whose document the operation is in: each `@service`\n * namespace around it, a nested one and the one outside it both. None, in a\n * program without a service, is the one document the emitter writes; in a\n * program with one, no document at all.\n */\nfunction servicesOf(program: Program, operation: Operation): Namespace[] {\n\tconst services: Namespace[] = [];\n\tfor (\n\t\tlet at = operation.interface?.namespace ?? operation.namespace;\n\t\tat !== undefined;\n\t\tat = at.namespace\n\t) {\n\t\tif (isService(program, at)) services.push(at);\n\t}\n\treturn services;\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 * in one service's document, one of them named by `@operationIds`, are an\n * error: one name in two interfaces, an interface that `extends` another and\n * so copies its operations, or an `@operationId` written elsewhere.\n * `@typespec/openapi3` would emit both without a word. An operation\n * template's instance, such as `Read<Pet>`, is never emitted; only the\n * operations declared from it (`op readPet is Read<Pet>`) are.\n */\nexport function validateOperationIds(program: Program): void {\n\tconst seen = new Map<Namespace | undefined, Map<string, boolean>>();\n\tconst hasServices = listServices(program).length > 0;\n\tnavigateProgram(program, {\n\t\toperation(operation) {\n\t\t\tif (!isEmitted(operation)) return;\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.\n\t\t\tconst id =\n\t\t\t\town ??\n\t\t\t\t(named ? operation.name : resolveOperationId(program, operation));\n\t\t\tconst services = servicesOf(program, operation);\n\t\t\t// Outside every service of a program that has one: in no document.\n\t\t\tconst emitted = services.length > 0 || !hasServices;\n\t\t\tlet duplicate = false;\n\t\t\tfor (const service of services.length > 0 ? services : [undefined]) {\n\t\t\t\tconst ids = seen.get(service) ?? new Map<string, boolean>();\n\t\t\t\tseen.set(service, ids);\n\t\t\t\tif (emitted && ids.has(id) && (named || ids.get(id))) duplicate = true;\n\t\t\t\tids.set(id, named || (ids.get(id) ?? false));\n\t\t\t}\n\t\t\tif (duplicate) {\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\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",
|
|
8
8
|
"/**\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 { validateOneReplyPerStatus } from './one-reply-per-status';\nimport { operationIds, validateOperationIds } from './operation-ids';\n\nexport { $lib } from './lib';\n\nexport function $onValidate(program: Program): void {\n\tvalidateOperationIds(program);\n\tvalidateOneReplyPerStatus(program);\n}\n\nexport const $decorators = {\n\tNxgt: { operationIds },\n};\n"
|
|
9
9
|
],
|
|
10
|
-
"mappings": ";AASA;AACA;AAAA;AAAA;;;ACTA;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,IACA,0BAA0B;AAAA,MACzB,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SAAS,eAAe,6EAA6E;AAAA,MACtG;AAAA,IACD;AAAA,IACA,uBAAuB;AAAA,MACtB,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SAAS,eAAe,0DAA0D;AAAA,MACnF;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA,IACN,cAAc,
|
|
11
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "EF940BB8E619B41E64756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
package/dist/lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,IAAI;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,IAAI;;;;;;;;;;uCA2Bf,CAAC"}
|
package/dist/operation-ids.d.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@operationIds`:
|
|
3
|
-
*
|
|
2
|
+
* `@operationIds`: the id of each operation is its name, as written:
|
|
3
|
+
* `@get findPostComments()` is `findPostComments`, where `@typespec/openapi3`
|
|
4
|
+
* would write `Posts_findPostComments`.
|
|
4
5
|
*/
|
|
5
|
-
import { type DecoratorContext, type Interface, type Program } from '@typespec/compiler';
|
|
6
|
+
import { type DecoratorContext, type Interface, type Namespace, type Program } from '@typespec/compiler';
|
|
6
7
|
/**
|
|
7
|
-
* Marks the interface; its ids are set once the program is
|
|
8
|
-
* template, TypeSpec runs it on each instance, which
|
|
9
|
-
* the interfaces extending it.
|
|
8
|
+
* Marks the interface or the namespace; its ids are set once the program is
|
|
9
|
+
* checked. On a template, TypeSpec runs it on each instance, which
|
|
10
|
+
* `isMarkedInterface` follows to the interfaces extending it.
|
|
10
11
|
*/
|
|
11
|
-
export declare function operationIds(context: DecoratorContext, target: Interface): void;
|
|
12
|
+
export declare function operationIds(context: DecoratorContext, target: Interface | Namespace): void;
|
|
12
13
|
/**
|
|
13
14
|
* 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
|
-
*
|
|
16
|
-
* in two
|
|
17
|
-
*
|
|
15
|
+
* `@operationIds` in the source does not matter. Two operations with one id
|
|
16
|
+
* in one service's document, one of them named by `@operationIds`, are an
|
|
17
|
+
* error: one name in two interfaces, an interface that `extends` another and
|
|
18
|
+
* so copies its operations, or an `@operationId` written elsewhere.
|
|
19
|
+
* `@typespec/openapi3` would emit both without a word. An operation
|
|
20
|
+
* template's instance, such as `Read<Pet>`, is never emitted; only the
|
|
21
|
+
* operations declared from it (`op readPet is Read<Pet>`) are.
|
|
18
22
|
*/
|
|
19
23
|
export declare function validateOperationIds(program: Program): void;
|
|
20
24
|
//# sourceMappingURL=operation-ids.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,SAAS,EAId,KAAK,SAAS,EAGd,KAAK,OAAO,EACZ,MAAM,oBAAoB,CAAC;AAQ5B;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,GAAG,SAAS,GAC3B,IAAI,CAEN;AAoED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmC3D"}
|
package/docs/README.md
CHANGED
|
@@ -9,7 +9,7 @@ The [package README](../README.md) is the short version.
|
|
|
9
9
|
| [Authentication](guide/auth.md) | protecting an operation with a `@nxgt/janus` session |
|
|
10
10
|
| [Scalars and columns](guide/columns.md) | typing ids and addresses, and the columns a row carries |
|
|
11
11
|
| [Headers](guide/headers.md) | making a write idempotent, or declaring a rate limit |
|
|
12
|
-
| [
|
|
13
|
-
| [Operation ids](guide/operation-ids.md) | naming the generated client's
|
|
12
|
+
| [Sorting](guide/sorting.md) | letting a client filter and sort a list |
|
|
13
|
+
| [Operation ids](guide/operation-ids.md) | naming each operation, and so the generated client's method, as written |
|
|
14
14
|
| [Troubleshooting](troubleshooting.md) | the compiler or the generator refused the spec |
|
|
15
15
|
| [Roadmap](roadmap.md) | wondering what is coming |
|
package/docs/guide/auth.md
CHANGED
|
@@ -16,10 +16,10 @@ namespace Blog;
|
|
|
16
16
|
|
|
17
17
|
@route("/posts/{postId}/comments")
|
|
18
18
|
interface Comments {
|
|
19
|
-
@get
|
|
19
|
+
@get findPostComments(@path postId: string): Comment[];
|
|
20
20
|
|
|
21
21
|
@useAuth(JanusAuth)
|
|
22
|
-
@delete
|
|
22
|
+
@delete deletePostComment(@path postId: string, @path commentId: string):
|
|
23
23
|
NoContentResponse | AuthenticationRequired | AccessDenied | ErrorWithoutBody<404>;
|
|
24
24
|
}
|
|
25
25
|
```
|
|
@@ -91,7 +91,7 @@ operation can declare the guard's 401 and the handler's 403:
|
|
|
91
91
|
|
|
92
92
|
```tsp
|
|
93
93
|
@useAuth(JanusAuth)
|
|
94
|
-
@post
|
|
94
|
+
@post createPost(@body post: Create<Post>): {
|
|
95
95
|
@statusCode _: 201;
|
|
96
96
|
@body post: Post;
|
|
97
97
|
} | BadRequest | AuthenticationRequired | Forbidden;
|
package/docs/guide/columns.md
CHANGED
|
@@ -80,7 +80,7 @@ that version, and throws `OptimisticLockError` otherwise. Declare the
|
|
|
80
80
|
`Conflict` it becomes:
|
|
81
81
|
|
|
82
82
|
```tsp
|
|
83
|
-
@patch
|
|
83
|
+
@patch updatePost(@path postId: uuid, @body post: MergePatchUpdate<Post>):
|
|
84
84
|
Post | BadRequest | NotFound | Conflict;
|
|
85
85
|
```
|
|
86
86
|
|
package/docs/guide/errors.md
CHANGED
|
@@ -54,7 +54,7 @@ namespace Blog;
|
|
|
54
54
|
|
|
55
55
|
@route("/posts")
|
|
56
56
|
interface Posts {
|
|
57
|
-
@get
|
|
57
|
+
@get getPost(@path postId: string): Post | NotFound;
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -143,7 +143,7 @@ A handler may send a 400 of its own, without `issues`. `BadRequestBody`
|
|
|
143
143
|
describes both, with `issues` optional:
|
|
144
144
|
|
|
145
145
|
```tsp
|
|
146
|
-
@patch
|
|
146
|
+
@patch updatePost(@path postId: string, @body post: MergePatchUpdate<Post>): Post | BadRequest;
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
The generator declares the validators' 400 itself, as `ValidationErrorBody`,
|
|
@@ -170,7 +170,7 @@ its own way, through `onValidationError`, turns that off with the generator's
|
|
|
170
170
|
Name them in an operation's return type, beside the success:
|
|
171
171
|
|
|
172
172
|
```tsp
|
|
173
|
-
@get
|
|
173
|
+
@get getPost(@path postId: string): Post | NotFound | Unauthorized | Forbidden;
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
## Another status, named
|
package/docs/guide/headers.md
CHANGED
|
@@ -19,7 +19,7 @@ namespace Blog;
|
|
|
19
19
|
|
|
20
20
|
@route("/posts")
|
|
21
21
|
interface Posts {
|
|
22
|
-
@post
|
|
22
|
+
@post createPost(...IdempotencyKeyHeader, @body post: Create<Post>): {
|
|
23
23
|
@statusCode _: 201;
|
|
24
24
|
...IdempotentReplayedHeader;
|
|
25
25
|
@body post: Post;
|
|
@@ -48,7 +48,7 @@ lock, gets one 409 with the description of the first, and the library warns,
|
|
|
48
48
|
## A rate limit
|
|
49
49
|
|
|
50
50
|
```tsp
|
|
51
|
-
@post
|
|
51
|
+
@post createPost(@body post: Create<Post>): {
|
|
52
52
|
@statusCode _: 201;
|
|
53
53
|
...RateLimitHeaders;
|
|
54
54
|
@body post: Post;
|