Skip to content

Commit 1070f3b

Browse files
committed
feat(cli): add basic addQuery and addMutation operations
1 parent c5e70fa commit 1070f3b

File tree

13 files changed

+438
-12
lines changed

13 files changed

+438
-12
lines changed

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,19 +215,16 @@ builder.mutation('modifyWidget', {
215215
path: '/v1/users/{userId}/widgets/{widgetId}',
216216

217217
request: {
218-
schema: builder.schemas.ref('ModifyWidgetInput'),
218+
schema: 'ModifyWidgetInput',
219219
parameters: {
220220
userId: { in: 'path', description: 'ID of the owning user of the widget' },
221221
widgetId: { in: 'path' },
222222
}
223223
}
224224
response: {
225-
schema: builder.schema.ref('Widget'),
225+
schema: 'Widget',
226226
},
227-
errors: [
228-
builder.components.ref({ responses: 'BadRequest' }),
229-
builder.components.ref({ responses: 'NotFound' }),
230-
]
227+
errors: ['BadRequest', 'NotFound'],
231228
})
232229
```
233230

@@ -249,7 +246,7 @@ paths:
249246
requestBody:
250247
application/json:
251248
// note the `Body` label appended to the end
252-
spec: { $ref: '#/components/schemas/ModifyWidgetInputBody' }
249+
schema: { $ref: '#/components/schemas/ModifyWidgetInputBody' }
253250
```
254251
255252
> **Note**

package-lock.json

