Skip to content

Commit 3c51c55

Browse files
mashazyuscheidtdavJerryVincentjona159
authored
Feat: post api boxes (#616)
* feat: add draft for port of user registration to resource route * feat: partly implement refresh token * docs: simplify contributing and add info about api routes and shared logic * feat(api): finalize user registration endpoint * fix(tests): get the tests to run be reconfiguring build steps * docs(db): readd db setup and seed scripts with README info for it * fix: wrong import of utils * refactor: remove leftover custom server stuff * fix(tests): add missing refresh token table * fix(tests): reenable remaining tests for registration * fix(ci): remove playwright and use correct node version * fix(ci): run the tests with a postgres container * feat(tests): add coverage report * fix(build): reorganize server modules to correctly split client/ server * fix(build): miss an import * fix(build): remove leftovers from custom server implementation * chore(deps): bump react-router dependencies * chore(deps): update react-router * feat/user me api (#559) * feat(api): add api routes for /users/me * fix(tests): api me PUT * feat(api): add delete me endpoint * feat(api): add root route (#560) * start * new commit * tested docs * added a route * Added API Docs * modified * removed unsupported packages * updated * Modified * script generation without using ts-node. * modified * fix: update package-lock.json * Updated (#575) * Updated README * Updated README * feat: add command for drizzle studio * feat: devices loader * feat: load single device * feat: uncomment get boxes, delete box path * feat(wip): add boxes test suite * feat: add devices service * fix: some types, formatting * Removed duplicate Documentation section (#576) * Updated README * Updated README * Removed duplicate section. * Update README.md * Feat/api email and password (#561) * feat(api): add email-confirmation endpoint * feat(api): add request password reset * feat(api): add password reset * feat(api): implement resend email confirmation (without sending yet) * feat/api auth (#562) * feat(api): add email-confirmation endpoint * feat(api): add request password reset * feat(api): add password reset * feat(api): implement resend email confirmation (without sending yet) * feat(api): add sign-in, sign-out and refresh-routes to api * feat(api): implement refresh endpoint --------- Co-authored-by: jona159 <[email protected]> * feat(api): boxes for user endpoints (#573) * feat/api misc (#571) * feat(api): boxes for user endpoints * feat(api): add tags and stats route scaffold * feat(api): implement tags route * refactor: remove unnecessary imports * feat(api): implement statistics route --------- Co-authored-by: jona159 <[email protected]> * feat(api): add route and test files * feat: add test code * feat: add dummy sensors to devices and implement getting them back * feat: prefer dev server in no production envs and hide dev in prod * feat(docs): start adding docs to route * feat: wip devices api * feat: finish up to the point where we need measurements * fix: api routes without need for measurements * fix: stats call * fix: remaining tests * fix: frontend issue from changing the service implementation * fix: tests * refactor: use modern syntax for assertion * feat: adjust for zod schema * feat: add drizzle check * feat: add phenomenon and dates to where clause * feat: update returned data format of users/me/boxes endpoint to match old data format * feat: expose additional device attributes * fix: return jwt token as access_token * feat: add post and get /boxes * refactor: move CreateBoxSchema to devices-service.server.ts * fix: comment get /boxes route since this functionality is not implemented * fix: linting errors * fix: failing tests * feat: add type check * feat: add check for authHeader for GET /users/me/boxes * refactor: return box with sensor data from createDevice * test: createDevice * test: post /boxes * fix: return last measurements as string * fix: lastMeasurements types * fix: linter warnings * refactor: device.server.ts * fix: linter warnings * doc: udpate post api documentation * fix: after merging dev into current branch. * refactor: remove access_token mentions from api responses * refactor: add check for request type for api/boxes endpoint * doc: update api/boxes documentation after allowing different request types * refactor: remove unused code and fix linter errors * fix: linting issues --------- Co-authored-by: David Scheidt <[email protected]> Co-authored-by: David Scheidt <[email protected]> Co-authored-by: JerryVincent <[email protected]> Co-authored-by: Jerry Vincent <[email protected]> Co-authored-by: jona159 <[email protected]> Co-authored-by: jona159 <[email protected]>
1 parent 829659e commit 3c51c55

File tree

13 files changed

+1678
-280
lines changed

13 files changed

+1678
-280
lines changed

app/lib/device-transform.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { type Device, type Sensor } from '~/schema';
2+
3+
export type DeviceWithSensors = Device & {
4+
sensors: Sensor[];
5+
};
6+
7+
export type TransformedDevice = {
8+
_id: string;
9+
name: string;
10+
description: string | null;
11+
image: string | null;
12+
link: string | null;
13+
grouptag: string[];
14+
exposure: string | null;
15+
model: string | null;
16+
latitude: number;
17+
longitude: number;
18+
useAuth: boolean | null;
19+
public: boolean | null;
20+
status: string | null;
21+
createdAt: Date;
22+
updatedAt: Date;
23+
expiresAt: Date | null;
24+
userId: string;
25+
sensorWikiModel?: string | null;
26+
currentLocation: {
27+
type: "Point";
28+
coordinates: number[];
29+
timestamp: string;
30+
};
31+
lastMeasurementAt: string;
32+
loc: Array<{
33+
type: "Feature";
34+
geometry: {
35+
type: "Point";
36+
coordinates: number[];
37+
timestamp: string;
38+
};
39+
}>;
40+
integrations: {
41+
mqtt: {
42+
enabled: boolean;
43+
};
44+
};
45+
sensors: Array<{
46+
_id: string;
47+
title: string | null;
48+
unit: string | null;
49+
sensorType: string | null;
50+
lastMeasurement: {
51+
value: string;
52+
createdAt: string;
53+
} | null;
54+
}>;
55+
};
56+
57+
/**
58+
* Transforms a device with sensors from database format to openSenseMap API format
59+
* @param box - Device object with sensors from database
60+
* @returns Transformed device in openSenseMap API format
61+
*
62+
* Note: Converts lastMeasurement.value from number to string to match API specification
63+
*/
64+
export function transformDeviceToApiFormat(
65+
box: DeviceWithSensors
66+
): TransformedDevice {
67+
const { id, tags, sensors, ...rest } = box;
68+
const timestamp = box.updatedAt.toISOString();
69+
const coordinates = [box.longitude, box.latitude];
70+
71+
return {
72+
_id: id,
73+
grouptag: tags || [],
74+
...rest,
75+
currentLocation: {
76+
type: "Point",
77+
coordinates,
78+
timestamp
79+
},
80+
lastMeasurementAt: timestamp,
81+
loc: [{
82+
geometry: { type: "Point", coordinates, timestamp },
83+
type: "Feature"
84+
}],
85+
integrations: { mqtt: { enabled: false } },
86+
sensors: sensors?.map((sensor) => ({
87+
_id: sensor.id,
88+
title: sensor.title,
89+
unit: sensor.unit,
90+
sensorType: sensor.sensorType,
91+
lastMeasurement: sensor.lastMeasurement
92+
? {
93+
createdAt: sensor.lastMeasurement.createdAt,
94+
// Convert numeric values to string to match API specification
95+
value: typeof sensor.lastMeasurement.value === 'number'
96+
? String(sensor.lastMeasurement.value)
97+
: sensor.lastMeasurement.value,
98+
}
99+
: null,
100+
})) || [],
101+
};
102+
}

app/lib/devices-service.server.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
import { Device, User } from '~/schema'
1+
import { z } from 'zod'
22
import {
33
deleteDevice as deleteDeviceById,
44
} from '~/models/device.server'
55
import { verifyLogin } from '~/models/user.server'
6-
import { z } from 'zod'
6+
import { type Device, type User } from '~/schema'
7+
8+
export const CreateBoxSchema = z.object({
9+
name: z.string().min(1, "Name is required").max(100, "Name too long"),
10+
exposure: z.enum(["indoor", "outdoor", "mobile", "unknown"]).optional().default("unknown"),
11+
location: z.array(z.number()).length(2, "Location must be [longitude, latitude]"),
12+
grouptag: z.array(z.string()).optional().default([]),
13+
model: z.enum(["homeV2Lora", "homeV2Ethernet", "homeV2Wifi", "senseBox:Edu", "luftdaten.info", "Custom"]).optional().default("Custom"),
14+
sensors: z.array(z.object({
15+
id: z.string(),
16+
icon: z.string().optional(),
17+
title: z.string().min(1, "Sensor title is required"),
18+
unit: z.string().min(1, "Sensor unit is required"),
19+
sensorType: z.string().min(1, "Sensor type is required"),
20+
})).optional().default([]),
21+
});
722

823
export const BoxesQuerySchema = z.object({
924
format: z.enum(["json", "geojson"] ,{

app/models/device.server.ts

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
11
import { point } from '@turf/helpers'
2-
import { eq, sql, desc, ilike, inArray, arrayContains, and } from 'drizzle-orm'
2+
import { eq, sql, desc, ilike, arrayContains, and } from 'drizzle-orm'
33
import { type Point } from 'geojson'
44
import { drizzleClient } from '~/db.server'
55
import { device, location, sensor, type Device, type Sensor } from '~/schema'
66

7+
const BASE_DEVICE_COLUMNS = {
8+
id: true,
9+
name: true,
10+
description: true,
11+
image: true,
12+
link: true,
13+
tags: true,
14+
exposure: true,
15+
model: true,
16+
latitude: true,
17+
longitude: true,
18+
status: true,
19+
createdAt: true,
20+
updatedAt: true,
21+
expiresAt: true,
22+
sensorWikiModel: true,
23+
} as const;
24+
25+
const DEVICE_COLUMNS_WITH_SENSORS = {
26+
...BASE_DEVICE_COLUMNS,
27+
useAuth: true,
28+
public: true,
29+
userId: true,
30+
} as const;
31+
732
export function getDevice({ id }: Pick<Device, 'id'>) {
833
return drizzleClient.query.device.findFirst({
934
where: (device, { eq }) => eq(device.id, id),
10-
columns: {
11-
createdAt: true,
12-
description: true,
13-
exposure: true,
14-
id: true,
15-
image: true,
16-
latitude: true,
17-
longitude: true,
18-
link: true,
19-
model: true,
20-
name: true,
21-
sensorWikiModel: true,
22-
status: true,
23-
updatedAt: true,
24-
tags: true,
25-
expiresAt: true,
26-
},
35+
columns: BASE_DEVICE_COLUMNS,
2736
with: {
2837
user: {
2938
columns: {
@@ -109,15 +118,9 @@ export function deleteDevice({ id }: Pick<Device, 'id'>) {
109118
export function getUserDevices(userId: Device['userId']) {
110119
return drizzleClient.query.device.findMany({
111120
where: (device, { eq }) => eq(device.userId, userId),
112-
columns: {
113-
id: true,
114-
name: true,
115-
latitude: true,
116-
longitude: true,
117-
exposure: true,
118-
model: true,
119-
createdAt: true,
120-
updatedAt: true,
121+
columns: DEVICE_COLUMNS_WITH_SENSORS,
122+
with: {
123+
sensors: true,
121124
},
122125
})
123126
}
@@ -356,9 +359,7 @@ interface BuildWhereClauseOptions {
356359
) {
357360
const { minimal, limit } = opts;
358361
const { includeColumns, whereClause } = buildWhereClause(opts);
359-
360362
columns = minimal ? MINIMAL_COLUMNS : { ...DEFAULT_COLUMNS, ...columns };
361-
362363
relations = {
363364
...relations,
364365
...includeColumns
@@ -388,7 +389,11 @@ export async function createDevice(deviceData: any, userId: string) {
388389
tags: deviceData.tags,
389390
userId: userId,
390391
name: deviceData.name,
392+
description: deviceData.description,
393+
image: deviceData.image,
394+
link: deviceData.link,
391395
exposure: deviceData.exposure,
396+
public: deviceData.public ?? false,
392397
expiresAt: deviceData.expiresAt
393398
? new Date(deviceData.expiresAt)
394399
: null,
@@ -400,18 +405,31 @@ export async function createDevice(deviceData: any, userId: string) {
400405
if (!createdDevice) {
401406
throw new Error('Failed to create device.')
402407
}
403-
// Add sensors in the same transaction
408+
409+
// Add sensors in the same transaction and collect them
410+
const createdSensors = [];
404411
if (deviceData.sensors && Array.isArray(deviceData.sensors)) {
405412
for (const sensorData of deviceData.sensors) {
406-
await tx.insert(sensor).values({
407-
title: sensorData.title,
408-
unit: sensorData.unit,
409-
sensorType: sensorData.sensorType,
410-
deviceId: createdDevice.id, // Reference the created device ID
411-
})
413+
const [newSensor] = await tx.insert(sensor)
414+
.values({
415+
title: sensorData.title,
416+
unit: sensorData.unit,
417+
sensorType: sensorData.sensorType,
418+
deviceId: createdDevice.id, // Reference the created device ID
419+
})
420+
.returning();
421+
422+
if (newSensor) {
423+
createdSensors.push(newSensor);
424+
}
412425
}
413426
}
414-
return createdDevice
427+
428+
// Return device with sensors
429+
return {
430+
...createdDevice,
431+
sensors: createdSensors
432+
};
415433
})
416434
return newDevice
417435
} catch (error) {

app/models/sensor.server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ export async function getSensorsWithLastMeasurement(
7575
count?: number,
7676
): Promise<SensorWithLatestMeasurement | SensorWithLatestMeasurement[]>;
7777

78-
7978
export async function getSensorsWithLastMeasurement(
8079
deviceId: Sensor["deviceId"],
8180
sensorId: Sensor["id"] | undefined = undefined,

0 commit comments

Comments
 (0)