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:
@@ -0,0 +1,91 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"metaId" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"email" TEXT,
|
||||
"avatarUrl" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"refreshToken" TEXT NOT NULL,
|
||||
"expiresAt" DATETIME NOT NULL,
|
||||
"deviceInfo" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LinkedAccount" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"displayName" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"avatarUrl" TEXT,
|
||||
"accessTokenEnc" TEXT NOT NULL,
|
||||
"refreshTokenEnc" TEXT NOT NULL,
|
||||
"tokenExpiresAt" DATETIME NOT NULL,
|
||||
"accessTokenIv" TEXT NOT NULL,
|
||||
"refreshTokenIv" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "LinkedAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StreamPlan" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
||||
"executionMode" TEXT NOT NULL DEFAULT 'IN_GAME',
|
||||
"gameId" TEXT NOT NULL DEFAULT '',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "StreamPlan_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "StreamDestination" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"planId" TEXT NOT NULL,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"linkedAccountId" TEXT NOT NULL DEFAULT '',
|
||||
"title" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL DEFAULT '',
|
||||
"privacyStatus" TEXT NOT NULL DEFAULT 'public',
|
||||
"gameId" TEXT NOT NULL DEFAULT '',
|
||||
"tags" TEXT NOT NULL DEFAULT '',
|
||||
"rtmpUrl" TEXT NOT NULL DEFAULT '',
|
||||
"streamKey" TEXT NOT NULL DEFAULT '',
|
||||
"broadcastId" TEXT NOT NULL DEFAULT '',
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
CONSTRAINT "StreamDestination_planId_fkey" FOREIGN KEY ("planId") REFERENCES "StreamPlan" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_metaId_key" ON "User"("metaId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_refreshToken_key" ON "Session"("refreshToken");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LinkedAccount_userId_idx" ON "LinkedAccount"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LinkedAccount_userId_serviceId_accountId_key" ON "LinkedAccount"("userId", "serviceId", "accountId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StreamPlan_userId_idx" ON "StreamPlan"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "StreamDestination_planId_idx" ON "StreamDestination"("planId");
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
@@ -54,14 +54,16 @@ model LinkedAccount {
|
||||
}
|
||||
|
||||
model StreamPlan {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
status String @default("DRAFT")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
destinations StreamDestination[]
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
status String @default("DRAFT")
|
||||
executionMode String @default("IN_GAME")
|
||||
gameId String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
destinations StreamDestination[]
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user