Lines changed: 28 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
"packages/*"
1515
],
1616
"devDependencies": {
17+
"@sinclair/typebox": "^0.28.11",
1718
"@typescript-eslint/eslint-plugin": "^5.59.6",
1819
"@typescript-eslint/parser": "^5.59.6",
19-
"eslint": "^8.40.0"
20+
"eslint": "^8.40.0",
21+
"openapi-types": "^12.1.0"
2022
}
2123
}

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"lint": "npm run lint:code",
3737
"lint:code": "eslint . --cache --fix --ext .ts",
3838
"pretest": "npm run lint && tsc --noEmit",
39-
"test": "node --loader ts-node/esm --test src/index.test.ts",
39+
"test": "node --loader ts-node/esm --test src/*.test.ts",
4040
"posttest": "npm run format"
4141
},
4242
"devDependencies": {

packages/cli/src/builder/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./service-builder.js";
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Type } from "@sinclair/typebox";
2+
import assert from "node:assert";
3+
import test from "node:test";
4+
import { ResponseDefinition } from "../types/operations.js";
5+
import { YARPCServiceBuilder } from "./service-builder.js";
6+
7+
const schemas = {
8+
GetWidgetInput: Type.Object({
9+
userId: Type.String(),
10+
widgetId: Type.String(),
11+
}),
12+
Widget: Type.Object({
13+
widgetId: Type.String(),
14+
name: Type.String(),
15+
}),
16+
Error: Type.Object({
17+
code: Type.String(),
18+
message: Type.String(),
19+
details: Type.Optional(Type.Any()),
20+
}),
21+
};
22+
23+
const errors: Record<string, ResponseDefinition<keyof typeof schemas>> = {
24+
NotFound: {
25+
description: "The entity was not found",
26+
schema: "Error",
27+
},
28+
};
29+
30+
const exampleQuery = {
31+
description: "getWidget",
32+
path: "/users/{userId}/widgets",
33+
request: {
34+
schema: "GetWidgetInput",
35+
parameters: {
36+
userId: { in: "path" },
37+
},
38+
},
39+
response: {
40+
schema: "Widget",
41+
},
42+
} as const;
43+
44+
const exampleMutation = {
45+
description: "createWidget",
46+
request: {
47+
schema: "CreateWidgetInput",
48+
},
49+
response: {
50+
schema: "Widget",
51+
},
52+
} as const;
53+
54+
test("addQuery", (t) => {
55+
const builder = new YARPCServiceBuilder({
56+
info: {
57+
title: "Test",
58+
version: "1.0.0",
59+
},
60+
components: {
61+
errors,
62+
schemas,
63+
},
64+
oas: {
65+
"x-service-name": "test",
66+
},
67+
});
68+
69+
t.test("adds a query", () => {
70+
builder.addQuery("foo", exampleQuery);
71+
assert.equal(builder.operations.foo, {
72+
type: "query",
73+
operationId: "foo",
74+
...exampleQuery,
75+
});
76+
});
77+
78+
t.test("throws if operationId already exists", () => {
79+
builder.addQuery("foo", exampleQuery);
80+
assert.throws(() => builder.addQuery("foo", exampleQuery), {
81+
message: `Operation with id "foo" already exists`,
82+
});
83+
});
84+
});
85+
86+
test("addMutation", (t) => {
87+
const builder = new YARPCServiceBuilder({
88+
info: {
89+
title: "Test",
90+
version: "1.0.0",
91+
},
92+
components: {
93+
schemas: {
94+
CreateWidgetInput: Type.Object({
95+
id: Type.Integer(),
96+
}),
97+
Widget: Type.Object({
98+
id: Type.Integer(),
99+
}),
100+
},
101+
},
102+
});
103+
104+
t.test("adds a query", () => {
105+
builder.addMutation("foo", exampleMutation);
106+
assert.equal(builder.operations.foo, {
107+
type: "query",
108+
operationId: "foo",
109+
...exampleMutation,
110+
});
111+
});
112+
113+
t.test("throws if operationId already exists", () => {
114+
builder.addMutation("foo", exampleMutation);
115+
assert.throws(() => builder.addMutation("foo", exampleMutation), {
116+
message: `Operation with id "foo" already exists`,
117+
});
118+
});
119+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import assert from "node:assert";
2+
import {
3+
OperationDefinition,
4+
ServiceComponentsObject,
5+
ServiceDefinition,
6+
} from "../types/index.js";
7+
import {
8+
AddMutationDefinition,
9+
AddQueryDefinition,
10+
withMutationDefaults,
11+
withQueryDefaults,
12+
} from "./utils.js";
13+
14+
export class YARPCServiceBuilder<
15+
TBase extends ServiceDefinition<ServiceComponentsObject>
16+
> {
17+
#service: ServiceDefinition<TBase["components"]>;
18+
#operations: Map<string, OperationDefinition<TBase["components"]>> =
19+
new Map();
20+
21+
constructor(config: ServiceDefinition<TBase["components"]>) {
22+
this.#service = config;
23+
}
24+
25+
get operations(): Record<string, OperationDefinition<TBase["components"]>> {
26+
return Object.fromEntries(this.#operations.entries());
27+
}
28+
29+
addQuery(
30+
operationId: string,
31+
operation: AddQueryDefinition<TBase["components"]>
32+
) {
33+
assert(
34+
!this.#operations.has(operationId),
35+
`Operation with id "${operationId}" already exists`
36+
);
37+
38+
this.#operations.set(
39+
operationId,
40+
Object.freeze(withQueryDefaults(operationId, operation))
41+
);
42+
}
43+
44+
addMutation(
45+
operationId: string,
46+
operation: AddMutationDefinition<TBase["components"]>
47+
) {
48+
assert(
49+
!this.#operations.has(operationId),
50+
`Operation with id "${operationId}" already exists`
51+
);
52+
53+
this.#operations.set(
54+
operationId,
55+
Object.freeze(withMutationDefaults(operationId, operation))
56+
);
57+
}
58+
}

packages/cli/src/builder/utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ComponentsObject } from "../types/oas";
2+
import {
3+
MutationDefinition,
4+
OperationDefinition,
5+
QueryDefinition,
6+
} from "../types/operations";
7+
8+
export type AddQueryDefinition<TComponents extends ComponentsObject> = Omit<
9+
QueryDefinition<TComponents>,
10+
"operationId" | "type"
11+
>;
12+
13+
export const withQueryDefaults = <TComponents extends ComponentsObject>(
14+
operationId: string,
15+
def: AddQueryDefinition<TComponents>
16+
): OperationDefinition<TComponents> => ({
17+
type: "query",
18+
operationId,
19+
method: "get",
20+
path: "/queries/" + operationId,
21+
response: {
22+
statusCode: 200,
23+
...def.response,
24+
},
25+
});
26+
27+
export type AddMutationDefinition<TComponents extends ComponentsObject> = Omit<
28+
MutationDefinition<TComponents>,
29+
"operationId" | "type"
30+
>;
31+
32+
export const withMutationDefaults = <TComponents extends ComponentsObject>(
33+
operationId: string,
34+
def: AddMutationDefinition<TComponents>
35+
): OperationDefinition<TComponents> => ({
36+
type: "mutation",
37+
operationId,
38+
method: "post",
39+
path: "/mutations/" + operationId,
40+
response: {
41+
statusCode: 200,
42+
...def.response,
43+
},
44+
});

packages/cli/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export const hello = "world";
1+
export * from "./builder/index.js";
2+
export * from "./types/index.js";

packages/cli/src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./oas.js";
2+
export * from "./operations.js";
3+
export * from "./service.js";

packages/cli/src/types/oas.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type {} from "node:http";
2+
import type { OpenAPIV3_1 } from "openapi-types";
3+
4+
export type HttpMethod =
5+
| "get"
6+
| "post"
7+
| "put"
8+
| "delete"
9+
| "patch"
10+
| "head"
11+
| "options";
12+
13+
export type ReferenceObject = OpenAPIV3_1.ReferenceObject;
14+
export type SchemaObject = OpenAPIV3_1.SchemaObject;
15+
16+
export interface OASDocument<
17+
TComponents extends ComponentsObject = ComponentsObject
18+
> extends Omit<OpenAPIV3_1.Document, "components"> {
19+
components: TComponents;
20+
}
21+
22+
export interface ComponentsObject
23+
extends Omit<OpenAPIV3_1.ComponentsObject, "schemas"> {
24+
schemas: Record<string, OpenAPIV3_1.SchemaObject>;
25+
}

0 commit comments

Comments
 (0)