+1
-2
.env.example
+1
-2
.env.example
+3
-1
apps/api/package.json
+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
}
+19
-5
apps/api/src/index.ts
+19
-5
apps/api/src/index.ts
···
5
5
import { logMiddleware } from './logger.js';
6
6
import pino from 'pino';
7
7
import { RedisClient } from 'bun';
8
+
import { registerGetProfile } from './xrpc/blue.recipes.actor.getProfile.js';
9
+
import { Hono } from 'hono';
10
+
import { mountXrpcRouter } from './util/hono.js';
8
11
9
12
const logger = pino();
10
13
const redis = new RedisClient(Bun.env.REDIS_URL ?? "redis://127.0.0.1:6379/0");
11
14
12
-
const router = new XRPCRouter({
15
+
const xrpcRouter = new XRPCRouter({
13
16
handleException: (err, _req) => {
14
17
if (err instanceof XRPCError) {
15
18
return err.toResponse();
···
32
35
],
33
36
});
34
37
35
-
registerGetRecipes(router, logger, redis);
36
-
registerGetRecipe(router, logger, redis);
38
+
// actor
39
+
registerGetProfile(xrpcRouter, logger, redis);
40
+
41
+
// feed
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);
37
51
38
52
const server = Bun.serve({
39
53
port: process.env.PORT || 3000,
40
-
...router
54
+
fetch: app.fetch,
41
55
});
42
56
43
-
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
+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
+
});
+21
apps/api/src/util/cdn.ts
+21
apps/api/src/util/cdn.ts
···
1
+
import { type Blob, type LegacyBlob } from "@atcute/lexicons";
2
+
import { isBlob, isLegacyBlob } from "@atcute/lexicons/interfaces";
3
+
4
+
const CDN_ROOT = "https://cdn.bsky.app/img/";
5
+
6
+
export const buildCdnUrl = (
7
+
type: 'feed_thumbnail' | 'post_image' | 'avatar',
8
+
did: string,
9
+
blob: Blob | LegacyBlob,
10
+
): string => {
11
+
let ref: string;
12
+
if (isLegacyBlob(blob)) {
13
+
ref = blob.cid;
14
+
} else if (isBlob(blob)) {
15
+
ref = blob.ref.$link;
16
+
} else {
17
+
throw new Error("Invalid blob type");
18
+
}
19
+
20
+
return `${CDN_ROOT}${type}/plain/${did}/${ref}`;
21
+
}
+40
apps/api/src/util/hono.ts
+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
+
};
+44
apps/api/src/xrpc/blue.recipes.actor.getProfile.ts
+44
apps/api/src/xrpc/blue.recipes.actor.getProfile.ts
···
1
+
import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server';
2
+
import { BlueRecipesActorGetProfile } from '@cookware/lexicons';
3
+
import { db, eq } from '@cookware/database';
4
+
import { getHandle, parseDid } from '../util/api.js';
5
+
import { Logger } from 'pino';
6
+
import { profilesTable, recipeTable } from '@cookware/database/schema';
7
+
import { RedisClient } from 'bun';
8
+
import { buildCdnUrl } from '../util/cdn.js';
9
+
10
+
export const registerGetProfile = (router: XRPCRouter, _logger: Logger, redis: RedisClient) => {
11
+
router.addQuery(BlueRecipesActorGetProfile.mainSchema, {
12
+
async handler({ params: { actor } }) {
13
+
const where = eq(profilesTable.did, await parseDid(actor));
14
+
const profile = await db.query.profilesTable.findFirst({
15
+
where,
16
+
orderBy: profilesTable.createdAt,
17
+
extras: {
18
+
recipesCount: db.$count(recipeTable, where).as('recipesCount'),
19
+
}
20
+
});
21
+
22
+
if (!profile) {
23
+
throw new XRPCError({
24
+
status: 404,
25
+
error: 'ProfileNotFound',
26
+
description: `Profile for actor ${actor} not found.`,
27
+
});
28
+
}
29
+
30
+
return json({
31
+
did: profile.did,
32
+
handle: await getHandle(profile.did, redis),
33
+
displayName: profile.displayName ?? undefined,
34
+
description: profile.description ?? undefined,
35
+
pronouns: profile.pronouns ?? undefined,
36
+
website: profile.website ?? undefined,
37
+
avatar: profile.avatarRef ? buildCdnUrl('avatar', profile.did, profile.avatarRef) : undefined,
38
+
banner: profile.bannerRef ? buildCdnUrl('feed_thumbnail', profile.did, profile.bannerRef) : undefined,
39
+
recipesCount: profile.recipesCount,
40
+
createdAt: profile.createdAt.toISOString(),
41
+
});
42
+
},
43
+
});
44
+
};
+36
-25
apps/api/src/xrpc/blue.recipes.feed.getRecipe.ts
+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
+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,
+8
-5
apps/web/package.json
+8
-5
apps/web/package.json
···
12
12
"dependencies": {
13
13
"@atcute/atproto": "^3.1.9",
14
14
"@atcute/client": "catalog:",
15
+
"@atcute/identity-resolver": "^1.1.4",
15
16
"@atcute/lexicons": "catalog:",
16
-
"@atcute/oauth-browser-client": "^1.0.7",
17
+
"@atcute/oauth-browser-client": "^2.0.1",
17
18
"@atproto/common": "^0.4.5",
18
19
"@atproto/common-web": "^0.3.1",
19
20
"@dnd-kit/core": "^6.3.1",
···
26
27
"@radix-ui/react-dialog": "^1.1.4",
27
28
"@radix-ui/react-dropdown-menu": "^2.1.4",
28
29
"@radix-ui/react-icons": "^1.3.2",
29
-
"@radix-ui/react-label": "^2.1.0",
30
-
"@radix-ui/react-separator": "^1.1.0",
31
-
"@radix-ui/react-slot": "^1.1.0",
30
+
"@radix-ui/react-label": "^2.1.8",
31
+
"@radix-ui/react-separator": "^1.1.8",
32
+
"@radix-ui/react-slot": "^1.2.4",
32
33
"@radix-ui/react-tooltip": "^1.1.4",
33
34
"@tanstack/react-query": "^5.62.2",
34
35
"@tanstack/react-query-devtools": "^5.62.2",
···
53
54
"@types/node": "^22.10.1",
54
55
"@types/react": "^19.0.0",
55
56
"@types/react-dom": "^19.0.0",
57
+
"@vitejs/plugin-react": "^5.1.1",
56
58
"@vitejs/plugin-react-swc": "^3.5.0",
57
59
"autoprefixer": "^10.4.20",
60
+
"babel-plugin-react-compiler": "^1.0.0",
58
61
"cssnano": "^7.0.6",
59
62
"eslint": "^9.15.0",
60
63
"eslint-plugin-react-hooks": "^5.0.0",
···
65
68
"tailwindcss": "^3.4.16",
66
69
"typescript": "~5.6.2",
67
70
"typescript-eslint": "^8.15.0",
68
-
"vite": "^6.0.1"
71
+
"vite": "^7.2.4"
69
72
}
70
73
}
-12
apps/web/public/client-metadata.json
-12
apps/web/public/client-metadata.json
···
1
-
{
2
-
"client_id": "https://recipes.blue/client-metadata.json",
3
-
"client_name": "Recipes",
4
-
"client_uri": "https://recipes.blue",
5
-
"redirect_uris": ["https://recipes.blue/"],
6
-
"scope": "atproto transition:generic",
7
-
"grant_types": ["authorization_code", "refresh_token"],
8
-
"response_types": ["code"],
9
-
"token_endpoint_auth_method": "none",
10
-
"application_type": "web",
11
-
"dpop_bound_access_tokens": true
12
-
}
+9
apps/web/public/oauth-client-metadata.json
+9
apps/web/public/oauth-client-metadata.json
···
1
+
{
2
+
"client_id": "https://recipes.blue/oauth-client-metadata.json",
3
+
"client_name": "Recipes.blue",
4
+
"redirect_uris": ["https://recipes.blue/"],
5
+
"scope": "atproto transition:generic",
6
+
"token_endpoint_auth_method": "none",
7
+
"application_type": "web",
8
+
"dpop_bound_access_tokens": true
9
+
}
+10
-6
apps/web/src/components/query-placeholder.tsx
+10
-6
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
-
import { XRPCError } from '@atcute/client';
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
{
···
32
33
} else if (query.isError) {
33
34
const { error } = query;
34
35
let errMsg = 'Unknown';
35
-
if (error instanceof XRPCError) {
36
-
errMsg = error.kind ?? `HTTP_${error.status}`;
36
+
if (isXRPCErrorPayload(error)) {
37
+
errMsg = error.message ?? `XRPC_${error.error}`;
37
38
} if (error instanceof Error) {
38
39
errMsg = `${error.message} (${error.name})`;
39
40
}
···
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
+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>
+242
apps/web/src/components/ui/field.tsx
+242
apps/web/src/components/ui/field.tsx
···
1
+
import { useMemo } from "react"
2
+
import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+
import { cn } from "@/lib/utils"
5
+
import { Label } from "@/components/ui/label"
6
+
import { Separator } from "@/components/ui/separator"
7
+
8
+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9
+
return (
10
+
<fieldset
11
+
data-slot="field-set"
12
+
className={cn(
13
+
"flex flex-col gap-6",
14
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15
+
className
16
+
)}
17
+
{...props}
18
+
/>
19
+
)
20
+
}
21
+
22
+
function FieldLegend({
23
+
className,
24
+
variant = "legend",
25
+
...props
26
+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27
+
return (
28
+
<legend
29
+
data-slot="field-legend"
30
+
data-variant={variant}
31
+
className={cn(
32
+
"mb-3 font-medium",
33
+
"data-[variant=legend]:text-base",
34
+
"data-[variant=label]:text-sm",
35
+
className
36
+
)}
37
+
{...props}
38
+
/>
39
+
)
40
+
}
41
+
42
+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43
+
return (
44
+
<div
45
+
data-slot="field-group"
46
+
className={cn(
47
+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48
+
className
49
+
)}
50
+
{...props}
51
+
/>
52
+
)
53
+
}
54
+
55
+
const fieldVariants = cva(
56
+
"group/field data-[invalid=true]:text-destructive flex w-full gap-3",
57
+
{
58
+
variants: {
59
+
orientation: {
60
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
61
+
horizontal: [
62
+
"flex-row items-center",
63
+
"[&>[data-slot=field-label]]:flex-auto",
64
+
"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start",
65
+
],
66
+
responsive: [
67
+
"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto",
68
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70
+
],
71
+
},
72
+
},
73
+
defaultVariants: {
74
+
orientation: "vertical",
75
+
},
76
+
}
77
+
)
78
+
79
+
function Field({
80
+
className,
81
+
orientation = "vertical",
82
+
...props
83
+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84
+
return (
85
+
<div
86
+
role="group"
87
+
data-slot="field"
88
+
data-orientation={orientation}
89
+
className={cn(fieldVariants({ orientation }), className)}
90
+
{...props}
91
+
/>
92
+
)
93
+
}
94
+
95
+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96
+
return (
97
+
<div
98
+
data-slot="field-content"
99
+
className={cn(
100
+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
101
+
className
102
+
)}
103
+
{...props}
104
+
/>
105
+
)
106
+
}
107
+
108
+
function FieldLabel({
109
+
className,
110
+
...props
111
+
}: React.ComponentProps<typeof Label>) {
112
+
return (
113
+
<Label
114
+
data-slot="field-label"
115
+
className={cn(
116
+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
117
+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
118
+
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
119
+
className
120
+
)}
121
+
{...props}
122
+
/>
123
+
)
124
+
}
125
+
126
+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
127
+
return (
128
+
<div
129
+
data-slot="field-label"
130
+
className={cn(
131
+
"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50",
132
+
className
133
+
)}
134
+
{...props}
135
+
/>
136
+
)
137
+
}
138
+
139
+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
140
+
return (
141
+
<p
142
+
data-slot="field-description"
143
+
className={cn(
144
+
"text-muted-foreground text-sm font-normal leading-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
145
+
"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5",
146
+
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
147
+
className
148
+
)}
149
+
{...props}
150
+
/>
151
+
)
152
+
}
153
+
154
+
function FieldSeparator({
155
+
children,
156
+
className,
157
+
...props
158
+
}: React.ComponentProps<"div"> & {
159
+
children?: React.ReactNode
160
+
}) {
161
+
return (
162
+
<div
163
+
data-slot="field-separator"
164
+
data-content={!!children}
165
+
className={cn(
166
+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
167
+
className
168
+
)}
169
+
{...props}
170
+
>
171
+
<Separator className="absolute inset-0 top-1/2" />
172
+
{children && (
173
+
<span
174
+
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
175
+
data-slot="field-separator-content"
176
+
>
177
+
{children}
178
+
</span>
179
+
)}
180
+
</div>
181
+
)
182
+
}
183
+
184
+
function FieldError({
185
+
className,
186
+
children,
187
+
errors,
188
+
...props
189
+
}: React.ComponentProps<"div"> & {
190
+
errors?: Array<{ message?: string } | undefined>
191
+
}) {
192
+
const content = useMemo(() => {
193
+
if (children) {
194
+
return children
195
+
}
196
+
197
+
if (!errors) {
198
+
return null
199
+
}
200
+
201
+
if (errors?.length === 1 && errors[0]?.message) {
202
+
return errors[0].message
203
+
}
204
+
205
+
return (
206
+
<ul className="ml-4 flex list-disc flex-col gap-1">
207
+
{errors.map(
208
+
(error, index) =>
209
+
error?.message && <li key={index}>{error.message}</li>
210
+
)}
211
+
</ul>
212
+
)
213
+
}, [children, errors])
214
+
215
+
if (!content) {
216
+
return null
217
+
}
218
+
219
+
return (
220
+
<div
221
+
role="alert"
222
+
data-slot="field-error"
223
+
className={cn("text-destructive text-sm font-normal", className)}
224
+
{...props}
225
+
>
226
+
{content}
227
+
</div>
228
+
)
229
+
}
230
+
231
+
export {
232
+
Field,
233
+
FieldLabel,
234
+
FieldDescription,
235
+
FieldError,
236
+
FieldGroup,
237
+
FieldLegend,
238
+
FieldSeparator,
239
+
FieldSet,
240
+
FieldContent,
241
+
FieldTitle,
242
+
}
+2
apps/web/src/components/ui/separator.tsx
+2
apps/web/src/components/ui/separator.tsx
-9
apps/web/src/forms/recipe.ts
-9
apps/web/src/forms/recipe.ts
+4
-17
apps/web/src/hooks/use-xrpc.tsx
+4
-17
apps/web/src/hooks/use-xrpc.tsx
···
1
-
import { SERVER_URL } from "@/lib/utils";
2
-
import { useAuth } from "@/state/auth";
3
-
import { Client, simpleFetchHandler } from "@atcute/client";
1
+
import { useClient } from "@/state/auth";
4
2
3
+
/** @deprecated Use `useClient` from `state/auth/client` instead. */
5
4
export function useXrpc() {
6
-
const { agent } = useAuth();
7
-
8
-
if (agent) {
9
-
return new Client({
10
-
handler: agent,
11
-
proxy: {
12
-
did: `did:web:${SERVER_URL}`,
13
-
serviceId: '#recipes_blue',
14
-
},
15
-
});
16
-
}
17
-
18
-
const handler = simpleFetchHandler({ service: `https://${SERVER_URL}` });
19
-
return new Client({ handler });
5
+
const client = useClient();
6
+
return client;
20
7
}
+29
-14
apps/web/src/main.tsx
+29
-14
apps/web/src/main.tsx
···
4
4
import { createRouter, RouterProvider } from '@tanstack/react-router';
5
5
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
6
6
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
7
-
import { configureOAuth } from '@atcute/oauth-browser-client';
7
+
import { configureOAuth, defaultIdentityResolver } from '@atcute/oauth-browser-client';
8
8
import './index.css'
9
-
import { AuthProvider, useAuth } from './state/auth';
10
9
import { ThemeProvider } from './components/theme-provider';
10
+
import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver, XrpcHandleResolver } from '@atcute/identity-resolver';
11
+
import { SessionProvider, useSession } from './state/auth/session';
12
+
import { ClientProvider, useClient } from './state/auth';
11
13
12
14
const router = createRouter({
13
15
routeTree,
14
16
context: {
15
-
auth: undefined!,
17
+
session: undefined!,
18
+
client: undefined!,
16
19
},
17
20
});
18
21
···
25
28
configureOAuth({
26
29
metadata: {
27
30
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
28
-
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
31
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI,
29
32
},
33
+
identityResolver: defaultIdentityResolver({
34
+
handleResolver: new XrpcHandleResolver({ serviceUrl: 'https://slingshot.microcosm.blue' }),
35
+
didDocumentResolver: new CompositeDidDocumentResolver({
36
+
methods: {
37
+
plc: new PlcDidDocumentResolver(),
38
+
web: new WebDidDocumentResolver(),
39
+
},
40
+
}),
41
+
}),
30
42
});
31
43
32
44
const queryClient = new QueryClient({
···
40
52
});
41
53
42
54
const InnerApp = () => {
43
-
const auth = useAuth();
44
-
return <RouterProvider router={router} context={{ auth }} />
55
+
const session = useSession();
56
+
const client = useClient();
57
+
return <RouterProvider router={router} context={{ session, client }} />
45
58
};
46
59
47
60
createRoot(document.getElementById('root')!).render(
48
61
<StrictMode>
49
-
<ThemeProvider defaultTheme="dark" storageKey="recipes-theme">
50
-
<AuthProvider>
51
-
<QueryClientProvider client={queryClient}>
52
-
<InnerApp />
53
-
<ReactQueryDevtools initialIsOpen={false} />
54
-
</QueryClientProvider>
55
-
</AuthProvider>
56
-
</ThemeProvider>
62
+
<SessionProvider>
63
+
<ClientProvider>
64
+
<ThemeProvider defaultTheme="dark" storageKey="recipes-theme">
65
+
<QueryClientProvider client={queryClient}>
66
+
<InnerApp />
67
+
<ReactQueryDevtools initialIsOpen={false} />
68
+
</QueryClientProvider>
69
+
</ThemeProvider>
70
+
</ClientProvider>
71
+
</SessionProvider>
57
72
</StrictMode>,
58
73
)
+9
-11
apps/web/src/queries/recipe.ts
+9
-11
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";
6
4
import { UseFormReturn } from "react-hook-form";
7
5
import { TID } from '@atproto/common-web';
8
-
import { recipeSchema } from "@/forms/recipe";
9
6
import { z } from "zod";
10
7
import { ActorIdentifier, Did } from "@atcute/lexicons";
11
8
12
9
import type {} from '@atcute/atproto';
13
10
import type {} from '@cookware/lexicons';
11
+
import { useClient } from "../state/auth/client";
14
12
15
13
const RQKEY_ROOT = 'posts';
16
14
export const RQKEY = (cursor: string, did: string, rkey: string) => [RQKEY_ROOT, cursor, did, rkey];
17
15
18
16
export const useRecipesQuery = (cursor: string, did?: Did) => {
19
-
const rpc = useXrpc();
17
+
const client = useClient();
20
18
return useQuery({
21
19
queryKey: RQKEY(cursor, did ?? '', ''),
22
20
queryFn: async () => {
23
-
const res = await rpc.get('blue.recipes.feed.getRecipes', {
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
+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
+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>
+289
-320
apps/web/src/routes/_.(app)/recipes/new.tsx
+289
-320
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
-
import { recipeSchema } from "@/forms/recipe";
44
12
45
13
export const Route = createFileRoute("/_/(app)/recipes/new")({
46
14
beforeLoad: async ({ context }) => {
47
-
if (!context.auth.isLoggedIn) {
15
+
if (!context.session.isLoggedIn) {
48
16
throw redirect({
49
17
to: '/login',
50
18
});
···
54
22
});
55
23
56
24
function RouteComponent() {
57
-
const form = useForm<z.infer<typeof recipeSchema>>({
58
-
resolver: zodResolver(recipeSchema),
59
-
defaultValues: {
60
-
title: "",
61
-
time: 0,
62
-
image: null,
63
-
description: "",
64
-
ingredients: [{ name: "" }],
65
-
steps: [{ text: "" }],
66
-
},
67
-
});
68
-
69
-
const { mutate, isPending } = useNewRecipeMutation(form);
70
-
71
-
const onSubmit = (values: z.infer<typeof recipeSchema>) => {
72
-
mutate({ recipe: values });
73
-
};
74
-
75
-
const imageRef = form.register("image");
76
-
77
-
const ingredients = useFieldArray({
78
-
control: form.control,
79
-
name: "ingredients",
80
-
});
81
-
82
-
const steps = useFieldArray({
83
-
control: form.control,
84
-
name: "steps",
85
-
});
86
-
87
-
return (
88
-
<>
89
-
<Breadcrumbs />
90
-
<div className="flex-1 flex-col p-4 pt-0 max-w-xl w-full mx-auto">
91
-
<Card>
92
-
<CardHeader>
93
-
<CardTitle>New recipe</CardTitle>
94
-
<CardDescription>Share your recipe with the world!</CardDescription>
95
-
</CardHeader>
96
-
<CardContent>
97
-
<Form {...form}>
98
-
<form
99
-
onSubmit={form.handleSubmit(onSubmit)}
100
-
className="space-y-8"
101
-
>
102
-
<FormField
103
-
name="title"
104
-
control={form.control}
105
-
render={({ field }) => (
106
-
<FormItem>
107
-
<FormLabel>Title</FormLabel>
108
-
<FormControl>
109
-
<Input placeholder="My awesome recipe!" {...field} />
110
-
</FormControl>
111
-
<FormDescription>
112
-
This is your recipe's name.
113
-
</FormDescription>
114
-
<FormMessage />
115
-
</FormItem>
116
-
)}
117
-
/>
118
-
119
-
<FormField
120
-
name="description"
121
-
control={form.control}
122
-
render={({ field: { value, ...field } }) => (
123
-
<FormItem>
124
-
<FormLabel>Description</FormLabel>
125
-
<FormControl>
126
-
<Textarea
127
-
className="resize-none"
128
-
value={value || ""}
129
-
{...field}
130
-
/>
131
-
</FormControl>
132
-
<FormDescription>Describe your recipe, maybe tell the world how tasty it is? (Optional)</FormDescription>
133
-
<FormMessage />
134
-
</FormItem>
135
-
)}
136
-
/>
137
-
138
-
<FormField
139
-
name="image"
140
-
control={form.control}
141
-
render={(_props) => (
142
-
<FormItem>
143
-
<FormLabel>Image</FormLabel>
144
-
<FormControl>
145
-
<Input
146
-
type="file"
147
-
className="resize-none"
148
-
{...imageRef}
149
-
/>
150
-
</FormControl>
151
-
<FormMessage />
152
-
</FormItem>
153
-
)}
154
-
/>
155
-
156
-
<FormField
157
-
name="time"
158
-
control={form.control}
159
-
render={({ field: { value, ...field } }) => (
160
-
<FormItem>
161
-
<FormLabel>Time</FormLabel>
162
-
<FormControl>
163
-
<Input
164
-
type="number"
165
-
className="resize-none"
166
-
value={value || ""}
167
-
{...field}
168
-
/>
169
-
</FormControl>
170
-
<FormDescription>How long (in minutes) does your recipe take to complete?</FormDescription>
171
-
<FormMessage />
172
-
</FormItem>
173
-
)}
174
-
/>
175
-
176
-
<div className="grid gap-2">
177
-
<Label>Ingredients</Label>
178
-
<Sortable
179
-
value={ingredients.fields}
180
-
onMove={({ activeIndex, overIndex }) =>
181
-
ingredients.move(activeIndex, overIndex)}
182
-
>
183
-
<div className="flex w-full flex-col gap-2">
184
-
{ingredients.fields.map((field, index) => (
185
-
<SortableItem key={field.id} value={field.id} asChild>
186
-
<div className="grid grid-cols-[2rem_0.3fr_1fr_2rem] items-center gap-2">
187
-
<SortableDragHandle
188
-
type="button"
189
-
variant="outline"
190
-
size="icon"
191
-
className="size-8 shrink-0"
192
-
>
193
-
<DragHandleDots2Icon
194
-
className="size-4"
195
-
aria-hidden="true"
196
-
/>
197
-
</SortableDragHandle>
198
-
199
-
<FormField
200
-
control={form.control}
201
-
name={`ingredients.${index}.amount`}
202
-
render={({ field: { value, ...field } }) => (
203
-
<FormItem>
204
-
<FormControl>
205
-
<Input
206
-
placeholder="Amount"
207
-
value={value || ""}
208
-
className="h-8"
209
-
{...field}
210
-
/>
211
-
</FormControl>
212
-
<FormMessage />
213
-
</FormItem>
214
-
)}
215
-
/>
216
-
217
-
<FormField
218
-
control={form.control}
219
-
name={`ingredients.${index}.name`}
220
-
render={({ field }) => (
221
-
<FormItem>
222
-
<FormControl>
223
-
<Input
224
-
placeholder="Ingredient"
225
-
className="h-8"
226
-
{...field}
227
-
/>
228
-
</FormControl>
229
-
<FormMessage />
230
-
</FormItem>
231
-
)}
232
-
/>
233
-
234
-
<Button
235
-
type="button"
236
-
variant="destructive"
237
-
className="size-8"
238
-
onClick={(e) => {
239
-
e.preventDefault();
240
-
ingredients.remove(index);
241
-
}}
242
-
>
243
-
<TrashIcon />
244
-
</Button>
245
-
</div>
246
-
</SortableItem>
247
-
))}
248
-
</div>
249
-
</Sortable>
250
-
<Button
251
-
type="button"
252
-
variant="secondary"
253
-
onClick={(e) => {
254
-
e.preventDefault();
255
-
ingredients.append({
256
-
name: "",
257
-
amount: "",
258
-
});
259
-
}}
260
-
>
261
-
Add
262
-
</Button>
263
-
</div>
264
-
265
-
<div className="grid gap-2">
266
-
<Label>Steps</Label>
267
-
<Sortable
268
-
value={steps.fields}
269
-
onMove={({ activeIndex, overIndex }) =>
270
-
steps.move(activeIndex, overIndex)}
271
-
>
272
-
<div className="flex w-full flex-col gap-2">
273
-
{steps.fields.map((field, index) => (
274
-
<SortableItem key={field.id} value={field.id} asChild>
275
-
<div className="grid grid-cols-[2rem_auto_2rem] items-center gap-2">
276
-
<SortableDragHandle
277
-
type="button"
278
-
variant="outline"
279
-
size="icon"
280
-
className="size-8 shrink-0"
281
-
>
282
-
<DragHandleDots2Icon
283
-
className="size-4"
284
-
aria-hidden="true"
285
-
/>
286
-
</SortableDragHandle>
287
-
<FormField
288
-
control={form.control}
289
-
name={`steps.${index}.text`}
290
-
render={({ field }) => (
291
-
<FormItem>
292
-
<FormControl>
293
-
<Input className="h-8" {...field} />
294
-
</FormControl>
295
-
<FormMessage />
296
-
</FormItem>
297
-
)}
298
-
/>
299
-
300
-
<Button
301
-
type="button"
302
-
variant="destructive"
303
-
className="size-8"
304
-
onClick={(e) => {
305
-
e.preventDefault();
306
-
steps.remove(index);
307
-
}}
308
-
>
309
-
<TrashIcon />
310
-
</Button>
311
-
</div>
312
-
</SortableItem>
313
-
))}
314
-
</div>
315
-
</Sortable>
316
-
<Button
317
-
type="button"
318
-
variant="secondary"
319
-
onClick={(e) => {
320
-
e.preventDefault();
321
-
steps.append({ text: "" });
322
-
}}
323
-
>
324
-
Add
325
-
</Button>
326
-
</div>
327
-
328
-
<div className="grid justify-end">
329
-
<Button
330
-
type="submit"
331
-
className="ml-auto"
332
-
disabled={isPending}
333
-
>
334
-
Submit
335
-
</Button>
336
-
</div>
337
-
</form>
338
-
</Form>
339
-
</CardContent>
340
-
</Card>
341
-
</div>
342
-
</>
343
-
);
25
+
return (<></>);
26
+
// const form = useForm<z.infer<typeof recipeSchema>>({
27
+
// resolver: zodResolver(recipeSchema),
28
+
// defaultValues: {
29
+
// title: "",
30
+
// time: 0,
31
+
// image: null,
32
+
// description: "",
33
+
// ingredients: [{ name: "" }],
34
+
// steps: [{ text: "" }],
35
+
// },
36
+
// });
37
+
//
38
+
// const { mutate, isPending } = useNewRecipeMutation(form);
39
+
//
40
+
// const onSubmit = (values: z.infer<typeof recipeSchema>) => {
41
+
// mutate({ recipe: values });
42
+
// };
43
+
//
44
+
// const imageRef = form.register("image");
45
+
//
46
+
// const ingredients = useFieldArray({
47
+
// control: form.control,
48
+
// name: "ingredients",
49
+
// });
50
+
//
51
+
// const steps = useFieldArray({
52
+
// control: form.control,
53
+
// name: "steps",
54
+
// });
55
+
//
56
+
// return (
57
+
// <>
58
+
// <Breadcrumbs />
59
+
// <div className="flex-1 flex-col p-4 pt-0 max-w-xl w-full mx-auto">
60
+
// <Card>
61
+
// <CardHeader>
62
+
// <CardTitle>New recipe</CardTitle>
63
+
// <CardDescription>Share your recipe with the world!</CardDescription>
64
+
// </CardHeader>
65
+
// <CardContent>
66
+
// <Form {...form}>
67
+
// <form
68
+
// onSubmit={form.handleSubmit(onSubmit)}
69
+
// className="space-y-8"
70
+
// >
71
+
// <FormField
72
+
// name="title"
73
+
// control={form.control}
74
+
// render={({ field }) => (
75
+
// <FormItem>
76
+
// <FormLabel>Title</FormLabel>
77
+
// <FormControl>
78
+
// <Input placeholder="My awesome recipe!" {...field} />
79
+
// </FormControl>
80
+
// <FormDescription>
81
+
// This is your recipe's name.
82
+
// </FormDescription>
83
+
// <FormMessage />
84
+
// </FormItem>
85
+
// )}
86
+
// />
87
+
//
88
+
// <FormField
89
+
// name="description"
90
+
// control={form.control}
91
+
// render={({ field: { value, ...field } }) => (
92
+
// <FormItem>
93
+
// <FormLabel>Description</FormLabel>
94
+
// <FormControl>
95
+
// <Textarea
96
+
// className="resize-none"
97
+
// value={value || ""}
98
+
// {...field}
99
+
// />
100
+
// </FormControl>
101
+
// <FormDescription>Describe your recipe, maybe tell the world how tasty it is? (Optional)</FormDescription>
102
+
// <FormMessage />
103
+
// </FormItem>
104
+
// )}
105
+
// />
106
+
//
107
+
// <FormField
108
+
// name="image"
109
+
// control={form.control}
110
+
// render={(_props) => (
111
+
// <FormItem>
112
+
// <FormLabel>Image</FormLabel>
113
+
// <FormControl>
114
+
// <Input
115
+
// type="file"
116
+
// className="resize-none"
117
+
// {...imageRef}
118
+
// />
119
+
// </FormControl>
120
+
// <FormMessage />
121
+
// </FormItem>
122
+
// )}
123
+
// />
124
+
//
125
+
// <FormField
126
+
// name="time"
127
+
// control={form.control}
128
+
// render={({ field: { value, ...field } }) => (
129
+
// <FormItem>
130
+
// <FormLabel>Time</FormLabel>
131
+
// <FormControl>
132
+
// <Input
133
+
// type="number"
134
+
// className="resize-none"
135
+
// value={value || ""}
136
+
// {...field}
137
+
// />
138
+
// </FormControl>
139
+
// <FormDescription>How long (in minutes) does your recipe take to complete?</FormDescription>
140
+
// <FormMessage />
141
+
// </FormItem>
142
+
// )}
143
+
// />
144
+
//
145
+
// <div className="grid gap-2">
146
+
// <Label>Ingredients</Label>
147
+
// <Sortable
148
+
// value={ingredients.fields}
149
+
// onMove={({ activeIndex, overIndex }) =>
150
+
// ingredients.move(activeIndex, overIndex)}
151
+
// >
152
+
// <div className="flex w-full flex-col gap-2">
153
+
// {ingredients.fields.map((field, index) => (
154
+
// <SortableItem key={field.id} value={field.id} asChild>
155
+
// <div className="grid grid-cols-[2rem_0.3fr_1fr_2rem] items-center gap-2">
156
+
// <SortableDragHandle
157
+
// type="button"
158
+
// variant="outline"
159
+
// size="icon"
160
+
// className="size-8 shrink-0"
161
+
// >
162
+
// <DragHandleDots2Icon
163
+
// className="size-4"
164
+
// aria-hidden="true"
165
+
// />
166
+
// </SortableDragHandle>
167
+
//
168
+
// <FormField
169
+
// control={form.control}
170
+
// name={`ingredients.${index}.amount`}
171
+
// render={({ field: { value, ...field } }) => (
172
+
// <FormItem>
173
+
// <FormControl>
174
+
// <Input
175
+
// placeholder="Amount"
176
+
// value={value || ""}
177
+
// className="h-8"
178
+
// {...field}
179
+
// />
180
+
// </FormControl>
181
+
// <FormMessage />
182
+
// </FormItem>
183
+
// )}
184
+
// />
185
+
//
186
+
// <FormField
187
+
// control={form.control}
188
+
// name={`ingredients.${index}.name`}
189
+
// render={({ field }) => (
190
+
// <FormItem>
191
+
// <FormControl>
192
+
// <Input
193
+
// placeholder="Ingredient"
194
+
// className="h-8"
195
+
// {...field}
196
+
// />
197
+
// </FormControl>
198
+
// <FormMessage />
199
+
// </FormItem>
200
+
// )}
201
+
// />
202
+
//
203
+
// <Button
204
+
// type="button"
205
+
// variant="destructive"
206
+
// className="size-8"
207
+
// onClick={(e) => {
208
+
// e.preventDefault();
209
+
// ingredients.remove(index);
210
+
// }}
211
+
// >
212
+
// <TrashIcon />
213
+
// </Button>
214
+
// </div>
215
+
// </SortableItem>
216
+
// ))}
217
+
// </div>
218
+
// </Sortable>
219
+
// <Button
220
+
// type="button"
221
+
// variant="secondary"
222
+
// onClick={(e) => {
223
+
// e.preventDefault();
224
+
// ingredients.append({
225
+
// name: "",
226
+
// amount: "",
227
+
// });
228
+
// }}
229
+
// >
230
+
// Add
231
+
// </Button>
232
+
// </div>
233
+
//
234
+
// <div className="grid gap-2">
235
+
// <Label>Steps</Label>
236
+
// <Sortable
237
+
// value={steps.fields}
238
+
// onMove={({ activeIndex, overIndex }) =>
239
+
// steps.move(activeIndex, overIndex)}
240
+
// >
241
+
// <div className="flex w-full flex-col gap-2">
242
+
// {steps.fields.map((field, index) => (
243
+
// <SortableItem key={field.id} value={field.id} asChild>
244
+
// <div className="grid grid-cols-[2rem_auto_2rem] items-center gap-2">
245
+
// <SortableDragHandle
246
+
// type="button"
247
+
// variant="outline"
248
+
// size="icon"
249
+
// className="size-8 shrink-0"
250
+
// >
251
+
// <DragHandleDots2Icon
252
+
// className="size-4"
253
+
// aria-hidden="true"
254
+
// />
255
+
// </SortableDragHandle>
256
+
// <FormField
257
+
// control={form.control}
258
+
// name={`steps.${index}.text`}
259
+
// render={({ field }) => (
260
+
// <FormItem>
261
+
// <FormControl>
262
+
// <Input className="h-8" {...field} />
263
+
// </FormControl>
264
+
// <FormMessage />
265
+
// </FormItem>
266
+
// )}
267
+
// />
268
+
//
269
+
// <Button
270
+
// type="button"
271
+
// variant="destructive"
272
+
// className="size-8"
273
+
// onClick={(e) => {
274
+
// e.preventDefault();
275
+
// steps.remove(index);
276
+
// }}
277
+
// >
278
+
// <TrashIcon />
279
+
// </Button>
280
+
// </div>
281
+
// </SortableItem>
282
+
// ))}
283
+
// </div>
284
+
// </Sortable>
285
+
// <Button
286
+
// type="button"
287
+
// variant="secondary"
288
+
// onClick={(e) => {
289
+
// e.preventDefault();
290
+
// steps.append({ text: "" });
291
+
// }}
292
+
// >
293
+
// Add
294
+
// </Button>
295
+
// </div>
296
+
//
297
+
// <div className="grid justify-end">
298
+
// <Button
299
+
// type="submit"
300
+
// className="ml-auto"
301
+
// disabled={isPending}
302
+
// >
303
+
// Submit
304
+
// </Button>
305
+
// </div>
306
+
// </form>
307
+
// </Form>
308
+
// </CardContent>
309
+
// </Card>
310
+
// </div>
311
+
// </>
312
+
// );
344
313
}
345
314
346
315
const Breadcrumbs = () => (
+52
-70
apps/web/src/routes/_.(auth)/login.tsx
+52
-70
apps/web/src/routes/_.(auth)/login.tsx
···
13
13
CardHeader,
14
14
CardTitle,
15
15
} from '@/components/ui/card'
16
+
import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'
16
17
import { Input } from '@/components/ui/input'
17
18
import { Label } from '@/components/ui/label'
18
19
import { Separator } from '@/components/ui/separator'
19
20
import { SidebarTrigger } from '@/components/ui/sidebar'
20
-
import { sleep } from '@/lib/utils'
21
-
import {
22
-
createAuthorizationUrl,
23
-
resolveFromIdentity,
24
-
} from '@atcute/oauth-browser-client'
21
+
import { useSession } from '@/state/auth/session'
25
22
import { useMutation } from '@tanstack/react-query'
26
23
import { createFileRoute } from '@tanstack/react-router'
27
24
import { useState } from 'react'
···
31
28
})
32
29
33
30
function RouteComponent() {
31
+
const { signIn } = useSession();
34
32
const [handle, setHandle] = useState('')
35
33
36
34
const { mutate, isPending, error } = useMutation({
37
35
mutationKey: ['login'],
38
36
mutationFn: async () => {
39
-
const { identity, metadata } = await resolveFromIdentity(handle)
40
-
41
-
const authUrl = await createAuthorizationUrl({
42
-
metadata: metadata,
43
-
identity: identity,
44
-
scope: 'atproto transition:generic',
45
-
})
46
-
47
-
await sleep(200)
48
-
49
-
return authUrl
50
-
},
51
-
onSuccess: async (authUrl: URL) => {
52
-
window.location.assign(authUrl)
53
-
54
-
await new Promise((_resolve, reject) => {
55
-
const listener = () => {
56
-
reject(new Error(`user aborted the login request`))
57
-
}
58
-
59
-
window.addEventListener('pageshow', listener, { once: true })
60
-
})
37
+
await signIn(handle);
38
+
return;
61
39
},
62
40
})
63
41
···
77
55
</div>
78
56
</header>
79
57
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-4 pt-0">
80
-
<Card className="max-w-sm w-full">
81
-
<CardHeader>
82
-
<CardTitle>Log in</CardTitle>
83
-
<CardDescription>
84
-
Enter your handle below to sign in to your account.
85
-
</CardDescription>
86
-
</CardHeader>
87
-
<CardContent>
88
-
<div className="flex flex-col gap-2">
89
-
<Label htmlFor="handle">Handle</Label>
90
-
<Input
91
-
className={`${error ? 'border-destructive text-destructive' : ''}`}
92
-
type="text"
93
-
id="handle"
94
-
name="handle"
95
-
placeholder="johndoe.bsky.social"
96
-
required
97
-
value={handle}
98
-
onChange={(e) => setHandle(e.currentTarget.value)}
99
-
/>
100
-
{error && (
101
-
<p className="text-sm font-medium text-destructive">
102
-
{error.message}
103
-
</p>
104
-
)}
105
-
</div>
106
-
</CardContent>
107
-
<CardFooter className="grid gap-2">
108
-
<Button onClick={() => mutate()} disabled={isPending}>
109
-
Log in
110
-
</Button>
111
-
<p className="text-sm text-muted-foreground text-center">
112
-
Don't have an account?{' '}
113
-
<a
114
-
className="font-bold text-primary"
115
-
href="https://bsky.app/"
116
-
target="_blank"
117
-
>
118
-
Sign up on Bluesky!
119
-
</a>
120
-
</p>
121
-
</CardFooter>
122
-
</Card>
58
+
<form onSubmit={e => {
59
+
e.preventDefault();
60
+
mutate();
61
+
}}>
62
+
<Card className="max-w-sm w-full">
63
+
<CardHeader>
64
+
<CardTitle>Log in</CardTitle>
65
+
<CardDescription>
66
+
Enter your Atmosphere handle below to sign in to your account.
67
+
</CardDescription>
68
+
</CardHeader>
69
+
<CardContent>
70
+
<FieldGroup>
71
+
<Field data-invalid={error ? true : false}>
72
+
<FieldLabel htmlFor="handle">Handle</FieldLabel>
73
+
<Input
74
+
id="handle"
75
+
placeholder="johndoe.bsky.social"
76
+
required
77
+
autoComplete="username"
78
+
aria-invalid={error ? 'true' : 'false'}
79
+
tabIndex={0}
80
+
autoFocus
81
+
value={handle}
82
+
onChange={(e) => setHandle(e.currentTarget.value)}
83
+
/>
84
+
{error && <FieldError>{error.message}</FieldError>}
85
+
</Field>
86
+
</FieldGroup>
87
+
</CardContent>
88
+
<CardFooter className="grid gap-2">
89
+
<Button type="submit" disabled={isPending}>
90
+
Log in
91
+
</Button>
92
+
<p className="text-sm text-muted-foreground text-center">
93
+
Don't have an account?{' '}
94
+
<a
95
+
className="font-bold text-primary"
96
+
href="https://bsky.app/"
97
+
target="_blank"
98
+
>
99
+
Sign up on Bluesky!
100
+
</a>
101
+
</p>
102
+
</CardFooter>
103
+
</Card>
104
+
</form>
123
105
</div>
124
106
</>
125
107
)
+3
-2
apps/web/src/routes/__root.tsx
+3
-2
apps/web/src/routes/__root.tsx
···
3
3
SidebarInset,
4
4
SidebarProvider,
5
5
} from '@/components/ui/sidebar'
6
-
import { AuthContextType } from '@/state/auth';
6
+
import { ClientContext, SessionContext } from '@/state/auth';
7
7
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
8
8
9
9
type RootContext = {
10
-
auth: AuthContextType;
10
+
session: SessionContext;
11
+
client: ClientContext['client'];
11
12
};
12
13
13
14
export const Route = createRootRouteWithContext<RootContext>()({
+8
-8
apps/web/src/screens/Recipes/RecipeCard.tsx
+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>
+38
apps/web/src/state/auth/client.tsx
+38
apps/web/src/state/auth/client.tsx
···
1
+
import { Client, simpleFetchHandler } from "@atcute/client";
2
+
import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react";
3
+
import { useSession } from "./session";
4
+
5
+
export type ClientContext = {
6
+
client: Client;
7
+
};
8
+
9
+
const clientContext = createContext<ClientContext>({
10
+
client: new Client({ handler: simpleFetchHandler({ service: import.meta.env.VITE_API_SERVICE }) }),
11
+
});
12
+
13
+
export const ClientProvider = ({ children }: PropsWithChildren) => {
14
+
const { agent } = useSession();
15
+
const [client, setClient] = useState<Client>(
16
+
() => new Client({ handler: simpleFetchHandler({ service: import.meta.env.VITE_API_SERVICE }) })
17
+
);
18
+
19
+
useEffect(() => {
20
+
setClient(new Client({
21
+
handler: agent ?? simpleFetchHandler({ service: import.meta.env.VITE_API_SERVICE }),
22
+
proxy: {
23
+
did: 'did:web:localhost',
24
+
serviceId: '#api_service'
25
+
},
26
+
}));
27
+
}, [agent]);
28
+
29
+
return (
30
+
<clientContext.Provider value={{ client: client }}>
31
+
{children}
32
+
</clientContext.Provider>
33
+
);
34
+
}
35
+
36
+
export const useClient = () => {
37
+
return useContext(clientContext).client;
38
+
}
+2
apps/web/src/state/auth/index.ts
+2
apps/web/src/state/auth/index.ts
+121
apps/web/src/state/auth/session.tsx
+121
apps/web/src/state/auth/session.tsx
···
1
+
import { isDid, isActorIdentifier } from "@atcute/lexicons/syntax";
2
+
import { createAuthorizationUrl, deleteStoredSession, finalizeAuthorization, getSession, OAuthUserAgent, Session } from "@atcute/oauth-browser-client";
3
+
import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react";
4
+
5
+
export type SessionContext = {
6
+
session: null | Session;
7
+
agent: null | OAuthUserAgent;
8
+
isLoading: boolean;
9
+
isLoggedIn: boolean;
10
+
signIn: (handle: string) => Promise<void>;
11
+
signOut: () => Promise<void>;
12
+
};
13
+
14
+
const sessionContext = createContext<SessionContext>({
15
+
session: null,
16
+
agent: null,
17
+
isLoading: false,
18
+
isLoggedIn: false,
19
+
signIn: async () => {
20
+
throw new Error("AuthContext not initialized");
21
+
},
22
+
signOut: async () => {
23
+
throw new Error("AuthContext not initialized");
24
+
},
25
+
});
26
+
27
+
const LS_LAST_SIGNED_IN = "recipes:last-signed-in";
28
+
29
+
export const SessionProvider = ({ children }: PropsWithChildren<{}>) => {
30
+
const [initialized, setInitialized] = useState(false);
31
+
const [loading, setLoading] = useState(true);
32
+
const [session, setSession] = useState<null | Session>(null);
33
+
const [agent, setAgent] = useState<null | OAuthUserAgent>(null);
34
+
35
+
useEffect(() => {
36
+
setInitialized(false);
37
+
setSession(null);
38
+
setAgent(null);
39
+
40
+
const params = new URLSearchParams(location.hash.slice(1));
41
+
if (params.has("state") && params.has("iss") && params.has("code")) {
42
+
// If there is an active auth attempt:
43
+
history.replaceState(null, "", location.pathname + location.search);
44
+
console.log("finalizing authorization...");
45
+
finalizeAuthorization(params)
46
+
.then(val => {
47
+
setSession(val.session);
48
+
setAgent(new OAuthUserAgent(val.session));
49
+
})
50
+
.catch(err => {
51
+
console.error("Failed to initialize session:", err);
52
+
})
53
+
.finally(() => {
54
+
setLoading(false);
55
+
setInitialized(true);
56
+
});
57
+
} else {
58
+
const lastSignedIn = localStorage.getItem(LS_LAST_SIGNED_IN);
59
+
if (lastSignedIn && isDid(lastSignedIn)) {
60
+
getSession(lastSignedIn, { allowStale: true })
61
+
.then((session) => {
62
+
setSession(session);
63
+
setAgent(new OAuthUserAgent(session));
64
+
})
65
+
.catch(err => {
66
+
console.error("Failed to initialize session:", err);
67
+
})
68
+
}
69
+
70
+
setLoading(false);
71
+
setInitialized(true);
72
+
}
73
+
}, []);
74
+
75
+
const signIn = async (handle: string) => {
76
+
if (!isActorIdentifier(handle)) throw new Error("Invalid handle or DID!");
77
+
const authUrl = await createAuthorizationUrl({
78
+
target: { type: 'account', identifier: handle },
79
+
scope: 'atproto transition:generic',
80
+
});
81
+
window.location.assign(authUrl);
82
+
};
83
+
84
+
const signOut = async () => {
85
+
if (!agent || !session) return;
86
+
87
+
const did = session.info.sub;
88
+
try {
89
+
const session = await getSession(did, { allowStale: true });
90
+
const agent = new OAuthUserAgent(session);
91
+
92
+
await agent.signOut();
93
+
setSession(null);
94
+
} catch(err) {
95
+
deleteStoredSession(did);
96
+
}
97
+
};
98
+
99
+
if (!initialized) return (
100
+
<p>Loading...</p>
101
+
);
102
+
103
+
return (
104
+
<sessionContext.Provider value={{
105
+
isLoading: loading,
106
+
isLoggedIn: session !== null,
107
+
session,
108
+
agent,
109
+
signIn,
110
+
signOut,
111
+
}}>
112
+
{children}
113
+
</sessionContext.Provider>
114
+
);
115
+
};
116
+
117
+
export const useSession = () => {
118
+
const ctx = useContext(sessionContext);
119
+
if (!ctx) throw new Error("useSession() must be called inside a <SessionProvider />!");
120
+
return ctx;
121
+
};
-78
apps/web/src/state/auth.tsx
-78
apps/web/src/state/auth.tsx
···
1
-
import { Did } from "@atcute/lexicons";
2
-
import { finalizeAuthorization, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client";
3
-
import { createContext, PropsWithChildren, useContext, useEffect, useState } from "react";
4
-
5
-
export type AuthContextType = {
6
-
isLoggedIn: boolean;
7
-
agent?: OAuthUserAgent;
8
-
logOut: () => Promise<void>;
9
-
};
10
-
11
-
const AuthContext = createContext<AuthContextType>({
12
-
isLoggedIn: false,
13
-
logOut: async () => {},
14
-
});
15
-
16
-
export const AuthProvider = ({ children }: PropsWithChildren) => {
17
-
const [isReady, setIsReady] = useState(false);
18
-
const [isLoggedIn, setIsLoggedIn] = useState(false);
19
-
const [agent, setAgent] = useState<OAuthUserAgent | undefined>(undefined);
20
-
21
-
useEffect(() => {
22
-
const init = async () => {
23
-
const params = new URLSearchParams(location.hash.slice(1));
24
-
25
-
if (params.has("state") && (params.has("code") || params.has("error"))) {
26
-
history.replaceState(null, "", location.pathname + location.search);
27
-
28
-
const session = await finalizeAuthorization(params);
29
-
const did = session.info.sub;
30
-
31
-
localStorage.setItem("lastSignedIn", did);
32
-
return session;
33
-
34
-
} else {
35
-
const lastSignedIn = localStorage.getItem("lastSignedIn");
36
-
37
-
if (lastSignedIn) {
38
-
try {
39
-
return await getSession(lastSignedIn as Did);
40
-
} catch (err) {
41
-
localStorage.removeItem("lastSignedIn");
42
-
throw err;
43
-
}
44
-
}
45
-
}
46
-
};
47
-
48
-
init()
49
-
.then(session => {
50
-
if (session) {
51
-
setAgent(new OAuthUserAgent(session));
52
-
setIsLoggedIn(true);
53
-
}
54
-
55
-
setIsReady(true)
56
-
})
57
-
.catch(() => {});
58
-
}, []);
59
-
60
-
if (!isReady) return null;
61
-
62
-
return (
63
-
<AuthContext.Provider value={{
64
-
isLoggedIn,
65
-
agent,
66
-
logOut: async () => {
67
-
setIsLoggedIn(false);
68
-
await agent?.signOut();
69
-
},
70
-
}}>
71
-
{children}
72
-
</AuthContext.Provider>
73
-
);
74
-
};
75
-
76
-
export const useAuth = () => {
77
-
return useContext(AuthContext);
78
-
};
+1
-3
apps/web/src/vite-env.d.ts
+1
-3
apps/web/src/vite-env.d.ts
···
1
1
/// <reference types="vite/client" />
2
-
/// <reference types="@cookware/lexicons" />
3
-
/// <reference types="@atcute/bluesky/lexicons" />
4
2
5
3
interface ImportMetaEnv {
6
4
readonly VITE_API_SERVICE: string;
7
5
readonly VITE_DEV_SERVER_PORT?: string;
8
6
readonly VITE_CLIENT_URI: string;
9
7
readonly VITE_OAUTH_CLIENT_ID: string;
10
-
readonly VITE_OAUTH_REDIRECT_URL: string;
8
+
readonly VITE_OAUTH_REDIRECT_URI: string;
11
9
readonly VITE_OAUTH_SCOPE: string;
12
10
}
13
11
+2
-2
apps/web/tailwind.config.js
+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: {
+27
-28
apps/web/vite.config.ts
+27
-28
apps/web/vite.config.ts
···
1
1
import { defineConfig } from 'vite'
2
-
import react from '@vitejs/plugin-react-swc'
2
+
import react from '@vitejs/plugin-react'
3
3
import { tanstackRouter } from '@tanstack/router-plugin/vite'
4
4
import path from 'path'
5
-
import metadata from "./public/client-metadata.json";
5
+
import metadata from "./public/oauth-client-metadata.json" with { type: 'json' };
6
6
7
7
const SERVER_HOST = "127.0.0.1";
8
8
const SERVER_PORT = 5173;
···
11
11
export default defineConfig({
12
12
plugins: [
13
13
tanstackRouter(),
14
-
react(),
14
+
react({ babel: { plugins: ['babel-plugin-react-compiler'] } }),
15
+
{
16
+
name: '_config',
17
+
config(_conf, { command }) {
18
+
if (command === 'build') {
19
+
process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id;
20
+
process.env.VITE_OAUTH_REDIRECT_URI = metadata.redirect_uris[0];
21
+
} else {
22
+
const redirectUri = (() => {
23
+
const url = new URL(metadata.redirect_uris[0]);
24
+
return `http://${SERVER_HOST}:${SERVER_PORT}${url.pathname}`;
25
+
})();
15
26
16
-
{
17
-
name: "oauth",
18
-
config(_conf, { command }) {
19
-
if (command === "build") {
20
-
process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id;
21
-
process.env.VITE_OAUTH_REDIRECT_URL = metadata.redirect_uris[0];
22
-
} else {
23
-
const redirectUri = ((): string => {
24
-
const url = new URL(metadata.redirect_uris[0]);
25
-
return `https://local.recipes.blue${url.pathname}`;
26
-
})();
27
+
const clientId =
28
+
`http://localhost` +
29
+
`?redirect_uri=${encodeURIComponent(redirectUri)}` +
30
+
`&scope=${encodeURIComponent(metadata.scope)}`;
27
31
28
-
const clientId =
29
-
`https://local.recipes.blue` +
30
-
`?redirect_uri=${encodeURIComponent(redirectUri)}` +
31
-
`&scope=${encodeURIComponent(metadata.scope)}`;
32
+
process.env.VITE_API_SERVICE = 'http://localhost:3000';
33
+
process.env.VITE_DEV_SERVER_PORT = '' + SERVER_PORT;
34
+
process.env.VITE_OAUTH_CLIENT_ID = clientId;
35
+
process.env.VITE_OAUTH_REDIRECT_URI = redirectUri;
36
+
}
32
37
33
-
process.env.VITE_DEV_SERVER_PORT = "" + SERVER_PORT;
34
-
process.env.VITE_OAUTH_CLIENT_ID = clientId;
35
-
process.env.VITE_OAUTH_REDIRECT_URL = redirectUri;
36
-
}
37
-
38
-
process.env.VITE_CLIENT_URI = metadata.client_uri;
39
-
process.env.VITE_OAUTH_SCOPE = metadata.scope;
40
-
},
41
-
},
38
+
process.env.VITE_CLIENT_URI = metadata.client_uri;
39
+
process.env.VITE_OAUTH_SCOPE = metadata.scope;
40
+
},
41
+
},
42
42
],
43
43
server: {
44
-
allowedHosts: ["local.recipes.blue"],
45
44
host: SERVER_HOST,
46
45
port: SERVER_PORT,
47
46
},
+54
-181
bun.lock
+54
-181
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
},
···
61
63
"dependencies": {
62
64
"@atcute/atproto": "^3.1.9",
63
65
"@atcute/client": "catalog:",
66
+
"@atcute/identity-resolver": "^1.1.4",
64
67
"@atcute/lexicons": "catalog:",
65
-
"@atcute/oauth-browser-client": "^1.0.7",
68
+
"@atcute/oauth-browser-client": "^2.0.1",
66
69
"@atproto/common": "^0.4.5",
67
70
"@atproto/common-web": "^0.3.1",
68
71
"@dnd-kit/core": "^6.3.1",
···
75
78
"@radix-ui/react-dialog": "^1.1.4",
76
79
"@radix-ui/react-dropdown-menu": "^2.1.4",
77
80
"@radix-ui/react-icons": "^1.3.2",
78
-
"@radix-ui/react-label": "^2.1.0",
79
-
"@radix-ui/react-separator": "^1.1.0",
80
-
"@radix-ui/react-slot": "^1.1.0",
81
+
"@radix-ui/react-label": "^2.1.8",
82
+
"@radix-ui/react-separator": "^1.1.8",
83
+
"@radix-ui/react-slot": "^1.2.4",
81
84
"@radix-ui/react-tooltip": "^1.1.4",
82
85
"@tanstack/react-query": "^5.62.2",
83
86
"@tanstack/react-query-devtools": "^5.62.2",
···
102
105
"@types/node": "^22.10.1",
103
106
"@types/react": "^19.0.0",
104
107
"@types/react-dom": "^19.0.0",
108
+
"@vitejs/plugin-react": "^5.1.1",
105
109
"@vitejs/plugin-react-swc": "^3.5.0",
106
110
"autoprefixer": "^10.4.20",
111
+
"babel-plugin-react-compiler": "^1.0.0",
107
112
"cssnano": "^7.0.6",
108
113
"eslint": "^9.15.0",
109
114
"eslint-plugin-react-hooks": "^5.0.0",
···
114
119
"tailwindcss": "^3.4.16",
115
120
"typescript": "~5.6.2",
116
121
"typescript-eslint": "^8.15.0",
117
-
"vite": "^6.0.1",
122
+
"vite": "^7.2.4",
118
123
},
119
124
},
120
125
"libs/database": {
···
123
128
"dependencies": {
124
129
"@libsql/client": "^0.15.15",
125
130
"drizzle-orm": "catalog:",
131
+
"pg": "^8.16.3",
126
132
"zod": "^3.23.8",
127
133
},
128
134
"devDependencies": {
···
132
138
"@cookware/tsconfig": "workspace:*",
133
139
"@types/bun": "catalog:",
134
140
"@types/node": "^22.10.1",
141
+
"@types/pg": "^8.15.6",
135
142
"drizzle-kit": "^0.29.0",
136
143
"typescript": "^5.2.2",
137
144
},
···
165
172
"@atcute/atproto": "^3.1.9",
166
173
"@atcute/bluesky": "^3.2.11",
167
174
"@atcute/client": "^4.0.5",
168
-
"@atcute/lexicons": "^1.2.4",
175
+
"@atcute/lexicons": "^1.2.5",
169
176
"@types/bun": "^1.3.3",
170
177
"drizzle-orm": "^0.44.7",
171
178
},
···
174
181
175
182
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
176
183
177
-
"@atcute/bluesky": ["@atcute/bluesky@3.2.10", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.2" } }, "sha512-qwQWTzRf3umnh2u41gdU+xWYkbzGlKDupc3zeOB+YjmuP1N9wEaUhwS8H7vgrqr0xC9SGNDjeUVcjC4m5BPLBg=="],
184
+
"@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="],
178
185
179
186
"@atcute/cbor": ["@atcute/cbor@2.2.8", "", { "dependencies": { "@atcute/cid": "^2.2.6", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5" } }, "sha512-UzOAN9BuN6JCXgn0ryV8qZuRJUDrNqrbLd6EFM8jc6RYssjRyGRxNy6RZ1NU/07Hd8Tq/0pz8+nQiMu5Zai5uw=="],
180
187
···
192
199
193
200
"@atcute/lex-cli": ["@atcute/lex-cli@2.3.3", "", { "dependencies": { "@atcute/lexicon-doc": "^2.0.0", "@badrap/valita": "^0.4.6", "@optique/core": "^0.6.2", "@optique/run": "^0.6.2", "picocolors": "^1.1.1", "prettier": "^3.6.2" }, "bin": { "lex-cli": "cli.mjs" } }, "sha512-L+fxfJWVBFiuS53yK2aDg7F3R0Ak7UOj9BOM2khofR6yBSKHrkCuk3b6GrkbcMKo+9O0ynaLsuXxUNlHvw/m3w=="],
194
201
195
-
"@atcute/lexicon-doc": ["@atcute/lexicon-doc@2.0.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-yWgcBYkvifczVODZSgdVkIljzIfdh50t+QXjkDL/FSu2RP43NGBEZ5xfZqJcT68/UoyE+doSg0dhvOEIlVGU/A=="],
202
+
"@atcute/lexicon-doc": ["@atcute/lexicon-doc@2.0.4", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.5", "@badrap/valita": "^0.4.6" } }, "sha512-YfwlYFoYiBvRIYG0I1zsINCTFugFtS8l67uT3nQ04zdKVflzdg8uUj8cNZYRNY1V7okoOPdikhR4kPFhYGyemw=="],
196
203
197
204
"@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="],
198
205
199
206
"@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="],
200
207
201
-
"@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@1.0.27", "", { "dependencies": { "@atcute/client": "^4.0.4", "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "nanoid": "^5.1.5" } }, "sha512-Ng1tCOTMLgFHHoIHXTtCZR1/ND62an1qxPX2kBoUzkxxd7iCP7IBYYqOiKyJYT5n1R4zS+s29hFS4t9mxXa5kQ=="],
208
+
"@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@2.0.1", "", { "dependencies": { "@atcute/client": "^4.0.5", "@atcute/identity": "^1.1.1", "@atcute/identity-resolver": "^1.1.4", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "nanoid": "^5.1.5" } }, "sha512-lG021GkeORG06zfFf4bH85egObjBEKHNgAWHvbtY/E2dX4wxo88hf370pJDx8acdnuUJLJ2VKPikJtZwo4Heeg=="],
202
209
203
210
"@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="],
204
211
···
293
300
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
294
301
295
302
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.27.1", "", { "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw=="],
303
+
304
+
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
305
+
306
+
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
296
307
297
308
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.5", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA=="],
298
309
···
672
683
673
684
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
674
685
675
-
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
686
+
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="],
676
687
677
688
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
678
689
···
790
801
791
802
"@typelex/emitter": ["@typelex/emitter@0.4.0", "", { "dependencies": { "@typespec/compiler": "^1.4.0" } }, "sha512-BaKny+8TA0yX5jZibkAodHHKLJ6l6xVe5ut7KeoUyTD63lSSuB9OXe8tWXrs2DbeR/hialCimHFZQ3xANleMow=="],
792
803
804
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
805
+
806
+
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
807
+
808
+
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
809
+
810
+
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
811
+
793
812
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
794
813
795
814
"@types/connect": ["@types/connect@3.4.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w=="],
···
802
821
803
822
"@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
804
823
805
-
"@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=="],
806
825
807
826
"@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="],
808
827
···
838
857
839
858
"@typespec/compiler": ["@typespec/compiler@1.6.0", "", { "dependencies": { "@babel/code-frame": "~7.27.1", "@inquirer/prompts": "^7.4.0", "ajv": "~8.17.1", "change-case": "~5.4.4", "env-paths": "^3.0.0", "globby": "~15.0.0", "is-unicode-supported": "^2.1.0", "mustache": "~4.2.0", "picocolors": "~1.1.1", "prettier": "~3.6.2", "semver": "^7.7.1", "tar": "^7.5.2", "temporal-polyfill": "^0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.12", "yaml": "~2.8.0", "yargs": "~18.0.0" }, "bin": { "tsp": "cmd/tsp.js", "tsp-server": "cmd/tsp-server.js" } }, "sha512-yxyV+ch8tnqiuU2gClv/mQEESoFwpkjo6177UkYfV0nVA9PzTg4zVVc7+WIMZk04wiLRRT3H1uc11FB1cwLY3g=="],
840
859
860
+
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="],
861
+
841
862
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.11.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.27", "@swc/core": "^1.12.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w=="],
842
863
843
864
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
···
877
898
"axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="],
878
899
879
900
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.10", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA=="],
901
+
902
+
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
880
903
881
904
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
882
905
···
1152
1175
1153
1176
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
1154
1177
1178
+
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
1179
+
1155
1180
"iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
1156
1181
1157
1182
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
···
1314
1339
1315
1340
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
1316
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
+
1317
1348
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
1349
+
1350
+
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
1318
1351
1319
1352
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
1320
1353
1321
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=="],
1322
1357
1323
1358
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
1324
1359
···
1437
1472
"react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="],
1438
1473
1439
1474
"react-hook-form": ["react-hook-form@7.66.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA=="],
1475
+
1476
+
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
1440
1477
1441
1478
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
1442
1479
···
1608
1645
1609
1646
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
1610
1647
1611
-
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
1648
+
"vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
1612
1649
1613
1650
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
1614
1651
···
1652
1689
1653
1690
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
1654
1691
1655
-
"@atcute/atproto/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1656
-
1657
-
"@atcute/bluesky/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1658
-
1659
-
"@atcute/client/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1660
-
1661
-
"@atcute/identity/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1662
-
1663
-
"@atcute/identity-resolver/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1664
-
1665
-
"@atcute/jetstream/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1666
-
1667
-
"@atcute/lexicon-doc/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1668
-
1669
-
"@atcute/oauth-browser-client/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1670
-
1671
-
"@atcute/xrpc-server/@atcute/lexicons": ["@atcute/lexicons@1.2.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-s6fl/SVjQMv7jiitLCcZ434X+VrTsJt7Fl9iJg8WXHJIELRz/U0sNUoP++oWd7bvPy1Vcd2Wnm+YtTm/Zn7AIQ=="],
1672
-
1673
1692
"@atproto-labs/simple-store-memory/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
1674
1693
1675
1694
"@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.3", "", { "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.23.8" } }, "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg=="],
···
1687
1706
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1688
1707
1689
1708
"@cookware/database/@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="],
1690
-
1691
-
"@cookware/lexicons/@atcute/bluesky": ["@atcute/bluesky@3.2.11", "", { "dependencies": { "@atcute/atproto": "^3.1.9", "@atcute/lexicons": "^1.2.5" } }, "sha512-AboS6y4t+zaxIq7E4noue10csSpIuk/Uwo30/l6GgGBDPXrd7STw8Yb5nGZQP+TdG/uC8/c2mm7UnY65SDOh6A=="],
1692
1709
1693
1710
"@cookware/web/@atcute/bluesky": ["@atcute/bluesky@1.0.15", "", { "peerDependencies": { "@atcute/client": "^1.0.0 || ^2.0.0" } }, "sha512-+EFiybmKQ97aBAgtaD+cKRJER5AMn3cZMkEwEg/pDdWyzxYJ9m1UgemmLdTgI8VrxPufKqdXS2nl7uO7TY6BPA=="],
1694
1711
···
1713
1730
"@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1714
1731
1715
1732
"@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="],
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=="],
1716
1735
1717
1736
"@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
1718
1737
···
1770
1789
1771
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=="],
1772
1791
1773
-
"@tanstack/react-router-devtools/vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
1774
-
1775
-
"@tanstack/router-devtools/vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
1776
-
1777
-
"@tanstack/router-devtools-core/vite": ["vite@7.2.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w=="],
1792
+
"@types/pg-pool/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="],
1778
1793
1779
1794
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
1780
1795
1781
1796
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1782
1797
1798
+
"@vitejs/plugin-react-swc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
1799
+
1783
1800
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
1784
1801
1785
1802
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
···
1918
1935
1919
1936
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@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=="],
1920
1937
1921
-
"@tanstack/react-router-devtools/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
1922
-
1923
-
"@tanstack/router-devtools-core/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
1924
-
1925
-
"@tanstack/router-devtools/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
1926
-
1927
1938
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
1928
1939
1929
1940
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
···
2039
2050
"@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
2040
2051
2041
2052
"@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
2042
-
2043
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
2044
-
2045
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
2046
-
2047
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
2048
-
2049
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
2050
-
2051
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
2052
-
2053
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
2054
-
2055
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
2056
-
2057
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
2058
-
2059
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
2060
-
2061
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
2062
-
2063
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
2064
-
2065
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
2066
-
2067
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
2068
-
2069
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
2070
-
2071
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
2072
-
2073
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
2074
-
2075
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
2076
-
2077
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
2078
-
2079
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
2080
-
2081
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
2082
-
2083
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
2084
-
2085
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
2086
-
2087
-
"@tanstack/react-router-devtools/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
2088
-
2089
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
2090
-
2091
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
2092
-
2093
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
2094
-
2095
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
2096
-
2097
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
2098
-
2099
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
2100
-
2101
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
2102
-
2103
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
2104
-
2105
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
2106
-
2107
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
2108
-
2109
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
2110
-
2111
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
2112
-
2113
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
2114
-
2115
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
2116
-
2117
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
2118
-
2119
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
2120
-
2121
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
2122
-
2123
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
2124
-
2125
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
2126
-
2127
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
2128
-
2129
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
2130
-
2131
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
2132
-
2133
-
"@tanstack/router-devtools-core/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
2134
-
2135
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
2136
-
2137
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
2138
-
2139
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
2140
-
2141
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
2142
-
2143
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
2144
-
2145
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
2146
-
2147
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
2148
-
2149
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
2150
-
2151
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
2152
-
2153
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
2154
-
2155
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
2156
-
2157
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
2158
-
2159
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
2160
-
2161
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
2162
-
2163
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
2164
-
2165
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
2166
-
2167
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
2168
-
2169
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
2170
-
2171
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
2172
-
2173
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
2174
-
2175
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
2176
-
2177
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
2178
-
2179
-
"@tanstack/router-devtools/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
2180
2053
}
2181
2054
}
+13
config/dev/db/compose.yaml
+13
config/dev/db/compose.yaml
-13
config/dev/libsql/compose.yaml
-13
config/dev/libsql/compose.yaml
+12
config/dev/redis/compose.yaml
+12
config/dev/redis/compose.yaml
+2
-32
docker-compose.yaml
+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
+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
+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
+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
-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
+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
-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
-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
-4
libs/database/migrations/0003_long_blue_marvel.sql
+255
-46
libs/database/migrations/meta/0000_snapshot.json
+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
-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
-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
-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
+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
+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
+2
libs/lexicons/lexicons/feed/defs.tsp
+23
-2
libs/lexicons/lexicons/profiles/defs.tsp
+23
-2
libs/lexicons/lexicons/profiles/defs.tsp
···
10
10
displayName?: string;
11
11
12
12
pronouns?: string;
13
+
avatar?: url;
14
+
15
+
@format("datetime")
16
+
createdAt?: string;
17
+
}
18
+
19
+
model ProfileViewDetailed {
20
+
@required did: did;
21
+
@required handle: handle;
22
+
23
+
@maxGraphemes(64)
24
+
@maxLength(640)
25
+
displayName?: string;
13
26
14
-
/** Small image to be displayed on the profile. */
15
-
avatar?: Blob<#["image/png", "image/jpeg"], 1000000>; // 1mb image
27
+
@maxGraphemes(256)
28
+
@maxLength(2500)
29
+
description?: string;
30
+
31
+
pronouns?: string;
32
+
website?: url;
33
+
avatar?: url;
34
+
banner?: url;
35
+
36
+
recipesCount?: integer;
16
37
17
38
@format("datetime")
18
39
createdAt?: string;
+9
libs/lexicons/lexicons/profiles/getProfile.tsp
+9
libs/lexicons/lexicons/profiles/getProfile.tsp
+1
libs/lexicons/lexicons/profiles/main.tsp
+1
libs/lexicons/lexicons/profiles/main.tsp
+1
-2
libs/lexicons/lexicons/profiles/profile.tsp
+1
-2
libs/lexicons/lexicons/profiles/profile.tsp
+1
libs/lexicons/lib/index.ts
+1
libs/lexicons/lib/index.ts
···
1
1
export * as BlueRecipesActorDefs from "./types/blue/recipes/actor/defs.js";
2
+
export * as BlueRecipesActorGetProfile from "./types/blue/recipes/actor/getProfile.js";
2
3
export * as BlueRecipesActorProfile from "./types/blue/recipes/actor/profile.js";
3
4
export * as BlueRecipesFeedDefs from "./types/blue/recipes/feed/defs.js";
4
5
export * as BlueRecipesFeedGetRecipe from "./types/blue/recipes/feed/getRecipe.js";
+39
-4
libs/lexicons/lib/types/blue/recipes/actor/defs.ts
+39
-4
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.string()),
9
+
createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
10
+
did: /*#__PURE__*/ v.didString(),
8
11
/**
9
-
* Small image to be displayed on the profile.
10
-
* @accept image/png, image/jpeg
11
-
* @maxSize 1000000
12
+
* @maxLength 640
13
+
* @maxGraphemes 64
12
14
*/
13
-
avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()),
15
+
displayName: /*#__PURE__*/ v.optional(
16
+
/*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [
17
+
/*#__PURE__*/ v.stringLength(0, 640),
18
+
/*#__PURE__*/ v.stringGraphemes(0, 64),
19
+
]),
20
+
),
21
+
handle: /*#__PURE__*/ v.handleString(),
22
+
pronouns: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
23
+
});
24
+
const _profileViewDetailedSchema = /*#__PURE__*/ v.object({
25
+
$type: /*#__PURE__*/ v.optional(
26
+
/*#__PURE__*/ v.literal("blue.recipes.actor.defs#profileViewDetailed"),
27
+
),
28
+
avatar: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
29
+
banner: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
14
30
createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
31
+
/**
32
+
* @maxLength 2500
33
+
* @maxGraphemes 256
34
+
*/
35
+
description: /*#__PURE__*/ v.optional(
36
+
/*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [
37
+
/*#__PURE__*/ v.stringLength(0, 2500),
38
+
/*#__PURE__*/ v.stringGraphemes(0, 256),
39
+
]),
40
+
),
15
41
did: /*#__PURE__*/ v.didString(),
16
42
/**
17
43
* @maxLength 640
···
25
51
),
26
52
handle: /*#__PURE__*/ v.handleString(),
27
53
pronouns: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
54
+
recipesCount: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.integer()),
55
+
website: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
28
56
});
29
57
30
58
type profileViewBasic$schematype = typeof _profileViewBasicSchema;
59
+
type profileViewDetailed$schematype = typeof _profileViewDetailedSchema;
31
60
32
61
export interface profileViewBasicSchema extends profileViewBasic$schematype {}
62
+
export interface profileViewDetailedSchema
63
+
extends profileViewDetailed$schematype {}
33
64
34
65
export const profileViewBasicSchema =
35
66
_profileViewBasicSchema as profileViewBasicSchema;
67
+
export const profileViewDetailedSchema =
68
+
_profileViewDetailedSchema as profileViewDetailedSchema;
36
69
37
70
export interface ProfileViewBasic
38
71
extends v.InferInput<typeof profileViewBasicSchema> {}
72
+
export interface ProfileViewDetailed
73
+
extends v.InferInput<typeof profileViewDetailedSchema> {}
+31
libs/lexicons/lib/types/blue/recipes/actor/getProfile.ts
+31
libs/lexicons/lib/types/blue/recipes/actor/getProfile.ts
···
1
+
import type {} from "@atcute/lexicons";
2
+
import * as v from "@atcute/lexicons/validations";
3
+
import type {} from "@atcute/lexicons/ambient";
4
+
import * as BlueRecipesActorDefs from "./defs.js";
5
+
6
+
const _mainSchema = /*#__PURE__*/ v.query("blue.recipes.actor.getProfile", {
7
+
params: /*#__PURE__*/ v.object({
8
+
actor: /*#__PURE__*/ v.actorIdentifierString(),
9
+
}),
10
+
output: {
11
+
type: "lex",
12
+
get schema() {
13
+
return BlueRecipesActorDefs.profileViewDetailedSchema;
14
+
},
15
+
},
16
+
});
17
+
18
+
type main$schematype = typeof _mainSchema;
19
+
20
+
export interface mainSchema extends main$schematype {}
21
+
22
+
export const mainSchema = _mainSchema as mainSchema;
23
+
24
+
export interface $params extends v.InferInput<mainSchema["params"]> {}
25
+
export type $output = v.InferXRPCBodyInput<mainSchema["output"]>;
26
+
27
+
declare module "@atcute/lexicons/ambient" {
28
+
interface XRPCQueries {
29
+
"blue.recipes.actor.getProfile": mainSchema;
30
+
}
31
+
}
+1
-1
libs/lexicons/lib/types/blue/recipes/actor/profile.ts
+1
-1
libs/lexicons/lib/types/blue/recipes/actor/profile.ts
+2
libs/lexicons/lib/types/blue/recipes/feed/defs.ts
+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