Skip to content

Commit 6c4ced0

Browse files
timber-theyTimberjona159
authored
Feat/utilize response utils (#657)
* feat: use StandardResponse for API requests * fix: change return code back to forbidden * fix: tests * fix: proper import in param-utils * fix: don't stringify and don't return anything for no content * fix: dont allow creating response body in no content util --------- Co-authored-by: Timber <[email protected]> Co-authored-by: jona159 <[email protected]>
1 parent ec6f6be commit 6c4ced0

34 files changed

+471
-1246
lines changed
Lines changed: 50 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,128 +1,53 @@
1-
import { type ActionFunction, type ActionFunctionArgs } from "react-router";
2-
import { postSingleMeasurement } from "~/lib/measurement-service.server";
1+
import { type ActionFunction, type ActionFunctionArgs } from 'react-router'
2+
import { postSingleMeasurement } from '~/lib/measurement-service.server'
3+
import { StandardResponse } from '~/utils/response-utils'
34

45
export const action: ActionFunction = async ({
5-
request,
6-
params,
6+
request,
7+
params,
78
}: ActionFunctionArgs): Promise<Response> => {
8-
try {
9-
const { deviceId, sensorId } = params;
10-
11-
if (!deviceId || !sensorId) {
12-
return Response.json(
13-
{
14-
code: "Bad Request",
15-
message: "Invalid device id or sensor id specified",
16-
},
17-
{
18-
status: 400,
19-
headers: {
20-
"Content-Type": "application/json; charset=utf-8",
21-
},
22-
},
23-
);
24-
}
25-
26-
const authorization = request.headers.get("authorization");
27-
const contentType = request.headers.get("content-type") || "";
28-
29-
if (!contentType.includes("application/json")) {
30-
return Response.json(
31-
{
32-
code: "Unsupported Media Type",
33-
message: "Content-Type must be application/json",
34-
},
35-
{
36-
status: 415,
37-
headers: {
38-
"Content-Type": "application/json; charset=utf-8",
39-
},
40-
},
41-
);
42-
}
43-
44-
const body = await request.json();
45-
46-
await postSingleMeasurement(deviceId, sensorId, body, authorization);
47-
48-
return new Response("Measurement saved in box", {
49-
status: 201,
50-
headers: {
51-
"Content-Type": "text/plain; charset=utf-8",
52-
},
53-
});
54-
} catch (err: any) {
55-
if (err.name === "UnauthorizedError") {
56-
return Response.json(
57-
{
58-
code: "Unauthorized",
59-
message: err.message,
60-
},
61-
{
62-
status: 401,
63-
headers: {
64-
"Content-Type": "application/json; charset=utf-8",
65-
},
66-
},
67-
);
68-
}
69-
70-
if (err.name === "NotFoundError") {
71-
return Response.json(
72-
{
73-
code: "Not Found",
74-
message: err.message,
75-
},
76-
{
77-
status: 404,
78-
headers: {
79-
"Content-Type": "application/json; charset=utf-8",
80-
},
81-
},
82-
);
83-
}
84-
85-
if (err.name === "UnprocessableEntityError" || err.type === "UnprocessableEntityError") {
86-
return Response.json(
87-
{
88-
code: "Unprocessable Entity",
89-
message: err.message,
90-
},
91-
{
92-
status: 422,
93-
headers: {
94-
"Content-Type": "application/json; charset=utf-8",
95-
},
96-
},
97-
);
98-
}
99-
100-
if (err.name === "ModelError" && err.type === "UnprocessableEntityError") {
101-
return Response.json(
102-
{
103-
code: "Unprocessable Entity",
104-
message: err.message,
105-
},
106-
{
107-
status: 422,
108-
headers: {
109-
"Content-Type": "application/json; charset=utf-8",
110-
},
111-
},
112-
);
113-
}
114-
115-
return Response.json(
116-
{
117-
code: "Internal Server Error",
118-
message: err.message || "An unexpected error occurred",
119-
},
120-
{
121-
status: 500,
122-
headers: {
123-
"Content-Type": "application/json; charset=utf-8",
124-
},
125-
},
126-
);
127-
}
128-
};
9+
try {
10+
const { deviceId, sensorId } = params
11+
12+
if (!deviceId || !sensorId)
13+
return StandardResponse.badRequest(
14+
'Invalid device id or sensor id specified',
15+
)
16+
17+
const authorization = request.headers.get('authorization')
18+
const contentType = request.headers.get('content-type') || ''
19+
20+
if (!contentType.includes('application/json'))
21+
return StandardResponse.unsupportedMediaType(
22+
'Content-Type must be application/json',
23+
)
24+
25+
const body = await request.json()
26+
27+
await postSingleMeasurement(deviceId, sensorId, body, authorization)
28+
29+
return new Response('Measurement saved in box', {
30+
status: 201,
31+
headers: {
32+
'Content-Type': 'text/plain; charset=utf-8',
33+
},
34+
})
35+
} catch (err: any) {
36+
if (err.name === 'UnauthorizedError')
37+
return StandardResponse.unauthorized(err.message)
38+
39+
if (err.name === 'NotFoundError')
40+
return StandardResponse.notFound(err.message)
41+
42+
if (
43+
err.name === 'UnprocessableEntityError' ||
44+
err.type === 'UnprocessableEntityError' ||
45+
(err.name === 'ModelError' && err.type === 'UnprocessableEntityError')
46+
)
47+
return StandardResponse.unprocessableContent(err.message)
48+
49+
return StandardResponse.internalServerError(
50+
err.message || 'An unexpected error occurred',
51+
)
52+
}
53+
}

