-
-
Notifications
You must be signed in to change notification settings - Fork 0
Migrate deleting a boxes measurements #658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
35eeb87
42593f0
dcd635f
be4ebed
b4b8421
8fbb075
1de6e69
08f4405
4a3f8e4
29d3034
d164738
7566724
44894b4
77b4cc9
5612a5f
2e335d5
bbf5430
bf19c7e
7d91045
a5699de
760914b
81a1f9c
acf1770
eedb806
a6245ea
4712c6a
f63dc07
7741945
3b620a0
f51f518
5f7a6ef
f9ecca4
bd0e52e
31f793c
511b94a
98617c3
9492e13
8a721f7
ef552e1
46e89cd
fcf4b8d
1bd97eb
46b0422
787b662
95acb53
eb84619
84f57ca
a232d6b
cf36b0a
0a35d3b
89e8c04
9a8e679
32bdb9e
bba0bf5
3a14e54
153e83a
a50bc7c
6a057f1
350364e
abf6a28
b632e35
cda8787
42a4fea
7ef062a
b82f392
29ac73c
095bd6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| import { type ActionFunctionArgs } from 'react-router' | ||
| import z from 'zod' | ||
| import { getUserFromJwt } from '~/lib/jwt' | ||
| import { getUserDevices } from '~/models/device.server' | ||
| import { | ||
| deleteMeasurementsForSensor, | ||
| deleteSensorMeasurementsForTimeRange, | ||
| deleteSensorMeasurementsForTimes, | ||
| } from '~/models/measurement.server' | ||
| import { StandardResponse } from '~/utils/response-utils' | ||
|
|
||
| export async function action({ request, params }: ActionFunctionArgs) { | ||
| try { | ||
| const { deviceId, sensorId } = params | ||
| if (!deviceId || !sensorId) | ||
| return StandardResponse.badRequest( | ||
| 'Invalid device id or sensor id specified', | ||
| ) | ||
|
|
||
| const jwtResponse = await getUserFromJwt(request) | ||
|
|
||
| if (typeof jwtResponse === 'string') | ||
| return StandardResponse.forbidden( | ||
| 'Invalid JWT authorization. Please sign in to obtain new JWT.', | ||
| ) | ||
|
|
||
| if (request.method !== 'DELETE') | ||
| return StandardResponse.methodNotAllowed('Endpoint only supports DELETE') | ||
|
|
||
| const userDevices = await getUserDevices(jwtResponse.id) | ||
| if (!userDevices.some((d) => d.id === deviceId)) | ||
| return StandardResponse.forbidden( | ||
| 'You are not allowed to delete data of the given device', | ||
| ) | ||
|
|
||
| const device = userDevices.find((d) => d.id === deviceId) | ||
| if (!device?.sensors.some((s) => s.id === sensorId)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the old app would have thrown a 404 here (see Box.js l. 387)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried with the existing api and it indeed returns 404. Thanks for pointing out! |
||
| return StandardResponse.forbidden( | ||
| 'You are not allowed to delete data of the given sensor', | ||
| ) | ||
|
|
||
| try { | ||
| const parsedParams = await parseQueryParams(request) | ||
| let count = 0 | ||
|
|
||
| if (parsedParams.deleteAllMeasurements) | ||
| count = (await deleteMeasurementsForSensor(sensorId)).count | ||
| else if (parsedParams.timestamps) | ||
| count = ( | ||
| await deleteSensorMeasurementsForTimes( | ||
| sensorId, | ||
| parsedParams.timestamps, | ||
| ) | ||
| ).count | ||
| else if (parsedParams['from-date'] && parsedParams['to-date']) | ||
| count = ( | ||
| await deleteSensorMeasurementsForTimeRange( | ||
| sensorId, | ||
| parsedParams['from-date'], | ||
| parsedParams['to-date'], | ||
| ) | ||
| ).count | ||
|
|
||
| return StandardResponse.ok({ | ||
| message: `Successfully deleted ${count} of sensor ${sensorId}`, | ||
| }) | ||
| } catch (e) { | ||
| if (e instanceof Response) return e | ||
| else throw e | ||
| } | ||
| } catch (err: any) { | ||
| return StandardResponse.internalServerError( | ||
| err.message || 'An unexpected error occured', | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| const DeleteQueryParams = z | ||
scheidtdav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .object({ | ||
| 'from-date': z | ||
| .string() | ||
| .transform((s) => new Date(s)) | ||
| .refine((d) => !isNaN(d.getTime()), { | ||
| message: 'from-date is invalid', | ||
| }) | ||
| .optional(), | ||
| 'to-date': z | ||
| .string() | ||
| .transform((s) => new Date(s)) | ||
| .refine((d) => !isNaN(d.getTime()), { | ||
| message: 'to-date is invalid', | ||
| }) | ||
| .optional(), | ||
| timestamps: z | ||
| .preprocess((val) => { | ||
| if (Array.isArray(val)) return val | ||
| else return [val] | ||
| }, z.array(z.string())) | ||
| .transform((a) => a.map((i) => new Date(i))) | ||
| .refine((a) => a.some((i) => !isNaN(i.getTime())), { | ||
| message: 'timestamps contains invalid input', | ||
| }) | ||
| .optional(), | ||
| deleteAllMeasurements: z.coerce.boolean().optional(), | ||
| }) | ||
| .superRefine((data, ctx) => { | ||
| const fromDateSet = data['from-date'] !== undefined | ||
| const toDateSet = data['to-date'] !== undefined | ||
| const timestampsSet = data.timestamps !== undefined | ||
| const deleteAllSet = data.deleteAllMeasurements !== undefined | ||
|
|
||
| if (deleteAllSet && (timestampsSet || fromDateSet || toDateSet)) { | ||
| const paths: string[] = [] | ||
| if (timestampsSet) paths.push('timestamps') | ||
| if (fromDateSet) paths.push('from-date') | ||
| if (toDateSet) paths.push('to-date') | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: 'Parameter deleteAllMeasurements can only be used by itself', | ||
| path: paths, | ||
| }) | ||
| } else if (!deleteAllSet && timestampsSet && fromDateSet && toDateSet) | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: | ||
| 'Please specify only timestamps or a range with from-date and to-date', | ||
| path: ['timestamps', 'from-date', 'to-date'], | ||
| }) | ||
| else if (!deleteAllSet && !timestampsSet && !fromDateSet && !toDateSet) | ||
| ctx.addIssue({ | ||
| code: z.ZodIssueCode.custom, | ||
| message: | ||
| 'Please specify only timestamps or a range with from-date and to-date', | ||
| path: ['timestamps', 'from-date', 'to-date'], | ||
| }) | ||
| }) | ||
|
|
||
| const parseQueryParams = async ( | ||
| request: Request, | ||
| ): Promise<z.infer<typeof DeleteQueryParams>> => { | ||
| const url = new URL(request.url) | ||
| let params: Record<string, any> | ||
| if (request.method !== 'GET') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ì think this endpoint should only support the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember why its there and you are perfectly correct.. removing it! |
||
| const contentType = request.headers.get('content-type') || '' | ||
| if (contentType.includes('application/json')) { | ||
| try { | ||
| params = await request.json() | ||
| } catch { | ||
| params = Object.fromEntries(url.searchParams) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think in principle it is a good idea to fall back to params, but i am a bit unsure if we should do this here with regards to backwards-compatibility. In the old docs the endpoint is described as using the request body, so i guess this would introduce a new behaviour? Also if the content type is invalid (so !=
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying it with the existing API, even though the docs specify it to work with body parameters, I actually does not seem to work with those at all. (funnily enough, you have to set content-type: application/json for it to work even though you never send json data. Definitely an inconsistency. The question is if we should loosen that or keep it for now?) |
||
| } | ||
| } else { | ||
| params = Object.fromEntries(url.searchParams) | ||
| } | ||
| } else { | ||
| params = Object.fromEntries(url.searchParams) | ||
| } | ||
|
|
||
| const parseResult = DeleteQueryParams.safeParse(params) | ||
|
|
||
| if (!parseResult.success) { | ||
| const firstError = parseResult.error.errors[0] | ||
| const message = firstError.message || 'Invalid query parameters' | ||
| throw StandardResponse.badRequest(message) | ||
| } | ||
|
|
||
| return parseResult.data | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,44 @@ | ||
| import { type LoaderFunction, type LoaderFunctionArgs } from "react-router"; | ||
| import { getLatestMeasurementsForSensor } from "~/lib/measurement-service.server"; | ||
| import { StandardResponse } from "~/utils/response-utils"; | ||
| import { type LoaderFunction, type LoaderFunctionArgs } from 'react-router' | ||
| import { getLatestMeasurementsForSensor } from '~/lib/measurement-service.server' | ||
| import { StandardResponse } from '~/utils/response-utils' | ||
|
|
||
| export const loader: LoaderFunction = async ({ | ||
| request, | ||
| params, | ||
| request, | ||
| params, | ||
| }: LoaderFunctionArgs): Promise<Response> => { | ||
| try { | ||
| const deviceId = params.deviceId; | ||
| if (deviceId === undefined) | ||
| return StandardResponse.badRequest("Invalid device id specified"); | ||
| try { | ||
| const deviceId = params.deviceId | ||
| if (deviceId === undefined) | ||
| return StandardResponse.badRequest('Invalid device id specified') | ||
|
|
||
| const sensorId = params.sensorId; | ||
| if (sensorId === undefined) | ||
| return StandardResponse.badRequest("Invalid sensor id specified"); | ||
| const sensorId = params.sensorId | ||
| if (sensorId === undefined) | ||
| return StandardResponse.badRequest('Invalid sensor id specified') | ||
|
|
||
| const searchParams = new URL(request.url).searchParams; | ||
| const onlyValue = | ||
| (searchParams.get("onlyValue")?.toLowerCase() ?? "") === "true"; | ||
| if (sensorId === undefined && onlyValue) | ||
| return StandardResponse.badRequest("onlyValue can only be used when a sensor id is specified"); | ||
| const searchParams = new URL(request.url).searchParams | ||
| const onlyValue = | ||
| (searchParams.get('onlyValue')?.toLowerCase() ?? '') === 'true' | ||
| if (sensorId === undefined && onlyValue) | ||
| return StandardResponse.badRequest( | ||
| 'onlyValue can only be used when a sensor id is specified', | ||
| ) | ||
|
|
||
| const meas = await getLatestMeasurementsForSensor(deviceId, sensorId, undefined); | ||
| const meas = await getLatestMeasurementsForSensor( | ||
| deviceId, | ||
| sensorId, | ||
| undefined, | ||
| ) | ||
|
|
||
| if (meas == null) | ||
| return StandardResponse.notFound("Device not found."); | ||
| if (meas == null) return StandardResponse.notFound('Device not found.') | ||
|
|
||
| if (onlyValue) | ||
| return StandardResponse.ok(meas["lastMeasurement"]?.value ?? null); | ||
| if (onlyValue) | ||
| return StandardResponse.ok(meas['lastMeasurement']?.value ?? null) | ||
|
|
||
| return StandardResponse.ok({ ...meas, _id: meas.id } /* for legacy purposes */); | ||
| } catch (err) { | ||
| console.warn(err); | ||
| return StandardResponse.internalServerError(); | ||
| } | ||
| }; | ||
| return StandardResponse.ok( | ||
| { ...meas, _id: meas.id } /* for legacy purposes */, | ||
| ) | ||
| } catch (err) { | ||
| console.warn(err) | ||
| return StandardResponse.internalServerError() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlikely to break anything, but the message in the old api was
User does not own this senseBox(mentioning this because in the old app there was also a test for this: ```expect(response).to.have.json({ code: 'Forbidden', message: 'User does not own this senseBox' });There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes good point, I'm thinking we should still changes this, because we should get rid of the strict "senseBox" branding and use the more generic "device" term. As you said its unlikely to break anything, so I guess we can keep the message?