@nxgt/typespec 0.11.0 → 0.13.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 +80 -10
- package/dist/emitter.d.ts +12 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/http-operations.d.ts +9 -0
- package/dist/http-operations.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +231 -55
- package/dist/index.js.map +12 -8
- package/dist/lib.d.ts +13 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/one-reply-per-status.d.ts +1 -5
- package/dist/one-reply-per-status.d.ts.map +1 -1
- package/dist/query-method.d.ts +18 -0
- package/dist/query-method.d.ts.map +1 -0
- package/dist/query-operations.d.ts +8 -0
- package/dist/query-operations.d.ts.map +1 -0
- package/dist/verb-methods.d.ts.map +1 -1
- package/dist/verbs.d.ts +2 -2
- package/docs/README.md +1 -0
- package/docs/guide/emitter.md +159 -0
- package/docs/guide/errors.md +2 -2
- package/docs/guide/getting-started.md +9 -5
- package/docs/guide/linter.md +2 -2
- package/docs/guide/operation-ids.md +62 -5
- package/docs/roadmap.md +23 -4
- package/docs/troubleshooting.md +102 -2
- package/lib/main.tsp +1 -0
- package/lib/query-method.tsp +26 -0
- package/package.json +16 -7
- package/templates/scaffolding.json +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
nxgt's HTTP conventions as a [TypeSpec](https://typespec.io) library, so an
|
|
4
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`,
|
|
5
|
+
OpenAPI 3.1 or 3.2 with its emitter, `@nxgt/typespec` (`@typespec/openapi3`, and a
|
|
6
|
+
real `QUERY` in 3.2), then generate the code with
|
|
6
7
|
[`@nxgt/openapi-codegen`](https://github.com/softistx/nxgt-http/blob/develop/packages/openapi-codegen/README.md).
|
|
7
8
|
Each shape here is one the `@nxgt/*` packages already send on the wire.
|
|
8
9
|
|
|
@@ -18,20 +19,22 @@ bun add -d @nxgt/typespec @typespec/compiler @typespec/http @typespec/openapi @t
|
|
|
18
19
|
|
|
19
20
|
`@typespec/compiler`, `@typespec/http` and `@typespec/openapi` 1.16 or later
|
|
20
21
|
are peer dependencies,
|
|
21
|
-
and `typescript` 6 or 7 too, as for every `@nxgt` package.
|
|
22
|
-
|
|
22
|
+
and `typescript` 6 or 7 too, as for every `@nxgt` package.
|
|
23
|
+
`@typespec/openapi3` 1.16 or later is an optional peer, loaded only when the
|
|
24
|
+
spec is emitted: install it to compile the spec to OpenAPI.
|
|
25
|
+
`@nxgt/openapi-codegen` generates the code from the document.
|
|
23
26
|
The generator's CLI, `nxgt-openapi`, runs on [Bun](https://bun.sh): it needs
|
|
24
27
|
`bun` on the PATH, even when called through `npx` or an npm script.
|
|
25
|
-
Emit OpenAPI 3.1 or 3.2 in `tspconfig.yaml
|
|
26
|
-
by default, and the generator refuses it. Every convention here is compiled
|
|
28
|
+
Emit OpenAPI 3.1 or 3.2 in `tspconfig.yaml` with this package's emitter:
|
|
29
|
+
`@typespec/openapi3` emits 3.0 by default, and the generator refuses it. Every convention here is compiled
|
|
27
30
|
to both in CI, and generates the same code from either.
|
|
28
31
|
|
|
29
32
|
```yaml
|
|
30
33
|
# api/tspconfig.yaml
|
|
31
34
|
emit:
|
|
32
|
-
- '@typespec
|
|
35
|
+
- '@nxgt/typespec'
|
|
33
36
|
options:
|
|
34
|
-
'@typespec
|
|
37
|
+
'@nxgt/typespec':
|
|
35
38
|
openapi-versions: ['3.1.0']
|
|
36
39
|
emitter-output-dir: '{project-root}/../openapi'
|
|
37
40
|
output-file: openapi.yaml
|
|
@@ -56,7 +59,7 @@ It installs the compiler, `@typespec/http`, `@typespec/openapi`,
|
|
|
56
59
|
`@typespec/openapi3`, this package and `@nxgt/openapi-codegen`, and writes:
|
|
57
60
|
|
|
58
61
|
- `tspconfig.yaml`: the linter's recommended rules, and OpenAPI 3.1 written
|
|
59
|
-
to `openapi/openapi.yaml`;
|
|
62
|
+
to `openapi/openapi.yaml` by the emitter `@nxgt/typespec`;
|
|
60
63
|
- `main.tsp`: the service in the namespace `Api`, with `@operationIds`, and a
|
|
61
64
|
`Users` resource with a paged, sorted list, `get`, `create`, `update` and
|
|
62
65
|
`delete`, and their error aliases;
|
|
@@ -456,6 +459,58 @@ The verbs `verbs` adds are not checked, nor an operation with its own
|
|
|
456
459
|
`@operationId`. More, with each verb's methods, in
|
|
457
460
|
[Operation ids](docs/guide/operation-ids.md).
|
|
458
461
|
|
|
462
|
+
### A search with a body
|
|
463
|
+
|
|
464
|
+
`QUERY` is the HTTP method for a safe request with a body, and
|
|
465
|
+
`@typespec/http` 1.17 declares none. Mark a `@post` with `@queryMethod`, and
|
|
466
|
+
emit OpenAPI 3.2 with the emitter [`@nxgt/typespec`](#the-emitter): the
|
|
467
|
+
document gets a real `query` operation. Only 3.2 has one, so the mark with
|
|
468
|
+
any older version in `openapi-versions`, or with the plain `@typespec/openapi3`
|
|
469
|
+
emitter, is an error, not a `POST`: `query-method-needs-openapi-3.2`,
|
|
470
|
+
`query-method-needs-nxgt-emitter`.
|
|
471
|
+
|
|
472
|
+
```tsp
|
|
473
|
+
@route("/authors")
|
|
474
|
+
interface Authors {
|
|
475
|
+
@post @queryMethod @route("/search")
|
|
476
|
+
search(@body criteria: AuthorCriteria): Page<Author> | BadRequest; // searchAuthors
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`search` and `query` accept a `GET`, a `POST` or a marked `POST` without the
|
|
481
|
+
`verb-method-mismatch` warning; `findById` or `create` marked warn, as a
|
|
482
|
+
`QUERY` is not their method. The decorator on anything but a `@post` is the
|
|
483
|
+
error `query-method-not-post`. When `@typespec/http` declares `QUERY`, the
|
|
484
|
+
decorator is what changes, not your spec. More, in
|
|
485
|
+
[Operation ids](docs/guide/operation-ids.md#a-query-in-openapi-32) and
|
|
486
|
+
[The emitter](docs/guide/emitter.md).
|
|
487
|
+
|
|
488
|
+
### The emitter
|
|
489
|
+
|
|
490
|
+
`@nxgt/typespec` is also a TypeSpec emitter. It runs `@typespec/openapi3` with
|
|
491
|
+
the options and the files you give that one, and in an OpenAPI 3.2 document
|
|
492
|
+
moves each `@post` marked `@queryMethod` to a `query` operation, in place and
|
|
493
|
+
without `x-nxgt-method`. Without a marked operation its output is
|
|
494
|
+
byte-identical to `@typespec/openapi3`'s. With a marked operation and an
|
|
495
|
+
older version in `openapi-versions` (unset means 3.0), it reports
|
|
496
|
+
`query-method-needs-openapi-3.2` and emits nothing.
|
|
497
|
+
|
|
498
|
+
```yaml
|
|
499
|
+
# api/tspconfig.yaml
|
|
500
|
+
emit:
|
|
501
|
+
- '@nxgt/typespec'
|
|
502
|
+
options:
|
|
503
|
+
'@nxgt/typespec':
|
|
504
|
+
openapi-versions: ['3.2.0'] # 3.1.0 is an error beside a @queryMethod
|
|
505
|
+
emitter-output-dir: '{project-root}/../openapi'
|
|
506
|
+
output-file: openapi.yaml
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`@nxgt/openapi-codegen` then generates `method: 'query'`, `@nxgt/openapi-hono`
|
|
510
|
+
serves `.query(...)`, and the `@nxgt/httpyz` client sends `QUERY`. A real
|
|
511
|
+
`QUERY` needs a server stack that accepts the method, and a preflight under
|
|
512
|
+
CORS: [The emitter](docs/guide/emitter.md#a-query-on-the-wire).
|
|
513
|
+
|
|
459
514
|
### Linter
|
|
460
515
|
|
|
461
516
|
Extend the library's ruleset in `tspconfig.yaml`, and `tsp compile` warns
|
|
@@ -490,12 +545,21 @@ passing example of each rule, in [Linter](docs/guide/linter.md).
|
|
|
490
545
|
|
|
491
546
|
| Decorator | On | What it does |
|
|
492
547
|
| --- | --- | --- |
|
|
548
|
+
| `@queryMethod` | an operation | marks a `@post` as a `QUERY`: needs the emitter `@nxgt/typespec` and OpenAPI 3.2, where the document gets a `query` operation, and is an error otherwise; `@operationIds` checks its verb as a `QUERY`'s |
|
|
493
549
|
| `@operationIds(options?: OperationIdsOptions)` | a namespace or an interface | makes each operation's id its name, as written, or a known verb with the interface's resource (`list` in `Users` is `listUsers`), in the namespace however deep or in the interface, unless it has an `@operationId` |
|
|
494
550
|
|
|
495
551
|
`OperationIdsOptions` is `#{ singular?: string, plural?: string, verbs?: Record<"singular" | "plural"> }`:
|
|
496
552
|
`singular` and `plural` name an interface's resource, and `verbs` adds or
|
|
497
553
|
overrides verbs, on a namespace or an interface.
|
|
498
554
|
|
|
555
|
+
`@queryMethod` has no options.
|
|
556
|
+
|
|
557
|
+
### Emitter
|
|
558
|
+
|
|
559
|
+
| Name | In | What it does |
|
|
560
|
+
| --- | --- | --- |
|
|
561
|
+
| `@nxgt/typespec` | `emit:` in `tspconfig.yaml` | `@typespec/openapi3` with its options and files; each marked `@post` becomes a `query` operation in a 3.2 document, and a marked one with an older version is an error. Needs the optional peer `@typespec/openapi3` |
|
|
562
|
+
|
|
499
563
|
### Diagnostics
|
|
500
564
|
|
|
501
565
|
| Code | Reported when |
|
|
@@ -504,7 +568,11 @@ overrides verbs, on a namespace or an interface.
|
|
|
504
568
|
| `resource-name-on-namespace` | error: `@operationIds` on a namespace is given `singular` or `plural`, which name an interface's resource |
|
|
505
569
|
| `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 |
|
|
506
570
|
| `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 |
|
|
507
|
-
| `verb-method-mismatch` | warning: an operation `@operationIds` names after one of the library's verbs, in an interface, is sent with a method that verb does not name: `create` with a `GET` |
|
|
571
|
+
| `verb-method-mismatch` | warning: an operation `@operationIds` names after one of the library's verbs, in an interface, is sent with a method that verb does not name: `create` with a `GET`, or marked `@queryMethod` |
|
|
572
|
+
| `query-method-not-post` | error: an operation marked `@queryMethod` is not a `@post` |
|
|
573
|
+
| `query-method-needs-openapi-3.2` | error: the emitter `@nxgt/typespec` is asked for a version below 3.2 (or none, which is 3.0) beside a `@queryMethod`; nothing is emitted |
|
|
574
|
+
| `query-method-needs-nxgt-emitter` | error: `@typespec/openapi3` is in `emit` beside a `@queryMethod` |
|
|
575
|
+
| `openapi3-missing` | error: the emitter `@nxgt/typespec` runs `@typespec/openapi3`, which is not installed |
|
|
508
576
|
|
|
509
577
|
### Linter rules
|
|
510
578
|
|
|
@@ -663,6 +731,8 @@ envelope and the rate limit's headers.
|
|
|
663
731
|
|
|
664
732
|
- [Getting started](docs/guide/getting-started.md): the `tsp init`
|
|
665
733
|
template, the files it writes, and the steps from the spec to a Hono route;
|
|
734
|
+
- [The emitter](docs/guide/emitter.md): `@nxgt/typespec` in `tspconfig.yaml`,
|
|
735
|
+
a `@queryMethod` as a real `QUERY` in OpenAPI 3.2, and what that needs;
|
|
666
736
|
- [Error replies](docs/guide/errors.md): the envelope, each response, the
|
|
667
737
|
aliases by verb, and what the generator makes of them;
|
|
668
738
|
- [Pagination](docs/guide/pagination.md): offset and cursor pages, and
|
|
@@ -681,7 +751,7 @@ envelope and the rate limit's headers.
|
|
|
681
751
|
and `PatchRequest`, and naming your own;
|
|
682
752
|
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, each
|
|
683
753
|
operation named as written or a verb with its resource, the method each
|
|
684
|
-
verb is sent with, the options, and the ids it refuses;
|
|
754
|
+
verb is sent with, `@queryMethod`, the options, and the ids it refuses;
|
|
685
755
|
- [Linter](docs/guide/linter.md): the ruleset `@nxgt/typespec/recommended`,
|
|
686
756
|
each rule with a failing and a passing spec, and how to turn one off;
|
|
687
757
|
- [troubleshooting](docs/troubleshooting.md);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The emitter `@nxgt/typespec`: `@typespec/openapi3`, with its options,
|
|
3
|
+
* checked by its own schema, and its files, and each `POST` marked
|
|
4
|
+
* `@queryMethod` a real `QUERY` in an OpenAPI 3.2 document
|
|
5
|
+
* (`query-operations.ts`). Only 3.2 has a `QUERY`: a marked operation with
|
|
6
|
+
* an older version asked for is an error, and nothing is emitted.
|
|
7
|
+
* `@typespec/openapi3` is loaded only here, so a spec that only imports the
|
|
8
|
+
* library does not need it.
|
|
9
|
+
*/
|
|
10
|
+
import { type EmitContext } from '@typespec/compiler';
|
|
11
|
+
export declare function $onEmit(context: EmitContext): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=emitter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emitter.d.ts","sourceRoot":"","sources":["../src/emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGN,KAAK,WAAW,EAEhB,MAAM,oBAAoB,CAAC;AAmE5B,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BjE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each HTTP operation of each service, once, as the emitter sends it: a
|
|
3
|
+
* service nested in another lists its operations in both. The services' own
|
|
4
|
+
* diagnostics are the emitter's to report.
|
|
5
|
+
*/
|
|
6
|
+
import type { Program } from '@typespec/compiler';
|
|
7
|
+
import { type HttpOperation } from '@typespec/http';
|
|
8
|
+
export declare function eachHttpOperation(program: Program, visit: (operation: HttpOperation) => void): void;
|
|
9
|
+
//# sourceMappingURL=http-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-operations.d.ts","sourceRoot":"","sources":["../src/http-operations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAExE,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,IAAI,GACvC,IAAI,CAUN"}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { Program } from '@typespec/compiler';
|
|
10
10
|
import { operationIds } from './operation-ids';
|
|
11
|
+
import { queryMethod } from './query-method';
|
|
12
|
+
export { $onEmit } from './emitter';
|
|
11
13
|
export { $lib } from './lib';
|
|
12
14
|
export { $linter } from './linter';
|
|
13
15
|
export declare function $onValidate(program: Program): void;
|
|
14
16
|
export declare const $decorators: {
|
|
15
17
|
Nxgt: {
|
|
16
18
|
operationIds: typeof operationIds;
|
|
19
|
+
queryMethod: typeof queryMethod;
|
|
17
20
|
};
|
|
18
21
|
};
|
|
19
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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;AAElD,OAAO,EAAE,YAAY,EAAwB,MAAM,iBAAiB,CAAC;
|
|
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;AAElD,OAAO,EAAE,YAAY,EAAwB,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAwB,MAAM,gBAAgB,CAAC;AAGnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOlD;AAED,eAAO,MAAM,WAAW;;;;;CAEvB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
// src/one-reply-per-status.ts
|
|
2
2
|
import { isVoidType } from "@typespec/compiler";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "@typespec/http";
|
|
3
|
+
|
|
4
|
+
// src/http-operations.ts
|
|
5
|
+
import { getAllHttpServices } from "@typespec/http";
|
|
6
|
+
function eachHttpOperation(program, visit) {
|
|
7
|
+
const [services] = getAllHttpServices(program);
|
|
8
|
+
const visited = new Set;
|
|
9
|
+
for (const service of services) {
|
|
10
|
+
for (const operation of service.operations) {
|
|
11
|
+
if (visited.has(operation.operation))
|
|
12
|
+
continue;
|
|
13
|
+
visited.add(operation.operation);
|
|
14
|
+
visit(operation);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
// src/lib.ts
|
|
8
20
|
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
|
|
@@ -27,6 +39,30 @@ var $lib = createTypeSpecLibrary({
|
|
|
27
39
|
default: paramMessage`${"operation"} is sent with ${"method"}, where its verb ${"verb"} is sent with ${"expected"}. Give it that method, or a name that is not a verb.`
|
|
28
40
|
}
|
|
29
41
|
},
|
|
42
|
+
"query-method-not-post": {
|
|
43
|
+
severity: "error",
|
|
44
|
+
messages: {
|
|
45
|
+
default: paramMessage`${"operation"} is marked @queryMethod and sent with ${"method"}: @queryMethod marks a @post, which @nxgt/typespec writes as a QUERY in OpenAPI 3.2. Make it a @post.`
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"query-method-needs-openapi-3.2": {
|
|
49
|
+
severity: "error",
|
|
50
|
+
messages: {
|
|
51
|
+
default: paramMessage`${"operation"} is marked @queryMethod: only OpenAPI 3.2 has a QUERY operation, and openapi-versions asks for ${"version"}. Emit 3.2 only, or take off @queryMethod.`
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"query-method-needs-nxgt-emitter": {
|
|
55
|
+
severity: "error",
|
|
56
|
+
messages: {
|
|
57
|
+
default: paramMessage`${"operation"} is marked @queryMethod: emit with @nxgt/typespec, which writes it as a QUERY; @typespec/openapi3 would write a POST.`
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"openapi3-missing": {
|
|
61
|
+
severity: "error",
|
|
62
|
+
messages: {
|
|
63
|
+
default: "The emitter @nxgt/typespec runs @typespec/openapi3, which is not installed: add it beside @nxgt/typespec."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
30
66
|
"duplicate-status-reply": {
|
|
31
67
|
severity: "error",
|
|
32
68
|
messages: {
|
|
@@ -40,7 +76,11 @@ var $lib = createTypeSpecLibrary({
|
|
|
40
76
|
}
|
|
41
77
|
}
|
|
42
78
|
},
|
|
79
|
+
capabilities: { dryRun: true },
|
|
43
80
|
state: {
|
|
81
|
+
queryMethod: {
|
|
82
|
+
description: "The operations marked with @queryMethod"
|
|
83
|
+
},
|
|
44
84
|
operationIds: {
|
|
45
85
|
description: "The interfaces and namespaces marked with @operationIds"
|
|
46
86
|
},
|
|
@@ -75,28 +115,21 @@ function mergeOf({
|
|
|
75
115
|
return;
|
|
76
116
|
}
|
|
77
117
|
function validateOneReplyPerStatus(program) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (checked.has(operation.operation))
|
|
118
|
+
eachHttpOperation(program, (operation) => {
|
|
119
|
+
for (const response of operation.responses) {
|
|
120
|
+
const code = mergeOf(response);
|
|
121
|
+
if (code === undefined)
|
|
83
122
|
continue;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
operation: operation.operation.name,
|
|
93
|
-
status: statusOf(response.statusCodes)
|
|
94
|
-
},
|
|
95
|
-
target: operation.operation
|
|
96
|
-
});
|
|
97
|
-
}
|
|
123
|
+
$lib.reportDiagnostic(program, {
|
|
124
|
+
code,
|
|
125
|
+
format: {
|
|
126
|
+
operation: operation.operation.name,
|
|
127
|
+
status: statusOf(response.statusCodes)
|
|
128
|
+
},
|
|
129
|
+
target: operation.operation
|
|
130
|
+
});
|
|
98
131
|
}
|
|
99
|
-
}
|
|
132
|
+
});
|
|
100
133
|
}
|
|
101
134
|
|
|
102
135
|
// src/operation-ids.ts
|
|
@@ -137,8 +170,8 @@ var METHODS = {
|
|
|
137
170
|
find: ["get", "head"],
|
|
138
171
|
count: ["get", "head"],
|
|
139
172
|
get: ["get", "head"],
|
|
140
|
-
search: ["get", "post"],
|
|
141
|
-
query: ["get", "post"],
|
|
173
|
+
search: ["get", "post", "query"],
|
|
174
|
+
query: ["get", "post", "query"],
|
|
142
175
|
create: ["post"],
|
|
143
176
|
createMany: ["post"],
|
|
144
177
|
update: ["put", "patch"],
|
|
@@ -314,41 +347,182 @@ function validateOperationIds(program) {
|
|
|
314
347
|
});
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
// src/query-method.ts
|
|
351
|
+
import { setExtension } from "@typespec/openapi";
|
|
352
|
+
function queryMethod(context, target) {
|
|
353
|
+
const { program } = context;
|
|
354
|
+
program.stateSet($lib.stateKeys.queryMethod).add(target);
|
|
355
|
+
setExtension(program, target, "x-nxgt-method", "query");
|
|
356
|
+
}
|
|
357
|
+
function isQueryMethod(program, operation) {
|
|
358
|
+
return program.stateSet($lib.stateKeys.queryMethod).has(operation);
|
|
359
|
+
}
|
|
360
|
+
function queryMethods(program) {
|
|
361
|
+
return [...program.stateSet($lib.stateKeys.queryMethod)];
|
|
362
|
+
}
|
|
363
|
+
function validateQueryMethods(program) {
|
|
364
|
+
eachHttpOperation(program, ({ operation, verb: method }) => {
|
|
365
|
+
if (!isQueryMethod(program, operation) || method === "post")
|
|
366
|
+
return;
|
|
367
|
+
$lib.reportDiagnostic(program, {
|
|
368
|
+
code: "query-method-not-post",
|
|
369
|
+
format: { operation: operation.name, method: method.toUpperCase() },
|
|
370
|
+
target: operation
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
if (!(program.compilerOptions.emit ?? []).includes("@typespec/openapi3")) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
for (const operation of queryMethods(program)) {
|
|
377
|
+
$lib.reportDiagnostic(program, {
|
|
378
|
+
code: "query-method-needs-nxgt-emitter",
|
|
379
|
+
format: { operation: operation.name },
|
|
380
|
+
target: operation
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
317
385
|
// src/verb-methods.ts
|
|
318
|
-
import { getAllHttpServices as getAllHttpServices2 } from "@typespec/http";
|
|
319
386
|
import { getOperationId as getOperationId2 } from "@typespec/openapi";
|
|
320
387
|
function methodsOf(methods) {
|
|
321
388
|
return methods.map((method) => method.toUpperCase()).join(" or ");
|
|
322
389
|
}
|
|
323
390
|
function validateVerbMethods(program) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
391
|
+
eachHttpOperation(program, ({ operation, verb: sent }) => {
|
|
392
|
+
if (!isNamed(program, operation))
|
|
393
|
+
return;
|
|
394
|
+
if (getOperationId2(program, operation) !== undefined)
|
|
395
|
+
return;
|
|
396
|
+
const verb = verbOf(program, operation);
|
|
397
|
+
if (verb === undefined || !Object.hasOwn(METHODS, verb))
|
|
398
|
+
return;
|
|
399
|
+
const method = sent === "post" && isQueryMethod(program, operation) ? "query" : sent;
|
|
400
|
+
const expected = METHODS[verb];
|
|
401
|
+
if (expected.includes(method))
|
|
402
|
+
return;
|
|
403
|
+
$lib.reportDiagnostic(program, {
|
|
404
|
+
code: "verb-method-mismatch",
|
|
405
|
+
format: {
|
|
406
|
+
operation: operation.name,
|
|
407
|
+
method: method.toUpperCase(),
|
|
408
|
+
verb,
|
|
409
|
+
expected: methodsOf(expected)
|
|
410
|
+
},
|
|
411
|
+
target: operation
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/emitter.ts
|
|
417
|
+
import {
|
|
418
|
+
NoTarget
|
|
419
|
+
} from "@typespec/compiler";
|
|
420
|
+
|
|
421
|
+
// src/query-operations.ts
|
|
422
|
+
import { parse, stringify } from "yaml";
|
|
423
|
+
function moveQueries(document) {
|
|
424
|
+
if (!String(document["openapi"]).startsWith("3.2"))
|
|
425
|
+
return false;
|
|
426
|
+
const paths = document["paths"];
|
|
427
|
+
if (typeof paths !== "object" || paths === null)
|
|
428
|
+
return false;
|
|
429
|
+
let moved = false;
|
|
430
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
431
|
+
const post = item["post"];
|
|
432
|
+
if (post?.["x-nxgt-method"] !== "query" || item["query"] !== undefined) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const { "x-nxgt-method": _, ...operation } = post;
|
|
436
|
+
paths[path] = Object.fromEntries(Object.entries(item).map(([key, value]) => key === "post" ? ["query", operation] : [key, value]));
|
|
437
|
+
moved = true;
|
|
438
|
+
}
|
|
439
|
+
return moved;
|
|
440
|
+
}
|
|
441
|
+
function withQueryOperations(content) {
|
|
442
|
+
const json = content.trimStart().startsWith("{");
|
|
443
|
+
const document = json ? JSON.parse(content) : parse(content);
|
|
444
|
+
if (typeof document !== "object" || document === null)
|
|
445
|
+
return content;
|
|
446
|
+
if (!moveQueries(document))
|
|
447
|
+
return content;
|
|
448
|
+
const text = json ? `${JSON.stringify(document, null, 2)}
|
|
449
|
+
` : stringify(document, {
|
|
450
|
+
singleQuote: true,
|
|
451
|
+
aliasDuplicateObjects: false,
|
|
452
|
+
lineWidth: 0,
|
|
453
|
+
compat: "yaml-1.1"
|
|
454
|
+
});
|
|
455
|
+
return content.includes(`\r
|
|
456
|
+
`) ? text.replace(/\n/g, `\r
|
|
457
|
+
`) : text;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/emitter.ts
|
|
461
|
+
var DEFAULT_VERSIONS = ["3.0.0"];
|
|
462
|
+
function invalidOptions(openapi3, options) {
|
|
463
|
+
const lib = openapi3.$lib;
|
|
464
|
+
const validator = "emitterOptionValidator" in lib ? lib.emitterOptionValidator : undefined;
|
|
465
|
+
if (typeof validator !== "object" || validator === null || !("validate" in validator) || typeof validator.validate !== "function") {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
return validator.validate(options, NoTarget);
|
|
469
|
+
}
|
|
470
|
+
async function loadOpenAPI3(context) {
|
|
471
|
+
try {
|
|
472
|
+
return await import("@typespec/openapi3");
|
|
473
|
+
} catch {
|
|
474
|
+
$lib.reportDiagnostic(context.program, {
|
|
475
|
+
code: "openapi3-missing",
|
|
476
|
+
target: NoTarget
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function queriesFit(context) {
|
|
482
|
+
const { program } = context;
|
|
483
|
+
const marked = queryMethods(program);
|
|
484
|
+
if (marked.length === 0)
|
|
485
|
+
return true;
|
|
486
|
+
const versions = context.options["openapi-versions"] ?? DEFAULT_VERSIONS;
|
|
487
|
+
const older = versions.find((version) => !version.startsWith("3.2"));
|
|
488
|
+
if (older === undefined)
|
|
489
|
+
return true;
|
|
490
|
+
for (const operation of marked) {
|
|
491
|
+
$lib.reportDiagnostic(program, {
|
|
492
|
+
code: "query-method-needs-openapi-3.2",
|
|
493
|
+
format: { operation: operation.name, version: older },
|
|
494
|
+
target: operation
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
async function $onEmit(context) {
|
|
500
|
+
const openapi3 = await loadOpenAPI3(context);
|
|
501
|
+
if (openapi3 === undefined)
|
|
502
|
+
return;
|
|
503
|
+
const { program } = context;
|
|
504
|
+
const invalid = invalidOptions(openapi3, context.options);
|
|
505
|
+
if (invalid.length > 0) {
|
|
506
|
+
program.reportDiagnostics(invalid);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!queriesFit(context))
|
|
510
|
+
return;
|
|
511
|
+
const host = program.host;
|
|
512
|
+
const writing = new Proxy(host, {
|
|
513
|
+
get(target, key) {
|
|
514
|
+
if (key === "writeFile") {
|
|
515
|
+
return (path, content) => target.writeFile(path, withQueryOperations(content));
|
|
516
|
+
}
|
|
517
|
+
const value = Reflect.get(target, key, target);
|
|
518
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
351
519
|
}
|
|
520
|
+
});
|
|
521
|
+
program.host = writing;
|
|
522
|
+
try {
|
|
523
|
+
await openapi3.$onEmit(context);
|
|
524
|
+
} finally {
|
|
525
|
+
program.host = host;
|
|
352
526
|
}
|
|
353
527
|
}
|
|
354
528
|
// src/linter.ts
|
|
@@ -475,19 +649,21 @@ var $linter = defineLinter({
|
|
|
475
649
|
|
|
476
650
|
// src/index.ts
|
|
477
651
|
function $onValidate(program) {
|
|
652
|
+
validateQueryMethods(program);
|
|
478
653
|
validateVerbMethods(program);
|
|
479
654
|
validateOperationIds(program);
|
|
480
655
|
validateOneReplyPerStatus(program);
|
|
481
656
|
}
|
|
482
657
|
var $decorators = {
|
|
483
|
-
Nxgt: { operationIds }
|
|
658
|
+
Nxgt: { operationIds, queryMethod }
|
|
484
659
|
};
|
|
485
660
|
export {
|
|
486
661
|
$decorators,
|
|
487
662
|
$lib,
|
|
488
663
|
$linter,
|
|
664
|
+
$onEmit,
|
|
489
665
|
$onValidate
|
|
490
666
|
};
|
|
491
667
|
|
|
492
|
-
//# debugId=
|
|
668
|
+
//# debugId=3B14A4A66C69185964756E2164756E21
|
|
493
669
|
//# sourceMappingURL=index.js.map
|