app/routes/api.boxes.$deviceId.data.$sensorId.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getMeasurements } from "~/models/sensor.server";
44
import { type Measurement } from "~/schema";
55
import { convertToCsv } from "~/utils/csv";
66
import { parseDateParam, parseEnumParam } from "~/utils/param-utils";
7-
import { badRequest, internalServerError, notFound } from "~/utils/response-utils";
7+
import { StandardResponse } from "~/utils/response-utils";
88

99
/**
1010
* @openapi
@@ -152,7 +152,7 @@ export const loader: LoaderFunction = async ({
152152

153153
let meas: Measurement[] | TransformedMeasurement[] = await getMeasurements(sensorId, fromDate.toISOString(), toDate.toISOString());
154154
if (meas == null)
155-
return notFound("Device not found.");
155+
return StandardResponse.notFound("Device not found.");
156156

157157
if (outliers)
158158
meas = transformOutliers(meas, outlierWindow, outliers == "replace");
@@ -177,7 +177,7 @@ export const loader: LoaderFunction = async ({
177177

178178
} catch (err) {
179179
console.warn(err);
180-
return internalServerError();
180+
return StandardResponse.internalServerError();
181181
}
182182
};
183183

@@ -196,10 +196,10 @@ function collectParameters(request: Request, params: Params<string>):
196196
// deviceId is there for legacy reasons
197197
const deviceId = params.deviceId;
198198
if (deviceId === undefined)
199-
return badRequest("Invalid device id specified");
199+
return StandardResponse.badRequest("Invalid device id specified");
200200
const sensorId = params.sensorId;
201201
if (sensorId === undefined)
202-
return badRequest("Invalid sensor id specified");
202+
return StandardResponse.badRequest("Invalid sensor id specified");
203203

204204
const url = new URL(request.url);
205205

@@ -211,7 +211,7 @@ function collectParameters(request: Request, params: Params<string>):
211211
let outlierWindow: number = 15;
212212
if (outlierWindowParam !== null) {
213213
if (Number.isNaN(outlierWindowParam) || Number(outlierWindowParam) < 1 || Number(outlierWindowParam) > 50)
214-
return badRequest("Illegal value for parameter outlier-window. Allowed values: numbers between 1 and 50");
214+
return StandardResponse.badRequest("Illegal value for parameter outlier-window. Allowed values: numbers between 1 and 50");
215215
outlierWindow = Number(outlierWindowParam);
216216
}
217217

Lines changed: 10 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import { type ActionFunction, type ActionFunctionArgs } from "react-router";
22
import { postNewMeasurements } from "~/lib/measurement-service.server";
3+
import { StandardResponse } from "~/utils/response-utils";
34

45
export const action: ActionFunction = async ({
56
request,
67
params,
78
}: ActionFunctionArgs): Promise<Response> => {
89
try {
910
const deviceId = params.deviceId;
10-
if (deviceId === undefined) {
11-
return Response.json(
12-
{
13-
code: "Bad Request",
14-
message: "Invalid device id specified",
15-
},
16-
{
17-
status: 400,
18-
headers: {
19-
"Content-Type": "application/json; charset=utf-8",
20-
},
21-
},
22-
);
23-
}
11+
if (deviceId === undefined)
12+
return StandardResponse.badRequest("Invalid device id specified");
2413

2514
const searchParams = new URL(request.url).searchParams;
2615
const luftdaten = searchParams.get("luftdaten") !== null;
@@ -55,57 +44,15 @@ export const action: ActionFunction = async ({
5544
});
5645
} catch (err: any) {
5746
// Handle different error types
58-
if (err.name === "UnauthorizedError") {
59-
return Response.json(
60-
{
61-
code: "Unauthorized",
62-
message: err.message,
63-
},
64-
{
65-
status: 401,
66-
headers: {
67-
"Content-Type": "application/json; charset=utf-8",
68-
},
69-
},
70-
);
71-
}
47+
if (err.name === "UnauthorizedError")
48+
return StandardResponse.unauthorized(err.message);
7249

73-
if (err.name === "ModelError" && err.type === "UnprocessableEntityError") {
74-
return Response.json(
75-
{
76-
code: "UnprocessableEntity",
77-
message: err.message,
78-
},
79-
{ status: 422 }
80-
);
81-
}
50+
if (err.name === "ModelError" && err.type === "UnprocessableEntityError")
51+
return StandardResponse.unprocessableContent(err.message);
8252

83-
if (err.name === "UnsupportedMediaTypeError") {
84-
return Response.json(
85-
{
86-
code: "Unsupported Media Type",
87-
message: err.message,
88-
},
89-
{
90-
status: 415,
91-
headers: {
92-
"Content-Type": "application/json; charset=utf-8",
93-
},
94-
},
95-
);
96-
}
53+
if (err.name === "UnsupportedMediaTypeError")
54+
return StandardResponse.unsupportedMediaType(err.message);
9755

98-
return Response.json(
99-
{
100-
code: "Internal Server Error",
101-
message: err.message || "An unexpected error occurred",
102-
},
103-
{
104-
status: 500,
105-
headers: {
106-
"Content-Type": "application/json; charset=utf-8",
107-
},
108-
},
109-
);
56+
return StandardResponse.internalServerError(err.message || "An unexpected error occurred");
11057
}
11158
};

app/routes/api.boxes.$deviceId.locations.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Params, type LoaderFunction, type LoaderFunctionArgs } from "react-router";
22
import { getLocations } from "~/models/device.server";
33
import { parseDateParam, parseEnumParam } from "~/utils/param-utils";
4-
import { badRequest, internalServerError, notFound } from "~/utils/response-utils";
4+
import { StandardResponse } from "~/utils/response-utils";
55

66
/**
77
* @openapi
@@ -104,7 +104,7 @@ export const loader: LoaderFunction = async ({
104104

105105
const locations = await getLocations({ id: deviceId}, fromDate, toDate);
106106
if (!locations)
107-
return notFound("Device not found");
107+
return StandardResponse.notFound("Device not found");
108108

109109
const jsonLocations = locations.map((location) => {
110110
return {
@@ -140,7 +140,7 @@ export const loader: LoaderFunction = async ({
140140

141141
} catch (err) {
142142
console.warn(err);
143-
return internalServerError();
143+
return StandardResponse.internalServerError();
144144
}
145145
};
146146

@@ -153,7 +153,7 @@ function collectParameters(request: Request, params: Params<string>):
153153
} {
154154
const deviceId = params.deviceId;
155155
if (deviceId === undefined)
156-
return badRequest("Invalid device id specified");
156+
return StandardResponse.badRequest("Invalid device id specified");
157157

158158
const url = new URL(request.url);
159159

0 commit comments

Comments
 (0)