@nxgt/typespec 0.2.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 +8 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +5 -5
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/operation-ids.d.ts +8 -1
- package/dist/operation-ids.d.ts.map +1 -1
- package/docs/guide/operation-ids.md +9 -2
- package/docs/guide/resources.md +52 -7
- package/docs/roadmap.md +11 -5
- package/docs/troubleshooting.md +9 -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}`
|
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;AAElD,OAAO,
|
|
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
|
@@ -29,7 +29,10 @@ var $lib = createTypeSpecLibrary({
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
state: {
|
|
32
|
-
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
|
+
}
|
|
33
36
|
}
|
|
34
37
|
});
|
|
35
38
|
|
|
@@ -84,6 +87,7 @@ function validateOneReplyPerStatus(program) {
|
|
|
84
87
|
|
|
85
88
|
// src/operation-ids.ts
|
|
86
89
|
import {
|
|
90
|
+
getFriendlyName,
|
|
87
91
|
isTemplateInstance,
|
|
88
92
|
navigateProgram
|
|
89
93
|
} from "@typespec/compiler";
|
|
@@ -95,6 +99,14 @@ import {
|
|
|
95
99
|
function operationIds(context, target) {
|
|
96
100
|
context.program.stateSet($lib.stateKeys.operationIds).add(target);
|
|
97
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
|
+
}
|
|
98
110
|
function isMarked(program, target) {
|
|
99
111
|
return program.stateSet($lib.stateKeys.operationIds).has(target) || target.sourceInterfaces.some((source) => isMarked(program, source));
|
|
100
112
|
}
|
|
@@ -108,7 +120,7 @@ function validateOperationIds(program) {
|
|
|
108
120
|
operation(operation) {
|
|
109
121
|
const named = isNamed(program, operation);
|
|
110
122
|
const own = getOperationId(program, operation);
|
|
111
|
-
const id = own ?? (named ? `${operation.name}${operation
|
|
123
|
+
const id = own ?? (named ? `${operation.name}${nounOf(program, operation)}` : resolveOperationId(program, operation));
|
|
112
124
|
if (seen.has(id) && (named || seen.get(id))) {
|
|
113
125
|
$lib.reportDiagnostic(program, {
|
|
114
126
|
code: "duplicate-operation-id",
|
|
@@ -129,7 +141,8 @@ function $onValidate(program) {
|
|
|
129
141
|
validateOneReplyPerStatus(program);
|
|
130
142
|
}
|
|
131
143
|
var $decorators = {
|
|
132
|
-
Nxgt: { operationIds }
|
|
144
|
+
Nxgt: { operationIds },
|
|
145
|
+
"Nxgt.Private": { itemOperation }
|
|
133
146
|
};
|
|
134
147
|
export {
|
|
135
148
|
$decorators,
|
|
@@ -137,5 +150,5 @@ export {
|
|
|
137
150
|
$onValidate
|
|
138
151
|
};
|
|
139
152
|
|
|
140
|
-
//# debugId=
|
|
153
|
+
//# debugId=D1BB8C6D97A7E43C64756E2164756E21
|
|
141
154
|
//# 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: { description: 'The interfaces marked with @operationIds' },\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\ttype Interface,\n\tisTemplateInstance,\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/** 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}${operation
|
|
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 {
|
|
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"
|
|
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,EAAE,aAAa,2CAA2C;AAAA,
|
|
11
|
-
"debugId": "
|
|
10
|
+
"mappings": "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",
|
|
11
|
+
"debugId": "D1BB8C6D97A7E43C64756E2164756E21",
|
|
12
12
|
"names": []
|
|
13
13
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
8
8
|
'merged-status-reply': {
|
|
9
9
|
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
10
10
|
};
|
|
11
|
-
}, Record<string, any>, "operationIds">;
|
|
11
|
+
}, Record<string, any>, "operationIds" | "itemOperations">;
|
|
12
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"}
|
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"}
|
|
@@ -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,11 +6,9 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
a body of one status code a warning, `merged-status-reply`, instead of
|
|
13
|
-
replies the emitter merges without a word.
|
|
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`.
|
|
14
12
|
|
|
15
13
|
## Later
|
|
16
14
|
|
|
@@ -20,6 +18,14 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
20
18
|
|
|
21
19
|
## Shipped
|
|
22
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
|
+
|
|
23
29
|
### 0.1.0
|
|
24
30
|
|
|
25
31
|
- **Error replies.** The envelope `@nxgt/openapi-hono` sends, and the
|
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
|
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
|
}
|