refactor(backend): simplify media schema and recording metadata
This commit is contained in:
@@ -3,7 +3,7 @@ import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { db } from '../db/client';
|
||||
import { deviceCommands, deviceLinks, devices } from '../db/schema';
|
||||
import { commands, deviceLinks, devices } from '../db/schema';
|
||||
import { simpleStreamingEnabled } from '../media/config';
|
||||
import { requireAuth } from '../middleware/auth';
|
||||
import { requireDeviceAuth } from '../middleware/device-auth';
|
||||
@@ -98,7 +98,7 @@ router.post('/', requireAuth, async (req, res) => {
|
||||
const now = new Date();
|
||||
|
||||
const [command] = await db
|
||||
.insert(deviceCommands)
|
||||
.insert(commands)
|
||||
.values({
|
||||
ownerUserId: authSession.user.id,
|
||||
sourceDeviceId: sourceDevice.id,
|
||||
@@ -118,7 +118,7 @@ router.post('/', requireAuth, async (req, res) => {
|
||||
|
||||
await dispatchCommandById(command.id);
|
||||
|
||||
const refreshed = await db.query.deviceCommands.findFirst({ where: eq(deviceCommands.id, command.id) });
|
||||
const refreshed = await db.query.commands.findFirst({ where: eq(commands.id, command.id) });
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Command queued',
|
||||
@@ -141,13 +141,13 @@ router.get('/', requireAuth, async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = await db.query.deviceCommands.findMany({
|
||||
where: eq(deviceCommands.ownerUserId, authSession.user.id),
|
||||
orderBy: [desc(deviceCommands.createdAt)],
|
||||
const commandResults = await db.query.commands.findMany({
|
||||
where: eq(commands.ownerUserId, authSession.user.id),
|
||||
orderBy: [desc(commands.createdAt)],
|
||||
limit: parsed.data.limit,
|
||||
});
|
||||
|
||||
const filtered = commands.filter((command) => {
|
||||
const filtered = commandResults.filter((command) => {
|
||||
if (parsed.data.sourceDeviceId && command.sourceDeviceId !== parsed.data.sourceDeviceId) {
|
||||
return false;
|
||||
}
|
||||
@@ -187,8 +187,8 @@ router.post('/:commandId/ack', requireDeviceAuth, async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = await db.query.deviceCommands.findFirst({
|
||||
where: eq(deviceCommands.id, parsedParams.data.commandId),
|
||||
const command = await db.query.commands.findFirst({
|
||||
where: eq(commands.id, parsedParams.data.commandId),
|
||||
});
|
||||
|
||||
if (!command) {
|
||||
@@ -204,14 +204,14 @@ router.post('/:commandId/ack', requireDeviceAuth, async (req, res) => {
|
||||
const now = new Date();
|
||||
|
||||
const [updated] = await db
|
||||
.update(deviceCommands)
|
||||
.update(commands)
|
||||
.set({
|
||||
status: parsed.data.status,
|
||||
acknowledgedAt: now,
|
||||
updatedAt: now,
|
||||
error: parsed.data.status === 'rejected' ? parsed.data.error ?? 'rejected' : null,
|
||||
})
|
||||
.where(eq(deviceCommands.id, command.id))
|
||||
.where(eq(commands.id, command.id))
|
||||
.returning();
|
||||
|
||||
res.json({ message: 'Command acknowledged', command: updated });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { db } from '../db/client';
|
||||
import { pushNotifications } from '../db/schema';
|
||||
import { notificationDeliveries } from '../db/schema';
|
||||
import { requireDeviceAuth } from '../middleware/device-auth';
|
||||
import { dispatchPushQueueOnce } from '../services/push';
|
||||
|
||||
@@ -33,12 +33,12 @@ router.get('/me', requireDeviceAuth, async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await db.query.pushNotifications.findMany({
|
||||
const result = await db.query.notificationDeliveries.findMany({
|
||||
where: and(
|
||||
eq(pushNotifications.ownerUserId, deviceAuth.userId),
|
||||
eq(pushNotifications.recipientDeviceId, deviceAuth.deviceId),
|
||||
eq(notificationDeliveries.ownerUserId, deviceAuth.userId),
|
||||
eq(notificationDeliveries.recipientDeviceId, deviceAuth.deviceId),
|
||||
),
|
||||
orderBy: [desc(pushNotifications.createdAt)],
|
||||
orderBy: [desc(notificationDeliveries.createdAt)],
|
||||
limit: parsed.data.limit,
|
||||
});
|
||||
|
||||
@@ -63,16 +63,16 @@ router.post('/:notificationId/read', requireDeviceAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(pushNotifications)
|
||||
.update(notificationDeliveries)
|
||||
.set({
|
||||
status: 'read',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(pushNotifications.id, parsedParams.data.notificationId),
|
||||
eq(pushNotifications.ownerUserId, deviceAuth.userId),
|
||||
eq(pushNotifications.recipientDeviceId, deviceAuth.deviceId),
|
||||
eq(notificationDeliveries.id, parsedParams.data.notificationId),
|
||||
eq(notificationDeliveries.ownerUserId, deviceAuth.userId),
|
||||
eq(notificationDeliveries.recipientDeviceId, deviceAuth.deviceId),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
|
||||
@@ -180,13 +180,6 @@ router.get('/:recordingId/download-url', requireDeviceAuth, async (req, res) =>
|
||||
return;
|
||||
}
|
||||
|
||||
const canAccess = recording.requesterDeviceId === deviceAuth.deviceId || recording.cameraDeviceId === deviceAuth.deviceId;
|
||||
|
||||
if (!canAccess) {
|
||||
res.status(403).json({ message: 'Device cannot access this recording' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (recording.status !== 'ready' || !recording.objectKey || !recording.bucket) {
|
||||
res.status(409).json({ message: 'Recording is not available yet' });
|
||||
return;
|
||||
@@ -228,7 +221,6 @@ router.get('/:recordingId/download-url', requireDeviceAuth, async (req, res) =>
|
||||
});
|
||||
});
|
||||
|
||||
// Internal helper used by stream lifecycle to create recording placeholder rows.
|
||||
export const createRecordingForStream = async (streamSessionId: string): Promise<void> => {
|
||||
const stream = await db.query.streamSessions.findFirst({ where: eq(streamSessions.id, streamSessionId) });
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from 'zod';
|
||||
|
||||
import { db } from '../db/client';
|
||||
import { mediaMode, simpleStreamingEnabled, streamRecordingEnabled } from '../media/config';
|
||||
import { deviceCommands, deviceLinks, devices, streamSessions } from '../db/schema';
|
||||
import { commands, deviceLinks, devices, streamSessions } from '../db/schema';
|
||||
import { createLiveMediaSession, mediaProvider } from '../media/service';
|
||||
import { sfuService } from '../media/sfu/service';
|
||||
import { requireDeviceAuth } from '../middleware/device-auth';
|
||||
@@ -204,7 +204,7 @@ router.post('/request', requireDeviceAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const [command] = await db
|
||||
.insert(deviceCommands)
|
||||
.insert(commands)
|
||||
.values({
|
||||
ownerUserId: deviceAuth.userId,
|
||||
sourceDeviceId: sourceDevice.id,
|
||||
@@ -235,7 +235,7 @@ router.post('/request', requireDeviceAuth, async (req, res) => {
|
||||
commandId: command.id,
|
||||
});
|
||||
|
||||
const refreshedCommand = await db.query.deviceCommands.findFirst({ where: eq(deviceCommands.id, command.id) });
|
||||
const refreshedCommand = await db.query.commands.findFirst({ where: eq(commands.id, command.id) });
|
||||
|
||||
const deliveredToRequester = sendRealtimeToDevice(sourceDevice.id, 'stream:requested', {
|
||||
streamSessionId: session.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { and, eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { db } from '../db/client';
|
||||
import { devices, videos } from '../db/schema';
|
||||
import { devices, events, recordings } from '../db/schema';
|
||||
import { requireAuth } from '../middleware/auth';
|
||||
import {
|
||||
ensureMinioBucket,
|
||||
@@ -18,6 +18,8 @@ const uploadUrlSchema = z.object({
|
||||
fileName: z.string().trim().min(1).max(255),
|
||||
deviceId: z.string().uuid(),
|
||||
prefix: z.string().trim().optional(),
|
||||
recordingId: z.string().uuid().optional(),
|
||||
eventId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
const downloadUrlSchema = z.object({
|
||||
@@ -67,44 +69,81 @@ router.post('/upload-url', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.data.eventId) {
|
||||
const event = await db.query.events.findFirst({
|
||||
where: and(eq(events.id, parsed.data.eventId), eq(events.userId, authSession.user.id)),
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
res.status(400).json({ message: 'Invalid eventId for this user' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const objectKey = buildObjectKey(authSession.user.id, parsed.data.fileName, parsed.data.prefix);
|
||||
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
|
||||
|
||||
const [videoRecord] = await db
|
||||
.insert(videos)
|
||||
.values({
|
||||
userId: authSession.user.id,
|
||||
deviceId: parsed.data.deviceId,
|
||||
objectKey,
|
||||
bucket: minioBucket,
|
||||
uploadUrl,
|
||||
status: 'upload_link_sent',
|
||||
expiresAt,
|
||||
updatedAt: now,
|
||||
})
|
||||
.returning({
|
||||
id: videos.id,
|
||||
objectKey: videos.objectKey,
|
||||
bucket: videos.bucket,
|
||||
status: videos.status,
|
||||
createdAt: videos.createdAt,
|
||||
expiresAt: videos.expiresAt,
|
||||
let persistedRecording;
|
||||
|
||||
if (parsed.data.recordingId) {
|
||||
const existingRecording = await db.query.recordings.findFirst({
|
||||
where: and(eq(recordings.id, parsed.data.recordingId), eq(recordings.ownerUserId, authSession.user.id)),
|
||||
});
|
||||
|
||||
if (!videoRecord) {
|
||||
res.status(500).json({ message: 'Unable to persist video metadata' });
|
||||
if (!existingRecording) {
|
||||
res.status(404).json({ message: 'Recording not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
[persistedRecording] = await db
|
||||
.update(recordings)
|
||||
.set({
|
||||
objectKey,
|
||||
bucket: minioBucket,
|
||||
status: 'awaiting_upload',
|
||||
updatedAt: now,
|
||||
error: null,
|
||||
})
|
||||
.where(eq(recordings.id, existingRecording.id))
|
||||
.returning();
|
||||
} else {
|
||||
[persistedRecording] = await db
|
||||
.insert(recordings)
|
||||
.values({
|
||||
ownerUserId: authSession.user.id,
|
||||
cameraDeviceId: parsed.data.deviceId,
|
||||
requesterDeviceId: null,
|
||||
eventId: parsed.data.eventId ?? null,
|
||||
objectKey,
|
||||
bucket: minioBucket,
|
||||
status: 'awaiting_upload',
|
||||
updatedAt: now,
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
if (!persistedRecording) {
|
||||
res.status(500).json({ message: 'Unable to persist recording metadata' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Dummy upload URL generated',
|
||||
message: 'Upload URL generated',
|
||||
bucket: minioBucket,
|
||||
objectKey,
|
||||
uploadUrl,
|
||||
expiresInSeconds: minioPresignedExpirySeconds,
|
||||
video: videoRecord,
|
||||
video: {
|
||||
id: persistedRecording.id,
|
||||
objectKey: persistedRecording.objectKey,
|
||||
bucket: persistedRecording.bucket,
|
||||
status: persistedRecording.status,
|
||||
createdAt: persistedRecording.createdAt,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user