From 4102f9990c13abfae9765cd0c95f481fc08b2f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 4 Mar 2026 18:42:58 +0100 Subject: [PATCH] fix: switch backend to CommonJS, generate Drizzle migrations, add migrate script - Remove "type": "module" and switch tsconfig to CommonJS/Node resolution to fix drizzle-kit ESM/CJS incompatibility - Strip .js extensions from all backend TypeScript imports - Fix gamification resolver: combine two .where() calls using and() - Fix index.ts: wrap top-level awaits in async main(), fix Fastify+yoga request handling via handleNodeRequestAndResponse - Generate initial Drizzle SQL migration (0000_pale_hellion.sql) for all 15 tables - Add src/scripts/migrate.ts: programmatic Drizzle migrator for production - Copy migrations folder into Docker image (Dockerfile.backend) - Add schema:migrate npm script Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile.backend | 1 + packages/backend/drizzle.config.ts | 2 +- packages/backend/package.json | 2 +- packages/backend/src/db/connection.ts | 2 +- packages/backend/src/db/schema/articles.ts | 4 +- packages/backend/src/db/schema/comments.ts | 2 +- .../backend/src/db/schema/gamification.ts | 4 +- packages/backend/src/db/schema/index.ts | 14 +- packages/backend/src/db/schema/recordings.ts | 4 +- packages/backend/src/db/schema/users.ts | 2 +- packages/backend/src/db/schema/videos.ts | 4 +- packages/backend/src/graphql/builder.ts | 4 +- packages/backend/src/graphql/context.ts | 8 +- packages/backend/src/graphql/index.ts | 2 +- .../backend/src/graphql/resolvers/articles.ts | 6 +- .../backend/src/graphql/resolvers/auth.ts | 14 +- .../backend/src/graphql/resolvers/comments.ts | 8 +- .../src/graphql/resolvers/gamification.ts | 11 +- .../backend/src/graphql/resolvers/models.ts | 6 +- .../src/graphql/resolvers/recordings.ts | 10 +- .../backend/src/graphql/resolvers/stats.ts | 6 +- .../backend/src/graphql/resolvers/users.ts | 6 +- .../backend/src/graphql/resolvers/videos.ts | 6 +- packages/backend/src/graphql/types/index.ts | 2 +- packages/backend/src/index.ts | 138 +- packages/backend/src/lib/gamification.ts | 4 +- .../src/migrations/0000_pale_hellion.sql | 233 ++ .../src/migrations/meta/0000_snapshot.json | 1931 +++++++++++++++++ .../backend/src/migrations/meta/_journal.json | 13 + packages/backend/src/scripts/migrate.ts | 23 + packages/backend/tsconfig.json | 4 +- 31 files changed, 2341 insertions(+), 135 deletions(-) create mode 100644 packages/backend/src/migrations/0000_pale_hellion.sql create mode 100644 packages/backend/src/migrations/meta/0000_snapshot.json create mode 100644 packages/backend/src/migrations/meta/_journal.json create mode 100644 packages/backend/src/scripts/migrate.ts diff --git a/Dockerfile.backend b/Dockerfile.backend index 90a17b9..e7f097f 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -45,6 +45,7 @@ RUN mkdir -p packages/backend COPY --from=builder --chown=node:node /app/packages/backend/dist ./packages/backend/dist COPY --from=builder --chown=node:node /app/packages/backend/node_modules ./packages/backend/node_modules COPY --from=builder --chown=node:node /app/packages/backend/package.json ./packages/backend/package.json +COPY --from=builder --chown=node:node /app/packages/backend/src/migrations ./packages/backend/migrations RUN mkdir -p /data/uploads && chown node:node /data/uploads diff --git a/packages/backend/drizzle.config.ts b/packages/backend/drizzle.config.ts index 89cd0fc..7fe740f 100644 --- a/packages/backend/drizzle.config.ts +++ b/packages/backend/drizzle.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ - schema: "./src/db/schema/index.ts", + schema: "./src/db/schema/*.ts", out: "./src/migrations", dialect: "postgresql", dbCredentials: { diff --git a/packages/backend/package.json b/packages/backend/package.json index 25d8ddd..d7e6a6b 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,7 +1,6 @@ { "name": "@sexy.pivoine.art/backend", "version": "1.0.0", - "type": "module", "private": true, "scripts": { "dev": "tsx watch src/index.ts", @@ -10,6 +9,7 @@ "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", + "schema:migrate": "tsx src/scripts/migrate.ts", "migrate": "tsx src/scripts/data-migration.ts" }, "dependencies": { diff --git a/packages/backend/src/db/connection.ts b/packages/backend/src/db/connection.ts index c36eea7..389f359 100644 --- a/packages/backend/src/db/connection.ts +++ b/packages/backend/src/db/connection.ts @@ -1,6 +1,6 @@ import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; -import * as schema from "./schema/index.js"; +import * as schema from "./schema/index"; const pool = new Pool({ connectionString: process.env.DATABASE_URL || "postgresql://sexy:sexy@localhost:5432/sexy", diff --git a/packages/backend/src/db/schema/articles.ts b/packages/backend/src/db/schema/articles.ts index 8a27541..c79b742 100644 --- a/packages/backend/src/db/schema/articles.ts +++ b/packages/backend/src/db/schema/articles.ts @@ -6,8 +6,8 @@ import { index, uniqueIndex, } from "drizzle-orm/pg-core"; -import { users } from "./users.js"; -import { files } from "./files.js"; +import { users } from "./users"; +import { files } from "./files"; export const articles = pgTable( "articles", diff --git a/packages/backend/src/db/schema/comments.ts b/packages/backend/src/db/schema/comments.ts index 15fea59..0097449 100644 --- a/packages/backend/src/db/schema/comments.ts +++ b/packages/backend/src/db/schema/comments.ts @@ -5,7 +5,7 @@ import { index, integer, } from "drizzle-orm/pg-core"; -import { users } from "./users.js"; +import { users } from "./users"; export const comments = pgTable( "comments", diff --git a/packages/backend/src/db/schema/gamification.ts b/packages/backend/src/db/schema/gamification.ts index 6af069e..35252df 100644 --- a/packages/backend/src/db/schema/gamification.ts +++ b/packages/backend/src/db/schema/gamification.ts @@ -8,8 +8,8 @@ import { pgEnum, uniqueIndex, } from "drizzle-orm/pg-core"; -import { users } from "./users.js"; -import { recordings } from "./recordings.js"; +import { users } from "./users"; +import { recordings } from "./recordings"; export const achievementStatusEnum = pgEnum("achievement_status", [ "draft", diff --git a/packages/backend/src/db/schema/index.ts b/packages/backend/src/db/schema/index.ts index 86b99b5..71b9c81 100644 --- a/packages/backend/src/db/schema/index.ts +++ b/packages/backend/src/db/schema/index.ts @@ -1,7 +1,7 @@ -export * from "./files.js"; -export * from "./users.js"; -export * from "./videos.js"; -export * from "./articles.js"; -export * from "./recordings.js"; -export * from "./comments.js"; -export * from "./gamification.js"; +export * from "./files"; +export * from "./users"; +export * from "./videos"; +export * from "./articles"; +export * from "./recordings"; +export * from "./comments"; +export * from "./gamification"; diff --git a/packages/backend/src/db/schema/recordings.ts b/packages/backend/src/db/schema/recordings.ts index 245c7af..411c637 100644 --- a/packages/backend/src/db/schema/recordings.ts +++ b/packages/backend/src/db/schema/recordings.ts @@ -9,8 +9,8 @@ import { uniqueIndex, jsonb, } from "drizzle-orm/pg-core"; -import { users } from "./users.js"; -import { videos } from "./videos.js"; +import { users } from "./users"; +import { videos } from "./videos"; export const recordingStatusEnum = pgEnum("recording_status", [ "draft", diff --git a/packages/backend/src/db/schema/users.ts b/packages/backend/src/db/schema/users.ts index e7e622a..ff89598 100644 --- a/packages/backend/src/db/schema/users.ts +++ b/packages/backend/src/db/schema/users.ts @@ -8,7 +8,7 @@ import { uniqueIndex, integer, } from "drizzle-orm/pg-core"; -import { files } from "./files.js"; +import { files } from "./files"; export const roleEnum = pgEnum("user_role", ["model", "viewer", "admin"]); diff --git a/packages/backend/src/db/schema/videos.ts b/packages/backend/src/db/schema/videos.ts index 27756be..966d76b 100644 --- a/packages/backend/src/db/schema/videos.ts +++ b/packages/backend/src/db/schema/videos.ts @@ -8,8 +8,8 @@ import { uniqueIndex, primaryKey, } from "drizzle-orm/pg-core"; -import { users } from "./users.js"; -import { files } from "./files.js"; +import { users } from "./users"; +import { files } from "./files"; export const videos = pgTable( "videos", diff --git a/packages/backend/src/graphql/builder.ts b/packages/backend/src/graphql/builder.ts index 7fdc0ac..60c5e8c 100644 --- a/packages/backend/src/graphql/builder.ts +++ b/packages/backend/src/graphql/builder.ts @@ -1,7 +1,7 @@ import SchemaBuilder from "@pothos/core"; import ErrorsPlugin from "@pothos/plugin-errors"; -import type { DB } from "../db/connection.js"; -import type { SessionUser } from "../lib/auth.js"; +import type { DB } from "../db/connection"; +import type { SessionUser } from "../lib/auth"; import type Redis from "ioredis"; import { GraphQLDateTime, GraphQLJSON } from "graphql-scalars"; diff --git a/packages/backend/src/graphql/context.ts b/packages/backend/src/graphql/context.ts index cb5b049..c9f2f8d 100644 --- a/packages/backend/src/graphql/context.ts +++ b/packages/backend/src/graphql/context.ts @@ -1,8 +1,8 @@ import type { YogaInitialContext } from "graphql-yoga"; -import type { Context } from "./builder.js"; -import { getSession } from "../lib/auth.js"; -import { db } from "../db/connection.js"; -import { redis } from "../lib/auth.js"; +import type { Context } from "./builder"; +import { getSession } from "../lib/auth"; +import { db } from "../db/connection"; +import { redis } from "../lib/auth"; export async function buildContext(ctx: YogaInitialContext & { request: Request; reply: unknown; db: typeof db; redis: typeof redis }): Promise { const request = ctx.request; diff --git a/packages/backend/src/graphql/index.ts b/packages/backend/src/graphql/index.ts index 41a58a3..16b0717 100644 --- a/packages/backend/src/graphql/index.ts +++ b/packages/backend/src/graphql/index.ts @@ -9,6 +9,6 @@ import "./resolvers/recordings.js"; import "./resolvers/comments.js"; import "./resolvers/gamification.js"; import "./resolvers/stats.js"; -import { builder } from "./builder.js"; +import { builder } from "./builder"; export const schema = builder.toSchema(); diff --git a/packages/backend/src/graphql/resolvers/articles.ts b/packages/backend/src/graphql/resolvers/articles.ts index 0c9c487..7a5dad5 100644 --- a/packages/backend/src/graphql/resolvers/articles.ts +++ b/packages/backend/src/graphql/resolvers/articles.ts @@ -1,6 +1,6 @@ -import { builder } from "../builder.js"; -import { ArticleType } from "../types/index.js"; -import { articles, users } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { ArticleType } from "../types/index"; +import { articles, users } from "../../db/schema/index"; import { eq, and, lte, desc } from "drizzle-orm"; builder.queryField("articles", (t) => diff --git a/packages/backend/src/graphql/resolvers/auth.ts b/packages/backend/src/graphql/resolvers/auth.ts index 6fc8169..6f0344f 100644 --- a/packages/backend/src/graphql/resolvers/auth.ts +++ b/packages/backend/src/graphql/resolvers/auth.ts @@ -1,12 +1,12 @@ import { GraphQLError } from "graphql"; -import { builder } from "../builder.js"; -import { CurrentUserType } from "../types/index.js"; -import { users } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { CurrentUserType } from "../types/index"; +import { users } from "../../db/schema/index"; import { eq } from "drizzle-orm"; -import { hash, verify as verifyArgon } from "../../lib/argon.js"; -import { setSession, deleteSession } from "../../lib/auth.js"; -import { sendVerification, sendPasswordReset } from "../../lib/email.js"; -import { slugify } from "../../lib/slugify.js"; +import { hash, verify as verifyArgon } from "../../lib/argon"; +import { setSession, deleteSession } from "../../lib/auth"; +import { sendVerification, sendPasswordReset } from "../../lib/email"; +import { slugify } from "../../lib/slugify"; import { nanoid } from "nanoid"; builder.mutationField("login", (t) => diff --git a/packages/backend/src/graphql/resolvers/comments.ts b/packages/backend/src/graphql/resolvers/comments.ts index 8d909c6..c58f1de 100644 --- a/packages/backend/src/graphql/resolvers/comments.ts +++ b/packages/backend/src/graphql/resolvers/comments.ts @@ -1,9 +1,9 @@ import { GraphQLError } from "graphql"; -import { builder } from "../builder.js"; -import { CommentType } from "../types/index.js"; -import { comments, users } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { CommentType } from "../types/index"; +import { comments, users } from "../../db/schema/index"; import { eq, and, desc } from "drizzle-orm"; -import { awardPoints, checkAchievements } from "../../lib/gamification.js"; +import { awardPoints, checkAchievements } from "../../lib/gamification"; builder.queryField("commentsForVideo", (t) => t.field({ diff --git a/packages/backend/src/graphql/resolvers/gamification.ts b/packages/backend/src/graphql/resolvers/gamification.ts index c0da1fb..4cd6543 100644 --- a/packages/backend/src/graphql/resolvers/gamification.ts +++ b/packages/backend/src/graphql/resolvers/gamification.ts @@ -1,7 +1,7 @@ -import { builder } from "../builder.js"; -import { LeaderboardEntryType, UserGamificationType, AchievementType } from "../types/index.js"; -import { user_stats, users, user_achievements, achievements, user_points } from "../../db/schema/index.js"; -import { eq, desc, gt, count, isNotNull } from "drizzle-orm"; +import { builder } from "../builder"; +import { LeaderboardEntryType, UserGamificationType, AchievementType } from "../types/index"; +import { user_stats, users, user_achievements, achievements, user_points } from "../../db/schema/index"; +import { eq, desc, gt, count, isNotNull, and } from "drizzle-orm"; builder.queryField("leaderboard", (t) => t.field({ @@ -73,8 +73,7 @@ builder.queryField("userGamification", (t) => }) .from(user_achievements) .leftJoin(achievements, eq(user_achievements.achievement_id, achievements.id)) - .where(eq(user_achievements.user_id, args.userId)) - .where(isNotNull(user_achievements.date_unlocked)) + .where(and(eq(user_achievements.user_id, args.userId), isNotNull(user_achievements.date_unlocked))) .orderBy(desc(user_achievements.date_unlocked)); const recentPoints = await ctx.db diff --git a/packages/backend/src/graphql/resolvers/models.ts b/packages/backend/src/graphql/resolvers/models.ts index 3589a0d..c54f3a6 100644 --- a/packages/backend/src/graphql/resolvers/models.ts +++ b/packages/backend/src/graphql/resolvers/models.ts @@ -1,6 +1,6 @@ -import { builder } from "../builder.js"; -import { ModelType } from "../types/index.js"; -import { users, user_photos, files } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { ModelType } from "../types/index"; +import { users, user_photos, files } from "../../db/schema/index"; import { eq, and, desc } from "drizzle-orm"; async function enrichModel(db: any, user: any) { diff --git a/packages/backend/src/graphql/resolvers/recordings.ts b/packages/backend/src/graphql/resolvers/recordings.ts index 074b270..e6e2507 100644 --- a/packages/backend/src/graphql/resolvers/recordings.ts +++ b/packages/backend/src/graphql/resolvers/recordings.ts @@ -1,10 +1,10 @@ import { GraphQLError } from "graphql"; -import { builder } from "../builder.js"; -import { RecordingType } from "../types/index.js"; -import { recordings, recording_plays } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { RecordingType } from "../types/index"; +import { recordings, recording_plays } from "../../db/schema/index"; import { eq, and, desc } from "drizzle-orm"; -import { slugify } from "../../lib/slugify.js"; -import { awardPoints, checkAchievements } from "../../lib/gamification.js"; +import { slugify } from "../../lib/slugify"; +import { awardPoints, checkAchievements } from "../../lib/gamification"; builder.queryField("recordings", (t) => t.field({ diff --git a/packages/backend/src/graphql/resolvers/stats.ts b/packages/backend/src/graphql/resolvers/stats.ts index d3c652e..632243b 100644 --- a/packages/backend/src/graphql/resolvers/stats.ts +++ b/packages/backend/src/graphql/resolvers/stats.ts @@ -1,6 +1,6 @@ -import { builder } from "../builder.js"; -import { StatsType } from "../types/index.js"; -import { users, videos } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { StatsType } from "../types/index"; +import { users, videos } from "../../db/schema/index"; import { eq, count } from "drizzle-orm"; builder.queryField("stats", (t) => diff --git a/packages/backend/src/graphql/resolvers/users.ts b/packages/backend/src/graphql/resolvers/users.ts index aff6ae5..31feb75 100644 --- a/packages/backend/src/graphql/resolvers/users.ts +++ b/packages/backend/src/graphql/resolvers/users.ts @@ -1,7 +1,7 @@ import { GraphQLError } from "graphql"; -import { builder } from "../builder.js"; -import { CurrentUserType, UserType } from "../types/index.js"; -import { users } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { CurrentUserType, UserType } from "../types/index"; +import { users } from "../../db/schema/index"; import { eq } from "drizzle-orm"; builder.queryField("me", (t) => diff --git a/packages/backend/src/graphql/resolvers/videos.ts b/packages/backend/src/graphql/resolvers/videos.ts index cbead35..9b89f1e 100644 --- a/packages/backend/src/graphql/resolvers/videos.ts +++ b/packages/backend/src/graphql/resolvers/videos.ts @@ -1,7 +1,7 @@ import { GraphQLError } from "graphql"; -import { builder } from "../builder.js"; -import { VideoType, VideoLikeResponseType, VideoPlayResponseType, VideoLikeStatusType } from "../types/index.js"; -import { videos, video_models, video_likes, video_plays, users, files } from "../../db/schema/index.js"; +import { builder } from "../builder"; +import { VideoType, VideoLikeResponseType, VideoPlayResponseType, VideoLikeStatusType } from "../types/index"; +import { videos, video_models, video_likes, video_plays, users, files } from "../../db/schema/index"; import { eq, and, lte, desc, inArray, count } from "drizzle-orm"; async function enrichVideo(db: any, video: any) { diff --git a/packages/backend/src/graphql/types/index.ts b/packages/backend/src/graphql/types/index.ts index a324b74..a61c920 100644 --- a/packages/backend/src/graphql/types/index.ts +++ b/packages/backend/src/graphql/types/index.ts @@ -1,4 +1,4 @@ -import { builder } from "../builder.js"; +import { builder } from "../builder"; // File type export const FileType = builder.objectRef<{ diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index ce20f8e..c723e8a 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -5,83 +5,89 @@ import fastifyMultipart from "@fastify/multipart"; import fastifyStatic from "@fastify/static"; import { createYoga } from "graphql-yoga"; import path from "path"; -import { schema } from "./graphql/index.js"; -import { buildContext } from "./graphql/context.js"; -import { db } from "./db/connection.js"; -import { redis } from "./lib/auth.js"; +import { schema } from "./graphql/index"; +import { buildContext } from "./graphql/context"; +import { db } from "./db/connection"; +import { redis } from "./lib/auth"; const PORT = parseInt(process.env.PORT || "4000"); const UPLOAD_DIR = process.env.UPLOAD_DIR || "/data/uploads"; const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:3000"; -const fastify = Fastify({ - logger: { - level: process.env.LOG_LEVEL || "info", - }, -}); +async function main() { + const fastify = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + }, + }); -await fastify.register(fastifyCookie, { - secret: process.env.COOKIE_SECRET || "change-me-in-production", -}); + await fastify.register(fastifyCookie, { + secret: process.env.COOKIE_SECRET || "change-me-in-production", + }); -await fastify.register(fastifyCors, { - origin: CORS_ORIGIN, - credentials: true, - methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], -}); + await fastify.register(fastifyCors, { + origin: CORS_ORIGIN, + credentials: true, + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + }); -await fastify.register(fastifyMultipart, { - limits: { - fileSize: 5 * 1024 * 1024 * 1024, // 5 GB - }, -}); + await fastify.register(fastifyMultipart, { + limits: { + fileSize: 5 * 1024 * 1024 * 1024, // 5 GB + }, + }); -await fastify.register(fastifyStatic, { - root: path.resolve(UPLOAD_DIR), - prefix: "/assets/", - decorateReply: false, -}); + await fastify.register(fastifyStatic, { + root: path.resolve(UPLOAD_DIR), + prefix: "/assets/", + decorateReply: false, + }); -const yoga = createYoga({ - schema, - context: buildContext, - graphqlEndpoint: "/graphql", - healthCheckEndpoint: "/health", - logging: { - debug: (...args) => fastify.log.debug(...args), - info: (...args) => fastify.log.info(...args), - warn: (...args) => fastify.log.warn(...args), - error: (...args) => fastify.log.error(...args), - }, -}); + const yoga = createYoga({ + schema, + context: buildContext, + graphqlEndpoint: "/graphql", + healthCheckEndpoint: "/health", + logging: { + debug: (msg: string) => fastify.log.debug(msg), + info: (msg: string) => fastify.log.info(msg), + warn: (msg: string) => fastify.log.warn(msg), + error: (msg: string) => fastify.log.error(msg), + }, + }); -fastify.route({ - url: "/graphql", - method: ["GET", "POST", "OPTIONS"], - handler: async (request, reply) => { - const response = await yoga.handleNodeRequestAndResponse(request, reply, { - request, - reply, - db, - redis, - }); - reply.status(response.status); - for (const [key, value] of response.headers.entries()) { - reply.header(key, value); - } - return reply.send(response.body); - }, -}); + fastify.route({ + url: "/graphql", + method: ["GET", "POST", "OPTIONS"], + handler: async (request, reply) => { + const response = await yoga.handleNodeRequestAndResponse( + request.raw, + reply.raw, + { reply, db, redis }, + ); + reply.status(response.status); + for (const [key, value] of response.headers.entries()) { + reply.header(key, value); + } + return reply.send(response.body); + }, + }); -fastify.get("/health", async (_request, reply) => { - return reply.send({ status: "ok", timestamp: new Date().toISOString() }); -}); + fastify.get("/health", async (_request, reply) => { + return reply.send({ status: "ok", timestamp: new Date().toISOString() }); + }); -try { - await fastify.listen({ port: PORT, host: "0.0.0.0" }); - fastify.log.info(`Backend running at http://0.0.0.0:${PORT}`); - fastify.log.info(`GraphQL at http://0.0.0.0:${PORT}/graphql`); -} catch (err) { - fastify.log.error(err); - process.exit(1); + try { + await fastify.listen({ port: PORT, host: "0.0.0.0" }); + fastify.log.info(`Backend running at http://0.0.0.0:${PORT}`); + fastify.log.info(`GraphQL at http://0.0.0.0:${PORT}/graphql`); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } } + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/packages/backend/src/lib/gamification.ts b/packages/backend/src/lib/gamification.ts index f8c6c56..d5f3ed9 100644 --- a/packages/backend/src/lib/gamification.ts +++ b/packages/backend/src/lib/gamification.ts @@ -1,5 +1,5 @@ import { eq, sql, and, gt, isNotNull, count, sum } from "drizzle-orm"; -import type { DB } from "../db/connection.js"; +import type { DB } from "../db/connection"; import { user_points, user_stats, @@ -9,7 +9,7 @@ import { user_achievements, achievements, users, -} from "../db/schema/index.js"; +} from "../db/schema/index"; export const POINT_VALUES = { RECORDING_CREATE: 50, diff --git a/packages/backend/src/migrations/0000_pale_hellion.sql b/packages/backend/src/migrations/0000_pale_hellion.sql new file mode 100644 index 0000000..b333b5a --- /dev/null +++ b/packages/backend/src/migrations/0000_pale_hellion.sql @@ -0,0 +1,233 @@ +CREATE TYPE "public"."achievement_status" AS ENUM('draft', 'published');--> statement-breakpoint +CREATE TYPE "public"."user_role" AS ENUM('model', 'viewer', 'admin');--> statement-breakpoint +CREATE TYPE "public"."recording_status" AS ENUM('draft', 'published', 'archived');--> statement-breakpoint +CREATE TABLE "articles" ( + "id" text PRIMARY KEY NOT NULL, + "slug" text NOT NULL, + "title" text NOT NULL, + "excerpt" text, + "content" text, + "image" text, + "tags" text[] DEFAULT '{}', + "publish_date" timestamp DEFAULT now() NOT NULL, + "author" text, + "category" text, + "featured" boolean DEFAULT false, + "date_created" timestamp DEFAULT now() NOT NULL, + "date_updated" timestamp +); +--> statement-breakpoint +CREATE TABLE "comments" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "comments_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "collection" text NOT NULL, + "item_id" text NOT NULL, + "comment" text NOT NULL, + "user_id" text NOT NULL, + "date_created" timestamp DEFAULT now() NOT NULL, + "date_updated" timestamp +); +--> statement-breakpoint +CREATE TABLE "files" ( + "id" text PRIMARY KEY NOT NULL, + "title" text, + "description" text, + "filename" text NOT NULL, + "mime_type" text, + "filesize" bigint, + "duration" integer, + "uploaded_by" text, + "date_created" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "achievements" ( + "id" text PRIMARY KEY NOT NULL, + "code" text NOT NULL, + "name" text NOT NULL, + "description" text, + "icon" text, + "category" text, + "required_count" integer DEFAULT 1 NOT NULL, + "points_reward" integer DEFAULT 0 NOT NULL, + "status" "achievement_status" DEFAULT 'published' NOT NULL, + "sort" integer DEFAULT 0 +); +--> statement-breakpoint +CREATE TABLE "user_achievements" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "user_achievements_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" text NOT NULL, + "achievement_id" text NOT NULL, + "progress" integer DEFAULT 0, + "date_unlocked" timestamp +); +--> statement-breakpoint +CREATE TABLE "user_points" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "user_points_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" text NOT NULL, + "action" text NOT NULL, + "points" integer NOT NULL, + "recording_id" text, + "date_created" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_stats" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "user_stats_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" text NOT NULL, + "total_raw_points" integer DEFAULT 0, + "total_weighted_points" real DEFAULT 0, + "recordings_count" integer DEFAULT 0, + "playbacks_count" integer DEFAULT 0, + "comments_count" integer DEFAULT 0, + "achievements_count" integer DEFAULT 0, + "last_updated" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "user_photos" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "user_photos_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" text NOT NULL, + "file_id" text NOT NULL, + "sort" integer DEFAULT 0 +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "password_hash" text NOT NULL, + "first_name" text, + "last_name" text, + "artist_name" text, + "slug" text, + "description" text, + "tags" text[] DEFAULT '{}', + "role" "user_role" DEFAULT 'viewer' NOT NULL, + "avatar" text, + "banner" text, + "email_verified" boolean DEFAULT false NOT NULL, + "email_verify_token" text, + "password_reset_token" text, + "password_reset_expiry" timestamp, + "date_created" timestamp DEFAULT now() NOT NULL, + "date_updated" timestamp +); +--> statement-breakpoint +CREATE TABLE "video_likes" ( + "id" text PRIMARY KEY NOT NULL, + "video_id" text NOT NULL, + "user_id" text NOT NULL, + "date_created" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "video_models" ( + "video_id" text NOT NULL, + "user_id" text NOT NULL, + CONSTRAINT "video_models_video_id_user_id_pk" PRIMARY KEY("video_id","user_id") +); +--> statement-breakpoint +CREATE TABLE "video_plays" ( + "id" text PRIMARY KEY NOT NULL, + "video_id" text NOT NULL, + "user_id" text, + "session_id" text, + "duration_watched" integer, + "completed" boolean DEFAULT false, + "date_created" timestamp DEFAULT now() NOT NULL, + "date_updated" timestamp +); +--> statement-breakpoint +CREATE TABLE "videos" ( + "id" text PRIMARY KEY NOT NULL, + "slug" text NOT NULL, + "title" text NOT NULL, + "description" text, + "image" text, + "movie" text, + "tags" text[] DEFAULT '{}', + "upload_date" timestamp DEFAULT now() NOT NULL, + "premium" boolean DEFAULT false, + "featured" boolean DEFAULT false, + "likes_count" integer DEFAULT 0, + "plays_count" integer DEFAULT 0 +); +--> statement-breakpoint +CREATE TABLE "recording_plays" ( + "id" text PRIMARY KEY NOT NULL, + "recording_id" text NOT NULL, + "user_id" text, + "duration_played" integer DEFAULT 0, + "completed" boolean DEFAULT false, + "date_created" timestamp DEFAULT now() NOT NULL, + "date_updated" timestamp +); +--> statement-breakpoint +CREATE TABLE "recordings" ( + "id" text PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "description" text, + "slug" text NOT NULL, + "duration" integer NOT NULL, + "events" jsonb DEFAULT '[]'::jsonb, + "device_info" jsonb DEFAULT '[]'::jsonb, + "user_id" text NOT NULL, + "status" "recording_status" DEFAULT 'draft' NOT NULL, + "tags" text[] DEFAULT '{}', + "linked_video" text, + "featured" boolean DEFAULT false, + "public" boolean DEFAULT false, + "original_recording_id" text, + "date_created" timestamp DEFAULT now() NOT NULL, + "date_updated" timestamp +); +--> statement-breakpoint +ALTER TABLE "articles" ADD CONSTRAINT "articles_image_files_id_fk" FOREIGN KEY ("image") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "articles" ADD CONSTRAINT "articles_author_users_id_fk" FOREIGN KEY ("author") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "comments" ADD CONSTRAINT "comments_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_achievements" ADD CONSTRAINT "user_achievements_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_achievements" ADD CONSTRAINT "user_achievements_achievement_id_achievements_id_fk" FOREIGN KEY ("achievement_id") REFERENCES "public"."achievements"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_points" ADD CONSTRAINT "user_points_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_points" ADD CONSTRAINT "user_points_recording_id_recordings_id_fk" FOREIGN KEY ("recording_id") REFERENCES "public"."recordings"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_stats" ADD CONSTRAINT "user_stats_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_photos" ADD CONSTRAINT "user_photos_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_photos" ADD CONSTRAINT "user_photos_file_id_files_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."files"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_avatar_files_id_fk" FOREIGN KEY ("avatar") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_banner_files_id_fk" FOREIGN KEY ("banner") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "video_likes" ADD CONSTRAINT "video_likes_video_id_videos_id_fk" FOREIGN KEY ("video_id") REFERENCES "public"."videos"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "video_likes" ADD CONSTRAINT "video_likes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "video_models" ADD CONSTRAINT "video_models_video_id_videos_id_fk" FOREIGN KEY ("video_id") REFERENCES "public"."videos"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "video_models" ADD CONSTRAINT "video_models_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "video_plays" ADD CONSTRAINT "video_plays_video_id_videos_id_fk" FOREIGN KEY ("video_id") REFERENCES "public"."videos"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "video_plays" ADD CONSTRAINT "video_plays_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "videos" ADD CONSTRAINT "videos_image_files_id_fk" FOREIGN KEY ("image") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "videos" ADD CONSTRAINT "videos_movie_files_id_fk" FOREIGN KEY ("movie") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recording_plays" ADD CONSTRAINT "recording_plays_recording_id_recordings_id_fk" FOREIGN KEY ("recording_id") REFERENCES "public"."recordings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recording_plays" ADD CONSTRAINT "recording_plays_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recordings" ADD CONSTRAINT "recordings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recordings" ADD CONSTRAINT "recordings_linked_video_videos_id_fk" FOREIGN KEY ("linked_video") REFERENCES "public"."videos"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "articles_slug_idx" ON "articles" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "articles_publish_date_idx" ON "articles" USING btree ("publish_date");--> statement-breakpoint +CREATE INDEX "articles_featured_idx" ON "articles" USING btree ("featured");--> statement-breakpoint +CREATE INDEX "comments_collection_item_idx" ON "comments" USING btree ("collection","item_id");--> statement-breakpoint +CREATE INDEX "comments_user_idx" ON "comments" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "files_uploaded_by_idx" ON "files" USING btree ("uploaded_by");--> statement-breakpoint +CREATE UNIQUE INDEX "achievements_code_idx" ON "achievements" USING btree ("code");--> statement-breakpoint +CREATE INDEX "user_achievements_user_idx" ON "user_achievements" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "user_achievements_unique_idx" ON "user_achievements" USING btree ("user_id","achievement_id");--> statement-breakpoint +CREATE INDEX "user_points_user_idx" ON "user_points" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "user_points_date_idx" ON "user_points" USING btree ("date_created");--> statement-breakpoint +CREATE UNIQUE INDEX "user_stats_user_idx" ON "user_stats" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "user_photos_user_idx" ON "user_photos" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");--> statement-breakpoint +CREATE UNIQUE INDEX "users_slug_idx" ON "users" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "users_role_idx" ON "users" USING btree ("role");--> statement-breakpoint +CREATE INDEX "video_likes_video_idx" ON "video_likes" USING btree ("video_id");--> statement-breakpoint +CREATE INDEX "video_likes_user_idx" ON "video_likes" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "video_plays_video_idx" ON "video_plays" USING btree ("video_id");--> statement-breakpoint +CREATE INDEX "video_plays_user_idx" ON "video_plays" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "video_plays_date_idx" ON "video_plays" USING btree ("date_created");--> statement-breakpoint +CREATE UNIQUE INDEX "videos_slug_idx" ON "videos" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "videos_upload_date_idx" ON "videos" USING btree ("upload_date");--> statement-breakpoint +CREATE INDEX "videos_featured_idx" ON "videos" USING btree ("featured");--> statement-breakpoint +CREATE INDEX "recording_plays_recording_idx" ON "recording_plays" USING btree ("recording_id");--> statement-breakpoint +CREATE INDEX "recording_plays_user_idx" ON "recording_plays" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "recordings_slug_idx" ON "recordings" USING btree ("slug");--> statement-breakpoint +CREATE INDEX "recordings_user_idx" ON "recordings" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "recordings_status_idx" ON "recordings" USING btree ("status");--> statement-breakpoint +CREATE INDEX "recordings_public_idx" ON "recordings" USING btree ("public"); \ No newline at end of file diff --git a/packages/backend/src/migrations/meta/0000_snapshot.json b/packages/backend/src/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..7432ace --- /dev/null +++ b/packages/backend/src/migrations/meta/0000_snapshot.json @@ -0,0 +1,1931 @@ +{ + "id": "96a0e6d2-35c5-43d6-a335-f066f56d6b42", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "excerpt": { + "name": "excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "publish_date": { + "name": "publish_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured": { + "name": "featured", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_updated": { + "name": "date_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "articles_slug_idx": { + "name": "articles_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_publish_date_idx": { + "name": "articles_publish_date_idx", + "columns": [ + { + "expression": "publish_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_featured_idx": { + "name": "articles_featured_idx", + "columns": [ + { + "expression": "featured", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "articles_image_files_id_fk": { + "name": "articles_image_files_id_fk", + "tableFrom": "articles", + "tableTo": "files", + "columnsFrom": [ + "image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "articles_author_users_id_fk": { + "name": "articles_author_users_id_fk", + "tableFrom": "articles", + "tableTo": "users", + "columnsFrom": [ + "author" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "comments_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "collection": { + "name": "collection", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_updated": { + "name": "date_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "comments_collection_item_idx": { + "name": "comments_collection_item_idx", + "columns": [ + { + "expression": "collection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "comments_user_idx": { + "name": "comments_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "files_uploaded_by_idx": { + "name": "files_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.achievements": { + "name": "achievements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "required_count": { + "name": "required_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "points_reward": { + "name": "points_reward", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "achievement_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'published'" + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "achievements_code_idx": { + "name": "achievements_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_achievements": { + "name": "user_achievements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_achievements_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "achievement_id": { + "name": "achievement_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "date_unlocked": { + "name": "date_unlocked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_achievements_user_idx": { + "name": "user_achievements_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_achievements_unique_idx": { + "name": "user_achievements_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "achievement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_achievements_user_id_users_id_fk": { + "name": "user_achievements_user_id_users_id_fk", + "tableFrom": "user_achievements", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_achievements_achievement_id_achievements_id_fk": { + "name": "user_achievements_achievement_id_achievements_id_fk", + "tableFrom": "user_achievements", + "tableTo": "achievements", + "columnsFrom": [ + "achievement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_points": { + "name": "user_points", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_points_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "recording_id": { + "name": "recording_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_points_user_idx": { + "name": "user_points_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_points_date_idx": { + "name": "user_points_date_idx", + "columns": [ + { + "expression": "date_created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_points_user_id_users_id_fk": { + "name": "user_points_user_id_users_id_fk", + "tableFrom": "user_points", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_points_recording_id_recordings_id_fk": { + "name": "user_points_recording_id_recordings_id_fk", + "tableFrom": "user_points", + "tableTo": "recordings", + "columnsFrom": [ + "recording_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_stats_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_raw_points": { + "name": "total_raw_points", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "total_weighted_points": { + "name": "total_weighted_points", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "recordings_count": { + "name": "recordings_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "playbacks_count": { + "name": "playbacks_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "comments_count": { + "name": "comments_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "achievements_count": { + "name": "achievements_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "user_stats_user_idx": { + "name": "user_stats_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_stats_user_id_users_id_fk": { + "name": "user_stats_user_id_users_id_fk", + "tableFrom": "user_stats", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_photos": { + "name": "user_photos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_photos_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort": { + "name": "sort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "user_photos_user_idx": { + "name": "user_photos_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_photos_user_id_users_id_fk": { + "name": "user_photos_user_id_users_id_fk", + "tableFrom": "user_photos", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_photos_file_id_files_id_fk": { + "name": "user_photos_file_id_files_id_fk", + "tableFrom": "user_photos", + "tableTo": "files", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artist_name": { + "name": "artist_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'viewer'" + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner": { + "name": "banner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verify_token": { + "name": "email_verify_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_reset_token": { + "name": "password_reset_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_reset_expiry": { + "name": "password_reset_expiry", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_updated": { + "name": "date_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_slug_idx": { + "name": "users_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_role_idx": { + "name": "users_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_avatar_files_id_fk": { + "name": "users_avatar_files_id_fk", + "tableFrom": "users", + "tableTo": "files", + "columnsFrom": [ + "avatar" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_banner_files_id_fk": { + "name": "users_banner_files_id_fk", + "tableFrom": "users", + "tableTo": "files", + "columnsFrom": [ + "banner" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.video_likes": { + "name": "video_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "video_id": { + "name": "video_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "video_likes_video_idx": { + "name": "video_likes_video_idx", + "columns": [ + { + "expression": "video_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "video_likes_user_idx": { + "name": "video_likes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "video_likes_video_id_videos_id_fk": { + "name": "video_likes_video_id_videos_id_fk", + "tableFrom": "video_likes", + "tableTo": "videos", + "columnsFrom": [ + "video_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "video_likes_user_id_users_id_fk": { + "name": "video_likes_user_id_users_id_fk", + "tableFrom": "video_likes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.video_models": { + "name": "video_models", + "schema": "", + "columns": { + "video_id": { + "name": "video_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "video_models_video_id_videos_id_fk": { + "name": "video_models_video_id_videos_id_fk", + "tableFrom": "video_models", + "tableTo": "videos", + "columnsFrom": [ + "video_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "video_models_user_id_users_id_fk": { + "name": "video_models_user_id_users_id_fk", + "tableFrom": "video_models", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "video_models_video_id_user_id_pk": { + "name": "video_models_video_id_user_id_pk", + "columns": [ + "video_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.video_plays": { + "name": "video_plays", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "video_id": { + "name": "video_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_watched": { + "name": "duration_watched", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "completed": { + "name": "completed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_updated": { + "name": "date_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "video_plays_video_idx": { + "name": "video_plays_video_idx", + "columns": [ + { + "expression": "video_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "video_plays_user_idx": { + "name": "video_plays_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "video_plays_date_idx": { + "name": "video_plays_date_idx", + "columns": [ + { + "expression": "date_created", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "video_plays_video_id_videos_id_fk": { + "name": "video_plays_video_id_videos_id_fk", + "tableFrom": "video_plays", + "tableTo": "videos", + "columnsFrom": [ + "video_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "video_plays_user_id_users_id_fk": { + "name": "video_plays_user_id_users_id_fk", + "tableFrom": "video_plays", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.videos": { + "name": "videos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "movie": { + "name": "movie", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "upload_date": { + "name": "upload_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "premium": { + "name": "premium", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "featured": { + "name": "featured", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "likes_count": { + "name": "likes_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "plays_count": { + "name": "plays_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + } + }, + "indexes": { + "videos_slug_idx": { + "name": "videos_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "videos_upload_date_idx": { + "name": "videos_upload_date_idx", + "columns": [ + { + "expression": "upload_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "videos_featured_idx": { + "name": "videos_featured_idx", + "columns": [ + { + "expression": "featured", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "videos_image_files_id_fk": { + "name": "videos_image_files_id_fk", + "tableFrom": "videos", + "tableTo": "files", + "columnsFrom": [ + "image" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "videos_movie_files_id_fk": { + "name": "videos_movie_files_id_fk", + "tableFrom": "videos", + "tableTo": "files", + "columnsFrom": [ + "movie" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recording_plays": { + "name": "recording_plays", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "recording_id": { + "name": "recording_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_played": { + "name": "duration_played", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "completed": { + "name": "completed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_updated": { + "name": "date_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "recording_plays_recording_idx": { + "name": "recording_plays_recording_idx", + "columns": [ + { + "expression": "recording_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_plays_user_idx": { + "name": "recording_plays_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recording_plays_recording_id_recordings_id_fk": { + "name": "recording_plays_recording_id_recordings_id_fk", + "tableFrom": "recording_plays", + "tableTo": "recordings", + "columnsFrom": [ + "recording_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recording_plays_user_id_users_id_fk": { + "name": "recording_plays_user_id_users_id_fk", + "tableFrom": "recording_plays", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recordings": { + "name": "recordings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "device_info": { + "name": "device_info", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "recording_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "linked_video": { + "name": "linked_video", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured": { + "name": "featured", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "original_recording_id": { + "name": "original_recording_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date_created": { + "name": "date_created", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "date_updated": { + "name": "date_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "recordings_slug_idx": { + "name": "recordings_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recordings_user_idx": { + "name": "recordings_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recordings_status_idx": { + "name": "recordings_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recordings_public_idx": { + "name": "recordings_public_idx", + "columns": [ + { + "expression": "public", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recordings_user_id_users_id_fk": { + "name": "recordings_user_id_users_id_fk", + "tableFrom": "recordings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recordings_linked_video_videos_id_fk": { + "name": "recordings_linked_video_videos_id_fk", + "tableFrom": "recordings", + "tableTo": "videos", + "columnsFrom": [ + "linked_video" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.achievement_status": { + "name": "achievement_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "model", + "viewer", + "admin" + ] + }, + "public.recording_status": { + "name": "recording_status", + "schema": "public", + "values": [ + "draft", + "published", + "archived" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/src/migrations/meta/_journal.json b/packages/backend/src/migrations/meta/_journal.json new file mode 100644 index 0000000..99831ca --- /dev/null +++ b/packages/backend/src/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1772645674513, + "tag": "0000_pale_hellion", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/backend/src/scripts/migrate.ts b/packages/backend/src/scripts/migrate.ts new file mode 100644 index 0000000..6d666d5 --- /dev/null +++ b/packages/backend/src/scripts/migrate.ts @@ -0,0 +1,23 @@ +import { Pool } from "pg"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import path from "path"; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL || "postgresql://sexy:sexy@localhost:5432/sexy", +}); + +const db = drizzle(pool); + +async function main() { + console.log("Running schema migrations..."); + const migrationsFolder = path.join(__dirname, "../../migrations"); + await migrate(db, { migrationsFolder }); + console.log("Schema migrations complete."); + await pool.end(); +} + +main().catch((err) => { + console.error("Migration failed:", err); + process.exit(1); +}); diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index a11f684..b100897 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "CommonJS", + "moduleResolution": "Node", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src",