The recipes.blue monorepo recipes.blue
recipes appview atproto
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: refactored query

+66 -43
+49 -35
apps/api/src/xrpc/blue.recipes.feed.getRecipe.ts
··· 1 1 import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 2 - import { BlueRecipesFeedGetRecipe } from '@cookware/lexicons'; 3 - import { db, and, eq } from '@cookware/database'; 4 - import { Client, simpleFetchHandler } from '@atcute/client'; 5 - import { getAuthorInfo, parseDid } from '../util/api.js'; 2 + import { BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe, BlueRecipesActorDefs } from '@cookware/lexicons'; 3 + import { db, and, or, eq } from '@cookware/database'; 4 + import { parseDid } from '../util/api.js'; 6 5 import { Logger } from 'pino'; 6 + import { parseResourceUri, ResourceUri } from '@atcute/lexicons'; 7 + import { recipeTable } from '@cookware/database/schema'; 8 + import { isLegacyBlob } from '@atcute/lexicons/interfaces'; 9 + 10 + const invalidUriError = (uri: string) => new XRPCError({ 11 + status: 400, 12 + error: 'InvalidURI', 13 + description: `The provided URI is invalid: ${uri}`, 14 + }); 7 15 8 16 export const registerGetRecipe = (router: XRPCRouter, _logger: Logger) => { 9 17 router.addQuery(BlueRecipesFeedGetRecipe.mainSchema, { 10 - async handler({ params: { did, rkey } }) { 11 - if (!did) throw new Error('Invalid DID'); 12 - if (!rkey) throw new Error('Invalid rkey'); 18 + async handler({ params: { uris } }) { 19 + const whereClauses = []; 13 20 14 - const actor = await parseDid(did); 21 + for (const uri of uris) { 22 + const parsed = parseResourceUri(uri); 23 + if (!parsed.ok) throw invalidUriError(uri); 24 + 25 + const { repo, collection, rkey } = parsed.value; 26 + if (!repo) throw invalidUriError(uri); 27 + if (collection !== BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected) throw invalidUriError(uri); 28 + if (!rkey) throw invalidUriError(uri); 15 29 16 - const recipe = await db.query.recipeTable.findFirst({ 17 - where: t => and(eq(t.authorDid, actor), eq(t.rkey, rkey)), 18 - }); 30 + const did = await parseDid(repo); 19 31 20 - if (!recipe) { 21 - throw new XRPCError({ 22 - status: 404, 23 - error: 'RecipeNotFound', 24 - description: 'No such recipe was found in the index.', 25 - }); 32 + whereClauses.push(and(eq(recipeTable.did, did), eq(recipeTable.rkey, rkey))); 26 33 } 27 34 28 - const rpc = new Client({ 29 - handler: simpleFetchHandler({ 30 - service: 'https://public.api.bsky.app', 31 - }), 35 + const recipes = await db.query.recipeTable.findMany({ 36 + orderBy: recipeTable.createdAt, 37 + where: or(...whereClauses), 32 38 }); 33 39 34 - const authorInfo = await getAuthorInfo(recipe.authorDid, rpc); 35 - 36 40 return json({ 37 - recipe: { 38 - author: authorInfo, 39 - title: recipe.title, 40 - time: recipe.time, 41 - serves: recipe.serves || undefined, 42 - description: recipe.description || undefined, 43 - ingredients: recipe.ingredients, 44 - steps: recipe.steps, 45 - imageUrl: recipe.imageRef 46 - ? `https://cdn.bsky.app/img/feed_thumbnail/plain/${recipe.authorDid}/${recipe.imageRef}@jpeg` 47 - : undefined, 48 - }, 41 + recipes: recipes.map((recipe) => ({ 42 + author: { 43 + $type: BlueRecipesActorDefs.profileViewBasicSchema.shape.$type.wrapped.expected, 44 + did: recipe.did, 45 + handle: 'hayden.moe', 46 + createdAt: new Date().toISOString(), 47 + }, 48 + cid: '', 49 + indexedAt: new Date().toISOString(), 50 + record: { 51 + $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected, 52 + title: recipe.title, 53 + description: recipe.description ?? undefined, 54 + time: recipe.time ?? undefined, 55 + serves: recipe.serves ?? undefined, 56 + ingredients: recipe.ingredients as BlueRecipesFeedRecipe.Ingredient[], 57 + steps: recipe.steps as BlueRecipesFeedRecipe.Step[], 58 + image: isLegacyBlob(recipe.imageRef) ? undefined : recipe.imageRef ?? undefined, 59 + createdAt: recipe.createdAt.toISOString(), 60 + }, 61 + uri: recipe.uri as ResourceUri, 62 + })), 49 63 }); 50 64 }, 51 65 });
+8 -6
libs/database/lib/schema.ts
··· 1 1 import { customType, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 2 import { BlueRecipesFeedRecipe, BlueRecipesActorProfile } from "@cookware/lexicons"; 3 - import { isCid, type AtprotoDid } from "@atcute/lexicons/syntax"; 4 - import { Blob } from "@atcute/lexicons"; 3 + import { isCid, ResourceUri, type AtprotoDid } from "@atcute/lexicons/syntax"; 4 + import { Blob, LegacyBlob } from "@atcute/lexicons"; 5 5 import { sql, type SQL } from "drizzle-orm"; 6 - import { isBlob, isCidLink, isLegacyBlob, LegacyBlob } from "@atcute/lexicons/interfaces"; 6 + import { isBlob, isCidLink, isLegacyBlob } from "@atcute/lexicons/interfaces"; 7 7 8 8 const dateIsoText = customType<{ data: Date; driverData: string }>({ 9 9 dataType() { ··· 13 13 fromDriver: (value) => new Date(value), 14 14 }); 15 15 16 - const atBlob = customType<{ data: Blob | LegacyBlob; driverData: string }>({ 16 + const atBlob = customType<{ data: Blob | LegacyBlob; driverData: string; }>({ 17 17 dataType() { 18 18 return "text"; 19 19 }, ··· 26 26 throw new Error('Invalid blob value'); 27 27 } 28 28 }, 29 - fromDriver: (value) => { 29 + fromDriver: (value): Blob | LegacyBlob => { 30 + if (typeof value !== 'string') throw new Error('Invalid blob ref data type'); 30 31 var parts = value.split(':'); 31 32 if (value.startsWith('l:')) { 32 33 if (parts.length !== 3) throw new Error('Invalid legacy blob ref format'); ··· 54 55 55 56 export const profilesTable = sqliteTable("profiles", { 56 57 uri: text('uri') 57 - .generatedAlwaysAs((): SQL => sql`'at://' || ${profilesTable.did} || '/${BlueRecipesActorProfile.mainSchema.object.shape.$type}/self'`), 58 + .generatedAlwaysAs((): SQL => sql`'at://' || ${profilesTable.did} || '/${BlueRecipesActorProfile.mainSchema.object.shape.$type}/self'`) 59 + .$type<ResourceUri>(), 58 60 did: text("did").$type<AtprotoDid>().notNull().primaryKey(), 59 61 ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`), 60 62
+1
libs/database/tsconfig.build.json
··· 1 1 { 2 2 "extends": "./tsconfig.json", 3 + "include": ["lib"], 3 4 "compilerOptions": { 4 5 "noEmit": false, 5 6 "outDir": "./dist"
+1 -1
libs/lexicons/lexicons/feed/getRecipe.tsp
··· 6 6 @query 7 7 @errors(NotFound, InvalidUri) 8 8 op main( 9 - @required uris: atUri, 9 + @required uris: atUri[], 10 10 ): { 11 11 @required recipes: blue.recipes.feed.defs.RecipeView[]; 12 12 };
+7 -1
libs/lexicons/lib/types/blue/recipes/feed/getRecipe.ts
··· 5 5 6 6 const _mainSchema = /*#__PURE__*/ v.query("blue.recipes.feed.getRecipe", { 7 7 params: /*#__PURE__*/ v.object({ 8 - uris: /*#__PURE__*/ v.resourceUriString(), 8 + /** 9 + * @minLength 1 10 + */ 11 + uris: /*#__PURE__*/ v.constrain( 12 + /*#__PURE__*/ v.array(/*#__PURE__*/ v.resourceUriString()), 13 + [/*#__PURE__*/ v.arrayLength(1)], 14 + ), 9 15 }), 10 16 output: { 11 17 type: "lex",