Add plan execution mode, game ID, and version to health endpoint

- Add executionMode and gameId columns to StreamPlan schema
- Add migration for new columns
- Support executionMode/gameId in plan create and update endpoints
- Add version field to health check response from package.json
This commit is contained in:
2026-02-28 22:38:56 +01:00
parent e16eb85071
commit 02755bd1f0
6 changed files with 220 additions and 11 deletions

View File

@@ -1,8 +1,12 @@
import { FastifyPluginAsync } from 'fastify';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { version } = require('../../package.json');
const healthRoutes: FastifyPluginAsync = async (fastify) => {
fastify.get('/health', async () => {
return { status: 'ok', timestamp: new Date().toISOString() };
return { status: 'ok', timestamp: new Date().toISOString(), version };
});
};

View File

@@ -3,13 +3,15 @@ import { requireAuth } from '../../middleware/require-auth.js';
import { AppError } from '../../plugins/error-handler.js';
import { getYouTubeBroadcastStatus, refreshYouTubeToken } from '../../services/youtube.service.js';
import { decrypt, encrypt } from '../../services/crypto.service.js';
import type { CreateStreamPlanBody, StreamPlanResponse, StreamDestinationResponse } from '../../types/api.js';
import type { CreateStreamPlanBody, UpdateStreamPlanBody, StreamPlanResponse, StreamDestinationResponse } from '../../types/api.js';
function formatPlan(plan: any): StreamPlanResponse {
return {
id: plan.id,
name: plan.name,
status: plan.status,
executionMode: plan.executionMode ?? 'IN_GAME',
gameId: plan.gameId ?? '',
createdAt: plan.createdAt.toISOString(),
updatedAt: plan.updatedAt.toISOString(),
destinations: (plan.destinations ?? []).map((d: any): StreamDestinationResponse => ({
@@ -113,6 +115,8 @@ const planRoutes: FastifyPluginAsync = async (fastify) => {
required: ['name', 'destinations'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 200 },
executionMode: { type: 'string', enum: ['IN_GAME', 'APP_STREAMING'] },
gameId: { type: 'string', maxLength: 200 },
destinations: {
type: 'array',
maxItems: 10,
@@ -135,7 +139,7 @@ const planRoutes: FastifyPluginAsync = async (fastify) => {
},
},
}, async (request, reply) => {
const { name, destinations } = request.body;
const { name, executionMode, gameId, destinations } = request.body;
// Verify user has the required linked accounts
const linkedAccounts = await fastify.prisma.linkedAccount.findMany({
@@ -170,6 +174,8 @@ const planRoutes: FastifyPluginAsync = async (fastify) => {
data: {
userId: request.userId,
name,
executionMode: executionMode ?? 'IN_GAME',
gameId: gameId ?? '',
destinations: {
create: resolvedDestinations.map((d) => {
const account = linkedAccountMap.get(d.linkedAccountId)!;
@@ -232,6 +238,98 @@ const planRoutes: FastifyPluginAsync = async (fastify) => {
await fastify.prisma.streamPlan.delete({ where: { id: plan.id } });
reply.status(200).send({ success: true });
});
// PUT /streams/plans/:id — update a DRAFT plan
fastify.put<{ Params: { id: string }; Body: UpdateStreamPlanBody }>('/streams/plans/:id', {
preHandler: [requireAuth],
schema: {
params: {
type: 'object',
required: ['id'],
properties: { id: { type: 'string' } },
},
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 200 },
executionMode: { type: 'string', enum: ['IN_GAME', 'APP_STREAMING'] },
gameId: { type: 'string', maxLength: 200 },
destinations: {
type: 'array',
maxItems: 10,
items: {
type: 'object',
required: ['linkedAccountId', 'title'],
properties: {
linkedAccountId: { type: 'string', minLength: 1 },
title: { type: 'string', minLength: 1, maxLength: 200 },
description: { type: 'string', maxLength: 5000 },
privacyStatus: { type: 'string', enum: ['public', 'unlisted', 'private'] },
gameId: { type: 'string', maxLength: 100 },
tags: { type: 'string', maxLength: 500 },
},
additionalProperties: false,
},
},
},
additionalProperties: false,
},
},
}, async (request, reply) => {
const plan = await fastify.prisma.streamPlan.findFirst({
where: { id: request.params.id, userId: request.userId },
include: { destinations: true },
});
if (!plan) throw new AppError(404, 'Stream plan not found');
if (plan.status !== 'DRAFT') throw new AppError(400, 'Only DRAFT plans can be edited');
const { name, executionMode, gameId, destinations } = request.body;
// If destinations provided, verify accounts and replace them
if (destinations) {
const linkedAccounts = await fastify.prisma.linkedAccount.findMany({
where: { userId: request.userId },
});
const linkedAccountMap = new Map(linkedAccounts.map((a) => [a.id, a]));
for (const dest of destinations) {
if (!linkedAccountMap.has(dest.linkedAccountId)) {
throw new AppError(400, `Linked account ${dest.linkedAccountId} not found`);
}
}
// Delete old destinations and create new ones
await fastify.prisma.streamDestination.deleteMany({ where: { planId: plan.id } });
for (const d of destinations) {
const account = linkedAccountMap.get(d.linkedAccountId)!;
await fastify.prisma.streamDestination.create({
data: {
planId: plan.id,
serviceId: account.serviceId,
linkedAccountId: d.linkedAccountId,
title: d.title,
description: d.description ?? '',
privacyStatus: d.privacyStatus ?? 'unlisted',
gameId: d.gameId ?? '',
tags: d.tags ?? '',
},
});
}
}
// Update plan fields
const updated = await fastify.prisma.streamPlan.update({
where: { id: plan.id },
data: {
...(name !== undefined && { name }),
...(executionMode !== undefined && { executionMode }),
...(gameId !== undefined && { gameId }),
},
include: { destinations: true },
});
reply.status(200).send(formatPlan(updated));
});
};
export default planRoutes;

View File

@@ -44,9 +44,18 @@ export interface LinkedAccountResponse {
// ── Streams ──────────────────────────────────────────────
export interface CreateStreamPlanBody {
name: string;
executionMode?: string;
gameId?: string;
destinations: CreateDestinationBody[];
}
export interface UpdateStreamPlanBody {
name?: string;
executionMode?: string;
gameId?: string;
destinations?: CreateDestinationBody[];
}
export interface CreateDestinationBody {
linkedAccountId: string;
title: string;
@@ -60,6 +69,8 @@ export interface StreamPlanResponse {
id: string;
name: string;
status: string;
executionMode: string;
gameId: string;
createdAt: string;
updatedAt: string;
destinations: StreamDestinationResponse[];