@nxgt/typespec 0.1.0 → 0.3.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 +27 -12
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -13
- package/dist/index.js.map +7 -6
- package/dist/lib.d.ts +7 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/one-reply-per-status.d.ts +17 -0
- package/dist/one-reply-per-status.d.ts.map +1 -0
- package/dist/operation-ids.d.ts +8 -1
- package/dist/operation-ids.d.ts.map +1 -1
- package/docs/guide/auth.md +4 -4
- package/docs/guide/headers.md +3 -2
- package/docs/guide/operation-ids.md +9 -2
- package/docs/guide/resources.md +52 -7
- package/docs/roadmap.md +22 -12
- package/docs/troubleshooting.md +64 -4
- package/lib/operation-ids.tsp +8 -0
- package/lib/resources.tsp +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -292,10 +292,14 @@ interface Posts extends Resource<Post, PostFilters, SortField = "createdAt" | "t
|
|
|
292
292
|
| Operation | Route | Answers |
|
|
293
293
|
| --- | --- | --- |
|
|
294
294
|
| `listPosts` | `GET /posts`, with the filters, `page`, `pageSize`, `orderBy`, `direction` | `Page<Post>` |
|
|
295
|
-
| `
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
295
|
+
| `readPost` | `GET /posts/{id}` | `Post \| NotFound` |
|
|
296
|
+
| `createPost` | `POST /posts`, `Create<Post>` | 201 `Post \| BadRequest \| Conflict` |
|
|
297
|
+
| `updatePost` | `PATCH /posts/{id}`, `MergePatchUpdate<Post>` | `Post \| BadRequest \| NotFound \| Conflict` |
|
|
298
|
+
| `deletePost` | `DELETE /posts/{id}` | 204, `NotFound` |
|
|
299
|
+
|
|
300
|
+
`list` is named after the interface, the four others after the item. Two resources of one item then share their ids,
|
|
301
|
+
which is an error, `duplicate-operation-id`: give the second its own
|
|
302
|
+
`@operationId`s ([troubleshooting](docs/troubleshooting.md)).
|
|
299
303
|
|
|
300
304
|
`orderBy` takes one of the `SortField`s (`SortField` is `"id"` by default), and `direction`
|
|
301
305
|
`asc` (the default) or `desc`, as `@nxgt/drizzle` sorts a page. `{id}`
|
|
@@ -336,6 +340,14 @@ of a marked interface that has another operation's id is an error,
|
|
|
336
340
|
| --- | --- | --- |
|
|
337
341
|
| `@operationIds` | an interface | names each operation `<operation><Interface>`, unless it has an `@operationId` |
|
|
338
342
|
|
|
343
|
+
### Diagnostics
|
|
344
|
+
|
|
345
|
+
| Code | Reported when |
|
|
346
|
+
| --- | --- |
|
|
347
|
+
| `duplicate-operation-id` | error: an operation of an `@operationIds` interface has another operation's id |
|
|
348
|
+
| `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 |
|
|
349
|
+
| `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 |
|
|
350
|
+
|
|
339
351
|
### Models
|
|
340
352
|
|
|
341
353
|
| Model | What it is |
|
|
@@ -415,14 +427,17 @@ envelope and the rate limit's headers.
|
|
|
415
427
|
@body body: ServiceUnavailableBody;
|
|
416
428
|
}
|
|
417
429
|
```
|
|
418
|
-
- **One status, one reply.**
|
|
419
|
-
`AuthenticationRequired` and `Unauthorized
|
|
420
|
-
envelope
|
|
421
|
-
|
|
422
|
-
`
|
|
423
|
-
|
|
424
|
-
description
|
|
425
|
-
|
|
430
|
+
- **One status, one reply.** `@typespec/openapi3` merges two replies of one
|
|
431
|
+
status into one. With `AuthenticationRequired` and `Unauthorized`, the 401
|
|
432
|
+
keeps the envelope and the reply without a body is lost: the library
|
|
433
|
+
refuses it, `duplicate-status-reply`. With `Conflict` beside
|
|
434
|
+
`IdempotencyInProgress`, or `UnprocessableEntity` beside
|
|
435
|
+
`IdempotencyKeyReused`, both bodies stay, under the first reply's
|
|
436
|
+
description: the library warns, `merged-status-reply`. Declare the one the
|
|
437
|
+
route sends; for the idempotent write, the `message` key tells them apart.
|
|
438
|
+
Two plain bodies, `Post | Draft`, bodies of different content types, and
|
|
439
|
+
two `@error` models without a `@statusCode` pass
|
|
440
|
+
([troubleshooting](docs/troubleshooting.md)).
|
|
426
441
|
- **Both versions at once write two folders.** With
|
|
427
442
|
`openapi-versions: ['3.1.0', '3.2.0']`, the emitter writes
|
|
428
443
|
`3.1.0/openapi.yaml` and `3.2.0/openapi.yaml`: point the generator's input
|
package/dist/index.d.ts
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* export would declare it a second time, in the global namespace.
|
|
8
8
|
*/
|
|
9
9
|
import type { Program } from '@typespec/compiler';
|
|
10
|
-
import { operationIds } from './operation-ids';
|
|
10
|
+
import { itemOperation, operationIds } from './operation-ids';
|
|
11
11
|
export { $lib } from './lib';
|
|
12
12
|
export declare function $onValidate(program: Program): void;
|
|
13
13
|
export declare const $decorators: {
|
|
14
14
|
Nxgt: {
|
|
15
15
|
operationIds: typeof operationIds;
|
|
16
16
|
};
|
|
17
|
+
'Nxgt.Private': {
|
|
18
|
+
itemOperation: typeof itemOperation;
|
|
19
|
+
};
|
|
17
20
|
};
|
|
18
21
|
//# 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;
|
|
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,EACN,aAAa,EACb,YAAY,EAEZ,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC;AAE7B,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAGlD;AAED,eAAO,MAAM,WAAW;;;;;;;CAGvB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
isTemplateInstance,
|
|
4
|
-
navigateProgram
|
|
5
|
-
} from "@typespec/compiler";
|
|
1
|
+
// src/one-reply-per-status.ts
|
|
2
|
+
import { isVoidType } from "@typespec/compiler";
|
|
6
3
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
setOperationId
|
|
10
|
-
} from "@typespec/openapi";
|
|
4
|
+
getAllHttpServices
|
|
5
|
+
} from "@typespec/http";
|
|
11
6
|
|
|
12
7
|
// src/lib.ts
|
|
13
8
|
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";
|
|
@@ -19,17 +14,99 @@ var $lib = createTypeSpecLibrary({
|
|
|
19
14
|
messages: {
|
|
20
15
|
default: paramMessage`Two operations are named ${"id"}: an OpenAPI operation id must be unique.`
|
|
21
16
|
}
|
|
17
|
+
},
|
|
18
|
+
"duplicate-status-reply": {
|
|
19
|
+
severity: "error",
|
|
20
|
+
messages: {
|
|
21
|
+
default: 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.`
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"merged-status-reply": {
|
|
25
|
+
severity: "warning",
|
|
26
|
+
messages: {
|
|
27
|
+
default: 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.`
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
},
|
|
24
31
|
state: {
|
|
25
|
-
operationIds: { description: "The interfaces marked with @operationIds" }
|
|
32
|
+
operationIds: { description: "The interfaces marked with @operationIds" },
|
|
33
|
+
itemOperations: {
|
|
34
|
+
description: "The operations named after an item, and the item's model"
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
});
|
|
28
38
|
|
|
39
|
+
// src/one-reply-per-status.ts
|
|
40
|
+
function statusOf(code) {
|
|
41
|
+
return typeof code === "object" ? `${code.start}-${code.end}` : `${code}`;
|
|
42
|
+
}
|
|
43
|
+
function mergeOf({
|
|
44
|
+
statusCodes,
|
|
45
|
+
responses
|
|
46
|
+
}) {
|
|
47
|
+
const bodies = responses.flatMap(({ body }) => body && !isVoidType(body.type) ? [body] : []);
|
|
48
|
+
if (bodies.length > 0 && bodies.length < responses.length) {
|
|
49
|
+
return "duplicate-status-reply";
|
|
50
|
+
}
|
|
51
|
+
if (typeof statusCodes !== "number")
|
|
52
|
+
return;
|
|
53
|
+
const seen = new Set;
|
|
54
|
+
for (const body of bodies) {
|
|
55
|
+
if (body.contentTypes.some((type) => seen.has(type))) {
|
|
56
|
+
return "merged-status-reply";
|
|
57
|
+
}
|
|
58
|
+
for (const type of body.contentTypes)
|
|
59
|
+
seen.add(type);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
function validateOneReplyPerStatus(program) {
|
|
64
|
+
const [services] = getAllHttpServices(program);
|
|
65
|
+
const checked = new Set;
|
|
66
|
+
for (const service of services) {
|
|
67
|
+
for (const operation of service.operations) {
|
|
68
|
+
if (checked.has(operation.operation))
|
|
69
|
+
continue;
|
|
70
|
+
checked.add(operation.operation);
|
|
71
|
+
for (const response of operation.responses) {
|
|
72
|
+
const code = mergeOf(response);
|
|
73
|
+
if (code === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
$lib.reportDiagnostic(program, {
|
|
76
|
+
code,
|
|
77
|
+
format: {
|
|
78
|
+
operation: operation.operation.name,
|
|
79
|
+
status: statusOf(response.statusCodes)
|
|
80
|
+
},
|
|
81
|
+
target: operation.operation
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
// src/operation-ids.ts
|
|
89
|
+
import {
|
|
90
|
+
getFriendlyName,
|
|
91
|
+
isTemplateInstance,
|
|
92
|
+
navigateProgram
|
|
93
|
+
} from "@typespec/compiler";
|
|
94
|
+
import {
|
|
95
|
+
getOperationId,
|
|
96
|
+
resolveOperationId,
|
|
97
|
+
setOperationId
|
|
98
|
+
} from "@typespec/openapi";
|
|
30
99
|
function operationIds(context, target) {
|
|
31
100
|
context.program.stateSet($lib.stateKeys.operationIds).add(target);
|
|
32
101
|
}
|
|
102
|
+
function itemOperation(context, target, item) {
|
|
103
|
+
context.program.stateMap($lib.stateKeys.itemOperations).set(target, item);
|
|
104
|
+
}
|
|
105
|
+
function nounOf(program, operation) {
|
|
106
|
+
const item = program.stateMap($lib.stateKeys.itemOperations).get(operation);
|
|
107
|
+
const named = item && (getFriendlyName(program, item) ?? (item.name !== "" && !isTemplateInstance(item) ? item.name : undefined));
|
|
108
|
+
return named ?? operation.interface?.name;
|
|
109
|
+
}
|
|
33
110
|
function isMarked(program, target) {
|
|
34
111
|
return program.stateSet($lib.stateKeys.operationIds).has(target) || target.sourceInterfaces.some((source) => isMarked(program, source));
|
|
35
112
|
}
|
|
@@ -43,7 +120,7 @@ function validateOperationIds(program) {
|
|
|
43
120
|
operation(operation) {
|
|
44
121
|
const named = isNamed(program, operation);
|
|
45
122
|
const own = getOperationId(program, operation);
|
|
46
|
-
const id = own ?? (named ? `${operation.name}${operation
|
|
123
|
+
const id = own ?? (named ? `${operation.name}${nounOf(program, operation)}` : resolveOperationId(program, operation));
|
|
47
124
|
if (seen.has(id) && (named || seen.get(id))) {
|
|
48
125
|
$lib.reportDiagnostic(program, {
|
|
49
126
|
code: "duplicate-operation-id",
|
|
@@ -61,9 +138,11 @@ function validateOperationIds(program) {
|
|
|
61
138
|
// src/index.ts
|
|
62
139
|
function $onValidate(program) {
|
|
63
140
|
validateOperationIds(program);
|
|
141
|
+
validateOneReplyPerStatus(program);
|
|
64
142
|
}
|
|
65
143
|
var $decorators = {
|
|
66
|
-
Nxgt: { operationIds }
|
|
144
|
+
Nxgt: { operationIds },
|
|
145
|
+
"Nxgt.Private": { itemOperation }
|
|
67
146
|
};
|
|
68
147
|
export {
|
|
69
148
|
$decorators,
|
|
@@ -71,5 +150,5 @@ export {
|
|
|
71
150
|
$onValidate
|
|
72
151
|
};
|
|
73
152
|
|
|
74
|
-
//# debugId=
|
|
153
|
+
//# debugId=D1BB8C6D97A7E43C64756E2164756E21
|
|
75
154
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/
|
|
3
|
+
"sources": ["../src/one-reply-per-status.ts", "../src/lib.ts", "../src/operation-ids.ts", "../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\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},\n\tstate: {\n\t\toperationIds: { description: 'The interfaces marked with @operationIds' },\n\t},\n});\n",
|
|
7
|
-
"/**\n *
|
|
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: { description: 'The interfaces marked with @operationIds' },\n\t\titemOperations: {\n\t\t\tdescription: \"The operations named after an item, and the item's model\",\n\t\t},\n\t},\n});\n",
|
|
7
|
+
"/**\n * `@operationIds`: names each operation of an interface after its method and\n * the interface, `<operation><Interface>`, such as `listPets`.\n */\nimport {\n\ttype DecoratorContext,\n\tgetFriendlyName,\n\ttype Interface,\n\tisTemplateInstance,\n\ttype Model,\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; its ids are set once the program is checked. On a\n * template, TypeSpec runs it on each instance, which `isMarked` follows to\n * the interfaces extending it.\n */\nexport function operationIds(\n\tcontext: DecoratorContext,\n\ttarget: Interface,\n): void {\n\tcontext.program.stateSet($lib.stateKeys.operationIds).add(target);\n}\n\n/**\n * Names the operation after one item, `<operation><Item>`, instead of its\n * interface, where `@operationIds` names it: `Resource` marks `read`,\n * `create`, `update` and `delete`, so `Authors` has `readAuthor`, but\n * `listAuthors`. Not public: `Resource`'s alone.\n */\nexport function itemOperation(\n\tcontext: DecoratorContext,\n\ttarget: Operation,\n\titem: Model,\n): void {\n\tcontext.program.stateMap($lib.stateKeys.itemOperations).set(target, item);\n}\n\n/**\n * The name `@operationIds` puts after the operation's: its item's, or its\n * interface's. An item without a name of its own, anonymous or an instance\n * such as `Versioned<Author>`, whose name would be `Versioned` alone, falls\n * back to the interface.\n */\nfunction nounOf(program: Program, operation: Operation): string | undefined {\n\tconst item: Model | undefined = program\n\t\t.stateMap($lib.stateKeys.itemOperations)\n\t\t.get(operation);\n\tconst named =\n\t\titem &&\n\t\t(getFriendlyName(program, item) ??\n\t\t\t(item.name !== '' && !isTemplateInstance(item) ? item.name : undefined));\n\treturn named ?? operation.interface?.name;\n}\n\n/** Marked itself, or extending a marked interface: an instance of a marked template included. */\nfunction isMarked(program: Program, target: Interface): boolean {\n\treturn (\n\t\tprogram.stateSet($lib.stateKeys.operationIds).has(target) ||\n\t\ttarget.sourceInterfaces.some((source) => isMarked(program, source))\n\t);\n}\n\n/**\n * Whether `@operationIds` names the operation's id, or checks it: its\n * interface is marked, and is not a template instance, which is never\n * emitted; only the interfaces extending it are.\n */\nfunction isNamed(program: Program, operation: Operation): boolean {\n\tconst container = operation.interface;\n\treturn (\n\t\tcontainer !== undefined &&\n\t\t!isTemplateInstance(container) &&\n\t\tisMarked(program, container)\n\t);\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 * one of them in a marked interface, are an error: two interfaces of one name\n * in two namespaces, an `@operationId` that `extends` copied, one written\n * elsewhere, or an operation in the service namespace, named after itself. `@typespec/openapi3` would emit both without a word.\n */\nexport function validateOperationIds(program: Program): void {\n\tconst seen = new Map<string, boolean>();\n\tnavigateProgram(program, {\n\t\toperation(operation) {\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; an operation in the service namespace is named\n\t\t\t// after itself.\n\t\t\tconst id =\n\t\t\t\town ??\n\t\t\t\t(named\n\t\t\t\t\t? `${operation.name}${nounOf(program, operation)}`\n\t\t\t\t\t: resolveOperationId(program, operation));\n\t\t\tif (seen.has(id) && (named || seen.get(id))) {\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\tseen.set(id, named || (seen.get(id) ?? false));\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
|
+
"/**\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 {\n\titemOperation,\n\toperationIds,\n\tvalidateOperationIds,\n} 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\t'Nxgt.Private': { itemOperation },\n};\n"
|
|
8
9
|
],
|
|
9
|
-
"mappings": ";
|
|
10
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "D1BB8C6D97A7E43C64756E2164756E21",
|
|
11
12
|
"names": []
|
|
12
13
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -2,5 +2,11 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
2
2
|
'duplicate-operation-id': {
|
|
3
3
|
readonly default: import("@typespec/compiler").CallableMessage<["id"]>;
|
|
4
4
|
};
|
|
5
|
-
|
|
5
|
+
'duplicate-status-reply': {
|
|
6
|
+
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
7
|
+
};
|
|
8
|
+
'merged-status-reply': {
|
|
9
|
+
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
10
|
+
};
|
|
11
|
+
}, Record<string, any>, "operationIds" | "itemOperations">;
|
|
6
12
|
//# sourceMappingURL=lib.d.ts.map
|
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;;;;;;;;;;0DA4Bf,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reply per status: `@typespec/openapi3` merges two replies of one
|
|
3
|
+
* status into one without a word. A reply without a body beside one with a
|
|
4
|
+
* body is lost in the merge, an error; a `void` body is none, as the emitter
|
|
5
|
+
* sees it. Two bodies of one content type are merged under the first reply's
|
|
6
|
+
* description, a warning where the status is a code. Under `*` or a range,
|
|
7
|
+
* where undocumented `@error` models share one description, they pass.
|
|
8
|
+
* Bodies of different content types are one reply, negotiated, and pass.
|
|
9
|
+
*/
|
|
10
|
+
import { type Program } from '@typespec/compiler';
|
|
11
|
+
/**
|
|
12
|
+
* Checks each operation of each service, as the emitter sees it, once: a
|
|
13
|
+
* service nested in another is listed in both. The services' own
|
|
14
|
+
* diagnostics are the emitter's to report.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateOneReplyPerStatus(program: Program): void;
|
|
17
|
+
//# sourceMappingURL=one-reply-per-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"one-reply-per-status.d.ts","sourceRoot":"","sources":["../src/one-reply-per-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAA8B,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAqC9E;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqBhE"}
|
package/dist/operation-ids.d.ts
CHANGED
|
@@ -2,13 +2,20 @@
|
|
|
2
2
|
* `@operationIds`: names each operation of an interface after its method and
|
|
3
3
|
* the interface, `<operation><Interface>`, such as `listPets`.
|
|
4
4
|
*/
|
|
5
|
-
import { type DecoratorContext, type Interface, type Program } from '@typespec/compiler';
|
|
5
|
+
import { type DecoratorContext, type Interface, type Model, type Operation, type Program } from '@typespec/compiler';
|
|
6
6
|
/**
|
|
7
7
|
* Marks the interface; its ids are set once the program is checked. On a
|
|
8
8
|
* template, TypeSpec runs it on each instance, which `isMarked` follows to
|
|
9
9
|
* the interfaces extending it.
|
|
10
10
|
*/
|
|
11
11
|
export declare function operationIds(context: DecoratorContext, target: Interface): void;
|
|
12
|
+
/**
|
|
13
|
+
* Names the operation after one item, `<operation><Item>`, instead of its
|
|
14
|
+
* interface, where `@operationIds` names it: `Resource` marks `read`,
|
|
15
|
+
* `create`, `update` and `delete`, so `Authors` has `readAuthor`, but
|
|
16
|
+
* `listAuthors`. Not public: `Resource`'s alone.
|
|
17
|
+
*/
|
|
18
|
+
export declare function itemOperation(context: DecoratorContext, target: Operation, item: Model): void;
|
|
12
19
|
/**
|
|
13
20
|
* Sets the ids, after every decorator, so the order of `@operationId` and
|
|
14
21
|
* `@operationIds` in the source does not matter. Two operations with one id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACN,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACN,KAAK,gBAAgB,EAErB,KAAK,SAAS,EAEd,KAAK,KAAK,EAEV,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,MAAM,oBAAoB,CAAC;AAQ5B;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,GACf,IAAI,CAEN;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC5B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,KAAK,GACT,IAAI,CAEN;AAyCD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2B3D"}
|
package/docs/guide/auth.md
CHANGED
|
@@ -97,7 +97,7 @@ operation can declare the guard's 401 and the handler's 403:
|
|
|
97
97
|
} | BadRequest | AuthenticationRequired | Forbidden;
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
Not both replies of one status, though:
|
|
101
|
-
`AuthenticationRequired` and `Unauthorized`
|
|
102
|
-
envelope, and the reply without a body
|
|
103
|
-
the one the route sends.
|
|
100
|
+
Not both replies of one status, though: the emitter would merge
|
|
101
|
+
`AuthenticationRequired` and `Unauthorized` into a single 401, with the
|
|
102
|
+
envelope, and lose the reply without a body. The library refuses it with
|
|
103
|
+
`duplicate-status-reply`: declare the one the route sends.
|
package/docs/guide/headers.md
CHANGED
|
@@ -41,8 +41,9 @@ interface Posts {
|
|
|
41
41
|
|
|
42
42
|
Both carry the `RateLimit-*` headers, as a route that also has a rate limit
|
|
43
43
|
answers them. An operation that declares `Conflict` too, for its optimistic
|
|
44
|
-
lock, gets one 409 with the description of the first
|
|
45
|
-
two, and tell them apart by the
|
|
44
|
+
lock, gets one 409 with the description of the first, and the library warns,
|
|
45
|
+
`merged-status-reply`: declare one of the two, and tell them apart by the
|
|
46
|
+
`message` key ([troubleshooting](../troubleshooting.md)).
|
|
46
47
|
|
|
47
48
|
## A rate limit
|
|
48
49
|
|
|
@@ -84,9 +84,13 @@ interface Pets extends Resource<Pet> {} // listPets
|
|
|
84
84
|
interface Orders extends Resource<Order> {} // listOrders
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
The library's own `Resource` goes one step further: `list` is named after
|
|
88
|
+
the interface, and `read`, `create`, `update` and `delete` after the item,
|
|
89
|
+
`readPet` ([Resources](resources.md)).
|
|
90
|
+
|
|
87
91
|
## Two operations named alike
|
|
88
92
|
|
|
89
|
-
An id must be unique, and
|
|
93
|
+
An id must be unique, and five cases break that:
|
|
90
94
|
|
|
91
95
|
- the namespace is not part of the id, so `Store.Pets` and `Shelter.Pets`
|
|
92
96
|
would both name their `list` `listPets`;
|
|
@@ -95,7 +99,10 @@ An id must be unique, and four cases break that:
|
|
|
95
99
|
- an `@operationId` written on another operation, in any interface, takes
|
|
96
100
|
the id `@operationIds` gives;
|
|
97
101
|
- an operation declared in the service namespace is named after itself, so
|
|
98
|
-
`op listPets()` takes the id of `Pets.list
|
|
102
|
+
`op listPets()` takes the id of `Pets.list`;
|
|
103
|
+
- two interfaces extend `Resource` with one item, so `Authors` and
|
|
104
|
+
`ArchivedAuthors` would both name their `read` `readAuthor`
|
|
105
|
+
([Resources](resources.md#two-resources-of-one-item)).
|
|
99
106
|
|
|
100
107
|
`@operationIds` refuses each with `duplicate-operation-id`, where
|
|
101
108
|
`@typespec/openapi3` would emit both without a word. Give one of them its own
|
package/docs/guide/resources.md
CHANGED
|
@@ -33,14 +33,55 @@ interface Authors extends Resource<Author, AuthorFilters, SortField = "name" | "
|
|
|
33
33
|
| Operation | Route | Body | Answers |
|
|
34
34
|
| --- | --- | --- | --- |
|
|
35
35
|
| `listAuthors` | `GET /authors` | | `Page<Author>` |
|
|
36
|
-
| `
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
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:
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
```
|
|
44
85
|
|
|
45
86
|
## The template's arguments
|
|
46
87
|
|
|
@@ -110,3 +151,7 @@ interface Comments extends Resource<Comment> {
|
|
|
110
151
|
Add `@useAuth(JanusAuth)` on the interface to protect all five, and name the
|
|
111
152
|
guards' replies in the operations you declare again
|
|
112
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/docs/roadmap.md
CHANGED
|
@@ -6,6 +6,28 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
+
- **Resource ids in the singular.** `Resource` names `read`, `create`,
|
|
10
|
+
`update` and `delete` after its item, `readPost`, and `list` after the
|
|
11
|
+
interface, `listPosts`, instead of `readPosts`.
|
|
12
|
+
|
|
13
|
+
## Later
|
|
14
|
+
|
|
15
|
+
- **`ETag` and conditional requests.** `ETag`, with `If-None-Match` and
|
|
16
|
+
`If-Match`, once the `@nxgt/*` packages send one the same way. The
|
|
17
|
+
optimistic lock is the body's `version` today (`Versioned`).
|
|
18
|
+
|
|
19
|
+
## Shipped
|
|
20
|
+
|
|
21
|
+
### 0.2.0
|
|
22
|
+
|
|
23
|
+
- **One reply per status.** An operation that declares a reply without a
|
|
24
|
+
body beside one with a body, such as `AuthenticationRequired` and
|
|
25
|
+
`Unauthorized`, is an error, `duplicate-status-reply`, and two replies with
|
|
26
|
+
a body of one status code a warning, `merged-status-reply`, instead of
|
|
27
|
+
replies the emitter merges without a word.
|
|
28
|
+
|
|
29
|
+
### 0.1.0
|
|
30
|
+
|
|
9
31
|
- **Error replies.** The envelope `@nxgt/openapi-hono` sends, and the
|
|
10
32
|
400, 401, 403, 404, 409, 422, 429 and 500 responses, with any other status
|
|
11
33
|
through `ErrorResponse<Status>`.
|
|
@@ -24,15 +46,3 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
24
46
|
by `orderBy` and `direction`.
|
|
25
47
|
- **Operation ids.** `@operationIds` names an interface's operations
|
|
26
48
|
`listPosts`, `createPosts`, instead of `Posts_list`.
|
|
27
|
-
|
|
28
|
-
## Next
|
|
29
|
-
|
|
30
|
-
- **One reply per status.** An error when an operation declares two replies
|
|
31
|
-
of one status, such as `AuthenticationRequired` and `Unauthorized`, which
|
|
32
|
-
the emitter merges without a warning today.
|
|
33
|
-
|
|
34
|
-
## Later
|
|
35
|
-
|
|
36
|
-
- **`ETag` and conditional requests.** `ETag`, with `If-None-Match` and
|
|
37
|
-
`If-Match`, once the `@nxgt/*` packages send one the same way. The
|
|
38
|
-
optimistic lock is the body's `version` today (`Versioned`).
|
package/docs/troubleshooting.md
CHANGED
|
@@ -60,17 +60,22 @@ operation:
|
|
|
60
60
|
`extends` copies it as is;
|
|
61
61
|
- an `@operationId` written elsewhere takes the id `@operationIds` gives;
|
|
62
62
|
- an operation declared in the service namespace is named after itself:
|
|
63
|
-
`op listPets()` takes the id of `Pets.list
|
|
63
|
+
`op listPets()` takes the id of `Pets.list`;
|
|
64
|
+
- two interfaces extend `Resource` with one item: `Authors` and
|
|
65
|
+
`ArchivedAuthors` both give `readAuthor`, as `Resource` names `read`,
|
|
66
|
+
`create`, `update` and `delete` after the item.
|
|
64
67
|
|
|
65
68
|
```text
|
|
66
69
|
error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets: an OpenAPI operation id must be unique.
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
**Why:** `@operationIds` names each operation `<operation><Interface
|
|
70
|
-
|
|
72
|
+
**Why:** `@operationIds` names each operation `<operation><Interface>`
|
|
73
|
+
(`<operation><Item>` for `Resource`'s `read`, `create`, `update` and
|
|
74
|
+
`delete`), without the namespace. `@typespec/openapi3` would emit both ids without a word, and
|
|
71
75
|
the generator would then refuse the spec.
|
|
72
76
|
|
|
73
|
-
**Fix:** give one of them its own `@operationId
|
|
77
|
+
**Fix:** give one of them its own `@operationId`, for a `Resource` in the
|
|
78
|
+
interface that extends it ([Resources](guide/resources.md#two-resources-of-one-item)). An inherited operation takes
|
|
74
79
|
one by being declared again in the interface that extends:
|
|
75
80
|
|
|
76
81
|
```tsp
|
|
@@ -84,6 +89,61 @@ interface Drafts extends Posts {
|
|
|
84
89
|
}
|
|
85
90
|
```
|
|
86
91
|
|
|
92
|
+
## `declares a reply without a body and one with a body of status …`
|
|
93
|
+
|
|
94
|
+
**When:** an operation declares, for one status, a reply without a body and
|
|
95
|
+
one with a body, such as the guard's 401 and the handler's:
|
|
96
|
+
|
|
97
|
+
```tsp
|
|
98
|
+
@get list(): Post[] | AuthenticationRequired | Unauthorized;
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
error @nxgt/typespec/duplicate-status-reply: list declares a reply without a body and one with a body of status 401: the emitter merges them into one, and the reply without a body is lost. Declare the one the route sends.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Why:** OpenAPI has one reply per status. `@typespec/openapi3` merges the two
|
|
106
|
+
without a warning: the 401 keeps the envelope, and a client never learns the
|
|
107
|
+
guard's reply has no body.
|
|
108
|
+
|
|
109
|
+
**Fix:** declare the one the route sends. A route behind a
|
|
110
|
+
`@nxgt/janus-hono` guard sends `AuthenticationRequired`; a handler that
|
|
111
|
+
answers with the envelope, `Unauthorized`:
|
|
112
|
+
|
|
113
|
+
```tsp
|
|
114
|
+
@get list(): Post[] | AuthenticationRequired;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## `declares two replies with a body of status …`
|
|
118
|
+
|
|
119
|
+
**When:** a warning: an operation declares two replies with a body for one
|
|
120
|
+
status code, such as the idempotent write's 409 beside the optimistic
|
|
121
|
+
lock's, or its 422, `IdempotencyKeyReused`, beside `UnprocessableEntity`:
|
|
122
|
+
|
|
123
|
+
```tsp
|
|
124
|
+
@post create(@body post: Post): Post | IdempotencyInProgress | Conflict;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
warning @nxgt/typespec/merged-status-reply: create declares two replies with a body of status 409: the emitter merges their bodies under the first one's description. Declare the one the route sends.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Why:** `@typespec/openapi3` keeps both bodies, as one reply whose body is
|
|
132
|
+
either, under the first one's description: the second's is gone, and a
|
|
133
|
+
header either reply declares required becomes optional.
|
|
134
|
+
|
|
135
|
+
**Fix:** declare the one the route sends. Both carry the envelope, and the
|
|
136
|
+
`message` key tells them apart:
|
|
137
|
+
|
|
138
|
+
```tsp
|
|
139
|
+
@post create(@body post: Post): Post | IdempotencyInProgress;
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Two plain bodies, `Post | Draft`, are one reply whose body is either; two
|
|
143
|
+
`@error` models without a `@statusCode` share `default`, and one
|
|
144
|
+
description; bodies of different content types are negotiated. All three
|
|
145
|
+
pass.
|
|
146
|
+
|
|
87
147
|
## `Unknown decorator @operationId`
|
|
88
148
|
|
|
89
149
|
**When:** a spec writes `@operationId` next to `@operationIds`.
|
package/lib/operation-ids.tsp
CHANGED
|
@@ -20,3 +20,11 @@ namespace Nxgt;
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
extern dec operationIds(target: Interface);
|
|
23
|
+
|
|
24
|
+
namespace Private {
|
|
25
|
+
/**
|
|
26
|
+
* Names the operation after `item`, `<operation><Item>`, where
|
|
27
|
+
* `@operationIds` names it. `Resource`'s alone: not public.
|
|
28
|
+
*/
|
|
29
|
+
extern dec itemOperation(target: Operation, item: Model);
|
|
30
|
+
}
|
package/lib/resources.tsp
CHANGED
|
@@ -23,8 +23,9 @@ model SortParameters<Field extends string> {
|
|
|
23
23
|
* The five operations of a resource, at `/<route>` and `/<route>/{id}`:
|
|
24
24
|
* `list` pages by offset, with the `Filters` the spec writes and a sort by
|
|
25
25
|
* `SortField`; `read`, `create`, `update` (a merge patch) and `delete`.
|
|
26
|
-
* Extend it with `@route`; `@operationIds` names
|
|
27
|
-
*
|
|
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`.
|
|
28
29
|
*
|
|
29
30
|
* @template Item The resource's model.
|
|
30
31
|
* @template Filters A model of `@query` properties, spread into `list`.
|
|
@@ -43,10 +44,12 @@ interface Resource<
|
|
|
43
44
|
list(...Filters, ...PageParameters, ...SortParameters<SortField>): Page<Item>;
|
|
44
45
|
|
|
45
46
|
/** One resource. */
|
|
47
|
+
@Private.itemOperation(Item)
|
|
46
48
|
@get
|
|
47
49
|
read(@path id: Id): Item | NotFound;
|
|
48
50
|
|
|
49
51
|
/** Creates a resource. */
|
|
52
|
+
@Private.itemOperation(Item)
|
|
50
53
|
@post
|
|
51
54
|
create(@body item: Create<Item>): {
|
|
52
55
|
@statusCode _: 201;
|
|
@@ -54,10 +57,12 @@ interface Resource<
|
|
|
54
57
|
} | BadRequest | Conflict;
|
|
55
58
|
|
|
56
59
|
/** Updates a resource with a merge patch. */
|
|
60
|
+
@Private.itemOperation(Item)
|
|
57
61
|
@patch
|
|
58
62
|
update(@path id: Id, @body item: MergePatchUpdate<Item>): Item | BadRequest | NotFound | Conflict;
|
|
59
63
|
|
|
60
64
|
/** Deletes a resource. */
|
|
65
|
+
@Private.itemOperation(Item)
|
|
61
66
|
@delete
|
|
62
67
|
delete(@path id: Id): NoContentResponse | NotFound;
|
|
63
68
|
}
|