Enhance Docker and backend configurations for improved deployment
- Updated docker-compose.yml to include environment variable support for services, enhancing flexibility in configuration. - Refactored Dockerfile to utilize build arguments for VITE environment variables, allowing for better customization during builds. - Improved Nginx configuration to handle larger video uploads by increasing client_max_body_size to 5GB. - Enhanced backend Dockerfile to include wget for health checks and improved startup logging for database migrations. - Added validation for critical environment variables in the backend to ensure necessary configurations are present before application startup. - Updated content streaming logic to support direct HLS URL construction, improving streaming reliability and user experience. - Refactored various components and services to streamline access checks and improve error handling during content playback.
This commit is contained in:
13
backend/.dockerignore
Normal file
13
backend/.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
.cursor
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -14,6 +14,9 @@ RUN npm prune --production
|
||||
FROM node:20-alpine AS production
|
||||
WORKDIR /app
|
||||
|
||||
# wget is needed for Docker/Portainer health checks
|
||||
RUN apk add --no-cache wget
|
||||
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/package-lock.json ./package-lock.json
|
||||
COPY --from=builder /app/dist ./dist
|
||||
@@ -21,5 +24,8 @@ COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
# Run TypeORM migrations on startup, then start the API
|
||||
CMD ["sh", "-c", "npx typeorm migration:run -d dist/database/ormconfig.js 2>/dev/null; export NODE_OPTIONS='--max-old-space-size=1024' && npm run start:prod"]
|
||||
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||
|
||||
# Run TypeORM migrations on startup, then start the API.
|
||||
# Migration errors are logged (not suppressed) so failed deploys are visible.
|
||||
CMD ["sh", "-c", "echo 'Running database migrations...' && npx typeorm migration:run -d dist/database/ormconfig.js && echo 'Migrations complete.' && npm run start:prod"]
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NostrAuthModule } from 'src/nostr-auth/nostr-auth.module';
|
||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||
import { TokenAuthGuard } from './guards/token.guard';
|
||||
import { HybridAuthGuard } from './guards/hybrid-auth.guard';
|
||||
import { OptionalHybridAuthGuard } from './guards/optional-hybrid-auth.guard';
|
||||
import { NostrSessionService } from './nostr-session.service';
|
||||
import { NostrSessionJwtGuard } from './guards/nostr-session-jwt.guard';
|
||||
import { UsersModule } from 'src/users/users.module';
|
||||
@@ -30,6 +31,7 @@ import { UsersModule } from 'src/users/users.module';
|
||||
JwtAuthGuard,
|
||||
TokenAuthGuard,
|
||||
HybridAuthGuard,
|
||||
OptionalHybridAuthGuard,
|
||||
NostrSessionService,
|
||||
NostrSessionJwtGuard,
|
||||
],
|
||||
@@ -39,6 +41,7 @@ import { UsersModule } from 'src/users/users.module';
|
||||
JwtAuthGuard,
|
||||
TokenAuthGuard,
|
||||
HybridAuthGuard,
|
||||
OptionalHybridAuthGuard,
|
||||
NostrSessionService,
|
||||
NostrSessionJwtGuard,
|
||||
],
|
||||
|
||||
30
backend/src/auth/guards/optional-hybrid-auth.guard.ts
Normal file
30
backend/src/auth/guards/optional-hybrid-auth.guard.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { HybridAuthGuard } from './hybrid-auth.guard';
|
||||
|
||||
/**
|
||||
* Optional version of HybridAuthGuard.
|
||||
*
|
||||
* Tries all authentication strategies (Nostr, NostrSessionJwt, Jwt).
|
||||
* If any succeeds, `request.user` is populated as normal.
|
||||
* If all fail, the request proceeds anyway with `request.user = undefined`.
|
||||
*
|
||||
* Use this for endpoints that should work for both authenticated and
|
||||
* anonymous users (e.g. streaming free content without login).
|
||||
*/
|
||||
@Injectable()
|
||||
export class OptionalHybridAuthGuard implements CanActivate {
|
||||
constructor(private readonly hybridAuthGuard: HybridAuthGuard) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
try {
|
||||
await this.hybridAuthGuard.canActivate(context);
|
||||
} catch {
|
||||
// Auth failed — that's OK for optional auth.
|
||||
// Ensure request.user is explicitly undefined so downstream
|
||||
// code can check whether the user is authenticated.
|
||||
const request = context.switchToHttp().getRequest();
|
||||
request.user = undefined;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
73
backend/src/common/validate-env.ts
Normal file
73
backend/src/common/validate-env.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Validates that all required environment variables are present at startup.
|
||||
* Fails fast with a clear error message listing every missing variable,
|
||||
* so misconfigured Portainer deployments are caught immediately.
|
||||
*/
|
||||
export function validateEnvironment(): void {
|
||||
const logger = new Logger('EnvironmentValidation');
|
||||
|
||||
const required: string[] = [
|
||||
'ENVIRONMENT',
|
||||
'DATABASE_HOST',
|
||||
'DATABASE_PORT',
|
||||
'DATABASE_USER',
|
||||
'DATABASE_PASSWORD',
|
||||
'DATABASE_NAME',
|
||||
'QUEUE_HOST',
|
||||
'QUEUE_PORT',
|
||||
'AWS_ACCESS_KEY',
|
||||
'AWS_SECRET_KEY',
|
||||
'S3_PRIVATE_BUCKET_NAME',
|
||||
'S3_PUBLIC_BUCKET_NAME',
|
||||
'NOSTR_JWT_SECRET',
|
||||
'AES_MASTER_SECRET',
|
||||
];
|
||||
|
||||
// BTCPay is required for payment processing
|
||||
const btcpayVars = [
|
||||
'BTCPAY_URL',
|
||||
'BTCPAY_API_KEY',
|
||||
'BTCPAY_STORE_ID',
|
||||
'BTCPAY_WEBHOOK_SECRET',
|
||||
];
|
||||
|
||||
const missing = required.filter((key) => !process.env[key]);
|
||||
const missingBtcpay = btcpayVars.filter((key) => !process.env[key]);
|
||||
|
||||
if (missingBtcpay.length > 0 && missingBtcpay.length < btcpayVars.length) {
|
||||
// Some BTCPay vars set but not all — likely a partial config error
|
||||
logger.error(
|
||||
`Partial BTCPay configuration detected. Missing: ${missingBtcpay.join(', ')}. ` +
|
||||
'Set all BTCPay variables or leave all empty to disable payments.',
|
||||
);
|
||||
missing.push(...missingBtcpay);
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
const message =
|
||||
`Missing required environment variables:\n` +
|
||||
missing.map((key) => ` - ${key}`).join('\n') +
|
||||
`\n\nSet these in Portainer Stack environment variables before deploying.`;
|
||||
|
||||
logger.error(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Warn about insecure defaults
|
||||
const insecureDefaults: Record<string, string> = {
|
||||
NOSTR_JWT_SECRET: 'change-this-to-a-long-random-secret-in-production',
|
||||
AES_MASTER_SECRET: 'change-this-32-byte-hex-secret-00',
|
||||
};
|
||||
|
||||
for (const [key, insecureValue] of Object.entries(insecureDefaults)) {
|
||||
if (process.env[key] === insecureValue) {
|
||||
logger.warn(
|
||||
`${key} is using an insecure default value. Generate a secure secret with: openssl rand -hex 32`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('Environment validation passed');
|
||||
}
|
||||
@@ -9,8 +9,11 @@ import {
|
||||
UseInterceptors,
|
||||
Post,
|
||||
Logger,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard';
|
||||
import { OptionalHybridAuthGuard } from 'src/auth/guards/optional-hybrid-auth.guard';
|
||||
import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator';
|
||||
import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard';
|
||||
import { ContentsService } from './contents.service';
|
||||
@@ -69,10 +72,25 @@ export class ContentsController {
|
||||
}
|
||||
|
||||
@Get(':id/stream')
|
||||
@UseGuards(HybridAuthGuard, SubscriptionsGuard)
|
||||
@Subscriptions(['enthusiast', 'film-buff', 'cinephile'])
|
||||
async stream(@Param('id') id: string) {
|
||||
@UseGuards(OptionalHybridAuthGuard)
|
||||
async stream(
|
||||
@Param('id') id: string,
|
||||
@Req() req: any,
|
||||
) {
|
||||
const content = await this.contentsService.stream(id);
|
||||
|
||||
// Determine if the content is free (no payment required)
|
||||
const projectPrice = Number(content.project?.rentalPrice ?? 0);
|
||||
const contentPrice = Number(content.rentalPrice ?? 0);
|
||||
const isFreeContent = projectPrice <= 0 && contentPrice <= 0;
|
||||
|
||||
// Paid content requires a valid authenticated user
|
||||
if (!isFreeContent && !req.user) {
|
||||
throw new UnauthorizedException(
|
||||
'Authentication required for paid content',
|
||||
);
|
||||
}
|
||||
|
||||
const dto = new StreamContentDTO(content);
|
||||
|
||||
// Check if the HLS manifest actually exists in the public bucket.
|
||||
@@ -90,7 +108,11 @@ export class ContentsController {
|
||||
);
|
||||
|
||||
if (hlsExists) {
|
||||
return dto;
|
||||
// Return the public S3 URL for the HLS manifest so the player
|
||||
// can fetch it directly from MinIO/S3 (avoids proxying through
|
||||
// the API and prevents CORS issues with relative segment paths).
|
||||
const publicUrl = getPublicS3Url(outputKey);
|
||||
return { ...dto, file: publicUrl };
|
||||
}
|
||||
|
||||
// HLS not available — serve a presigned URL for the original file
|
||||
|
||||
@@ -3,15 +3,18 @@ import { AppModule } from './app.module';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { useContainer } from 'class-validator';
|
||||
// Sentry instrumentation removed (see instrument.ts)
|
||||
import * as express from 'express';
|
||||
import {
|
||||
ExpressAdapter,
|
||||
NestExpressApplication,
|
||||
} from '@nestjs/platform-express';
|
||||
import { RawBodyRequest } from './types/raw-body-request';
|
||||
import { validateEnvironment } from './common/validate-env';
|
||||
|
||||
async function bootstrap() {
|
||||
// Fail fast if critical env vars are missing
|
||||
validateEnvironment();
|
||||
|
||||
const server = express();
|
||||
const captureRawBody = (
|
||||
request: RawBodyRequest,
|
||||
@@ -46,7 +49,10 @@ async function bootstrap() {
|
||||
|
||||
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
||||
|
||||
if (process.env.ENVIRONMENT === 'development') {
|
||||
if (
|
||||
process.env.ENVIRONMENT === 'development' ||
|
||||
process.env.ENVIRONMENT === 'local'
|
||||
) {
|
||||
const swagConfig = new DocumentBuilder()
|
||||
.setTitle('IndeeHub API')
|
||||
.setDescription('This is the API for the IndeeHub application')
|
||||
@@ -61,19 +67,29 @@ async function bootstrap() {
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
|
||||
if (process.env.ENVIRONMENT === 'production') {
|
||||
// Build CORS origin list from FRONTEND_URL + known domains
|
||||
const origins: string[] = [
|
||||
'https://indeehub.studio',
|
||||
'https://www.indeehub.studio',
|
||||
'https://app.indeehub.studio',
|
||||
'https://bff.indeehub.studio',
|
||||
];
|
||||
|
||||
if (process.env.FRONTEND_URL) {
|
||||
origins.push(process.env.FRONTEND_URL);
|
||||
}
|
||||
if (process.env.DOMAIN) {
|
||||
origins.push(`https://${process.env.DOMAIN}`);
|
||||
}
|
||||
|
||||
app.enableCors({
|
||||
origin: [
|
||||
'https://indeehub.studio',
|
||||
'https://www.indeehub.studio',
|
||||
'https://app.indeehub.studio',
|
||||
'https://bff.indeehub.studio',
|
||||
'https://indeehub.retool.com',
|
||||
'https://www.indeehub.retool.com',
|
||||
],
|
||||
origin: [...new Set(origins)],
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
credentials: true,
|
||||
});
|
||||
} else app.enableCors();
|
||||
} else {
|
||||
app.enableCors();
|
||||
}
|
||||
|
||||
await app.listen(process.env.PORT || 4000);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class BaseProjectDTO {
|
||||
synopsis?: string;
|
||||
trailer?: string;
|
||||
poster?: string;
|
||||
streamingUrl?: string;
|
||||
status: Status;
|
||||
type: Type;
|
||||
format?: Format;
|
||||
@@ -58,11 +59,14 @@ export class BaseProjectDTO {
|
||||
} else {
|
||||
// Pick the best content for the film slot. When a project has
|
||||
// multiple content rows (e.g. an auto-created placeholder plus
|
||||
// the real upload), prefer the one with a rental price set or
|
||||
// a file uploaded rather than blindly taking contents[0].
|
||||
// the real upload), prefer completed content with a file.
|
||||
const best = project.contents?.length
|
||||
? [...project.contents].sort((a, b) => {
|
||||
// Prefer content with a file
|
||||
// Prefer completed content first
|
||||
const aCompleted = a.status === 'completed' ? 1 : 0;
|
||||
const bCompleted = b.status === 'completed' ? 1 : 0;
|
||||
if (bCompleted !== aCompleted) return bCompleted - aCompleted;
|
||||
// Then prefer content with a file
|
||||
const aFile = a.file ? 1 : 0;
|
||||
const bFile = b.file ? 1 : 0;
|
||||
if (bFile !== aFile) return bFile - aFile;
|
||||
@@ -88,7 +92,20 @@ export class BaseProjectDTO {
|
||||
}
|
||||
|
||||
if (project.poster) {
|
||||
this.poster = getPublicS3Url(project.poster);
|
||||
// External URLs and local static paths (starting with / or http)
|
||||
// should not be transformed through the S3 bucket URL
|
||||
if (
|
||||
project.poster.startsWith('http') ||
|
||||
project.poster.startsWith('/')
|
||||
) {
|
||||
this.poster = project.poster;
|
||||
} else {
|
||||
this.poster = getPublicS3Url(project.poster);
|
||||
}
|
||||
}
|
||||
|
||||
if (project.streamingUrl) {
|
||||
this.streamingUrl = project.streamingUrl;
|
||||
}
|
||||
|
||||
this.screenings = project.screenings
|
||||
|
||||
@@ -171,6 +171,9 @@ export class ProjectsService {
|
||||
'contents.poster',
|
||||
]);
|
||||
|
||||
// Always load the genre so the DTO can include it in the response
|
||||
projectsQuery.leftJoinAndSelect('project.genre', 'genre');
|
||||
|
||||
if (query.status) {
|
||||
if (query.status === 'published') {
|
||||
const completed = this.contentsService.getCompletedProjectsSubquery();
|
||||
@@ -185,7 +188,7 @@ export class ProjectsService {
|
||||
if (query.search) {
|
||||
projectsQuery.leftJoin('project.projectSubgenres', 'projectSubgenres');
|
||||
projectsQuery.leftJoin('projectSubgenres.subgenre', 'subgenre');
|
||||
projectsQuery.leftJoin('project.genre', 'genre');
|
||||
// genre already joined above via leftJoinAndSelect
|
||||
|
||||
projectsQuery.leftJoin('contents.cast', 'castMembers');
|
||||
projectsQuery.leftJoin('castMembers.filmmaker', 'castFilmmaker');
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
* Populates the PostgreSQL database with:
|
||||
* 1. Genres (Documentary, Drama, etc.)
|
||||
* 2. Test users with Nostr pubkeys and active subscriptions
|
||||
* 3. IndeeHub films (native delivery mode)
|
||||
* 4. TopDoc films (native delivery mode, YouTube streaming URLs)
|
||||
* 5. Projects and contents for both film sets
|
||||
* 3. IndeeHub films (native delivery mode, free)
|
||||
*
|
||||
* TopDoc documentary films are NOT seeded — they live in the frontend
|
||||
* mock data (src/data/topDocFilms.ts) and appear only when the user
|
||||
* switches to the "TopDoc Films" content source.
|
||||
*
|
||||
* Run: node dist/scripts/seed-content.js
|
||||
* Requires: DATABASE_HOST, DATABASE_PORT, DATABASE_USER, etc. in env
|
||||
@@ -24,7 +26,6 @@ const client = new Client({
|
||||
});
|
||||
|
||||
// ── Test Users ────────────────────────────────────────────────
|
||||
// Using the same dev personas from the frontend seed
|
||||
const testUsers = [
|
||||
{
|
||||
id: randomUUID(),
|
||||
@@ -70,7 +71,6 @@ const indeeHubFilms = [
|
||||
'A groundbreaking documentary exploring the intersection of faith, finance, and the future of money through the lens of Bitcoin.',
|
||||
poster: '/images/films/posters/god-bless-bitcoin.webp',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Bitcoin', 'Religion'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
@@ -80,7 +80,6 @@ const indeeHubFilms = [
|
||||
'A compelling narrative exploring the emotional weight of our past.',
|
||||
poster: '/images/films/posters/thethingswecarry.webp',
|
||||
genre: 'Drama',
|
||||
categories: ['Drama'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
@@ -89,7 +88,6 @@ const indeeHubFilms = [
|
||||
synopsis: 'An intense confrontation that tests the limits of human resolve.',
|
||||
poster: '/images/films/posters/duel.png',
|
||||
genre: 'Action',
|
||||
categories: ['Drama', 'Action'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
@@ -100,66 +98,6 @@ const indeeHubFilms = [
|
||||
poster:
|
||||
'/images/films/posters/2b0d7349-c010-47a0-b584-49e1bf86ab2f.png',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Finance', 'Bitcoin'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
];
|
||||
|
||||
// ── TopDoc Films ──────────────────────────────────────────────
|
||||
const topDocFilms = [
|
||||
{
|
||||
id: 'tdf-god-bless-bitcoin',
|
||||
title: 'God Bless Bitcoin',
|
||||
synopsis:
|
||||
'Exploring the intersection of faith and Bitcoin.',
|
||||
poster: '/images/films/posters/topdoc/god-bless-bitcoin.jpg',
|
||||
streamingUrl: 'https://www.youtube.com/embed/3XEuqixD2Zg',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Bitcoin'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
id: 'tdf-bitcoin-end-of-money',
|
||||
title: 'Bitcoin: The End of Money as We Know It',
|
||||
synopsis:
|
||||
'Tracing the history of money from barter to Bitcoin.',
|
||||
poster: '/images/films/posters/topdoc/bitcoin-end-of-money.jpg',
|
||||
streamingUrl: 'https://www.youtube.com/embed/zpNlG3VtcBM',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Bitcoin', 'Economics'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
id: 'tdf-bitcoin-beyond-bubble',
|
||||
title: 'Bitcoin: Beyond the Bubble',
|
||||
synopsis:
|
||||
'An accessible explainer tracing currency evolution.',
|
||||
poster: '/images/films/posters/topdoc/bitcoin-beyond-bubble.jpg',
|
||||
streamingUrl: 'https://www.youtube.com/embed/URrmfEu0cZ8',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Bitcoin', 'Economics'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
id: 'tdf-bitcoin-gospel',
|
||||
title: 'The Bitcoin Gospel',
|
||||
synopsis:
|
||||
'The true believers argue Bitcoin is a gamechanger for the global economy.',
|
||||
poster: '/images/films/posters/topdoc/bitcoin-gospel.jpg',
|
||||
streamingUrl: 'https://www.youtube.com/embed/2I6dXRK9oJo',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Bitcoin'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
id: 'tdf-banking-on-bitcoin',
|
||||
title: 'Banking on Bitcoin',
|
||||
synopsis:
|
||||
'Chronicles idealists and entrepreneurs as they redefine money.',
|
||||
poster: '/images/films/posters/topdoc/banking-on-bitcoin.jpg',
|
||||
streamingUrl: 'https://www.youtube.com/embed/BbMT1Mhv7OQ',
|
||||
genre: 'Documentary',
|
||||
categories: ['Documentary', 'Bitcoin', 'Finance'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
];
|
||||
@@ -169,7 +107,6 @@ async function seed() {
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// Run inside a transaction
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Seed genres
|
||||
@@ -242,47 +179,15 @@ async function seed() {
|
||||
],
|
||||
);
|
||||
|
||||
// Create a content record for the film
|
||||
// Content with status 'completed' so it appears in public API listings
|
||||
const contentId = `content-${film.id}`;
|
||||
await client.query(
|
||||
`INSERT INTO contents (id, project_id, title, synopsis, status, "order", release_date, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, 'ready', 1, NOW(), NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title`,
|
||||
[contentId, film.id, film.title, film.synopsis],
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Seed TopDoc films
|
||||
console.log('[seed] Seeding TopDoc films...');
|
||||
for (const film of topDocFilms) {
|
||||
const genreId = genreLookup[film.genre] || null;
|
||||
await client.query(
|
||||
`INSERT INTO projects (id, name, title, slug, synopsis, poster, status, type, genre_id, delivery_mode, streaming_url, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'published', 'film', $7, $8, $9, NOW(), NOW())
|
||||
`INSERT INTO contents (id, project_id, title, synopsis, status, "order", rental_price, release_date, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, 'completed', 1, 0, NOW(), NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
synopsis = EXCLUDED.synopsis,
|
||||
poster = EXCLUDED.poster,
|
||||
delivery_mode = EXCLUDED.delivery_mode,
|
||||
streaming_url = EXCLUDED.streaming_url`,
|
||||
[
|
||||
film.id,
|
||||
film.title,
|
||||
film.title,
|
||||
film.id,
|
||||
film.synopsis,
|
||||
film.poster,
|
||||
genreId,
|
||||
film.deliveryMode,
|
||||
film.streamingUrl,
|
||||
],
|
||||
);
|
||||
|
||||
const contentId = `content-${film.id}`;
|
||||
await client.query(
|
||||
`INSERT INTO contents (id, project_id, title, synopsis, status, "order", release_date, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, 'ready', 1, NOW(), NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title`,
|
||||
status = 'completed',
|
||||
rental_price = 0`,
|
||||
[contentId, film.id, film.title, film.synopsis],
|
||||
);
|
||||
}
|
||||
@@ -292,7 +197,6 @@ async function seed() {
|
||||
console.log(` - ${genres.length} genres`);
|
||||
console.log(` - ${testUsers.length} test users with subscriptions`);
|
||||
console.log(` - ${indeeHubFilms.length} IndeeHub films`);
|
||||
console.log(` - ${topDocFilms.length} TopDoc films`);
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('[seed] Error seeding database:', error);
|
||||
|
||||
Reference in New Issue
Block a user