an app to share curated trails sidetrail.app
atproto nextjs react rsc
at main 144 lines 5.1 kB view raw
1import { pgTable, text, timestamp, jsonb, bigint, integer } from "drizzle-orm/pg-core"; 2 3// ============================================================================ 4// Record types stored as JSONB 5// ============================================================================ 6 7export type TrailRecord = { 8 $type: "app.sidetrail.trail"; 9 title: string; 10 description: string; 11 stops: Array<{ 12 tid: string; 13 title: string; 14 content: string; 15 buttonText?: string; 16 external?: { 17 uri: string; 18 title?: string; 19 description?: string; 20 thumb?: { $type: "blob"; ref: { $link: string }; mimeType: string }; 21 }; 22 }>; 23 accentColor: string; 24 backgroundColor: string; 25 createdAt: string; 26}; 27 28export type WalkRecord = { 29 $type: "app.sidetrail.walk"; 30 trail: { uri: string; cid: string }; 31 visitedStops: string[]; 32 createdAt: string; 33 updatedAt?: string; 34}; 35 36export type CompletionRecord = { 37 $type: "app.sidetrail.completion"; 38 trail: { uri: string; cid: string }; 39 createdAt: string; 40}; 41 42export type DraftRecord = { 43 $type: "app.sidetrail.draft"; 44 title: string; 45 description: string; 46 stops: Array<{ 47 tid: string; 48 title: string; 49 content: string; 50 buttonText?: string; 51 external?: { 52 uri: string; 53 title?: string; 54 description?: string; 55 thumb?: string; // URL string (not blob ref - uploaded on publish) 56 }; 57 }>; 58 accentColor: string; 59 backgroundColor: string; 60 createdAt: string; 61 updatedAt: string; 62}; 63 64// ============================================================================ 65// Tables 66// ============================================================================ 67 68export const trails = pgTable("trails", { 69 uri: text("uri").primaryKey(), 70 cid: text("cid").notNull(), 71 authorDid: text("author_did").notNull(), 72 rkey: text("rkey").notNull(), 73 record: jsonb("record").notNull().$type<TrailRecord>(), 74 createdAt: timestamp("created_at", { withTimezone: true }).notNull(), 75 indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull().defaultNow(), 76}); 77 78export const walks = pgTable("walks", { 79 uri: text("uri").primaryKey(), 80 cid: text("cid").notNull(), 81 authorDid: text("author_did").notNull(), 82 rkey: text("rkey").notNull(), 83 trailUri: text("trail_uri").notNull(), 84 record: jsonb("record").notNull().$type<WalkRecord>(), 85 createdAt: timestamp("created_at", { withTimezone: true }).notNull(), 86 indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull().defaultNow(), 87}); 88 89export const completions = pgTable("completions", { 90 uri: text("uri").primaryKey(), 91 cid: text("cid").notNull(), 92 authorDid: text("author_did").notNull(), 93 rkey: text("rkey").notNull(), 94 trailUri: text("trail_uri").notNull(), 95 record: jsonb("record").notNull().$type<CompletionRecord>(), 96 createdAt: timestamp("created_at", { withTimezone: true }).notNull(), 97 indexedAt: timestamp("indexed_at", { withTimezone: true }).notNull().defaultNow(), 98}); 99 100export const drafts = pgTable("drafts", { 101 id: text("id").primaryKey(), // Composite key: "author_did:rkey" 102 authorDid: text("author_did").notNull(), 103 rkey: text("rkey").notNull(), 104 record: jsonb("record").notNull().$type<DraftRecord>(), 105 createdAt: timestamp("created_at", { withTimezone: true }).notNull(), 106 updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(), 107 version: integer("version").notNull().default(1), 108}); 109 110export const ingestionCursor = pgTable("ingestion_cursor", { 111 id: integer("id").primaryKey().default(1), 112 cursorUs: bigint("cursor_us", { mode: "number" }).notNull(), 113 updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), 114}); 115 116// Account status tracking for takedown/suspension handling 117// Status values follow AT Protocol account lifecycle: 118// - null: active account (default) 119// - 'deactivated': user temporarily paused account (hide content, keep data) 120// - 'suspended': host temporarily paused account (hide content, keep data) 121// - 'takendown': host took down account (delete content) 122// - 'deleted': account deleted (delete content) 123export const accounts = pgTable("accounts", { 124 did: text("did").primaryKey(), 125 active: integer("active").notNull().default(1), // 1 = active, 0 = inactive 126 status: text("status"), // null, 'deactivated', 'suspended', 'takendown', 'deleted' 127 seq: bigint("seq", { mode: "number" }).notNull(), 128 shadowban: integer("shadowban").notNull().default(0), // 1 = hide from homepage 129 updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), 130}); 131 132// Auth tables 133export const authState = pgTable("auth_state", { 134 key: text("key").primaryKey(), 135 state: text("state").notNull(), 136 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), 137}); 138 139export const authSession = pgTable("auth_session", { 140 key: text("key").primaryKey(), 141 session: text("session").notNull(), 142 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), 143 updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), 144});