The recipes.blue monorepo recipes.blue
recipes appview atproto

Compare changes

Choose any two refs to compare.

+1 -2
.env.example
··· 1 1 # Database 2 - DATABASE_URL=http://localhost:4001 3 - DATABASE_AUTH_TOKEN= 2 + DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres 4 3 5 4 # API 6 5 PORT=3000
+3 -1
apps/api/package.json
··· 4 4 "private": true, 5 5 "scripts": { 6 6 "build": "bun --bun run check-types && bun --bun run compile", 7 - "dev": "bun run --hot src/index.ts", 7 + "dev": "bun run --hot src/index.ts | pino-pretty", 8 8 "check-types": "tsc --noEmit", 9 9 "compile": "bun build src/index.ts --compile --minify --sourcemap --outfile=dist/api --target=bun", 10 10 "clean": "rimraf dist" ··· 20 20 "@cookware/lexicons": "workspace:*", 21 21 "@libsql/client": "^0.14.0", 22 22 "drizzle-orm": "catalog:", 23 + "hono": "^4.10.7", 23 24 "pino": "^9.5.0" 24 25 }, 25 26 "devDependencies": { ··· 27 28 "@cookware/tsconfig": "workspace:*", 28 29 "@types/bun": "catalog:", 29 30 "drizzle-kit": "^0.29.0", 31 + "pino-pretty": "^13.1.2", 30 32 "rimraf": "^6.0.1" 31 33 } 32 34 }
+15 -6
apps/api/src/index.ts
··· 6 6 import pino from 'pino'; 7 7 import { RedisClient } from 'bun'; 8 8 import { registerGetProfile } from './xrpc/blue.recipes.actor.getProfile.js'; 9 + import { Hono } from 'hono'; 10 + import { mountXrpcRouter } from './util/hono.js'; 9 11 10 12 const logger = pino(); 11 13 const redis = new RedisClient(Bun.env.REDIS_URL ?? "redis://127.0.0.1:6379/0"); 12 14 13 - const router = new XRPCRouter({ 15 + const xrpcRouter = new XRPCRouter({ 14 16 handleException: (err, _req) => { 15 17 if (err instanceof XRPCError) { 16 18 return err.toResponse(); ··· 34 36 }); 35 37 36 38 // actor 37 - registerGetProfile(router, logger, redis); 39 + registerGetProfile(xrpcRouter, logger, redis); 38 40 39 41 // feed 40 - registerGetRecipes(router, logger, redis); 41 - registerGetRecipe(router, logger, redis); 42 + registerGetRecipes(xrpcRouter, logger, redis); 43 + registerGetRecipe(xrpcRouter, logger, redis); 44 + 45 + const app = new Hono(); 46 + 47 + // mount xrpc router at /xrpc 48 + const xrpcApp = new Hono(); 49 + mountXrpcRouter(xrpcApp, xrpcRouter); 50 + app.route('/xrpc', xrpcApp); 42 51 43 52 const server = Bun.serve({ 44 53 port: process.env.PORT || 3000, 45 - ...router 54 + fetch: app.fetch, 46 55 }); 47 56 48 - console.log(`Server running on http://localhost:${server.port}`); 57 + logger.info({ url: server.url.toString() }, `Recipes.blue API started up`);
+18
apps/api/src/util/api.ts
··· 4 4 import { ActorIdentifier, AtprotoDid, Handle, isHandle } from '@atcute/lexicons/syntax'; 5 5 import { isAtprotoDid } from '@atcute/identity'; 6 6 import { RedisClient } from 'bun'; 7 + import { ProfileViewBasic } from '../../../../libs/lexicons/dist/types/blue/recipes/actor/defs.js'; 8 + import { Blob, LegacyBlob } from '@atcute/lexicons'; 9 + import { buildCdnUrl } from './cdn.js'; 7 10 8 11 const handleResolver = new CompositeHandleResolver({ 9 12 strategy: 'race', ··· 43 46 44 47 return handle; 45 48 } 49 + 50 + export const buildProfileViewBasic = async (author: { 51 + did: AtprotoDid; 52 + displayName: string; 53 + pronouns: string | null; 54 + avatarRef: Blob | LegacyBlob | null; 55 + createdAt: Date; 56 + }, redis: RedisClient): Promise<ProfileViewBasic> => ({ 57 + did: author.did, 58 + handle: await getHandle(author.did, redis), 59 + displayName: author.displayName, 60 + pronouns: author.pronouns ?? undefined, 61 + avatar: author.avatarRef ? buildCdnUrl('avatar', author.did, author.avatarRef) : undefined, 62 + createdAt: author.createdAt.toISOString(), 63 + });
+40
apps/api/src/util/hono.ts
··· 1 + import { XRPCRouter } from '@atcute/xrpc-server'; 2 + import type { Context, Hono } from 'hono'; 3 + 4 + export type ApiContext = {}; 5 + 6 + /** 7 + * mounts an @atcute/xrpc-server router into hono as a nested route 8 + * 9 + * basically just bridges the two request handlers since both are 10 + * web standard Request/Response. you can optionally pass hono context 11 + * properties to xrpc handlers via the request object 12 + */ 13 + export const mountXrpcRouter = ( 14 + app: Hono, 15 + router: XRPCRouter, 16 + injectContext?: (c: Context) => ApiContext, 17 + ) => { 18 + app.all('*', async (c) => { 19 + let request = c.req.raw; 20 + 21 + // if context injector provided, attach properties to request 22 + if (injectContext) { 23 + const contextData = injectContext(c); 24 + request = Object.assign(request, contextData); 25 + } 26 + 27 + const response = await router.fetch(request); 28 + return response; 29 + }); 30 + }; 31 + 32 + /** 33 + * helper to extract injected context from xrpc request 34 + * use this in your xrpc handlers to access hono context data 35 + */ 36 + export const getInjectedContext = ( 37 + request: Request 38 + ): ApiContext => { 39 + return request as any as ApiContext; 40 + };
+1 -1
apps/api/src/xrpc/blue.recipes.actor.getProfile.ts
··· 35 35 pronouns: profile.pronouns ?? undefined, 36 36 website: profile.website ?? undefined, 37 37 avatar: profile.avatarRef ? buildCdnUrl('avatar', profile.did, profile.avatarRef) : undefined, 38 - banner: profile.avatarRef ? buildCdnUrl('avatar', profile.did, profile.avatarRef) : undefined, 38 + banner: profile.bannerRef ? buildCdnUrl('feed_thumbnail', profile.did, profile.bannerRef) : undefined, 39 39 recipesCount: profile.recipesCount, 40 40 createdAt: profile.createdAt.toISOString(), 41 41 });
+36 -25
apps/api/src/xrpc/blue.recipes.feed.getRecipe.ts
··· 1 1 import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 2 - import { BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe, BlueRecipesActorDefs } from '@cookware/lexicons'; 2 + import { BlueRecipesFeedDefs, BlueRecipesFeedGetRecipe, BlueRecipesFeedRecipe } from '@cookware/lexicons'; 3 3 import { db, and, or, eq } from '@cookware/database'; 4 - import { parseDid } from '../util/api.js'; 4 + import { buildProfileViewBasic, parseDid } from '../util/api.js'; 5 5 import { Logger } from 'pino'; 6 6 import { parseResourceUri, ResourceUri } from '@atcute/lexicons'; 7 7 import { recipeTable } from '@cookware/database/schema'; 8 8 import { isLegacyBlob } from '@atcute/lexicons/interfaces'; 9 9 import { RedisClient } from 'bun'; 10 + import { buildCdnUrl } from '../util/cdn.js'; 10 11 11 12 const invalidUriError = (uri: string) => new XRPCError({ 12 13 status: 400, ··· 14 15 description: `The provided URI is invalid: ${uri}`, 15 16 }); 16 17 17 - export const registerGetRecipe = (router: XRPCRouter, _logger: Logger, _redis: RedisClient) => { 18 + export const registerGetRecipe = (router: XRPCRouter, _logger: Logger, redis: RedisClient) => { 18 19 router.addQuery(BlueRecipesFeedGetRecipe.mainSchema, { 19 20 async handler({ params: { uris } }) { 20 21 const whereClauses = []; ··· 36 37 const recipes = await db.query.recipeTable.findMany({ 37 38 orderBy: recipeTable.createdAt, 38 39 where: or(...whereClauses), 40 + with: { 41 + author: { 42 + columns: { 43 + did: true, 44 + displayName: true, 45 + pronouns: true, 46 + avatarRef: true, 47 + createdAt: true, 48 + }, 49 + }, 50 + }, 39 51 }); 40 52 41 53 return json({ 42 - recipes: recipes.map((recipe) => ({ 43 - author: { 44 - $type: BlueRecipesActorDefs.profileViewBasicSchema.shape.$type.wrapped.expected, 45 - did: recipe.did, 46 - handle: 'hayden.moe', 47 - createdAt: new Date().toISOString(), 48 - }, 49 - cid: '', 50 - indexedAt: new Date().toISOString(), 51 - record: { 52 - $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected, 53 - title: recipe.title, 54 - description: recipe.description ?? undefined, 55 - time: recipe.time ?? undefined, 56 - serves: recipe.serves ?? undefined, 57 - ingredients: recipe.ingredients as BlueRecipesFeedRecipe.Ingredient[], 58 - steps: recipe.steps as BlueRecipesFeedRecipe.Step[], 59 - image: isLegacyBlob(recipe.imageRef) ? undefined : recipe.imageRef ?? undefined, 60 - createdAt: recipe.createdAt.toISOString(), 61 - }, 62 - uri: recipe.uri as ResourceUri, 63 - })), 54 + recipes: await Promise.all( 55 + recipes.map(async ({ author, ...recipe }) => ({ 56 + uri: recipe.uri as ResourceUri, 57 + author: await buildProfileViewBasic(author, redis), 58 + cid: recipe.cid, 59 + rkey: recipe.rkey, 60 + imageUrl: recipe.imageRef ? buildCdnUrl('post_image', recipe.did, recipe.imageRef) : undefined, 61 + indexedAt: recipe.ingestedAt.toISOString(), 62 + record: { 63 + $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected, 64 + title: recipe.title, 65 + description: recipe.description ?? undefined, 66 + time: recipe.time ?? undefined, 67 + serves: recipe.serves ?? undefined, 68 + ingredients: recipe.ingredients as BlueRecipesFeedRecipe.Ingredient[], 69 + steps: recipe.steps as BlueRecipesFeedRecipe.Step[], 70 + image: isLegacyBlob(recipe.imageRef) ? undefined : recipe.imageRef ?? undefined, 71 + createdAt: recipe.createdAt.toISOString(), 72 + }, 73 + })), 74 + ), 64 75 }); 65 76 }, 66 77 });
+13 -3
apps/api/src/xrpc/blue.recipes.feed.getRecipes.ts
··· 7 7 import { Logger } from 'pino'; 8 8 import { isLegacyBlob } from '@atcute/lexicons/interfaces'; 9 9 import { RedisClient } from 'bun'; 10 + import { buildCdnUrl } from '../util/cdn.js'; 10 11 11 12 export const registerGetRecipes = (router: XRPCRouter, _logger: Logger, redis: RedisClient) => { 12 13 router.addQuery(BlueRecipesFeedGetRecipes.mainSchema, { ··· 27 28 limit: limit, 28 29 where: whereClauses ? and(...whereClauses) : undefined, 29 30 with: { 30 - author: true, 31 + author: { 32 + columns: { 33 + did: true, 34 + displayName: true, 35 + pronouns: true, 36 + avatarRef: true, 37 + createdAt: true, 38 + }, 39 + }, 31 40 }, 32 41 }); 33 42 ··· 44 53 did: recipe.author.did, 45 54 handle: await getHandle(recipe.author.did, redis), 46 55 displayName: recipe.author.displayName, 47 - avatar: isLegacyBlob(recipe.author.avatarRef) ? undefined : recipe.author.avatarRef ?? undefined, 56 + avatar: recipe.author.avatarRef ? buildCdnUrl('avatar', recipe.author.did, recipe.author.avatarRef) : undefined, 48 57 pronouns: recipe.author.pronouns ?? undefined, 49 58 createdAt: recipe.author.createdAt.toISOString(), 50 59 }, 51 - cid: '', 60 + cid: recipe.cid, 61 + rkey: recipe.rkey, 52 62 indexedAt: recipe.ingestedAt.toISOString(), 53 63 record: { 54 64 $type: BlueRecipesFeedRecipe.mainSchema.object.shape.$type.expected,
+2 -2
apps/web/src/components/nav-user-opts.tsx
··· 7 7 SidebarMenuButton, 8 8 SidebarMenuItem, 9 9 } from "@/components/ui/sidebar" 10 - import { useAuth } from "@/state/auth" 10 + import { useSession } from "@/state/auth" 11 11 import { Link } from "@tanstack/react-router"; 12 12 import { LifeBuoy, Pencil, Send } from "lucide-react"; 13 13 14 14 export function NavUserOpts() { 15 - const { isLoggedIn } = useAuth(); 15 + const { isLoggedIn } = useSession(); 16 16 17 17 if (!isLoggedIn) { 18 18 return (
+5 -5
apps/web/src/components/nav-user.tsx
··· 20 20 } from "@/components/ui/sidebar" 21 21 import { Button } from "./ui/button" 22 22 import { Link } from "@tanstack/react-router" 23 - import { useAuth } from "@/state/auth" 23 + import { useSession } from "@/state/auth" 24 24 import { Skeleton } from "./ui/skeleton" 25 25 import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar" 26 26 import { useUserQuery } from "@/queries/self" 27 27 28 28 export function NavUser() { 29 29 const { isMobile } = useSidebar() 30 - const { isLoggedIn, agent, logOut } = useAuth(); 30 + const { isLoggedIn, agent, signOut } = useSession(); 31 31 32 32 const userQuery = useUserQuery(); 33 33 ··· 74 74 className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" 75 75 > 76 76 <Avatar className="h-8 w-8 rounded-lg"> 77 - <AvatarImage src={`https://cdn.bsky.app/img/avatar_thumbnail/plain/${agent.sub}/${userQuery.data.avatar?.ref.$link}@jpeg`} alt={userQuery.data.displayName} /> 77 + <AvatarImage src={userQuery.data.avatar} alt={userQuery.data.displayName} /> 78 78 <AvatarFallback className="rounded-lg">{userQuery.data.displayName}</AvatarFallback> 79 79 </Avatar> 80 80 <div className="grid flex-1 text-left text-sm leading-tight"> ··· 92 92 <DropdownMenuLabel className="p-0 font-normal"> 93 93 <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> 94 94 <Avatar className="h-8 w-8 rounded-lg"> 95 - <AvatarImage src={`https://cdn.bsky.app/img/avatar_thumbnail/plain/${agent.sub}/${userQuery.data.avatar?.ref.$link}@jpeg`} alt={userQuery.data.displayName} /> 95 + <AvatarImage src={userQuery.data.avatar} alt={userQuery.data.displayName} /> 96 96 <AvatarFallback className="rounded-lg">{userQuery.data.displayName}</AvatarFallback> 97 97 </Avatar> 98 98 <div className="grid flex-1 text-left text-sm leading-tight"> ··· 101 101 </div> 102 102 </DropdownMenuLabel> 103 103 <DropdownMenuSeparator /> 104 - <DropdownMenuItem className="cursor-pointer" onClick={() => logOut()}> 104 + <DropdownMenuItem className="cursor-pointer" onClick={() => signOut()}> 105 105 <LogOut /> 106 106 Log out 107 107 </DropdownMenuItem>
+7 -3
apps/web/src/components/query-placeholder.tsx
··· 1 1 import type { UseQueryResult } from '@tanstack/react-query'; 2 - import { PropsWithChildren, ReactNode } from 'react'; 2 + import { ReactNode } from 'react'; 3 3 import { Skeleton } from './ui/skeleton'; 4 4 import { Alert, AlertDescription, AlertTitle } from './ui/alert'; 5 5 import { AlertCircle } from 'lucide-react'; 6 6 import { isXRPCErrorPayload } from '@atcute/client'; 7 7 8 - type QueryPlaceholderProps<TData, TError> = PropsWithChildren<{ 8 + type QueryPlaceholderProps<TData, TError> = { 9 9 query: UseQueryResult<TData, TError>; 10 10 cards?: boolean; 11 11 cardsCount?: number; 12 12 noData?: ReactNode; 13 - }>; 13 + children: ReactNode | ReactNode[] | ((data: TData) => ReactNode | ReactNode[]); 14 + }; 14 15 15 16 const QueryPlaceholder = <TData = {}, TError = Error>( 16 17 { ··· 50 51 </Alert> 51 52 ) 52 53 } else if (query.data) { 54 + if (typeof children === 'function') { 55 + return children(query.data); 56 + } 53 57 return children; 54 58 } 55 59 return noData;
+11 -11
apps/web/src/components/recipe-card.tsx
··· 1 - import { BlueRecipesFeedGetRecipes } from "@atcute/client/lexicons"; 2 1 import { Card, CardContent, CardFooter, CardHeader } from "./ui/card"; 3 2 import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; 4 3 import { Link } from "@tanstack/react-router"; 5 4 import { Clock, ListOrdered, Users, Utensils } from "lucide-react"; 5 + import { BlueRecipesFeedGetRecipes } from "@cookware/lexicons"; 6 6 7 7 type RecipeCardProps = { 8 - recipe: BlueRecipesFeedGetRecipes.Result; 8 + recipe: BlueRecipesFeedGetRecipes.$output['recipes'][0]; 9 9 }; 10 10 11 11 function truncateDescription(description: string, maxLength: number = 120) { ··· 18 18 <Link to="/recipes/$author/$rkey" params={{ author: recipe.author.handle, rkey: recipe.rkey }} className="w-full"> 19 19 <Card className="overflow-hidden"> 20 20 <CardHeader className="p-0"> 21 - { recipe.imageUrl && 21 + { recipe.record.image && 22 22 <div className="relative h-48 w-full"> 23 23 <img 24 24 src={recipe.imageUrl} 25 - alt={recipe.title} 25 + alt={recipe.record.title} 26 26 className="h-full w-full object-cover" 27 27 /> 28 28 </div> 29 29 } 30 30 </CardHeader> 31 31 <CardContent className="p-4"> 32 - <h3 className="text-lg font-semibold mb-2">{recipe.title}</h3> 32 + <h3 className="text-lg font-semibold mb-2">{recipe.record.title}</h3> 33 33 <p className="text-sm text-muted-foreground mb-4"> 34 - {truncateDescription(recipe.description || '')} 34 + {truncateDescription(recipe.record.description || '')} 35 35 </p> 36 36 </CardContent> 37 37 <CardFooter className="p-4 pt-0"> 38 38 <div className="w-full flex items-center justify-between"> 39 39 <div className="flex items-center"> 40 40 <Avatar className="h-8 w-8 mr-2"> 41 - <AvatarImage src={recipe.author.avatarUrl} alt={recipe.author.displayName} /> 41 + <AvatarImage src={recipe.author.avatar} alt={recipe.author.displayName} /> 42 42 <AvatarFallback className="rounded-lg">{recipe.author.displayName?.charAt(0)}</AvatarFallback> 43 43 </Avatar> 44 44 <span className="text-sm text-muted-foreground">{recipe.author.displayName}</span> ··· 46 46 <div className="flex gap-6 justify-between items-center text-sm text-muted-foreground"> 47 47 <div className="flex items-center"> 48 48 <Utensils className="w-4 h-4 mr-1" /> 49 - <span>{recipe.ingredients}</span> 49 + <span>{recipe.record.ingredients.length}</span> 50 50 </div> 51 51 52 52 <div className="flex items-center"> 53 53 <ListOrdered className="w-4 h-4 mr-1" /> 54 - <span>{recipe.steps}</span> 54 + <span>{recipe.record.steps.length}</span> 55 55 </div> 56 56 57 57 <div className="flex items-center"> 58 58 <Users className="w-4 h-4 mr-1" /> 59 - <span>{recipe.serves}</span> 59 + <span>{recipe.record.serves}</span> 60 60 </div> 61 61 62 62 <div className="flex items-center"> 63 63 <Clock className="w-4 h-4 mr-1" /> 64 - <span>{recipe.time} min</span> 64 + <span>{recipe.record.time} min</span> 65 65 </div> 66 66 </div> 67 67 </div>
+6 -8
apps/web/src/queries/recipe.ts
··· 1 - import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { useAuth } from "@/state/auth"; 3 1 import { queryOptions, useMutation, useQuery } from "@tanstack/react-query"; 4 2 import { Client } from "@atcute/client"; 5 3 import { notFound } from "@tanstack/react-router"; ··· 23 21 const res = await client.get('blue.recipes.feed.getRecipes', { 24 22 params: { cursor, did }, 25 23 }); 24 + if (!res.ok) throw res.data; 26 25 return res.data; 27 26 }, 28 27 }); 29 28 }; 30 29 31 - export const recipeQueryOptions = (rpc: Client, did: Did, rkey: string) => { 30 + export const recipeQueryOptions = (rpc: Client, actor: ActorIdentifier, rkey: string) => { 32 31 return queryOptions({ 33 - queryKey: RQKEY('', did, rkey), 32 + queryKey: RQKEY('', actor, rkey), 34 33 queryFn: async () => { 35 34 const { ok, data } = await rpc.get('blue.recipes.feed.getRecipe', { 36 - params: { did, rkey }, 35 + params: { uris: [`at://${actor}/blue.recipes.feed.recipe/${rkey}`] }, 37 36 }); 38 37 39 38 if (!ok) { ··· 51 50 }; 52 51 53 52 export const useRecipeQuery = (did: Did, rkey: string) => { 54 - const rpc = useXrpc(); 53 + const rpc = useClient(); 55 54 return useQuery(recipeQueryOptions(rpc, did, rkey)); 56 55 }; 57 56 58 57 export const useNewRecipeMutation = (form: UseFormReturn<z.infer<typeof recipeSchema>>) => { 59 - const { agent } = useAuth(); 60 - const rpc = useXrpc(); 58 + const rpc = useClient(); 61 59 return useMutation({ 62 60 mutationKey: ['recipes.new'], 63 61 mutationFn: async ({ recipe: { image, ...recipe } }: { recipe: z.infer<typeof recipeSchema> }) => {
+7 -11
apps/web/src/queries/self.ts
··· 1 - import { useXrpc } from "@/hooks/use-xrpc"; 2 - import { useAuth } from "@/state/auth"; 3 - import { AppBskyActorProfile } from "@atcute/client/lexicons"; 4 - import { At } from "@atcute/client/lexicons"; 1 + import { useClient, useSession } from "@/state/auth"; 2 + import { BlueRecipesActorDefs } from "@cookware/lexicons"; 5 3 import { useQuery } from "@tanstack/react-query"; 6 4 7 5 export const useUserQuery = () => { 8 - const { isLoggedIn, agent } = useAuth(); 9 - const rpc = useXrpc(); 6 + const { isLoggedIn, agent } = useSession(); 7 + const rpc = useClient(); 10 8 11 9 return useQuery({ 12 10 queryKey: ['self'], 13 11 queryFn: async () => { 14 - const res = await rpc.get('com.atproto.repo.getRecord', { 12 + const res = await rpc.get('blue.recipes.actor.getProfile', { 15 13 params: { 16 - repo: agent?.sub as At.DID, 17 - collection: 'app.bsky.actor.profile', 18 - rkey: 'self', 14 + actor: agent?.sub! 19 15 }, 20 16 }); 21 17 22 - return res.data.value as AppBskyActorProfile.Record; 18 + return res.data as BlueRecipesActorDefs.ProfileViewDetailed; 23 19 }, 24 20 enabled: isLoggedIn, 25 21 });
+3 -3
apps/web/src/routes/_.(app)/index.lazy.tsx
··· 30 30 <BreadcrumbList> 31 31 <BreadcrumbItem className="hidden md:block"> 32 32 <BreadcrumbLink asChild> 33 - <Link href="/">Community</Link> 33 + <Link to="/">Community</Link> 34 34 </BreadcrumbLink> 35 35 </BreadcrumbItem> 36 36 <BreadcrumbSeparator className="hidden md:block" /> ··· 48 48 <div className="flex-1 flex flex-col items-center p-4"> 49 49 <div className="flex flex-col gap-4 max-w-2xl w-full items-center"> 50 50 <QueryPlaceholder query={query} cards cardsCount={12}> 51 - {query.data?.recipes.map((recipe, idx) => ( 51 + {data => data.recipes.map(recipe => ( 52 52 <RecipeCard 53 53 recipe={recipe} 54 - key={idx} 54 + key={`${recipe.author.did}-${recipe.rkey}`} 55 55 /> 56 56 ))} 57 57 </QueryPlaceholder>
+20 -20
apps/web/src/routes/_.(app)/recipes/$author/$rkey/index.lazy.tsx
··· 12 12 import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' 13 13 import { recipeQueryOptions } from '@/queries/recipe' 14 14 import { useSuspenseQuery } from '@tanstack/react-query' 15 - import { useXrpc } from '@/hooks/use-xrpc' 16 15 import { Badge } from '@/components/ui/badge' 17 16 import { Clock, Users } from 'lucide-react' 18 - import { useAuth } from '@/state/auth' 17 + import { useClient, useSession } from '@/state/auth' 19 18 import { Button } from '@/components/ui/button' 19 + import { ActorIdentifier } from '@atcute/lexicons' 20 20 21 21 export const Route = createLazyFileRoute('/_/(app)/recipes/$author/$rkey/')({ 22 22 component: RouteComponent, 23 23 }) 24 24 25 25 function RouteComponent() { 26 - const rpc = useXrpc(); 26 + const rpc = useClient(); 27 27 const { author, rkey } = Route.useParams() 28 28 const { 29 - data: { recipe }, 29 + data: { recipes }, 30 30 error, 31 - } = useSuspenseQuery(recipeQueryOptions(rpc, author, rkey)) 32 - const { isLoggedIn, agent } = useAuth(); 31 + } = useSuspenseQuery(recipeQueryOptions(rpc, author as ActorIdentifier, rkey)) 32 + const { isLoggedIn, agent } = useSession(); 33 33 34 - if (error) return <p>Error</p> 34 + if (error || !recipes[0]) return <p>Error</p> 35 35 36 36 return ( 37 37 <> ··· 55 55 <BreadcrumbSeparator className="hidden md:block" /> 56 56 <BreadcrumbItem className="hidden md:block"> 57 57 <BreadcrumbLink asChild> 58 - <Link to="/recipes/$author" params={{ author: recipe.author.handle }}> 59 - {recipe.author.displayName} 58 + <Link to="/recipes/$author" params={{ author: recipes[0].author.handle }}> 59 + {recipes[0].author.displayName} 60 60 </Link> 61 61 </BreadcrumbLink> 62 62 </BreadcrumbItem> 63 63 <BreadcrumbSeparator className="hidden md:block" /> 64 64 <BreadcrumbItem> 65 - <BreadcrumbPage>{recipe.title}</BreadcrumbPage> 65 + <BreadcrumbPage>{recipes[0].record.title}</BreadcrumbPage> 66 66 </BreadcrumbItem> 67 67 </BreadcrumbList> 68 68 </Breadcrumb> ··· 72 72 <Card className="w-full"> 73 73 74 74 <CardHeader> 75 - <CardTitle className="text-3xl font-bold">{recipe.title}</CardTitle> 76 - <CardDescription>{recipe.description}</CardDescription> 75 + <CardTitle className="text-3xl font-bold">{recipes[0].record.title}</CardTitle> 76 + <CardDescription>{recipes[0].record.description}</CardDescription> 77 77 </CardHeader> 78 78 79 79 <CardContent className="space-y-6"> 80 80 { 81 - recipe.imageUrl && 81 + recipes[0].record.image && 82 82 <img 83 - src={recipe.imageUrl} 84 - alt={recipe.title} 83 + src={recipes[0].record.image.ref.$link} 84 + alt={recipes[0].record.title} 85 85 className="h-64 w-full object-cover rounded-md" 86 86 /> 87 87 } 88 88 <div className="flex flex-wrap gap-4"> 89 89 <Badge variant="secondary" className="flex items-center gap-2"> 90 90 <Clock className="size-4" /> 91 - <span>{recipe.time} mins</span> 91 + <span>{recipes[0].record.time} mins</span> 92 92 </Badge> 93 93 <Badge variant="secondary" className="flex items-center gap-2"> 94 94 <Users className="size-4" /> 95 - <span>Serves {recipe.serves ?? '1'}</span> 95 + <span>Serves {recipes[0].record.serves ?? '1'}</span> 96 96 </Badge> 97 97 </div> 98 98 99 99 <div> 100 100 <h3 className="text-xl font-semibold mb-2">Ingredients</h3> 101 101 <ul className="list-disc list-inside space-y-1"> 102 - {recipe.ingredients.map((ing, idx) => ( 102 + {recipes[0].record.ingredients.map((ing, idx) => ( 103 103 <li key={idx}> 104 104 <b>{ing.amount}</b> {ing.name} 105 105 </li> ··· 110 110 <div> 111 111 <h3 className="text-xl font-semibold mb-2">Steps</h3> 112 112 <ol className="list-decimal list-outside space-y-1 ml-4"> 113 - {recipe.steps.map((ing, idx) => ( 113 + {recipes[0].record.steps.map((ing, idx) => ( 114 114 <li key={idx}>{ing.text}</li> 115 115 ))} 116 116 </ol> 117 117 </div> 118 118 </CardContent> 119 119 <CardFooter className="flex justify-between"> 120 - {(isLoggedIn && agent?.sub == recipe.author.did) && ( 120 + {(isLoggedIn && agent?.sub == recipes[0].author.did) && ( 121 121 <div className="flex items-center gap-x-4"> 122 122 <Button variant="outline">Edit</Button> 123 123 <Button variant="destructive">Delete</Button>
+1 -32
apps/web/src/routes/_.(app)/recipes/new.tsx
··· 9 9 } from "@/components/ui/breadcrumb"; 10 10 import { Separator } from "@/components/ui/separator"; 11 11 import { SidebarTrigger } from "@/components/ui/sidebar"; 12 - import { useFieldArray, useForm } from "react-hook-form"; 13 - import { z } from "zod"; 14 - import { zodResolver } from "@hookform/resolvers/zod"; 15 - import { 16 - Form, 17 - FormControl, 18 - FormDescription, 19 - FormField, 20 - FormItem, 21 - FormLabel, 22 - FormMessage, 23 - } from "@/components/ui/form"; 24 - import { Button } from "@/components/ui/button"; 25 - import { Input } from "@/components/ui/input"; 26 - import { Textarea } from "@/components/ui/textarea"; 27 - import { 28 - Card, 29 - CardContent, 30 - CardDescription, 31 - CardHeader, 32 - CardTitle, 33 - } from "@/components/ui/card"; 34 - import { 35 - Sortable, 36 - SortableDragHandle, 37 - SortableItem, 38 - } from "@/components/ui/sortable"; 39 - import { DragHandleDots2Icon } from "@radix-ui/react-icons"; 40 - import { Label } from "@/components/ui/label"; 41 - import { TrashIcon } from "lucide-react"; 42 - import { useNewRecipeMutation } from "@/queries/recipe"; 43 12 44 13 export const Route = createFileRoute("/_/(app)/recipes/new")({ 45 14 beforeLoad: async ({ context }) => { 46 - if (!context.auth.isLoggedIn) { 15 + if (!context.session.isLoggedIn) { 47 16 throw redirect({ 48 17 to: '/login', 49 18 });
+8 -8
apps/web/src/screens/Recipes/RecipeCard.tsx
··· 1 1 import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; 2 2 import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; 3 - import { BlueRecipesFeedGetRecipes } from "@atcute/client/lexicons"; 4 3 import { Link } from "@tanstack/react-router"; 5 4 import { Clock, CookingPot, ListIcon } from "lucide-react"; 5 + import { BlueRecipesFeedGetRecipes } from "@cookware/lexicons"; 6 6 7 7 type RecipeCardProps = { 8 - recipe: BlueRecipesFeedGetRecipes.Result; 8 + recipe: BlueRecipesFeedGetRecipes.$output['recipes'][0]; 9 9 }; 10 10 11 11 export const RecipeCard = ({ recipe }: RecipeCardProps) => { ··· 13 13 <Link to="/recipes/$author/$rkey" params={{ author: recipe.author.handle, rkey: recipe.rkey }} className="w-full"> 14 14 <Card className="w-full"> 15 15 <CardHeader> 16 + <CardTitle>{recipe.record.title}</CardTitle> 16 17 <CardDescription className="flex items-center space-x-2"> 17 18 <Avatar className="h-6 w-6 rounded-lg"> 18 - <AvatarImage src={recipe.author.avatarUrl} alt={recipe.author.displayName} /> 19 + <AvatarImage src={recipe.author.avatar} alt={recipe.author.displayName} /> 19 20 <AvatarFallback className="rounded-lg">{recipe.author.displayName}</AvatarFallback> 20 21 </Avatar> 21 22 22 23 <span>{recipe.author.displayName}</span> 23 24 </CardDescription> 24 - <CardTitle>{recipe.title}</CardTitle> 25 25 </CardHeader> 26 26 <CardContent> 27 - <p>{recipe.description}</p> 27 + <p>{recipe.record.description}</p> 28 28 </CardContent> 29 29 <CardFooter className="flex gap-6 text-sm text-muted-foreground"> 30 30 <span className="flex items-center gap-2"> 31 - <ListIcon className="size-4" /> <span>{recipe.steps}</span> 31 + <ListIcon className="size-4" /> <span>{recipe.record.steps.length}</span> 32 32 </span> 33 33 34 34 <span className="flex items-center gap-2"> 35 - <CookingPot className="size-4" /> <span>{recipe.ingredients}</span> 35 + <CookingPot className="size-4" /> <span>{recipe.record.ingredients.length}</span> 36 36 </span> 37 37 38 38 <span className="flex items-center gap-2"> 39 - <Clock className="size-4" /> <span>{recipe.time} mins</span> 39 + <Clock className="size-4" /> <span>{recipe.record.time} mins</span> 40 40 </span> 41 41 </CardFooter> 42 42 </Card>
+2 -2
apps/web/tailwind.config.js
··· 1 1 import animate from 'tailwindcss-animate'; 2 2 /** @type {import('tailwindcss').Config} */ 3 3 export default { 4 - darkMode: ["class"], 5 - content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 4 + darkMode: ["class"], 5 + content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], 6 6 theme: { 7 7 extend: { 8 8 borderRadius: {
+21 -1
bun.lock
··· 22 22 "@cookware/lexicons": "workspace:*", 23 23 "@libsql/client": "^0.14.0", 24 24 "drizzle-orm": "catalog:", 25 + "hono": "^4.10.7", 25 26 "pino": "^9.5.0", 26 27 }, 27 28 "devDependencies": { ··· 29 30 "@cookware/tsconfig": "workspace:*", 30 31 "@types/bun": "catalog:", 31 32 "drizzle-kit": "^0.29.0", 33 + "pino-pretty": "^13.1.2", 32 34 "rimraf": "^6.0.1", 33 35 }, 34 36 }, ··· 126 128 "dependencies": { 127 129 "@libsql/client": "^0.15.15", 128 130 "drizzle-orm": "catalog:", 131 + "pg": "^8.16.3", 129 132 "zod": "^3.23.8", 130 133 }, 131 134 "devDependencies": { ··· 135 138 "@cookware/tsconfig": "workspace:*", 136 139 "@types/bun": "catalog:", 137 140 "@types/node": "^22.10.1", 141 + "@types/pg": "^8.15.6", 138 142 "drizzle-kit": "^0.29.0", 139 143 "typescript": "^5.2.2", 140 144 }, ··· 817 821 818 822 "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], 819 823 820 - "@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], 824 + "@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], 821 825 822 826 "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], 823 827 ··· 1171 1175 1172 1176 "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], 1173 1177 1178 + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], 1179 + 1174 1180 "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], 1175 1181 1176 1182 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], ··· 1333 1339 1334 1340 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 1335 1341 1342 + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], 1343 + 1344 + "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], 1345 + 1346 + "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], 1347 + 1336 1348 "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], 1337 1349 1350 + "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], 1351 + 1338 1352 "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], 1339 1353 1340 1354 "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], 1355 + 1356 + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], 1341 1357 1342 1358 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 1343 1359 ··· 1715 1731 1716 1732 "@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], 1717 1733 1734 + "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], 1735 + 1718 1736 "@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], 1719 1737 1720 1738 "@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], ··· 1770 1788 "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], 1771 1789 1772 1790 "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], 1791 + 1792 + "@types/pg-pool/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], 1773 1793 1774 1794 "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 1775 1795
+13
config/dev/db/compose.yaml
··· 1 + --- 2 + volumes: 3 + postgres: {} 4 + 5 + services: 6 + postgres: 7 + image: postgres:18 8 + environment: 9 + POSTGRES_PASSWORD: postgres 10 + ports: 11 + - 5432:5432 12 + volumes: 13 + - postgres:/var/lib/postgresql/18/docker
-13
config/dev/libsql/compose.yaml
··· 1 - --- 2 - volumes: 3 - libsql: {} 4 - 5 - services: 6 - libsql: 7 - image: ghcr.io/tursodatabase/libsql-server:latest 8 - environment: 9 - SQLD_NODE: primary 10 - ports: 11 - - 4001:8080 12 - volumes: 13 - - libsql:/var/lib/sqld
+12
config/dev/redis/compose.yaml
··· 1 + --- 2 + volumes: 3 + redis: 4 + 5 + services: 6 + redis: 7 + image: redis:8 8 + command: redis-server --save 60 1 --loglevel warning 9 + ports: 10 + - 6379:6379 11 + volumes: 12 + - redis:/data
+2 -32
docker-compose.yaml
··· 1 1 --- 2 2 include: 3 - - path: config/dev/caddy/compose.yaml 4 - - path: config/dev/libsql/compose.yaml 3 + - path: config/dev/db/compose.yaml 4 + - path: config/dev/redis/compose.yaml 5 5 6 6 networks: 7 7 recipesblue: 8 - 9 - services: 10 - redis: 11 - image: redis:8 12 - ports: [6379:6379] 13 - 14 - api: 15 - build: 16 - context: . 17 - dockerfile: apps/api/Dockerfile 18 - restart: unless-stopped 19 - networks: [recipesblue] 20 - ports: 21 - - "3000:3000" 22 - environment: 23 - - DATABASE_URL=http://libsql:8080 24 - - PORT=3000 25 - depends_on: 26 - - libsql 27 - 28 - ingester: 29 - build: 30 - context: . 31 - dockerfile: apps/ingester/Dockerfile 32 - restart: unless-stopped 33 - networks: [recipesblue] 34 - environment: 35 - - DATABASE_URL=http://libsql:8080 36 - depends_on: 37 - - libsql
+2 -3
libs/database/drizzle.config.ts
··· 3 3 export default { 4 4 schema: "./lib/schema.ts", 5 5 out: "./migrations", 6 - dialect: "turso", 6 + dialect: "postgresql", 7 7 dbCredentials: { 8 - url: process.env.DATABASE_URL || "http://localhost:4001", 9 - authToken: process.env.DATABASE_AUTH_TOKEN 8 + url: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', 10 9 } 11 10 } satisfies Config;
+5 -6
libs/database/lib/index.ts
··· 1 - import { drizzle } from 'drizzle-orm/libsql'; 2 - import { createClient } from '@libsql/client'; 1 + import { drizzle } from 'drizzle-orm/node-postgres'; 2 + import { Pool } from 'pg'; 3 3 4 - const client = createClient({ 5 - url: process.env.TURSO_CONNECTION_URL || 'http://localhost:4001', 6 - authToken: process.env.TURSO_AUTH_TOKEN || '', 4 + const pool = new Pool({ 5 + connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/postgres', 7 6 }); 8 7 9 8 import * as schema from './schema.js'; 10 - export const db = drizzle(client, { schema }); 9 + export const db = drizzle(pool, { schema }); 11 10 12 11 // Re-export drizzle-orm functions to ensure single instance 13 12 export { eq, and, or, desc, asc, sql } from 'drizzle-orm';
+14 -13
libs/database/lib/schema.ts
··· 1 - import { customType, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 - import { BlueRecipesFeedRecipe, BlueRecipesActorProfile } from "@cookware/lexicons"; 1 + import { customType, index, integer, primaryKey, pgTable, text, jsonb, varchar } from "drizzle-orm/pg-core"; 2 + import { BlueRecipesFeedRecipe } from "@cookware/lexicons"; 3 3 import { Cid, isCid, ResourceUri, type AtprotoDid } from "@atcute/lexicons/syntax"; 4 4 import { Blob, LegacyBlob } from "@atcute/lexicons"; 5 5 import { relations, sql, type SQL } from "drizzle-orm"; ··· 53 53 }, 54 54 }); 55 55 56 - export const profilesTable = sqliteTable("profiles", { 56 + export const profilesTable = pgTable("profiles", { 57 57 uri: text('uri') 58 58 .generatedAlwaysAs((): SQL => sql`'at://' || ${profilesTable.did} || '/blue.recipes.actor.profile/self'`) 59 59 .$type<ResourceUri>(), 60 + 60 61 cid: text("cid").$type<Cid>().notNull(), 61 62 did: text("did").$type<AtprotoDid>().notNull().primaryKey(), 62 63 ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`), 63 64 64 - displayName: text('display_name', { length: 640 }).notNull(), 65 - description: text('description', { length: 2500 }), 66 - pronouns: text('pronouns', { length: 200 }), 65 + displayName: varchar('display_name', { length: 640 }).notNull(), 66 + description: varchar('description', { length: 2500 }), 67 + pronouns: varchar('pronouns', { length: 200 }), 67 68 website: text('website'), 68 69 avatarRef: atBlob('avatar'), 69 70 bannerRef: atBlob('banner'), ··· 74 75 index('profiles_iat_idx').on(t.ingestedAt), 75 76 ])); 76 77 77 - export const recipeTable = sqliteTable("recipes", { 78 + export const recipeTable = pgTable("recipes", { 78 79 uri: text('uri') 79 80 .generatedAlwaysAs((): SQL => sql`'at://' || ${recipeTable.did} || '/blue.recipes.feed.recipe/' || ${recipeTable.rkey}`), 80 81 ··· 88 89 imageRef: atBlob('image'), 89 90 90 91 title: text('title').notNull(), 91 - time: int('time').notNull().default(0), 92 - serves: int('serves'), 92 + time: integer('time').notNull().default(0), 93 + serves: integer('serves'), 93 94 description: text('description'), 94 95 95 - ingredients: text('ingredients', { mode: 'json' }).$type<BlueRecipesFeedRecipe.Main['ingredients']>().notNull(), 96 - ingredientsCount: int('ingredients_count').generatedAlwaysAs((): SQL => sql`json_array_length(${recipeTable.ingredients})`), 96 + ingredients: jsonb('ingredients').$type<BlueRecipesFeedRecipe.Main['ingredients']>().notNull(), 97 + ingredientsCount: integer('ingredients_count').generatedAlwaysAs((): SQL => sql`jsonb_array_length(${recipeTable.ingredients})`), 97 98 98 - steps: text('steps', { mode: 'json' }).$type<BlueRecipesFeedRecipe.Main['steps']>().notNull(), 99 - stepsCount: int('steps_count').generatedAlwaysAs((): SQL => sql`json_array_length(${recipeTable.steps})`), 99 + steps: jsonb('steps').$type<BlueRecipesFeedRecipe.Main['steps']>().notNull(), 100 + stepsCount: integer('steps_count').generatedAlwaysAs((): SQL => sql`jsonb_array_length(${recipeTable.steps})`), 100 101 101 102 createdAt: dateIsoText("created_at").notNull(), 102 103 ingestedAt: dateIsoText("ingested_at").notNull().default(sql`CURRENT_TIMESTAMP`),
-16
libs/database/migrations/0000_kind_ultron.sql
··· 1 - CREATE TABLE `recipes` ( 2 - `uri` text GENERATED ALWAYS AS ("author_did" || '/' || "rkey") VIRTUAL, 3 - `author_did` text NOT NULL, 4 - `rkey` text NOT NULL, 5 - `image_ref` text, 6 - `title` text NOT NULL, 7 - `time` integer DEFAULT 0 NOT NULL, 8 - `serves` integer, 9 - `description` text, 10 - `ingredients` text NOT NULL, 11 - `ingredients_count` integer GENERATED ALWAYS AS (json_array_length("ingredients")) VIRTUAL, 12 - `steps` text NOT NULL, 13 - `steps_count` integer GENERATED ALWAYS AS (json_array_length("steps")) VIRTUAL, 14 - `created_at` text NOT NULL, 15 - PRIMARY KEY(`author_did`, `rkey`) 16 - );
+46
libs/database/migrations/0000_young_hellcat.sql
··· 1 + CREATE TABLE IF NOT EXISTS "profiles" ( 2 + "uri" text GENERATED ALWAYS AS ('at://' || "profiles"."did" || '/blue.recipes.actor.profile/self') STORED, 3 + "cid" text NOT NULL, 4 + "did" text PRIMARY KEY NOT NULL, 5 + "ingested_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, 6 + "display_name" varchar(640) NOT NULL, 7 + "description" varchar(2500), 8 + "pronouns" varchar(200), 9 + "website" text, 10 + "avatar" text, 11 + "banner" text, 12 + "created_at" text NOT NULL 13 + ); 14 + --> statement-breakpoint 15 + CREATE TABLE IF NOT EXISTS "recipes" ( 16 + "uri" text GENERATED ALWAYS AS ('at://' || "recipes"."author_did" || '/blue.recipes.feed.recipe/' || "recipes"."rkey") STORED, 17 + "cid" text NOT NULL, 18 + "author_did" text NOT NULL, 19 + "rkey" text NOT NULL, 20 + "image" text, 21 + "title" text NOT NULL, 22 + "time" integer DEFAULT 0 NOT NULL, 23 + "serves" integer, 24 + "description" text, 25 + "ingredients" jsonb NOT NULL, 26 + "ingredients_count" integer GENERATED ALWAYS AS (jsonb_array_length("recipes"."ingredients")) STORED, 27 + "steps" jsonb NOT NULL, 28 + "steps_count" integer GENERATED ALWAYS AS (jsonb_array_length("recipes"."steps")) STORED, 29 + "created_at" text NOT NULL, 30 + "ingested_at" text DEFAULT CURRENT_TIMESTAMP NOT NULL, 31 + CONSTRAINT "recipes_author_did_rkey_pk" PRIMARY KEY("author_did","rkey") 32 + ); 33 + --> statement-breakpoint 34 + DO $$ BEGIN 35 + ALTER TABLE "recipes" ADD CONSTRAINT "recipes_author_did_profiles_did_fk" FOREIGN KEY ("author_did") REFERENCES "public"."profiles"("did") ON DELETE cascade ON UPDATE no action; 36 + EXCEPTION 37 + WHEN duplicate_object THEN null; 38 + END $$; 39 + --> statement-breakpoint 40 + CREATE INDEX IF NOT EXISTS "profiles_cid_idx" ON "profiles" USING btree ("cid");--> statement-breakpoint 41 + CREATE INDEX IF NOT EXISTS "profiles_cat_idx" ON "profiles" USING btree ("created_at");--> statement-breakpoint 42 + CREATE INDEX IF NOT EXISTS "profiles_iat_idx" ON "profiles" USING btree ("ingested_at");--> statement-breakpoint 43 + CREATE INDEX IF NOT EXISTS "recipes_title_idx" ON "recipes" USING btree ("title");--> statement-breakpoint 44 + CREATE INDEX IF NOT EXISTS "recipes_cid_idx" ON "recipes" USING btree ("cid");--> statement-breakpoint 45 + CREATE INDEX IF NOT EXISTS "recipes_cat_idx" ON "recipes" USING btree ("created_at");--> statement-breakpoint 46 + CREATE INDEX IF NOT EXISTS "recipes_iat_idx" ON "recipes" USING btree ("ingested_at");
-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;
-4
libs/database/migrations/0002_cheerful_venom.sql
··· 1 - ALTER TABLE `profiles` DROP COLUMN `uri`;--> statement-breakpoint 2 - ALTER TABLE `profiles` ADD `uri` text GENERATED ALWAYS AS ('at://' || "did" || '/blue.recipes.actor.profile/self') VIRTUAL;--> statement-breakpoint 3 - ALTER TABLE `recipes` DROP COLUMN `uri`;--> statement-breakpoint 4 - ALTER TABLE `recipes` ADD `uri` text GENERATED ALWAYS AS ('at://' || "author_did" || '/blue.recipes.feed.recipe/' || "rkey") VIRTUAL;
-4
libs/database/migrations/0003_long_blue_marvel.sql
··· 1 - ALTER TABLE `profiles` ADD `cid` text NOT NULL;--> statement-breakpoint 2 - CREATE INDEX `profiles_cid_idx` ON `profiles` (`cid`);--> statement-breakpoint 3 - ALTER TABLE `recipes` ADD `cid` text NOT NULL;--> statement-breakpoint 4 - CREATE INDEX `recipes_cid_idx` ON `recipes` (`cid`);
+255 -46
libs/database/migrations/meta/0000_snapshot.json
··· 1 1 { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "7b2675f9-5d97-4fac-983e-978efd250faf", 2 + "id": "5d896b70-087b-421e-8d94-c100476cd926", 5 3 "prevId": "00000000-0000-0000-0000-000000000000", 4 + "version": "7", 5 + "dialect": "postgresql", 6 6 "tables": { 7 - "recipes": { 7 + "public.profiles": { 8 + "name": "profiles", 9 + "schema": "", 10 + "columns": { 11 + "uri": { 12 + "name": "uri", 13 + "type": "text", 14 + "primaryKey": false, 15 + "notNull": false, 16 + "generated": { 17 + "as": "'at://' || \"profiles\".\"did\" || '/blue.recipes.actor.profile/self'", 18 + "type": "stored" 19 + } 20 + }, 21 + "cid": { 22 + "name": "cid", 23 + "type": "text", 24 + "primaryKey": false, 25 + "notNull": true 26 + }, 27 + "did": { 28 + "name": "did", 29 + "type": "text", 30 + "primaryKey": true, 31 + "notNull": true 32 + }, 33 + "ingested_at": { 34 + "name": "ingested_at", 35 + "type": "text", 36 + "primaryKey": false, 37 + "notNull": true, 38 + "default": "CURRENT_TIMESTAMP" 39 + }, 40 + "display_name": { 41 + "name": "display_name", 42 + "type": "varchar(640)", 43 + "primaryKey": false, 44 + "notNull": true 45 + }, 46 + "description": { 47 + "name": "description", 48 + "type": "varchar(2500)", 49 + "primaryKey": false, 50 + "notNull": false 51 + }, 52 + "pronouns": { 53 + "name": "pronouns", 54 + "type": "varchar(200)", 55 + "primaryKey": false, 56 + "notNull": false 57 + }, 58 + "website": { 59 + "name": "website", 60 + "type": "text", 61 + "primaryKey": false, 62 + "notNull": false 63 + }, 64 + "avatar": { 65 + "name": "avatar", 66 + "type": "text", 67 + "primaryKey": false, 68 + "notNull": false 69 + }, 70 + "banner": { 71 + "name": "banner", 72 + "type": "text", 73 + "primaryKey": false, 74 + "notNull": false 75 + }, 76 + "created_at": { 77 + "name": "created_at", 78 + "type": "text", 79 + "primaryKey": false, 80 + "notNull": true 81 + } 82 + }, 83 + "indexes": { 84 + "profiles_cid_idx": { 85 + "name": "profiles_cid_idx", 86 + "columns": [ 87 + { 88 + "expression": "cid", 89 + "isExpression": false, 90 + "asc": true, 91 + "nulls": "last" 92 + } 93 + ], 94 + "isUnique": false, 95 + "concurrently": false, 96 + "method": "btree", 97 + "with": {} 98 + }, 99 + "profiles_cat_idx": { 100 + "name": "profiles_cat_idx", 101 + "columns": [ 102 + { 103 + "expression": "created_at", 104 + "isExpression": false, 105 + "asc": true, 106 + "nulls": "last" 107 + } 108 + ], 109 + "isUnique": false, 110 + "concurrently": false, 111 + "method": "btree", 112 + "with": {} 113 + }, 114 + "profiles_iat_idx": { 115 + "name": "profiles_iat_idx", 116 + "columns": [ 117 + { 118 + "expression": "ingested_at", 119 + "isExpression": false, 120 + "asc": true, 121 + "nulls": "last" 122 + } 123 + ], 124 + "isUnique": false, 125 + "concurrently": false, 126 + "method": "btree", 127 + "with": {} 128 + } 129 + }, 130 + "foreignKeys": {}, 131 + "compositePrimaryKeys": {}, 132 + "uniqueConstraints": {}, 133 + "policies": {}, 134 + "checkConstraints": {}, 135 + "isRLSEnabled": false 136 + }, 137 + "public.recipes": { 8 138 "name": "recipes", 139 + "schema": "", 9 140 "columns": { 10 141 "uri": { 11 142 "name": "uri", 12 143 "type": "text", 13 144 "primaryKey": false, 14 145 "notNull": false, 15 - "autoincrement": false, 16 146 "generated": { 17 - "as": "(\"author_did\" || '/' || \"rkey\")", 18 - "type": "virtual" 147 + "as": "'at://' || \"recipes\".\"author_did\" || '/blue.recipes.feed.recipe/' || \"recipes\".\"rkey\"", 148 + "type": "stored" 19 149 } 20 150 }, 151 + "cid": { 152 + "name": "cid", 153 + "type": "text", 154 + "primaryKey": false, 155 + "notNull": true 156 + }, 21 157 "author_did": { 22 158 "name": "author_did", 23 159 "type": "text", 24 160 "primaryKey": false, 25 - "notNull": true, 26 - "autoincrement": false 161 + "notNull": true 27 162 }, 28 163 "rkey": { 29 164 "name": "rkey", 30 165 "type": "text", 31 166 "primaryKey": false, 32 - "notNull": true, 33 - "autoincrement": false 167 + "notNull": true 34 168 }, 35 - "image_ref": { 36 - "name": "image_ref", 169 + "image": { 170 + "name": "image", 37 171 "type": "text", 38 172 "primaryKey": false, 39 - "notNull": false, 40 - "autoincrement": false 173 + "notNull": false 41 174 }, 42 175 "title": { 43 176 "name": "title", 44 177 "type": "text", 45 178 "primaryKey": false, 46 - "notNull": true, 47 - "autoincrement": false 179 + "notNull": true 48 180 }, 49 181 "time": { 50 182 "name": "time", 51 183 "type": "integer", 52 184 "primaryKey": false, 53 185 "notNull": true, 54 - "autoincrement": false, 55 186 "default": 0 56 187 }, 57 188 "serves": { 58 189 "name": "serves", 59 190 "type": "integer", 60 191 "primaryKey": false, 61 - "notNull": false, 62 - "autoincrement": false 192 + "notNull": false 63 193 }, 64 194 "description": { 65 195 "name": "description", 66 196 "type": "text", 67 197 "primaryKey": false, 68 - "notNull": false, 69 - "autoincrement": false 198 + "notNull": false 70 199 }, 71 200 "ingredients": { 72 201 "name": "ingredients", 73 - "type": "text", 202 + "type": "jsonb", 74 203 "primaryKey": false, 75 - "notNull": true, 76 - "autoincrement": false 204 + "notNull": true 77 205 }, 78 206 "ingredients_count": { 79 207 "name": "ingredients_count", 80 208 "type": "integer", 81 209 "primaryKey": false, 82 210 "notNull": false, 83 - "autoincrement": false, 84 211 "generated": { 85 - "as": "(json_array_length(\"ingredients\"))", 86 - "type": "virtual" 212 + "as": "jsonb_array_length(\"recipes\".\"ingredients\")", 213 + "type": "stored" 87 214 } 88 215 }, 89 216 "steps": { 90 217 "name": "steps", 91 - "type": "text", 218 + "type": "jsonb", 92 219 "primaryKey": false, 93 - "notNull": true, 94 - "autoincrement": false 220 + "notNull": true 95 221 }, 96 222 "steps_count": { 97 223 "name": "steps_count", 98 224 "type": "integer", 99 225 "primaryKey": false, 100 226 "notNull": false, 101 - "autoincrement": false, 102 227 "generated": { 103 - "as": "(json_array_length(\"steps\"))", 104 - "type": "virtual" 228 + "as": "jsonb_array_length(\"recipes\".\"steps\")", 229 + "type": "stored" 105 230 } 106 231 }, 107 232 "created_at": { 108 233 "name": "created_at", 234 + "type": "text", 235 + "primaryKey": false, 236 + "notNull": true 237 + }, 238 + "ingested_at": { 239 + "name": "ingested_at", 109 240 "type": "text", 110 241 "primaryKey": false, 111 242 "notNull": true, 112 - "autoincrement": false 243 + "default": "CURRENT_TIMESTAMP" 113 244 } 114 245 }, 115 - "indexes": {}, 116 - "foreignKeys": {}, 246 + "indexes": { 247 + "recipes_title_idx": { 248 + "name": "recipes_title_idx", 249 + "columns": [ 250 + { 251 + "expression": "title", 252 + "isExpression": false, 253 + "asc": true, 254 + "nulls": "last" 255 + } 256 + ], 257 + "isUnique": false, 258 + "concurrently": false, 259 + "method": "btree", 260 + "with": {} 261 + }, 262 + "recipes_cid_idx": { 263 + "name": "recipes_cid_idx", 264 + "columns": [ 265 + { 266 + "expression": "cid", 267 + "isExpression": false, 268 + "asc": true, 269 + "nulls": "last" 270 + } 271 + ], 272 + "isUnique": false, 273 + "concurrently": false, 274 + "method": "btree", 275 + "with": {} 276 + }, 277 + "recipes_cat_idx": { 278 + "name": "recipes_cat_idx", 279 + "columns": [ 280 + { 281 + "expression": "created_at", 282 + "isExpression": false, 283 + "asc": true, 284 + "nulls": "last" 285 + } 286 + ], 287 + "isUnique": false, 288 + "concurrently": false, 289 + "method": "btree", 290 + "with": {} 291 + }, 292 + "recipes_iat_idx": { 293 + "name": "recipes_iat_idx", 294 + "columns": [ 295 + { 296 + "expression": "ingested_at", 297 + "isExpression": false, 298 + "asc": true, 299 + "nulls": "last" 300 + } 301 + ], 302 + "isUnique": false, 303 + "concurrently": false, 304 + "method": "btree", 305 + "with": {} 306 + } 307 + }, 308 + "foreignKeys": { 309 + "recipes_author_did_profiles_did_fk": { 310 + "name": "recipes_author_did_profiles_did_fk", 311 + "tableFrom": "recipes", 312 + "tableTo": "profiles", 313 + "columnsFrom": [ 314 + "author_did" 315 + ], 316 + "columnsTo": [ 317 + "did" 318 + ], 319 + "onDelete": "cascade", 320 + "onUpdate": "no action" 321 + } 322 + }, 117 323 "compositePrimaryKeys": { 118 324 "recipes_author_did_rkey_pk": { 325 + "name": "recipes_author_did_rkey_pk", 119 326 "columns": [ 120 327 "author_did", 121 328 "rkey" 122 - ], 123 - "name": "recipes_author_did_rkey_pk" 329 + ] 124 330 } 125 331 }, 126 332 "uniqueConstraints": {}, 127 - "checkConstraints": {} 333 + "policies": {}, 334 + "checkConstraints": {}, 335 + "isRLSEnabled": false 128 336 } 129 337 }, 130 - "views": {}, 131 338 "enums": {}, 339 + "schemas": {}, 340 + "sequences": {}, 341 + "roles": {}, 342 + "policies": {}, 343 + "views": {}, 132 344 "_meta": { 345 + "columns": {}, 133 346 "schemas": {}, 134 - "tables": {}, 135 - "columns": {} 136 - }, 137 - "internal": { 138 - "indexes": {} 347 + "tables": {} 139 348 } 140 349 }
-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 - }
-284
libs/database/migrations/meta/0002_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "25f6fc02-0357-4a4a-a43c-6fc138a21401", 5 - "prevId": "d6f06b7d-9822-43ee-b96c-3b980a5e4953", 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\" || '/blue.recipes.actor.profile/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\" || '/blue.recipes.feed.recipe/' || \"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 - }, 281 - "internal": { 282 - "indexes": {} 283 - } 284 - }
-312
libs/database/migrations/meta/0003_snapshot.json
··· 1 - { 2 - "version": "6", 3 - "dialect": "sqlite", 4 - "id": "ca3337d9-69a0-468d-8364-0f05e91a0233", 5 - "prevId": "25f6fc02-0357-4a4a-a43c-6fc138a21401", 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\" || '/blue.recipes.actor.profile/self')", 18 - "type": "virtual" 19 - } 20 - }, 21 - "cid": { 22 - "name": "cid", 23 - "type": "text", 24 - "primaryKey": false, 25 - "notNull": true, 26 - "autoincrement": false 27 - }, 28 - "did": { 29 - "name": "did", 30 - "type": "text", 31 - "primaryKey": true, 32 - "notNull": true, 33 - "autoincrement": false 34 - }, 35 - "ingested_at": { 36 - "name": "ingested_at", 37 - "type": "text", 38 - "primaryKey": false, 39 - "notNull": true, 40 - "autoincrement": false, 41 - "default": "CURRENT_TIMESTAMP" 42 - }, 43 - "display_name": { 44 - "name": "display_name", 45 - "type": "text(640)", 46 - "primaryKey": false, 47 - "notNull": true, 48 - "autoincrement": false 49 - }, 50 - "description": { 51 - "name": "description", 52 - "type": "text(2500)", 53 - "primaryKey": false, 54 - "notNull": false, 55 - "autoincrement": false 56 - }, 57 - "pronouns": { 58 - "name": "pronouns", 59 - "type": "text(200)", 60 - "primaryKey": false, 61 - "notNull": false, 62 - "autoincrement": false 63 - }, 64 - "website": { 65 - "name": "website", 66 - "type": "text", 67 - "primaryKey": false, 68 - "notNull": false, 69 - "autoincrement": false 70 - }, 71 - "avatar": { 72 - "name": "avatar", 73 - "type": "text", 74 - "primaryKey": false, 75 - "notNull": false, 76 - "autoincrement": false 77 - }, 78 - "banner": { 79 - "name": "banner", 80 - "type": "text", 81 - "primaryKey": false, 82 - "notNull": false, 83 - "autoincrement": false 84 - }, 85 - "created_at": { 86 - "name": "created_at", 87 - "type": "text", 88 - "primaryKey": false, 89 - "notNull": true, 90 - "autoincrement": false 91 - } 92 - }, 93 - "indexes": { 94 - "profiles_cid_idx": { 95 - "name": "profiles_cid_idx", 96 - "columns": [ 97 - "cid" 98 - ], 99 - "isUnique": false 100 - }, 101 - "profiles_cat_idx": { 102 - "name": "profiles_cat_idx", 103 - "columns": [ 104 - "created_at" 105 - ], 106 - "isUnique": false 107 - }, 108 - "profiles_iat_idx": { 109 - "name": "profiles_iat_idx", 110 - "columns": [ 111 - "ingested_at" 112 - ], 113 - "isUnique": false 114 - } 115 - }, 116 - "foreignKeys": {}, 117 - "compositePrimaryKeys": {}, 118 - "uniqueConstraints": {}, 119 - "checkConstraints": {} 120 - }, 121 - "recipes": { 122 - "name": "recipes", 123 - "columns": { 124 - "uri": { 125 - "name": "uri", 126 - "type": "text", 127 - "primaryKey": false, 128 - "notNull": false, 129 - "autoincrement": false, 130 - "generated": { 131 - "as": "('at://' || \"author_did\" || '/blue.recipes.feed.recipe/' || \"rkey\")", 132 - "type": "virtual" 133 - } 134 - }, 135 - "cid": { 136 - "name": "cid", 137 - "type": "text", 138 - "primaryKey": false, 139 - "notNull": true, 140 - "autoincrement": false 141 - }, 142 - "author_did": { 143 - "name": "author_did", 144 - "type": "text", 145 - "primaryKey": false, 146 - "notNull": true, 147 - "autoincrement": false 148 - }, 149 - "rkey": { 150 - "name": "rkey", 151 - "type": "text", 152 - "primaryKey": false, 153 - "notNull": true, 154 - "autoincrement": false 155 - }, 156 - "image": { 157 - "name": "image", 158 - "type": "text", 159 - "primaryKey": false, 160 - "notNull": false, 161 - "autoincrement": false 162 - }, 163 - "title": { 164 - "name": "title", 165 - "type": "text", 166 - "primaryKey": false, 167 - "notNull": true, 168 - "autoincrement": false 169 - }, 170 - "time": { 171 - "name": "time", 172 - "type": "integer", 173 - "primaryKey": false, 174 - "notNull": true, 175 - "autoincrement": false, 176 - "default": 0 177 - }, 178 - "serves": { 179 - "name": "serves", 180 - "type": "integer", 181 - "primaryKey": false, 182 - "notNull": false, 183 - "autoincrement": false 184 - }, 185 - "description": { 186 - "name": "description", 187 - "type": "text", 188 - "primaryKey": false, 189 - "notNull": false, 190 - "autoincrement": false 191 - }, 192 - "ingredients": { 193 - "name": "ingredients", 194 - "type": "text", 195 - "primaryKey": false, 196 - "notNull": true, 197 - "autoincrement": false 198 - }, 199 - "ingredients_count": { 200 - "name": "ingredients_count", 201 - "type": "integer", 202 - "primaryKey": false, 203 - "notNull": false, 204 - "autoincrement": false, 205 - "generated": { 206 - "as": "(json_array_length(\"ingredients\"))", 207 - "type": "virtual" 208 - } 209 - }, 210 - "steps": { 211 - "name": "steps", 212 - "type": "text", 213 - "primaryKey": false, 214 - "notNull": true, 215 - "autoincrement": false 216 - }, 217 - "steps_count": { 218 - "name": "steps_count", 219 - "type": "integer", 220 - "primaryKey": false, 221 - "notNull": false, 222 - "autoincrement": false, 223 - "generated": { 224 - "as": "(json_array_length(\"steps\"))", 225 - "type": "virtual" 226 - } 227 - }, 228 - "created_at": { 229 - "name": "created_at", 230 - "type": "text", 231 - "primaryKey": false, 232 - "notNull": true, 233 - "autoincrement": false 234 - }, 235 - "ingested_at": { 236 - "name": "ingested_at", 237 - "type": "text", 238 - "primaryKey": false, 239 - "notNull": true, 240 - "autoincrement": false, 241 - "default": "CURRENT_TIMESTAMP" 242 - } 243 - }, 244 - "indexes": { 245 - "recipes_title_idx": { 246 - "name": "recipes_title_idx", 247 - "columns": [ 248 - "title" 249 - ], 250 - "isUnique": false 251 - }, 252 - "recipes_cid_idx": { 253 - "name": "recipes_cid_idx", 254 - "columns": [ 255 - "cid" 256 - ], 257 - "isUnique": false 258 - }, 259 - "recipes_cat_idx": { 260 - "name": "recipes_cat_idx", 261 - "columns": [ 262 - "created_at" 263 - ], 264 - "isUnique": false 265 - }, 266 - "recipes_iat_idx": { 267 - "name": "recipes_iat_idx", 268 - "columns": [ 269 - "ingested_at" 270 - ], 271 - "isUnique": false 272 - } 273 - }, 274 - "foreignKeys": { 275 - "recipes_author_did_profiles_did_fk": { 276 - "name": "recipes_author_did_profiles_did_fk", 277 - "tableFrom": "recipes", 278 - "tableTo": "profiles", 279 - "columnsFrom": [ 280 - "author_did" 281 - ], 282 - "columnsTo": [ 283 - "did" 284 - ], 285 - "onDelete": "cascade", 286 - "onUpdate": "no action" 287 - } 288 - }, 289 - "compositePrimaryKeys": { 290 - "recipes_author_did_rkey_pk": { 291 - "columns": [ 292 - "author_did", 293 - "rkey" 294 - ], 295 - "name": "recipes_author_did_rkey_pk" 296 - } 297 - }, 298 - "uniqueConstraints": {}, 299 - "checkConstraints": {} 300 - } 301 - }, 302 - "views": {}, 303 - "enums": {}, 304 - "_meta": { 305 - "schemas": {}, 306 - "tables": {}, 307 - "columns": {} 308 - }, 309 - "internal": { 310 - "indexes": {} 311 - } 312 - }
+4 -25
libs/database/migrations/meta/_journal.json
··· 1 1 { 2 2 "version": "7", 3 - "dialect": "sqlite", 3 + "dialect": "postgresql", 4 4 "entries": [ 5 5 { 6 6 "idx": 0, 7 - "version": "6", 8 - "when": 1764024817179, 9 - "tag": "0000_kind_ultron", 10 - "breakpoints": true 11 - }, 12 - { 13 - "idx": 1, 14 - "version": "6", 15 - "when": 1764102063385, 16 - "tag": "0001_past_umar", 17 - "breakpoints": true 18 - }, 19 - { 20 - "idx": 2, 21 - "version": "6", 22 - "when": 1764113357363, 23 - "tag": "0002_cheerful_venom", 24 - "breakpoints": true 25 - }, 26 - { 27 - "idx": 3, 28 - "version": "6", 29 - "when": 1764113735823, 30 - "tag": "0003_long_blue_marvel", 7 + "version": "7", 8 + "when": 1764420650497, 9 + "tag": "0000_young_hellcat", 31 10 "breakpoints": true 32 11 } 33 12 ]
+2
libs/database/package.json
··· 27 27 "@cookware/tsconfig": "workspace:*", 28 28 "@types/bun": "catalog:", 29 29 "@types/node": "^22.10.1", 30 + "@types/pg": "^8.15.6", 30 31 "drizzle-kit": "^0.29.0", 31 32 "typescript": "^5.2.2" 32 33 }, 33 34 "dependencies": { 34 35 "@libsql/client": "^0.15.15", 35 36 "drizzle-orm": "catalog:", 37 + "pg": "^8.16.3", 36 38 "zod": "^3.23.8" 37 39 } 38 40 }
+2
libs/lexicons/lexicons/feed/defs.tsp
··· 6 6 model RecipeView { 7 7 @required uri: atUri; 8 8 @required cid: cid; 9 + @required rkey: string; 10 + imageUrl?: url; 9 11 @required author: blue.recipes.actor.defs.ProfileViewBasic; 10 12 @required record: blue.recipes.feed.recipe.Main; 11 13 @required indexedAt: datetime;
+1 -1
libs/lexicons/lexicons/profiles/defs.tsp
··· 10 10 displayName?: string; 11 11 12 12 pronouns?: string; 13 - avatar?: uri; 13 + avatar?: url; 14 14 15 15 @format("datetime") 16 16 createdAt?: string;
+1 -1
libs/lexicons/lib/types/blue/recipes/actor/defs.ts
··· 5 5 $type: /*#__PURE__*/ v.optional( 6 6 /*#__PURE__*/ v.literal("blue.recipes.actor.defs#profileViewBasic"), 7 7 ), 8 - avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.genericUriString()), 8 + avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 9 9 createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 10 10 did: /*#__PURE__*/ v.didString(), 11 11 /**
+2
libs/lexicons/lib/types/blue/recipes/feed/defs.ts
··· 18 18 return BlueRecipesActorDefs.profileViewBasicSchema; 19 19 }, 20 20 cid: /*#__PURE__*/ v.cidString(), 21 + imageUrl: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 21 22 indexedAt: /*#__PURE__*/ v.datetimeString(), 22 23 get record() { 23 24 return BlueRecipesFeedRecipe.mainSchema; 24 25 }, 26 + rkey: /*#__PURE__*/ v.string(), 25 27 uri: /*#__PURE__*/ v.resourceUriString(), 26 28 }); 27 29