Skip to content

Commit ef1e6ab

Browse files
authored
Merge pull request #130 from ty-ras/issue/129-contextless-endpoints-by-default
Issue/129 contextless endpoints by default
2 parents 217e576 + f95f9c5 commit ef1e6ab

13 files changed

+653
-64
lines changed

endpoint-spec/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ty-ras/endpoint-spec",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"author": {
55
"name": "Stanislav Muhametsin",
66
"email": "[email protected]",

endpoint-spec/src/__test__/additional-data.spec.ts

+104
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import * as mp from "./missing-parts";
99
import * as epValidation from "./endpoint-validation";
1010
import * as protocol from "./protocol";
1111

12+
/* eslint-disable sonarjs/no-duplicate-string */
13+
1214
test("Verify that modifying additional endpoint spec data type works", async (c) => {
1315
c.plan(14);
1416

@@ -112,6 +114,108 @@ test("Verify that modifying additional endpoint spec data type works", async (c)
112114
c.deepEqual(permissionChecks, []);
113115
});
114116

117+
test("Verify that modifying additional endpoint spec data type works also for inline endpoint functions", async (c) => {
118+
c.plan(14);
119+
120+
// Create app and tracking array
121+
const { app, permissionChecks } = newBuilder();
122+
type StateSpecBase = spec.StateSpecBaseOfAppBuilder<typeof app>;
123+
124+
// Create endpoints
125+
const url = app.url`/${mp.urlParameter("urlParam", protocol.urlParam)}`({});
126+
const authState = {
127+
userId: true,
128+
} as const satisfies StateSpecBase;
129+
const unauthState = {
130+
userId: false,
131+
} as const satisfies StateSpecBase;
132+
133+
const seenArgs: Array<unknown> = [];
134+
135+
const endpointInfos = [
136+
url.endpoint<Endpoint1>({})(
137+
{
138+
method: "GET",
139+
state: authState,
140+
responseBody: mp.responseBody("simpleResponseBody"),
141+
permissions: "permissions",
142+
},
143+
(args) => {
144+
seenArgs.push(args);
145+
return "simpleResponseBody" as const;
146+
},
147+
),
148+
149+
url.endpoint<Endpoint2>({})(
150+
{
151+
method: "POST",
152+
state: unauthState,
153+
responseBody: mp.responseBody("simpleResponseBody"),
154+
},
155+
(args) => {
156+
seenArgs.push(args);
157+
return "simpleResponseBody" as const;
158+
},
159+
),
160+
];
161+
162+
const { endpoints } = app.createEndpoints({}, endpointInfos);
163+
164+
// Initial asserts
165+
c.deepEqual(endpoints.length, 1);
166+
c.deepEqual(permissionChecks, []);
167+
168+
// Check endpoint with permissions
169+
await epValidation.validateEndpoint(
170+
c,
171+
endpoints[0],
172+
() => seenArgs,
173+
// No additional prefix is needed
174+
"",
175+
// Remove body, query, and state from inputs as they are unused by this endpoint
176+
(info) => {
177+
const result = info.splice(3);
178+
result.unshift(["stateInformation.stateInfo.0", "userId"]);
179+
return result;
180+
},
181+
// Expected output is also different
182+
{
183+
contentType: "text/plain",
184+
output: '"simpleResponseBody"',
185+
},
186+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
187+
(args) => data.omit(args, "body", "query") as any,
188+
);
189+
c.deepEqual(permissionChecks, ["permissions"]);
190+
191+
// Check endpoint without permissions
192+
permissionChecks.length = 0;
193+
seenArgs.length = 0;
194+
await epValidation.validateEndpoint(
195+
c,
196+
endpoints[0],
197+
() => seenArgs,
198+
// No additional prefix is needed
199+
"",
200+
// Remove body, query, and state from inputs as they are unused by this endpoint
201+
(info) => {
202+
const result = info.splice(3);
203+
result.unshift(["stateInformation.stateInfo.0", "userId"]);
204+
return result;
205+
},
206+
// Expected output is also different
207+
{
208+
contentType: "text/plain",
209+
output: '"simpleResponseBody"',
210+
},
211+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
212+
(args) => data.omit(args, "body", "query") as any,
213+
// Now invoke the unauthenticatedEndpoint() method by using POST
214+
"POST",
215+
);
216+
c.deepEqual(permissionChecks, []);
217+
});
218+
115219
/**
116220
* This [higher-kinded types (HKT)](https://www.matechs.com/blog/encoding-hkts-in-typescript-once-again) interface simulates real-life situation, where endpoints which require authentication (their state spec contains "userId" property), also must specify the permissions that the operation requires.
117221
* In this test setup, the permission type is simply "string", but in reality, it can be something more complex and domain-specific.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @file This file contains tests related to context visibility using TyRAS builder.
3+
*/
4+
5+
import test from "ava";
6+
import type * as spec from "..";
7+
import * as mp from "./missing-parts";
8+
import * as protocol from "./protocol";
9+
import * as epValidation from "./endpoint-validation";
10+
11+
test("Verify that making context visible works", async (c) => {
12+
c.plan(6);
13+
14+
const app = mp.newBuilder({}).showContextToEndpoints();
15+
16+
const url = app.url`/api/something/${mp.urlParameter(
17+
"urlParam",
18+
protocol.urlParam,
19+
)}`({});
20+
const state = {
21+
userId: true,
22+
} as const;
23+
const seenArgs: Array<
24+
spec.GetMethodArgs<protocol.SomeEndpoint, typeof url, typeof state>
25+
> = [];
26+
const endpoint = url.endpoint<protocol.SomeEndpoint>({})(
27+
{
28+
method: "GET",
29+
responseBody: mp.responseBody(protocol.responseBody),
30+
state,
31+
query: mp.query({
32+
queryParam: {
33+
decoder: protocol.queryParam,
34+
required: false,
35+
},
36+
}),
37+
requestBody: app.requestBody(protocol.requestBody),
38+
responseHeaders: mp.responseHeaders({
39+
responseHeader: {
40+
encoder: protocol.resHeader,
41+
required: true,
42+
},
43+
}),
44+
},
45+
(args) => {
46+
seenArgs.push(args);
47+
return {
48+
body: "responseBody",
49+
headers: {
50+
responseHeader: "resHeader",
51+
},
52+
} as const;
53+
},
54+
);
55+
56+
const { endpoints } = app.createEndpoints({}, endpoint);
57+
c.deepEqual(
58+
endpoints.length,
59+
1,
60+
"There must be exactly one endpoint created by application builder.",
61+
);
62+
await epValidation.validateEndpoint(
63+
c,
64+
endpoints[0],
65+
() => seenArgs,
66+
undefined,
67+
undefined,
68+
undefined,
69+
undefined,
70+
undefined,
71+
true,
72+
);
73+
});
74+
75+
test("Verify that hiding context works", async (c) => {
76+
c.plan(6);
77+
78+
const app = mp
79+
.newBuilder({})
80+
.showContextToEndpoints()
81+
.hideContextForEndpoints();
82+
83+
const url = app.url`/api/something/${mp.urlParameter(
84+
"urlParam",
85+
protocol.urlParam,
86+
)}`({});
87+
const state = {
88+
userId: true,
89+
} as const;
90+
const seenArgs: Array<
91+
spec.GetMethodArgs<protocol.SomeEndpoint, typeof url, typeof state>
92+
> = [];
93+
const endpoint = url.endpoint<protocol.SomeEndpoint>({})(
94+
{
95+
method: "GET",
96+
responseBody: mp.responseBody(protocol.responseBody),
97+
state,
98+
query: mp.query({
99+
queryParam: {
100+
decoder: protocol.queryParam,
101+
required: false,
102+
},
103+
}),
104+
requestBody: app.requestBody(protocol.requestBody),
105+
responseHeaders: mp.responseHeaders({
106+
responseHeader: {
107+
encoder: protocol.resHeader,
108+
required: true,
109+
},
110+
}),
111+
},
112+
(args) => {
113+
seenArgs.push(args);
114+
return {
115+
body: "responseBody",
116+
headers: {
117+
responseHeader: "resHeader",
118+
},
119+
} as const;
120+
},
121+
);
122+
123+
const { endpoints } = app.createEndpoints({}, endpoint);
124+
c.deepEqual(
125+
endpoints.length,
126+
1,
127+
"There must be exactly one endpoint created by application builder.",
128+
);
129+
await epValidation.validateEndpoint(
130+
c,
131+
endpoints[0],
132+
() => seenArgs,
133+
undefined,
134+
undefined,
135+
undefined,
136+
undefined,
137+
undefined,
138+
false,
139+
);
140+
});

endpoint-spec/src/__test__/endpoint-validation.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const validateEndpoint = async (
2828
args: ep.AppEndpointHandlerFunctionArgs<mp.ServerContext>,
2929
) => ep.AppEndpointHandlerFunctionArgs<mp.ServerContext> = (args) => args,
3030
method: protocol.HttpMethod = "GET",
31+
includeContext = false,
3132
) => {
3233
c.truthy(endpoint, "Given endpoint must be of given type");
3334

@@ -88,7 +89,13 @@ export const validateEndpoint = async (
8889
const result = await handler(args);
8990
if (result.error === "none") {
9091
c.deepEqual(result.data, expectedOutput);
91-
c.deepEqual(getInstanceData(), [data.omit(args, "headers")]);
92+
c.deepEqual(getInstanceData(), [
93+
data.omit(
94+
args,
95+
"headers",
96+
...(includeContext ? [] : ["context" as const]),
97+
),
98+
]);
9299
} else {
93100
c.fail("Handler did not return validated response body.");
94101
c.log(result);

endpoint-spec/src/__test__/inline.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const stateSpec = {
2121

2222
export const implementation: spec.InlineEndpointImplementation<
2323
mp.DefaultStateHKT,
24-
mp.ServerContext,
24+
never,
2525
protocol.SomeEndpoint,
2626
typeof stateSpec
2727
> = (args) => {

endpoint-spec/src/__test__/missing-parts.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function newBuilder(
2929
DefaultStateHKT,
3030
{},
3131
ServerContext,
32+
never,
3233
typeof CONTENT_TYPE,
3334
typeof CONTENT_TYPE,
3435
typeof CONTENT_TYPE,

0 commit comments

Comments
 (0)