diff --git a/.env.example b/.env.example index 06069496..bdf9f7f8 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,9 @@ OSEM_API_URL="https://api.opensensemap.org/" DIRECTUS_URL="https://coelho.opensensemap.org" SENSORWIKI_API_URL="https://api.sensors.wiki/" +MQTT_SERVICE_URL="http://localhost:3001" +MQTT_SERVICE_KEY="dev-service-key-change-in-production" + MYBADGES_API_URL = "https://api.v2.mybadges.org/" MYBADGES_URL = "https://mybadges.org/" MYBADGES_SERVERADMIN_USERNAME = "" diff --git a/app/lib/env.ts b/app/lib/env.ts new file mode 100644 index 00000000..45f0646c --- /dev/null +++ b/app/lib/env.ts @@ -0,0 +1,6 @@ +import "dotenv/config" + +export const env = { + MQTT_SERVICE_URL: process.env.MQTT_SERVICE_URL!, + MQTT_SERVICE_KEY: process.env.MQTT_SERVICE_KEY!, +} \ No newline at end of file diff --git a/app/lib/mqttClient.ts b/app/lib/mqttClient.ts new file mode 100644 index 00000000..3e83016f --- /dev/null +++ b/app/lib/mqttClient.ts @@ -0,0 +1,104 @@ +import { env } from "./env" +import { setMqttIntegrationEnabled } from "~/models/integration.server" + +interface MqttClientResponse { + success: boolean + deviceId: string +} + +interface MqttStatusResponse { + deviceId: string + connected: boolean +} + +interface MqttHealthResponse { + status: string + connections: number + timestamp: string +} + +class MqttClient { + private baseUrl = env.MQTT_SERVICE_URL + private serviceKey = env.MQTT_SERVICE_KEY + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}` + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + 'x-service-key': this.serviceKey, + ...options.headers, + }, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ + error: 'Unknown error' + })) + throw new Error( + `MQTT Service error: ${response.status} - ${error.error || error.message || 'Unknown error'}` + ) + } + + return response.json() + } + + /** + * Connect a device to its MQTT broker + */ + async connectBox(params: { box_id: string }): Promise { + await setMqttIntegrationEnabled(params.box_id, true) + return this.request( + `/devices/${params.box_id}/connect`, + { method: 'POST' } + ) + } + + /** + * Disconnect a device from its MQTT broker + */ + async disconnectBox(params: { box_id: string }): Promise { + await setMqttIntegrationEnabled(params.box_id, false) + return this.request( + `/devices/${params.box_id}/disconnect`, + { method: 'POST' } + ) + } + + /** + * Reconnect a device (disconnect then connect with fresh config) + */ + async reconnectBox(params: { box_id: string }): Promise { + return this.request( + `/devices/${params.box_id}/reconnect`, + { method: 'POST' } + ) + } + + /** + * Get connection status for a device + */ + async getStatus(deviceId: string): Promise { + return this.request( + `/devices/${deviceId}/status`, + { method: 'GET' } + ) + } + + /** + * Get health status of the MQTT service + */ + async getHealth(): Promise { + return this.request( + '/health', + { method: 'GET' } + ) + } +} + +export const mqttClient = new MqttClient() \ No newline at end of file diff --git a/app/models/integration.server.ts b/app/models/integration.server.ts new file mode 100644 index 00000000..f80414a2 --- /dev/null +++ b/app/models/integration.server.ts @@ -0,0 +1,56 @@ +import { eq } from 'drizzle-orm' +import { drizzleClient } from '~/db.server' +import { mqttIntegration, deviceToIntegrations } from '~/schema' + +export async function setMqttIntegrationEnabled( + deviceId: string, + enabled: boolean, + ) { + await drizzleClient + .update(mqttIntegration) + .set({ enabled }) + .where(eq(mqttIntegration.deviceId, deviceId)) + } + +export async function getMqttIntegrationByDeviceId(deviceId: string) { + const [result] = await drizzleClient + .select({ + deviceId: deviceToIntegrations.deviceId, + integrationId: mqttIntegration.id, + enabled: mqttIntegration.enabled, + url: mqttIntegration.url, + topic: mqttIntegration.topic, + messageFormat: mqttIntegration.messageFormat, + decodeOptions: mqttIntegration.decodeOptions, + connectionOptions: mqttIntegration.connectionOptions, + }) + .from(deviceToIntegrations) + .innerJoin( + mqttIntegration, + eq(deviceToIntegrations.mqttIntegrationId, mqttIntegration.id), + ) + .where(eq(deviceToIntegrations.deviceId, deviceId)) + .limit(1) + + return result +} + +export async function getAllActiveMqttIntegrations() { + return await drizzleClient + .select({ + deviceId: deviceToIntegrations.deviceId, + integrationId: mqttIntegration.id, + enabled: mqttIntegration.enabled, + url: mqttIntegration.url, + topic: mqttIntegration.topic, + messageFormat: mqttIntegration.messageFormat, + decodeOptions: mqttIntegration.decodeOptions, + connectionOptions: mqttIntegration.connectionOptions, + }) + .from(deviceToIntegrations) + .innerJoin( + mqttIntegration, + eq(deviceToIntegrations.mqttIntegrationId, mqttIntegration.id), + ) + .where(eq(mqttIntegration.enabled, true)) +} diff --git a/app/routes/api.integrations.$deviceId.mqtt.ts b/app/routes/api.integrations.$deviceId.mqtt.ts new file mode 100644 index 00000000..62e6b8b2 --- /dev/null +++ b/app/routes/api.integrations.$deviceId.mqtt.ts @@ -0,0 +1,39 @@ +import { type LoaderFunctionArgs } from "react-router" +import { getMqttIntegrationByDeviceId } from "~/models/integration.server" +import { StandardResponse } from "~/utils/response-utils" + +export async function loader({ params, request }: LoaderFunctionArgs) { + try { + const deviceId = params.deviceId + + if (!deviceId) { + return StandardResponse.badRequest("Missing deviceId") + } + + const key = request.headers.get("x-service-key") + + if (key != process.env.MQTT_SERVICE_KEY){ + return StandardResponse.unauthorized("Key invalid, access denied.") + } + + const integration = await getMqttIntegrationByDeviceId(deviceId) + + if (!integration) { + return StandardResponse.notFound("MQTT integration not found") + } + + return Response.json({ + deviceId: integration.deviceId, + integrationId: integration.integrationId, + enabled: integration.enabled, + url: integration.url, + topic: integration.topic, + messageFormat: integration.messageFormat, + decodeOptions: integration.decodeOptions, + connectionOptions: integration.connectionOptions, + }) + } catch (err) { + console.error("Error fetching MQTT integration:", err) + return StandardResponse.internalServerError() + } +} diff --git a/app/routes/api.integrations.mqtt.active.ts b/app/routes/api.integrations.mqtt.active.ts new file mode 100644 index 00000000..054168a6 --- /dev/null +++ b/app/routes/api.integrations.mqtt.active.ts @@ -0,0 +1,31 @@ +import { type LoaderFunction } from 'react-router' +import { getAllActiveMqttIntegrations } from '~/models/integration.server' +import { StandardResponse } from '~/utils/response-utils' + +export const loader: LoaderFunction = async ({ request }) => { + try { + const key = request.headers.get("x-service-key") + + if (key != process.env.MQTT_SERVICE_KEY){ + return StandardResponse.unauthorized("Key invalid, access denied.") + } + + const integrations = await getAllActiveMqttIntegrations() + + const response = integrations.map((integration) => ({ + deviceId: integration.deviceId, + integrationId: integration.integrationId, + enabled: integration.enabled, + url: integration.url, + topic: integration.topic, + messageFormat: integration.messageFormat, + decodeOptions: integration.decodeOptions, + connectionOptions: integration.connectionOptions, + })) + + return Response.json(response) + } catch (err) { + console.error('Error fetching active MQTT integrations:', err) + return StandardResponse.internalServerError() + } +} diff --git a/app/routes/api.measurements.ingest.ts b/app/routes/api.measurements.ingest.ts new file mode 100644 index 00000000..06c9746f --- /dev/null +++ b/app/routes/api.measurements.ingest.ts @@ -0,0 +1,70 @@ +import { type ActionFunctionArgs } from 'react-router' +import { z } from 'zod' +import { getDevice } from '~/models/device.server' +import { saveMeasurements } from '~/models/measurement.server' +import { StandardResponse } from '~/utils/response-utils' + +const MeasurementSchema = z.object({ + sensor_id: z.string(), + value: z.number(), + createdAt: z.string().datetime(), + location: z + .object({ + lat: z.number(), + lng: z.number(), + altitude: z.number().optional(), + }) + .optional(), +}) + +const BatchMeasurementSchema = z.object({ + deviceId: z.string(), + measurements: z.array(MeasurementSchema), +}) + +export async function action({ request }: ActionFunctionArgs) { + try { + let body + try { + body = await request.json() + } catch (err) { + return StandardResponse.badRequest('Invalid JSON in request body') + } + + const validationResult = BatchMeasurementSchema.safeParse(body) + if (!validationResult.success) { + return StandardResponse.badRequest( + validationResult.error.errors[0].message, + ) + } + + const { deviceId, measurements: rawMeasurements } = validationResult.data + + const device = await getDevice({ id: deviceId }) + if (!device) { + return StandardResponse.notFound('Device not found') + } + + if (!device.sensors || device.sensors.length === 0) { + return StandardResponse.badRequest('Device has no sensors configured') + } + + const measurements = rawMeasurements.map((m) => ({ + sensor_id: m.sensor_id, + value: m.value, + createdAt: m.createdAt ? new Date(m.createdAt) : undefined, + location: m.location, + })) + + try { + await saveMeasurements(device, measurements) + } catch (saveErr) { + // Still return 202 + } + + return new Response(null, { status: 202 }) + } catch (err) { + if (err instanceof Response) throw err + return StandardResponse.internalServerError() + } +} diff --git a/app/schema/device.ts b/app/schema/device.ts index 006d2973..ba419b96 100644 --- a/app/schema/device.ts +++ b/app/schema/device.ts @@ -21,6 +21,7 @@ import { location } from './location' import { logEntry } from './log-entry' import { sensor } from './sensor' import { user } from './user' +import { deviceToIntegrations } from './integration' /** * Table @@ -84,6 +85,10 @@ export const deviceRelations = relations(device, ({ one, many }) => ({ sensors: many(sensor), locations: many(deviceToLocation), logEntries: many(logEntry), + integrations: one(deviceToIntegrations, { + fields: [device.id], + references: [deviceToIntegrations.deviceId], + }), })) // Many-to-many diff --git a/app/schema/enum.ts b/app/schema/enum.ts index da4eb094..cf452630 100644 --- a/app/schema/enum.ts +++ b/app/schema/enum.ts @@ -1,35 +1,52 @@ -import { pgEnum } from "drizzle-orm/pg-core"; -import { z } from "zod"; +import { pgEnum } from 'drizzle-orm/pg-core' +import { z } from 'zod' + +export const MqttMessageFormatEnum = pgEnum('message_format', [ + 'json', + 'csv', + 'application/json', + 'text/csv', + 'debug_plain', + '', +]) + +export const TtnProfileEnum = pgEnum('ttn_profile', [ + 'json', + 'debug', + 'sensebox/home', + 'lora-serialization', + 'cayenne-lpp', +]) // 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', + 'senseBox:Edu', + 'luftdaten.info', + 'Custom', +]) diff --git a/app/schema/index.ts b/app/schema/index.ts index 8099e8d5..a2d3c07e 100644 --- a/app/schema/index.ts +++ b/app/schema/index.ts @@ -1,14 +1,15 @@ -export * from "./device"; -export * from "./enum"; -export * from "./measurement"; -export * from "./password"; -export * from "./profile"; -export * from "./profile-image"; -export * from "./types"; -export * from "./sensor"; -export * from "./user"; -export * from "./location"; -export * from "./log-entry"; -export * from "./refreshToken"; -export * from "./claim"; -export * from "./accessToken"; +export * from './device' +export * from './enum' +export * from './measurement' +export * from './password' +export * from './profile' +export * from './profile-image' +export * from './types' +export * from './sensor' +export * from './user' +export * from './location' +export * from './log-entry' +export * from './integration' +export * from './refreshToken' +export * from './claim' +export * from './accessToken' diff --git a/app/schema/integration.ts b/app/schema/integration.ts new file mode 100644 index 00000000..8da70855 --- /dev/null +++ b/app/schema/integration.ts @@ -0,0 +1,86 @@ +import { createId } from '@paralleldrive/cuid2' +import { + boolean, + integer, + json, + pgTable, + primaryKey, + text, +} from 'drizzle-orm/pg-core' +import { MqttMessageFormatEnum, TtnProfileEnum } from './enum' +import { device } from './device' +import { relations, sql } from 'drizzle-orm' + +export const mqttIntegration = pgTable('mqtt_integration', { + id: text('id') + .primaryKey() + .notNull() + .$defaultFn(() => createId()), + enabled: boolean('enabled').default(false).notNull(), + url: text('url').notNull(), + topic: text('topic').notNull(), + messageFormat: MqttMessageFormatEnum('message_format') + .default('json') + .notNull(), + decodeOptions: json('decode_options'), + connectionOptions: json('connection_options'), + deviceId: text('device_id').references(() => device.id, { + onDelete: 'cascade', + }), +}) + +export const ttnIntegration = pgTable('ttn_integration', { + id: text('id') + .primaryKey() + .notNull() + .$defaultFn(() => createId()), + enabled: boolean('enabled').default(false).notNull(), + devId: text('dev_id').notNull(), + appId: text('app_id').notNull(), + port: integer('port'), + profile: TtnProfileEnum('profile').default('json').notNull(), + decodeOptions: json('decode_options') + .$type() + .default(sql`'{}'::json`), + deviceId: text('device_id').references(() => device.id, { + onDelete: 'cascade', + }), +}) + +export const deviceToIntegrations = pgTable( + 'device_to_integrations', + { + deviceId: text('device_id') + .notNull() + .references(() => device.id, { onDelete: 'cascade' }), + mqttIntegrationId: text('mqtt_integration_id').references( + () => mqttIntegration.id, + { + onDelete: 'set null', + }, + ), + ttnIntegrationId: text('ttn_integration_id').references( + () => ttnIntegration.id, + { + onDelete: 'set null', + }, + ), + }, + (t) => ({ + pk: primaryKey({ columns: [t.deviceId] }), + }), +) + +export const deviceToIntegrationsRelations = relations( + deviceToIntegrations, + ({ one }) => ({ + mqttIntegration: one(mqttIntegration, { + fields: [deviceToIntegrations.mqttIntegrationId], + references: [mqttIntegration.id], + }), + ttnIntegration: one(ttnIntegration, { + fields: [deviceToIntegrations.ttnIntegrationId], + references: [ttnIntegration.id], + }), + }), +) diff --git a/app/utils/env.server.ts b/app/utils/env.server.ts index bc67a6fb..272c8e81 100644 --- a/app/utils/env.server.ts +++ b/app/utils/env.server.ts @@ -17,6 +17,8 @@ const schema = z.object({ MYBADGES_ISSUERID_OSEM: z.string(), MYBADGES_CLIENT_ID: z.string(), MYBADGES_CLIENT_SECRET: z.string(), + MQTT_SERVICE_URL: z.string(), + MQTT_SERVICE_KEY: z.string() }); declare global { @@ -45,6 +47,7 @@ export function getEnv() { MYBADGES_API_URL: process.env.MYBADGES_API_URL, MYBADGES_URL: process.env.MYBADGES_URL, SENSORWIKI_API_URL: process.env.SENSORWIKI_API_URL, + MQTT_SERVICE_URL: process.env.MQTT_SERVICE_URL, }; } diff --git a/drizzle/0024_yummy_tony_stark.sql b/drizzle/0024_yummy_tony_stark.sql new file mode 100644 index 00000000..2b457b3c --- /dev/null +++ b/drizzle/0024_yummy_tony_stark.sql @@ -0,0 +1,36 @@ +CREATE TYPE "public"."message_format" AS ENUM('json', 'csv', 'application/json', 'text/csv', 'debug_plain', '');--> statement-breakpoint +CREATE TYPE "public"."ttn_profile" AS ENUM('json', 'debug', 'sensebox/home', 'lora-serialization', 'cayenne-lpp');--> statement-breakpoint +CREATE TABLE "device_to_integrations" ( + "device_id" text NOT NULL, + "mqtt_integration_id" text, + "ttn_integration_id" text, + CONSTRAINT "device_to_integrations_device_id_pk" PRIMARY KEY("device_id") +); +--> statement-breakpoint +CREATE TABLE "mqtt_integration" ( + "id" text PRIMARY KEY NOT NULL, + "enabled" boolean DEFAULT false NOT NULL, + "url" text NOT NULL, + "topic" text NOT NULL, + "message_format" "message_format" DEFAULT 'json' NOT NULL, + "decode_options" json, + "connection_options" json, + "device_id" text +); +--> statement-breakpoint +CREATE TABLE "ttn_integration" ( + "id" text PRIMARY KEY NOT NULL, + "enabled" boolean DEFAULT false NOT NULL, + "dev_id" text NOT NULL, + "app_id" text NOT NULL, + "port" integer, + "profile" "ttn_profile" DEFAULT 'json' NOT NULL, + "decode_options" json DEFAULT '{}'::json, + "device_id" text +); +--> statement-breakpoint +ALTER TABLE "device_to_integrations" ADD CONSTRAINT "device_to_integrations_device_id_device_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."device"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_to_integrations" ADD CONSTRAINT "device_to_integrations_mqtt_integration_id_mqtt_integration_id_fk" FOREIGN KEY ("mqtt_integration_id") REFERENCES "public"."mqtt_integration"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_to_integrations" ADD CONSTRAINT "device_to_integrations_ttn_integration_id_ttn_integration_id_fk" FOREIGN KEY ("ttn_integration_id") REFERENCES "public"."ttn_integration"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "mqtt_integration" ADD CONSTRAINT "mqtt_integration_device_id_device_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."device"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ttn_integration" ADD CONSTRAINT "ttn_integration_device_id_device_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."device"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0024_snapshot.json b/drizzle/meta/0024_snapshot.json new file mode 100644 index 00000000..0934bfaa --- /dev/null +++ b/drizzle/meta/0024_snapshot.json @@ -0,0 +1,1521 @@ +{ + "id": "b4bc2011-8fc4-4d19-9fde-edeede1726f2", + "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 + }, + "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.device_to_integrations": { + "name": "device_to_integrations", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mqtt_integration_id": { + "name": "mqtt_integration_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ttn_integration_id": { + "name": "ttn_integration_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_integrations_device_id_device_id_fk": { + "name": "device_to_integrations_device_id_device_id_fk", + "tableFrom": "device_to_integrations", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_to_integrations_mqtt_integration_id_mqtt_integration_id_fk": { + "name": "device_to_integrations_mqtt_integration_id_mqtt_integration_id_fk", + "tableFrom": "device_to_integrations", + "tableTo": "mqtt_integration", + "columnsFrom": [ + "mqtt_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "device_to_integrations_ttn_integration_id_ttn_integration_id_fk": { + "name": "device_to_integrations_ttn_integration_id_ttn_integration_id_fk", + "tableFrom": "device_to_integrations", + "tableTo": "ttn_integration", + "columnsFrom": [ + "ttn_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_integrations_device_id_pk": { + "name": "device_to_integrations_device_id_pk", + "columns": [ + "device_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mqtt_integration": { + "name": "mqtt_integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_format": { + "name": "message_format", + "type": "message_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'json'" + }, + "decode_options": { + "name": "decode_options", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "connection_options": { + "name": "connection_options", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mqtt_integration_device_id_device_id_fk": { + "name": "mqtt_integration_device_id_device_id_fk", + "tableFrom": "mqtt_integration", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ttn_integration": { + "name": "ttn_integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dev_id": { + "name": "dev_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "profile": { + "name": "profile", + "type": "ttn_profile", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'json'" + }, + "decode_options": { + "name": "decode_options", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'::json" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ttn_integration_device_id_device_id_fk": { + "name": "ttn_integration_device_id_device_id_fk", + "tableFrom": "ttn_integration", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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" + ] + }, + "public.message_format": { + "name": "message_format", + "schema": "public", + "values": [ + "json", + "csv", + "application/json", + "text/csv", + "debug_plain", + "" + ] + }, + "public.ttn_profile": { + "name": "ttn_profile", + "schema": "public", + "values": [ + "json", + "debug", + "sensebox/home", + "lora-serialization", + "cayenne-lpp" + ] + } + }, + "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..0d45d7c1 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": 1767465161290, + "tag": "0024_yummy_tony_stark", + "breakpoints": true } ] } \ No newline at end of file