diff --git a/app/components/map/layers/cluster/box-marker.tsx b/app/components/map/layers/cluster/box-marker.tsx index 582df870..6e8b4751 100644 --- a/app/components/map/layers/cluster/box-marker.tsx +++ b/app/components/map/layers/cluster/box-marker.tsx @@ -4,6 +4,7 @@ import { useState } from 'react' import { type MarkerProps, Marker, useMap } from 'react-map-gl' import { useMatches, useNavigate, useSearchParams } from 'react-router' import { useGlobalCompareMode } from '~/components/device-detail/useGlobalCompareMode' +import { validLngLat } from '~/lib/location' import { cn } from '~/lib/utils' import { type Device } from '~/schema' @@ -51,6 +52,8 @@ export default function BoxMarker({ device, ...props }: BoxMarkerProps) { return 0 } + if (!validLngLat(props.longitude, props.latitude)) return null + return ( { + // the value range for lat is [-90, 90] and for longitude [-180, 180) + return lat >= -90 && lat <= 90 && lng >= -180 && lng < 180 +} diff --git a/app/lib/measurement-service.server.ts b/app/lib/measurement-service.server.ts index d46af144..6f1a6468 100644 --- a/app/lib/measurement-service.server.ts +++ b/app/lib/measurement-service.server.ts @@ -1,19 +1,34 @@ -import { decodeMeasurements, hasDecoder } from "~/lib/decoding-service.server"; -import { type DeviceWithoutSensors, getDeviceWithoutSensors, getDevice, findAccessToken } from "~/models/device.server"; -import { saveMeasurements } from "~/models/measurement.server"; -import { getSensorsWithLastMeasurement, getSensorWithLastMeasurement } from "~/models/sensor.server"; -import { type SensorWithLatestMeasurement } from "~/schema"; - -export type DeviceWithSensors = DeviceWithoutSensors & {sensors: SensorWithLatestMeasurement[]} - -export async function getLatestMeasurementsForSensor(boxId: string, sensorId: string, count?: number): - Promise { - - const device: DeviceWithoutSensors = await getDeviceWithoutSensors({ id: boxId }); - if (!device) return null; +import { validLngLat } from './location' +import { decodeMeasurements, hasDecoder } from '~/lib/decoding-service.server' +import { + type DeviceWithoutSensors, + getDeviceWithoutSensors, + getDevice, + findAccessToken, +} from '~/models/device.server' +import { saveMeasurements } from '~/models/measurement.server' +import { + getSensorsWithLastMeasurement, + getSensorWithLastMeasurement, +} from '~/models/sensor.server' +import { type SensorWithLatestMeasurement } from '~/schema' + +export type DeviceWithSensors = DeviceWithoutSensors & { + sensors: SensorWithLatestMeasurement[] +} - // single sensor, no need for having info about device - return await getSensorWithLastMeasurement(device.id, sensorId, count); +export async function getLatestMeasurementsForSensor( + boxId: string, + sensorId: string, + count?: number, +): Promise { + const device: DeviceWithoutSensors = await getDeviceWithoutSensors({ + id: boxId, + }) + if (!device) return null + + // single sensor, no need for having info about device + return await getSensorWithLastMeasurement(device.id, sensorId, count) } /** @@ -22,184 +37,217 @@ export async function getLatestMeasurementsForSensor(boxId: string, sensorId: st * @param sensorId * @param count */ -export async function getLatestMeasurements ( - boxId: string, - count?: number, +export async function getLatestMeasurements( + boxId: string, + count?: number, ): Promise { - const device: DeviceWithoutSensors = await getDeviceWithoutSensors({ id: boxId }); - if (!device) return null; - - const sensorsWithMeasurements = await getSensorsWithLastMeasurement( - device.id, count); - - const deviceWithSensors: DeviceWithSensors = device as DeviceWithSensors - deviceWithSensors.sensors = sensorsWithMeasurements; - return deviceWithSensors; -}; + const device: DeviceWithoutSensors = await getDeviceWithoutSensors({ + id: boxId, + }) + if (!device) return null + + const sensorsWithMeasurements = await getSensorsWithLastMeasurement( + device.id, + count, + ) + + const deviceWithSensors: DeviceWithSensors = device as DeviceWithSensors + deviceWithSensors.sensors = sensorsWithMeasurements + return deviceWithSensors +} interface PostMeasurementsOptions { - contentType: string; - luftdaten: boolean; - hackair: boolean; - authorization?: string | null; + contentType: string + luftdaten: boolean + hackair: boolean + authorization?: string | null } interface SingleMeasurementBody { - value: number; - createdAt?: string; - location?: [number, number, number] | { lat: number; lng: number; height?: number }; + value: number + createdAt?: string + location?: + | [number, number, number] + | { lat: number; lng: number; height?: number } } interface LocationData { - lng: number; - lat: number; - height?: number; + lng: number + lat: number + height?: number } -const normalizeLocation = (location: SingleMeasurementBody['location']): LocationData | null => { - if (!location) return null; - - if (Array.isArray(location)) { - if (location.length < 2) return null; - return { - lng: location[0], - lat: location[1], - height: location[2], - }; - } - - if (typeof location === 'object' && 'lat' in location && 'lng' in location) { - return { - lng: location.lng, - lat: location.lat, - height: location.height, - }; - } - - return null; -}; - -const validateLocationCoordinates = (loc: LocationData): boolean => { - return loc.lng >= -180 && loc.lng <= 180 && - loc.lat >= -90 && loc.lat <= 90; -}; +const normalizeLocation = ( + location: SingleMeasurementBody['location'], +): LocationData | null => { + if (!location) return null + + if (Array.isArray(location)) { + if (location.length < 2) return null + + return { + lng: normalizeLongitude(location[0]), + lat: location[1], + height: location[2], + } + } + + if (typeof location === 'object' && 'lat' in location && 'lng' in location) { + return { + lng: normalizeLongitude(location.lng), + lat: location.lat, + height: location.height, + } + } + + return null +} + +/** + * Longitude at +180 are mathematically equal to longitudes at -180 + * and are therefore normalized to -180 for consistency. + * @param lng The longitude value to normalize + * @returns A normalized longitude in the value range [-180, 180) + */ +const normalizeLongitude = (lng: number): number => + lng % 180 === 0 ? -180 : lng export const postNewMeasurements = async ( - deviceId: string, - body: any, - options: PostMeasurementsOptions, + deviceId: string, + body: any, + options: PostMeasurementsOptions, ): Promise => { - const { luftdaten, hackair, authorization } = options; - let { contentType } = options; - - if (hackair) { - contentType = "hackair"; - } else if (luftdaten) { - contentType = "luftdaten"; - } - - if (!hasDecoder(contentType)) { - throw new Error("UnsupportedMediaTypeError: Unsupported content-type."); - } - - const device = await getDevice({id: deviceId}); - if (!device) { - throw new Error("NotFoundError: Device not found"); - } - - if (device.useAuth) { - const deviceAccessToken = await findAccessToken(deviceId); - - if (deviceAccessToken?.token && deviceAccessToken.token !== authorization) { - const error = new Error("Device access token not valid!"); - error.name = "UnauthorizedError"; - throw error; - } - } - - const measurements = await decodeMeasurements(body, { - contentType, - sensors: device.sensors, - }); - - await saveMeasurements(device, measurements); -}; + const { luftdaten, hackair, authorization } = options + let { contentType } = options + + if (hackair) { + contentType = 'hackair' + } else if (luftdaten) { + contentType = 'luftdaten' + } + + if (!hasDecoder(contentType)) { + throw new Error('UnsupportedMediaTypeError: Unsupported content-type.') + } + + const device = await getDevice({ id: deviceId }) + if (!device) { + throw new Error('NotFoundError: Device not found') + } + + if (device.useAuth) { + const deviceAccessToken = await findAccessToken(deviceId) + + if (deviceAccessToken?.token && deviceAccessToken.token !== authorization) { + const error = new Error('Device access token not valid!') + error.name = 'UnauthorizedError' + throw error + } + } + + const measurements = await decodeMeasurements(body, { + contentType, + sensors: device.sensors, + }) + + for (const m of measurements) { + const locationData: LocationData | null = m.location ?? null + if (locationData && !validLngLat(locationData.lng, locationData.lat)) { + const error = new Error('Invalid location coordinates') + error.name = 'UnprocessableEntityError' + throw error + } + } + + await saveMeasurements(device, measurements) +} export const postSingleMeasurement = async ( - deviceId: string, - sensorId: string, - body: SingleMeasurementBody, - authorization?: string | null, + deviceId: string, + sensorId: string, + body: SingleMeasurementBody, + authorization?: string | null, ): Promise => { - try { - if (typeof body.value !== 'number' || isNaN(body.value)) { - const error = new Error("Invalid measurement value"); - error.name = "UnprocessableEntityError"; - throw error; - } - - const device = await getDevice({ id: deviceId }); - - if (!device) { - const error = new Error("Device not found"); - error.name = "NotFoundError"; - throw error; - } - - const sensor = device.sensors?.find((s: any) => s.id === sensorId); - if (!sensor) { - const error = new Error("Sensor not found on device"); - error.name = "NotFoundError"; - throw error; - } - - if (device.useAuth) { - const deviceAccessToken = await findAccessToken(deviceId); - - if (deviceAccessToken?.token && deviceAccessToken.token !== authorization) { - const error = new Error("Device access token not valid!"); - error.name = "UnauthorizedError"; - throw error; - } - } - - let timestamp: Date | undefined; - if (body.createdAt) { - timestamp = new Date(body.createdAt); - - if (isNaN(timestamp.getTime())) { - const error = new Error("Invalid timestamp format"); - error.name = "UnprocessableEntityError"; - throw error; - } - } - - let locationData: LocationData | null = null; - if (body.location) { - locationData = normalizeLocation(body.location); - - if (locationData && !validateLocationCoordinates(locationData)) { - const error = new Error("Invalid location coordinates"); - error.name = "UnprocessableEntityError"; - throw error; - } - } - - const measurements = [{ - sensor_id: sensorId, - value: body.value, - createdAt: timestamp, - location: locationData, - }]; - - await saveMeasurements(device, measurements); - } catch (error) { - if (error instanceof Error && - ['UnauthorizedError', 'NotFoundError', 'UnprocessableEntityError'].includes(error.name)) { - throw error; - } - - console.error('Error in postSingleMeasurement:', error); - throw error; - } -}; \ No newline at end of file + try { + if (typeof body.value !== 'number' || isNaN(body.value)) { + const error = new Error('Invalid measurement value') + error.name = 'UnprocessableEntityError' + throw error + } + + const device = await getDevice({ id: deviceId }) + + if (!device) { + const error = new Error('Device not found') + error.name = 'NotFoundError' + throw error + } + + const sensor = device.sensors?.find((s: any) => s.id === sensorId) + if (!sensor) { + const error = new Error('Sensor not found on device') + error.name = 'NotFoundError' + throw error + } + + if (device.useAuth) { + const deviceAccessToken = await findAccessToken(deviceId) + + if ( + deviceAccessToken?.token && + deviceAccessToken.token !== authorization + ) { + const error = new Error('Device access token not valid!') + error.name = 'UnauthorizedError' + throw error + } + } + + let timestamp: Date | undefined + if (body.createdAt) { + timestamp = new Date(body.createdAt) + + if (isNaN(timestamp.getTime())) { + const error = new Error('Invalid timestamp format') + error.name = 'UnprocessableEntityError' + throw error + } + } + + let locationData: LocationData | null = null + if (body.location) { + locationData = normalizeLocation(body.location) + + if (locationData && !validLngLat(locationData.lng, locationData.lat)) { + const error = new Error('Invalid location coordinates') + error.name = 'UnprocessableEntityError' + throw error + } + } + + const measurements = [ + { + sensor_id: sensorId, + value: body.value, + createdAt: timestamp, + location: locationData, + }, + ] + + await saveMeasurements(device, measurements) + } catch (error) { + if ( + error instanceof Error && + [ + 'UnauthorizedError', + 'NotFoundError', + 'UnprocessableEntityError', + ].includes(error.name) + ) { + throw error + } + + console.error('Error in postSingleMeasurement:', error) + throw error + } +} diff --git a/app/schema/location.ts b/app/schema/location.ts index e2be8f04..0cda81f9 100644 --- a/app/schema/location.ts +++ b/app/schema/location.ts @@ -1,4 +1,4 @@ -import { relations } from 'drizzle-orm' +import { relations, sql } from 'drizzle-orm' import { bigserial, geometry, @@ -21,10 +21,15 @@ export const location = pgTable( srid: 4326, }).notNull(), }, - (t) => ({ - locationIndex: index('location_index').using('gist', t.location), - unique_location: unique().on(t.location), - }), + (t) => [ + index('location_index').using('gist', t.location), + unique().on(t.location), + sql`CONSTRAINT check_location CHECK ( + ST_X(${t.location}) >= -180 AND + ST_X(${t.location}) < 180 AND + ST_Y(${t.location}) BETWEEN -90 AND 90 + )`, + ], ) /** diff --git a/app/utils.ts b/app/utils.ts index 6bdac5d2..869074cf 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,32 +1,33 @@ -import moment from "moment"; -import { useMemo } from "react"; -import { useMatches } from "react-router"; +import moment from 'moment' +import { useMemo } from 'react' +import { useMatches } from 'react-router' +import { validLngLat } from './lib/location' import { - validateUsername, - validateEmail as validateEmailNew, -} from "./lib/user-service"; -import { type MyBadge } from "./models/badge.server"; -import { type User } from "./schema/user"; + validateUsername, + validateEmail as validateEmailNew, +} from './lib/user-service' +import { type MyBadge } from './models/badge.server' +import { type User } from './schema/user' -const DEFAULT_REDIRECT = "/"; +const DEFAULT_REDIRECT = '/' export interface BadgeClass { - entityType: string; - entityId: string; - openBadgeId: string; - createdAt: string; - createdBy: string; - issuer: string; - issuerOpenBadgeId: string; - name: string; - image: string; - description: string; - criteriaUrl: string | null; - criteriaNarrative: string; - alignments: any[]; - tags: any[]; - expires?: any; - extensions?: any; + entityType: string + entityId: string + openBadgeId: string + createdAt: string + createdBy: string + issuer: string + issuerOpenBadgeId: string + name: string + image: string + description: string + criteriaUrl: string | null + criteriaNarrative: string + alignments: any[] + tags: any[] + expires?: any + extensions?: any } /** @@ -37,18 +38,18 @@ export interface BadgeClass { * @param {string} defaultRedirect The redirect to use if the to is unsafe. */ export function safeRedirect( - to: FormDataEntryValue | string | null | undefined, - defaultRedirect: string = DEFAULT_REDIRECT, + to: FormDataEntryValue | string | null | undefined, + defaultRedirect: string = DEFAULT_REDIRECT, ) { - if (!to || typeof to !== "string") { - return defaultRedirect; - } + if (!to || typeof to !== 'string') { + return defaultRedirect + } - if (!to.startsWith("/") || to.startsWith("//")) { - return defaultRedirect; - } + if (!to.startsWith('/') || to.startsWith('//')) { + return defaultRedirect + } - return to; + return to } /** @@ -56,8 +57,8 @@ export function safeRedirect( * @deprecated Use {@link validateEmailNew} instead */ export function validateEmail(email: unknown): email is string { - if (typeof email !== "string") return false; - return validateEmailNew(email).isValid; + if (typeof email !== 'string') return false + return validateEmailNew(email).isValid } /** @@ -65,101 +66,109 @@ export function validateEmail(email: unknown): email is string { * @deprecated Use {@link validateUsername} instead */ export function validateName(name: string) { - if (name.length === 0) { - return { isValid: false, errorMsg: "username_required" }; - } else if (name.length < 4) { - return { isValid: false, errorMsg: "username_min_characters" }; - } else if ( - name && - !/^[a-zA-Z0-9][a-zA-Z0-9\s._-]+[a-zA-Z0-9-_.]$/.test(name.toString()) - ) { - return { isValid: false, errorMsg: "username_invalid" }; - } + if (name.length === 0) { + return { isValid: false, errorMsg: 'username_required' } + } else if (name.length < 4) { + return { isValid: false, errorMsg: 'username_min_characters' } + } else if ( + name && + !/^[a-zA-Z0-9][a-zA-Z0-9\s._-]+[a-zA-Z0-9-_.]$/.test(name.toString()) + ) { + return { isValid: false, errorMsg: 'username_invalid' } + } - return { isValid: true }; + return { isValid: true } } //* validate passwords type (changePassword page) export function validatePassType(passwords: any) { - const index = passwords.findIndex( - (password: any) => typeof password !== "string" || password.length === 0, - ); - return { isValid: index == -1 ? true : false, index: index }; + const index = passwords.findIndex( + (password: any) => typeof password !== 'string' || password.length === 0, + ) + return { isValid: index == -1 ? true : false, index: index } } //* validate passwords length (changePassword page) export function validatePassLength(passwords: any) { - const index = passwords.findIndex((password: any) => password.length < 8); - return { isValid: index == -1 ? true : false, index: index }; + const index = passwords.findIndex((password: any) => password.length < 8) + return { isValid: index == -1 ? true : false, index: index } } export function getFilteredDevices( - devices: any, - filterParams: URLSearchParams, + devices: any, + filterParams: URLSearchParams, ) { - const statusFilter = filterParams.get("status")?.toLowerCase().split(",") || [ - "all", - ]; - const exposureFilter = filterParams - .get("exposure") - ?.toLowerCase() - .split(",") || ["all"]; - const phenomenonList = filterParams - .get("phenomenon") - ?.toLowerCase() - .split(","); - const tagsFilter = filterParams.get("tags")?.toLowerCase().split(",") || []; - let results = devices.features.filter((device: any) => { - const sensorsList = device.properties.sensors?.map((s: any) => - s.title.toLowerCase(), - ); - const deviceTags = - device.properties.tags?.map((tag: string) => tag.toLowerCase()) || []; // Convert device tags to lowercase + const statusFilter = filterParams.get('status')?.toLowerCase().split(',') || [ + 'all', + ] + const exposureFilter = filterParams + .get('exposure') + ?.toLowerCase() + .split(',') || ['all'] + const phenomenonList = filterParams + .get('phenomenon') + ?.toLowerCase() + .split(',') + const tagsFilter = filterParams.get('tags')?.toLowerCase().split(',') || [] + const results = devices.features.filter((device: any) => { + // prevent data from invalid locations to be processed + if ( + 'latitude' in device.properties && + 'longitude' in device.properties && + !validLngLat(device.properties.longitude, device.properties.latitude) + ) + return false - return ( - // If "all" is selected, include all exposures; otherwise, check for matches - // If tags are provided, check if the device contains any of the selected tags - (exposureFilter.includes("all") || - exposureFilter.includes(device.properties.exposure.toLowerCase())) && - // If "all" is selected, include all statuses; otherwise, check for matches - (statusFilter.includes("all") || - statusFilter.includes(device.properties.status.toLowerCase())) && - // If phenomenon is provided, check if any sensor matches the selected phenomenon - (!filterParams.get("phenomenon") || - sensorsList.some((s: any) => phenomenonList?.includes(s))) && - (tagsFilter.length === 0 || - tagsFilter.some((tag) => deviceTags.includes(tag))) - ); - }); + const sensorsList = device.properties.sensors?.map((s: any) => + s.title.toLowerCase(), + ) + const deviceTags = + device.properties.tags?.map((tag: string) => tag.toLowerCase()) || [] // Convert device tags to lowercase - return { - type: "FeatureCollection", - features: results, - }; + return ( + // If "all" is selected, include all exposures; otherwise, check for matches + // If tags are provided, check if the device contains any of the selected tags + (exposureFilter.includes('all') || + exposureFilter.includes(device.properties.exposure.toLowerCase())) && + // If "all" is selected, include all statuses; otherwise, check for matches + (statusFilter.includes('all') || + statusFilter.includes(device.properties.status.toLowerCase())) && + // If phenomenon is provided, check if any sensor matches the selected phenomenon + (!filterParams.get('phenomenon') || + sensorsList.some((s: any) => phenomenonList?.includes(s))) && + (tagsFilter.length === 0 || + tagsFilter.some((tag) => deviceTags.includes(tag))) + ) + }) + + return { + type: 'FeatureCollection', + features: results, + } } //* Get Minute Formatted String - last sensor measurement update export function getMinuteFormattedString(lastMeasurementAt: string) { - const secondsAgo = moment().diff(moment(lastMeasurementAt), "seconds"); + const secondsAgo = moment().diff(moment(lastMeasurementAt), 'seconds') - if (secondsAgo === null || secondsAgo === undefined) { - return "-"; - } else { - if (secondsAgo < 120) { - return "now"; - } - return `${Math.floor(secondsAgo / 60)} minutes ago`; - } + if (secondsAgo === null || secondsAgo === undefined) { + return '-' + } else { + if (secondsAgo < 120) { + return 'now' + } + return `${Math.floor(secondsAgo / 60)} minutes ago` + } } export function diffFromCreateDate(DeviceCreatedAt: string) { - const createDate = moment(DeviceCreatedAt); - const yearsFromCreate = moment().diff(createDate, "years"); - return `Created ${ - yearsFromCreate === 0 - ? `${moment().diff(createDate, "days")} day(s)` - : `${yearsFromCreate} year` + (yearsFromCreate > 1 ? "s" : "") - } ago`; + const createDate = moment(DeviceCreatedAt) + const yearsFromCreate = moment().diff(createDate, 'years') + return `Created ${ + yearsFromCreate === 0 + ? `${moment().diff(createDate, 'days')} day(s)` + : `${yearsFromCreate} year` + (yearsFromCreate > 1 ? 's' : '') + } ago` } /** @@ -168,24 +177,24 @@ export function diffFromCreateDate(DeviceCreatedAt: string) { * @returns {MyBadge[]} - Array of unique, non-revoked badges. */ export function getUniqueActiveBadges(badges: MyBadge[]): MyBadge[] { - // Check if the badges array is empty - if (!badges) return []; - // Create a set to track unique badge class IDs - const uniqueBadgeClassIds = new Set(); + // Check if the badges array is empty + if (!badges) return [] + // Create a set to track unique badge class IDs + const uniqueBadgeClassIds = new Set() - // Filter the badges - return badges.filter((badge) => { - // Check if the badge is not revoked and has a unique badge class ID - if ( - !badge.revoked && - !uniqueBadgeClassIds.has(badge.badgeclassOpenBadgeId) - ) { - // Add the badge class ID to the set - uniqueBadgeClassIds.add(badge.badgeclassOpenBadgeId); - return true; - } - return false; - }); + // Filter the badges + return badges.filter((badge) => { + // Check if the badge is not revoked and has a unique badge class ID + if ( + !badge.revoked && + !uniqueBadgeClassIds.has(badge.badgeclassOpenBadgeId) + ) { + // Add the badge class ID to the set + uniqueBadgeClassIds.add(badge.badgeclassOpenBadgeId) + return true + } + return false + }) } /** @@ -195,28 +204,28 @@ export function getUniqueActiveBadges(badges: MyBadge[]): MyBadge[] { * @returns {BadgeClass[]} - Sorted array of badge classes. */ export function sortBadges( - allBadges: (BadgeClass | null)[], - ownedBadges: (MyBadge | null)[], + allBadges: (BadgeClass | null)[], + ownedBadges: (MyBadge | null)[], ): BadgeClass[] { - // Filter out null values from allBadges and ownedBadges - const validAllBadges = allBadges.filter( - (badge): badge is BadgeClass => badge !== null, - ); - const validOwnedBadges = ownedBadges.filter( - (badge): badge is MyBadge => badge !== null, - ); + // Filter out null values from allBadges and ownedBadges + const validAllBadges = allBadges.filter( + (badge): badge is BadgeClass => badge !== null, + ) + const validOwnedBadges = ownedBadges.filter( + (badge): badge is MyBadge => badge !== null, + ) - // Create a set of owned badge class IDs - const ownedBadgeClassIds = new Set( - validOwnedBadges.map((badge) => badge.badgeclassOpenBadgeId), - ); + // Create a set of owned badge class IDs + const ownedBadgeClassIds = new Set( + validOwnedBadges.map((badge) => badge.badgeclassOpenBadgeId), + ) - // Sort the badges such that owned badges come first - return validAllBadges.sort((a, b) => { - const aOwned = ownedBadgeClassIds.has(a.openBadgeId); - const bOwned = ownedBadgeClassIds.has(b.openBadgeId); - return aOwned === bOwned ? 0 : aOwned ? -1 : 1; - }); + // Sort the badges such that owned badges come first + return validAllBadges.sort((a, b) => { + const aOwned = ownedBadgeClassIds.has(a.openBadgeId) + const bOwned = ownedBadgeClassIds.has(b.openBadgeId) + return aOwned === bOwned ? 0 : aOwned ? -1 : 1 + }) } /** @@ -226,35 +235,35 @@ export function sortBadges( * @returns {JSON|undefined} The router data or undefined if not found */ export function useMatchesData( - id: string, + id: string, ): Record | undefined { - const matchingRoutes = useMatches(); - const route = useMemo( - () => matchingRoutes.find((route) => route.id === id), - [matchingRoutes, id], - ); + const matchingRoutes = useMatches() + const route = useMemo( + () => matchingRoutes.find((route) => route.id === id), + [matchingRoutes, id], + ) - return route?.data as Record; + return route?.data as Record } function isUser(user: any): user is User { - return user && typeof user === "object" && typeof user.email === "string"; + return user && typeof user === 'object' && typeof user.email === 'string' } export function useOptionalUser(): User | undefined { - const data = useMatchesData("root"); - if (!data || !isUser(data.user)) { - return undefined; - } - return data.user; + const data = useMatchesData('root') + if (!data || !isUser(data.user)) { + return undefined + } + return data.user } export function useUser(): User { - const maybeUser = useOptionalUser(); - if (!maybeUser) { - throw new Error( - "No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.", - ); - } - return maybeUser; + const maybeUser = useOptionalUser() + if (!maybeUser) { + throw new Error( + 'No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.', + ) + } + return maybeUser } diff --git a/drizzle/0023_check_location.sql b/drizzle/0023_check_location.sql new file mode 100644 index 00000000..d1e50dd8 --- /dev/null +++ b/drizzle/0023_check_location.sql @@ -0,0 +1,7 @@ +-- Custom SQL migration file, put your code below! -- +ALTER TABLE location + ADD CONSTRAINT check_location CHECK ( + ST_X(location) >= -180 AND + ST_X(location) < 180 AND + ST_Y(location) BETWEEN -90 AND 90 + ); \ No newline at end of file diff --git a/drizzle/meta/0023_snapshot.json b/drizzle/meta/0023_snapshot.json new file mode 100644 index 00000000..7b257b01 --- /dev/null +++ b/drizzle/meta/0023_snapshot.json @@ -0,0 +1,1263 @@ +{ + "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 diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 176578a1..2e48cd63 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1761122113831, "tag": "0022_odd_sugar_man", "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1765380754120, + "tag": "0023_check_location", + "breakpoints": true } ] } \ No newline at end of file