Implement backend API and database services in Docker setup
- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO. - Introduced PostgreSQL and Redis services with health checks and configurations for data persistence. - Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets. - Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage. - Enhanced the Dockerfile to support the new API environment variables and configurations. - Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
305
backend/src/scripts/seed-content.ts
Normal file
305
backend/src/scripts/seed-content.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Database Seed Script
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* Run: node dist/scripts/seed-content.js
|
||||
* Requires: DATABASE_HOST, DATABASE_PORT, DATABASE_USER, etc. in env
|
||||
*/
|
||||
|
||||
import { Client } from 'pg';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const client = new Client({
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: Number(process.env.DATABASE_PORT || '5432'),
|
||||
user: process.env.DATABASE_USER || 'indeedhub',
|
||||
password: process.env.DATABASE_PASSWORD || 'indeedhub_dev_2026',
|
||||
database: process.env.DATABASE_NAME || 'indeedhub',
|
||||
});
|
||||
|
||||
// ── Test Users ────────────────────────────────────────────────
|
||||
// Using the same dev personas from the frontend seed
|
||||
const testUsers = [
|
||||
{
|
||||
id: randomUUID(),
|
||||
email: 'alice@indeedhub.local',
|
||||
legalName: 'Alice Developer',
|
||||
nostrPubkey:
|
||||
'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
|
||||
},
|
||||
{
|
||||
id: randomUUID(),
|
||||
email: 'bob@indeedhub.local',
|
||||
legalName: 'Bob Filmmaker',
|
||||
nostrPubkey:
|
||||
'b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3',
|
||||
},
|
||||
{
|
||||
id: randomUUID(),
|
||||
email: 'charlie@indeedhub.local',
|
||||
legalName: 'Charlie Audience',
|
||||
nostrPubkey:
|
||||
'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
|
||||
},
|
||||
];
|
||||
|
||||
// ── Genres ────────────────────────────────────────────────────
|
||||
const genres = [
|
||||
{ id: randomUUID(), name: 'Documentary', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Drama', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Action', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Horror', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Comedy', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Thriller', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Science Fiction', type: 'film' },
|
||||
{ id: randomUUID(), name: 'Animation', type: 'film' },
|
||||
];
|
||||
|
||||
// ── IndeeHub Films ────────────────────────────────────────────
|
||||
const indeeHubFilms = [
|
||||
{
|
||||
id: 'god-bless-bitcoin',
|
||||
title: 'God Bless Bitcoin',
|
||||
synopsis:
|
||||
'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',
|
||||
},
|
||||
{
|
||||
id: 'thethingswecarry',
|
||||
title: 'The Things We Carry',
|
||||
synopsis:
|
||||
'A compelling narrative exploring the emotional weight of our past.',
|
||||
poster: '/images/films/posters/thethingswecarry.webp',
|
||||
genre: 'Drama',
|
||||
categories: ['Drama'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
id: 'duel',
|
||||
title: 'Duel',
|
||||
synopsis: 'An intense confrontation that tests the limits of human resolve.',
|
||||
poster: '/images/films/posters/duel.png',
|
||||
genre: 'Action',
|
||||
categories: ['Drama', 'Action'],
|
||||
deliveryMode: 'native',
|
||||
},
|
||||
{
|
||||
id: '2b0d7349-c010-47a0-b584-49e1bf86ab2f',
|
||||
title: 'Hard Money',
|
||||
synopsis:
|
||||
'Understanding sound money principles and monetary sovereignty.',
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
async function seed() {
|
||||
console.log('[seed] Connecting to database...');
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
// Run inside a transaction
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Seed genres
|
||||
console.log('[seed] Seeding genres...');
|
||||
for (const genre of genres) {
|
||||
await client.query(
|
||||
`INSERT INTO genres (id, name, type, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name`,
|
||||
[genre.id, genre.name, genre.type],
|
||||
);
|
||||
}
|
||||
|
||||
// Build genre lookup
|
||||
const genreLookup: Record<string, string> = {};
|
||||
const genreRows = await client.query('SELECT id, name FROM genres');
|
||||
for (const row of genreRows.rows) {
|
||||
genreLookup[row.name] = row.id;
|
||||
}
|
||||
|
||||
// 2. Seed test users
|
||||
console.log('[seed] Seeding test users...');
|
||||
for (const user of testUsers) {
|
||||
await client.query(
|
||||
`INSERT INTO users (id, email, legal_name, nostr_pubkey, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
nostr_pubkey = EXCLUDED.nostr_pubkey`,
|
||||
[user.id, user.email, user.legalName, user.nostrPubkey],
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Seed subscriptions for test users (cinephile plan)
|
||||
console.log('[seed] Seeding subscriptions...');
|
||||
for (const user of testUsers) {
|
||||
const subId = randomUUID();
|
||||
const periodEnd = new Date();
|
||||
periodEnd.setFullYear(periodEnd.getFullYear() + 1);
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO subscriptions (id, user_id, type, period, status, period_end, created_at)
|
||||
VALUES ($1, $2, 'cinephile', 'yearly', 'succeeded', $3, NOW())
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[subId, user.id, periodEnd],
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Seed IndeeHub films
|
||||
console.log('[seed] Seeding IndeeHub films...');
|
||||
for (const film of indeeHubFilms) {
|
||||
const genreId = genreLookup[film.genre] || null;
|
||||
await client.query(
|
||||
`INSERT INTO projects (id, name, title, slug, synopsis, poster, status, type, genre_id, delivery_mode, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'published', 'film', $7, $8, NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
synopsis = EXCLUDED.synopsis,
|
||||
poster = EXCLUDED.poster,
|
||||
delivery_mode = EXCLUDED.delivery_mode`,
|
||||
[
|
||||
film.id,
|
||||
film.title,
|
||||
film.title,
|
||||
film.id,
|
||||
film.synopsis,
|
||||
film.poster,
|
||||
genreId,
|
||||
film.deliveryMode,
|
||||
],
|
||||
);
|
||||
|
||||
// Create a content record for the film
|
||||
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())
|
||||
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`,
|
||||
[contentId, film.id, film.title, film.synopsis],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('[seed] Database seeded successfully!');
|
||||
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);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
seed().catch(console.error);
|
||||
Reference in New Issue
Block a user