diff --git a/app/lib/device-transform.ts b/app/lib/device-transform.ts index 0937c223..88d3257b 100644 --- a/app/lib/device-transform.ts +++ b/app/lib/device-transform.ts @@ -1,102 +1,107 @@ -import { type Device, type Sensor } from '~/schema'; +import { type Device, type Sensor } from '~/schema' export type DeviceWithSensors = Device & { - sensors: Sensor[]; -}; + sensors: Sensor[] +} export type TransformedDevice = { - _id: string; - name: string; - description: string | null; - image: string | null; - link: string | null; - grouptag: string[]; - exposure: string | null; - model: string | null; - latitude: number; - longitude: number; - useAuth: boolean | null; - public: boolean | null; - status: string | null; - createdAt: Date; - updatedAt: Date; - expiresAt: Date | null; - userId: string; - sensorWikiModel?: string | null; - currentLocation: { - type: "Point"; - coordinates: number[]; - timestamp: string; - }; - lastMeasurementAt: string; - loc: Array<{ - type: "Feature"; - geometry: { - type: "Point"; - coordinates: number[]; - timestamp: string; - }; - }>; - integrations: { - mqtt: { - enabled: boolean; - }; - }; - sensors: Array<{ - _id: string; - title: string | null; - unit: string | null; - sensorType: string | null; - lastMeasurement: { - value: string; - createdAt: string; - } | null; - }>; -}; + _id: string + name: string + description: string | null + image: string | null + link: string | null + grouptag: string[] + exposure: string | null + model: string | null + latitude: number + longitude: number + useAuth: boolean | null + public: boolean | null + status: string | null + createdAt: Date + updatedAt: Date + expiresAt: Date | null + userId: string + sensorWikiModel?: string | null + currentLocation: { + type: 'Point' + coordinates: number[] + timestamp: string + } + lastMeasurementAt: string + loc: Array<{ + type: 'Feature' + geometry: { + type: 'Point' + coordinates: number[] + timestamp: string + } + }> + integrations: { + mqtt: { + enabled: boolean + } + } + sensors: Array<{ + _id: string + title: string | null + unit: string | null + sensorType: string | null + lastMeasurement: { + value: string + createdAt: string + } | null + }> +} /** * Transforms a device with sensors from database format to openSenseMap API format * @param box - Device object with sensors from database * @returns Transformed device in openSenseMap API format - * + * * Note: Converts lastMeasurement.value from number to string to match API specification */ export function transformDeviceToApiFormat( - box: DeviceWithSensors + box: DeviceWithSensors, ): TransformedDevice { - const { id, tags, sensors, ...rest } = box; - const timestamp = box.updatedAt.toISOString(); - const coordinates = [box.longitude, box.latitude]; - - return { - _id: id, - grouptag: tags || [], - ...rest, - currentLocation: { - type: "Point", - coordinates, - timestamp - }, - lastMeasurementAt: timestamp, - loc: [{ - geometry: { type: "Point", coordinates, timestamp }, - type: "Feature" - }], - integrations: { mqtt: { enabled: false } }, - sensors: sensors?.map((sensor) => ({ - _id: sensor.id, - title: sensor.title, - unit: sensor.unit, - sensorType: sensor.sensorType, - lastMeasurement: sensor.lastMeasurement - ? { - createdAt: sensor.lastMeasurement.createdAt, - // Convert numeric values to string to match API specification - value: typeof sensor.lastMeasurement.value === 'number' - ? String(sensor.lastMeasurement.value) - : sensor.lastMeasurement.value, - } - : null, - })) || [], - }; + const { id, tags, sensors, ...rest } = box + const timestamp = box.updatedAt.toISOString() + const coordinates = [box.longitude, box.latitude] + + return { + _id: id, + grouptag: tags || [], + ...rest, + currentLocation: { + type: 'Point', + coordinates, + timestamp, + }, + lastMeasurementAt: timestamp, + loc: [ + { + geometry: { type: 'Point', coordinates, timestamp }, + type: 'Feature', + }, + ], + integrations: { mqtt: { enabled: false } }, + sensors: + sensors?.map((sensor) => ({ + _id: sensor.id, + title: sensor.title, + unit: sensor.unit, + sensorType: sensor.sensorType, + icon: sensor.icon, + lastMeasurement: sensor.lastMeasurement + ? { + createdAt: sensor.lastMeasurement.createdAt, + // Convert number to string to match API specification + value: + typeof sensor.lastMeasurement.value === 'number' + ? String(sensor.lastMeasurement.value) + : sensor.lastMeasurement.value, + } + : null, + })) || [], + } } diff --git a/app/models/device.server.ts b/app/models/device.server.ts index ee122780..4b461eed 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -1,581 +1,839 @@ -import { point } from "@turf/helpers"; -import { eq, sql, desc, ilike, arrayContains, and, between } from "drizzle-orm"; +import { point } from '@turf/helpers' +import { eq, sql, desc, ilike, arrayContains, and, between } from 'drizzle-orm' import BaseNewDeviceEmail, { - messages as BaseNewDeviceMessages, -} from "emails/base-new-device"; -import { messages as NewLufdatenDeviceMessages } from "emails/new-device-luftdaten"; -import { messages as NewSenseboxDeviceMessages } from "emails/new-device-sensebox"; -import { type Point } from "geojson"; -import { drizzleClient } from "~/db.server"; -import { sendMail } from "~/lib/mail.server"; + messages as BaseNewDeviceMessages, +} from 'emails/base-new-device' +import { messages as NewLufdatenDeviceMessages } from 'emails/new-device-luftdaten' +import { messages as NewSenseboxDeviceMessages } from 'emails/new-device-sensebox' +import { type Point } from 'geojson' +import { drizzleClient } from '~/db.server' +import { sendMail } from '~/lib/mail.server' import { - device, - deviceToLocation, - location, - sensor, - user, - type Device, - type Sensor, -} from "~/schema"; + device, + deviceToLocation, + location, + sensor, + user, + type Device, + type Sensor, +} from '~/schema' +import { getSensorsForModel } from '~/utils/model-definitions' const BASE_DEVICE_COLUMNS = { - id: true, - name: true, - description: true, - image: true, - link: true, - tags: true, - exposure: true, - model: true, - latitude: true, - longitude: true, - status: true, - createdAt: true, - updatedAt: true, - expiresAt: true, - useAuth: true, - sensorWikiModel: true, -} as const; + id: true, + name: true, + description: true, + image: true, + link: true, + tags: true, + exposure: true, + model: true, + latitude: true, + longitude: true, + status: true, + createdAt: true, + updatedAt: true, + expiresAt: true, + useAuth: true, + sensorWikiModel: true, +} as const const DEVICE_COLUMNS_WITH_SENSORS = { - ...BASE_DEVICE_COLUMNS, - useAuth: true, - public: true, - userId: true, -} as const; - -export function getDevice({ id }: Pick) { - return drizzleClient.query.device.findFirst({ - where: (device, { eq }) => eq(device.id, id), - columns: BASE_DEVICE_COLUMNS, - with: { - user: { - columns: { - id: true, - }, - }, - logEntries: { - where: (entry, { eq }) => eq(entry.public, true), - columns: { - id: true, - content: true, - createdAt: true, - public: true, - deviceId: true, - }, - }, - locations: { - // https://github.com/drizzle-team/drizzle-orm/pull/2778 - // with: { - // geometry: true - // }, - columns: { - // time: true, - }, - extras: { - time: sql`time`.as("time"), - }, - with: { - geometry: { - columns: {}, - extras: { - x: sql`ST_X(${location.location})`.as("x"), - y: sql`ST_Y(${location.location})`.as("y"), - }, - }, - }, - // limit: 1000, - }, - sensors: true, - }, - }); + ...BASE_DEVICE_COLUMNS, + useAuth: true, + public: true, + userId: true, +} as const + +export class DeviceUpdateError extends Error { + constructor( + message: string, + public statusCode: number = 400, + ) { + super(message) + this.name = 'DeviceUpdateError' + } +} + +export function getDevice({ id }: Pick) { + return drizzleClient.query.device.findFirst({ + where: (device, { eq }) => eq(device.id, id), + columns: BASE_DEVICE_COLUMNS, + with: { + user: { + columns: { + id: true, + }, + }, + logEntries: { + where: (entry, { eq }) => eq(entry.public, true), + columns: { + id: true, + content: true, + createdAt: true, + public: true, + deviceId: true, + }, + }, + locations: { + // https://github.com/drizzle-team/drizzle-orm/pull/2778 + // with: { + // geometry: true + // }, + columns: { + // time: true, + }, + extras: { + time: sql`time`.as('time'), + }, + with: { + geometry: { + columns: {}, + extras: { + x: sql`ST_X(${location.location})`.as('x'), + y: sql`ST_Y(${location.location})`.as('y'), + }, + }, + }, + // limit: 1000, + }, + sensors: true, + }, + }) } export function getLocations( - { id }: Pick, - fromDate: Date, - toDate: Date + { id }: Pick, + fromDate: Date, + toDate: Date, ) { - return drizzleClient - .select({ - time: deviceToLocation.time, - x: sql`ST_X(${location.location})`.as("x"), - y: sql`ST_Y(${location.location})`.as("y"), - }) - .from(location) - .innerJoin(deviceToLocation, eq(deviceToLocation.locationId, location.id)) - .where( - and( - eq(deviceToLocation.deviceId, id), - between(deviceToLocation.time, fromDate, toDate) - ) - ) - .orderBy(desc(deviceToLocation.time)); + return drizzleClient + .select({ + time: deviceToLocation.time, + x: sql`ST_X(${location.location})`.as('x'), + y: sql`ST_Y(${location.location})`.as('y'), + }) + .from(location) + .innerJoin(deviceToLocation, eq(deviceToLocation.locationId, location.id)) + .where( + and( + eq(deviceToLocation.deviceId, id), + between(deviceToLocation.time, fromDate, toDate), + ), + ) + .orderBy(desc(deviceToLocation.time)) } -export function getDeviceWithoutSensors({ id }: Pick) { - return drizzleClient.query.device.findFirst({ - where: (device, { eq }) => eq(device.id, id), - columns: { - id: true, - name: true, - exposure: true, - updatedAt: true, - latitude: true, - longitude: true, - }, - }); +export function getDeviceWithoutSensors({ id }: Pick) { + return drizzleClient.query.device.findFirst({ + where: (device, { eq }) => eq(device.id, id), + columns: { + id: true, + name: true, + exposure: true, + updatedAt: true, + latitude: true, + longitude: true, + }, + }) } export type DeviceWithoutSensors = Awaited< - ReturnType ->; - -export function updateDeviceInfo({ - id, - name, - exposure, -}: Pick) { - return drizzleClient - .update(device) - .set({ name: name, exposure: exposure }) - .where(eq(device.id, id)); -} + ReturnType +> export function updateDeviceLocation({ - id, - latitude, - longitude, -}: Pick) { - return drizzleClient - .update(device) - .set({ latitude: latitude, longitude: longitude }) - .where(eq(device.id, id)); + id, + latitude, + longitude, +}: Pick) { + return drizzleClient + .update(device) + .set({ latitude: latitude, longitude: longitude }) + .where(eq(device.id, id)) +} + +export type UpdateDeviceArgs = { + name?: string + exposure?: string + grouptag?: string | string[] + description?: string + link?: string + image?: string + model?: string + useAuth?: boolean + location?: { lat: number; lng: number; height?: number } + sensors?: SensorUpdateArgs[] +} + +type SensorUpdateArgs = { + _id?: string + title?: string + unit?: string + sensorType?: string + icon?: string + deleted?: any + edited?: any + new?: any +} + +export async function updateDevice( + deviceId: string, + args: UpdateDeviceArgs, +): Promise { + const setColumns: Record = {} + const updatableFields: (keyof UpdateDeviceArgs)[] = [ + 'name', + 'exposure', + 'description', + 'image', + 'model', + 'useAuth', + 'link', + ] + + for (const field of updatableFields) { + if (args[field] !== undefined) { + // Handle empty string -> null for specific fields (backwards compatibility) + if ( + (field === 'description' || field === 'link' || field === 'image') && + args[field] === '' + ) { + setColumns[field] = null + } else { + setColumns[field] = args[field] + } + } + } + + if ('grouptag' in args) { + if (Array.isArray(args.grouptag)) { + // Empty array -> null for backwards compatibility + setColumns['tags'] = args.grouptag.length === 0 ? null : args.grouptag + } else if (args.grouptag != null) { + // Empty string -> null + setColumns['tags'] = args.grouptag === '' ? null : [args.grouptag] + } else { + setColumns['tags'] = null + } + } + + const result = await drizzleClient.transaction(async (tx) => { + if (args.location) { + const { lat, lng, height } = args.location + + const pointWKT = `POINT(${lng} ${lat})` + + const [existingLocation] = await tx + .select() + .from(location) + .where(sql`ST_Equals(location, ST_GeomFromText(${pointWKT}, 4326))`) + .limit(1) + + let locationId: bigint + + if (existingLocation) { + locationId = existingLocation.id + } else { + const [newLocation] = await tx + .insert(location) + .values({ + location: sql`ST_GeomFromText(${pointWKT}, 4326)`, + }) + .returning() + + if (!newLocation) { + throw new Error('Failed to create location') + } + + locationId = newLocation.id + } + + await tx + .insert(deviceToLocation) + .values({ + deviceId, + locationId, + time: sql`NOW()`, + }) + .onConflictDoNothing() + + setColumns['latitude'] = lat + setColumns['longitude'] = lng + } + + let updatedDevice + if (Object.keys(setColumns).length > 0) { + ;[updatedDevice] = await tx + .update(device) + .set({ ...setColumns, updatedAt: sql`NOW()` }) + .where(eq(device.id, deviceId)) + .returning() + + if (!updatedDevice) { + throw new DeviceUpdateError(`Device ${deviceId} not found`, 404) + } + } else { + ;[updatedDevice] = await tx + .select() + .from(device) + .where(eq(device.id, deviceId)) + + if (!updatedDevice) { + throw new DeviceUpdateError(`Device ${deviceId} not found`, 404) + } + } + + if (args.sensors?.length) { + const existingSensors = await tx + .select() + .from(sensor) + .where(eq(sensor.deviceId, deviceId)) + + const sensorsToDelete = args.sensors.filter( + (s) => 'deleted' in s && s._id, + ) + const remainingSensorCount = + existingSensors.length - sensorsToDelete.length + + if (sensorsToDelete.length > 0 && remainingSensorCount < 1) { + throw new DeviceUpdateError( + 'Unable to delete sensor(s). A box needs at least one sensor.', + ) + } + + for (const s of args.sensors) { + const hasDeleted = 'deleted' in s + const hasEdited = 'edited' in s + const hasNew = 'new' in s + + if (!hasDeleted && !hasEdited && !hasNew) { + continue + } + + if (hasDeleted) { + if (!s._id) { + throw new DeviceUpdateError('Sensor deletion requires _id') + } + + const sensorExists = existingSensors.some( + (existing) => existing.id === s._id, + ) + + if (!sensorExists) { + throw new DeviceUpdateError( + `Sensor with id ${s._id} not found for deletion.`, + ) + } + + await tx.delete(sensor).where(eq(sensor.id, s._id)) + } else if (hasEdited && hasNew) { + if (!s.title || !s.unit || !s.sensorType) { + throw new DeviceUpdateError( + 'New sensor requires title, unit, and sensorType', + ) + } + + await tx.insert(sensor).values({ + title: s.title, + unit: s.unit, + sensorType: s.sensorType, + icon: s.icon, + deviceId, + }) + } else if (hasEdited && s._id) { + const sensorExists = existingSensors.some( + (existing) => existing.id === s._id, + ) + + if (!sensorExists) { + throw new DeviceUpdateError( + `Sensor with id ${s._id} not found for editing.`, + ) + } + + if (!s.title || !s.unit || !s.sensorType) { + throw new DeviceUpdateError( + 'Editing sensor requires all properties: _id, title, unit, sensorType, icon', + ) + } + + await tx + .update(sensor) + .set({ + title: s.title, + unit: s.unit, + sensorType: s.sensorType, + icon: s.icon, + updatedAt: sql`NOW()`, + }) + .where(eq(sensor.id, s._id)) + } + } + } + return updatedDevice + }) + + return result } -export function deleteDevice({ id }: Pick) { - return drizzleClient.delete(device).where(eq(device.id, id)); +export function deleteDevice({ id }: Pick) { + return drizzleClient.delete(device).where(eq(device.id, id)) } -export function getUserDevices(userId: Device["userId"]) { - return drizzleClient.query.device.findMany({ - where: (device, { eq }) => eq(device.userId, userId), - columns: DEVICE_COLUMNS_WITH_SENSORS, - with: { - sensors: true, - }, - }); +export function getUserDevices(userId: Device['userId']) { + return drizzleClient.query.device.findMany({ + where: (device, { eq }) => eq(device.userId, userId), + columns: DEVICE_COLUMNS_WITH_SENSORS, + with: { + sensors: true, + }, + }) } -type DevicesFormat = "json" | "geojson"; +type DevicesFormat = 'json' | 'geojson' -export async function getDevices(format: "json"): Promise; +export async function getDevices(format: 'json'): Promise export async function getDevices( - format: "geojson" -): Promise>; + format: 'geojson', +): Promise> export async function getDevices( - format?: DevicesFormat -): Promise>; - -export async function getDevices(format: DevicesFormat = "json") { - const devices = await drizzleClient.query.device.findMany({ - columns: { - id: true, - name: true, - latitude: true, - longitude: true, - exposure: true, - status: true, - createdAt: true, - tags: true, - }, - }); - - if (format === "geojson") { - const geojson: GeoJSON.FeatureCollection = { - type: "FeatureCollection", - features: [], - }; - - for (const device of devices) { - const coordinates = [device.longitude, device.latitude]; - const feature = point(coordinates, device); - geojson.features.push(feature); - } - - return geojson; - } - - return devices; + format?: DevicesFormat, +): Promise> + +export async function getDevices(format: DevicesFormat = 'json') { + const devices = await drizzleClient.query.device.findMany({ + columns: { + id: true, + name: true, + latitude: true, + longitude: true, + exposure: true, + status: true, + createdAt: true, + tags: true, + }, + }) + + if (format === 'geojson') { + const geojson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection', + features: [], + } + + for (const device of devices) { + const coordinates = [device.longitude, device.latitude] + const feature = point(coordinates, device) + geojson.features.push(feature) + } + + return geojson + } + + return devices } export async function getDevicesWithSensors() { - const rows = await drizzleClient - .select({ - device: device, - sensor: { - id: sensor.id, - title: sensor.title, - sensorWikiPhenomenon: sensor.sensorWikiPhenomenon, - lastMeasurement: sensor.lastMeasurement, - }, - }) - .from(device) - .leftJoin(sensor, eq(sensor.deviceId, device.id)); - const geojson: GeoJSON.FeatureCollection = { - type: "FeatureCollection", - features: [], - }; - - type PartialSensor = Pick< - Sensor, - "id" | "title" | "sensorWikiPhenomenon" | "lastMeasurement" - >; - const deviceMap = new Map< - string, - { device: Device & { sensors: PartialSensor[] } } - >(); - - const resultArray: Array<{ device: Device & { sensors: PartialSensor[] } }> = - rows.reduce( - (acc, row) => { - const device = row.device; - const sensor = row.sensor; - - if (!deviceMap.has(device.id)) { - const newDevice = { - device: { ...device, sensors: sensor ? [sensor] : [] }, - }; - deviceMap.set(device.id, newDevice); - acc.push(newDevice); - } else if (sensor) { - deviceMap.get(device.id)!.device.sensors.push(sensor); - } - - return acc; - }, - [] as Array<{ device: Device & { sensors: PartialSensor[] } }> - ); - - for (const device of resultArray) { - const coordinates = [device.device.longitude, device.device.latitude]; - const feature = point(coordinates, device.device); - geojson.features.push(feature); - } - - return geojson; + const rows = await drizzleClient + .select({ + device: device, + sensor: { + id: sensor.id, + title: sensor.title, + sensorWikiPhenomenon: sensor.sensorWikiPhenomenon, + lastMeasurement: sensor.lastMeasurement, + }, + }) + .from(device) + .leftJoin(sensor, eq(sensor.deviceId, device.id)) + const geojson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection', + features: [], + } + + type PartialSensor = Pick< + Sensor, + 'id' | 'title' | 'sensorWikiPhenomenon' | 'lastMeasurement' + > + const deviceMap = new Map< + string, + { device: Device & { sensors: PartialSensor[] } } + >() + + const resultArray: Array<{ device: Device & { sensors: PartialSensor[] } }> = + rows.reduce( + (acc, row) => { + const device = row.device + const sensor = row.sensor + + if (!deviceMap.has(device.id)) { + const newDevice = { + device: { ...device, sensors: sensor ? [sensor] : [] }, + } + deviceMap.set(device.id, newDevice) + acc.push(newDevice) + } else if (sensor) { + deviceMap.get(device.id)!.device.sensors.push(sensor) + } + + return acc + }, + [] as Array<{ device: Device & { sensors: PartialSensor[] } }>, + ) + + for (const device of resultArray) { + const coordinates = [device.device.longitude, device.device.latitude] + const feature = point(coordinates, device.device) + geojson.features.push(feature) + } + + return geojson } interface BuildWhereClauseOptions { - name?: string; - phenomenon?: string; - fromDate?: string | Date; - toDate?: string | Date; - bbox?: { - coordinates: number[][][]; - }; - near?: [number, number]; // [lat, lng] - maxDistance?: number; - grouptag?: string[]; - exposure?: string[]; - model?: string[]; + name?: string + phenomenon?: string + fromDate?: string | Date + toDate?: string | Date + bbox?: { + coordinates: (number | undefined)[][][] + } + near?: [number, number] // [lat, lng] + maxDistance?: number + grouptag?: string[] + exposure?: string[] + model?: string[] } export interface FindDevicesOptions extends BuildWhereClauseOptions { - minimal?: string | boolean; - limit?: number; - format?: "json" | "geojson"; + minimal?: string | boolean + limit?: number + format?: 'json' | 'geojson' } interface WhereClauseResult { - includeColumns: Record; - whereClause: any[]; + includeColumns: Record + whereClause: any[] } const buildWhereClause = function buildWhereClause( - opts: BuildWhereClauseOptions = {} + opts: BuildWhereClauseOptions = {}, ): WhereClauseResult { - const { - name, - phenomenon, - fromDate, - toDate, - bbox, - near, - maxDistance, - grouptag, - } = opts; - const clause = []; - const columns = {}; - - if (name) { - clause.push(ilike(device.name, `%${name}%`)); - } - - if (phenomenon) { - // @ts-ignore - columns["sensors"] = { - // @ts-ignore - where: (sensor, { ilike }) => - // @ts-ignore - ilike(sensorTable["title"], `%${phenomenon}%`), - }; - } - - // simple string parameters - // for (const param of ['exposure', 'model'] as const) { - // if (opts[param]) { - // clause.push(inArray(device[param], opts[param]!)); - // } - // } - - if (grouptag) { - clause.push(arrayContains(device.tags, grouptag)); - } - - // https://orm.drizzle.team/learn/guides/postgis-geometry-point - if (bbox) { - const [latSW, lngSW] = bbox.coordinates[0][0]; - const [latNE, lngNE] = bbox.coordinates[0][2]; - clause.push( - sql`ST_Contains( + const { + name, + phenomenon, + fromDate, + toDate, + bbox, + near, + maxDistance, + grouptag, + } = opts + const clause = [] + const columns = {} + + if (name) { + clause.push(ilike(device.name, `%${name}%`)) + } + + if (phenomenon) { + // @ts-ignore + columns['sensors'] = { + // @ts-ignore + where: (sensor, { ilike }) => + // @ts-ignore + ilike(sensorTable['title'], `%${phenomenon}%`), + } + } + + // simple string parameters + // for (const param of ['exposure', 'model'] as const) { + // if (opts[param]) { + // clause.push(inArray(device[param], opts[param]!)); + // } + // } + + if (grouptag) { + clause.push(arrayContains(device.tags, grouptag)) + } + + // https://orm.drizzle.team/learn/guides/postgis-geometry-point + if (bbox && bbox.coordinates[0]) { + const [latSW, lngSW] = bbox.coordinates[0][0] + const [latNE, lngNE] = bbox.coordinates[0][2] + clause.push( + sql`ST_Contains( ST_MakeEnvelope(${lngSW}, ${latSW}, ${lngNE}, ${latNE}, 4326), ST_SetSRID(ST_MakePoint(${device.longitude}, ${device.latitude}), 4326) - )` - ); - } + )`, + ) + } - if (near && maxDistance !== undefined) { - clause.push( - sql`ST_DWithin( + if (near && maxDistance !== undefined) { + clause.push( + sql`ST_DWithin( ST_SetSRID(ST_MakePoint(${device.longitude}, ${device.latitude}), 4326), ST_SetSRID(ST_MakePoint(${near[1]}, ${near[0]}), 4326), ${maxDistance} - )` - ); - } - - if (phenomenon && (fromDate || toDate)) { - // @ts-ignore - columns["sensors"] = { - include: { - measurements: { - where: (measurement: any) => { - const conditions = []; - - if (fromDate && toDate) { - conditions.push( - sql`${measurement.createdAt} BETWEEN ${fromDate} AND ${toDate}` - ); - } else if (fromDate) { - conditions.push(sql`${measurement.createdAt} >= ${fromDate}`); - } else if (toDate) { - conditions.push(sql`${measurement.createdAt} <= ${toDate}`); - } - - return and(...conditions); - }, - }, - }, - }; - } - - return { - includeColumns: columns, - whereClause: clause, - }; -}; + )`, + ) + } + + if (phenomenon && (fromDate || toDate)) { + // @ts-ignore + columns['sensors'] = { + include: { + measurements: { + where: (measurement: any) => { + const conditions = [] + + if (fromDate && toDate) { + conditions.push( + sql`${measurement.createdAt} BETWEEN ${fromDate} AND ${toDate}`, + ) + } else if (fromDate) { + conditions.push(sql`${measurement.createdAt} >= ${fromDate}`) + } else if (toDate) { + conditions.push(sql`${measurement.createdAt} <= ${toDate}`) + } + + return and(...conditions) + }, + }, + }, + } + } + + return { + includeColumns: columns, + whereClause: clause, + } +} const MINIMAL_COLUMNS = { - id: true, - name: true, - exposure: true, - longitude: true, - latitude: true, -}; + id: true, + name: true, + exposure: true, + longitude: true, + latitude: true, +} const DEFAULT_COLUMNS = { - id: true, - name: true, - model: true, - exposure: true, - grouptag: true, - image: true, - description: true, - link: true, - createdAt: true, - updatedAt: true, - longitude: true, - latitude: true, -}; + id: true, + name: true, + model: true, + exposure: true, + grouptag: true, + image: true, + description: true, + link: true, + createdAt: true, + updatedAt: true, + longitude: true, + latitude: true, +} export async function findDevices( - opts: FindDevicesOptions = {}, - columns: Record = {}, - relations: Record = {} + opts: FindDevicesOptions = {}, + columns: Record = {}, + relations: Record = {}, ) { - const { minimal, limit } = opts; - const { includeColumns, whereClause } = buildWhereClause(opts); - columns = minimal ? MINIMAL_COLUMNS : { ...DEFAULT_COLUMNS, ...columns }; - relations = { - ...relations, - ...includeColumns, - }; - const devices = await drizzleClient.query.device.findMany({ - ...(Object.keys(columns).length !== 0 && { columns }), - ...(Object.keys(relations).length !== 0 && { with: relations }), - ...(Object.keys(whereClause).length !== 0 && { - where: (_, { and }) => and(...whereClause), - }), - limit, - }); - - return devices; + const { minimal, limit } = opts + const { includeColumns, whereClause } = buildWhereClause(opts) + columns = minimal ? MINIMAL_COLUMNS : { ...DEFAULT_COLUMNS, ...columns } + relations = { + ...relations, + ...includeColumns, + } + const devices = await drizzleClient.query.device.findMany({ + ...(Object.keys(columns).length !== 0 && { columns }), + ...(Object.keys(relations).length !== 0 && { with: relations }), + ...(Object.keys(whereClause).length !== 0 && { + where: (_, { and }) => and(...whereClause), + }), + limit, + }) + + return devices } export async function createDevice(deviceData: any, userId: string) { - try { - const [newDevice, usr] = await drizzleClient.transaction(async (tx) => { - // Get the user info - const [u] = await tx - .select() - .from(user) - .where(eq(user.id, userId)) - .limit(1); - - // Create the device - const [createdDevice] = await tx - .insert(device) - .values({ - id: deviceData.id, - useAuth: deviceData.useAuth ?? true, - model: deviceData.model, - tags: deviceData.tags, - userId: userId, - name: deviceData.name, - description: deviceData.description, - image: deviceData.image, - link: deviceData.link, - exposure: deviceData.exposure, - public: deviceData.public ?? false, - expiresAt: deviceData.expiresAt - ? new Date(deviceData.expiresAt) - : null, - latitude: deviceData.latitude, - longitude: deviceData.longitude, - }) - .returning(); - - if (!createdDevice) { - throw new Error("Failed to create device."); - } - - // Add sensors in the same transaction and collect them - const createdSensors = []; - if (deviceData.sensors && Array.isArray(deviceData.sensors)) { - for (const sensorData of deviceData.sensors) { - const [newSensor] = await tx - .insert(sensor) - .values({ - title: sensorData.title, - unit: sensorData.unit, - sensorType: sensorData.sensorType, - deviceId: createdDevice.id, // Reference the created device ID - }) - .returning(); - - if (newSensor) { - createdSensors.push(newSensor); - } - } - } - - // Return device with sensors - return [ - { - ...createdDevice, - sensors: createdSensors, - }, - u, - ]; - }); - - const lng = (usr.language?.split("_")[0] as "de" | "en") ?? "en"; - switch (newDevice.model) { - case "luftdaten.info": - await sendMail({ - recipientAddress: usr.email, - recipientName: usr.name, - subject: NewLufdatenDeviceMessages[lng].heading, - body: BaseNewDeviceEmail({ - user: { name: usr.name }, - device: newDevice, - language: lng, - content: NewLufdatenDeviceMessages, - }), - }); - break; - case "homeV2Ethernet": - case "homeV2Lora": - case "homeV2Wifi": - case "senseBox:Edu": - await sendMail({ - recipientAddress: usr.email, - recipientName: usr.name, - subject: NewSenseboxDeviceMessages[lng].heading, - body: BaseNewDeviceEmail({ - user: { name: usr.name }, - device: newDevice, - language: lng, - content: NewSenseboxDeviceMessages, - }), - }); - break; - default: - await sendMail({ - recipientAddress: usr.email, - recipientName: usr.name, - subject: BaseNewDeviceMessages[lng].heading, - body: BaseNewDeviceEmail({ - user: { name: usr.name }, - device: newDevice, - language: lng, - content: BaseNewDeviceMessages, - }), - }); - break; - } - - return newDevice; - } catch (error) { - console.error("Error creating device with sensors:", error); - throw new Error("Failed to create device and its sensors."); - } + try { + const [newDevice, usr] = await drizzleClient.transaction(async (tx) => { + // Get the user info + const [u] = await tx + .select() + .from(user) + .where(eq(user.id, userId)) + .limit(1) + + // Determine sensors to use + let sensorsToAdd = deviceData.sensors + + // If model and sensors are both specified, reject (backwards compatibility) + if (deviceData.model && deviceData.sensors) { + throw new Error( + 'Parameters model and sensors cannot be specified at the same time.', + ) + } + + // If model is specified but sensors are not, get sensors from model layout + if (deviceData.model && !deviceData.sensors) { + const modelSensors = getSensorsForModel(deviceData.model as any) + if (modelSensors) { + sensorsToAdd = modelSensors + } + } + + // Create the device + const [createdDevice] = await tx + .insert(device) + .values({ + id: deviceData.id, + useAuth: deviceData.useAuth ?? true, + model: deviceData.model, + tags: deviceData.tags, + userId: userId, + name: deviceData.name, + description: deviceData.description, + image: deviceData.image, + link: deviceData.link, + exposure: deviceData.exposure, + public: deviceData.public ?? false, + expiresAt: deviceData.expiresAt + ? new Date(deviceData.expiresAt) + : null, + latitude: deviceData.latitude, + longitude: deviceData.longitude, + }) + .returning() + + if (!createdDevice) { + throw new Error('Failed to create device.') + } + + // Add sensors in the same transaction and collect them + const createdSensors = [] + if ( + sensorsToAdd && + Array.isArray(sensorsToAdd) && + sensorsToAdd.length > 0 + ) { + for (const sensorData of sensorsToAdd) { + const [newSensor] = await tx + .insert(sensor) + .values({ + title: sensorData.title, + unit: sensorData.unit, + sensorType: sensorData.sensorType, + icon: sensorData.icon, + deviceId: createdDevice.id, + }) + .returning() + + if (newSensor) { + createdSensors.push(newSensor) + } + } + } + + // Return device with sensors + return [ + { + ...createdDevice, + sensors: createdSensors, + }, + u, + ] + }) + + const lng = (usr.language?.split('_')[0] as 'de' | 'en') ?? 'en' + switch (newDevice.model) { + case 'luftdaten.info': + case 'luftdaten_sds011': + case 'luftdaten_sds011_bme280': + case 'luftdaten_sds011_bmp180': + case 'luftdaten_sds011_dht11': + case 'luftdaten_sds011_dht22': + await sendMail({ + recipientAddress: usr.email, + recipientName: usr.name, + subject: NewLufdatenDeviceMessages[lng].heading, + body: BaseNewDeviceEmail({ + user: { name: usr.name }, + device: newDevice, + language: lng, + content: NewLufdatenDeviceMessages, + }), + }) + break + case 'homeV2Ethernet': + case 'homeV2Lora': + case 'homeV2Wifi': + case 'homeEthernet': + case 'homeEthernetFeinstaub': + case 'homeWifi': + case 'homeWifiFeinstaub': + case 'senseBox:Edu': + await sendMail({ + recipientAddress: usr.email, + recipientName: usr.name, + subject: NewSenseboxDeviceMessages[lng].heading, + body: BaseNewDeviceEmail({ + user: { name: usr.name }, + device: newDevice, + language: lng, + content: NewSenseboxDeviceMessages, + }), + }) + break + default: + await sendMail({ + recipientAddress: usr.email, + recipientName: usr.name, + subject: BaseNewDeviceMessages[lng].heading, + body: BaseNewDeviceEmail({ + user: { name: usr.name }, + device: newDevice, + language: lng, + content: BaseNewDeviceMessages, + }), + }) + break + } + + return newDevice + } catch (error) { + console.error('Error creating device with sensors:', error) + throw new Error( + `Failed to create device and its sensors: ${error instanceof Error ? error.message : String(error)}`, + ) + } } // get the 10 latest created (createdAt property) devices with id, name, latitude, and longitude export async function getLatestDevices() { - const devices = await drizzleClient - .select({ - id: device.id, - name: device.name, - latitude: device.latitude, - longitude: device.longitude, - }) - .from(device) - .orderBy(desc(device.createdAt)) - .limit(10); - - return devices; + const devices = await drizzleClient + .select({ + id: device.id, + name: device.name, + latitude: device.latitude, + longitude: device.longitude, + }) + .from(device) + .orderBy(desc(device.createdAt)) + .limit(10) + + return devices } export async function findAccessToken( - deviceId: string + deviceId: string, ): Promise<{ token: string } | null> { - const result = await drizzleClient.query.accessToken.findFirst({ - where: (token, { eq }) => eq(token.deviceId, deviceId), - }); + const result = await drizzleClient.query.accessToken.findFirst({ + where: (token, { eq }) => eq(token.deviceId, deviceId), + }) - if (!result || !result.token) return null; + if (!result || !result.token) return null - return { token: result.token }; + return { token: result.token } } diff --git a/app/routes/api.boxes.ts b/app/routes/api.boxes.ts index e867dbbc..ef39f30d 100644 --- a/app/routes/api.boxes.ts +++ b/app/routes/api.boxes.ts @@ -1,10 +1,10 @@ -import { type ActionFunction, type ActionFunctionArgs } from "react-router"; -import { transformDeviceToApiFormat } from "~/lib/device-transform"; -import { CreateBoxSchema } from "~/lib/devices-service.server"; -import { getUserFromJwt } from "~/lib/jwt"; -import { createDevice } from "~/models/device.server"; -import { type User } from "~/schema"; -import { StandardResponse } from "~/utils/response-utils"; +import { type ActionFunction, type ActionFunctionArgs } from 'react-router' +import { transformDeviceToApiFormat } from '~/lib/device-transform' +import { CreateBoxSchema } from '~/lib/devices-service.server' +import { getUserFromJwt } from '~/lib/jwt' +import { createDevice } from '~/models/device.server' +import { type User } from '~/schema' +import { StandardResponse } from '~/utils/response-utils' /** * @openapi @@ -325,71 +325,83 @@ import { StandardResponse } from "~/utils/response-utils"; */ export const action: ActionFunction = async ({ - request, + request, }: ActionFunctionArgs) => { - try { - // Check authentication - const jwtResponse = await getUserFromJwt(request); + try { + // Check authentication + const jwtResponse = await getUserFromJwt(request) - if (typeof jwtResponse === "string") - return StandardResponse.forbidden("Invalid JWT authorization. Please sign in to obtain new JWT."); + if (typeof jwtResponse === 'string') + return StandardResponse.forbidden( + 'Invalid JWT authorization. Please sign in to obtain new JWT.', + ) - switch (request.method) { - case "POST": - return await post(request, jwtResponse); - default: - return StandardResponse.methodNotAllowed("Method Not Allowed"); - } - } catch (err) { - console.error("Error in action:", err); - return StandardResponse.internalServerError(); - } -}; + switch (request.method) { + case 'POST': + return await post(request, jwtResponse) + default: + return StandardResponse.methodNotAllowed('Method Not Allowed') + } + } catch (err) { + console.error('Error in action:', err) + return StandardResponse.internalServerError() + } +} async function post(request: Request, user: User) { - try { - // Parse and validate request body - let requestData; - try { - requestData = await request.json(); - } catch { - return StandardResponse.badRequest("Invalid JSON in request body"); - } - - // Validate request data - const validationResult = CreateBoxSchema.safeParse(requestData); - if (!validationResult.success) { - return Response.json({ - code: "Bad Request", - message: "Invalid request data", - errors: validationResult.error.errors.map(err => `${err.path.join('.')}: ${err.message}`), - }, { status: 400 }); - } + try { + // Parse and validate request body + let requestData + try { + requestData = await request.json() + } catch { + return StandardResponse.badRequest('Invalid JSON in request body') + } - const validatedData = validationResult.data; + // Validate request data + const validationResult = CreateBoxSchema.safeParse(requestData) + if (!validationResult.success) { + return Response.json( + { + code: 'Bad Request', + message: 'Invalid request data', + errors: validationResult.error.errors.map( + (err) => `${err.path.join('.')}: ${err.message}`, + ), + }, + { status: 400 }, + ) + } - // Extract longitude and latitude from location array [longitude, latitude] - const [longitude, latitude] = validatedData.location; - const newBox = await createDevice({ - name: validatedData.name, - exposure: validatedData.exposure, - model: validatedData.model, - latitude: latitude, - longitude: longitude, - tags: validatedData.grouptag, - sensors: validatedData.sensors.map(sensor => ({ - title: sensor.title, - sensorType: sensor.sensorType, - unit: sensor.unit, - })), - }, user.id); + const validatedData = validationResult.data + const sensorsProvided = validatedData.sensors?.length > 0 + // Extract longitude and latitude from location array [longitude, latitude] + const [longitude, latitude] = validatedData.location + const newBox = await createDevice( + { + name: validatedData.name, + exposure: validatedData.exposure, + model: sensorsProvided ? undefined : validatedData.model, + latitude: latitude, + longitude: longitude, + tags: validatedData.grouptag, + sensors: sensorsProvided + ? validatedData.sensors.map((s) => ({ + title: s.title, + sensorType: s.sensorType, + unit: s.unit, + })) + : undefined, + }, + user.id, + ) - // Build response object using helper function - const responseData = transformDeviceToApiFormat(newBox); + // Build response object using helper function + const responseData = transformDeviceToApiFormat(newBox) - return StandardResponse.created(responseData); - } catch (err) { - console.error("Error creating box:", err); - return StandardResponse.internalServerError(); - } + return StandardResponse.created(responseData) + } catch (err) { + console.error('Error creating box:', err) + return StandardResponse.internalServerError() + } } diff --git a/app/routes/api.device.$deviceId.ts b/app/routes/api.device.$deviceId.ts index 5d31aa3c..663c68f6 100644 --- a/app/routes/api.device.$deviceId.ts +++ b/app/routes/api.device.$deviceId.ts @@ -1,6 +1,13 @@ -import { type LoaderFunctionArgs } from "react-router"; -import { getDevice } from "~/models/device.server"; -import { StandardResponse } from "~/utils/response-utils"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from 'react-router' +import { transformDeviceToApiFormat } from '~/lib/device-transform' +import { getUserFromJwt } from '~/lib/jwt' +import { + DeviceUpdateError, + getDevice, + updateDevice, + type UpdateDeviceArgs, +} from '~/models/device.server' +import { StandardResponse } from '~/utils/response-utils' /** * @openapi @@ -63,21 +70,185 @@ import { StandardResponse } from "~/utils/response-utils"; * description: Internal server error */ export async function loader({ params }: LoaderFunctionArgs) { - const { deviceId } = params; + const { deviceId } = params - if (!deviceId) - return StandardResponse.badRequest("Device ID is required."); + if (!deviceId) return StandardResponse.badRequest('Device ID is required.') - try { - const device = await getDevice({ id: deviceId }); + try { + const device = await getDevice({ id: deviceId }) - if (!device) - return StandardResponse.notFound("Device not found."); + if (!device) return StandardResponse.notFound('Device not found.') - return StandardResponse.ok(device); - } catch (error) { - console.error("Error fetching box:", error); + return StandardResponse.ok(device) + } catch (error) { + console.error('Error fetching box:', error) - return StandardResponse.internalServerError(); - } + if (error instanceof Response) { + throw error + } + + return new Response( + JSON.stringify({ error: 'Internal server error while fetching box' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }, + ) + } +} + +export async function action({ request, params }: ActionFunctionArgs) { + const { deviceId } = params + + if (!deviceId) { + return Response.json({ error: 'Device ID is required.' }, { status: 400 }) + } + + const jwtResponse = await getUserFromJwt(request) + + if (typeof jwtResponse === 'string') { + return Response.json( + { + code: 'Forbidden', + message: + 'Invalid JWT authorization. Please sign in to obtain a new JWT.', + }, + { status: 403 }, + ) + } + + switch (request.method) { + case 'PUT': + return await put(request, jwtResponse, deviceId) + default: + return Response.json({ message: 'Method Not Allowed' }, { status: 405 }) + } +} + +async function put(request: Request, user: any, deviceId: string) { + const body = await request.json() + + const currentDevice = await getDevice({ id: deviceId }) + if (!currentDevice) { + return Response.json( + { code: 'NotFound', message: 'Device not found' }, + { status: 404 }, + ) + } + + // Check for conflicting parameters (backwards compatibility) + if (body.sensors && body.addons?.add) { + return Response.json( + { + code: 'BadRequest', + message: 'sensors and addons can not appear in the same request.', + }, + { status: 400 }, + ) + } + + if (body.addons?.add === 'feinstaub') { + const homeModels = ['homeWifi', 'homeEthernet'] + if (currentDevice.model && homeModels.includes(currentDevice.model)) { + body.model = `${currentDevice.model}Feinstaub` + + const hasPM10 = currentDevice.sensors.some( + (s) => s.sensorType === 'SDS 011' && s.title === 'PM10', + ) + const hasPM25 = currentDevice.sensors.some( + (s) => s.sensorType === 'SDS 011' && s.title === 'PM2.5', + ) + + if (!hasPM10 || !hasPM25) { + body.sensors = [ + ...(body.sensors ?? []), + !hasPM10 && { + new: true, + title: 'PM10', + unit: 'µg/m³', + sensorType: 'SDS 011', + // icon: 'osem-cloud', + }, + !hasPM25 && { + new: true, + title: 'PM2.5', + unit: 'µg/m³', + sensorType: 'SDS 011', + // icon: 'osem-cloud', + }, + ].filter(Boolean) + } + } + } + + // Handle addons (merge with grouptag) + if (body.addons?.add) { + const currentTags = Array.isArray(body.grouptag) ? body.grouptag : [] + body.grouptag = Array.from(new Set([...currentTags, body.addons.add])) + } + + // Handle image deletion + if (body.deleteImage === true) { + body.image = '' + } + + // Prepare location if provided + let locationData: { lat: number; lng: number; height?: number } | undefined + if (body.location) { + locationData = { + lat: body.location.lat, + lng: body.location.lng, + } + if (body.location.height !== undefined) { + locationData.height = body.location.height + } + } + + const updateArgs: UpdateDeviceArgs = { + name: body.name, + exposure: body.exposure, + description: body.description, + image: body.image, + model: body.model, + useAuth: body.useAuth, + link: body.weblink, + location: locationData, + grouptag: body.grouptag, + sensors: body.sensors, + } + + try { + const updatedDevice = await updateDevice(deviceId, updateArgs) + + const deviceWithSensors = await getDevice({ id: updatedDevice.id }) + + const apiResponse = transformDeviceToApiFormat(deviceWithSensors as any) + + return Response.json(apiResponse, { status: 200 }) + } catch (error) { + console.error('Error updating device:', error) + + // Handle specific device update errors + if (error instanceof DeviceUpdateError) { + return Response.json( + { + code: error.statusCode === 400 ? 'BadRequest' : 'NotFound', + message: error.message, + }, + { status: error.statusCode }, + ) + } + + // Return generic error for unexpected errors + return Response.json( + { + code: 'InternalServerError', + message: + error instanceof Error ? error.message : 'Failed to update device', + }, + { status: 500 }, + ) + } } diff --git a/app/routes/api.ts b/app/routes/api.ts index 394f28d0..a25dbd3b 100644 --- a/app/routes/api.ts +++ b/app/routes/api.ts @@ -137,10 +137,10 @@ const routes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { path: `boxes/transfer/:boxId`, method: 'PUT', }, - // { - // path: `boxes/:boxId`, - // method: "PUT", - // }, + { + path: `boxes/:boxId`, + method: 'PUT', + }, { path: `boxes/:boxId`, method: 'DELETE', diff --git a/app/routes/device.$deviceId.edit.general.tsx b/app/routes/device.$deviceId.edit.general.tsx index af37f226..743797e1 100644 --- a/app/routes/device.$deviceId.edit.general.tsx +++ b/app/routes/device.$deviceId.edit.general.tsx @@ -1,300 +1,302 @@ -import { Save } from "lucide-react"; -import React, { useState } from "react"; -import { type ActionFunctionArgs, type LoaderFunctionArgs, - data, - redirect, - Form, - useActionData, - useLoaderData, - useOutletContext } from "react-router"; -import invariant from "tiny-invariant"; -import ErrorMessage from "~/components/error-message"; +import { Save } from 'lucide-react' +import React, { useState } from 'react' import { - deleteDevice, - getDeviceWithoutSensors, - updateDeviceInfo, -} from "~/models/device.server"; -import { verifyLogin } from "~/models/user.server"; -import { getUserEmail, getUserId } from "~/utils/session.server"; + type ActionFunctionArgs, + type LoaderFunctionArgs, + data, + redirect, + Form, + useActionData, + useLoaderData, + useOutletContext, +} from 'react-router' +import invariant from 'tiny-invariant' +import ErrorMessage from '~/components/error-message' +import { + updateDevice, + deleteDevice, + getDeviceWithoutSensors, +} from '~/models/device.server' +import { verifyLogin } from '~/models/user.server' +import { getUserEmail, getUserId } from '~/utils/session.server' //***************************************************** export async function loader({ request, params }: LoaderFunctionArgs) { - //* if user is not logged in, redirect to home - const userId = await getUserId(request); - if (!userId) return redirect("/"); + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') - const deviceID = params.deviceId; + const deviceID = params.deviceId - if (typeof deviceID !== "string") { - return redirect("/profile/me"); - } + if (typeof deviceID !== 'string') { + return redirect('/profile/me') + } - const deviceData = await getDeviceWithoutSensors({ id: deviceID }); + const deviceData = await getDeviceWithoutSensors({ id: deviceID }) - return { device: deviceData }; + return { device: deviceData } } //***************************************************** export async function action({ request, params }: ActionFunctionArgs) { - const formData = await request.formData(); - const { intent, name, exposure, passwordDelete } = - Object.fromEntries(formData); + const formData = await request.formData() + const { intent, name, exposure, passwordDelete } = + Object.fromEntries(formData) + + const exposureLowerCase = exposure?.toString().toLowerCase() - const errors = { - exposure: exposure ? null : "Invalid exposure.", - passwordDelete: passwordDelete ? null : "Password is required.", - }; + const errors = { + exposure: exposure ? null : 'Invalid exposure.', + passwordDelete: passwordDelete ? null : 'Password is required.', + } - const deviceID = params.deviceId; - invariant(typeof deviceID === "string", " Device id not found."); - invariant(typeof name === "string", "Device name is required."); - invariant(typeof exposure === "string", "Device name is required."); + const deviceID = params.deviceId + invariant(typeof deviceID === 'string', ' Device id not found.') + invariant(typeof name === 'string', 'Device name is required.') + invariant(typeof exposure === 'string', 'Device name is required.') - if ( - exposure !== "indoor" && - exposure !== "outdoor" && - exposure !== "mobile" && - exposure !== "unknown" - ) { - return data({ - errors: { - exposure: exposure ? null : "Invalid exposure.", - passwordDelete: errors.passwordDelete, - }, - status: 400, - }); - } + if ( + exposureLowerCase !== 'indoor' && + exposureLowerCase !== 'outdoor' && + exposureLowerCase !== 'mobile' && + exposureLowerCase !== 'unknown' + ) { + return data({ + errors: { + exposure: exposure ? null : 'Invalid exposure.', + passwordDelete: errors.passwordDelete, + }, + status: 400, + }) + } - switch (intent) { - case "save": { - await updateDeviceInfo({ id: deviceID, name: name, exposure: exposure }); - return data({ - errors: { - exposure: null, - passwordDelete: null, - }, - status: 200, - }); - } - case "delete": { - //* check password validaty - if (errors.passwordDelete) { - return data({ - errors, - status: 400, - }); - } - //* 1. get user email - const userEmail = await getUserEmail(request); - invariant(typeof userEmail === "string", "email not found"); - invariant( - typeof passwordDelete === "string", - "password must be a string", - ); - //* 2. check entered password - const user = await verifyLogin(userEmail, passwordDelete); - //* 3. retrun error if password is not correct - if (!user) { - return data( - { - errors: { - exposure: exposure ? null : "Invalid exposure.", - passwordDelete: "Invalid password", - }, - }, - { status: 400 }, - ); - } - //* 4. delete device - await deleteDevice({ id: deviceID }); + switch (intent) { + case 'save': { + await updateDevice(deviceID, { name, exposure: exposureLowerCase }) + return data({ + errors: { + exposure: null, + passwordDelete: null, + }, + status: 200, + }) + } + case 'delete': { + //* check password validaty + if (errors.passwordDelete) { + return data({ + errors, + status: 400, + }) + } + //* 1. get user email + const userEmail = await getUserEmail(request) + invariant(typeof userEmail === 'string', 'email not found') + invariant(typeof passwordDelete === 'string', 'password must be a string') + //* 2. check entered password + const user = await verifyLogin(userEmail, passwordDelete) + //* 3. retrun error if password is not correct + if (!user) { + return data( + { + errors: { + exposure: exposure ? null : 'Invalid exposure.', + passwordDelete: 'Invalid password', + }, + }, + { status: 400 }, + ) + } + //* 4. delete device + await deleteDevice({ id: deviceID }) - return redirect("/profile/me"); - } - } + return redirect('/profile/me') + } + } - return redirect(""); + return redirect('') } //********************************** export default function () { - const { device } = useLoaderData(); - const actionData = useActionData(); - const [passwordDelVal, setPasswordVal] = useState(""); //* to enable delete account button - //* focus when an error occured - const nameRef = React.useRef(null); - const passwordDelRef = React.useRef(null); - const [name, setName] = useState(device?.name); - const [exposure, setExposure] = useState(device?.exposure); - //* to view toast on edit page - const [setToastOpen] = useOutletContext<[(_open: boolean) => void]>(); + const { device } = useLoaderData() + const actionData = useActionData() + const [passwordDelVal, setPasswordVal] = useState('') //* to enable delete account button + //* focus when an error occured + const nameRef = React.useRef(null) + const passwordDelRef = React.useRef(null) + const [name, setName] = useState(device?.name) + const [exposure, setExposure] = useState(device?.exposure) + //* to view toast on edit page + const [setToastOpen] = useOutletContext<[(_open: boolean) => void]>() - React.useEffect(() => { - if (actionData) { - const hasErrors = Object.values(actionData?.errors).some( - (errorMessage) => errorMessage, - ); + React.useEffect(() => { + if (actionData) { + const hasErrors = Object.values(actionData?.errors).some( + (errorMessage) => errorMessage, + ) - //* when device data updated successfully - if (!hasErrors) { - setToastOpen(true); - // setToastOpenTest(true); - } - //* when password is null - else if (hasErrors && actionData?.errors?.passwordDelete) { - passwordDelRef.current?.focus(); - } - } - }, [actionData, setToastOpen]); + //* when device data updated successfully + if (!hasErrors) { + setToastOpen(true) + // setToastOpenTest(true); + } + //* when password is null + else if (hasErrors && actionData?.errors?.passwordDelete) { + passwordDelRef.current?.focus() + } + } + }, [actionData, setToastOpen]) - return ( -
- {/* general form */} -
-
- {/* Form */} -
- {/* Heading */} -
- {/* Title */} -
-
-

General

-
-
- -
-
-
+ return ( +
+ {/* general form */} +
+
+ {/* Form */} + + {/* Heading */} +
+ {/* Title */} +
+
+

General

+
+
+ +
+
+
- {/* divider */} -
+ {/* divider */} +
-
- {/* */} - {/* Name */} -
- +
+ {/* */} + {/* Name */} +
+ -
- setName(e.target.value)} - ref={nameRef} - aria-describedby="name-error" - className="w-full rounded border border-gray-200 px-2 py-1 text-base" - /> -
-
+
+ setName(e.target.value)} + ref={nameRef} + aria-describedby="name-error" + className="w-full rounded border border-gray-200 px-2 py-1 text-base" + /> +
+
- {/* Exposure */} -
- + {/* Exposure */} +
+ -
- -
-
+
+ +
+
- {/* Delete device */} -
-

- Delete senseBox -

-
+ {/* Delete device */} +
+

+ Delete senseBox +

+
-
-

- If you really want to delete your station, please type your - current password - all measurements will be deleted as well. -

-
-
- setPasswordVal(e.target.value)} - /> - {actionData?.errors?.passwordDelete && ( -
- {actionData.errors.passwordDelete} -
- )} -
- {/* Delete button */} -
- -
- {/* */} -
- -
-
-
- ); +
+

+ If you really want to delete your station, please type your + current password - all measurements will be deleted as well. +

+
+
+ setPasswordVal(e.target.value)} + /> + {actionData?.errors?.passwordDelete && ( +
+ {actionData.errors.passwordDelete} +
+ )} +
+ {/* Delete button */} +
+ +
+ {/* */} +
+ +
+
+
+ ) } export function ErrorBoundary() { - return ( -
- -
- ); + return ( +
+ +
+ ) } diff --git a/app/schema/enum.ts b/app/schema/enum.ts index da4eb094..ba7cd39e 100644 --- a/app/schema/enum.ts +++ b/app/schema/enum.ts @@ -1,35 +1,45 @@ -import { pgEnum } from "drizzle-orm/pg-core"; -import { z } from "zod"; +import { pgEnum } from 'drizzle-orm/pg-core' +import { z } from 'zod' // Enum for device exposure types -export const DeviceExposureEnum = pgEnum("exposure", [ - "indoor", - "outdoor", - "mobile", - "unknown", -]); +export const DeviceExposureEnum = pgEnum('exposure', [ + 'indoor', + 'outdoor', + 'mobile', + 'unknown', +]) // Zod schema for validating device exposure types -export const DeviceExposureZodEnum = z.enum(DeviceExposureEnum.enumValues); +export const DeviceExposureZodEnum = z.enum(DeviceExposureEnum.enumValues) // Type inferred from the Zod schema for device exposure types -export type DeviceExposureType = z.infer; +export type DeviceExposureType = z.infer // Enum for device status types -export const DeviceStatusEnum = pgEnum("status", ["active", "inactive", "old"]); +export const DeviceStatusEnum = pgEnum('status', ['active', 'inactive', 'old']) // Zod schema for validating device status types -export const DeviceStatusZodEnum = z.enum(DeviceStatusEnum.enumValues); +export const DeviceStatusZodEnum = z.enum(DeviceStatusEnum.enumValues) // Type inferred from the Zod schema for device status types -export type DeviceStatusType = z.infer; +export type DeviceStatusType = z.infer // Enum for device model types -export const DeviceModelEnum = pgEnum("model", [ - "homeV2Lora", - "homeV2Ethernet", - "homeV2Wifi", - "senseBox:Edu", - "luftdaten.info", - "Custom", -]); +export const DeviceModelEnum = pgEnum('model', [ + 'homeV2Lora', + 'homeV2Ethernet', + 'homeV2Wifi', + 'homeEthernet', + 'homeWifi', + 'homeEthernetFeinstaub', + 'homeWifiFeinstaub', + 'luftdaten_sds011', + 'luftdaten_sds011_dht11', + 'luftdaten_sds011_dht22', + 'luftdaten_sds011_bmp180', + 'luftdaten_sds011_bme280', + 'hackair_home_v2', + 'senseBox:Edu', + 'luftdaten.info', + 'Custom', +]) diff --git a/app/schema/sensor.ts b/app/schema/sensor.ts index 6c823aa1..949ccfe9 100644 --- a/app/schema/sensor.ts +++ b/app/schema/sensor.ts @@ -10,7 +10,7 @@ import { device } from "./device"; import { DeviceStatusEnum } from "./enum"; import { type Measurement } from "./measurement"; -function generateHexId(): string { +export function generateHexId(): string { return randomBytes(12).toString('hex'); } @@ -34,6 +34,7 @@ export const sensor = pgTable("sensor", { title: text("title"), unit: text("unit"), sensorType: text("sensor_type"), + icon: text("icon"), status: DeviceStatusEnum("status").default("inactive"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), diff --git a/app/utils/addon-definitions.ts b/app/utils/addon-definitions.ts new file mode 100644 index 00000000..04ca03d9 --- /dev/null +++ b/app/utils/addon-definitions.ts @@ -0,0 +1,19 @@ +export const addonDefinitions: Record< + string, + { title: string; unit: string; sensorType: string; icon?: string }[] +> = { + feinstaub: [ + { + title: 'PM10', + unit: 'µg/m³', + sensorType: 'SDS 011', + icon: 'particulate_matter', + }, + { + title: 'PM2.5', + unit: 'µg/m³', + sensorType: 'SDS 011', + icon: 'particulate_matter', + }, + ], +} diff --git a/app/utils/model-definitions.ts b/app/utils/model-definitions.ts index 33a1bd8d..77218258 100644 --- a/app/utils/model-definitions.ts +++ b/app/utils/model-definitions.ts @@ -1,91 +1,122 @@ -import { sensorDefinitions } from "./sensor-definitions"; +import { sensorDefinitions } from './sensor-definitions' // Models Definition export const modelDefinitions = { - senseBoxHomeV2: [ - sensorDefinitions.hdc1080_temperature, - sensorDefinitions.hdc1080_humidity, - sensorDefinitions.bmp280_pressure, - sensorDefinitions.tsl45315_lightintensity, - sensorDefinitions.veml6070_uvintensity, - sensorDefinitions.sds011_pm10, - sensorDefinitions.sds011_pm25, - sensorDefinitions.bme680_humidity, - sensorDefinitions.bme680_temperature, - sensorDefinitions.bme680_pressure, - sensorDefinitions.bme680_voc, - sensorDefinitions.smt50_soilmoisture, - sensorDefinitions.smt50_soiltemperature, - sensorDefinitions.soundlevelmeter, - sensorDefinitions.windspeed, - sensorDefinitions.scd30_co2, - sensorDefinitions.dps310_pressure, - sensorDefinitions.sps30_pm1, - sensorDefinitions.sps30_pm4, - sensorDefinitions.sps30_pm10, - sensorDefinitions.sps30_pm25, - ], - "senseBox:Edu": [ - sensorDefinitions.hdc1080_temperature, - sensorDefinitions.hdc1080_humidity, - sensorDefinitions.bmp280_pressure, - sensorDefinitions.tsl45315_lightintensity, - sensorDefinitions.veml6070_uvintensity, - sensorDefinitions.sds011_pm10, - sensorDefinitions.sds011_pm25, - sensorDefinitions.bme680_humidity, - sensorDefinitions.bme680_temperature, - sensorDefinitions.bme680_pressure, - sensorDefinitions.bme680_voc, - sensorDefinitions.smt50_soilmoisture, - sensorDefinitions.smt50_soiltemperature, - sensorDefinitions.soundlevelmeter, - sensorDefinitions.windspeed, - sensorDefinitions.scd30_co2, - sensorDefinitions.dps310_pressure, - sensorDefinitions.sps30_pm1, - sensorDefinitions.sps30_pm4, - sensorDefinitions.sps30_pm10, - sensorDefinitions.sps30_pm25, - ], - "luftdaten.info": [ - sensorDefinitions.pms1003_pm01, - sensorDefinitions.pms1003_pm10, - sensorDefinitions.pms1003_pm25, - sensorDefinitions.pms3003_pm01, - sensorDefinitions.pms3003_pm10, - sensorDefinitions.pms3003_pm25, - sensorDefinitions.pms5003_pm01, - sensorDefinitions.pms5003_pm10, - sensorDefinitions.pms5003_pm25, - sensorDefinitions.pms7003_pm01, - sensorDefinitions.pms7003_pm10, - sensorDefinitions.pms7003_pm25, - sensorDefinitions.sds011_pm10, - sensorDefinitions.sds011_pm25, - sensorDefinitions.sps30_pm1, - sensorDefinitions.sps30_pm4, - sensorDefinitions.sps30_pm10, - sensorDefinitions.sps30_pm25, - sensorDefinitions.sht3x_humidity, - sensorDefinitions.sht3x_temperature, - sensorDefinitions.bmp180_temperature, - sensorDefinitions.bmp180_pressure_pa, - sensorDefinitions.bmp180_pressure_hpa, - sensorDefinitions.bme280_humidity, - sensorDefinitions.bme280_temperature, - sensorDefinitions.bme280_pressure_pa, - sensorDefinitions.bme280_pressure_hpa, - sensorDefinitions.dht11_humidity, - sensorDefinitions.dht11_temperature, - sensorDefinitions.dht22_humidity, - sensorDefinitions.dht22_temperature, - ], - // if custom, return all sensors - Custom: Object.values(sensorDefinitions), -}; + senseBoxHomeV2: [ + sensorDefinitions.hdc1080_temperature, + sensorDefinitions.hdc1080_humidity, + sensorDefinitions.bmp280_pressure, + sensorDefinitions.tsl45315_lightintensity, + sensorDefinitions.veml6070_uvintensity, + sensorDefinitions.sds011_pm10, + sensorDefinitions.sds011_pm25, + sensorDefinitions.bme680_humidity, + sensorDefinitions.bme680_temperature, + sensorDefinitions.bme680_pressure, + sensorDefinitions.bme680_voc, + sensorDefinitions.smt50_soilmoisture, + sensorDefinitions.smt50_soiltemperature, + sensorDefinitions.soundlevelmeter, + sensorDefinitions.windspeed, + sensorDefinitions.scd30_co2, + sensorDefinitions.dps310_pressure, + sensorDefinitions.sps30_pm1, + sensorDefinitions.sps30_pm4, + sensorDefinitions.sps30_pm10, + sensorDefinitions.sps30_pm25, + ], + 'senseBox:Edu': [ + sensorDefinitions.hdc1080_temperature, + sensorDefinitions.hdc1080_humidity, + sensorDefinitions.bmp280_pressure, + sensorDefinitions.tsl45315_lightintensity, + sensorDefinitions.veml6070_uvintensity, + sensorDefinitions.sds011_pm10, + sensorDefinitions.sds011_pm25, + sensorDefinitions.bme680_humidity, + sensorDefinitions.bme680_temperature, + sensorDefinitions.bme680_pressure, + sensorDefinitions.bme680_voc, + sensorDefinitions.smt50_soilmoisture, + sensorDefinitions.smt50_soiltemperature, + sensorDefinitions.soundlevelmeter, + sensorDefinitions.windspeed, + sensorDefinitions.scd30_co2, + sensorDefinitions.dps310_pressure, + sensorDefinitions.sps30_pm1, + sensorDefinitions.sps30_pm4, + sensorDefinitions.sps30_pm10, + sensorDefinitions.sps30_pm25, + ], + 'luftdaten.info': [ + sensorDefinitions.pms1003_pm01, + sensorDefinitions.pms1003_pm10, + sensorDefinitions.pms1003_pm25, + sensorDefinitions.pms3003_pm01, + sensorDefinitions.pms3003_pm10, + sensorDefinitions.pms3003_pm25, + sensorDefinitions.pms5003_pm01, + sensorDefinitions.pms5003_pm10, + sensorDefinitions.pms5003_pm25, + sensorDefinitions.pms7003_pm01, + sensorDefinitions.pms7003_pm10, + sensorDefinitions.pms7003_pm25, + sensorDefinitions.sds011_pm10, + sensorDefinitions.sds011_pm25, + sensorDefinitions.sps30_pm1, + sensorDefinitions.sps30_pm4, + sensorDefinitions.sps30_pm10, + sensorDefinitions.sps30_pm25, + sensorDefinitions.sht3x_humidity, + sensorDefinitions.sht3x_temperature, + sensorDefinitions.bmp180_temperature, + sensorDefinitions.bmp180_pressure_pa, + sensorDefinitions.bmp180_pressure_hpa, + sensorDefinitions.bme280_humidity, + sensorDefinitions.bme280_temperature, + sensorDefinitions.bme280_pressure_pa, + sensorDefinitions.bme280_pressure_hpa, + sensorDefinitions.dht11_humidity, + sensorDefinitions.dht11_temperature, + sensorDefinitions.dht22_humidity, + sensorDefinitions.dht22_temperature, + ], + homeEthernet: [ + sensorDefinitions.hdc1008_temperature, + sensorDefinitions.hdc1008_humidity, + sensorDefinitions.bmp280_pressure, + sensorDefinitions.tsl45315_lightintensity, + sensorDefinitions.veml6070_uvintensity, + ], + + homeWifi: [ + sensorDefinitions.hdc1008_temperature, + sensorDefinitions.hdc1008_humidity, + sensorDefinitions.bmp280_pressure, + sensorDefinitions.tsl45315_lightintensity, + sensorDefinitions.veml6070_uvintensity, + ], + homeEthernetFeinstaub: [ + sensorDefinitions.hdc1008_temperature, + sensorDefinitions.hdc1008_humidity, + sensorDefinitions.bmp280_pressure, + sensorDefinitions.tsl45315_lightintensity, + sensorDefinitions.veml6070_uvintensity, + sensorDefinitions.sds011_pm10, + sensorDefinitions.sds011_pm25, + ], + homeWifiFeinstaub: [ + sensorDefinitions.hdc1008_temperature, + sensorDefinitions.hdc1008_humidity, + sensorDefinitions.bmp280_pressure, + sensorDefinitions.tsl45315_lightintensity, + sensorDefinitions.veml6070_uvintensity, + sensorDefinitions.sds011_pm10, + sensorDefinitions.sds011_pm25, + ], +} // Exporting models export const getSensorsForModel = (model: keyof typeof modelDefinitions) => { - return modelDefinitions[model] || null; -}; + return modelDefinitions[model] || null +} diff --git a/drizzle/0023_red_chameleon.sql b/drizzle/0023_red_chameleon.sql new file mode 100644 index 00000000..afc6b666 --- /dev/null +++ b/drizzle/0023_red_chameleon.sql @@ -0,0 +1,11 @@ +ALTER TYPE "public"."model" ADD VALUE 'homeEthernet' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'homeWifi' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'homeEthernetFeinstaub' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'homeWifiFeinstaub' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_dht11' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_dht22' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_bmp180' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_bme280' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'hackair_home_v2' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TABLE "sensor" ADD COLUMN "icon" text; \ No newline at end of file diff --git a/drizzle/0024_first_kitty_pryde.sql b/drizzle/0024_first_kitty_pryde.sql new file mode 100644 index 00000000..afc6b666 --- /dev/null +++ b/drizzle/0024_first_kitty_pryde.sql @@ -0,0 +1,11 @@ +ALTER TYPE "public"."model" ADD VALUE 'homeEthernet' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'homeWifi' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'homeEthernetFeinstaub' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'homeWifiFeinstaub' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_dht11' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_dht22' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_bmp180' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'luftdaten_sds011_bme280' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TYPE "public"."model" ADD VALUE 'hackair_home_v2' BEFORE 'senseBox:Edu';--> statement-breakpoint +ALTER TABLE "sensor" ADD COLUMN "icon" text; \ No newline at end of file diff --git a/drizzle/meta/0022_snapshot.json b/drizzle/meta/0022_snapshot.json index bb478b93..2437bf9a 100644 --- a/drizzle/meta/0022_snapshot.json +++ b/drizzle/meta/0022_snapshot.json @@ -1,1263 +1,1187 @@ { - "id": "95fc2b5e-a6d7-426d-bfd8-7c5238f5722b", - "prevId": "85481101-dd0d-4e15-9158-11971b8ba509", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.device": { - "name": "device", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tags": { - "name": "tags", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "ARRAY[]::text[]" - }, - "link": { - "name": "link", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "use_auth": { - "name": "use_auth", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "exposure": { - "name": "exposure", - "type": "exposure", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "status", - "typeSchema": "public", - "primaryKey": false, - "notNull": false, - "default": "'inactive'" - }, - "model": { - "name": "model", - "type": "model", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires_at": { - "name": "expires_at", - "type": "date", - "primaryKey": false, - "notNull": false - }, - "latitude": { - "name": "latitude", - "type": "double precision", - "primaryKey": false, - "notNull": true - }, - "longitude": { - "name": "longitude", - "type": "double precision", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "sensor_wiki_model": { - "name": "sensor_wiki_model", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.device_to_location": { - "name": "device_to_location", - "schema": "", - "columns": { - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "location_id": { - "name": "location_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "time": { - "name": "time", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "device_to_location_device_id_device_id_fk": { - "name": "device_to_location_device_id_device_id_fk", - "tableFrom": "device_to_location", - "tableTo": "device", - "columnsFrom": [ - "device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "device_to_location_location_id_location_id_fk": { - "name": "device_to_location_location_id_location_id_fk", - "tableFrom": "device_to_location", - "tableTo": "location", - "columnsFrom": [ - "location_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "device_to_location_device_id_location_id_time_pk": { - "name": "device_to_location_device_id_location_id_time_pk", - "columns": [ - "device_id", - "location_id", - "time" - ] - } - }, - "uniqueConstraints": { - "device_to_location_device_id_location_id_time_unique": { - "name": "device_to_location_device_id_location_id_time_unique", - "nullsNotDistinct": false, - "columns": [ - "device_id", - "location_id", - "time" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.measurement": { - "name": "measurement", - "schema": "", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "value": { - "name": "value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "location_id": { - "name": "location_id", - "type": "bigint", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "measurement_location_id_location_id_fk": { - "name": "measurement_location_id_location_id_fk", - "tableFrom": "measurement", - "tableTo": "location", - "columnsFrom": [ - "location_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "measurement_sensor_id_time_unique": { - "name": "measurement_sensor_id_time_unique", - "nullsNotDistinct": false, - "columns": [ - "sensor_id", - "time" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.password": { - "name": "password", - "schema": "", - "columns": { - "hash": { - "name": "hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "password_user_id_user_id_fk": { - "name": "password_user_id_user_id_fk", - "tableFrom": "password", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.password_reset_request": { - "name": "password_reset_request", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "password_reset_request_user_id_user_id_fk": { - "name": "password_reset_request_user_id_user_id_fk", - "tableFrom": "password_reset_request", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "password_reset_request_user_id_unique": { - "name": "password_reset_request_user_id_unique", - "nullsNotDistinct": false, - "columns": [ - "user_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.profile": { - "name": "profile", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "profile_user_id_user_id_fk": { - "name": "profile_user_id_user_id_fk", - "tableFrom": "profile", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "profile_username_unique": { - "name": "profile_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.profile_image": { - "name": "profile_image", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "alt_text": { - "name": "alt_text", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "blob": { - "name": "blob", - "type": "bytea", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "profile_id": { - "name": "profile_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "profile_image_profile_id_profile_id_fk": { - "name": "profile_image_profile_id_profile_id_fk", - "tableFrom": "profile_image", - "tableTo": "profile", - "columnsFrom": [ - "profile_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sensor": { - "name": "sensor", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "unit": { - "name": "unit", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sensor_type": { - "name": "sensor_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "status", - "typeSchema": "public", - "primaryKey": false, - "notNull": false, - "default": "'inactive'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "sensor_wiki_type": { - "name": "sensor_wiki_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sensor_wiki_phenomenon": { - "name": "sensor_wiki_phenomenon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sensor_wiki_unit": { - "name": "sensor_wiki_unit", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "lastMeasurement": { - "name": "lastMeasurement", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "data": { - "name": "data", - "type": "json", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "sensor_device_id_device_id_fk": { - "name": "sensor_device_id_device_id_fk", - "tableFrom": "sensor", - "tableTo": "device", - "columnsFrom": [ - "device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "unconfirmed_email": { - "name": "unconfirmed_email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'user'" - }, - "language": { - "name": "language", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'en_US'" - }, - "email_is_confirmed": { - "name": "email_is_confirmed", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "email_confirmation_token": { - "name": "email_confirmation_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - }, - "user_unconfirmed_email_unique": { - "name": "user_unconfirmed_email_unique", - "nullsNotDistinct": false, - "columns": [ - "unconfirmed_email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.location": { - "name": "location", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "bigserial", - "primaryKey": true, - "notNull": true - }, - "location": { - "name": "location", - "type": "geometry(point)", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "location_index": { - "name": "location_index", - "columns": [ - { - "expression": "location", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gist", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "location_location_unique": { - "name": "location_location_unique", - "nullsNotDistinct": false, - "columns": [ - "location" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.log_entry": { - "name": "log_entry", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.refresh_token": { - "name": "refresh_token", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "refresh_token_user_id_user_id_fk": { - "name": "refresh_token_user_id_user_id_fk", - "tableFrom": "refresh_token", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.token_revocation": { - "name": "token_revocation", - "schema": "", - "columns": { - "hash": { - "name": "hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.claim": { - "name": "claim", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "box_id": { - "name": "box_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "claim_expires_at_idx": { - "name": "claim_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "claim_box_id_device_id_fk": { - "name": "claim_box_id_device_id_fk", - "tableFrom": "claim", - "tableTo": "device", - "columnsFrom": [ - "box_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_box_id": { - "name": "unique_box_id", - "nullsNotDistinct": false, - "columns": [ - "box_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.access_token": { - "name": "access_token", - "schema": "", - "columns": { - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "access_token_device_id_device_id_fk": { - "name": "access_token_device_id_device_id_fk", - "tableFrom": "access_token", - "tableTo": "device", - "columnsFrom": [ - "device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.exposure": { - "name": "exposure", - "schema": "public", - "values": [ - "indoor", - "outdoor", - "mobile", - "unknown" - ] - }, - "public.model": { - "name": "model", - "schema": "public", - "values": [ - "homeV2Lora", - "homeV2Ethernet", - "homeV2Wifi", - "senseBox:Edu", - "luftdaten.info", - "Custom" - ] - }, - "public.status": { - "name": "status", - "schema": "public", - "values": [ - "active", - "inactive", - "old" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": { - "public.measurement_10min": { - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "name": "measurement_10min", - "schema": "public", - "isExisting": true, - "materialized": true - }, - "public.measurement_1day": { - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "name": "measurement_1day", - "schema": "public", - "isExisting": true, - "materialized": true - }, - "public.measurement_1hour": { - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "name": "measurement_1hour", - "schema": "public", - "isExisting": true, - "materialized": true - }, - "public.measurement_1month": { - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "name": "measurement_1month", - "schema": "public", - "isExisting": true, - "materialized": true - }, - "public.measurement_1year": { - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "name": "measurement_1year", - "schema": "public", - "isExisting": true, - "materialized": true - } - }, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "id": "95fc2b5e-a6d7-426d-bfd8-7c5238f5722b", + "prevId": "85481101-dd0d-4e15-9158-11971b8ba509", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": ["device_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": ["location_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": ["device_id", "location_id", "time"] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": ["device_id", "location_id", "time"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": ["location_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": ["sensor_id", "time"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": ["username"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": ["profile_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": ["device_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": ["unconfirmed_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": ["location"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": ["box_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": ["box_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.access_token": { + "name": "access_token", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "access_token_device_id_device_id_fk": { + "name": "access_token_device_id_device_id_fk", + "tableFrom": "access_token", + "tableTo": "device", + "columnsFrom": ["device_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": ["indoor", "outdoor", "mobile", "unknown"] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "senseBox:Edu", + "luftdaten.info", + "Custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": ["active", "inactive", "old"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/0023_snapshot.json b/drizzle/meta/0023_snapshot.json index 7b257b01..87b9b2ae 100644 --- a/drizzle/meta/0023_snapshot.json +++ b/drizzle/meta/0023_snapshot.json @@ -1,1263 +1,1187 @@ { - "id": "b7903c96-4a1f-498b-abb4-07815b2d42d8", - "prevId": "95fc2b5e-a6d7-426d-bfd8-7c5238f5722b", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.device": { - "name": "device", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tags": { - "name": "tags", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "ARRAY[]::text[]" - }, - "link": { - "name": "link", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "use_auth": { - "name": "use_auth", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "exposure": { - "name": "exposure", - "type": "exposure", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "status", - "typeSchema": "public", - "primaryKey": false, - "notNull": false, - "default": "'inactive'" - }, - "model": { - "name": "model", - "type": "model", - "typeSchema": "public", - "primaryKey": false, - "notNull": false - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "expires_at": { - "name": "expires_at", - "type": "date", - "primaryKey": false, - "notNull": false - }, - "latitude": { - "name": "latitude", - "type": "double precision", - "primaryKey": false, - "notNull": true - }, - "longitude": { - "name": "longitude", - "type": "double precision", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "sensor_wiki_model": { - "name": "sensor_wiki_model", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.device_to_location": { - "name": "device_to_location", - "schema": "", - "columns": { - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "location_id": { - "name": "location_id", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "time": { - "name": "time", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "device_to_location_device_id_device_id_fk": { - "name": "device_to_location_device_id_device_id_fk", - "tableFrom": "device_to_location", - "columnsFrom": [ - "device_id" - ], - "tableTo": "device", - "columnsTo": [ - "id" - ], - "onUpdate": "cascade", - "onDelete": "cascade" - }, - "device_to_location_location_id_location_id_fk": { - "name": "device_to_location_location_id_location_id_fk", - "tableFrom": "device_to_location", - "columnsFrom": [ - "location_id" - ], - "tableTo": "location", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "no action" - } - }, - "compositePrimaryKeys": { - "device_to_location_device_id_location_id_time_pk": { - "name": "device_to_location_device_id_location_id_time_pk", - "columns": [ - "device_id", - "location_id", - "time" - ] - } - }, - "uniqueConstraints": { - "device_to_location_device_id_location_id_time_unique": { - "name": "device_to_location_device_id_location_id_time_unique", - "columns": [ - "device_id", - "location_id", - "time" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.measurement": { - "name": "measurement", - "schema": "", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "value": { - "name": "value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "location_id": { - "name": "location_id", - "type": "bigint", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "measurement_location_id_location_id_fk": { - "name": "measurement_location_id_location_id_fk", - "tableFrom": "measurement", - "columnsFrom": [ - "location_id" - ], - "tableTo": "location", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "measurement_sensor_id_time_unique": { - "name": "measurement_sensor_id_time_unique", - "columns": [ - "sensor_id", - "time" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.password": { - "name": "password", - "schema": "", - "columns": { - "hash": { - "name": "hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "password_user_id_user_id_fk": { - "name": "password_user_id_user_id_fk", - "tableFrom": "password", - "columnsFrom": [ - "user_id" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "cascade", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.password_reset_request": { - "name": "password_reset_request", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "password_reset_request_user_id_user_id_fk": { - "name": "password_reset_request_user_id_user_id_fk", - "tableFrom": "password_reset_request", - "columnsFrom": [ - "user_id" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "password_reset_request_user_id_unique": { - "name": "password_reset_request_user_id_unique", - "columns": [ - "user_id" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.profile": { - "name": "profile", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "profile_user_id_user_id_fk": { - "name": "profile_user_id_user_id_fk", - "tableFrom": "profile", - "columnsFrom": [ - "user_id" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "cascade", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "profile_username_unique": { - "name": "profile_username_unique", - "columns": [ - "username" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.profile_image": { - "name": "profile_image", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "alt_text": { - "name": "alt_text", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "blob": { - "name": "blob", - "type": "bytea", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "profile_id": { - "name": "profile_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "profile_image_profile_id_profile_id_fk": { - "name": "profile_image_profile_id_profile_id_fk", - "tableFrom": "profile_image", - "columnsFrom": [ - "profile_id" - ], - "tableTo": "profile", - "columnsTo": [ - "id" - ], - "onUpdate": "cascade", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.sensor": { - "name": "sensor", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "unit": { - "name": "unit", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sensor_type": { - "name": "sensor_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "status", - "typeSchema": "public", - "primaryKey": false, - "notNull": false, - "default": "'inactive'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "sensor_wiki_type": { - "name": "sensor_wiki_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sensor_wiki_phenomenon": { - "name": "sensor_wiki_phenomenon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sensor_wiki_unit": { - "name": "sensor_wiki_unit", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "lastMeasurement": { - "name": "lastMeasurement", - "type": "json", - "primaryKey": false, - "notNull": false - }, - "data": { - "name": "data", - "type": "json", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "sensor_device_id_device_id_fk": { - "name": "sensor_device_id_device_id_fk", - "tableFrom": "sensor", - "columnsFrom": [ - "device_id" - ], - "tableTo": "device", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "unconfirmed_email": { - "name": "unconfirmed_email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'user'" - }, - "language": { - "name": "language", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'en_US'" - }, - "email_is_confirmed": { - "name": "email_is_confirmed", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "email_confirmation_token": { - "name": "email_confirmation_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "columns": [ - "email" - ], - "nullsNotDistinct": false - }, - "user_unconfirmed_email_unique": { - "name": "user_unconfirmed_email_unique", - "columns": [ - "unconfirmed_email" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.location": { - "name": "location", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "bigserial", - "primaryKey": true, - "notNull": true - }, - "location": { - "name": "location", - "type": "geometry(point)", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "location_index": { - "name": "location_index", - "columns": [ - { - "expression": "location", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "gist", - "concurrently": false - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "location_location_unique": { - "name": "location_location_unique", - "columns": [ - "location" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.log_entry": { - "name": "log_entry", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.refresh_token": { - "name": "refresh_token", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "refresh_token_user_id_user_id_fk": { - "name": "refresh_token_user_id_user_id_fk", - "tableFrom": "refresh_token", - "columnsFrom": [ - "user_id" - ], - "tableTo": "user", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.token_revocation": { - "name": "token_revocation", - "schema": "", - "columns": { - "hash": { - "name": "hash", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.claim": { - "name": "claim", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "box_id": { - "name": "box_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "claim_expires_at_idx": { - "name": "claim_expires_at_idx", - "columns": [ - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "with": {}, - "method": "btree", - "concurrently": false - } - }, - "foreignKeys": { - "claim_box_id_device_id_fk": { - "name": "claim_box_id_device_id_fk", - "tableFrom": "claim", - "columnsFrom": [ - "box_id" - ], - "tableTo": "device", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_box_id": { - "name": "unique_box_id", - "columns": [ - "box_id" - ], - "nullsNotDistinct": false - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.access_token": { - "name": "access_token", - "schema": "", - "columns": { - "device_id": { - "name": "device_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "access_token_device_id_device_id_fk": { - "name": "access_token_device_id_device_id_fk", - "tableFrom": "access_token", - "columnsFrom": [ - "device_id" - ], - "tableTo": "device", - "columnsTo": [ - "id" - ], - "onUpdate": "no action", - "onDelete": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.exposure": { - "name": "exposure", - "schema": "public", - "values": [ - "indoor", - "outdoor", - "mobile", - "unknown" - ] - }, - "public.model": { - "name": "model", - "schema": "public", - "values": [ - "homeV2Lora", - "homeV2Ethernet", - "homeV2Wifi", - "senseBox:Edu", - "luftdaten.info", - "Custom" - ] - }, - "public.status": { - "name": "status", - "schema": "public", - "values": [ - "active", - "inactive", - "old" - ] - } - }, - "schemas": {}, - "views": { - "public.measurement_10min": { - "name": "measurement_10min", - "schema": "public", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "materialized": true, - "isExisting": true - }, - "public.measurement_1day": { - "name": "measurement_1day", - "schema": "public", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "materialized": true, - "isExisting": true - }, - "public.measurement_1hour": { - "name": "measurement_1hour", - "schema": "public", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "materialized": true, - "isExisting": true - }, - "public.measurement_1month": { - "name": "measurement_1month", - "schema": "public", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "materialized": true, - "isExisting": true - }, - "public.measurement_1year": { - "name": "measurement_1year", - "schema": "public", - "columns": { - "sensor_id": { - "name": "sensor_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "time": { - "name": "time", - "type": "timestamp (3) with time zone", - "primaryKey": false, - "notNull": false - }, - "avg_value": { - "name": "avg_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "total_values": { - "name": "total_values", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "min_value": { - "name": "min_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - }, - "max_value": { - "name": "max_value", - "type": "double precision", - "primaryKey": false, - "notNull": false - } - }, - "materialized": true, - "isExisting": true - } - }, - "sequences": {}, - "roles": {}, - "policies": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "id": "b7903c96-4a1f-498b-abb4-07815b2d42d8", + "prevId": "95fc2b5e-a6d7-426d-bfd8-7c5238f5722b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "columnsFrom": ["device_id"], + "tableTo": "device", + "columnsTo": ["id"], + "onUpdate": "cascade", + "onDelete": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "columnsFrom": ["location_id"], + "tableTo": "location", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": ["device_id", "location_id", "time"] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "columns": ["device_id", "location_id", "time"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "columnsFrom": ["location_id"], + "tableTo": "location", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "columns": ["sensor_id", "time"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "columns": ["user_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "columns": ["username"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "columnsFrom": ["profile_id"], + "tableTo": "profile", + "columnsTo": ["id"], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "columnsFrom": ["device_id"], + "tableTo": "device", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": ["email"], + "nullsNotDistinct": false + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "columns": ["unconfirmed_email"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gist", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "columns": ["location"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "columnsFrom": ["user_id"], + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "columnsFrom": ["box_id"], + "tableTo": "device", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "columns": ["box_id"], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.access_token": { + "name": "access_token", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "access_token_device_id_device_id_fk": { + "name": "access_token_device_id_device_id_fk", + "tableFrom": "access_token", + "columnsFrom": ["device_id"], + "tableTo": "device", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": ["indoor", "outdoor", "mobile", "unknown"] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "senseBox:Edu", + "luftdaten.info", + "Custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": ["active", "inactive", "old"] + } + }, + "schemas": {}, + "views": { + "public.measurement_10min": { + "name": "measurement_10min", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1day": { + "name": "measurement_1day", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1hour": { + "name": "measurement_1hour", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1month": { + "name": "measurement_1month", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1year": { + "name": "measurement_1year", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + } + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/0024_snapshot.json b/drizzle/meta/0024_snapshot.json new file mode 100644 index 00000000..a4eb1392 --- /dev/null +++ b/drizzle/meta/0024_snapshot.json @@ -0,0 +1,1279 @@ +{ + "id": "be35aeec-1a4d-449f-bbe9-b290b6a79093", + "prevId": "b7903c96-4a1f-498b-abb4-07815b2d42d8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "sensor_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": [ + "unconfirmed_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": [ + "location" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": [ + "box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": [ + "box_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.access_token": { + "name": "access_token", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "access_token_device_id_device_id_fk": { + "name": "access_token_device_id_device_id_fk", + "tableFrom": "access_token", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "Custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2e48cd63..0bbba872 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1765380754120, "tag": "0023_check_location", "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1767972133643, + "tag": "0024_first_kitty_pryde", + "breakpoints": true } ] } \ No newline at end of file diff --git a/tests/models/device.server.spec.ts b/tests/models/device.server.spec.ts index 87a96216..7424f9cd 100644 --- a/tests/models/device.server.spec.ts +++ b/tests/models/device.server.spec.ts @@ -42,7 +42,7 @@ describe('Device Model: createDevice', () => { latitude: 51.969, longitude: 7.596, exposure: 'outdoor', - model: 'homeV2Wifi', + // model: "homeV2Wifi", sensors: [ { title: 'Temperature', unit: '°C', sensorType: 'HDC1080' }, { title: 'Humidity', unit: '%', sensorType: 'HDC1080' }, @@ -101,18 +101,37 @@ describe('Device Model: createDevice', () => { expect(result.sensors).toHaveLength(0) }) + it('should create device with tags/grouptag', async () => { + const deviceData = { + name: 'Tagged Device', + latitude: 51.5, + longitude: 7.5, + exposure: 'outdoor', + // model: 'Custom', + tags: ['weather', 'city', 'test'], + sensors: [{ title: 'Temperature', unit: '°C', sensorType: 'DHT22' }], + } + + const result = await createDevice(deviceData, userId) + + createdDeviceIds.push(result.id) + expect(result).toHaveProperty('tags') + expect(Array.isArray(result.tags)).toBe(true) + expect(result.tags).toEqual(['weather', 'city', 'test']) + expect(result.sensors).toHaveLength(1) + }) + it('should create device with optional fields', async () => { const deviceData = { name: 'Full Featured Device', latitude: 51.0, longitude: 7.0, exposure: 'mobile', - model: 'homeV2Lora', description: 'A comprehensive test device', image: 'https://example.com/device.jpg', link: 'https://example.com', public: true, - tags: [], + tags: ['test'], sensors: [{ title: 'Temperature', unit: '°C', sensorType: 'SHT31' }], } @@ -124,7 +143,6 @@ describe('Device Model: createDevice', () => { expect(result).toHaveProperty('link', 'https://example.com') expect(result).toHaveProperty('public', true) expect(result).toHaveProperty('exposure', 'mobile') - expect(result).toHaveProperty('model', 'homeV2Lora') expect(result.sensors).toHaveLength(1) }) diff --git a/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts b/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts index 459b4329..287dc47b 100644 --- a/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts +++ b/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts @@ -22,7 +22,7 @@ const DEVICE_SENSOR_ID_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', + //model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [ diff --git a/tests/routes/api.boxes.$deviceId.locations.spec.ts b/tests/routes/api.boxes.$deviceId.locations.spec.ts index 42af89e6..dd0a77e8 100644 --- a/tests/routes/api.boxes.$deviceId.locations.spec.ts +++ b/tests/routes/api.boxes.$deviceId.locations.spec.ts @@ -24,7 +24,6 @@ const DEVICE_SENSOR_ID_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [ diff --git a/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts b/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts index 4cc366d4..120e120e 100644 --- a/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts +++ b/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts @@ -17,7 +17,7 @@ const DEVICE_SENSOR_ID_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', + //model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [ diff --git a/tests/routes/api.boxes.$deviceId.sensors.spec.ts b/tests/routes/api.boxes.$deviceId.sensors.spec.ts index 623b5d33..82280f19 100644 --- a/tests/routes/api.boxes.$deviceId.sensors.spec.ts +++ b/tests/routes/api.boxes.$deviceId.sensors.spec.ts @@ -16,7 +16,7 @@ const DEVICE_SENSOR_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', + //model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [ diff --git a/tests/routes/api.boxes.spec.ts b/tests/routes/api.boxes.spec.ts index 909b5281..f8e36e38 100644 --- a/tests/routes/api.boxes.spec.ts +++ b/tests/routes/api.boxes.spec.ts @@ -46,7 +46,7 @@ describe('openSenseMap API Routes: /boxes', () => { name: 'Test Weather Station', location: [7.596, 51.969], exposure: 'outdoor', - model: 'homeV2Wifi', + //model: 'homeV2Wifi', grouptag: ['weather', 'test'], sensors: [ { diff --git a/tests/routes/api.device.feinstaub.spec.ts b/tests/routes/api.device.feinstaub.spec.ts new file mode 100644 index 00000000..4fdde0f7 --- /dev/null +++ b/tests/routes/api.device.feinstaub.spec.ts @@ -0,0 +1,204 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import { BASE_URL } from 'vitest.setup' +import { createToken } from '~/lib/jwt' +import { registerUser } from '~/lib/user-service.server' +import { createDevice, deleteDevice, getDevice } from '~/models/device.server' +import { deleteUserByEmail } from '~/models/user.server' +import { action as deviceUpdateAction } from '~/routes/api.device.$deviceId' +import { type User, type Device } from '~/schema' + +const TEST_USER = { + name: 'feinstaubAddonUpdateTestUser', + email: 'feinstaubUpdate.addon@test', + password: 'secureTestPassword123!', +} + +let user: User +let jwt: string +let baseDevice: Device + +const generateMinimalDevice = (model = 'homeEthernetFeinstaub') => ({ + exposure: 'mobile', + latitude: 12.34, + longitude: 56.78, + name: 'senseBox' + Date.now(), + model: model, +}) + +describe('Device API: Feinstaub Addon behavior', () => { + let queryableDevice: Device | null = null + + beforeAll(async () => { + const testUser = await registerUser( + TEST_USER.name, + TEST_USER.email, + TEST_USER.password, + 'en_US', + ) + user = testUser as User + const { token: t } = await createToken(testUser as User) + jwt = t + + queryableDevice = await createDevice( + { + ...generateMinimalDevice(), + latitude: 123, + longitude: 12, + tags: ['newgroup'], + }, + (testUser as User).id, + ) + }) + + afterAll(async () => { + await deleteUserByEmail(TEST_USER.email) + await deleteDevice({ id: queryableDevice!.id }) + }) + + it('should allow to register a homeEthernetFeinstaub device and include SDS011 sensors', async () => { + const device = await createDevice(generateMinimalDevice(), user.id) + + const fetched = await getDevice({ id: device.id }) + expect(fetched).toBeDefined() + + const hasPM10 = fetched!.sensors.some( + (s) => s.sensorType === 'SDS 011' && s.title === 'PM10', + ) + const hasPM25 = fetched!.sensors.some( + (s) => s.sensorType === 'SDS 011' && s.title === 'PM2.5', + ) + + expect(hasPM10).toBe(true) + expect(hasPM25).toBe(true) + + await deleteDevice({ id: device.id }) + }) + + it('should allow to register a homeWifiFeinstaub device and include SDS011 sensors', async () => { + const device = await createDevice( + generateMinimalDevice('homeWifiFeinstaub'), + user.id, + ) + + const fetched = await getDevice({ id: device.id }) + expect(fetched).toBeDefined() + + const hasPM10 = fetched!.sensors.some( + (s) => s.sensorType === 'SDS 011' && s.title === 'PM10', + ) + const hasPM25 = fetched!.sensors.some( + (s) => s.sensorType === 'SDS 011' && s.title === 'PM2.5', + ) + + expect(hasPM10).toBe(true) + expect(hasPM25).toBe(true) + + await deleteDevice({ id: device.id }) + }) + + it('should allow to add the feinstaub addon via PUT for a homeWifi device', async () => { + const device = await createDevice( + generateMinimalDevice('homeWifiFeinstaub'), + user.id, + ) + + const updatePayload = { addons: { add: 'feinstaub' } } + + const request = new Request(`${BASE_URL}/${device.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(updatePayload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: device.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + + expect(data.model).toBe('homeWifiFeinstaub') + + const hasPM10 = data.sensors.some( + (s: any) => s.sensorType === 'SDS 011' && s.title === 'PM10', + ) + const hasPM25 = data.sensors.some( + (s: any) => s.sensorType === 'SDS 011' && s.title === 'PM2.5', + ) + + expect(hasPM10).toBe(true) + expect(hasPM25).toBe(true) + + const secondRequest = new Request(`${BASE_URL}/${device.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(updatePayload), + }) as unknown as Request + + // Second PUT should be idempotent — same sensors + const secondResponse: any = await deviceUpdateAction({ + request: secondRequest, + params: { deviceId: device.id }, + context: {} as any, + }) + + expect(secondResponse.status).toBe(200) + const secondData = await secondResponse.json() + expect(secondData.sensors).toEqual(data.sensors) + + await deleteDevice({ id: device.id }) + }) + + it('should do nothing when adding the feinstaub addon to a non-home device', async () => { + const device = await createDevice( + { + ...generateMinimalDevice('Custom'), + // sensors: [{ title: 'temp', unit: 'K', sensorType: 'some Sensor' }], + }, + user.id, + ) + + const updatePayload = { addons: { add: 'feinstaub' } } + + const request = new Request(`${BASE_URL}/${device.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(updatePayload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: device.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + + // Model should not change + expect(data.model).toBe('Custom') + + // Should not have SDS011 sensors + const hasPM10 = data.sensors.some( + (s: any) => s.sensorType === 'SDS 011' && s.title === 'PM10', + ) + const hasPM25 = data.sensors.some( + (s: any) => s.sensorType === 'SDS 011' && s.title === 'PM2.5', + ) + expect(hasPM10).toBe(false) + expect(hasPM25).toBe(false) + + await deleteDevice({ id: device.id }) + }) +}) diff --git a/tests/routes/api.device.sensors.spec.ts b/tests/routes/api.device.sensors.spec.ts new file mode 100644 index 00000000..93772720 --- /dev/null +++ b/tests/routes/api.device.sensors.spec.ts @@ -0,0 +1,356 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import { BASE_URL } from 'vitest.setup' +import { createToken } from '~/lib/jwt' +import { registerUser } from '~/lib/user-service.server' +import { createDevice, deleteDevice, getDevice } from '~/models/device.server' +import { deleteUserByEmail } from '~/models/user.server' +import { action as deviceUpdateAction } from '~/routes/api.device.$deviceId' +import { type User, type Device } from '~/schema' + +const DEVICE_TEST_USER = { + name: 'deviceTestUpdateSensors', + email: 'test@deviceSensorsUpdate.endpoint', + password: 'highlySecurePasswordForTesting', +} + +let user: User +let jwt: string +let queryableDevice: Device + +const generateMinimalDevice = () => ({ + exposure: 'mobile', + location: { lat: 12.34, lng: 56.78 }, + name: 'senseBox' + Date.now(), +}) + +describe('Device Sensors API: updating sensors', () => { + beforeAll(async () => { + const testUser = await registerUser( + DEVICE_TEST_USER.name, + DEVICE_TEST_USER.email, + DEVICE_TEST_USER.password, + 'en_US', + ) + user = testUser as User + const { token } = await createToken(user) + jwt = token + + queryableDevice = await createDevice( + { + ...generateMinimalDevice(), + latitude: 12.34, + longitude: 56.78, + sensors: [ + { + title: 'Temperature', + unit: '°C', + sensorType: 'DHT22', + }, + { + title: 'Humidity', + unit: '%', + sensorType: 'DHT22', + }, + { + title: 'Pressure', + unit: 'hPa', + sensorType: 'BMP280', + }, + { + title: 'Light', + unit: 'lux', + sensorType: 'TSL2561', + }, + { + title: 'UV', + unit: 'µW/cm²', + sensorType: 'VEML6070', + }, + ], + }, + user.id, + ) + }) + + afterAll(async () => { + await deleteDevice({ id: queryableDevice.id }) + await deleteUserByEmail(DEVICE_TEST_USER.email) + }) + + it('should allow to add a sensor', async () => { + const newSensor = { + title: 'PM10', + unit: 'µg/m³', + sensorType: 'SDS 011', + icon: 'osem-particulate-matter', + new: 'true', + edited: 'true', + } + + const payload = { sensors: [newSensor] } + + const request = new Request(`${BASE_URL}/${queryableDevice.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(payload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + + const addedSensor = data.sensors.find( + (s: any) => s.title === newSensor.title, + ) + expect(addedSensor).toBeDefined() + expect(addedSensor.unit).toBe(newSensor.unit) + expect(addedSensor.sensorType).toBe(newSensor.sensorType) + expect(addedSensor.icon).toBe(newSensor.icon) + + const freshDevice = await getDevice({ id: queryableDevice.id }) + const verifiedSensor = freshDevice?.sensors?.find( + (s: any) => s.title === newSensor.title, + ) + expect(verifiedSensor).toBeDefined() + }) + + it('should allow to add multiple sensors via PUT', async () => { + const newSensors = [ + { + title: 'PM2.5', + unit: 'µg/m³', + sensorType: 'SDS 011', + edited: 'true', + new: 'true', + }, + { + title: 'CO2', + unit: 'ppm', + sensorType: 'MH-Z19', + edited: 'true', + new: 'true', + }, + ] + + const payload = { sensors: newSensors } + + const request = new Request(`${BASE_URL}/${queryableDevice.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(payload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + + const hasPM25 = data.sensors.some((s: any) => s.title === 'PM2.5') + const hasCO2 = data.sensors.some((s: any) => s.title === 'CO2') + + expect(hasPM25).toBe(true) + expect(hasCO2).toBe(true) + + const freshDevice = await getDevice({ id: queryableDevice.id }) + const verifiedPM25 = freshDevice?.sensors?.some( + (s: any) => s.title === 'PM2.5', + ) + const verifiedCO2 = freshDevice?.sensors?.some( + (s: any) => s.title === 'CO2', + ) + + expect(verifiedPM25).toBe(true) + expect(verifiedCO2).toBe(true) + }) + + it('should allow to edit a sensor', async () => { + const freshDevice = await getDevice({ id: queryableDevice.id }) + const existingSensor = freshDevice?.sensors?.[0] + + if (!existingSensor) { + throw new Error('No sensors found on device') + } + + const updatedSensor = { + _id: existingSensor.id, + title: 'editedTitle', + unit: 'editedUnit', + sensorType: 'editedType', + icon: 'editedIcon', + edited: 'true', + } + + const payload = { sensors: [updatedSensor] } + + const request = new Request(`${BASE_URL}/${queryableDevice.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(payload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + + const editedSensor = data.sensors.find( + (s: any) => s._id === existingSensor.id, + ) + expect(editedSensor).toBeDefined() + expect(editedSensor.title).toBe(updatedSensor.title) + expect(editedSensor.unit).toBe(updatedSensor.unit) + expect(editedSensor.sensorType).toBe(updatedSensor.sensorType) + }) + + it('should allow to delete a single sensor via PUT', async () => { + const freshDevice = await getDevice({ id: queryableDevice.id }) + + if (!freshDevice?.sensors || freshDevice.sensors.length < 2) { + throw new Error('Not enough sensors to test deletion') + } + + const sensorToDelete = freshDevice.sensors[0] + const initialSensorCount = freshDevice.sensors.length + + const payload = { + sensors: [ + { + _id: sensorToDelete.id, + deleted: true, + }, + ], + } + + const request = new Request(`${BASE_URL}/${queryableDevice.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(payload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + expect(data.sensors).toHaveLength(initialSensorCount - 1) + + const deletedSensorStillExists = data.sensors.some( + (s: any) => s._id === sensorToDelete.id, + ) + expect(deletedSensorStillExists).toBe(false) + + const updatedDevice = await getDevice({ id: queryableDevice.id }) + expect(updatedDevice?.sensors?.length).toBe(initialSensorCount - 1) + }) + + it('should allow to delete multiple sensors at once', async () => { + const freshDevice = await getDevice({ id: queryableDevice.id }) + + if (!freshDevice?.sensors || freshDevice.sensors.length < 3) { + throw new Error('Not enough sensors to test deletion') + } + + const sensorsToDelete = freshDevice.sensors.slice(0, 2).map((s: any) => ({ + _id: s.id, + deleted: true, + })) + + const initialSensorCount = freshDevice.sensors.length + + const payload = { sensors: sensorsToDelete } + + const request = new Request(`${BASE_URL}/${queryableDevice.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(payload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice.id }, + context: {} as any, + }) + + expect(response.status).toBe(200) + const data = await response.json() + expect(data.sensors).toHaveLength(initialSensorCount - 2) + + const remainingSensors = data.sensors.map((s: any) => s._id) + + sensorsToDelete.forEach((s: any) => { + expect(remainingSensors).not.toContain(s._id) + }) + + const updatedDevice = await getDevice({ id: queryableDevice.id }) + expect(updatedDevice?.sensors?.length).toBe(initialSensorCount - 2) + }) + + it('should NOT allow to delete all sensors', async () => { + const freshDevice = await getDevice({ id: queryableDevice.id }) + + if (!freshDevice?.sensors) { + throw new Error('No sensors found on device') + } + + const allSensors = freshDevice.sensors.map((s: any) => ({ + _id: s.id, + deleted: true, + })) + + const payload = { sensors: allSensors } + + const request = new Request(`${BASE_URL}/${queryableDevice.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(payload), + }) as unknown as Request + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice.id }, + context: {} as any, + }) + + expect(response.status).toBe(400) + const data = await response.json() + expect(data.code).toBe('BadRequest') + expect(data.message).toContain('Unable to delete sensor') + + const unchangedDevice = await getDevice({ id: queryableDevice.id }) + expect(unchangedDevice?.sensors?.length).toBe(freshDevice.sensors.length) + }) +}) diff --git a/tests/routes/api.devices.spec.ts b/tests/routes/api.devices.spec.ts index 21f2f981..8e8dc274 100644 --- a/tests/routes/api.devices.spec.ts +++ b/tests/routes/api.devices.spec.ts @@ -9,7 +9,10 @@ import { createToken } from '~/lib/jwt' import { registerUser } from '~/lib/user-service.server' import { createDevice, deleteDevice } from '~/models/device.server' import { deleteUserByEmail } from '~/models/user.server' -import { loader as deviceLoader } from '~/routes/api.device.$deviceId' +import { + loader as deviceLoader, + action as deviceUpdateAction, +} from '~/routes/api.device.$deviceId' import { loader as devicesLoader, action as devicesAction, @@ -50,33 +53,13 @@ describe('openSenseMap API Routes: /boxes', () => { ...generateMinimalDevice(), latitude: 123, longitude: 12, - tags: ['newgroup'], + tags: ['testgroup'], }, (testUser as User).id, ) }) describe('GET', () => { - it('should search for boxes with a specific name', async () => { - // Arrange - const request = new Request( - `${BASE_URL}?format=geojson&name=${queryableDevice?.name}`, - { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }, - ) - - // Act - const response: any = await devicesLoader({ - request: request, - } as LoaderFunctionArgs) - - expect(response).toBeDefined() - expect(Array.isArray(response?.features)).toBe(true) - expect(response?.features.length).lessThanOrEqual(5) // 5 is default limit - }) - it('should search for boxes with a specific name and limit the results', async () => { // Arrange const request = new Request( @@ -300,7 +283,7 @@ describe('openSenseMap API Routes: /boxes', () => { it('should allow to filter boxes by grouptag', async () => { // Arrange - const request = new Request(`${BASE_URL}?grouptag=newgroup`, { + const request = new Request(`${BASE_URL}?grouptag=testgroup`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) @@ -364,6 +347,72 @@ describe('openSenseMap API Routes: /boxes', () => { } } }) + + it('should reject filtering boxes near a location with wrong parameter values', async () => { + // Arrange + const request = new Request(`${BASE_URL}?near=test,60`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }) + + // Act & Assert + await expect(async () => { + await devicesLoader({ + request: request, + } as LoaderFunctionArgs) + }).rejects.toThrow() + }) + + it('should return 422 error on wrong format parameter', async () => { + // Arrange + const request = new Request(`${BASE_URL}?format=potato`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }) + + try { + await devicesLoader({ + request: request, + } as LoaderFunctionArgs) + expect(true).toBe(false) + } catch (error) { + expect(error).toBeInstanceOf(Response) + expect((error as Response).status).toBe(422) + + const errorData = await (error as Response).json() + expect(errorData.error).toBe('Invalid format parameter') + } + }) + + it('should return geojson format when requested', async () => { + // Arrange + const request = new Request(`${BASE_URL}?format=geojson`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }) + + // Act + const geojsonData: any = await devicesLoader({ + request: request, + } as LoaderFunctionArgs) + + expect(geojsonData).toBeDefined() + if (geojsonData) { + // Assert - this should always be GeoJSON since that's what the loader returns + expect(geojsonData.type).toBe('FeatureCollection') + expect(Array.isArray(geojsonData.features)).toBe(true) + + if (geojsonData.features.length > 0) { + expect(geojsonData.features[0].type).toBe('Feature') + expect(geojsonData.features[0].geometry).toBeDefined() + // @ts-ignore + expect(geojsonData.features[0].geometry.coordinates[0]).toBeDefined() + // @ts-ignore + expect(geojsonData.features[0].geometry.coordinates[1]).toBeDefined() + expect(geojsonData.features[0].properties).toBeDefined() + } + } + }) }) describe('POST', () => { @@ -567,6 +616,252 @@ describe('openSenseMap API Routes: /boxes', () => { }) }) + describe('PUT', () => { + it('should allow to update the device via PUT', async () => { + const update_payload = { + name: 'neuername', + exposure: 'indoor', + grouptag: 'testgroup', + description: 'total neue beschreibung', + location: { lat: 54.2, lng: 21.1 }, + weblink: 'http://www.google.de', + image: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=', + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + + const data = await response.json() + + expect(data.name).toBe(update_payload.name) + expect(data.exposure).toBe(update_payload.exposure) + expect(Array.isArray(data.grouptag)).toBe(true) + expect(data.grouptag).toContain(update_payload.grouptag) + expect(data.description).toBe(update_payload.description) + + expect(data.currentLocation).toEqual({ + type: 'Point', + coordinates: [ + update_payload.location.lng, + update_payload.location.lat, + ], + timestamp: expect.any(String), + }) + + expect(data.loc).toEqual([ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ + update_payload.location.lng, + update_payload.location.lat, + ], + timestamp: expect.any(String), + }, + }, + ]) + }) + + it('should allow to update the device via PUT with array as grouptags', async () => { + const update_payload = { + name: 'neuername', + exposure: 'outdoor', + grouptag: ['testgroup'], + description: 'total neue beschreibung', + location: { lat: 54.2, lng: 21.1 }, + weblink: 'http://www.google.de', + image: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=', + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response: any = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + + const data = await response.json() + expect(data.name).toBe(update_payload.name) + expect(data.exposure).toBe(update_payload.exposure) + + expect(Array.isArray(data.grouptag)).toBe(true) + expect(data.grouptag).toEqual(update_payload.grouptag) + + expect(data.description).toBe(update_payload.description) + expect(data.currentLocation.coordinates).toEqual([ + update_payload.location.lng, + update_payload.location.lat, + ]) + expect(data.loc[0].geometry.coordinates).toEqual([ + update_payload.location.lng, + update_payload.location.lat, + ]) + + //TODO: this fails, check if we actually need timestamps in images + // const parts = data.image.split('_') + // const ts36 = parts[1].replace('.png', '') + // const tsMs = parseInt(ts36, 36) * 1000 + // expect(Date.now() - tsMs).toBeLessThan(1000) + }) + it('should remove image when deleteImage=true', async () => { + const update_payload = { + deleteImage: true, + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + const data = await response.json() + expect(data.image).toBeNull() + }) + + it('should nullify description when set to empty string', async () => { + const update_payload = { + description: '', + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + const data = await response.json() + expect(data.description).toBeNull() + }) + + it('should clear group tags when empty array provided', async () => { + const update_payload = { + grouptag: [], + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + const data = await response.json() + expect(data.grouptag).toHaveLength(0) + }) + + it('should merge addons.add into grouptags', async () => { + const update_payload = { + addons: { add: 'feinstaub' }, + grouptag: ['existinggroup'], + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + const data = await response.json() + + expect(Array.isArray(data.grouptag)).toBe(true) + expect(data.grouptag).toContain('existinggroup') + expect(data.grouptag).toContain('feinstaub') + }) + + it('should accept multi-valued grouptag array', async () => { + const update_payload = { + grouptag: ['tag1', 'tag2', 'tag3'], + } + + const request = new Request(`${BASE_URL}/${queryableDevice?.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify(update_payload), + }) + + const response = await deviceUpdateAction({ + request, + params: { deviceId: queryableDevice?.id }, + context: {} as AppLoadContext, + } as ActionFunctionArgs) + + expect(response.status).toBe(200) + const data = await response.json() + expect(data.grouptag).toEqual( + expect.arrayContaining(['tag1', 'tag2', 'tag3']), + ) + }) + }) + describe('DELETE', () => { let deletableDevice: Device | null = null diff --git a/tests/routes/api.location.spec.ts b/tests/routes/api.location.spec.ts index abb48c3b..ec572df8 100644 --- a/tests/routes/api.location.spec.ts +++ b/tests/routes/api.location.spec.ts @@ -22,7 +22,6 @@ const TEST_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [ diff --git a/tests/routes/api.measurements.spec.ts b/tests/routes/api.measurements.spec.ts index 51a7af64..7414ba8c 100644 --- a/tests/routes/api.measurements.spec.ts +++ b/tests/routes/api.measurements.spec.ts @@ -22,7 +22,7 @@ const TEST_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', + //model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [ diff --git a/tests/routes/api.tags.spec.ts b/tests/routes/api.tags.spec.ts index 6617bcb9..c7c0ab93 100644 --- a/tests/routes/api.tags.spec.ts +++ b/tests/routes/api.tags.spec.ts @@ -12,7 +12,7 @@ const TEST_TAG_BOX = { name: `'${TAGS_TEST_USER.name}'s Box`, exposure: 'outdoor', expiresAt: null, - tags: ['tag1', 'tag2'], + tags: ['tag1', 'tag2', 'testgroup'], latitude: 0, longitude: 0, model: 'luftdaten.info', @@ -79,7 +79,7 @@ describe('openSenseMap API Routes: /tags', () => { 'application/json; charset=utf-8', ) expect(Array.isArray(body.data)).toBe(true) - expect(body.data).toHaveLength(2) + expect(body.data).toHaveLength(3) }) afterAll(async () => { diff --git a/tests/utils/measurement-server-helper.spec.ts b/tests/utils/measurement-server-helper.spec.ts index 8cdf8b02..de81788f 100644 --- a/tests/utils/measurement-server-helper.spec.ts +++ b/tests/utils/measurement-server-helper.spec.ts @@ -34,7 +34,7 @@ const DEVICE_SENSOR_ID_BOX = { tags: [], latitude: 0, longitude: 0, - model: 'luftdaten.info', + //model: 'luftdaten.info', mqttEnabled: false, ttnEnabled: false, sensors: [