diff --git a/app/(app)/admin/_client.tsx b/app/(admin)/admin/_client.tsx similarity index 99% rename from app/(app)/admin/_client.tsx rename to app/(admin)/admin/_client.tsx index 0c9166e3f..35c33d90a 100644 --- a/app/(app)/admin/_client.tsx +++ b/app/(admin)/admin/_client.tsx @@ -77,7 +77,7 @@ const AdminDashboard = () => { const { data: reportCounts } = api.report.getCounts.useQuery(); return ( -
{"// "}admin
diff --git a/app/(app)/admin/moderation/_client.tsx b/app/(admin)/admin/moderation/_client.tsx
similarity index 100%
rename from app/(app)/admin/moderation/_client.tsx
rename to app/(admin)/admin/moderation/_client.tsx
diff --git a/app/(app)/admin/moderation/page.tsx b/app/(admin)/admin/moderation/page.tsx
similarity index 50%
rename from app/(app)/admin/moderation/page.tsx
rename to app/(admin)/admin/moderation/page.tsx
index 66132c76c..86157e85c 100644
--- a/app/(app)/admin/moderation/page.tsx
+++ b/app/(admin)/admin/moderation/page.tsx
@@ -1,5 +1,3 @@
-import { getServerAuthSession } from "@/server/auth";
-import { redirect } from "next/navigation";
import { Suspense } from "react";
import ModerationQueue from "./_client";
@@ -8,13 +6,8 @@ export const metadata = {
description: "Review and manage reported content",
};
-export default async function Page() {
- const session = await getServerAuthSession();
-
- if (!session?.user || session.user.role !== "ADMIN") {
- redirect("/");
- }
-
+// Admin-role gate is enforced in app/(admin)/layout.tsx.
+export default function Page() {
return (
${flagsThisRun} new auto-flag(s); ${pendingReports} item(s) waiting in the moderation queue.
+ {"// "}admin
+ Codú daily review
+
+ ${rows
+ .map(
+ ([label, value]) =>
+ `
+
+ `;
+
+ await sendEmail({
+ recipient: env.ADMIN_EMAIL,
+ subject: `Codú daily review — ${flagsThisRun} new flag(s), ${pendingReports} pending`,
+ htmlMessage,
+ });
+ return true;
+}
+
+async function loadVocab(): Promise<{
+ vocab: TopicVocabEntry[];
+ slugToId: Map `,
+ )
+ .join("")}
+ ${label} ${value}
+ );
+ }
+ return (
+
+ {name?.[0]?.toUpperCase() || "C"}
+
+ );
+}
diff --git a/docs/plans/2026-06-14-admin-shell-and-ai-content-design.md b/docs/plans/2026-06-14-admin-shell-and-ai-content-design.md
new file mode 100644
index 000000000..e27bcc6f7
--- /dev/null
+++ b/docs/plans/2026-06-14-admin-shell-and-ai-content-design.md
@@ -0,0 +1,284 @@
+# Admin shell + AI content pipeline — design
+
+Date: 2026-06-14
+Status: design approved (brainstormed with founder); admin shell to be built first.
+
+## Why
+
+Two problems, one foundation:
+
+1. **The admin dashboard is crushed.** Admin pages live at `app/(app)/admin/*`, so
+ they inherit `(app)/layout.tsx` → `AppShell`, which wraps every child in the
+ public 3-column rail grid (`LeftRail` / narrow center / `RightRail`). Management
+ tables get the ~600px center column the feed uses. Admin is private — it should
+ not share the public member chrome at all.
+2. **Managing the platform is manual.** As a solo founder, auditing content,
+ moderation, users, and quality needs to be a glance, not a dig. We also want
+ AI-derived topic/sentiment/quality signals so the feed can be personalized.
+
+These connect: the admin shell becomes the cockpit for the AI content pipeline.
+
+## Scope decision
+
+- Write this one design doc covering all three efforts (admin shell, AI review
+ cron, personalization).
+- **Build the admin shell now.** The cron, AI metadata, and personalized ranking
+ are designed here and built as later phases.
+
+---
+
+## 1. Admin shell — route-group as layout boundary
+
+### The mental model
+
+Next.js route groups are already the "back out of a layout" mechanism. The top
+level already has siblings, each its own layout world:
+
+```
+app/
+ (app)/ ← the rail shell (LeftRail / center / RightRail). The social surface.
+ (auth)/ ← auth chrome
+ (editor)/ ← editor chrome
+ (marketing)/ ← marketing chrome
+ (admin)/ ← NEW: AdminShell. Private management. Full width, own nav.
+```
+
+`AppShell` is **not global** — it is scoped to `(app)`. A sibling group escapes it
+structurally, with no runtime flag. The only reason it _feels_ global is that
+nearly everything was dropped into `(app)`, and the two pages that wanted no rails
+(`/speakers`, `/volunteer`) used a runtime hack: the `BARE_ROUTES` array inside
+`AppShell` that conditionally drops the rails. That hack is the smell — a page in
+the shell group that doesn't want the shell.
+
+**Rule going forward:** a page that does not want the rail shell does not live in
+`(app)`. Pick the sibling group whose chrome fits, or add a new group. No runtime
+flags, no fighting a parent layout.
+
+Future non-shell pages ("other things I'll want up") are deferred — but the pattern
+is documented so adding a `(bare)` public group or more admin tools later is a
+structural 2-minute move with no rework. Optional cleanup (not in this work):
+retire `BARE_ROUTES` by moving `/speakers` + `/volunteer` into a `(bare)` group.
+
+### The structure to build
+
+```
+app/(admin)/
+ layout.tsx ← AdminShell + admin-gate (role check) enforced ONCE here
+ admin/
+ page.tsx ← Overview dashboard (moved)
+ users/ page.tsx + _client.tsx (moved)
+ sources/ page.tsx + _client.tsx (moved)
+ tags/ page.tsx + _client.tsx (moved)
+ moderation/ page.tsx + _client.tsx (moved)
+```
+
+Route groups don't change URLs — every `/admin/*` link, bookmark, and redirect
+keeps working. The page files move from `(app)/admin/*` to `(admin)/admin/*`.
+
+### AdminShell
+
+- **Left sidebar** (persistent): Overview, Moderation, Users, Sources, Tags,
+ and placeholders for the new surfaces — Content, Insights, Settings. Active-state
+ styling mirrors `LeftRail`.
+- **Slim top bar**: page title / breadcrumb, "← Back to site" link, founder avatar.
+- **Full-width fluid content** (`max-w-screen-2xl`, real padding) so tables breathe.
+- Reuses existing design tokens (`bg-canvas`, `border-hairline`, `font-display`,
+ the `eyebrow` / `slash` motifs) so it reads as Codú, not a bolted-on admin theme.
+
+### Auth
+
+The `session.user.role !== "ADMIN"` → `redirect("/")` gate moves into
+`(admin)/layout.tsx`, enforced once for the whole section. Each `page.tsx` drops
+its own gate. No engagement side-effects (`recordDailyActivity`, `ensureReferral`)
+or public rails run here — it's private.
+
+---
+
+## 2. AI metadata data model (provenance-aware)
+
+Metadata is a general layer that BOTH the founder (manual) and the cron (AI) write
+to. Provenance is tracked via a `source` field so manual tags are authoritative and
+the nightly job never overwrites them.
+
+### `post_metadata` (1:1 with posts) — per-post signal envelope
+
+```
+postId uuid PK/FK → posts.id (cascade)
+sentiment varchar -- nullable; "positive" | "neutral" | "negative"
+sentimentScore real -- -1..1
+qualityScore real -- 0..1 (spam / low-effort signal)
+qualityReason text -- short model rationale
+modelId text -- Bedrock model that produced it; null if human-set
+analyzedAt timestamptz -- last AI pass; THIS IS THE INCREMENTAL WATERMARK
+schemaVersion integer -- bump to force re-analysis of everything
+```
+
+A separate 1:1 table (not columns on `posts`) keeps the hot `posts` row lean and
+lets the cron write without bumping `posts.updatedAt`.
+
+### Controlled topic vocabulary
+
+LLM free-text drifts ("RAG" / "rag" / "retrieval-augmented"). Topics resolve
+against a curated list so manual + AI tags share one clean namespace.
+
+```
+topic id, slug, label, status(active|pending), createdAt
+ -- seeded: rag, agents, prompting, evals, nextjs, indie-hacking,
+ -- fundraising, ... ; model picks from the list, may propose
+ -- new ones into `pending` for founder approval in admin.
+
+post_topic postId uuid FK, topicId int FK, confidence real (nullable for manual),
+ source varchar ("ai" | "manual"), createdAt timestamptz,
+ PRIMARY KEY (postId, topicId)
+```
+
+`post_metadata` = per-post AI verdict; `post_topic` = normalized, queryable topic
+edges that power ranking. Human `Tag` / `post_tags` stays untouched.
+
+> Note: a `profile.myInterests` query + `openTopics` action already exist in the
+> rail ("Your topics"). The personalization layer should reconcile with / build on
+> that existing interest concept rather than introduce a parallel one.
+
+### The coexistence rule (manual + AI)
+
+- The cron only ever **deletes and rewrites `source = 'ai'`** rows in `post_topic`.
+ `source = 'manual'` edges are never touched.
+- For `post_metadata`, a manually-set field leaves a marker (e.g. `modelId = null`)
+ that tells the cron to skip overwriting it.
+- So the founder can hand-tag a post; the nightly job fills the blanks around it.
+
+---
+
+## 3. Nightly review cron — incremental, comment-aware
+
+One route, **`/api/cron/daily-review`** (Bearer `CRON_SECRET`, same auth as
+`promote-scheduled`), invoked by an EventBridge rule + Lambda invoker — the
+established pattern in `cdk/lib/cron-stack.ts`.
+
+### Incremental worklist ("never re-tag unchanged content")
+
+`analyzedAt` per row IS the watermark — no fragile global cursor.
+
+- **Posts to analyze:** `posts LEFT JOIN post_metadata` where
+ `analyzedAt IS NULL` **OR** `posts.updatedAt > analyzedAt` **OR**
+ `schemaVersion < N`. Unchanged-since-last-analysis posts are not in the worklist.
+ Editing a post re-enters it; bumping `schemaVersion` re-enters everything.
+- **Comments to moderate:** same idea via a `moderatedAt` column on comments —
+ only new/edited comments are screened.
+- **Empty worklist → no-op.** Nothing new, nothing runs, near-zero cost.
+
+### Robustness
+
+- **Capped per run** (e.g. 100 posts / 200 comments), like `promote-scheduled`'s
+ `limit(100)`. Leftovers roll to the next run — a backfill can't blow the Lambda
+ timeout.
+- **Per-item try/catch + Sentry** — one bad row never kills the batch (fail-open,
+ matching `autoReview`).
+- **Bedrock gate** — `isBedrockEnabled()` false → moderation falls back to the
+ `screenContent` heuristic; tagging/quality passes are skipped gracefully.
+
+### The four passes
+
+1. **Topic + sentiment tagging** (posts) → writes `post_metadata` + `source='ai'`
+ edges in `post_topic`, never touching manual edges. Reuses the Bedrock
+ `InvokeModel` plumbing from `autoReview.ts`.
+2. **Re-screen moderation** (posts AND comments) → anything flagged becomes a row
+ in the existing `reports` queue (see below), surfacing in the moderation UI.
+3. **Quality / spam scoring** (posts) → fills `qualityScore` / `qualityReason`.
+4. **Daily digest** → counts the day (new users / posts / comments, flags raised,
+ pending queue) and **emails the founder only when something needs attention**;
+ otherwise just updates a dashboard widget. No daily noise.
+
+### AI flags → existing moderation queue (schema change)
+
+Extend `reports` so AI flags share the one queue the founder already checks:
+
+```
+reports.source varchar default "user" -- "user" | "system"
+reports.reporterId -> make NULLABLE -- system flags have no human reporter
+```
+
+AI-raised reports render in the existing moderation UI tagged "auto-flagged."
+One queue, one place to look.
+
+---
+
+## 4. Personalized feed ranking (last phase)
+
+Built on transparent, explicit signals first — not an opaque model — so it stays
+debuggable for a solo founder.
+
+### Interest profile — two sources
+
+1. **Explicit (ship first):** users follow / mute topics.
+ `user_topic_pref (userId, topicId, pref: follow|mute)`. Reconcile with the
+ existing `myInterests` / "Your topics" UI. Followed topics boost; muted are
+ filtered out. Predictable, user-controlled.
+2. **Implicit affinity (layer after):**
+ `user_topic_affinity (userId, topicId, score, updatedAt)`, computed by an
+ incremental job from existing interactions — `post_votes` (strong),
+ `bookmarks` (strong), `comments` (medium), views (weak) — mapped through each
+ post's `post_topic` edges, with time decay so interests stay current.
+
+### Ranking = transparent weighted blend (server-side)
+
+```
+score = w1·recency
+ + w2·baseQuality (votes / existing signals)
+ + w3·topicAffinity (explicit follows + implicit score)
+ − muteFilter
+```
+
+Weights in config so they're tunable. A sum of named terms means "why did this
+rank here?" is always answerable.
+
+### Cold start & safety
+
+No profile (logged-out / new user) → today's chronological/trending feed,
+unchanged. Personalization is purely additive. Everything keys off the
+`post_topic` edges from §2 — no new content analysis needed. Per-user ranked feed
+cached with a short TTL; affinity recompute is incremental (active users only).
+
+**Recommendation:** build explicit follow/mute first (most of the value, a
+fraction of the complexity), add implicit affinity once topics are flowing.
+
+---
+
+## 5. Phasing + "robust to manage as a solo founder"
+
+### Phases
+
+- **Phase 1 (this work): admin shell.** `(admin)` route group, AdminShell, move
+ pages, centralize auth. Unblocks everything else by giving the cockpit room.
+- **Phase 2: AI metadata + nightly cron.** Schema (`post_metadata`, `topic`,
+ `post_topic`, `comments.moderatedAt`, `reports.source`/nullable reporter),
+ `/api/cron/daily-review`, EventBridge + Lambda, admin **Content** + **Insights**
+ views (review AI tags, approve pending topics, see flags/quality).
+- **Phase 3: personalization.** Explicit follow/mute → ranked feed; then implicit
+ affinity.
+
+### Robustness ideas to fold into the admin cockpit
+
+- **One moderation queue** — user reports + AI flags together (§3).
+- **Daily digest** — the day on one screen; email only when action is needed.
+- **Audit log** — record admin actions (bans, deletes, topic approvals) so a solo
+ founder has a paper trail.
+- **Quality/spam surfacing** — sort the Content view by `qualityScore` to find
+ low-effort content fast; down-rank rather than delete where possible.
+- **Pending-topic approval** — keep the topic vocabulary clean with one click.
+- **Everything incremental + fail-open** — jobs skip when there's nothing to do and
+ never block the platform on an AI/infra failure.
+
+---
+
+## Implementation notes (Phase 1)
+
+- Create `app/(admin)/layout.tsx` (server component): fetch session, gate on
+ `role === "ADMIN"`, render `AdminShell`.
+- Create `components/Admin/AdminShell.tsx` (+ sidebar nav, top bar) reusing tokens.
+- `git mv` the five page directories from `app/(app)/admin/` to `app/(admin)/admin/`.
+- Strip the per-page `getServerAuthSession` + redirect gate from the moved
+ `page.tsx` files (now handled by the layout); keep any page-specific data
+ fetching.
+- Verify: `next build` / typecheck, and that `/admin` + each subroute renders
+ full-width without the public rails.
diff --git a/drizzle/0038_ai_content_metadata.sql b/drizzle/0038_ai_content_metadata.sql
new file mode 100644
index 000000000..0d0b172e9
--- /dev/null
+++ b/drizzle/0038_ai_content_metadata.sql
@@ -0,0 +1,85 @@
+CREATE TYPE "public"."report_source" AS ENUM('user', 'system');--> statement-breakpoint
+CREATE TYPE "public"."sentiment" AS ENUM('positive', 'neutral', 'negative');--> statement-breakpoint
+CREATE TYPE "public"."tag_source" AS ENUM('ai', 'manual');--> statement-breakpoint
+CREATE TYPE "public"."topic_status" AS ENUM('active', 'pending');--> statement-breakpoint
+CREATE TABLE "post_metadata" (
+ "post_id" uuid PRIMARY KEY NOT NULL,
+ "sentiment" "sentiment",
+ "sentiment_score" real,
+ "quality_score" real,
+ "quality_reason" text,
+ "model_id" text,
+ "analyzed_at" timestamp(3) with time zone,
+ "schema_version" integer DEFAULT 1 NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "post_topic" (
+ "post_id" uuid NOT NULL,
+ "topic_id" integer NOT NULL,
+ "confidence" real,
+ "source" "tag_source" DEFAULT 'ai' NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ CONSTRAINT "post_topic_post_id_topic_id_pk" PRIMARY KEY("post_id","topic_id")
+);
+--> statement-breakpoint
+CREATE TABLE "topic" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "slug" varchar(60) NOT NULL,
+ "label" varchar(80) NOT NULL,
+ "status" "topic_status" DEFAULT 'active' NOT NULL,
+ "created_at" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "reports" ALTER COLUMN "reporter_id" DROP NOT NULL;--> statement-breakpoint
+ALTER TABLE "comments" ADD COLUMN "moderated_at" timestamp(3) with time zone;--> statement-breakpoint
+ALTER TABLE "reports" ADD COLUMN "source" "report_source" DEFAULT 'user' NOT NULL;--> statement-breakpoint
+ALTER TABLE "post_metadata" ADD CONSTRAINT "post_metadata_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "post_topic" ADD CONSTRAINT "post_topic_post_id_posts_id_fk" FOREIGN KEY ("post_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "post_topic" ADD CONSTRAINT "post_topic_topic_id_topic_id_fk" FOREIGN KEY ("topic_id") REFERENCES "public"."topic"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "post_metadata_analyzed_at_idx" ON "post_metadata" USING btree ("analyzed_at");--> statement-breakpoint
+CREATE INDEX "post_topic_topic_id_idx" ON "post_topic" USING btree ("topic_id");--> statement-breakpoint
+CREATE INDEX "post_topic_source_idx" ON "post_topic" USING btree ("source");--> statement-breakpoint
+CREATE UNIQUE INDEX "topic_slug_key" ON "topic" USING btree ("slug");--> statement-breakpoint
+CREATE INDEX "topic_status_idx" ON "topic" USING btree ("status");--> statement-breakpoint
+-- Seed the controlled topic vocabulary (idempotent). The nightly review cron
+-- maps AI-suggested topics to these slugs; the model may propose new ones into
+-- `status = 'pending'` for admin approval.
+INSERT INTO "topic" ("slug", "label", "status") VALUES
+ ('ai-agents', 'AI Agents', 'active'),
+ ('rag', 'RAG', 'active'),
+ ('prompt-engineering', 'Prompt Engineering', 'active'),
+ ('evals', 'Evals', 'active'),
+ ('llm-apps', 'LLM Apps', 'active'),
+ ('fine-tuning', 'Fine-tuning', 'active'),
+ ('vector-databases', 'Vector Databases', 'active'),
+ ('ai-coding-tools', 'AI Coding Tools', 'active'),
+ ('mcp', 'MCP', 'active'),
+ ('open-models', 'Open Models', 'active'),
+ ('computer-vision', 'Computer Vision', 'active'),
+ ('voice-ai', 'Voice AI', 'active'),
+ ('nextjs', 'Next.js', 'active'),
+ ('react', 'React', 'active'),
+ ('typescript', 'TypeScript', 'active'),
+ ('python', 'Python', 'active'),
+ ('frontend', 'Frontend', 'active'),
+ ('backend', 'Backend', 'active'),
+ ('databases', 'Databases', 'active'),
+ ('devops', 'DevOps', 'active'),
+ ('web-performance', 'Web Performance', 'active'),
+ ('css', 'CSS', 'active'),
+ ('security', 'Security', 'active'),
+ ('data-engineering', 'Data Engineering', 'active'),
+ ('mobile', 'Mobile', 'active'),
+ ('open-source', 'Open Source', 'active'),
+ ('indie-hacking', 'Indie Hacking', 'active'),
+ ('build-in-public', 'Build in Public', 'active'),
+ ('saas', 'SaaS', 'active'),
+ ('bootstrapping', 'Bootstrapping', 'active'),
+ ('fundraising', 'Fundraising', 'active'),
+ ('growth-marketing', 'Growth & Marketing', 'active'),
+ ('product', 'Product', 'active'),
+ ('design', 'Design', 'active'),
+ ('career', 'Career', 'active'),
+ ('startups', 'Startups', 'active'),
+ ('no-code', 'No-code', 'active')
+ON CONFLICT ("slug") DO NOTHING;
\ No newline at end of file
diff --git a/drizzle/meta/0038_snapshot.json b/drizzle/meta/0038_snapshot.json
new file mode 100644
index 000000000..e42812c05
--- /dev/null
+++ b/drizzle/meta/0038_snapshot.json
@@ -0,0 +1,6155 @@
+{
+ "id": "4e6b6c33-b5ca-494b-9e68-7b370d344ffc",
+ "prevId": "53405aa0-451c-400b-b4a2-96d9e08c5833",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_userId_user_id_fk": {
+ "name": "account_userId_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_providerAccountId_pk": {
+ "name": "account_provider_providerAccountId_pk",
+ "columns": ["provider", "providerAccountId"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.AggregatedArticle": {
+ "name": "AggregatedArticle",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "sourceId": {
+ "name": "sourceId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "shortId": {
+ "name": "shortId",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(350)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalUrl": {
+ "name": "externalUrl",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imageUrl": {
+ "name": "imageUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ogImageUrl": {
+ "name": "ogImageUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sourceAuthor": {
+ "name": "sourceAuthor",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "publishedAt": {
+ "name": "publishedAt",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fetchedAt": {
+ "name": "fetchedAt",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "upvotes": {
+ "name": "upvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downvotes": {
+ "name": "downvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "clickCount": {
+ "name": "clickCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "aggregated_article_source_idx": {
+ "name": "aggregated_article_source_idx",
+ "columns": [
+ {
+ "expression": "sourceId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "aggregated_article_slug_idx": {
+ "name": "aggregated_article_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "aggregated_article_published_idx": {
+ "name": "aggregated_article_published_idx",
+ "columns": [
+ {
+ "expression": "publishedAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "aggregated_article_url_idx": {
+ "name": "aggregated_article_url_idx",
+ "columns": [
+ {
+ "expression": "externalUrl",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "AggregatedArticle_sourceId_FeedSource_id_fk": {
+ "name": "AggregatedArticle_sourceId_FeedSource_id_fk",
+ "tableFrom": "AggregatedArticle",
+ "tableTo": "FeedSource",
+ "columnsFrom": ["sourceId"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.AggregatedArticleBookmark": {
+ "name": "AggregatedArticleBookmark",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "articleId": {
+ "name": "articleId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "article_bookmark_unique": {
+ "name": "article_bookmark_unique",
+ "columns": [
+ {
+ "expression": "articleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "article_bookmark_user_idx": {
+ "name": "article_bookmark_user_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "AggregatedArticleBookmark_articleId_AggregatedArticle_id_fk": {
+ "name": "AggregatedArticleBookmark_articleId_AggregatedArticle_id_fk",
+ "tableFrom": "AggregatedArticleBookmark",
+ "tableTo": "AggregatedArticle",
+ "columnsFrom": ["articleId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "AggregatedArticleBookmark_userId_user_id_fk": {
+ "name": "AggregatedArticleBookmark_userId_user_id_fk",
+ "tableFrom": "AggregatedArticleBookmark",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.AggregatedArticleTag": {
+ "name": "AggregatedArticleTag",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "articleId": {
+ "name": "articleId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tagId": {
+ "name": "tagId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "article_tag_unique": {
+ "name": "article_tag_unique",
+ "columns": [
+ {
+ "expression": "articleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tagId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "article_tag_article_idx": {
+ "name": "article_tag_article_idx",
+ "columns": [
+ {
+ "expression": "articleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "AggregatedArticleTag_articleId_AggregatedArticle_id_fk": {
+ "name": "AggregatedArticleTag_articleId_AggregatedArticle_id_fk",
+ "tableFrom": "AggregatedArticleTag",
+ "tableTo": "AggregatedArticle",
+ "columnsFrom": ["articleId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "AggregatedArticleTag_tagId_Tag_id_fk": {
+ "name": "AggregatedArticleTag_tagId_Tag_id_fk",
+ "tableFrom": "AggregatedArticleTag",
+ "tableTo": "Tag",
+ "columnsFrom": ["tagId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.AggregatedArticleVote": {
+ "name": "AggregatedArticleVote",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "articleId": {
+ "name": "articleId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "voteType": {
+ "name": "voteType",
+ "type": "VoteType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "article_vote_unique": {
+ "name": "article_vote_unique",
+ "columns": [
+ {
+ "expression": "articleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "article_vote_article_idx": {
+ "name": "article_vote_article_idx",
+ "columns": [
+ {
+ "expression": "articleId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "article_vote_user_idx": {
+ "name": "article_vote_user_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "AggregatedArticleVote_articleId_AggregatedArticle_id_fk": {
+ "name": "AggregatedArticleVote_articleId_AggregatedArticle_id_fk",
+ "tableFrom": "AggregatedArticleVote",
+ "tableTo": "AggregatedArticle",
+ "columnsFrom": ["articleId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "AggregatedArticleVote_userId_user_id_fk": {
+ "name": "AggregatedArticleVote_userId_user_id_fk",
+ "tableFrom": "AggregatedArticleVote",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.badge": {
+ "name": "badge",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(60)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "varchar(8)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "badge_key_unique": {
+ "name": "badge_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.BannedUsers": {
+ "name": "BannedUsers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bannedById": {
+ "name": "bannedById",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "BannedUsers_userId_key": {
+ "name": "BannedUsers_userId_key",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "BannedUsers_userId_user_id_fk": {
+ "name": "BannedUsers_userId_user_id_fk",
+ "tableFrom": "BannedUsers",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "BannedUsers_bannedById_user_id_fk": {
+ "name": "BannedUsers_bannedById_user_id_fk",
+ "tableFrom": "BannedUsers",
+ "tableTo": "user",
+ "columnsFrom": ["bannedById"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "BannedUsers_id_unique": {
+ "name": "BannedUsers_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Bookmark": {
+ "name": "Bookmark",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "postId": {
+ "name": "postId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "Bookmark_userId_postId_key": {
+ "name": "Bookmark_userId_postId_key",
+ "columns": [
+ {
+ "expression": "postId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Bookmark_postId_Post_id_fk": {
+ "name": "Bookmark_postId_Post_id_fk",
+ "tableFrom": "Bookmark",
+ "tableTo": "Post",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Bookmark_userId_user_id_fk": {
+ "name": "Bookmark_userId_user_id_fk",
+ "tableFrom": "Bookmark",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Bookmark_id_unique": {
+ "name": "Bookmark_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.bookmarks": {
+ "name": "bookmarks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "bookmarks_user_id_idx": {
+ "name": "bookmarks_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_post_id_idx": {
+ "name": "bookmarks_post_id_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "bookmarks_post_id_posts_id_fk": {
+ "name": "bookmarks_post_id_posts_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "bookmarks_user_id_user_id_fk": {
+ "name": "bookmarks_user_id_user_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "bookmarks_post_id_user_id_key": {
+ "name": "bookmarks_post_id_user_id_key",
+ "nullsNotDistinct": false,
+ "columns": ["post_id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Comment": {
+ "name": "Comment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "postId": {
+ "name": "postId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parentId": {
+ "name": "parentId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "Comment_postId_index": {
+ "name": "Comment_postId_index",
+ "columns": [
+ {
+ "expression": "postId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Comment_postId_Post_id_fk": {
+ "name": "Comment_postId_Post_id_fk",
+ "tableFrom": "Comment",
+ "tableTo": "Post",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Comment_userId_user_id_fk": {
+ "name": "Comment_userId_user_id_fk",
+ "tableFrom": "Comment",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Comment_parentId_fkey": {
+ "name": "Comment_parentId_fkey",
+ "tableFrom": "Comment",
+ "tableTo": "Comment",
+ "columnsFrom": ["parentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Comment_id_unique": {
+ "name": "Comment_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.comment_votes": {
+ "name": "comment_votes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vote_type": {
+ "name": "vote_type",
+ "type": "vote_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "comment_votes_comment_id_idx": {
+ "name": "comment_votes_comment_id_idx",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "comment_votes_comment_id_comments_id_fk": {
+ "name": "comment_votes_comment_id_comments_id_fk",
+ "tableFrom": "comment_votes",
+ "tableTo": "comments",
+ "columnsFrom": ["comment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comment_votes_user_id_user_id_fk": {
+ "name": "comment_votes_user_id_user_id_fk",
+ "tableFrom": "comment_votes",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "comment_votes_comment_id_user_id_key": {
+ "name": "comment_votes_comment_id_user_id_key",
+ "nullsNotDistinct": false,
+ "columns": ["comment_id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.comments": {
+ "name": "comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "depth": {
+ "name": "depth",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "upvotes_count": {
+ "name": "upvotes_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downvotes_count": {
+ "name": "downvotes_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "moderated_at": {
+ "name": "moderated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "legacy_comment_id": {
+ "name": "legacy_comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "comments_post_id_idx": {
+ "name": "comments_post_id_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comments_author_id_idx": {
+ "name": "comments_author_id_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comments_parent_id_idx": {
+ "name": "comments_parent_id_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comments_created_at_idx": {
+ "name": "comments_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "comments_legacy_comment_id_idx": {
+ "name": "comments_legacy_comment_id_idx",
+ "columns": [
+ {
+ "expression": "legacy_comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "comments_post_id_posts_id_fk": {
+ "name": "comments_post_id_posts_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comments_author_id_user_id_fk": {
+ "name": "comments_author_id_user_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "user",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comments_parent_id_fkey": {
+ "name": "comments_parent_id_fkey",
+ "tableFrom": "comments",
+ "tableTo": "comments",
+ "columnsFrom": ["parent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Content": {
+ "name": "Content",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "ContentType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalUrl": {
+ "name": "externalUrl",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "imageUrl": {
+ "name": "imageUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ogImageUrl": {
+ "name": "ogImageUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sourceId": {
+ "name": "sourceId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sourceAuthor": {
+ "name": "sourceAuthor",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published": {
+ "name": "published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "publishedAt": {
+ "name": "publishedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "upvotes": {
+ "name": "upvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downvotes": {
+ "name": "downvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "readTimeMins": {
+ "name": "readTimeMins",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "clickCount": {
+ "name": "clickCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "canonicalUrl": {
+ "name": "canonicalUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "coverImage": {
+ "name": "coverImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "showComments": {
+ "name": "showComments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "Content_slug_key": {
+ "name": "Content_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Content_type_index": {
+ "name": "Content_type_index",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Content_userId_index": {
+ "name": "Content_userId_index",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Content_sourceId_index": {
+ "name": "Content_sourceId_index",
+ "columns": [
+ {
+ "expression": "sourceId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Content_publishedAt_index": {
+ "name": "Content_publishedAt_index",
+ "columns": [
+ {
+ "expression": "publishedAt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Content_published_index": {
+ "name": "Content_published_index",
+ "columns": [
+ {
+ "expression": "published",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Content_userId_user_id_fk": {
+ "name": "Content_userId_user_id_fk",
+ "tableFrom": "Content",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Content_sourceId_FeedSource_id_fk": {
+ "name": "Content_sourceId_FeedSource_id_fk",
+ "tableFrom": "Content",
+ "tableTo": "FeedSource",
+ "columnsFrom": ["sourceId"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ContentBookmark": {
+ "name": "ContentBookmark",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contentId": {
+ "name": "contentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ContentBookmark_contentId_Content_id_fk": {
+ "name": "ContentBookmark_contentId_Content_id_fk",
+ "tableFrom": "ContentBookmark",
+ "tableTo": "Content",
+ "columnsFrom": ["contentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentBookmark_userId_user_id_fk": {
+ "name": "ContentBookmark_userId_user_id_fk",
+ "tableFrom": "ContentBookmark",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ContentBookmark_contentId_userId_key": {
+ "name": "ContentBookmark_contentId_userId_key",
+ "nullsNotDistinct": false,
+ "columns": ["contentId", "userId"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ContentReport": {
+ "name": "ContentReport",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contentId": {
+ "name": "contentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discussionId": {
+ "name": "discussionId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postId": {
+ "name": "postId",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reporterId": {
+ "name": "reporterId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "ReportReason",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "details": {
+ "name": "details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "ReportStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "reviewedById": {
+ "name": "reviewedById",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewedAt": {
+ "name": "reviewedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actionTaken": {
+ "name": "actionTaken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "ContentReport_status_index": {
+ "name": "ContentReport_status_index",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ContentReport_reporterId_index": {
+ "name": "ContentReport_reporterId_index",
+ "columns": [
+ {
+ "expression": "reporterId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ContentReport_contentId_index": {
+ "name": "ContentReport_contentId_index",
+ "columns": [
+ {
+ "expression": "contentId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ContentReport_discussionId_index": {
+ "name": "ContentReport_discussionId_index",
+ "columns": [
+ {
+ "expression": "discussionId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ContentReport_postId_index": {
+ "name": "ContentReport_postId_index",
+ "columns": [
+ {
+ "expression": "postId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ContentReport_contentId_Content_id_fk": {
+ "name": "ContentReport_contentId_Content_id_fk",
+ "tableFrom": "ContentReport",
+ "tableTo": "Content",
+ "columnsFrom": ["contentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentReport_discussionId_Discussion_id_fk": {
+ "name": "ContentReport_discussionId_Discussion_id_fk",
+ "tableFrom": "ContentReport",
+ "tableTo": "Discussion",
+ "columnsFrom": ["discussionId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentReport_postId_posts_id_fk": {
+ "name": "ContentReport_postId_posts_id_fk",
+ "tableFrom": "ContentReport",
+ "tableTo": "posts",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentReport_reporterId_user_id_fk": {
+ "name": "ContentReport_reporterId_user_id_fk",
+ "tableFrom": "ContentReport",
+ "tableTo": "user",
+ "columnsFrom": ["reporterId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentReport_reviewedById_user_id_fk": {
+ "name": "ContentReport_reviewedById_user_id_fk",
+ "tableFrom": "ContentReport",
+ "tableTo": "user",
+ "columnsFrom": ["reviewedById"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ContentTag": {
+ "name": "ContentTag",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contentId": {
+ "name": "contentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tagId": {
+ "name": "tagId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ContentTag_contentId_Content_id_fk": {
+ "name": "ContentTag_contentId_Content_id_fk",
+ "tableFrom": "ContentTag",
+ "tableTo": "Content",
+ "columnsFrom": ["contentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentTag_tagId_Tag_id_fk": {
+ "name": "ContentTag_tagId_Tag_id_fk",
+ "tableFrom": "ContentTag",
+ "tableTo": "Tag",
+ "columnsFrom": ["tagId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ContentTag_contentId_tagId_key": {
+ "name": "ContentTag_contentId_tagId_key",
+ "nullsNotDistinct": false,
+ "columns": ["contentId", "tagId"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ContentVote": {
+ "name": "ContentVote",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "contentId": {
+ "name": "contentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "voteType": {
+ "name": "voteType",
+ "type": "VoteType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "ContentVote_contentId_index": {
+ "name": "ContentVote_contentId_index",
+ "columns": [
+ {
+ "expression": "contentId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ContentVote_contentId_Content_id_fk": {
+ "name": "ContentVote_contentId_Content_id_fk",
+ "tableFrom": "ContentVote",
+ "tableTo": "Content",
+ "columnsFrom": ["contentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "ContentVote_userId_user_id_fk": {
+ "name": "ContentVote_userId_user_id_fk",
+ "tableFrom": "ContentVote",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ContentVote_contentId_userId_key": {
+ "name": "ContentVote_contentId_userId_key",
+ "nullsNotDistinct": false,
+ "columns": ["contentId", "userId"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Discussion": {
+ "name": "Discussion",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "contentId": {
+ "name": "contentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parentId": {
+ "name": "parentId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "upvotes": {
+ "name": "upvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downvotes": {
+ "name": "downvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "Discussion_contentId_index": {
+ "name": "Discussion_contentId_index",
+ "columns": [
+ {
+ "expression": "contentId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Discussion_userId_index": {
+ "name": "Discussion_userId_index",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Discussion_contentId_Content_id_fk": {
+ "name": "Discussion_contentId_Content_id_fk",
+ "tableFrom": "Discussion",
+ "tableTo": "Content",
+ "columnsFrom": ["contentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Discussion_userId_user_id_fk": {
+ "name": "Discussion_userId_user_id_fk",
+ "tableFrom": "Discussion",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Discussion_parentId_fkey": {
+ "name": "Discussion_parentId_fkey",
+ "tableFrom": "Discussion",
+ "tableTo": "Discussion",
+ "columnsFrom": ["parentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Discussion_id_unique": {
+ "name": "Discussion_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.DiscussionVote": {
+ "name": "DiscussionVote",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "discussionId": {
+ "name": "discussionId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "voteType": {
+ "name": "voteType",
+ "type": "VoteType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "DiscussionVote_discussionId_index": {
+ "name": "DiscussionVote_discussionId_index",
+ "columns": [
+ {
+ "expression": "discussionId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "DiscussionVote_discussionId_Discussion_id_fk": {
+ "name": "DiscussionVote_discussionId_Discussion_id_fk",
+ "tableFrom": "DiscussionVote",
+ "tableTo": "Discussion",
+ "columnsFrom": ["discussionId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "DiscussionVote_userId_user_id_fk": {
+ "name": "DiscussionVote_userId_user_id_fk",
+ "tableFrom": "DiscussionVote",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "DiscussionVote_discussionId_userId_key": {
+ "name": "DiscussionVote_discussionId_userId_key",
+ "nullsNotDistinct": false,
+ "columns": ["discussionId", "userId"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.EmailChangeHistory": {
+ "name": "EmailChangeHistory",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oldEmail": {
+ "name": "oldEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "newEmail": {
+ "name": "newEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "changedAt": {
+ "name": "changedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userAgent": {
+ "name": "userAgent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "EmailChangeHistory_userId_user_id_fk": {
+ "name": "EmailChangeHistory_userId_user_id_fk",
+ "tableFrom": "EmailChangeHistory",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.EmailChangeRequest": {
+ "name": "EmailChangeRequest",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "newEmail": {
+ "name": "newEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "EmailChangeRequest_userId_user_id_fk": {
+ "name": "EmailChangeRequest_userId_user_id_fk",
+ "tableFrom": "EmailChangeRequest",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "EmailChangeRequest_token_unique": {
+ "name": "EmailChangeRequest_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.feed_sources": {
+ "name": "feed_sources",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo_url": {
+ "name": "logo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "feed_source_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_fetched_at": {
+ "name": "last_fetched_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_count": {
+ "name": "error_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "feed_sources_url_key": {
+ "name": "feed_sources_url_key",
+ "columns": [
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "feed_sources_slug_key": {
+ "name": "feed_sources_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "feed_sources_user_id_idx": {
+ "name": "feed_sources_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "feed_sources_user_id_user_id_fk": {
+ "name": "feed_sources_user_id_user_id_fk",
+ "tableFrom": "feed_sources",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.FeedSource": {
+ "name": "FeedSource",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "websiteUrl": {
+ "name": "websiteUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logoUrl": {
+ "name": "logoUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "FeedSourceStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ACTIVE'"
+ },
+ "lastFetchedAt": {
+ "name": "lastFetchedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastSuccessAt": {
+ "name": "lastSuccessAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "errorCount": {
+ "name": "errorCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "lastError": {
+ "name": "lastError",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "FeedSource_url_key": {
+ "name": "FeedSource_url_key",
+ "columns": [
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "FeedSource_slug_key": {
+ "name": "FeedSource_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "FeedSource_status_index": {
+ "name": "FeedSource_status_index",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "FeedSource_id_unique": {
+ "name": "FeedSource_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Flagged": {
+ "name": "Flagged",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notifierId": {
+ "name": "notifierId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postId": {
+ "name": "postId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commentId": {
+ "name": "commentId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "Flagged_userId_user_id_fk": {
+ "name": "Flagged_userId_user_id_fk",
+ "tableFrom": "Flagged",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Flagged_notifierId_user_id_fk": {
+ "name": "Flagged_notifierId_user_id_fk",
+ "tableFrom": "Flagged",
+ "tableTo": "user",
+ "columnsFrom": ["notifierId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Flagged_postId_Post_id_fk": {
+ "name": "Flagged_postId_Post_id_fk",
+ "tableFrom": "Flagged",
+ "tableTo": "Post",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Flagged_commentId_Comment_id_fk": {
+ "name": "Flagged_commentId_Comment_id_fk",
+ "tableFrom": "Flagged",
+ "tableTo": "Comment",
+ "columnsFrom": ["commentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Flagged_id_unique": {
+ "name": "Flagged_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.follow": {
+ "name": "follow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "follower_id": {
+ "name": "follower_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "following_id": {
+ "name": "following_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "follow_pair_idx": {
+ "name": "follow_pair_idx",
+ "columns": [
+ {
+ "expression": "follower_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "following_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "follow_follower_idx": {
+ "name": "follow_follower_idx",
+ "columns": [
+ {
+ "expression": "follower_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "follow_following_idx": {
+ "name": "follow_following_idx",
+ "columns": [
+ {
+ "expression": "following_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "follow_follower_id_user_id_fk": {
+ "name": "follow_follower_id_user_id_fk",
+ "tableFrom": "follow",
+ "tableTo": "user",
+ "columnsFrom": ["follower_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "follow_following_id_user_id_fk": {
+ "name": "follow_following_id_user_id_fk",
+ "tableFrom": "follow",
+ "tableTo": "user",
+ "columnsFrom": ["following_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.job": {
+ "name": "job",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_logo": {
+ "name": "company_logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "job_title": {
+ "name": "job_title",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "job_description": {
+ "name": "job_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "job_location": {
+ "name": "job_location",
+ "type": "varchar(60)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "application_url": {
+ "name": "application_url",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "job_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remote": {
+ "name": "remote",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "relocation": {
+ "name": "relocation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "visa_sponsorship": {
+ "name": "visa_sponsorship",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "ai_native": {
+ "name": "ai_native",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "job_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "featured": {
+ "name": "featured",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "price_cents": {
+ "name": "price_cents",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "currency": {
+ "name": "currency",
+ "type": "varchar(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'EUR'"
+ },
+ "payment_provider": {
+ "name": "payment_provider",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payment_ref": {
+ "name": "payment_ref",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "paid_at": {
+ "name": "paid_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_by_id": {
+ "name": "approved_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejection_reason": {
+ "name": "rejection_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "job_slug_idx": {
+ "name": "job_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_status_idx": {
+ "name": "job_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_featured_idx": {
+ "name": "job_featured_idx",
+ "columns": [
+ {
+ "expression": "featured",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_type_idx": {
+ "name": "job_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_user_id_idx": {
+ "name": "job_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_published_at_idx": {
+ "name": "job_published_at_idx",
+ "columns": [
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_expires_at_idx": {
+ "name": "job_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "job_user_id_user_id_fk": {
+ "name": "job_user_id_user_id_fk",
+ "tableFrom": "job",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "job_approved_by_id_user_id_fk": {
+ "name": "job_approved_by_id_user_id_fk",
+ "tableFrom": "job",
+ "tableTo": "user",
+ "columnsFrom": ["approved_by_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Like": {
+ "name": "Like",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "postId": {
+ "name": "postId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commentId": {
+ "name": "commentId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "Like_userId_commentId_key": {
+ "name": "Like_userId_commentId_key",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "commentId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Like_userId_postId_key": {
+ "name": "Like_userId_postId_key",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "postId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Like_userId_user_id_fk": {
+ "name": "Like_userId_user_id_fk",
+ "tableFrom": "Like",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Like_postId_Post_id_fk": {
+ "name": "Like_postId_Post_id_fk",
+ "tableFrom": "Like",
+ "tableTo": "Post",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Like_commentId_Comment_id_fk": {
+ "name": "Like_commentId_Comment_id_fk",
+ "tableFrom": "Like",
+ "tableTo": "Comment",
+ "columnsFrom": ["commentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Like_id_unique": {
+ "name": "Like_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Notification": {
+ "name": "Notification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "type": {
+ "name": "type",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "postId": {
+ "name": "postId",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "commentId": {
+ "name": "commentId",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notifierId": {
+ "name": "notifierId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "Notification_userId_index": {
+ "name": "Notification_userId_index",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Notification_userId_user_id_fk": {
+ "name": "Notification_userId_user_id_fk",
+ "tableFrom": "Notification",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Notification_postId_posts_id_fk": {
+ "name": "Notification_postId_posts_id_fk",
+ "tableFrom": "Notification",
+ "tableTo": "posts",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Notification_commentId_comments_id_fk": {
+ "name": "Notification_commentId_comments_id_fk",
+ "tableFrom": "Notification",
+ "tableTo": "comments",
+ "columnsFrom": ["commentId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "Notification_notifierId_user_id_fk": {
+ "name": "Notification_notifierId_user_id_fk",
+ "tableFrom": "Notification",
+ "tableTo": "user",
+ "columnsFrom": ["notifierId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Notification_id_unique": {
+ "name": "Notification_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.point_event": {
+ "name": "point_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "point_action",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "points": {
+ "name": "points",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_type": {
+ "name": "source_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "point_event_user_idx": {
+ "name": "point_event_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "point_event_user_created_idx": {
+ "name": "point_event_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "point_event_created_idx": {
+ "name": "point_event_created_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "point_event_user_id_user_id_fk": {
+ "name": "point_event_user_id_user_id_fk",
+ "tableFrom": "point_event",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "point_event_actor_id_user_id_fk": {
+ "name": "point_event_actor_id_user_id_fk",
+ "tableFrom": "point_event",
+ "tableTo": "user",
+ "columnsFrom": ["actor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "point_event_dedupe_idx": {
+ "name": "point_event_dedupe_idx",
+ "nullsNotDistinct": true,
+ "columns": ["user_id", "action", "source_id", "actor_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Post": {
+ "name": "Post",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "canonicalUrl": {
+ "name": "canonicalUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "coverImage": {
+ "name": "coverImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved": {
+ "name": "approved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "varchar(156)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "readTimeMins": {
+ "name": "readTimeMins",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "published": {
+ "name": "published",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "showComments": {
+ "name": "showComments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "likes": {
+ "name": "likes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "upvotes": {
+ "name": "upvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downvotes": {
+ "name": "downvotes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "Post_id_key": {
+ "name": "Post_id_key",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Post_slug_key": {
+ "name": "Post_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Post_slug_index": {
+ "name": "Post_slug_index",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Post_userId_index": {
+ "name": "Post_userId_index",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Post_userId_user_id_fk": {
+ "name": "Post_userId_user_id_fk",
+ "tableFrom": "Post",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Post_id_unique": {
+ "name": "Post_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.post_tags": {
+ "name": "post_tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "post_tags_post_id_idx": {
+ "name": "post_tags_post_id_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "post_tags_tag_id_idx": {
+ "name": "post_tags_tag_id_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "post_tags_post_id_posts_id_fk": {
+ "name": "post_tags_post_id_posts_id_fk",
+ "tableFrom": "post_tags",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "post_tags_tag_id_Tag_id_fk": {
+ "name": "post_tags_tag_id_Tag_id_fk",
+ "tableFrom": "post_tags",
+ "tableTo": "Tag",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "post_tags_post_id_tag_id_key": {
+ "name": "post_tags_post_id_tag_id_key",
+ "nullsNotDistinct": false,
+ "columns": ["post_id", "tag_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.post_votes": {
+ "name": "post_votes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "vote_type": {
+ "name": "vote_type",
+ "type": "vote_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "post_votes_post_id_idx": {
+ "name": "post_votes_post_id_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "post_votes_post_id_posts_id_fk": {
+ "name": "post_votes_post_id_posts_id_fk",
+ "tableFrom": "post_votes",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "post_votes_user_id_user_id_fk": {
+ "name": "post_votes_user_id_user_id_fk",
+ "tableFrom": "post_votes",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "post_votes_post_id_user_id_key": {
+ "name": "post_votes_post_id_user_id_key",
+ "nullsNotDistinct": false,
+ "columns": ["post_id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.post_follow": {
+ "name": "post_follow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "post_follow_pair_idx": {
+ "name": "post_follow_pair_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "post_follow_post_idx": {
+ "name": "post_follow_post_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "post_follow_user_id_user_id_fk": {
+ "name": "post_follow_user_id_user_id_fk",
+ "tableFrom": "post_follow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "post_follow_post_id_posts_id_fk": {
+ "name": "post_follow_post_id_posts_id_fk",
+ "tableFrom": "post_follow",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.post_metadata": {
+ "name": "post_metadata",
+ "schema": "",
+ "columns": {
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "sentiment": {
+ "name": "sentiment",
+ "type": "sentiment",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sentiment_score": {
+ "name": "sentiment_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quality_score": {
+ "name": "quality_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quality_reason": {
+ "name": "quality_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model_id": {
+ "name": "model_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "analyzed_at": {
+ "name": "analyzed_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schema_version": {
+ "name": "schema_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ }
+ },
+ "indexes": {
+ "post_metadata_analyzed_at_idx": {
+ "name": "post_metadata_analyzed_at_idx",
+ "columns": [
+ {
+ "expression": "analyzed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "post_metadata_post_id_posts_id_fk": {
+ "name": "post_metadata_post_id_posts_id_fk",
+ "tableFrom": "post_metadata",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.PostTag": {
+ "name": "PostTag",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tagId": {
+ "name": "tagId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "postId": {
+ "name": "postId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "PostTag_tagId_postId_key": {
+ "name": "PostTag_tagId_postId_key",
+ "columns": [
+ {
+ "expression": "tagId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "postId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "PostTag_tagId_Tag_id_fk": {
+ "name": "PostTag_tagId_Tag_id_fk",
+ "tableFrom": "PostTag",
+ "tableTo": "Tag",
+ "columnsFrom": ["tagId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "PostTag_postId_Post_id_fk": {
+ "name": "PostTag_postId_Post_id_fk",
+ "tableFrom": "PostTag",
+ "tableTo": "Post",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.post_topic": {
+ "name": "post_topic",
+ "schema": "",
+ "columns": {
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "topic_id": {
+ "name": "topic_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "confidence": {
+ "name": "confidence",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "tag_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ai'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "post_topic_topic_id_idx": {
+ "name": "post_topic_topic_id_idx",
+ "columns": [
+ {
+ "expression": "topic_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "post_topic_source_idx": {
+ "name": "post_topic_source_idx",
+ "columns": [
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "post_topic_post_id_posts_id_fk": {
+ "name": "post_topic_post_id_posts_id_fk",
+ "tableFrom": "post_topic",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "post_topic_topic_id_topic_id_fk": {
+ "name": "post_topic_topic_id_topic_id_fk",
+ "tableFrom": "post_topic",
+ "tableTo": "topic",
+ "columnsFrom": ["topic_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "post_topic_post_id_topic_id_pk": {
+ "name": "post_topic_post_id_topic_id_pk",
+ "columns": ["post_id", "topic_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.PostVote": {
+ "name": "PostVote",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "postId": {
+ "name": "postId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "voteType": {
+ "name": "voteType",
+ "type": "VoteType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "PostVote_postId_index": {
+ "name": "PostVote_postId_index",
+ "columns": [
+ {
+ "expression": "postId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "PostVote_postId_Post_id_fk": {
+ "name": "PostVote_postId_Post_id_fk",
+ "tableFrom": "PostVote",
+ "tableTo": "Post",
+ "columnsFrom": ["postId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "PostVote_userId_user_id_fk": {
+ "name": "PostVote_userId_user_id_fk",
+ "tableFrom": "PostVote",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "PostVote_postId_userId_key": {
+ "name": "PostVote_postId_userId_key",
+ "nullsNotDistinct": false,
+ "columns": ["postId", "userId"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.posts": {
+ "name": "posts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "post_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url_id": {
+ "name": "url_id",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "canonical_url": {
+ "name": "canonical_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_url": {
+ "name": "external_url",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalUrlNormalized": {
+ "name": "externalUrlNormalized",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "moderationNote": {
+ "name": "moderationNote",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_author": {
+ "name": "source_author",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reading_time": {
+ "name": "reading_time",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "upvotes_count": {
+ "name": "upvotes_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downvotes_count": {
+ "name": "downvotes_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "comments_count": {
+ "name": "comments_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "views_count": {
+ "name": "views_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "post_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "featured": {
+ "name": "featured",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "pinned_until": {
+ "name": "pinned_until",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "show_comments": {
+ "name": "show_comments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "legacy_post_id": {
+ "name": "legacy_post_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "posts_author_id_idx": {
+ "name": "posts_author_id_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_slug_idx": {
+ "name": "posts_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_url_id_key": {
+ "name": "posts_url_id_key",
+ "columns": [
+ {
+ "expression": "url_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_legacy_post_id_idx": {
+ "name": "posts_legacy_post_id_idx",
+ "columns": [
+ {
+ "expression": "legacy_post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_status_idx": {
+ "name": "posts_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_published_at_idx": {
+ "name": "posts_published_at_idx",
+ "columns": [
+ {
+ "expression": "published_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_type_idx": {
+ "name": "posts_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_source_id_idx": {
+ "name": "posts_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_featured_idx": {
+ "name": "posts_featured_idx",
+ "columns": [
+ {
+ "expression": "featured",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_external_url_normalized_idx": {
+ "name": "posts_external_url_normalized_idx",
+ "columns": [
+ {
+ "expression": "externalUrlNormalized",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "posts_author_id_user_id_fk": {
+ "name": "posts_author_id_user_id_fk",
+ "tableFrom": "posts",
+ "tableTo": "user",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "posts_source_id_feed_sources_id_fk": {
+ "name": "posts_source_id_feed_sources_id_fk",
+ "tableFrom": "posts",
+ "tableTo": "feed_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.publication_follow": {
+ "name": "publication_follow",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "publication_follow_pair_idx": {
+ "name": "publication_follow_pair_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "publication_follow_source_idx": {
+ "name": "publication_follow_source_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "publication_follow_user_id_user_id_fk": {
+ "name": "publication_follow_user_id_user_id_fk",
+ "tableFrom": "publication_follow",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "publication_follow_source_id_feed_sources_id_fk": {
+ "name": "publication_follow_source_id_feed_sources_id_fk",
+ "tableFrom": "publication_follow",
+ "tableTo": "feed_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reports": {
+ "name": "reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reporter_id": {
+ "name": "reporter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "report_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "reason": {
+ "name": "reason",
+ "type": "report_reason",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "details": {
+ "name": "details",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "report_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "reviewed_by_id": {
+ "name": "reviewed_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_taken": {
+ "name": "action_taken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "reports_status_idx": {
+ "name": "reports_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_reporter_id_idx": {
+ "name": "reports_reporter_id_idx",
+ "columns": [
+ {
+ "expression": "reporter_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_post_id_idx": {
+ "name": "reports_post_id_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reports_comment_id_idx": {
+ "name": "reports_comment_id_idx",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reports_post_id_posts_id_fk": {
+ "name": "reports_post_id_posts_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "posts",
+ "columnsFrom": ["post_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_comment_id_comments_id_fk": {
+ "name": "reports_comment_id_comments_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "comments",
+ "columnsFrom": ["comment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_reporter_id_user_id_fk": {
+ "name": "reports_reporter_id_user_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "user",
+ "columnsFrom": ["reporter_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_reviewed_by_id_user_id_fk": {
+ "name": "reports_reviewed_by_id_user_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "user",
+ "columnsFrom": ["reviewed_by_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "sessionToken": {
+ "name": "sessionToken",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_userId_user_id_fk": {
+ "name": "session_userId_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["userId"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.SponsorInquiry": {
+ "name": "SponsorInquiry",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company": {
+ "name": "company",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "interests": {
+ "name": "interests",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "budgetRange": {
+ "name": "budgetRange",
+ "type": "SponsorBudgetRange",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'EXPLORING'"
+ },
+ "goals": {
+ "name": "goals",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "SponsorInquiryStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'PENDING'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "SponsorInquiry_status_index": {
+ "name": "SponsorInquiry_status_index",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "SponsorInquiry_email_index": {
+ "name": "SponsorInquiry_email_index",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Tag": {
+ "name": "Tag",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "post_count": {
+ "name": "post_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "Tag_title_key": {
+ "name": "Tag_title_key",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Tag_slug_key": {
+ "name": "Tag_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Tag_id_unique": {
+ "name": "Tag_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag_merge_suggestions": {
+ "name": "tag_merge_suggestions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_tag_id": {
+ "name": "source_tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_tag_id": {
+ "name": "target_tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "similarity_score": {
+ "name": "similarity_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "tag_merge_suggestion_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "reviewed_by_id": {
+ "name": "reviewed_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "tag_merge_suggestions_source_tag_idx": {
+ "name": "tag_merge_suggestions_source_tag_idx",
+ "columns": [
+ {
+ "expression": "source_tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tag_merge_suggestions_target_tag_idx": {
+ "name": "tag_merge_suggestions_target_tag_idx",
+ "columns": [
+ {
+ "expression": "target_tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tag_merge_suggestions_status_idx": {
+ "name": "tag_merge_suggestions_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tag_merge_suggestions_source_tag_id_Tag_id_fk": {
+ "name": "tag_merge_suggestions_source_tag_id_Tag_id_fk",
+ "tableFrom": "tag_merge_suggestions",
+ "tableTo": "Tag",
+ "columnsFrom": ["source_tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tag_merge_suggestions_target_tag_id_Tag_id_fk": {
+ "name": "tag_merge_suggestions_target_tag_id_Tag_id_fk",
+ "tableFrom": "tag_merge_suggestions",
+ "tableTo": "Tag",
+ "columnsFrom": ["target_tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tag_merge_suggestions_reviewed_by_id_user_id_fk": {
+ "name": "tag_merge_suggestions_reviewed_by_id_user_id_fk",
+ "tableFrom": "tag_merge_suggestions",
+ "tableTo": "user",
+ "columnsFrom": ["reviewed_by_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "tag_merge_suggestions_source_target_key": {
+ "name": "tag_merge_suggestions_source_target_key",
+ "nullsNotDistinct": false,
+ "columns": ["source_tag_id", "target_tag_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.topic": {
+ "name": "topic",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(60)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "topic_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {
+ "topic_slug_key": {
+ "name": "topic_slug_key",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "topic_status_idx": {
+ "name": "topic_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'/images/person.png'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "bio": {
+ "name": "bio",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "location": {
+ "name": "location",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "websiteUrl": {
+ "name": "websiteUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "emailNotifications": {
+ "name": "emailNotifications",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "newsletter": {
+ "name": "newsletter",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "gender": {
+ "name": "gender",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dateOfBirth": {
+ "name": "dateOfBirth",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "professionalOrStudent": {
+ "name": "professionalOrStudent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workplace": {
+ "name": "workplace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "jobTitle": {
+ "name": "jobTitle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "levelOfStudy": {
+ "name": "levelOfStudy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "course": {
+ "name": "course",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "Role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'USER'"
+ },
+ "referral_code": {
+ "name": "referral_code",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "invited_by": {
+ "name": "invited_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "topics": {
+ "name": "topics",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "experience_level": {
+ "name": "experience_level",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "onboarded_at": {
+ "name": "onboarded_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "User_username_key": {
+ "name": "User_username_key",
+ "columns": [
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_username_lower_key": {
+ "name": "user_username_lower_key",
+ "columns": [
+ {
+ "expression": "lower(\"username\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "User_email_key": {
+ "name": "User_email_key",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "User_username_id_idx": {
+ "name": "User_username_id_idx",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "User_username_index": {
+ "name": "User_username_index",
+ "columns": [
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "User_referral_code_key": {
+ "name": "User_referral_code_key",
+ "columns": [
+ {
+ "expression": "referral_code",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "User_invited_by_idx": {
+ "name": "User_invited_by_idx",
+ "columns": [
+ {
+ "expression": "invited_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_badge": {
+ "name": "user_badge",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "badge_id": {
+ "name": "badge_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "awarded_at": {
+ "name": "awarded_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "celebrated_at": {
+ "name": "celebrated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_badge_user_badge_idx": {
+ "name": "user_badge_user_badge_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "badge_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_badge_user_idx": {
+ "name": "user_badge_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_badge_user_id_user_id_fk": {
+ "name": "user_badge_user_id_user_id_fk",
+ "tableFrom": "user_badge",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_badge_badge_id_badge_id_fk": {
+ "name": "user_badge_badge_id_badge_id_fk",
+ "tableFrom": "user_badge",
+ "tableTo": "badge",
+ "columnsFrom": ["badge_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_streak": {
+ "name": "user_streak",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "current_streak": {
+ "name": "current_streak",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "longest_streak": {
+ "name": "longest_streak",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_active_on": {
+ "name": "last_active_on",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "freezes_available": {
+ "name": "freezes_available",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_streak_user_id_user_id_fk": {
+ "name": "user_streak_user_id_user_id_fk",
+ "tableFrom": "user_streak",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.feed_source_status": {
+ "name": "feed_source_status",
+ "schema": "public",
+ "values": ["active", "paused", "error"]
+ },
+ "public.job_status": {
+ "name": "job_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "pending_payment",
+ "pending",
+ "active",
+ "expired",
+ "rejected"
+ ]
+ },
+ "public.job_type": {
+ "name": "job_type",
+ "schema": "public",
+ "values": ["full-time", "part-time", "freelancer", "other"]
+ },
+ "public.ContentType": {
+ "name": "ContentType",
+ "schema": "public",
+ "values": ["POST", "LINK", "QUESTION", "VIDEO", "DISCUSSION"]
+ },
+ "public.FeedSourceStatus": {
+ "name": "FeedSourceStatus",
+ "schema": "public",
+ "values": ["ACTIVE", "PAUSED", "ERROR"]
+ },
+ "public.ReportReason": {
+ "name": "ReportReason",
+ "schema": "public",
+ "values": [
+ "SPAM",
+ "HARASSMENT",
+ "HATE_SPEECH",
+ "MISINFORMATION",
+ "COPYRIGHT",
+ "NSFW",
+ "OFF_TOPIC",
+ "OTHER"
+ ]
+ },
+ "public.ReportStatus": {
+ "name": "ReportStatus",
+ "schema": "public",
+ "values": ["PENDING", "REVIEWED", "DISMISSED", "ACTIONED"]
+ },
+ "public.VoteType": {
+ "name": "VoteType",
+ "schema": "public",
+ "values": ["UP", "DOWN"]
+ },
+ "public.point_action": {
+ "name": "point_action",
+ "schema": "public",
+ "values": [
+ "post_published",
+ "comment_created",
+ "upvote_received",
+ "daily_active",
+ "shipped",
+ "referral"
+ ]
+ },
+ "public.post_status": {
+ "name": "post_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "published",
+ "scheduled",
+ "unlisted",
+ "in_review",
+ "rejected"
+ ]
+ },
+ "public.post_type": {
+ "name": "post_type",
+ "schema": "public",
+ "values": ["article", "discussion", "link", "resource", "til", "question"]
+ },
+ "public.report_reason": {
+ "name": "report_reason",
+ "schema": "public",
+ "values": [
+ "spam",
+ "harassment",
+ "hate_speech",
+ "misinformation",
+ "copyright",
+ "nsfw",
+ "off_topic",
+ "other"
+ ]
+ },
+ "public.report_source": {
+ "name": "report_source",
+ "schema": "public",
+ "values": ["user", "system"]
+ },
+ "public.report_status": {
+ "name": "report_status",
+ "schema": "public",
+ "values": ["pending", "reviewed", "dismissed", "actioned"]
+ },
+ "public.Role": {
+ "name": "Role",
+ "schema": "public",
+ "values": ["MODERATOR", "ADMIN", "USER"]
+ },
+ "public.sentiment": {
+ "name": "sentiment",
+ "schema": "public",
+ "values": ["positive", "neutral", "negative"]
+ },
+ "public.SponsorBudgetRange": {
+ "name": "SponsorBudgetRange",
+ "schema": "public",
+ "values": [
+ "EXPLORING",
+ "UNDER_500",
+ "BETWEEN_500_2000",
+ "BETWEEN_2000_5000",
+ "OVER_5000"
+ ]
+ },
+ "public.SponsorInquiryStatus": {
+ "name": "SponsorInquiryStatus",
+ "schema": "public",
+ "values": ["PENDING", "CONTACTED", "CONVERTED", "CLOSED"]
+ },
+ "public.tag_merge_suggestion_status": {
+ "name": "tag_merge_suggestion_status",
+ "schema": "public",
+ "values": ["pending", "approved", "rejected"]
+ },
+ "public.tag_source": {
+ "name": "tag_source",
+ "schema": "public",
+ "values": ["ai", "manual"]
+ },
+ "public.topic_status": {
+ "name": "topic_status",
+ "schema": "public",
+ "values": ["active", "pending"]
+ },
+ "public.vote_type": {
+ "name": "vote_type",
+ "schema": "public",
+ "values": ["up", "down"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 4941ec145..00730e29a 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -267,6 +267,13 @@
"when": 1781296891023,
"tag": "0037_backfill_onboarding_badge",
"breakpoints": true
+ },
+ {
+ "idx": 38,
+ "version": "7",
+ "when": 1781464677918,
+ "tag": "0038_ai_content_metadata",
+ "breakpoints": true
}
]
}
diff --git a/server/db/schema.ts b/server/db/schema.ts
index eeecc36e2..18d35c308 100644
--- a/server/db/schema.ts
+++ b/server/db/schema.ts
@@ -14,6 +14,7 @@ import {
varchar,
unique,
uuid,
+ real,
} from "drizzle-orm/pg-core";
import { relations, sql } from "drizzle-orm";
@@ -67,6 +68,21 @@ export const reportStatus = pgEnum("report_status", [
"dismissed",
"actioned",
]);
+// Who raised a report: a human ("user") or the automated review cron ("system").
+export const reportSource = pgEnum("report_source", ["user", "system"]);
+
+// AI content pipeline (see docs/plans/2026-06-14-admin-shell-and-ai-content-design.md)
+export const sentiment = pgEnum("sentiment", [
+ "positive",
+ "neutral",
+ "negative",
+]);
+// Curated topic vocabulary lifecycle: active (usable) or pending (model-proposed,
+// awaiting admin approval).
+export const topicStatus = pgEnum("topic_status", ["active", "pending"]);
+// Provenance of a post<->topic edge. The nightly cron only ever rewrites its own
+// `ai` edges; `manual` edges (set by an admin) are never touched.
+export const tagSource = pgEnum("tag_source", ["ai", "manual"]);
// Job board
export const jobType = pgEnum("job_type", [
@@ -430,6 +446,8 @@ export const postsRelations = relations(posts, ({ one, many }) => ({
bookmarks: many(bookmarks),
tags: many(post_tags),
reports: many(reports),
+ metadata: one(post_metadata),
+ aiTopics: many(post_topic),
}));
// COMMENTS TABLE
@@ -479,6 +497,14 @@ export const comments = pgTable(
withTimezone: true,
}), // Soft delete for "[deleted]" placeholders
+ // Nightly auto-moderation watermark: last time the review cron screened this
+ // comment. NULL or < updatedAt => the comment re-enters the moderation pass.
+ moderatedAt: timestamp("moderated_at", {
+ precision: 3,
+ mode: "string",
+ withTimezone: true,
+ }),
+
// Migration tracking: references legacy Comment.id
legacyCommentId: integer("legacy_comment_id"),
},
@@ -659,9 +685,12 @@ export const reports = pgTable(
commentId: uuid("comment_id").references(() => comments.id, {
onDelete: "cascade",
}),
- reporterId: text("reporter_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
+ // Nullable: system (auto-flagged) reports have no human reporter.
+ reporterId: text("reporter_id").references(() => user.id, {
+ onDelete: "cascade",
+ }),
+ // "user" = human report, "system" = raised by the nightly review cron.
+ source: reportSource("source").default("user").notNull(),
reason: reportReason("reason").notNull(),
details: text("details"),
status: reportStatus("status").default("pending").notNull(),
@@ -708,6 +737,106 @@ export const reportsRelations = relations(reports, ({ one }) => ({
}),
}));
+// AI CONTENT METADATA (nightly review cron — Phase 2)
+// See docs/plans/2026-06-14-admin-shell-and-ai-content-design.md
+
+// Per-post signal envelope, 1:1 with posts. Separate table (not columns on
+// posts) keeps the hot posts row lean and lets the cron write without bumping
+// posts.updatedAt. `analyzedAt` IS the incremental watermark.
+export const post_metadata = pgTable(
+ "post_metadata",
+ {
+ postId: uuid("post_id")
+ .primaryKey()
+ .references(() => posts.id, { onDelete: "cascade" }),
+ sentiment: sentiment("sentiment"),
+ sentimentScore: real("sentiment_score"),
+ qualityScore: real("quality_score"),
+ qualityReason: text("quality_reason"),
+ // The Bedrock model that produced this row; NULL means a human set it (so
+ // the cron skips overwriting manually-curated values).
+ modelId: text("model_id"),
+ analyzedAt: timestamp("analyzed_at", {
+ precision: 3,
+ mode: "string",
+ withTimezone: true,
+ }),
+ // Bump in code to force re-analysis of every post on the next run.
+ schemaVersion: integer("schema_version").default(1).notNull(),
+ },
+ (table) => ({
+ analyzedAtIdx: index("post_metadata_analyzed_at_idx").on(table.analyzedAt),
+ }),
+);
+
+export const postMetadataRelations = relations(post_metadata, ({ one }) => ({
+ post: one(posts, {
+ fields: [post_metadata.postId],
+ references: [posts.id],
+ }),
+}));
+
+// Controlled topic vocabulary so AI + manual tags share one clean namespace and
+// LLM free-text ("RAG"/"rag"/"retrieval-augmented") can't drift.
+export const topic = pgTable(
+ "topic",
+ {
+ id: serial("id").primaryKey().notNull(),
+ slug: varchar("slug", { length: 60 }).notNull(),
+ label: varchar("label", { length: 80 }).notNull(),
+ status: topicStatus("status").default("active").notNull(),
+ createdAt: timestamp("created_at", {
+ precision: 3,
+ mode: "string",
+ withTimezone: true,
+ })
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ },
+ (table) => ({
+ slugKey: uniqueIndex("topic_slug_key").on(table.slug),
+ statusIdx: index("topic_status_idx").on(table.status),
+ }),
+);
+
+export const topicRelations = relations(topic, ({ many }) => ({
+ posts: many(post_topic),
+}));
+
+// Normalized post<->topic edges that power personalized ranking. `source`
+// distinguishes AI suggestions from admin-curated tags; the cron only rewrites
+// `ai` edges.
+export const post_topic = pgTable(
+ "post_topic",
+ {
+ postId: uuid("post_id")
+ .notNull()
+ .references(() => posts.id, { onDelete: "cascade" }),
+ topicId: integer("topic_id")
+ .notNull()
+ .references(() => topic.id, { onDelete: "cascade" }),
+ confidence: real("confidence"), // nullable: manual tags have none
+ source: tagSource("source").default("ai").notNull(),
+ createdAt: timestamp("created_at", {
+ precision: 3,
+ mode: "string",
+ withTimezone: true,
+ })
+ .default(sql`CURRENT_TIMESTAMP`)
+ .notNull(),
+ },
+ (table) => ({
+ pk: primaryKey({ columns: [table.postId, table.topicId] }),
+ topicIdIdx: index("post_topic_topic_id_idx").on(table.topicId),
+ sourceIdx: index("post_topic_source_idx").on(table.source),
+ }),
+);
+
+export const postTopicRelations = relations(post_topic, ({ one }) => ({
+ post: one(posts, { fields: [post_topic.postId], references: [posts.id] }),
+ topic: one(topic, { fields: [post_topic.topicId], references: [topic.id] }),
+}));
+
// TAGS (shared between legacy and new system)
export const tag = pgTable(
diff --git a/server/lib/contentAnalysis.test.ts b/server/lib/contentAnalysis.test.ts
new file mode 100644
index 000000000..991589b3b
--- /dev/null
+++ b/server/lib/contentAnalysis.test.ts
@@ -0,0 +1,109 @@
+import { describe, it, expect } from "vitest";
+import {
+ parseAnalysis,
+ analyzePost,
+ type TopicVocabEntry,
+} from "./contentAnalysis";
+
+const VOCAB: TopicVocabEntry[] = [
+ { slug: "rag", label: "RAG" },
+ { slug: "ai-agents", label: "AI Agents" },
+ { slug: "nextjs", label: "Next.js" },
+];
+
+const input = { title: "t", body: "b" };
+
+describe("parseAnalysis", () => {
+ it("parses a full valid analysis and keeps only vocab topics", () => {
+ const a = parseAnalysis(
+ JSON.stringify({
+ topics: [
+ { slug: "rag", confidence: 0.9 },
+ { slug: "not-a-real-topic", confidence: 0.8 },
+ ],
+ proposedTopics: ["llmops"],
+ sentiment: "positive",
+ sentimentScore: 0.7,
+ qualityScore: 0.85,
+ qualityReason: "substantial",
+ moderation: { verdict: "allow", category: "none", reason: "" },
+ }),
+ VOCAB,
+ input,
+ );
+ expect(a.topics).toEqual([{ slug: "rag", confidence: 0.9 }]);
+ expect(a.proposedTopics).toEqual(["llmops"]);
+ expect(a.sentiment).toBe("positive");
+ expect(a.qualityScore).toBe(0.85);
+ expect(a.moderation.verdict).toBe("allow");
+ });
+
+ it("drops a proposed topic that already exists in the vocab", () => {
+ const a = parseAnalysis(
+ JSON.stringify({ topics: [], proposedTopics: ["rag", "newthing"] }),
+ VOCAB,
+ input,
+ );
+ expect(a.proposedTopics).toEqual(["newthing"]);
+ });
+
+ it("clamps out-of-range scores", () => {
+ const a = parseAnalysis(
+ JSON.stringify({ sentimentScore: 5, qualityScore: -2 }),
+ VOCAB,
+ input,
+ );
+ expect(a.sentimentScore).toBe(1);
+ expect(a.qualityScore).toBe(0);
+ });
+
+ it("extracts JSON embedded in surrounding prose", () => {
+ const a = parseAnalysis(
+ 'Here: {"moderation":{"verdict":"review","category":"nsfw","reason":"x"}} ok',
+ VOCAB,
+ input,
+ );
+ expect(a.moderation.verdict).toBe("review");
+ expect(a.moderation.category).toBe("nsfw");
+ });
+
+ it("falls back to the heuristic on unparseable garbage (fail-open allow on clean text)", () => {
+ const a = parseAnalysis("not json at all", VOCAB, {
+ title: "Shipping my AI side project",
+ body: "A clean writeup about what I built and learned.",
+ });
+ expect(a.topics).toEqual([]);
+ expect(a.moderation.verdict).toBe("allow");
+ });
+
+ it("treats an unknown moderation verdict as review (fail-safe)", () => {
+ const a = parseAnalysis(
+ JSON.stringify({ moderation: { verdict: "banana" } }),
+ VOCAB,
+ input,
+ );
+ expect(a.moderation.verdict).toBe("review");
+ });
+});
+
+describe("analyzePost (disabled path)", () => {
+ it("returns heuristic-only analysis with no AI signals when Bedrock is disabled", async () => {
+ // Under Vitest isBedrockEnabled() is forced false (NODE_ENV==="test").
+ delete process.env.BEDROCK_MODEL_ID;
+ delete process.env.ACCESS_KEY;
+
+ const a = await analyzePost(
+ {
+ type: "article",
+ title: "Shipping my first AI side project",
+ body: "I built a small tool this weekend and shared what I learned.",
+ },
+ VOCAB,
+ );
+
+ expect(a.topics).toEqual([]);
+ expect(a.sentiment).toBeNull();
+ expect(a.qualityScore).toBeNull();
+ expect(a.moderation.verdict).toBe("allow");
+ });
+});
diff --git a/server/lib/contentAnalysis.ts b/server/lib/contentAnalysis.ts
new file mode 100644
index 000000000..72f86a80a
--- /dev/null
+++ b/server/lib/contentAnalysis.ts
@@ -0,0 +1,231 @@
+import { InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
+import * as Sentry from "@sentry/nextjs";
+import { bedrockClient, isBedrockEnabled } from "@/server/lib/bedrock";
+import { screenContent } from "@/server/lib/moderation";
+import { fetchPageText } from "@/server/lib/fetchPage";
+
+// Bump to force the nightly cron to re-analyse every post (compared against
+// post_metadata.schemaVersion). Increment when this prompt or the output shape
+// changes in a way that should invalidate existing rows.
+export const ANALYSIS_SCHEMA_VERSION = 1;
+
+export type Sentiment = "positive" | "neutral" | "negative";
+
+export interface TopicSuggestion {
+ /** A slug from the supplied controlled vocabulary. */
+ slug: string;
+ /** 0..1 model confidence. */
+ confidence: number;
+}
+
+export interface ContentAnalysis {
+ /** Topics resolved against the controlled vocabulary (slugs only). */
+ topics: TopicSuggestion[];
+ /** New topic slugs the model proposes that weren't in the vocabulary. */
+ proposedTopics: string[];
+ sentiment: Sentiment | null;
+ /** -1..1; negative .. positive. */
+ sentimentScore: number | null;
+ /** 0..1; higher = higher quality / lower spam risk. */
+ qualityScore: number | null;
+ qualityReason: string;
+ /** Moderation verdict, mirroring autoReview's shape. */
+ moderation: { verdict: "allow" | "review"; category: string; reason: string };
+}
+
+export interface AnalyzePostInput {
+ type?: string | null;
+ title?: string | null;
+ body?: string | null;
+ externalUrl?: string | null;
+}
+
+export interface TopicVocabEntry {
+ slug: string;
+ label: string;
+}
+
+const MAX_BODY_CHARS = 6000;
+const MAX_TOPICS = 4;
+
+function buildSystemPrompt(vocab: TopicVocabEntry[]): string {
+ const list = vocab.map((t) => `${t.slug} (${t.label})`).join(", ");
+ return `You analyse posts for Codú, a community for AI builders and indie hackers. Return a single JSON object describing the post. No prose, JSON only.
+
+Do FOUR things:
+1. TOPICS: pick up to ${MAX_TOPICS} of the most relevant topics. You MUST choose slugs from this controlled list: [${list}]. Only include a topic if it genuinely fits. If the post is clearly about an important topic that is missing from the list, add its kebab-case slug to "proposedTopics" (do NOT put proposed topics in "topics").
+2. SENTIMENT: the overall tone toward its subject — "positive", "neutral" or "negative" — and a sentimentScore from -1 (very negative) to 1 (very positive).
+3. QUALITY: a qualityScore from 0 (spam / zero-effort / link-only with no substance) to 1 (substantial, useful, well-formed), with a one-sentence qualityReason.
+4. MODERATION: be LOOSE and FAIR — allow by default, including people sharing their OWN projects/launches. Only "review" the clearly bad: pornographic/NSFW, crypto/token shilling, scams/phishing, malicious or fabricated links, or content plainly off-theme for a developer/AI-builder community. When in doubt, allow.
+
+Reply with ONLY this JSON shape:
+{"topics":[{"slug":string,"confidence":number}],"proposedTopics":[string],"sentiment":"positive"|"neutral"|"negative","sentimentScore":number,"qualityScore":number,"qualityReason":string,"moderation":{"verdict":"allow"|"review","category":string,"reason":string}}
+Use moderation category "none" and reason "" for an allow.`;
+}
+
+/**
+ * Heuristic-only fallback when Bedrock isn't configured (local/dev/test). Mirrors
+ * autoReview's fail-open contract: we still produce a moderation verdict from the
+ * cheap screenContent heuristic, but skip the AI-only signals (topics/sentiment/
+ * quality) by leaving them empty/null so the cron writes nothing misleading.
+ */
+function heuristicAnalysis(input: AnalyzePostInput): ContentAnalysis {
+ const result = screenContent({ title: input.title, body: input.body });
+ return {
+ topics: [],
+ proposedTopics: [],
+ sentiment: null,
+ sentimentScore: null,
+ qualityScore: null,
+ qualityReason: "",
+ moderation: result.ok
+ ? { verdict: "allow", category: "none", reason: "" }
+ : {
+ verdict: "review",
+ category: "heuristic",
+ reason: result.reasons.join(", "),
+ },
+ };
+}
+
+/**
+ * Analyse a single post with Bedrock: topic tagging, sentiment, quality scoring
+ * and a moderation verdict in ONE model call. Gated and FAIL-OPEN, exactly like
+ * autoReview():
+ * - Bedrock not configured -> heuristic moderation only, no AI signals.
+ * - On ANY thrown error -> capture to Sentry and return the heuristic result so
+ * a model/infra failure never blocks or corrupts the pipeline.
+ */
+export async function analyzePost(
+ input: AnalyzePostInput,
+ vocab: TopicVocabEntry[],
+): Promise