From 4fa525d8dbd2079227e12ed04ab2537ecb8d3b44 Mon Sep 17 00:00:00 2001 From: Matiss Jurevics Date: Sun, 21 Dec 2025 15:50:00 +0000 Subject: [PATCH] feat(db): add phase1 device role, links, and commands schema --- Backend/db/schema.ts | 42 ++++++++++++++++++- Backend/docs/openapi.ts | 33 ++++++++------- Backend/drizzle/0006_steady_control_plane.sql | 40 ++++++++++++++++++ Backend/drizzle/meta/_journal.json | 9 +++- 4 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 Backend/drizzle/0006_steady_control_plane.sql diff --git a/Backend/db/schema.ts b/Backend/db/schema.ts index 801a783..3490fdf 100644 --- a/Backend/db/schema.ts +++ b/Backend/db/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, timestamp, uuid, varchar, text, boolean } from 'drizzle-orm/pg-core'; +import { pgTable, timestamp, uuid, varchar, text, boolean, integer, jsonb, unique } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: uuid('id').defaultRandom().primaryKey(), @@ -15,11 +15,49 @@ export const devices = pgTable('devices', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id').notNull().references(() => users.id), name: varchar('name', { length: 255 }), + role: varchar('role', { length: 32 }).default('client').notNull(), + platform: varchar('platform', { length: 32 }), + appVersion: varchar('app_version', { length: 64 }), + pushToken: text('push_token'), + status: varchar('status', { length: 32 }).default('offline').notNull(), isCamera: boolean('is_camera').default(false).notNull(), lastSeenAt: timestamp('last_seen_at', { withTimezone: true }), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }); +export const deviceLinks = pgTable( + 'device_links', + { + id: uuid('id').defaultRandom().primaryKey(), + ownerUserId: uuid('owner_user_id').notNull().references(() => users.id), + cameraDeviceId: uuid('camera_device_id').notNull().references(() => devices.id), + clientDeviceId: uuid('client_device_id').notNull().references(() => devices.id), + status: varchar('status', { length: 32 }).default('active').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + uniqueDevicePair: unique('device_links_camera_client_unique').on(table.cameraDeviceId, table.clientDeviceId), + }), +); + +export const deviceCommands = pgTable('device_commands', { + id: uuid('id').defaultRandom().primaryKey(), + ownerUserId: uuid('owner_user_id').notNull().references(() => users.id), + sourceDeviceId: uuid('source_device_id').notNull().references(() => devices.id), + targetDeviceId: uuid('target_device_id').notNull().references(() => devices.id), + commandType: varchar('command_type', { length: 64 }).notNull(), + payload: jsonb('payload').$type | null>().default(null), + status: varchar('status', { length: 32 }).default('queued').notNull(), + retryCount: integer('retry_count').default(0).notNull(), + lastDispatchedAt: timestamp('last_dispatched_at', { withTimezone: true }), + acknowledgedAt: timestamp('acknowledged_at', { withTimezone: true }), + error: text('error'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + export const events = pgTable('events', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id').notNull().references(() => users.id), @@ -99,6 +137,8 @@ export const verifications = pgTable('verification', { export const schema = { users, devices, + deviceLinks, + deviceCommands, events, videos, notifications, diff --git a/Backend/docs/openapi.ts b/Backend/docs/openapi.ts index 4ff3a30..faf18a5 100644 --- a/Backend/docs/openapi.ts +++ b/Backend/docs/openapi.ts @@ -405,8 +405,7 @@ registry.registerPath({ export function buildOpenApiDocument() { const generator = new OpenApiGeneratorV3(registry.definitions); - - return generator.generateDocument({ + const document = generator.generateDocument({ openapi: '3.0.3', info: { title: 'Backend API', @@ -419,19 +418,23 @@ export function buildOpenApiDocument() { { name: 'Videos', description: 'Authenticated video object operations' }, { name: 'Admin', description: 'Basic-auth protected admin operations' }, ], - components: { - securitySchemes: { - cookieAuth: { - type: 'apiKey', - in: 'cookie', - name: 'better-auth.session_token', - description: 'Better Auth session cookie', - }, - basicAuth: { - type: 'http', - scheme: 'basic', - }, + }); + + document.components = { + ...(document.components ?? {}), + securitySchemes: { + cookieAuth: { + type: 'apiKey', + in: 'cookie', + name: 'better-auth.session_token', + description: 'Better Auth session cookie', + }, + basicAuth: { + type: 'http', + scheme: 'basic', }, }, - }); + }; + + return document; } diff --git a/Backend/drizzle/0006_steady_control_plane.sql b/Backend/drizzle/0006_steady_control_plane.sql new file mode 100644 index 0000000..5508811 --- /dev/null +++ b/Backend/drizzle/0006_steady_control_plane.sql @@ -0,0 +1,40 @@ +ALTER TABLE "devices" ADD COLUMN "role" varchar(32) DEFAULT 'client' NOT NULL;--> statement-breakpoint +ALTER TABLE "devices" ADD COLUMN "platform" varchar(32);--> statement-breakpoint +ALTER TABLE "devices" ADD COLUMN "app_version" varchar(64);--> statement-breakpoint +ALTER TABLE "devices" ADD COLUMN "push_token" text;--> statement-breakpoint +ALTER TABLE "devices" ADD COLUMN "status" varchar(32) DEFAULT 'offline' NOT NULL;--> statement-breakpoint +ALTER TABLE "devices" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint +UPDATE "devices" SET "role" = CASE WHEN "is_camera" THEN 'camera' ELSE 'client' END;--> statement-breakpoint +CREATE TABLE "device_links" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "owner_user_id" uuid NOT NULL, + "camera_device_id" uuid NOT NULL, + "client_device_id" uuid NOT NULL, + "status" varchar(32) DEFAULT 'active' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "device_links_camera_client_unique" UNIQUE("camera_device_id","client_device_id") +); +--> statement-breakpoint +CREATE TABLE "device_commands" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "owner_user_id" uuid NOT NULL, + "source_device_id" uuid NOT NULL, + "target_device_id" uuid NOT NULL, + "command_type" varchar(64) NOT NULL, + "payload" jsonb DEFAULT 'null'::jsonb, + "status" varchar(32) DEFAULT 'queued' NOT NULL, + "retry_count" integer DEFAULT 0 NOT NULL, + "last_dispatched_at" timestamp with time zone, + "acknowledged_at" timestamp with time zone, + "error" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "device_links" ADD CONSTRAINT "device_links_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_links" ADD CONSTRAINT "device_links_camera_device_id_devices_id_fk" FOREIGN KEY ("camera_device_id") REFERENCES "public"."devices"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_links" ADD CONSTRAINT "device_links_client_device_id_devices_id_fk" FOREIGN KEY ("client_device_id") REFERENCES "public"."devices"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_commands" ADD CONSTRAINT "device_commands_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_commands" ADD CONSTRAINT "device_commands_source_device_id_devices_id_fk" FOREIGN KEY ("source_device_id") REFERENCES "public"."devices"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "device_commands" ADD CONSTRAINT "device_commands_target_device_id_devices_id_fk" FOREIGN KEY ("target_device_id") REFERENCES "public"."devices"("id") ON DELETE no action ON UPDATE no action; diff --git a/Backend/drizzle/meta/_journal.json b/Backend/drizzle/meta/_journal.json index 5393cc5..483e3cb 100644 --- a/Backend/drizzle/meta/_journal.json +++ b/Backend/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1770412956419, "tag": "0005_sudden_corsair", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1770413956419, + "tag": "0006_steady_control_plane", + "breakpoints": true } ] -} \ No newline at end of file +}