The recipes.blue monorepo recipes.blue
recipes appview atproto

feat: add db table for profile record

hayden.moe a4776126 1e3596f6

verified
+389 -7
+73 -7
libs/database/lib/schema.ts
··· 1 - import { customType, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 - import type { BlueRecipesFeedRecipe } from "@cookware/lexicons"; 3 - import { type AtprotoDid } from "@atcute/lexicons/syntax"; 1 + import { customType, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 + import { BlueRecipesFeedRecipe, BlueRecipesActorProfile } from "@cookware/lexicons"; 3 + import { isCid, type AtprotoDid } from "@atcute/lexicons/syntax"; 4 + import { Blob } from "@atcute/lexicons"; 4 5 import { sql, type SQL } from "drizzle-orm"; 6 + import { isBlob, isCidLink, isLegacyBlob, LegacyBlob } from "@atcute/lexicons/interfaces"; 5 7 6 8 const dateIsoText = customType<{ data: Date; driverData: string }>({ 7 9 dataType() { ··· 11 13 fromDriver: (value) => new Date(value), 12 14 }); 13 15 16 + const atBlob = customType<{ data: Blob | LegacyBlob; driverData: string }>({ 17 + dataType() { 18 + return "text"; 19 + }, 20 + toDriver: (value) => { 21 + if (isLegacyBlob(value)) { 22 + return `l:${value.cid}:${value.mimeType}`; 23 + } else if (isBlob(value)) { 24 + return `b:${value.ref.$link}:${value.mimeType}:${value.size}`; 25 + } else { 26 + throw new Error('Invalid blob value'); 27 + } 28 + }, 29 + fromDriver: (value) => { 30 + var parts = value.split(':'); 31 + if (value.startsWith('l:')) { 32 + if (parts.length !== 3) throw new Error('Invalid legacy blob ref format'); 33 + if (!isCid(parts[1]!)) throw new Error('Invalid CID in legacy blob ref'); 34 + return { 35 + cid: parts[1]!, 36 + mimeType: parts[2]!, 37 + } as LegacyBlob; 38 + } else if (value.startsWith('b:')) { 39 + if (parts.length !== 4) throw new Error('Invalid blob ref format'); 40 + if (!isCidLink(parts[0])) throw new Error('Invalid CID link in blob ref'); 41 + if (isNaN(parseInt(parts[2]!, 10))) throw new Error('Invalid size in blob ref'); 42 + 43 + return { 44 + $type: 'blob', 45 + mimeType: parts[2], 46 + size: parseInt(parts[3]!), 47 + ref: { $link: parts[1] } 48 + } as Blob; 49 + } else { 50 + throw new Error('Invalid blob ref prefix'); 51 + } 52 + }, 53 + }); 54 + 55 + export const profilesTable = sqliteTable("profiles", { 56 + uri: text('uri') 57 + .generatedAlwaysAs((): SQL => sql`'at://' || ${profilesTable.did} || '/${BlueRecipesActorProfile.mainSchema.object.shape.$type}/self'`), 58 + did: text("did").$type<AtprotoDid>().notNull().primaryKey(), 59 + ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`), 60 + 61 + displayName: text('display_name', { length: 640 }).notNull(), 62 + description: text('description', { length: 2500 }), 63 + pronouns: text('pronouns', { length: 200 }), 64 + website: text('website'), 65 + avatarRef: atBlob('avatar'), 66 + bannerRef: atBlob('banner'), 67 + createdAt: dateIsoText("created_at").notNull(), 68 + }, t => ([ 69 + index('profiles_cat_idx').on(t.createdAt), 70 + index('profiles_iat_idx').on(t.ingestedAt), 71 + ])); 72 + 14 73 export const recipeTable = sqliteTable("recipes", { 15 74 uri: text('uri') 16 - .generatedAlwaysAs((): SQL => sql`${recipeTable.authorDid} || '/' || ${recipeTable.rkey}`), 75 + .generatedAlwaysAs((): SQL => sql`'at://' || ${recipeTable.did} || '/${BlueRecipesFeedRecipe.mainSchema.object.shape.$type}/' || ${recipeTable.rkey}`), 17 76 18 - authorDid: text("author_did").$type<AtprotoDid>().notNull(), 77 + did: text("author_did") 78 + .$type<AtprotoDid>() 79 + .notNull() 80 + .references(() => profilesTable.did, { onDelete: 'cascade' }), 19 81 rkey: text('rkey').notNull(), 20 82 21 - imageRef: text('image_ref'), 83 + imageRef: atBlob('image'), 22 84 23 85 title: text('title').notNull(), 24 86 time: int('time').notNull().default(0), ··· 32 94 stepsCount: int('steps_count').generatedAlwaysAs((): SQL => sql`json_array_length(${recipeTable.steps})`), 33 95 34 96 createdAt: dateIsoText("created_at").notNull(), 97 + ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`), 35 98 }, t => ([ 36 - primaryKey({ columns: [ t.authorDid, t.rkey ] }), 99 + index('recipes_title_idx').on(t.title), 100 + index('recipes_cat_idx').on(t.createdAt), 101 + index('recipes_iat_idx').on(t.ingestedAt), 102 + primaryKey({ columns: [ t.did, t.rkey ] }), 37 103 ]));
+23
libs/database/migrations/0001_past_umar.sql
··· 1 + ALTER TABLE `recipes` RENAME COLUMN "image_ref" TO "image";--> statement-breakpoint 2 + CREATE TABLE `profiles` ( 3 + `uri` text GENERATED ALWAYS AS ('at://' || "did" || '/?/self') VIRTUAL, 4 + `did` text PRIMARY KEY NOT NULL, 5 + `ingested_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL, 6 + `display_name` text(640) NOT NULL, 7 + `description` text(2500), 8 + `pronouns` text(200), 9 + `website` text, 10 + `avatar` text, 11 + `banner` text, 12 + `created_at` text NOT NULL 13 + ); 14 + --> statement-breakpoint 15 + CREATE INDEX `profiles_cat_idx` ON `profiles` (`created_at`);--> statement-breakpoint 16 + CREATE INDEX `profiles_iat_idx` ON `profiles` (`ingested_at`);--> statement-breakpoint 17 + ALTER TABLE `recipes` DROP COLUMN `uri`;--> statement-breakpoint 18 + ALTER TABLE `recipes` ADD `uri` text GENERATED ALWAYS AS ('at://' || "author_did" || '/?/' || "rkey") VIRTUAL;--> statement-breakpoint 19 + ALTER TABLE `recipes` ADD `ingested_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL;--> statement-breakpoint 20 + CREATE INDEX `recipes_title_idx` ON `recipes` (`title`);--> statement-breakpoint 21 + CREATE INDEX `recipes_cat_idx` ON `recipes` (`created_at`);--> statement-breakpoint 22 + CREATE INDEX `recipes_iat_idx` ON `recipes` (`ingested_at`);--> statement-breakpoint 23 + ALTER TABLE `recipes` ALTER COLUMN "author_did" TO "author_did" text NOT NULL REFERENCES profiles(did) ON DELETE cascade ON UPDATE no action;
+286
libs/database/migrations/meta/0001_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "d6f06b7d-9822-43ee-b96c-3b980a5e4953", 5 + "prevId": "7b2675f9-5d97-4fac-983e-978efd250faf", 6 + "tables": { 7 + "profiles": { 8 + "name": "profiles", 9 + "columns": { 10 + "uri": { 11 + "name": "uri", 12 + "type": "text", 13 + "primaryKey": false, 14 + "notNull": false, 15 + "autoincrement": false, 16 + "generated": { 17 + "as": "('at://' || \"did\" || '/?/self')", 18 + "type": "virtual" 19 + } 20 + }, 21 + "did": { 22 + "name": "did", 23 + "type": "text", 24 + "primaryKey": true, 25 + "notNull": true, 26 + "autoincrement": false 27 + }, 28 + "ingested_at": { 29 + "name": "ingested_at", 30 + "type": "text", 31 + "primaryKey": false, 32 + "notNull": true, 33 + "autoincrement": false, 34 + "default": "CURRENT_TIMESTAMP" 35 + }, 36 + "display_name": { 37 + "name": "display_name", 38 + "type": "text(640)", 39 + "primaryKey": false, 40 + "notNull": true, 41 + "autoincrement": false 42 + }, 43 + "description": { 44 + "name": "description", 45 + "type": "text(2500)", 46 + "primaryKey": false, 47 + "notNull": false, 48 + "autoincrement": false 49 + }, 50 + "pronouns": { 51 + "name": "pronouns", 52 + "type": "text(200)", 53 + "primaryKey": false, 54 + "notNull": false, 55 + "autoincrement": false 56 + }, 57 + "website": { 58 + "name": "website", 59 + "type": "text", 60 + "primaryKey": false, 61 + "notNull": false, 62 + "autoincrement": false 63 + }, 64 + "avatar": { 65 + "name": "avatar", 66 + "type": "text", 67 + "primaryKey": false, 68 + "notNull": false, 69 + "autoincrement": false 70 + }, 71 + "banner": { 72 + "name": "banner", 73 + "type": "text", 74 + "primaryKey": false, 75 + "notNull": false, 76 + "autoincrement": false 77 + }, 78 + "created_at": { 79 + "name": "created_at", 80 + "type": "text", 81 + "primaryKey": false, 82 + "notNull": true, 83 + "autoincrement": false 84 + } 85 + }, 86 + "indexes": { 87 + "profiles_cat_idx": { 88 + "name": "profiles_cat_idx", 89 + "columns": [ 90 + "created_at" 91 + ], 92 + "isUnique": false 93 + }, 94 + "profiles_iat_idx": { 95 + "name": "profiles_iat_idx", 96 + "columns": [ 97 + "ingested_at" 98 + ], 99 + "isUnique": false 100 + } 101 + }, 102 + "foreignKeys": {}, 103 + "compositePrimaryKeys": {}, 104 + "uniqueConstraints": {}, 105 + "checkConstraints": {} 106 + }, 107 + "recipes": { 108 + "name": "recipes", 109 + "columns": { 110 + "uri": { 111 + "name": "uri", 112 + "type": "text", 113 + "primaryKey": false, 114 + "notNull": false, 115 + "autoincrement": false, 116 + "generated": { 117 + "as": "('at://' || \"author_did\" || '/?/' || \"rkey\")", 118 + "type": "virtual" 119 + } 120 + }, 121 + "author_did": { 122 + "name": "author_did", 123 + "type": "text", 124 + "primaryKey": false, 125 + "notNull": true, 126 + "autoincrement": false 127 + }, 128 + "rkey": { 129 + "name": "rkey", 130 + "type": "text", 131 + "primaryKey": false, 132 + "notNull": true, 133 + "autoincrement": false 134 + }, 135 + "image": { 136 + "name": "image", 137 + "type": "text", 138 + "primaryKey": false, 139 + "notNull": false, 140 + "autoincrement": false 141 + }, 142 + "title": { 143 + "name": "title", 144 + "type": "text", 145 + "primaryKey": false, 146 + "notNull": true, 147 + "autoincrement": false 148 + }, 149 + "time": { 150 + "name": "time", 151 + "type": "integer", 152 + "primaryKey": false, 153 + "notNull": true, 154 + "autoincrement": false, 155 + "default": 0 156 + }, 157 + "serves": { 158 + "name": "serves", 159 + "type": "integer", 160 + "primaryKey": false, 161 + "notNull": false, 162 + "autoincrement": false 163 + }, 164 + "description": { 165 + "name": "description", 166 + "type": "text", 167 + "primaryKey": false, 168 + "notNull": false, 169 + "autoincrement": false 170 + }, 171 + "ingredients": { 172 + "name": "ingredients", 173 + "type": "text", 174 + "primaryKey": false, 175 + "notNull": true, 176 + "autoincrement": false 177 + }, 178 + "ingredients_count": { 179 + "name": "ingredients_count", 180 + "type": "integer", 181 + "primaryKey": false, 182 + "notNull": false, 183 + "autoincrement": false, 184 + "generated": { 185 + "as": "(json_array_length(\"ingredients\"))", 186 + "type": "virtual" 187 + } 188 + }, 189 + "steps": { 190 + "name": "steps", 191 + "type": "text", 192 + "primaryKey": false, 193 + "notNull": true, 194 + "autoincrement": false 195 + }, 196 + "steps_count": { 197 + "name": "steps_count", 198 + "type": "integer", 199 + "primaryKey": false, 200 + "notNull": false, 201 + "autoincrement": false, 202 + "generated": { 203 + "as": "(json_array_length(\"steps\"))", 204 + "type": "virtual" 205 + } 206 + }, 207 + "created_at": { 208 + "name": "created_at", 209 + "type": "text", 210 + "primaryKey": false, 211 + "notNull": true, 212 + "autoincrement": false 213 + }, 214 + "ingested_at": { 215 + "name": "ingested_at", 216 + "type": "text", 217 + "primaryKey": false, 218 + "notNull": true, 219 + "autoincrement": false, 220 + "default": "CURRENT_TIMESTAMP" 221 + } 222 + }, 223 + "indexes": { 224 + "recipes_title_idx": { 225 + "name": "recipes_title_idx", 226 + "columns": [ 227 + "title" 228 + ], 229 + "isUnique": false 230 + }, 231 + "recipes_cat_idx": { 232 + "name": "recipes_cat_idx", 233 + "columns": [ 234 + "created_at" 235 + ], 236 + "isUnique": false 237 + }, 238 + "recipes_iat_idx": { 239 + "name": "recipes_iat_idx", 240 + "columns": [ 241 + "ingested_at" 242 + ], 243 + "isUnique": false 244 + } 245 + }, 246 + "foreignKeys": { 247 + "recipes_author_did_profiles_did_fk": { 248 + "name": "recipes_author_did_profiles_did_fk", 249 + "tableFrom": "recipes", 250 + "tableTo": "profiles", 251 + "columnsFrom": [ 252 + "author_did" 253 + ], 254 + "columnsTo": [ 255 + "did" 256 + ], 257 + "onDelete": "cascade", 258 + "onUpdate": "no action" 259 + } 260 + }, 261 + "compositePrimaryKeys": { 262 + "recipes_author_did_rkey_pk": { 263 + "columns": [ 264 + "author_did", 265 + "rkey" 266 + ], 267 + "name": "recipes_author_did_rkey_pk" 268 + } 269 + }, 270 + "uniqueConstraints": {}, 271 + "checkConstraints": {} 272 + } 273 + }, 274 + "views": {}, 275 + "enums": {}, 276 + "_meta": { 277 + "schemas": {}, 278 + "tables": {}, 279 + "columns": { 280 + "\"recipes\".\"image_ref\"": "\"recipes\".\"image\"" 281 + } 282 + }, 283 + "internal": { 284 + "indexes": {} 285 + } 286 + }
+7
libs/database/migrations/meta/_journal.json
··· 8 8 "when": 1764024817179, 9 9 "tag": "0000_kind_ultron", 10 10 "breakpoints": true 11 + }, 12 + { 13 + "idx": 1, 14 + "version": "6", 15 + "when": 1764102063385, 16 + "tag": "0001_past_umar", 17 + "breakpoints": true 11 18 } 12 19 ] 13 20 }