@nxgt/typespec 0.7.0 → 0.9.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.
@@ -27,6 +27,67 @@ no `package.json` to resolve it from.
27
27
  devDependencies, with the command in the
28
28
  [README's Install section](../README.md#install).
29
29
 
30
+ ## `init-template-invalid-json: Unable to parse …/scaffolding.json: Unexpected token … is not valid JSON`
31
+
32
+ **When:** `tsp init` with the package's template, before it writes anything.
33
+
34
+ ```text
35
+ error init-template-invalid-json: Unable to parse https://unpkg.com/@nxgt/typespec@0.8.0/templates/scaffolding.json: Unexpected token 'N', "Not found:"... is not valid JSON. Check that the template URL is correct.
36
+ ```
37
+
38
+ **Why:** the URL names no template, so unpkg answers a page of text, `Not
39
+ found: …` or `Package version not found: …`, which `tsp init` cannot parse.
40
+ The template ships from 0.9.0 on: a version before it in the URL, a version
41
+ that is not published, or a typo in the path all answer that page.
42
+
43
+ **Fix:** drop the version, or name 0.9.0 or later, and keep the path as it is:
44
+
45
+ ```sh
46
+ npm view @nxgt/typespec version # 0.9.0 or later
47
+ npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
48
+ ```
49
+
50
+ ## `init-template-download-failed: Failed to download template from …: fetch failed`
51
+
52
+ **When:** `tsp init` with the package's template, before it writes anything.
53
+
54
+ ```text
55
+ error init-template-download-failed: Failed to download template from https://unpkg.com/@nxgt/typespec/templates/scaffolding.json: fetch failed. Check that the template URL is correct.
56
+ ```
57
+
58
+ **Why:** `tsp init` could not reach the host: no network, a firewall or
59
+ proxy in the way, or a host name mistyped. Nothing answered, unlike the entry
60
+ above, where unpkg answers a page that is not the template.
61
+
62
+ **Fix:** check that the host answers from that machine, then run `tsp init`
63
+ again:
64
+
65
+ ```sh
66
+ curl -fsSI https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
67
+ ```
68
+
69
+ ## `env: 'bun': No such file or directory`
70
+
71
+ **When:** `nxgt-openapi generate` runs, through `npx`, an npm script such as
72
+ the template's `api`, or a CI step, on a machine without Bun. A shell in
73
+ another locale quotes it `‘bun’`; macOS prints `env: bun: No such file or
74
+ directory`. The exit code is 127.
75
+
76
+ **Why:** `@nxgt/openapi-codegen`'s CLI, `nxgt-openapi`, runs on
77
+ [Bun](https://bun.sh): its first line is `#!/usr/bin/env bun`. `npx` and
78
+ `npm run` find the script, then the system looks for `bun` to run it.
79
+ `tsp compile` runs on Node and is not affected.
80
+
81
+ **Fix:** install Bun, and make sure `bun` is on the PATH of the shell or the
82
+ CI job that runs the generator:
83
+
84
+ ```sh
85
+ curl -fsSL https://bun.sh/install | bash
86
+ bunx nxgt-openapi generate
87
+ ```
88
+
89
+ In GitHub Actions, add `oven-sh/setup-bun@v2` before the step.
90
+
30
91
  ## `Duplicate type name: 'NotFoundBody'`
31
92
 
32
93
  **When:** `tsp compile` runs on a spec that declares its own model named like
@@ -295,6 +356,139 @@ Two plain bodies, `Post | Draft`, are one reply whose body is either; two
295
356
  description; bodies of different content types are negotiated. All three
296
357
  pass.
297
358
 
359
+ ## `returns an array: return Page<Item> or CursorPage<Item>`
360
+
361
+ **When:** a warning, with the ruleset `@nxgt/typespec/recommended` on: an
362
+ operation named `list`, `search` or `query`, alone or before a capital
363
+ letter, has a success reply whose body is an array.
364
+
365
+ ```tsp
366
+ @route("/users")
367
+ interface Users {
368
+ @get list(): User[];
369
+ }
370
+ ```
371
+
372
+ ```text
373
+ warning @nxgt/typespec/list-returns-page: list returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
374
+ ```
375
+
376
+ **Why:** an array cannot grow a `total` or a `nextCursor` later without
377
+ breaking its clients. A page can.
378
+
379
+ **Fix:** return a page ([Pagination](guide/pagination.md)):
380
+
381
+ ```tsp
382
+ @get list(...PageParameters): Page<User>;
383
+ ```
384
+
385
+ Or, for a list that will never page, silence that one operation:
386
+
387
+ ```tsp
388
+ #suppress "@nxgt/typespec/list-returns-page" "a fixed set of ten"
389
+ @get @route("/top") listTop(): User[];
390
+ ```
391
+
392
+ ## `The service … has no @operationIds: its ids are the emitter's, such as Users_list`
393
+
394
+ **When:** a warning, with the ruleset `@nxgt/typespec/recommended` on: a
395
+ `@service` namespace is not marked with `@operationIds`, neither itself nor
396
+ a namespace around it. `@operationIds` on its interfaces alone does not
397
+ count.
398
+
399
+ ```tsp
400
+ @service
401
+ namespace Shop {
402
+ @route("/health") @get op health(): void;
403
+ }
404
+ ```
405
+
406
+ ```text
407
+ warning @nxgt/typespec/service-operation-ids: The service Shop has no @operationIds: its ids are the emitter's, such as Users_list. Mark the namespace with @operationIds.
408
+ ```
409
+
410
+ **Why:** the emitter names each operation after its interface,
411
+ `Users_list`, and the generated client's methods and types take that name.
412
+
413
+ **Fix:** mark the namespace ([Operation ids](guide/operation-ids.md)):
414
+
415
+ ```tsp
416
+ @service
417
+ @operationIds
418
+ namespace Shop {
419
+ @route("/health") @get op health(): void;
420
+ }
421
+ ```
422
+
423
+ Marking an existing service renames its operations, and the generated
424
+ client's methods with them. To keep the old ids, silence the rule on that
425
+ namespace:
426
+
427
+ ```tsp
428
+ #suppress "@nxgt/typespec/service-operation-ids" "its clients already call Health_check"
429
+ @service
430
+ namespace Health {
431
+ @route("/health") @get op check(): void;
432
+ }
433
+ ```
434
+
435
+ ## `answers … with a body that is not the nxgt envelope`
436
+
437
+ **When:** a warning, with the ruleset `@nxgt/typespec/recommended` on: a
438
+ reply of status 400 or above, of a range from 400 up, or `default` (an
439
+ `@error` model without `@statusCode`) has a body that is not a model with
440
+ `status`, `message` and `timestamp`.
441
+
442
+ ```tsp
443
+ model Teapot {
444
+ @statusCode _: 418;
445
+ @body body: { reason: string };
446
+ }
447
+
448
+ @route("/users")
449
+ interface Users {
450
+ @post create(@body user: User): User | Teapot;
451
+ }
452
+ ```
453
+
454
+ ```text
455
+ warning @nxgt/typespec/error-body-shape: create answers 418 with a body that is not the nxgt envelope: declare BadRequest, NotFound or another of the library's errors, or a body that spreads or extends ErrorBody<Status>.
456
+ ```
457
+
458
+ A range is named `500-599`, and `default` `*`.
459
+
460
+ **Why:** `@nxgt/openapi-hono` answers its own 400 and 500 with the envelope,
461
+ and a client reads `message` from every error. Another shape is one more
462
+ case for each client.
463
+
464
+ **Fix:** declare one of the library's errors, `ErrorResponse<Status>`, or a
465
+ body that spreads or extends `ErrorBody<Status>` ([Error replies](guide/errors.md)):
466
+
467
+ ```tsp
468
+ model TeapotBody {
469
+ ...ErrorBody<418>;
470
+ reason: string;
471
+ }
472
+
473
+ @error
474
+ model Teapot {
475
+ @statusCode _: 418;
476
+ @body body: TeapotBody;
477
+ }
478
+ ```
479
+
480
+ A reply without a body, such as `AuthenticationRequired`, passes. For a
481
+ spec whose errors have another shape on purpose, turn the rule off in
482
+ `tspconfig.yaml` ([Linter](guide/linter.md#turn-a-rule-off)):
483
+
484
+ ```yaml
485
+ linter:
486
+ extends:
487
+ - '@nxgt/typespec/recommended'
488
+ disable:
489
+ '@nxgt/typespec/error-body-shape': 'the legacy routes answer their own errors'
490
+ ```
491
+
298
492
  ## `Unknown decorator @operationId`
299
493
 
300
494
  **When:** a spec writes `@operationId` next to `@operationIds`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/typespec",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "lib",
12
+ "templates",
12
13
  "docs",
13
14
  "README.md",
14
15
  "package.json",
@@ -0,0 +1,33 @@
1
+ # {{name}}
2
+
3
+ The API is written in TypeSpec, in `main.tsp`, with the
4
+ [`@nxgt/typespec`](https://www.npmjs.com/package/@nxgt/typespec) conventions.
5
+
6
+ Compile it to `openapi/openapi.yaml`, then generate the types, validators
7
+ and routes into `src/generated`. The generator, `nxgt-openapi`, runs on
8
+ [Bun](https://bun.sh): it needs `bun` on the PATH, even through `npx` or an
9
+ npm script.
10
+
11
+ ```sh
12
+ npx tsp compile .
13
+ npx nxgt-openapi generate
14
+ ```
15
+
16
+ Chain both in `package.json`, so the code never lags the spec:
17
+
18
+ ```json
19
+ {
20
+ "scripts": {
21
+ "api": "tsp compile . && nxgt-openapi generate",
22
+ "api:check": "tsp compile . && nxgt-openapi generate --check"
23
+ }
24
+ }
25
+ ```
26
+
27
+ `tspconfig.yaml` turns on the linter's recommended rules, which warn of a
28
+ list without a page, a service without `@operationIds`, and an error reply
29
+ without the envelope.
30
+
31
+ The next steps, serving the routes with Hono and renaming the `Users`
32
+ resource, are in
33
+ [Getting started](https://github.com/softistx/nxgt-http/blob/develop/packages/typespec/docs/guide/getting-started.md).
@@ -0,0 +1,68 @@
1
+ // The API's spec. `tsp compile .` writes openapi/openapi.yaml, and
2
+ // `nxgt-openapi generate` the types, validators and routes from it.
3
+ import "@typespec/http";
4
+ import "@nxgt/typespec";
5
+
6
+ using Http;
7
+ using Nxgt;
8
+
9
+ /** The {{name}} API. */
10
+ @service(#{ title: "{{name}}" })
11
+ @operationIds
12
+ namespace Api;
13
+
14
+ /** Someone who uses the API. */
15
+ model User {
16
+ @visibility(Lifecycle.Read)
17
+ id: uuid;
18
+
19
+ @minLength(1)
20
+ @maxLength(80)
21
+ name: string;
22
+
23
+ email: email;
24
+
25
+ ...Timestamps;
26
+ }
27
+
28
+ /** What the user list filters on. */
29
+ model UserFilters {
30
+ /** Users of this name. */
31
+ @query
32
+ name?: string;
33
+ }
34
+
35
+ // Verbs @operationIds completes: list is listUsers, get is getUser.
36
+ @route("/users")
37
+ @tag("Users")
38
+ interface Users {
39
+ /** One page of the users, filtered and sorted. */
40
+ @get
41
+ list(
42
+ ...UserFilters,
43
+ ...PageParameters,
44
+ ...SortParameters<"name" | "createdAt">,
45
+ ): Page<User> | ListErrors;
46
+
47
+ /** One user. */
48
+ @get
49
+ get(@path id: uuid): User | GetErrors;
50
+
51
+ /** Creates a user. */
52
+ @post
53
+ create(@body user: Create<User>): {
54
+ @statusCode _: 201;
55
+ @body user: User;
56
+ } | CreateErrors;
57
+
58
+ /** Updates a user with a merge patch. */
59
+ @patch
60
+ update(
61
+ @path id: uuid,
62
+ @body user: MergePatchUpdate<User>,
63
+ ): User | UpdateErrors;
64
+
65
+ /** Deletes a user. */
66
+ @delete
67
+ delete(@path id: uuid): NoContentResponse | DeleteErrors;
68
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from '@nxgt/openapi-codegen';
2
+
3
+ export default defineConfig({
4
+ input: 'openapi/openapi.yaml',
5
+ output: 'src/generated',
6
+ hono: true,
7
+ });
@@ -0,0 +1,37 @@
1
+ {
2
+ "nxgt": {
3
+ "title": "nxgt REST API",
4
+ "description": "A REST API with the @nxgt conventions: ids as written, pages, the error envelope and the linter, generated with @nxgt/openapi-codegen.",
5
+ "compilerVersion": "1.16.0",
6
+ "libraries": [
7
+ "@typespec/http",
8
+ "@typespec/openapi",
9
+ "@nxgt/typespec",
10
+ "@nxgt/openapi-codegen"
11
+ ],
12
+ "config": {
13
+ "linter": {
14
+ "extends": ["@nxgt/typespec/recommended"]
15
+ }
16
+ },
17
+ "emitters": {
18
+ "@typespec/openapi3": {
19
+ "selected": true,
20
+ "label": "OpenAPI 3.1 document",
21
+ "options": {
22
+ "emitter-output-dir": "{project-root}/openapi",
23
+ "openapi-versions": ["3.1.0"],
24
+ "output-file": "openapi.yaml"
25
+ }
26
+ }
27
+ },
28
+ "files": [
29
+ { "path": "nxgt/main.tsp", "destination": "main.tsp" },
30
+ {
31
+ "path": "nxgt/openapi-codegen.config.ts",
32
+ "destination": "openapi-codegen.config.ts"
33
+ },
34
+ { "path": "nxgt/README.md", "destination": "README.md" }
35
+ ]
36
+ }
37
+ }